tau-coding-agent 0.1.6 → 0.2.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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -6,7 +6,7 @@
6
6
  * - Shows the braille spinner backwards while context is compacting
7
7
  * - Shows a ? marker while an extension prompt is waiting for input
8
8
  * - Updates title with the current tool name during tool execution
9
- * - Treats background /review runs as working state via review:start/review:end events
9
+ * - Includes background Review and Subagent work via their session-scoped lifecycle events
10
10
  *
11
11
  * Pi now emits native OSC 9;4 progress indicators, so this extension only manages the title.
12
12
  */
@@ -16,231 +16,214 @@ import path from "node:path";
16
16
 
17
17
  const STATUS_SPINNER_INTERVAL_MS = 80;
18
18
  const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
19
- const REVIEW_EVENT_START = "review:start";
20
- const REVIEW_EVENT_END = "review:end";
21
-
22
- let sessionName: string | undefined;
23
- let sessionCwd: string | undefined;
24
- let currentTool: string | undefined;
25
- let isWorking = false;
26
- let isCompacting = false;
27
- let frameIndex = 0;
28
- let spinnerTimer: ReturnType<typeof setInterval> | undefined;
29
- let pendingPromptCount = 0;
30
- let latestCtx: ExtensionContext | undefined;
31
- let currentSessionKey: string | undefined;
32
- const activeReviewSessions = new Set<string>();
33
-
34
- function buildTitle(extra?: string, marker = "π"): string {
35
- const cwd = sessionCwd ?? process.cwd();
36
- const segments: string[] = [marker, path.basename(cwd)];
37
- if (sessionName) segments.push(sessionName);
38
- if (extra) segments.push(extra);
39
- return segments.join(" · ");
40
- }
41
-
42
- function clearSpinnerTimer(): void {
43
- if (!spinnerTimer) return;
44
- clearInterval(spinnerTimer);
45
- spinnerTimer = undefined;
46
- }
47
-
48
- function currentFrame(): string {
49
- return STATUS_SPINNER_FRAMES[frameIndex % STATUS_SPINNER_FRAMES.length];
50
- }
51
-
52
- function resetFrame(): void {
53
- frameIndex = 0;
54
- }
55
19
 
56
- function advanceFrame(): void {
57
- const step = isCompacting ? -1 : 1;
58
- frameIndex = (frameIndex + step + STATUS_SPINNER_FRAMES.length) % STATUS_SPINNER_FRAMES.length;
59
- }
60
-
61
- function hasPendingPrompts(): boolean {
62
- return pendingPromptCount > 0;
63
- }
20
+ export default function (pi: ExtensionAPI) {
21
+ let sessionName: string | undefined;
22
+ let sessionCwd: string | undefined;
23
+ const activeTools = new Map<string, string>();
24
+ let isWorking = false;
25
+ let isCompacting = false;
26
+ let frameIndex = 0;
27
+ let spinnerTimer: ReturnType<typeof setInterval> | undefined;
28
+ let promptPending = false;
29
+ let latestCtx: ExtensionContext | undefined;
30
+ let currentSessionKey: string | undefined;
31
+ const activeBackgroundSessions = new Map([
32
+ ["review", new Set<string>()],
33
+ ["subagent", new Set<string>()],
34
+ ]);
35
+
36
+ function buildTitle(extra?: string, marker = "π"): string {
37
+ const cwd = sessionCwd ?? process.cwd();
38
+ const segments: string[] = [marker, path.basename(cwd)];
39
+ if (sessionName) segments.push(sessionName);
40
+ if (extra) segments.push(extra);
41
+ return segments.join(" · ");
42
+ }
64
43
 
65
- function getSessionKey(ctx: ExtensionContext): string {
66
- return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
67
- }
44
+ function clearSpinnerTimer(): void {
45
+ if (!spinnerTimer) return;
46
+ clearInterval(spinnerTimer);
47
+ spinnerTimer = undefined;
48
+ }
68
49
 
69
- function extractReviewSessionKey(data: unknown): string | undefined {
70
- if (!data || typeof data !== "object") return undefined;
71
- const payload = data as { sessionKey?: unknown };
72
- if (typeof payload.sessionKey !== "string") return undefined;
73
- const sessionKey = payload.sessionKey.trim();
74
- return sessionKey.length > 0 ? sessionKey : undefined;
75
- }
50
+ function currentFrame(): string {
51
+ return STATUS_SPINNER_FRAMES[frameIndex % STATUS_SPINNER_FRAMES.length];
52
+ }
76
53
 
77
- function hasActiveReviewRuns(): boolean {
78
- if (!currentSessionKey) return false;
79
- return activeReviewSessions.has(currentSessionKey);
80
- }
54
+ function resetFrame(): void {
55
+ frameIndex = 0;
56
+ }
81
57
 
82
- function isBusy(): boolean {
83
- return isWorking || isCompacting || hasActiveReviewRuns();
84
- }
58
+ function advanceFrame(): void {
59
+ const step = isCompacting ? -1 : 1;
60
+ frameIndex = (frameIndex + step + STATUS_SPINNER_FRAMES.length) % STATUS_SPINNER_FRAMES.length;
61
+ }
85
62
 
86
- function getWorkingExtra(): string | undefined {
87
- if (currentTool) return currentTool;
88
- if (isCompacting) return "compacting";
89
- if (!isWorking && hasActiveReviewRuns()) return "review";
90
- return undefined;
91
- }
63
+ function hasPendingPrompts(): boolean {
64
+ return promptPending;
65
+ }
92
66
 
93
- function renderWorkingTitle(ctx: ExtensionContext): void {
94
- ctx.ui.setTitle(buildTitle(getWorkingExtra(), currentFrame()));
95
- }
67
+ function getActiveBackgroundRun(): string | undefined {
68
+ for (const [kind, sessions] of activeBackgroundSessions) {
69
+ if (currentSessionKey && sessions.has(currentSessionKey)) return kind;
70
+ }
71
+ return undefined;
72
+ }
96
73
 
97
- function renderPromptTitle(ctx: ExtensionContext): void {
98
- const extra = isBusy() ? getWorkingExtra() : undefined;
99
- ctx.ui.setTitle(buildTitle(extra, "?"));
100
- }
74
+ function isBusy(): boolean {
75
+ return isWorking || isCompacting || getActiveBackgroundRun() !== undefined;
76
+ }
101
77
 
102
- function renderActiveTitle(ctx: ExtensionContext): void {
103
- if (hasPendingPrompts()) {
104
- renderPromptTitle(ctx);
105
- return;
78
+ function getWorkingExtra(): string | undefined {
79
+ const currentTool = [...activeTools.values()].at(-1);
80
+ if (currentTool) return currentTool;
81
+ if (isCompacting) return "compacting";
82
+ if (!isWorking) return getActiveBackgroundRun();
83
+ return undefined;
106
84
  }
107
85
 
108
- if (isBusy()) {
109
- renderWorkingTitle(ctx);
110
- return;
86
+ function renderWorkingTitle(ctx: ExtensionContext): void {
87
+ ctx.ui.setTitle(buildTitle(getWorkingExtra(), currentFrame()));
111
88
  }
112
89
 
113
- ctx.ui.setTitle(buildTitle());
114
- }
90
+ function renderPromptTitle(ctx: ExtensionContext): void {
91
+ const extra = isBusy() ? getWorkingExtra() : undefined;
92
+ ctx.ui.setTitle(buildTitle(extra, "?"));
93
+ }
115
94
 
116
- function startSpinnerTimer(ctx: ExtensionContext): void {
117
- clearSpinnerTimer();
118
- spinnerTimer = setInterval(() => {
119
- if (!isBusy() || hasPendingPrompts()) return;
120
- advanceFrame();
121
- renderWorkingTitle(ctx);
122
- }, STATUS_SPINNER_INTERVAL_MS);
123
- }
95
+ function renderActiveTitle(ctx: ExtensionContext): void {
96
+ if (hasPendingPrompts()) {
97
+ renderPromptTitle(ctx);
98
+ return;
99
+ }
124
100
 
125
- function startSpinner(ctx: ExtensionContext): void {
126
- clearSpinnerTimer();
127
- isWorking = true;
128
- currentTool = undefined;
129
- resetFrame();
130
- renderActiveTitle(ctx);
101
+ if (isBusy()) {
102
+ renderWorkingTitle(ctx);
103
+ return;
104
+ }
131
105
 
132
- if (!hasPendingPrompts()) {
133
- startSpinnerTimer(ctx);
106
+ ctx.ui.setTitle(buildTitle());
134
107
  }
135
- }
136
108
 
137
- function stopSpinner(ctx: ExtensionContext): void {
138
- isWorking = false;
139
- currentTool = undefined;
140
- clearSpinnerTimer();
109
+ function startSpinnerTimer(ctx: ExtensionContext): void {
110
+ clearSpinnerTimer();
111
+ spinnerTimer = setInterval(() => {
112
+ if (!isBusy() || hasPendingPrompts()) return;
113
+ advanceFrame();
114
+ renderWorkingTitle(ctx);
115
+ }, STATUS_SPINNER_INTERVAL_MS);
116
+ }
141
117
 
142
- if (hasPendingPrompts()) {
118
+ function startSpinner(ctx: ExtensionContext): void {
119
+ clearSpinnerTimer();
120
+ isWorking = true;
121
+ activeTools.clear();
122
+ resetFrame();
143
123
  renderActiveTitle(ctx);
144
- return;
145
- }
146
124
 
147
- if (isBusy()) {
148
- renderWorkingTitle(ctx);
149
- startSpinnerTimer(ctx);
150
- return;
125
+ if (!hasPendingPrompts()) {
126
+ startSpinnerTimer(ctx);
127
+ }
151
128
  }
152
129
 
153
- renderActiveTitle(ctx);
154
- }
130
+ function stopSpinner(ctx: ExtensionContext): void {
131
+ isWorking = false;
132
+ activeTools.clear();
133
+ clearSpinnerTimer();
155
134
 
156
- function handlePromptStart(ctx: ExtensionContext): void {
157
- clearSpinnerTimer();
158
- renderActiveTitle(ctx);
159
- }
135
+ if (hasPendingPrompts()) {
136
+ renderActiveTitle(ctx);
137
+ return;
138
+ }
139
+
140
+ if (isBusy()) {
141
+ renderWorkingTitle(ctx);
142
+ startSpinnerTimer(ctx);
143
+ return;
144
+ }
160
145
 
161
- function handlePromptEnd(ctx: ExtensionContext): void {
162
- if (hasPendingPrompts()) {
163
146
  renderActiveTitle(ctx);
164
- return;
165
147
  }
166
148
 
167
- if (isBusy()) {
168
- renderWorkingTitle(ctx);
169
- startSpinnerTimer(ctx);
170
- return;
149
+ function handlePromptStart(ctx: ExtensionContext): void {
150
+ clearSpinnerTimer();
151
+ renderActiveTitle(ctx);
171
152
  }
172
153
 
173
- renderActiveTitle(ctx);
174
- }
154
+ function handlePromptEnd(ctx: ExtensionContext): void {
155
+ if (hasPendingPrompts()) {
156
+ renderActiveTitle(ctx);
157
+ return;
158
+ }
175
159
 
176
- function startCompaction(ctx: ExtensionContext, signal: AbortSignal): void {
177
- isWorking = false;
178
- isCompacting = true;
179
- currentTool = undefined;
180
- resetFrame();
181
- signal.addEventListener("abort", () => stopCompaction(ctx), { once: true });
160
+ if (isBusy()) {
161
+ renderWorkingTitle(ctx);
162
+ startSpinnerTimer(ctx);
163
+ return;
164
+ }
182
165
 
183
- renderActiveTitle(ctx);
184
- if (!hasPendingPrompts()) {
185
- startSpinnerTimer(ctx);
166
+ renderActiveTitle(ctx);
186
167
  }
187
- }
188
-
189
- function stopCompaction(ctx: ExtensionContext, options?: { render?: boolean }): void {
190
- if (!isCompacting) return;
191
168
 
192
- isCompacting = false;
169
+ function startCompaction(ctx: ExtensionContext, signal: AbortSignal): void {
170
+ isCompacting = true;
171
+ activeTools.clear();
172
+ resetFrame();
173
+ signal.addEventListener("abort", () => stopCompaction(ctx), { once: true });
193
174
 
194
- if (options?.render === false) return;
195
-
196
- if (isBusy()) {
197
175
  renderActiveTitle(ctx);
198
176
  if (!hasPendingPrompts()) {
199
177
  startSpinnerTimer(ctx);
200
178
  }
201
- return;
202
179
  }
203
180
 
204
- clearSpinnerTimer();
205
- renderActiveTitle(ctx);
206
- }
181
+ function stopCompaction(ctx: ExtensionContext, options?: { render?: boolean }): void {
182
+ if (!isCompacting) return;
207
183
 
208
- function syncSessionTitle(ctx: ExtensionContext): void {
209
- renderActiveTitle(ctx);
210
- }
184
+ isCompacting = false;
211
185
 
212
- function markReviewSessionActive(sessionKey: string): void {
213
- activeReviewSessions.add(sessionKey);
214
- }
186
+ if (options?.render === false) return;
215
187
 
216
- function markReviewSessionInactive(sessionKey: string): void {
217
- activeReviewSessions.delete(sessionKey);
218
- }
188
+ if (isBusy()) {
189
+ renderActiveTitle(ctx);
190
+ if (!hasPendingPrompts()) {
191
+ startSpinnerTimer(ctx);
192
+ }
193
+ return;
194
+ }
219
195
 
220
- function handleReviewStart(ctx: ExtensionContext): void {
221
- renderActiveTitle(ctx);
222
- if (!hasPendingPrompts()) {
223
- startSpinnerTimer(ctx);
196
+ clearSpinnerTimer();
197
+ renderActiveTitle(ctx);
224
198
  }
225
- }
226
199
 
227
- function handleReviewEnd(ctx: ExtensionContext): void {
228
- if (hasPendingPrompts()) {
200
+ function syncSessionTitle(ctx: ExtensionContext): void {
229
201
  renderActiveTitle(ctx);
230
- return;
231
202
  }
232
203
 
233
- if (isBusy()) {
204
+ function handleBackgroundStart(ctx: ExtensionContext): void {
234
205
  renderActiveTitle(ctx);
235
- startSpinnerTimer(ctx);
236
- return;
206
+ if (!hasPendingPrompts()) {
207
+ startSpinnerTimer(ctx);
208
+ }
237
209
  }
238
210
 
239
- clearSpinnerTimer();
240
- renderActiveTitle(ctx);
241
- }
211
+ function handleBackgroundEnd(ctx: ExtensionContext): void {
212
+ if (hasPendingPrompts()) {
213
+ renderActiveTitle(ctx);
214
+ return;
215
+ }
216
+
217
+ if (isBusy()) {
218
+ renderActiveTitle(ctx);
219
+ startSpinnerTimer(ctx);
220
+ return;
221
+ }
222
+
223
+ clearSpinnerTimer();
224
+ renderActiveTitle(ctx);
225
+ }
242
226
 
243
- export default function (pi: ExtensionAPI) {
244
227
  pi.on("session_start", async (_event, ctx) => {
245
228
  if (!ctx.hasUI) return;
246
229
  latestCtx = ctx;
@@ -254,6 +237,13 @@ export default function (pi: ExtensionAPI) {
254
237
  }
255
238
  });
256
239
 
240
+ pi.on("session_info_changed", (event, ctx) => {
241
+ if (!ctx.hasUI) return;
242
+ latestCtx = ctx;
243
+ sessionName = event.name;
244
+ renderActiveTitle(ctx);
245
+ });
246
+
257
247
  pi.on("agent_start", async (_event, ctx) => {
258
248
  if (!ctx.hasUI) return;
259
249
  latestCtx = ctx;
@@ -268,16 +258,16 @@ export default function (pi: ExtensionAPI) {
268
258
  });
269
259
 
270
260
  pi.on("tool_execution_start", async (event, ctx) => {
271
- currentTool = event.toolName;
272
261
  if (!ctx.hasUI) return;
262
+ activeTools.set(event.toolCallId, event.toolName);
273
263
  latestCtx = ctx;
274
264
  if (!isWorking) return;
275
265
  renderActiveTitle(ctx);
276
266
  });
277
267
 
278
- pi.on("tool_execution_end", async (_event, ctx) => {
279
- currentTool = undefined;
268
+ pi.on("tool_execution_end", async (event, ctx) => {
280
269
  if (!ctx.hasUI) return;
270
+ activeTools.delete(event.toolCallId);
281
271
  latestCtx = ctx;
282
272
  if (!isWorking) return;
283
273
  renderActiveTitle(ctx);
@@ -295,55 +285,56 @@ export default function (pi: ExtensionAPI) {
295
285
  stopCompaction(ctx);
296
286
  });
297
287
 
298
- pi.events.on("ui:prompt_start", () => {
299
- pendingPromptCount += 1;
288
+ pi.on("session_compact_failed", async (_event, ctx) => {
289
+ if (!ctx.hasUI) return;
290
+ latestCtx = ctx;
291
+ stopCompaction(ctx);
292
+ });
300
293
 
301
- const ctx = latestCtx;
302
- if (!ctx || !ctx.hasUI) return;
294
+ pi.on("ui_prompt_start", async (_event, ctx) => {
295
+ promptPending = true;
296
+ latestCtx = ctx;
297
+ if (!ctx.hasUI) return;
303
298
  handlePromptStart(ctx);
304
299
  });
305
300
 
306
- pi.events.on("ui:prompt_end", () => {
307
- if (pendingPromptCount === 0) return;
308
- pendingPromptCount -= 1;
309
-
310
- const ctx = latestCtx;
311
- if (!ctx || !ctx.hasUI) return;
301
+ pi.on("ui_prompt_end", async (_event, ctx) => {
302
+ promptPending = false;
303
+ latestCtx = ctx;
304
+ if (!ctx.hasUI) return;
312
305
  handlePromptEnd(ctx);
313
306
  });
314
307
 
315
- pi.events.on(REVIEW_EVENT_START, (data) => {
316
- const sessionKey = extractReviewSessionKey(data);
317
- if (!sessionKey) return;
318
-
319
- markReviewSessionActive(sessionKey);
320
-
321
- const ctx = latestCtx;
322
- if (!ctx || !ctx.hasUI) return;
323
- if (currentSessionKey !== sessionKey) return;
324
- handleReviewStart(ctx);
325
- });
326
-
327
- pi.events.on(REVIEW_EVENT_END, (data) => {
328
- const sessionKey = extractReviewSessionKey(data);
329
- if (!sessionKey) return;
330
-
331
- markReviewSessionInactive(sessionKey);
332
-
333
- const ctx = latestCtx;
334
- if (!ctx || !ctx.hasUI) return;
335
- if (currentSessionKey !== sessionKey) return;
336
- handleReviewEnd(ctx);
337
- });
308
+ for (const [kind, sessions] of activeBackgroundSessions) {
309
+ pi.events.on(`${kind}:start`, (data) => {
310
+ const sessionKey = extractSessionKey(data);
311
+ if (!sessionKey) return;
312
+ sessions.add(sessionKey);
313
+
314
+ const ctx = latestCtx;
315
+ if (!ctx || !ctx.hasUI || currentSessionKey !== sessionKey) return;
316
+ handleBackgroundStart(ctx);
317
+ });
318
+
319
+ pi.events.on(`${kind}:end`, (data) => {
320
+ const sessionKey = extractSessionKey(data);
321
+ if (!sessionKey) return;
322
+ sessions.delete(sessionKey);
323
+
324
+ const ctx = latestCtx;
325
+ if (!ctx || !ctx.hasUI || currentSessionKey !== sessionKey) return;
326
+ handleBackgroundEnd(ctx);
327
+ });
328
+ }
338
329
 
339
330
  pi.on("session_shutdown", async (_event, ctx) => {
340
331
  clearSpinnerTimer();
341
332
  isWorking = false;
342
333
  isCompacting = false;
343
- currentTool = undefined;
344
- pendingPromptCount = 0;
334
+ activeTools.clear();
335
+ promptPending = false;
345
336
  const sessionKey = getSessionKey(ctx);
346
- activeReviewSessions.delete(sessionKey);
337
+ for (const sessions of activeBackgroundSessions.values()) sessions.delete(sessionKey);
347
338
  if (currentSessionKey === sessionKey) {
348
339
  currentSessionKey = undefined;
349
340
  sessionCwd = undefined;
@@ -352,3 +343,15 @@ export default function (pi: ExtensionAPI) {
352
343
  latestCtx = undefined;
353
344
  });
354
345
  }
346
+
347
+ function getSessionKey(ctx: ExtensionContext): string {
348
+ return ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
349
+ }
350
+
351
+ function extractSessionKey(data: unknown): string | undefined {
352
+ if (!data || typeof data !== "object") return undefined;
353
+ const payload = data as { sessionKey?: unknown };
354
+ if (typeof payload.sessionKey !== "string") return undefined;
355
+ const sessionKey = payload.sessionKey.trim();
356
+ return sessionKey.length > 0 ? sessionKey : undefined;
357
+ }