Skip to content

Commit 4ce53d4

Browse files
feat(vscode): VS Code extension for live session overlay (#49)
Adds vscode-extension/ — a VS Code / Cursor extension that shows agent-trace session activity directly in the editor. Features: - Status bar: live cost, tool call count, active tool name - Gutter annotations: colored left border + inline label on agent-read and agent-modified files (read = blue, write = amber) - Event stream panel: live feed in Explorer sidebar (tool calls, file ops, LLM requests, errors) with pause/resume button - Pause agent: writes .agent-traces/.pause-request; watch.py picks it up and sends SIGSTOP/SIGCONT to the agent process watch.py: add _check_pause_file() called on every idle poll cycle to honour pause/resume requests from the extension without requiring a daemon or direct process access from the editor. Zero overhead when no session is active — fs.watch on .active-session, no polling timers until a session starts. Closes #49 Co-authored-by: Ona <no-reply@ona.com>
1 parent c72f97c commit 4ce53d4

12 files changed

Lines changed: 1394 additions & 0 deletions

File tree

‎src/agent_trace/watch.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,27 @@ def check_event(
630630
_IDLE_SENTINEL = object() # yielded when poll_interval elapses with no new event
631631

632632

633+
def _check_pause_file(store: "TraceStore", state: "WatchState", out: "TextIO") -> None:
634+
"""Honour a .pause-request file written by the VS Code extension.
635+
636+
Presence of the file → SIGSTOP the agent (if a PID is known).
637+
Absence after a pause → SIGCONT to resume.
638+
"""
639+
pause_file = store.base_dir / ".pause-request"
640+
wants_pause = pause_file.exists()
641+
642+
if wants_pause and not state.paused and state.agent_pid:
643+
out.write(f"[watch] Pause requested by editor — SIGSTOP pid {state.agent_pid}\n")
644+
out.flush()
645+
_pause_process(state.agent_pid)
646+
state.paused = True
647+
elif not wants_pause and state.paused and state.agent_pid:
648+
out.write(f"[watch] Resume requested by editor — SIGCONT pid {state.agent_pid}\n")
649+
out.flush()
650+
_resume_process(state.agent_pid)
651+
state.paused = False
652+
653+
633654
def _tail_events(events_file: Path, poll_interval: float = 0.5):
634655
"""Generator that yields TraceEvent objects or _IDLE_SENTINEL each poll cycle.
635656
@@ -693,6 +714,8 @@ def watch_session(
693714
break
694715

695716
if item is _IDLE_SENTINEL:
717+
# Check for pause-request signal file written by the VS Code extension
718+
_check_pause_file(store, state, out)
696719
continue
697720

698721
event: TraceEvent = item # type: ignore[assignment]

‎vscode-extension/.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
out/
3+
*.vsix

‎vscode-extension/.vscodeignore‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.vscode/**
2+
.vscode-test/**
3+
src/**
4+
.gitignore
5+
tsconfig.json
6+
**/*.map
7+
node_modules/**

‎vscode-extension/README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# agent-trace for VS Code
2+
3+
Live session overlay for [agent-trace](https://github.com/Siddhant-K-code/agent-trace). Shows what your agent is doing without leaving the editor.
4+
5+
## Features
6+
7+
**Status bar** — cost, tool call count, and active tool name, updated on every event. Click to open the event stream panel.
8+
9+
```
10+
$(pulse) agent $0.0042 47 calls [Read]
11+
```
12+
13+
**Gutter annotations** — files the agent has read or modified get a colored left border and inline label:
14+
15+
```
16+
src/auth/middleware.ts ← agent read 3×, modified 1× this session
17+
src/db/schema.ts ← agent read 1× this session
18+
```
19+
20+
**Event stream panel** — live feed of every tool call, file op, LLM request, and error in the Explorer sidebar. Same information as `agent-strace watch` but in the editor.
21+
22+
**Pause button** — stop the agent mid-session without killing it. Writes a signal file that `agent-strace watch` picks up and sends SIGSTOP to the agent process. Resume resumes it.
23+
24+
## Requirements
25+
26+
- [agent-trace](https://pypi.org/project/agent-strace/) installed (`pip install agent-strace` or `uv tool install agent-strace`)
27+
- A session started via `agent-strace setup` (Claude Code hooks) or `agent-strace record` (MCP proxy)
28+
29+
The extension activates automatically when a `.agent-traces/` directory exists in the workspace root.
30+
31+
## Usage
32+
33+
1. Install agent-trace and set up hooks:
34+
```bash
35+
agent-strace setup # adds hooks to .claude/settings.json
36+
```
37+
2. Open your project in VS Code / Cursor.
38+
3. Start Claude Code — the status bar item appears as soon as the session starts.
39+
4. Open the **Agent Trace** panel in the Explorer sidebar for the full event stream.
40+
41+
The **Pause** button in the panel (or `agent-trace: Pause Agent` command) sends SIGSTOP to the agent. This requires `agent-strace watch` to be running in a terminal alongside the session.
42+
43+
## Configuration
44+
45+
| Setting | Default | Description |
46+
|---|---|---|
47+
| `agentTrace.traceDir` | `.agent-traces` | Path to trace store, relative to workspace root |
48+
| `agentTrace.showGutterAnnotations` | `true` | Gutter icons on agent-touched files |
49+
| `agentTrace.showInlineText` | `true` | Inline read/write counts at top of file |
50+
51+
## How it works
52+
53+
The extension watches `.agent-traces/.active-session` for the current session ID, then tails `events.ndjson` for new events using `fs.watch`. No polling when idle. No network calls. No new processes.
54+
55+
Pause works by writing `.agent-traces/.pause-request` — `agent-strace watch` checks for this file on every poll cycle and sends SIGSTOP / SIGCONT to the agent PID.

‎vscode-extension/package.json‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"name": "agent-trace",
3+
"displayName": "agent-trace",
4+
"description": "Live session overlay for agent-trace: status bar, gutter annotations, and event stream panel.",
5+
"version": "0.1.0",
6+
"publisher": "Siddhant-K-code",
7+
"license": "MIT",
8+
"repository": {
9+
"type": "git",
10+
"url": "https://github.com/Siddhant-K-code/agent-trace"
11+
},
12+
"engines": {
13+
"vscode": "^1.85.0"
14+
},
15+
"categories": ["Other"],
16+
"keywords": ["ai", "agent", "trace", "observability", "claude", "mcp"],
17+
"icon": "icon.png",
18+
"activationEvents": [
19+
"workspaceContains:.agent-traces"
20+
],
21+
"main": "./out/extension.js",
22+
"contributes": {
23+
"commands": [
24+
{
25+
"command": "agentTrace.pauseAgent",
26+
"title": "agent-trace: Pause Agent",
27+
"icon": "$(debug-pause)"
28+
},
29+
{
30+
"command": "agentTrace.resumeAgent",
31+
"title": "agent-trace: Resume Agent",
32+
"icon": "$(debug-continue)"
33+
},
34+
{
35+
"command": "agentTrace.openPanel",
36+
"title": "agent-trace: Open Event Stream"
37+
},
38+
{
39+
"command": "agentTrace.clearDecorations",
40+
"title": "agent-trace: Clear File Decorations"
41+
}
42+
],
43+
"views": {
44+
"explorer": [
45+
{
46+
"type": "webview",
47+
"id": "agentTrace.eventStream",
48+
"name": "Agent Trace",
49+
"when": "agentTrace.sessionActive"
50+
}
51+
]
52+
},
53+
"configuration": {
54+
"title": "agent-trace",
55+
"properties": {
56+
"agentTrace.traceDir": {
57+
"type": "string",
58+
"default": ".agent-traces",
59+
"description": "Path to the trace store directory, relative to workspace root."
60+
},
61+
"agentTrace.showGutterAnnotations": {
62+
"type": "boolean",
63+
"default": true,
64+
"description": "Show gutter icons on files the agent has read or modified."
65+
},
66+
"agentTrace.showInlineText": {
67+
"type": "boolean",
68+
"default": true,
69+
"description": "Show inline read/write counts at the top of agent-touched files."
70+
}
71+
}
72+
}
73+
},
74+
"scripts": {
75+
"vscode:prepublish": "npm run compile",
76+
"compile": "tsc -p ./",
77+
"watch": "tsc -watch -p ./",
78+
"lint": "eslint src --ext ts"
79+
},
80+
"devDependencies": {
81+
"@types/node": "^20.0.0",
82+
"@types/vscode": "^1.85.0",
83+
"typescript": "^5.3.0"
84+
}
85+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Gutter annotations and inline text for agent-touched files.
3+
*
4+
* Files the agent read: subtle left-border highlight + inline "← agent read Nx"
5+
* Files the agent modified: stronger highlight + inline "← agent modified Nx"
6+
*
7+
* Decorations are applied to the first line of the file (non-intrusive).
8+
* Cleared when the session ends or the user runs agentTrace.clearDecorations.
9+
*/
10+
11+
import * as vscode from "vscode";
12+
import { FileAccess, SessionState } from "./traceStore";
13+
14+
// Decoration type for files the agent has read (but not written)
15+
const readDecoration = vscode.window.createTextEditorDecorationType({
16+
isWholeLine: true,
17+
overviewRulerColor: new vscode.ThemeColor("editorInfo.foreground"),
18+
overviewRulerLane: vscode.OverviewRulerLane.Left,
19+
borderWidth: "0 0 0 2px",
20+
borderStyle: "solid",
21+
borderColor: new vscode.ThemeColor("editorInfo.foreground"),
22+
light: { borderColor: "#4a9eff55" },
23+
dark: { borderColor: "#4a9eff55" },
24+
});
25+
26+
// Decoration type for files the agent has written
27+
const writeDecoration = vscode.window.createTextEditorDecorationType({
28+
isWholeLine: true,
29+
overviewRulerColor: new vscode.ThemeColor("editorWarning.foreground"),
30+
overviewRulerLane: vscode.OverviewRulerLane.Left,
31+
borderWidth: "0 0 0 3px",
32+
borderStyle: "solid",
33+
borderColor: new vscode.ThemeColor("editorWarning.foreground"),
34+
light: { borderColor: "#e5a00d88" },
35+
dark: { borderColor: "#e5a00d88" },
36+
});
37+
38+
export class DecorationManager extends vscode.Disposable {
39+
private readonly disposables: vscode.Disposable[] = [];
40+
41+
constructor() {
42+
super(() => this._disposeAll());
43+
44+
// Re-apply decorations when the active editor changes
45+
this.disposables.push(
46+
vscode.window.onDidChangeActiveTextEditor((editor) => {
47+
if (editor && this.currentState) {
48+
this._applyToEditor(editor, this.currentState.fileAccess);
49+
}
50+
})
51+
);
52+
}
53+
54+
private currentState: SessionState | null = null;
55+
56+
update(state: SessionState | null): void {
57+
this.currentState = state;
58+
59+
if (!state) {
60+
this._clearAll();
61+
return;
62+
}
63+
64+
for (const editor of vscode.window.visibleTextEditors) {
65+
this._applyToEditor(editor, state.fileAccess);
66+
}
67+
}
68+
69+
clear(): void {
70+
this._clearAll();
71+
this.currentState = null;
72+
}
73+
74+
private _applyToEditor(
75+
editor: vscode.TextEditor,
76+
fileAccess: Map<string, FileAccess>
77+
): void {
78+
const filePath = editor.document.uri.fsPath;
79+
const access = fileAccess.get(filePath);
80+
81+
if (!access) {
82+
editor.setDecorations(readDecoration, []);
83+
editor.setDecorations(writeDecoration, []);
84+
return;
85+
}
86+
87+
const firstLine = new vscode.Range(0, 0, 0, 0);
88+
89+
if (access.writes > 0) {
90+
const label = _label(access);
91+
editor.setDecorations(readDecoration, []);
92+
editor.setDecorations(writeDecoration, [
93+
{
94+
range: firstLine,
95+
renderOptions: {
96+
after: {
97+
contentText: ` ← ${label}`,
98+
color: new vscode.ThemeColor("editorWarning.foreground"),
99+
fontStyle: "italic",
100+
margin: "0 0 0 2em",
101+
},
102+
},
103+
},
104+
]);
105+
} else {
106+
const label = _label(access);
107+
editor.setDecorations(writeDecoration, []);
108+
editor.setDecorations(readDecoration, [
109+
{
110+
range: firstLine,
111+
renderOptions: {
112+
after: {
113+
contentText: ` ← ${label}`,
114+
color: new vscode.ThemeColor("editorInfo.foreground"),
115+
fontStyle: "italic",
116+
margin: "0 0 0 2em",
117+
},
118+
},
119+
},
120+
]);
121+
}
122+
}
123+
124+
private _clearAll(): void {
125+
for (const editor of vscode.window.visibleTextEditors) {
126+
editor.setDecorations(readDecoration, []);
127+
editor.setDecorations(writeDecoration, []);
128+
}
129+
}
130+
131+
private _disposeAll(): void {
132+
this._clearAll();
133+
readDecoration.dispose();
134+
writeDecoration.dispose();
135+
for (const d of this.disposables) { d.dispose(); }
136+
}
137+
}
138+
139+
function _label(access: FileAccess): string {
140+
const parts: string[] = [];
141+
if (access.reads > 0) {
142+
parts.push(`agent read ${access.reads}×`);
143+
}
144+
if (access.writes > 0) {
145+
parts.push(`agent modified ${access.writes}×`);
146+
}
147+
return parts.join(", ") + " this session";
148+
}

0 commit comments

Comments
 (0)