Skip to main content

context_bar_core/
agent_context.rs

1//! Combined, agent-readable brief.
2//!
3//! Produces `.context-bar/AGENT.md` — a compact briefing any coding agent
4//! (Codex ACP, Claude Code, future MCP bridge) can read on demand. Format
5//! is stable so a future bridge can ship without redesigning the engine.
6//!
7//! Uncertainty: Zed Preview ACP threads do not currently surface extension
8//! slash commands, and no public automatic-prompt hook is verified. Agents
9//! are expected to read this file via filesystem or a future MCP/ACP bridge.
10
11use std::path::Path;
12
13use crate::context_engine::{ContextSnapshot, WindowSummary};
14
15pub fn render(snapshot: &ContextSnapshot) -> String {
16    let mut out = String::new();
17
18    // ── Header ───────────────────────────────────────────────────────────────
19    let repo = Path::new(&snapshot.worktree_root)
20        .file_name()
21        .and_then(|n| n.to_str())
22        .unwrap_or(&snapshot.worktree_root);
23    // "2026-05-11T08:23:09.304042Z" → "2026-05-11 08:23Z"
24    let ts = snapshot
25        .updated_at
26        .get(..16)
27        .unwrap_or(&snapshot.updated_at)
28        .replace('T', " ");
29    out.push_str(&format!(
30        "# AGENT BRIEF — {} · {} · {}Z\n\n",
31        repo, snapshot.branch, ts
32    ));
33
34    // ── Current focus ────────────────────────────────────────────────────────
35    out.push_str("## Current focus\n");
36    let now_files = &snapshot.now.top_files;
37    if now_files.is_empty() {
38        out.push_str("No file activity in the last 15 min.\n");
39    } else {
40        let primary = &now_files[0];
41        let also: Vec<&str> = now_files[1..].iter().take(3).map(|s| s.as_str()).collect();
42        if also.is_empty() {
43            out.push_str(&format!("Active: {}\n", primary));
44        } else {
45            out.push_str(&format!("Active: {}  |  {}\n", primary, also.join("  ")));
46        }
47        if let Some(area) = snapshot.now.focus_areas.first() {
48            out.push_str(&format!("Area: {}/\n", area));
49        }
50    }
51    out.push('\n');
52
53    // ── Open loops ───────────────────────────────────────────────────────────
54    let changes = &snapshot.now.change_summary;
55    if !changes.is_empty() {
56        out.push_str("## Open loops\n");
57        let staged: Vec<&str> = changes
58            .iter()
59            .filter(|c| c.staged)
60            .map(|c| c.path.as_str())
61            .collect();
62        let modified: Vec<&str> = changes
63            .iter()
64            .filter(|c| c.unstaged && c.code != "??")
65            .map(|c| c.path.as_str())
66            .collect();
67        let untracked: Vec<&str> = changes
68            .iter()
69            .filter(|c| c.code == "??")
70            .map(|c| c.path.as_str())
71            .collect();
72
73        if !staged.is_empty() {
74            out.push_str(&format!(
75                "- Staged ({}): {}\n",
76                staged.len(),
77                staged.join("  ")
78            ));
79        }
80        if !modified.is_empty() {
81            out.push_str(&format!(
82                "- Unstaged ({}): {}\n",
83                modified.len(),
84                modified.join("  ")
85            ));
86        }
87        if !untracked.is_empty() {
88            out.push_str(&format!(
89                "- Untracked ({}): {}\n",
90                untracked.len(),
91                untracked.join("  ")
92            ));
93        }
94        out.push('\n');
95    }
96
97    // ── Now (last 15 min) ────────────────────────────────────────────────────
98    render_now(&mut out, &snapshot.now);
99
100    // ── Session (last 5 hr) ──────────────────────────────────────────────────
101    render_session(&mut out, &snapshot.session);
102
103    // ── Week (last 7 days) ───────────────────────────────────────────────────
104    render_week(&mut out, &snapshot.week, &snapshot.branch);
105
106    // ── Footer ───────────────────────────────────────────────────────────────
107    out.push_str("---\n");
108    out.push_str(
109        "context.json is the machine-readable source. Regenerated on save; do not edit.\n",
110    );
111
112    out
113}
114
115fn render_now(out: &mut String, window: &WindowSummary) {
116    if window.top_files.is_empty() {
117        return;
118    }
119    out.push_str("## Now  (last 15 min)\n");
120    let files: Vec<&str> = window.top_files.iter().take(5).map(|s| s.as_str()).collect();
121    out.push_str(&format!("{}\n\n", files.join("  ")));
122}
123
124fn render_session(out: &mut String, window: &WindowSummary) {
125    out.push_str("## Session  (last 5 hr)\n");
126
127    if let Some(area) = window.focus_areas.first() {
128        out.push_str(&format!("Focus: {}/\n", area));
129    }
130
131    if !window.commit_refs.is_empty() {
132        let commits: Vec<String> = window
133            .commit_refs
134            .iter()
135            .take(4)
136            .map(|c| format!("{} {}", c.sha, c.subject))
137            .collect();
138        out.push_str(&format!("Commits: {}\n", commits.join("  ·  ")));
139    } else {
140        out.push_str("Commits: none in window\n");
141    }
142
143    out.push('\n');
144}
145
146fn render_week(out: &mut String, window: &WindowSummary, branch: &str) {
147    out.push_str("## Week  (last 7 days)\n");
148
149    let n = window.commit_refs.len();
150    if n == 0 {
151        out.push_str("No commits in window.\n");
152    } else {
153        let subjects: Vec<&str> = window
154            .commit_refs
155            .iter()
156            .take(3)
157            .map(|c| c.subject.as_str())
158            .collect();
159        out.push_str(&format!(
160            "Direction: {} commit(s) on {}  —  {}\n",
161            n,
162            branch,
163            subjects.join("  ·  ")
164        ));
165    }
166
167    // Only emit themes that carry real signal (skip "focus on X" / "root-level edits")
168    let signal_themes: Vec<&str> = window
169        .themes
170        .iter()
171        .filter(|t| !t.starts_with("focus on") && t.as_str() != "root-level edits")
172        .map(|t| t.as_str())
173        .take(3)
174        .collect();
175    if !signal_themes.is_empty() {
176        out.push_str(&format!("Themes: {}\n", signal_themes.join("  ·  ")));
177    }
178
179    out.push('\n');
180}