Skip to main content

context_bar_core/
state_writer.rs

1use std::{
2    fs::{self, File},
3    io::Write,
4    path::{Path, PathBuf},
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use crate::agent_context;
9use crate::context_engine::{ContextSnapshot, render_window_markdown};
10use crate::hud;
11
12#[cfg(target_arch = "wasm32")]
13use zed_extension_api::serde_json;
14
15#[derive(Clone, Debug)]
16pub struct StateWriteResult {
17    pub state_path: PathBuf,
18    pub now_brief_path: PathBuf,
19    pub session_brief_path: PathBuf,
20    pub week_brief_path: PathBuf,
21    pub agent_brief_path: PathBuf,
22    pub claude_brief_path: PathBuf,
23    pub hud_path: PathBuf,
24}
25
26/// Write all artifacts. Each file is staged to a sibling `*.tmp.<pid>.<nanos>`
27/// path, fsynced, then renamed into place, so an agent reading
28/// `.context-bar/AGENT.md` (or any brief) never observes a truncated mid-write
29/// file and the renamed bytes are durable on disk. Renames on the same
30/// filesystem are atomic on POSIX and Windows ReplaceFileW.
31pub fn write(root: &Path, snapshot: &ContextSnapshot) -> Result<StateWriteResult, String> {
32    let state_dir = root.join(".context-bar");
33    fs::create_dir_all(&state_dir)
34        .map_err(|error| format!("failed to create {}: {error}", state_dir.display()))?;
35
36    let state_path = state_dir.join("state.json");
37    let now_brief_path = state_dir.join("brief-now.md");
38    let session_brief_path = state_dir.join("brief-session.md");
39    let week_brief_path = state_dir.join("brief-week.md");
40    let agent_brief_path = state_dir.join("AGENT.md");
41    let claude_brief_path = root.join("CLAUDE.md");
42    let hud_path = state_dir.join("hud.md");
43
44    let json = serde_json::to_string_pretty(snapshot)
45        .map_err(|error| format!("failed to serialize state.json: {error}"))?;
46
47    // Render agent context once; same bytes go to AGENT.md and CLAUDE.md.
48    let agent_md = agent_context::render(snapshot);
49
50    atomic_write(&state_path, json.as_bytes())?;
51    atomic_write(
52        &now_brief_path,
53        render_window_markdown(snapshot, "now").as_bytes(),
54    )?;
55    atomic_write(
56        &session_brief_path,
57        render_window_markdown(snapshot, "session").as_bytes(),
58    )?;
59    atomic_write(
60        &week_brief_path,
61        render_window_markdown(snapshot, "week").as_bytes(),
62    )?;
63    atomic_write(&agent_brief_path, agent_md.as_bytes())?;
64    atomic_write(&claude_brief_path, agent_md.as_bytes())?;
65    atomic_write(&hud_path, hud::render(snapshot, &snapshot.usage).as_bytes())?;
66
67    Ok(StateWriteResult {
68        state_path,
69        now_brief_path,
70        session_brief_path,
71        week_brief_path,
72        agent_brief_path,
73        claude_brief_path,
74        hud_path,
75    })
76}
77
78/// Crash-safe write: write to a unique tmp sibling, fsync, then rename.
79/// Public to crate so other modules (e.g. claude_statusline) share the same
80/// durability guarantees.
81pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
82    let nanos = SystemTime::now()
83        .duration_since(UNIX_EPOCH)
84        .unwrap_or_default()
85        .as_nanos();
86    let pid = std::process::id();
87    let orig_ext = path
88        .extension()
89        .and_then(|e| e.to_str())
90        .unwrap_or("");
91    let suffix = if orig_ext.is_empty() {
92        format!("tmp.{pid}.{nanos}")
93    } else {
94        format!("{orig_ext}.tmp.{pid}.{nanos}")
95    };
96    let tmp = path.with_extension(suffix);
97
98    {
99        let mut f = File::create(&tmp)
100            .map_err(|error| format!("failed to create {}: {error}", tmp.display()))?;
101        f.write_all(bytes)
102            .map_err(|error| format!("failed to write {}: {error}", tmp.display()))?;
103        // fsync so the renamed file is durable, not just visible.
104        f.sync_all()
105            .map_err(|error| format!("failed to fsync {}: {error}", tmp.display()))?;
106    }
107
108    fs::rename(&tmp, path).map_err(|error| {
109        let _ = fs::remove_file(&tmp);
110        format!("failed to rename {} -> {}: {error}", tmp.display(), path.display())
111    })
112}