tau-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/usage/anthropic.ts +198 -0
  19. package/extensions/usage/github-copilot.ts +204 -0
  20. package/extensions/usage/google-gemini-cli.ts +232 -0
  21. package/extensions/usage/index.ts +2310 -0
  22. package/extensions/usage/minimax.ts +208 -0
  23. package/extensions/usage/openai-codex.ts +180 -0
  24. package/extensions/usage/openrouter.ts +168 -0
  25. package/extensions/usage/providers.ts +26 -0
  26. package/extensions/usage/shared.ts +156 -0
  27. package/extensions/usage/types.ts +53 -0
  28. package/extensions/usage/zai.ts +186 -0
  29. package/extensions/websearch/README.md +68 -0
  30. package/extensions/websearch/browser/chromium.ts +330 -0
  31. package/extensions/websearch/browser/discovery.ts +75 -0
  32. package/extensions/websearch/browser/firefox.ts +150 -0
  33. package/extensions/websearch/browser/sqlite.ts +38 -0
  34. package/extensions/websearch/config.ts +76 -0
  35. package/extensions/websearch/index.ts +312 -0
  36. package/extensions/websearch/normalize.ts +155 -0
  37. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  38. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  39. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  40. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  41. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  42. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  43. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  44. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  45. package/extensions/websearch/providers/shared.ts +76 -0
  46. package/extensions/websearch/types.ts +52 -0
  47. package/extensions/worktree.ts +2223 -0
  48. package/package.json +72 -0
  49. package/skills/browser-tools/SKILL.md +253 -0
  50. package/skills/browser-tools/scripts/browser-content.js +100 -0
  51. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  52. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  53. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  54. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  55. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  56. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  57. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  58. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  59. package/skills/browser-tools/scripts/browser-start.js +281 -0
  60. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  61. package/skills/browser-tools/scripts/utils.js +60 -0
  62. package/skills/git-clean-history/SKILL.md +45 -0
  63. package/skills/git-commit/SKILL.md +55 -0
  64. package/skills/oracle/SKILL.md +64 -0
  65. package/skills/oracle/scripts/oracle +249 -0
  66. package/skills/oracle/scripts/oracle-bundle +227 -0
  67. package/skills/sentry/SKILL.md +198 -0
  68. package/skills/sentry/lib/auth.js +99 -0
  69. package/skills/sentry/scripts/fetch-event.js +325 -0
  70. package/skills/sentry/scripts/fetch-issue.js +368 -0
  71. package/skills/sentry/scripts/list-issues.js +245 -0
  72. package/skills/sentry/scripts/search-events.js +291 -0
  73. package/skills/sentry/scripts/search-logs.js +234 -0
  74. package/skills/update-changelog/SKILL.md +135 -0
  75. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Ghostty terminal title integration.
3
+ *
4
+ * - Shows project/session in the terminal title
5
+ * - Shows a braille spinner in the title while the agent is working
6
+ * - Shows a ? marker while an extension prompt is waiting for input
7
+ * - Updates title with the current tool name during tool execution
8
+ * - Treats background /review runs as working state via review:start/review:end events
9
+ *
10
+ * Pi now emits native OSC 9;4 progress indicators, so this extension only manages the title.
11
+ */
12
+
13
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
14
+ import path from "node:path";
15
+
16
+ const STATUS_SPINNER_INTERVAL_MS = 80;
17
+ const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
18
+ const REVIEW_EVENT_START = "review:start";
19
+ const REVIEW_EVENT_END = "review:end";
20
+
21
+ let sessionName: string | undefined;
22
+ let sessionCwd: string | undefined;
23
+ let currentTool: string | undefined;
24
+ let isWorking = false;
25
+ let frameIndex = 0;
26
+ let spinnerTimer: ReturnType<typeof setInterval> | undefined;
27
+ let pendingPromptCount = 0;
28
+ let latestCtx: ExtensionContext | undefined;
29
+ let currentSessionKey: string | undefined;
30
+ const activeReviewSessions = new Set<string>();
31
+
32
+ function buildTitle(extra?: string, marker = "π"): string {
33
+ const cwd = sessionCwd ?? process.cwd();
34
+ const segments: string[] = [marker, path.basename(cwd)];
35
+ if (sessionName) segments.push(sessionName);
36
+ if (extra) segments.push(extra);
37
+ return segments.join(" · ");
38
+ }
39
+
40
+ function clearSpinnerTimer(): void {
41
+ if (!spinnerTimer) return;
42
+ clearInterval(spinnerTimer);
43
+ spinnerTimer = undefined;
44
+ }
45
+
46
+ function currentFrame(): string {
47
+ return STATUS_SPINNER_FRAMES[frameIndex % STATUS_SPINNER_FRAMES.length];
48
+ }
49
+
50
+ function hasPendingPrompts(): boolean {
51
+ return pendingPromptCount > 0;
52
+ }
53
+
54
+ function getSessionKey(ctx: ExtensionContext): string {
55
+ return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
56
+ }
57
+
58
+ function extractReviewSessionKey(data: unknown): string | undefined {
59
+ if (!data || typeof data !== "object") return undefined;
60
+ const payload = data as { sessionKey?: unknown };
61
+ if (typeof payload.sessionKey !== "string") return undefined;
62
+ const sessionKey = payload.sessionKey.trim();
63
+ return sessionKey.length > 0 ? sessionKey : undefined;
64
+ }
65
+
66
+ function hasActiveReviewRuns(): boolean {
67
+ if (!currentSessionKey) return false;
68
+ return activeReviewSessions.has(currentSessionKey);
69
+ }
70
+
71
+ function isBusy(): boolean {
72
+ return isWorking || hasActiveReviewRuns();
73
+ }
74
+
75
+ function getWorkingExtra(): string | undefined {
76
+ if (currentTool) return currentTool;
77
+ if (!isWorking && hasActiveReviewRuns()) return "review";
78
+ return undefined;
79
+ }
80
+
81
+ function renderWorkingTitle(ctx: ExtensionContext): void {
82
+ ctx.ui.setTitle(buildTitle(getWorkingExtra(), currentFrame()));
83
+ }
84
+
85
+ function renderPromptTitle(ctx: ExtensionContext): void {
86
+ const extra = isBusy() ? getWorkingExtra() : undefined;
87
+ ctx.ui.setTitle(buildTitle(extra, "?"));
88
+ }
89
+
90
+ function renderActiveTitle(ctx: ExtensionContext): void {
91
+ if (hasPendingPrompts()) {
92
+ renderPromptTitle(ctx);
93
+ return;
94
+ }
95
+
96
+ if (isBusy()) {
97
+ renderWorkingTitle(ctx);
98
+ return;
99
+ }
100
+
101
+ ctx.ui.setTitle(buildTitle());
102
+ }
103
+
104
+ function startSpinnerTimer(ctx: ExtensionContext): void {
105
+ clearSpinnerTimer();
106
+ spinnerTimer = setInterval(() => {
107
+ if (!isBusy() || hasPendingPrompts()) return;
108
+ frameIndex = (frameIndex + 1) % STATUS_SPINNER_FRAMES.length;
109
+ renderWorkingTitle(ctx);
110
+ }, STATUS_SPINNER_INTERVAL_MS);
111
+ }
112
+
113
+ function startSpinner(ctx: ExtensionContext): void {
114
+ clearSpinnerTimer();
115
+ isWorking = true;
116
+ currentTool = undefined;
117
+ frameIndex = 0;
118
+ renderActiveTitle(ctx);
119
+
120
+ if (!hasPendingPrompts()) {
121
+ startSpinnerTimer(ctx);
122
+ }
123
+ }
124
+
125
+ function stopSpinner(ctx: ExtensionContext): void {
126
+ isWorking = false;
127
+ currentTool = undefined;
128
+ clearSpinnerTimer();
129
+
130
+ if (hasPendingPrompts()) {
131
+ renderActiveTitle(ctx);
132
+ return;
133
+ }
134
+
135
+ if (hasActiveReviewRuns()) {
136
+ renderWorkingTitle(ctx);
137
+ startSpinnerTimer(ctx);
138
+ return;
139
+ }
140
+
141
+ renderActiveTitle(ctx);
142
+ }
143
+
144
+ function handlePromptStart(ctx: ExtensionContext): void {
145
+ clearSpinnerTimer();
146
+ renderActiveTitle(ctx);
147
+ }
148
+
149
+ function handlePromptEnd(ctx: ExtensionContext): void {
150
+ if (hasPendingPrompts()) {
151
+ renderActiveTitle(ctx);
152
+ return;
153
+ }
154
+
155
+ if (isBusy()) {
156
+ renderWorkingTitle(ctx);
157
+ startSpinnerTimer(ctx);
158
+ return;
159
+ }
160
+
161
+ renderActiveTitle(ctx);
162
+ }
163
+
164
+ function syncSessionTitle(ctx: ExtensionContext): void {
165
+ renderActiveTitle(ctx);
166
+ }
167
+
168
+ function markReviewSessionActive(sessionKey: string): void {
169
+ activeReviewSessions.add(sessionKey);
170
+ }
171
+
172
+ function markReviewSessionInactive(sessionKey: string): void {
173
+ activeReviewSessions.delete(sessionKey);
174
+ }
175
+
176
+ function handleReviewStart(ctx: ExtensionContext): void {
177
+ renderActiveTitle(ctx);
178
+ if (!hasPendingPrompts()) {
179
+ startSpinnerTimer(ctx);
180
+ }
181
+ }
182
+
183
+ function handleReviewEnd(ctx: ExtensionContext): void {
184
+ if (isWorking) {
185
+ renderActiveTitle(ctx);
186
+ return;
187
+ }
188
+
189
+ if (hasPendingPrompts()) {
190
+ renderActiveTitle(ctx);
191
+ return;
192
+ }
193
+
194
+ if (hasActiveReviewRuns()) {
195
+ renderActiveTitle(ctx);
196
+ startSpinnerTimer(ctx);
197
+ return;
198
+ }
199
+
200
+ clearSpinnerTimer();
201
+ renderActiveTitle(ctx);
202
+ }
203
+
204
+ export default function (pi: ExtensionAPI) {
205
+ pi.on("session_start", async (_event, ctx) => {
206
+ if (!ctx.hasUI) return;
207
+ latestCtx = ctx;
208
+ currentSessionKey = getSessionKey(ctx);
209
+ sessionCwd = ctx.cwd;
210
+ sessionName = pi.getSessionName();
211
+ syncSessionTitle(ctx);
212
+ clearSpinnerTimer();
213
+ if (isBusy() && !hasPendingPrompts()) {
214
+ startSpinnerTimer(ctx);
215
+ }
216
+ });
217
+
218
+ pi.on("agent_start", async (_event, ctx) => {
219
+ if (!ctx.hasUI) return;
220
+ latestCtx = ctx;
221
+ startSpinner(ctx);
222
+ });
223
+
224
+ pi.on("agent_end", async (_event, ctx) => {
225
+ if (!ctx.hasUI) return;
226
+ latestCtx = ctx;
227
+ stopSpinner(ctx);
228
+ });
229
+
230
+ pi.on("tool_execution_start", async (event, ctx) => {
231
+ currentTool = event.toolName;
232
+ if (!ctx.hasUI) return;
233
+ latestCtx = ctx;
234
+ if (!isWorking) return;
235
+ renderActiveTitle(ctx);
236
+ });
237
+
238
+ pi.on("tool_execution_end", async (_event, ctx) => {
239
+ currentTool = undefined;
240
+ if (!ctx.hasUI) return;
241
+ latestCtx = ctx;
242
+ if (!isWorking) return;
243
+ renderActiveTitle(ctx);
244
+ });
245
+
246
+ pi.events.on("ui:prompt_start", () => {
247
+ pendingPromptCount += 1;
248
+
249
+ const ctx = latestCtx;
250
+ if (!ctx || !ctx.hasUI) return;
251
+ handlePromptStart(ctx);
252
+ });
253
+
254
+ pi.events.on("ui:prompt_end", () => {
255
+ if (pendingPromptCount === 0) return;
256
+ pendingPromptCount -= 1;
257
+
258
+ const ctx = latestCtx;
259
+ if (!ctx || !ctx.hasUI) return;
260
+ handlePromptEnd(ctx);
261
+ });
262
+
263
+ pi.events.on(REVIEW_EVENT_START, (data) => {
264
+ const sessionKey = extractReviewSessionKey(data);
265
+ if (!sessionKey) return;
266
+
267
+ markReviewSessionActive(sessionKey);
268
+
269
+ const ctx = latestCtx;
270
+ if (!ctx || !ctx.hasUI) return;
271
+ if (currentSessionKey !== sessionKey) return;
272
+ handleReviewStart(ctx);
273
+ });
274
+
275
+ pi.events.on(REVIEW_EVENT_END, (data) => {
276
+ const sessionKey = extractReviewSessionKey(data);
277
+ if (!sessionKey) return;
278
+
279
+ markReviewSessionInactive(sessionKey);
280
+
281
+ const ctx = latestCtx;
282
+ if (!ctx || !ctx.hasUI) return;
283
+ if (currentSessionKey !== sessionKey) return;
284
+ handleReviewEnd(ctx);
285
+ });
286
+
287
+ pi.on("session_shutdown", async (_event, ctx) => {
288
+ clearSpinnerTimer();
289
+ isWorking = false;
290
+ currentTool = undefined;
291
+ pendingPromptCount = 0;
292
+ const sessionKey = getSessionKey(ctx);
293
+ activeReviewSessions.delete(sessionKey);
294
+ if (currentSessionKey === sessionKey) {
295
+ currentSessionKey = undefined;
296
+ sessionCwd = undefined;
297
+ sessionName = undefined;
298
+ }
299
+ latestCtx = undefined;
300
+ });
301
+ }
@@ -0,0 +1,277 @@
1
+ import {
2
+ isEditToolResult,
3
+ isWriteToolResult,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@mariozechner/pi-coding-agent";
7
+ import { spawn } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { copyFile, mkdtemp, rm } from "node:fs/promises";
10
+ import path from "node:path";
11
+
12
+ const STATUS_KEY = "1-git-diff-stats";
13
+ const REFRESH_DEBOUNCE_MS = 250;
14
+ const WRITE_REFRESH_DEBOUNCE_MS = 1_000;
15
+
16
+ type DiffStats = {
17
+ added: number;
18
+ removed: number;
19
+ };
20
+
21
+ function isInsideGitRepo(startDir: string): boolean {
22
+ for (let currentDir = path.resolve(startDir); ; currentDir = path.dirname(currentDir)) {
23
+ if (existsSync(path.join(currentDir, ".git"))) return true;
24
+ if (path.dirname(currentDir) === currentDir) return false;
25
+ }
26
+ }
27
+
28
+ function mergeNumstatEntries(output: string, statsByPath: Map<string, DiffStats>): void {
29
+ for (const line of output.split("\n")) {
30
+ if (!line) continue;
31
+
32
+ const firstTab = line.indexOf("\t");
33
+ if (firstTab === -1) continue;
34
+
35
+ const secondTab = line.indexOf("\t", firstTab + 1);
36
+ if (secondTab === -1) continue;
37
+
38
+ const filePath = line.slice(secondTab + 1);
39
+ if (statsByPath.has(filePath)) continue;
40
+
41
+ const addedToken = line.slice(0, firstTab);
42
+ const removedToken = line.slice(firstTab + 1, secondTab);
43
+ statsByPath.set(filePath, {
44
+ added: addedToken === "-" ? 0 : Number(addedToken) || 0,
45
+ removed: removedToken === "-" ? 0 : Number(removedToken) || 0,
46
+ });
47
+ }
48
+ }
49
+
50
+ async function gitText(
51
+ cwd: string,
52
+ args: string[],
53
+ options?: {
54
+ env?: NodeJS.ProcessEnv;
55
+ stdin?: string;
56
+ },
57
+ ): Promise<string> {
58
+ return new Promise((resolve, reject) => {
59
+ const child = spawn("git", args, {
60
+ cwd,
61
+ stdio: ["pipe", "pipe", "pipe"],
62
+ env: options?.env ? { ...process.env, ...options.env } : process.env,
63
+ });
64
+
65
+ let stdout = "";
66
+ let stderr = "";
67
+
68
+ child.on("error", reject);
69
+ child.stdin?.on("error", () => {});
70
+ child.stdout?.on("data", (chunk: Buffer) => {
71
+ stdout += chunk.toString("utf8");
72
+ });
73
+ child.stderr?.on("data", (chunk: Buffer) => {
74
+ stderr += chunk.toString("utf8");
75
+ });
76
+ child.on("close", (code) => {
77
+ const exitCode = code ?? -1;
78
+ if (exitCode !== 0) {
79
+ const details = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
80
+ reject(new Error(details || `git ${args.join(" ")} failed`));
81
+ return;
82
+ }
83
+
84
+ resolve(stdout.trim());
85
+ });
86
+
87
+ child.stdin?.end(options?.stdin);
88
+ });
89
+ }
90
+
91
+ async function computeLocalStats(cwd: string): Promise<DiffStats | undefined> {
92
+ if (!isInsideGitRepo(cwd)) return undefined;
93
+
94
+ const gitDir = await gitText(cwd, ["rev-parse", "--path-format=absolute", "--git-dir"]);
95
+ const tempDir = await mkdtemp(path.join(gitDir, "pi-git-diff-stats-"));
96
+ const tempIndex = path.join(tempDir, "index");
97
+ const realIndex = path.join(gitDir, "index");
98
+
99
+ try {
100
+ try {
101
+ await copyFile(realIndex, tempIndex);
102
+ } catch (error) {
103
+ const code = (error as NodeJS.ErrnoException).code;
104
+ if (code !== "ENOENT") throw error;
105
+ }
106
+
107
+ const addIntent = gitText(cwd, ["add", "-N", "--all"], { env: { GIT_INDEX_FILE: tempIndex } });
108
+ const head = spawn("git", ["rev-parse", "--verify", "--quiet", "HEAD"], {
109
+ cwd,
110
+ stdio: ["ignore", "pipe", "pipe"],
111
+ });
112
+
113
+ let headStdout = "";
114
+ let headStderr = "";
115
+ head.stdout?.on("data", (chunk: Buffer) => {
116
+ headStdout += chunk.toString("utf8");
117
+ });
118
+ head.stderr?.on("data", (chunk: Buffer) => {
119
+ headStderr += chunk.toString("utf8");
120
+ });
121
+
122
+ const headOidPromise = new Promise<string | undefined>((resolve, reject) => {
123
+ head.on("error", reject);
124
+ head.on("close", (code) => {
125
+ if (code === 0) {
126
+ resolve(headStdout.trim());
127
+ return;
128
+ }
129
+ if (code === 1) {
130
+ resolve(undefined);
131
+ return;
132
+ }
133
+
134
+ const details = [headStdout.trim(), headStderr.trim()].filter(Boolean).join("\n");
135
+ reject(new Error(details || "git rev-parse --verify --quiet HEAD failed"));
136
+ });
137
+ });
138
+
139
+ const [, headOid] = await Promise.all([addIntent, headOidPromise]);
140
+ const baseOid =
141
+ headOid ?? (await gitText(cwd, ["hash-object", "-t", "tree", "--stdin"], { stdin: "" }));
142
+
143
+ const [workingTreeDiff, stagedDiff] = await Promise.all([
144
+ gitText(cwd, ["diff", "--numstat", baseOid, "--"], { env: { GIT_INDEX_FILE: tempIndex } }),
145
+ gitText(cwd, ["diff", "--cached", "--numstat", baseOid, "--"], {
146
+ env: { GIT_INDEX_FILE: tempIndex },
147
+ }),
148
+ ]);
149
+
150
+ const statsByPath = new Map<string, DiffStats>();
151
+ mergeNumstatEntries(workingTreeDiff, statsByPath);
152
+ mergeNumstatEntries(stagedDiff, statsByPath);
153
+ if (statsByPath.size === 0) return undefined;
154
+
155
+ let added = 0;
156
+ let removed = 0;
157
+ for (const fileStats of statsByPath.values()) {
158
+ added += fileStats.added;
159
+ removed += fileStats.removed;
160
+ }
161
+
162
+ return { added, removed };
163
+ } finally {
164
+ await rm(tempDir, { recursive: true, force: true });
165
+ }
166
+ }
167
+
168
+ export default function gitDiffStatsExtension(pi: ExtensionAPI) {
169
+ let ctx: ExtensionContext | undefined;
170
+ let stats: DiffStats | undefined;
171
+ let generation = 0;
172
+ let refreshTimer: NodeJS.Timeout | undefined;
173
+ let refreshInFlight: Promise<void> | null = null;
174
+ let refreshQueued = false;
175
+
176
+ function reset(nextCtx?: ExtensionContext): void {
177
+ clearStatus();
178
+ ctx = nextCtx;
179
+ stats = undefined;
180
+ generation += 1;
181
+ resetRefreshState();
182
+
183
+ if (nextCtx?.hasUI) scheduleRefresh(0);
184
+ }
185
+
186
+ function clearStatus(): void {
187
+ ctx?.ui.setStatus(STATUS_KEY, undefined);
188
+ }
189
+
190
+ function renderStatus(): void {
191
+ const activeCtx = ctx;
192
+ if (!activeCtx?.hasUI) return;
193
+
194
+ activeCtx.ui.setStatus(
195
+ STATUS_KEY,
196
+ stats ? activeCtx.ui.theme.fg("dim", `+${stats.added} -${stats.removed}`) : undefined,
197
+ );
198
+ }
199
+
200
+ function resetRefreshState(): void {
201
+ if (refreshTimer) clearTimeout(refreshTimer);
202
+ refreshTimer = undefined;
203
+ refreshInFlight = null;
204
+ refreshQueued = false;
205
+ }
206
+
207
+ function scheduleRefresh(delay = REFRESH_DEBOUNCE_MS): void {
208
+ const activeCtx = ctx;
209
+ if (!activeCtx?.hasUI) return;
210
+
211
+ if (refreshTimer) clearTimeout(refreshTimer);
212
+
213
+ const cwd = activeCtx.cwd;
214
+ const refreshGeneration = generation;
215
+ refreshTimer = setTimeout(() => {
216
+ refreshTimer = undefined;
217
+ void refresh(cwd, refreshGeneration);
218
+ }, delay);
219
+ }
220
+
221
+ async function refresh(cwd: string, refreshGeneration: number): Promise<void> {
222
+ if (refreshInFlight) {
223
+ refreshQueued = true;
224
+ return;
225
+ }
226
+
227
+ const promise = (async () => {
228
+ try {
229
+ const nextStats = await computeLocalStats(cwd);
230
+ if (generation !== refreshGeneration) return;
231
+
232
+ stats = nextStats;
233
+ renderStatus();
234
+ } catch {
235
+ if (generation !== refreshGeneration) return;
236
+
237
+ stats = undefined;
238
+ clearStatus();
239
+ }
240
+ })();
241
+
242
+ refreshInFlight = promise;
243
+
244
+ try {
245
+ await promise;
246
+ } finally {
247
+ if (refreshInFlight === promise) {
248
+ refreshInFlight = null;
249
+ if (refreshQueued) {
250
+ refreshQueued = false;
251
+ scheduleRefresh(0);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ pi.on("session_start", async (_event, nextCtx) => {
258
+ reset(nextCtx);
259
+ });
260
+
261
+ pi.on("tool_result", async (event, nextCtx) => {
262
+ ctx = nextCtx;
263
+
264
+ if (isEditToolResult(event) || isWriteToolResult(event)) {
265
+ scheduleRefresh(WRITE_REFRESH_DEBOUNCE_MS);
266
+ }
267
+ });
268
+
269
+ pi.on("turn_end", async (_event, nextCtx) => {
270
+ ctx = nextCtx;
271
+ scheduleRefresh();
272
+ });
273
+
274
+ pi.on("session_shutdown", async () => {
275
+ reset(undefined);
276
+ });
277
+ }