feat(runtime-api): expose git status metadata for agent view (#2862)

This commit is contained in:
Hunter Bown
2026-06-06 02:51:21 -07:00
committed by GitHub
parent cc3cbc823c
commit 5bd2f6a99b
5 changed files with 107 additions and 11 deletions
+5 -4
View File
@@ -136,10 +136,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
preview backed by recent runtime thread summaries, plus a read-only
`GET /v1/snapshots` endpoint for GUI clients to inspect side-git restore
points. The extension now renders those restore points read-only in its Agent
View, and thread summaries include read-only workspace and branch metadata so
the VS Code Agent View can show when a thread or agent lane is on another
branch. Agent View and restore-point data now auto-refresh on a configurable
read-only interval so branch/workspace changes become visible without a
View, and thread summaries include read-only workspace, branch, current Git
head, and dirty-state metadata so the VS Code Agent View can show when a
thread or agent lane is on another branch or has changed worktree state. Agent
View and restore-point data now auto-refresh on a configurable
read-only interval so branch/workspace/status changes become visible without a
manual refresh. Agent View refreshes keep thread branch/workspace rows
independent from restore-point loading, so a snapshot-listing failure no
longer clears already-available thread metadata. This answers the VS Code GUI
+5 -4
View File
@@ -136,10 +136,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
preview backed by recent runtime thread summaries, plus a read-only
`GET /v1/snapshots` endpoint for GUI clients to inspect side-git restore
points. The extension now renders those restore points read-only in its Agent
View, and thread summaries include read-only workspace and branch metadata so
the VS Code Agent View can show when a thread or agent lane is on another
branch. Agent View and restore-point data now auto-refresh on a configurable
read-only interval so branch/workspace changes become visible without a
View, and thread summaries include read-only workspace, branch, current Git
head, and dirty-state metadata so the VS Code Agent View can show when a
thread or agent lane is on another branch or has changed worktree state. Agent
View and restore-point data now auto-refresh on a configurable
read-only interval so branch/workspace/status changes become visible without a
manual refresh. Agent View refreshes keep thread branch/workspace rows
independent from restore-point loading, so a snapshot-listing failure no
longer clears already-available thread metadata. This answers the VS Code GUI
+92 -2
View File
@@ -266,6 +266,8 @@ struct ThreadSummary {
mode: String,
workspace: PathBuf,
branch: Option<String>,
head: Option<String>,
dirty: bool,
archived: bool,
updated_at: chrono::DateTime<Utc>,
latest_turn_id: Option<String>,
@@ -277,6 +279,8 @@ struct WorkspaceStatusResponse {
workspace: PathBuf,
git_repo: bool,
branch: Option<String>,
head: Option<String>,
dirty: bool,
staged: usize,
unstaged: usize,
untracked: usize,
@@ -284,6 +288,13 @@ struct WorkspaceStatusResponse {
behind: Option<u32>,
}
#[derive(Debug, Default)]
struct WorkspaceGitMetadata {
branch: Option<String>,
head: Option<String>,
dirty: bool,
}
#[derive(Debug, Serialize)]
struct SkillEntry {
name: String,
@@ -1241,13 +1252,16 @@ async fn list_threads_summary(
}
}
let workspace_git = collect_workspace_git_metadata(&thread.workspace);
summaries.push(ThreadSummary {
id: thread.id,
title,
preview,
model: thread.model,
mode: thread.mode,
branch: current_git_branch(&thread.workspace),
branch: workspace_git.branch,
head: workspace_git.head,
dirty: workspace_git.dirty,
workspace: thread.workspace,
archived: thread.archived,
updated_at: thread.updated_at,
@@ -2000,6 +2014,8 @@ fn collect_workspace_status(workspace: &std::path::Path) -> WorkspaceStatusRespo
workspace: workspace.to_path_buf(),
git_repo: false,
branch: None,
head: None,
dirty: false,
staged: 0,
unstaged: 0,
untracked: 0,
@@ -2015,7 +2031,10 @@ fn collect_workspace_status(workspace: &std::path::Path) -> WorkspaceStatusRespo
}
status.git_repo = true;
status.branch = current_git_branch(workspace);
let metadata = collect_workspace_git_metadata(workspace);
status.branch = metadata.branch;
status.head = metadata.head;
status.dirty = metadata.dirty;
if let Some(porcelain) = run_git(workspace, &["status", "--porcelain=v1"]) {
for line in porcelain.lines() {
@@ -2049,6 +2068,22 @@ fn collect_workspace_status(workspace: &std::path::Path) -> WorkspaceStatusRespo
status
}
fn collect_workspace_git_metadata(workspace: &std::path::Path) -> WorkspaceGitMetadata {
let Some(repo_check) = run_git(workspace, &["rev-parse", "--is-inside-work-tree"]) else {
return WorkspaceGitMetadata::default();
};
if repo_check.trim() != "true" {
return WorkspaceGitMetadata::default();
}
WorkspaceGitMetadata {
branch: current_git_branch(workspace),
head: current_git_head(workspace),
dirty: run_git(workspace, &["status", "--porcelain=v1"])
.is_some_and(|porcelain| !porcelain.trim().is_empty()),
}
}
fn run_git(workspace: &std::path::Path, args: &[&str]) -> Option<String> {
let output = crate::dependencies::Git::output(args, workspace).ok()?;
if !output.status.success() {
@@ -2075,6 +2110,12 @@ fn current_git_branch(workspace: &std::path::Path) -> Option<String> {
(!short_hash.is_empty()).then(|| format!("detached@{short_hash}"))
}
fn current_git_head(workspace: &std::path::Path) -> Option<String> {
let head = run_git(workspace, &["rev-parse", "--short", "HEAD"])?;
let head = head.trim();
(!head.is_empty()).then(|| head.to_string())
}
fn resolve_skills_dir(config: &Config, workspace: &std::path::Path) -> PathBuf {
// Canonicalize the workspace once so the symlink-containment check below
// compares like-for-like. If the workspace can't be canonicalized at all
@@ -2459,6 +2500,43 @@ mod tests {
Ok(())
}
#[test]
fn workspace_status_reports_head_and_dirty_counts() -> Result<()> {
let tmp = tempfile::tempdir()?;
let repo = tmp.path().join("repo");
fs::create_dir_all(&repo)?;
run_test_git(&repo, &["init", "-b", "main"])?;
fs::write(repo.join("tracked.txt"), "clean\n")?;
run_test_git(&repo, &["add", "tracked.txt"])?;
run_test_git(
&repo,
&[
"-c",
"user.name=CodeWhale Test",
"-c",
"user.email=codewhale@example.invalid",
"commit",
"-m",
"init",
],
)?;
let clean = collect_workspace_status(&repo);
assert!(clean.git_repo);
assert_eq!(clean.branch.as_deref(), Some("main"));
assert!(clean.head.as_deref().is_some_and(|head| !head.is_empty()));
assert!(!clean.dirty);
fs::write(repo.join("tracked.txt"), "dirty\n")?;
fs::write(repo.join("untracked.txt"), "new\n")?;
let dirty = collect_workspace_status(&repo);
assert!(dirty.dirty);
assert_eq!(dirty.unstaged, 1);
assert_eq!(dirty.untracked, 1);
Ok(())
}
#[test]
fn session_detail_tool_use_preserves_caller_metadata() {
let detail = session_to_detail(saved_session_with_blocks(vec![
@@ -2949,6 +3027,10 @@ mod tests {
.as_str()
.context("missing git thread id")?
.to_string();
fs::write(
repo.join("dirty.txt"),
"worktree changed after thread spawn\n",
)?;
let plain_thread: serde_json::Value = client
.post(format!("http://{addr}/v1/threads"))
@@ -2979,6 +3061,12 @@ mod tests {
.find(|item| item["id"] == git_thread_id)
.context("missing git workspace summary")?;
assert_eq!(git_summary["branch"], "feature/agent");
assert!(
git_summary["head"]
.as_str()
.is_some_and(|head| !head.is_empty())
);
assert_eq!(git_summary["dirty"], true);
assert_eq!(git_summary["workspace"], repo.to_string_lossy().as_ref());
let plain_summary = summaries
@@ -2986,6 +3074,8 @@ mod tests {
.find(|item| item["id"] == plain_thread_id)
.context("missing plain workspace summary")?;
assert_eq!(plain_summary["branch"], serde_json::Value::Null);
assert_eq!(plain_summary["head"], serde_json::Value::Null);
assert_eq!(plain_summary["dirty"], false);
assert_eq!(
plain_summary["workspace"],
non_git.to_string_lossy().as_ref()
+4
View File
@@ -191,6 +191,8 @@ workspace metadata:
"model": "deepseek-v4-pro",
"mode": "agent",
"branch": "feature/runtime-api",
"head": "abc1234",
"dirty": false,
"workspace": "/Users/you/projects/codewhale",
"archived": false,
"updated_at": "2026-06-06T05:43:00Z",
@@ -201,6 +203,8 @@ workspace metadata:
`branch` is resolved from the thread workspace at request time and may be
`null` when the workspace is not a Git repository or the branch cannot be read.
`head` is the current short Git commit for that workspace when available.
`dirty` is true when the workspace has staged, unstaged, or untracked changes.
`workspace` is included so editor clients can show when an agent lane is working
outside the current VS Code folder.
+1 -1
View File
@@ -53,7 +53,7 @@ config source, result, and follow-up issue or PR.
| Transcript tool-collapse smoke or explicit defer | UX steward | ship | #2776 (`c76ec4752`) landed dense successful tool-run collapse with guardrails for failed/running/shell/patch/review/diff cells; focused widget coverage includes `chat_widget_collapses_dense_tool_runs_by_default`, `chat_widget_expands_dense_tool_runs_on_demand`, and `chat_widget_expanded_mode_leaves_dense_tool_runs_visible`. |
| Sidebar detail popovers smoke or explicit defer | UX steward | ship | #2778 (`3cb49233e`) added row-level hover metadata and wrapping detail popovers for truncated Work/Tasks/Agents rows; #2806 (`19f5c7aa6`) preserved current sub-agent progress in the sidebar hover text. Focused coverage includes `sidebar_hover_rows_mark_source_text_diff_as_truncated` and `subagent_hover_text_preserves_full_agent_id_and_progress`. |
| Plan review/handoff artifact smoke | Plan steward | ship | #2770 (`7ac8063b6`) added rich PlanArtifact sections through the transcript/Plan prompt path; focused coverage includes `plan_update_cell_renders_rich_artifact_metadata` and `plan_prompt_renders_rich_plan_artifact_sections`. |
| VS Code Agent View branch/workspace visibility smoke | GUI steward | ship | #2825 (`1bacaf763`) added `workspace` / `branch` metadata to `/v1/threads/summary`; #2832 (`50b773f1d`) added read-only auto-refresh so branch/workspace changes can appear without manual refresh. |
| VS Code Agent View branch/workspace visibility smoke | GUI steward | ship | #2825 (`1bacaf763`) added `workspace` / `branch` metadata to `/v1/threads/summary`; #2832 (`50b773f1d`) added read-only auto-refresh so branch/workspace changes can appear without manual refresh. The current stewardship slice extends the same read-only metadata with current Git `head` and `dirty` worktree state for editor/agent-lane visibility. |
## v0.9.0 Feature Gates