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
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { stripVTControlCharacters } from "node:util";
3
4
  import {
4
5
  getAgentDir,
5
6
  type AppKeybinding,
@@ -10,6 +11,7 @@ import {
10
11
  createLsToolDefinition,
11
12
  createReadToolDefinition,
12
13
  type AgentToolResult,
14
+ type BashToolDetails,
13
15
  type ExtensionAPI,
14
16
  type ExtensionContext,
15
17
  type KeybindingsManager,
@@ -20,18 +22,24 @@ import {
20
22
  } from "@earendil-works/pi-coding-agent";
21
23
  import {
22
24
  Container,
25
+ Loader,
23
26
  Text,
27
+ truncateToWidth,
24
28
  type AutocompleteProvider,
25
29
  type Component,
26
30
  type EditorComponent,
27
31
  type Focusable,
32
+ type TuiMouseEvent,
33
+ type TuiMouseEventResult,
34
+ type TUI,
28
35
  } from "@earendil-works/pi-tui";
29
36
 
30
37
  // --- Constants ---
31
38
 
32
39
  const CONFIG_FILE = "tool-display-mode.json";
33
- const MODES = ["default", "expanded", "minimal"] as const;
34
- const DEFAULT_MODE = "default";
40
+ const MODES = ["collapsed", "expanded", "minimal"] as const;
41
+ const INITIAL_MODE = MODES[0];
42
+ const ACTIVITY_WIDGET = "tool-display-activity";
35
43
 
36
44
  // These are current Pi built-in tool output messages, used only because grep/find/ls
37
45
  // do not expose structured zero-result details yet.
@@ -39,6 +47,7 @@ const GREP_NO_MATCHES_OUTPUT = "No matches found"; // core/tools/grep.ts
39
47
  const FIND_NO_MATCHES_OUTPUT = "No files found matching pattern"; // core/tools/find.ts
40
48
  const LS_EMPTY_DIRECTORY_OUTPUT = "(empty directory)"; // core/tools/ls.ts
41
49
 
50
+ // PowerShell is intentionally omitted because Tau does not officially support Windows.
42
51
  const TOOL_FACTORIES = {
43
52
  read: createReadToolDefinition,
44
53
  bash: createBashToolDefinition,
@@ -55,13 +64,17 @@ type AnyToolDefinition = ToolDefinition<any, any, any>;
55
64
  type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
56
65
  type JsonObject = Record<string, unknown>;
57
66
  type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
67
+ type WorkingIndicator = NonNullable<Parameters<CustomEditor["setWorkingStatusIndicator"]>[0]>;
68
+ type ActivitySource = "subagent" | "review";
69
+ type ActivityState = { text: string; idle: boolean };
58
70
 
59
71
  type ToolDisplayModeConfig = {
60
72
  mode: Mode;
61
73
  };
62
74
 
63
75
  type CustomEditorLike = EditorComponent &
64
- Partial<Focusable> & {
76
+ Partial<Focusable> &
77
+ Partial<Pick<CustomEditor, "embedWorkingStatus" | "setWorkingStatusIndicator">> & {
65
78
  actionHandlers?: Map<AppKeybinding, () => void>;
66
79
  onEscape?: () => void;
67
80
  onCtrlD?: () => void;
@@ -73,7 +86,7 @@ type CustomEditorLike = EditorComponent &
73
86
  // --- Config ---
74
87
 
75
88
  function emptyConfig(): ToolDisplayModeConfig {
76
- return { mode: DEFAULT_MODE };
89
+ return { mode: INITIAL_MODE };
77
90
  }
78
91
 
79
92
  function getConfigPath(): string {
@@ -99,7 +112,7 @@ function parseConfig(value: unknown): ToolDisplayModeConfig {
99
112
  if (!isObject(value)) return emptyConfig();
100
113
 
101
114
  return {
102
- mode: parseMode(value.mode) ?? DEFAULT_MODE,
115
+ mode: parseMode(value.mode) ?? INITIAL_MODE,
103
116
  };
104
117
  }
105
118
 
@@ -126,61 +139,39 @@ async function saveConfig(config: ToolDisplayModeConfig): Promise<void> {
126
139
 
127
140
  // --- Tool definitions ---
128
141
 
129
- function getToolDefinition(
130
- cache: Map<string, Partial<Record<ToolName, AnyToolDefinition>>>,
131
- name: ToolName,
132
- cwd: string,
133
- ): AnyToolDefinition {
134
- let tools = cache.get(cwd);
135
- if (!tools) {
136
- tools = {};
137
- cache.set(cwd, tools);
138
- }
139
-
140
- return (tools[name] ??= TOOL_FACTORIES[name](cwd));
141
- }
142
-
143
142
  function shouldRegisterToolRenderer(tools: ToolInfo[], name: ToolName): boolean {
144
143
  const existingTool = tools.find((tool) => tool.name === name);
145
144
  return existingTool?.sourceInfo.source === "builtin";
146
145
  }
147
146
 
148
147
  function createToolDisplayDefinition(options: {
149
- cache: Map<string, Partial<Record<ToolName, AnyToolDefinition>>>;
150
148
  name: ToolName;
151
149
  getMode: () => Mode;
152
150
  }): AnyToolDefinition {
153
- const { cache, name, getMode } = options;
154
- const base = getToolDefinition(cache, name, process.cwd());
151
+ const { name, getMode } = options;
152
+ const base: AnyToolDefinition = TOOL_FACTORIES[name](process.cwd());
155
153
 
156
154
  return {
157
155
  ...base,
158
156
 
159
- async execute(toolCallId, params, signal, onUpdate, ctx) {
160
- return getToolDefinition(cache, name, ctx.cwd).execute(
161
- toolCallId,
162
- params,
163
- signal,
164
- onUpdate,
165
- ctx,
166
- );
167
- },
168
-
169
- renderCall(args, theme, context) {
170
- const renderer = getToolDefinition(cache, name, context.cwd).renderCall;
171
- return renderer?.(args, theme, { ...context, lastComponent: undefined }) ?? emptyComponent();
172
- },
173
-
174
157
  renderResult(result, options, theme, context) {
175
158
  const mode = getMode();
176
- const renderer = getToolDefinition(cache, name, context.cwd).renderResult;
159
+ const renderer = base.renderResult;
160
+ const previous = context.lastComponent;
177
161
 
178
162
  if (mode === "minimal") {
163
+ const component =
164
+ previous instanceof MinimalResultComponent
165
+ ? previous
166
+ : new MinimalResultComponent(previous);
179
167
  if (name === "bash") {
180
- renderer?.(result, options, theme, { ...context, lastComponent: undefined });
168
+ component.nativeComponent = renderer?.(result, options, theme, {
169
+ ...context,
170
+ lastComponent: component.nativeComponent,
171
+ });
181
172
  }
182
-
183
- return renderMinimalResult(name, result, options, theme, context);
173
+ component.setText(formatMinimalResult(name, result, options, theme, context));
174
+ return component;
184
175
  }
185
176
 
186
177
  const expanded = mode === "expanded";
@@ -188,7 +179,8 @@ function createToolDisplayDefinition(options: {
188
179
  renderer?.(result, { ...options, expanded }, theme, {
189
180
  ...context,
190
181
  expanded,
191
- lastComponent: undefined,
182
+ lastComponent:
183
+ previous instanceof MinimalResultComponent ? previous.nativeComponent : previous,
192
184
  }) ?? emptyComponent()
193
185
  );
194
186
  },
@@ -199,7 +191,7 @@ function createToolDisplayDefinition(options: {
199
191
 
200
192
  function nextMode(currentMode: Mode): Mode {
201
193
  const index = MODES.indexOf(currentMode);
202
- return MODES[(index + 1) % MODES.length] ?? DEFAULT_MODE;
194
+ return MODES[(index + 1) % MODES.length] ?? INITIAL_MODE;
203
195
  }
204
196
 
205
197
  function applyMode(ctx: ExtensionContext, mode: Mode): void {
@@ -226,19 +218,25 @@ function reportSaveError(ctx: ExtensionContext, error: unknown): void {
226
218
 
227
219
  // --- Minimal rendering ---
228
220
 
229
- function renderMinimalResult(
221
+ class MinimalResultComponent extends Text {
222
+ constructor(public nativeComponent: Component | undefined) {
223
+ super("", 0, 0);
224
+ }
225
+ }
226
+
227
+ function formatMinimalResult(
230
228
  name: ToolName,
231
229
  result: AgentToolResult<any>,
232
230
  options: ToolRenderResultOptions,
233
231
  theme: Theme,
234
232
  context: AnyToolRenderContext,
235
- ): Component {
233
+ ): string {
236
234
  if (context.isError) {
237
- return renderMinimalText(theme, "error", lastNonEmptyLine(textOutput(result)) ?? "error");
235
+ return theme.fg("error", lastNonEmptyLine(textOutput(result)) ?? "error");
238
236
  }
239
237
 
240
238
  if (options.isPartial) {
241
- return renderMinimalText(theme, "muted", "running...");
239
+ return theme.fg("muted", "running...");
242
240
  }
243
241
 
244
242
  let summary: string;
@@ -260,18 +258,15 @@ function renderMinimalResult(
260
258
  break;
261
259
  }
262
260
 
263
- return renderMinimalText(theme, "muted", `↳ ${summary}`);
264
- }
265
-
266
- function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
267
- return new Text(theme.fg(color, text), 0, 0);
261
+ return theme.fg("muted", `↳ ${summary}`);
268
262
  }
269
263
 
270
264
  function bashSummary(result: AgentToolResult<any>): string {
271
- const text = stripTrailingNotice(textOutput(result)).trim();
265
+ const text = textOutput(result).trim();
272
266
  if (!text || text === "(no output)") return "no output";
273
267
 
274
- const lines = countLines(text);
268
+ const details = result.details as BashToolDetails | undefined;
269
+ const lines = details?.truncation?.outputLines ?? countLines(text);
275
270
  return `${lines} ${plural(lines, "line")}`;
276
271
  }
277
272
 
@@ -337,7 +332,7 @@ function lastNonEmptyLine(text: string): string | undefined {
337
332
  }
338
333
 
339
334
  function plural(count: number, singular: string): string {
340
- return count === 1 ? singular : `${singular}s`;
335
+ return count === 1 ? singular : singular === "entry" ? "entries" : `${singular}s`;
341
336
  }
342
337
 
343
338
  function emptyComponent(): Container {
@@ -346,6 +341,28 @@ function emptyComponent(): Container {
346
341
 
347
342
  // --- Editor ---
348
343
 
344
+ /** Adapt public Loader output to CustomEditor's native border layout. Check parity on Pi upgrades. */
345
+ class BackgroundWorkingIndicator extends Loader implements WorkingIndicator {
346
+ readonly kind = "working";
347
+
348
+ dispose(): void {
349
+ this.stop();
350
+ }
351
+
352
+ renderInBorder(width: number): string {
353
+ const line = super.render(width + 2)[1] ?? "";
354
+ return truncateToWidth(
355
+ line.startsWith(" ") ? line.slice(1).trimEnd() : line.trimEnd(),
356
+ width,
357
+ "",
358
+ );
359
+ }
360
+
361
+ renderSpinnerInBorder(width: number): string {
362
+ return truncateToWidth(this.getRenderedIndicator(), width, "");
363
+ }
364
+ }
365
+
349
366
  class ToolDisplayEditor implements EditorComponent, Focusable {
350
367
  readonly actionHandlers: Map<AppKeybinding, () => void>;
351
368
  private readonly customBase: CustomEditorLike;
@@ -354,11 +371,19 @@ class ToolDisplayEditor implements EditorComponent, Focusable {
354
371
  private fallbackOnCtrlD?: () => void;
355
372
  private fallbackOnPasteImage?: () => void;
356
373
  private fallbackOnExtensionShortcut?: (data: string) => boolean;
374
+ private nativeIndicator: WorkingIndicator | undefined;
375
+ private backgroundIndicator: BackgroundWorkingIndicator | undefined;
376
+ private backgroundMessage: string | undefined;
377
+ private workingMessage: string | undefined;
357
378
 
358
379
  constructor(
359
380
  private readonly base: EditorComponent,
360
381
  private readonly appKeybindings: KeybindingsManager,
361
382
  private readonly cycleMode: () => void,
383
+ private readonly tui: TUI,
384
+ private readonly getActivity: () => ActivityState | undefined,
385
+ private readonly setWorkingMessage: (message?: string) => void,
386
+ private readonly statusColor: (text: string) => string,
362
387
  ) {
363
388
  this.customBase = base as CustomEditorLike;
364
389
  this.actionHandlers = this.customBase.actionHandlers ?? new Map();
@@ -393,6 +418,62 @@ class ToolDisplayEditor implements EditorComponent, Focusable {
393
418
  return this.base.wantsKeyRelease;
394
419
  }
395
420
 
421
+ get embedWorkingStatus(): boolean {
422
+ return (
423
+ this.customBase.embedWorkingStatus === true &&
424
+ typeof this.customBase.setWorkingStatusIndicator === "function"
425
+ );
426
+ }
427
+
428
+ setWorkingStatusIndicator(
429
+ indicator: Parameters<CustomEditor["setWorkingStatusIndicator"]>[0],
430
+ ): void {
431
+ this.nativeIndicator = indicator;
432
+ this.refreshActivity();
433
+ }
434
+
435
+ refreshActivity(): boolean {
436
+ const activity = this.embedWorkingStatus ? this.getActivity() : undefined;
437
+ const nativeWorking = this.nativeIndicator?.kind === "working";
438
+ const foregroundMessage = activity && nativeWorking ? `Working, ${activity.text}` : undefined;
439
+ if (foregroundMessage !== this.workingMessage) {
440
+ this.workingMessage = foregroundMessage;
441
+ this.setWorkingMessage(foregroundMessage);
442
+ }
443
+
444
+ const backgroundMessage = activity?.idle && !this.nativeIndicator ? activity.text : undefined;
445
+ if (backgroundMessage) {
446
+ if (!this.backgroundIndicator) {
447
+ this.backgroundIndicator = new BackgroundWorkingIndicator(
448
+ this.tui,
449
+ this.statusColor,
450
+ this.statusColor,
451
+ backgroundMessage,
452
+ );
453
+ } else if (backgroundMessage !== this.backgroundMessage) {
454
+ this.backgroundIndicator.setMessage(backgroundMessage);
455
+ }
456
+ } else {
457
+ this.backgroundIndicator?.dispose();
458
+ this.backgroundIndicator = undefined;
459
+ }
460
+ this.backgroundMessage = backgroundMessage;
461
+ this.customBase.setWorkingStatusIndicator?.(this.nativeIndicator ?? this.backgroundIndicator);
462
+ return Boolean(activity && (nativeWorking || this.backgroundIndicator));
463
+ }
464
+
465
+ disposeActivity(): void {
466
+ const hadBackground = this.backgroundIndicator !== undefined;
467
+ this.backgroundIndicator?.dispose();
468
+ this.backgroundIndicator = undefined;
469
+ this.backgroundMessage = undefined;
470
+ if (this.workingMessage !== undefined) {
471
+ this.workingMessage = undefined;
472
+ this.setWorkingMessage();
473
+ }
474
+ if (hadBackground) this.customBase.setWorkingStatusIndicator?.(undefined);
475
+ }
476
+
396
477
  get borderColor(): ((str: string) => string) | undefined {
397
478
  return this.base.borderColor;
398
479
  }
@@ -470,10 +551,12 @@ class ToolDisplayEditor implements EditorComponent, Focusable {
470
551
  }
471
552
 
472
553
  render(width: number): string[] {
554
+ this.refreshActivity();
473
555
  return this.base.render(width);
474
556
  }
475
557
 
476
558
  invalidate(): void {
559
+ this.backgroundIndicator?.invalidate();
477
560
  this.base.invalidate();
478
561
  }
479
562
 
@@ -485,24 +568,99 @@ class ToolDisplayEditor implements EditorComponent, Focusable {
485
568
 
486
569
  this.base.handleInput(data);
487
570
  }
571
+
572
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
573
+ return this.base.handleMouse?.(event);
574
+ }
488
575
  }
489
576
 
490
577
  export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
491
- let mode: Mode = DEFAULT_MODE;
578
+ let mode: Mode = INITIAL_MODE;
492
579
  let registeredToolRenderers = false;
493
580
  let installedEditorFactory: EditorFactory | undefined;
494
581
  let previousEditorFactory: EditorFactory | undefined;
495
- const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
582
+ let editor: ToolDisplayEditor | undefined;
583
+ let context: ExtensionContext | undefined;
584
+ let sessionKey: string | undefined;
585
+ let activityView: object | undefined;
586
+ let requestRender: (() => void) | undefined;
587
+ let promptActive = false;
588
+ let compacting = false;
589
+ const activities = new Map<ActivitySource, string>();
590
+
591
+ const getActivity = (): ActivityState | undefined => {
592
+ if (
593
+ !context ||
594
+ !activityView ||
595
+ context.ui.getEditorComponent() !== installedEditorFactory ||
596
+ context.ui.getToolsExpanded() ||
597
+ promptActive ||
598
+ compacting
599
+ )
600
+ return undefined;
601
+ const text = [activities.get("subagent"), activities.get("review")].filter(Boolean).join(", ");
602
+ return text ? { text, idle: context.isIdle() } : undefined;
603
+ };
604
+
605
+ const refreshEditorActivity = (): boolean => {
606
+ if (context && context.ui.getEditorComponent() !== installedEditorFactory) {
607
+ editor?.disposeActivity();
608
+ editor = undefined;
609
+ }
610
+ return editor?.refreshActivity() ?? false;
611
+ };
612
+
613
+ const refreshActivity = (): void => {
614
+ refreshEditorActivity();
615
+ requestRender?.();
616
+ };
617
+
618
+ pi.events.on("tau:activity", (data) => {
619
+ if (!context || !isObject(data) || data.sessionKey !== sessionKey) return;
620
+ if (data.source !== "subagent" && data.source !== "review") return;
621
+ if (data.text !== undefined && typeof data.text !== "string") return;
622
+ const text =
623
+ typeof data.text === "string"
624
+ ? stripVTControlCharacters(data.text).replace(/\s+/g, " ").trim()
625
+ : undefined;
626
+ const changed = activities.get(data.source) !== (text || undefined);
627
+ if (text) activities.set(data.source, text);
628
+ else activities.delete(data.source);
629
+ data.handled = refreshEditorActivity();
630
+ if (changed) requestRender?.();
631
+ });
632
+ pi.on("agent_start", refreshActivity);
633
+ pi.on("agent_settled", refreshActivity);
634
+ pi.on("ui_prompt_start", () => {
635
+ promptActive = true;
636
+ refreshActivity();
637
+ });
638
+ pi.on("ui_prompt_end", () => {
639
+ promptActive = false;
640
+ refreshActivity();
641
+ });
642
+ pi.on("session_before_compact", () => {
643
+ compacting = true;
644
+ refreshActivity();
645
+ });
646
+ const finishCompaction = (): void => {
647
+ compacting = false;
648
+ refreshActivity();
649
+ };
650
+ pi.on("session_compact", finishCompaction);
651
+ pi.on("session_compact_failed", finishCompaction);
496
652
 
497
653
  const setMode = (ctx: ExtensionContext, next: Mode): void => {
498
654
  mode = next;
499
655
  applyMode(ctx, mode);
656
+ refreshActivity();
500
657
  showModeChange(ctx, mode);
501
658
 
502
659
  void saveConfig({ mode }).catch((error) => reportSaveError(ctx, error));
503
660
  };
504
661
 
505
662
  pi.on("session_start", async (_event, ctx) => {
663
+ if (!ctx.hasUI) return;
506
664
  mode = (await loadConfig()).mode;
507
665
 
508
666
  if (!registeredToolRenderers) {
@@ -510,29 +668,74 @@ export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
510
668
  for (const name of Object.keys(TOOL_FACTORIES) as ToolName[]) {
511
669
  if (!shouldRegisterToolRenderer(tools, name)) continue;
512
670
 
513
- pi.registerTool(
514
- createToolDisplayDefinition({ cache: toolCache, name, getMode: () => mode }),
515
- );
671
+ pi.registerTool(createToolDisplayDefinition({ name, getMode: () => mode }));
516
672
  }
517
673
  registeredToolRenderers = true;
518
674
  }
519
675
 
520
676
  applyMode(ctx, mode);
521
- if (!ctx.hasUI) return;
522
677
 
523
- if (ctx.ui.getEditorComponent() === installedEditorFactory) return;
678
+ if (installedEditorFactory && ctx.ui.getEditorComponent() === installedEditorFactory) return;
524
679
 
680
+ context = ctx.mode === "tui" ? ctx : undefined;
681
+ sessionKey =
682
+ ctx.sessionManager.getSessionFile() ?? `session:${ctx.sessionManager.getSessionId()}`;
525
683
  previousEditorFactory = ctx.ui.getEditorComponent();
526
684
  installedEditorFactory = (tui, theme, keybindings) => {
685
+ editor?.disposeActivity();
527
686
  const baseEditor =
528
687
  previousEditorFactory?.(tui, theme, keybindings) ??
529
- new CustomEditor(tui, theme, keybindings);
530
- return new ToolDisplayEditor(baseEditor, keybindings, () => setMode(ctx, nextMode(mode)));
688
+ new CustomEditor(tui, theme, keybindings, { embedWorkingStatus: true });
689
+ editor = new ToolDisplayEditor(
690
+ baseEditor,
691
+ keybindings,
692
+ () => setMode(ctx, nextMode(mode)),
693
+ tui,
694
+ getActivity,
695
+ (message) => ctx.ui.setWorkingMessage(message),
696
+ (text) =>
697
+ (
698
+ baseEditor.borderColor ??
699
+ ctx.ui.theme.getThinkingBorderColor(ctx.thinkingLevel ?? "off")
700
+ )(text),
701
+ );
702
+ return editor;
531
703
  };
532
704
  ctx.ui.setEditorComponent(installedEditorFactory);
705
+ if (ctx.mode === "tui") {
706
+ ctx.ui.setWidget(ACTIVITY_WIDGET, (tui) => {
707
+ const owner = {};
708
+ activityView = owner;
709
+ requestRender = () => tui.requestRender();
710
+ return {
711
+ // Observe native redraws for expansion changes and replacement editors without polling.
712
+ render() {
713
+ if (activityView === owner) refreshEditorActivity();
714
+ return [];
715
+ },
716
+ invalidate() {},
717
+ dispose() {
718
+ if (activityView !== owner) return;
719
+ activityView = undefined;
720
+ requestRender = undefined;
721
+ editor?.disposeActivity();
722
+ },
723
+ };
724
+ });
725
+ }
533
726
  });
534
727
 
535
728
  pi.on("session_shutdown", async (_event, ctx) => {
729
+ activityView = undefined;
730
+ editor?.disposeActivity();
731
+ if (context) context.ui.setWidget(ACTIVITY_WIDGET, undefined);
732
+ context = undefined;
733
+ sessionKey = undefined;
734
+ activities.clear();
735
+ promptActive = false;
736
+ compacting = false;
737
+ requestRender = undefined;
738
+ editor = undefined;
536
739
  if (ctx.hasUI && ctx.ui.getEditorComponent() === installedEditorFactory) {
537
740
  ctx.ui.setEditorComponent(previousEditorFactory);
538
741
  }