@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.1

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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -20,7 +20,7 @@ import {
20
20
  modelsAreEqual,
21
21
  type UsageReport,
22
22
  } from "@oh-my-pi/pi-ai";
23
- import type { Component, EditorTheme, SlashCommand } from "@oh-my-pi/pi-tui";
23
+ import type { Component, EditorTheme, LoaderMessageColorFn, OverlayHandle, SlashCommand } from "@oh-my-pi/pi-tui";
24
24
  import {
25
25
  Container,
26
26
  clearRenderCache,
@@ -65,12 +65,7 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
65
65
  import type { Goal, GoalModeState } from "../goals/state";
66
66
  import { resolveLocalUrlToPath } from "../internal-urls";
67
67
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
68
- import {
69
- humanizePlanTitle,
70
- type PlanApprovalDetails,
71
- renameApprovedPlanFile,
72
- resolvePlanTitle,
73
- } from "../plan-mode/approved-plan";
68
+ import { humanizePlanTitle, type PlanApprovalDetails, resolveApprovedPlan } from "../plan-mode/approved-plan";
74
69
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
75
70
  import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
76
71
  type: "text",
@@ -94,6 +89,7 @@ import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-colo
94
89
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
95
90
  import type { AssistantMessageComponent } from "./components/assistant-message";
96
91
  import type { BashExecutionComponent } from "./components/bash-execution";
92
+ import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
97
93
  import { CustomEditor } from "./components/custom-editor";
98
94
  import { DynamicBorder } from "./components/dynamic-border";
99
95
  import { ErrorBannerComponent } from "./components/error-banner";
@@ -101,6 +97,7 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
101
97
  import type { HookEditorComponent } from "./components/hook-editor";
102
98
  import type { HookInputComponent } from "./components/hook-input";
103
99
  import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
100
+ import { PlanReviewOverlay } from "./components/plan-review-overlay";
104
101
  import { StatusLineComponent } from "./components/status-line";
105
102
  import type { ToolExecutionHandle } from "./components/tool-execution";
106
103
  import { TranscriptContainer } from "./components/transcript-container";
@@ -114,6 +111,7 @@ import { MCPCommandController } from "./controllers/mcp-command-controller";
114
111
  import { OmfgController } from "./controllers/omfg-controller";
115
112
  import { SelectorController } from "./controllers/selector-controller";
116
113
  import { SSHCommandController } from "./controllers/ssh-command-controller";
114
+ import { TanCommandController } from "./controllers/tan-command-controller";
117
115
  import { TodoCommandController } from "./controllers/todo-command-controller";
118
116
  import {
119
117
  consumeLoopLimitIteration,
@@ -250,20 +248,6 @@ export interface InteractiveModeOptions {
250
248
  initialMessages?: string[];
251
249
  }
252
250
 
253
- /**
254
- * Plan-review preview block. Once rendered it is static (a one-shot Markdown of
255
- * the plan file), so even while it sits as the live bottom block beneath the
256
- * approval selector its scrolled-off head is safe to commit to native
257
- * scrollback. Reporting append-only lets an over-tall plan + selector commit the
258
- * plan's head instead of clipping it — without this a plain {@link Container} is
259
- * deferred and a long plan is cut off the top on ED3-risk terminals.
260
- */
261
- class PlanReviewBlock extends Container {
262
- isTranscriptBlockAppendOnly(): boolean {
263
- return true;
264
- }
265
- }
266
-
267
251
  export class InteractiveMode implements InteractiveModeContext {
268
252
  session: AgentSession;
269
253
  sessionManager: SessionManager;
@@ -287,7 +271,6 @@ export class InteractiveMode implements InteractiveModeContext {
287
271
  statusLine: StatusLineComponent;
288
272
 
289
273
  isInitialized = false;
290
- isBackgrounded = false;
291
274
  isBashMode = false;
292
275
  toolOutputExpanded = false;
293
276
  todoExpanded = false;
@@ -355,12 +338,14 @@ export class InteractiveMode implements InteractiveModeContext {
355
338
  #planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
356
339
  #pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
357
340
  #planModeHasEntered = false;
358
- #planReviewContainer: Container | undefined;
341
+ #planReviewOverlay: PlanReviewOverlay | undefined;
342
+ #planReviewOverlayHandle: OverlayHandle | undefined;
359
343
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
360
344
  mcpManager?: import("../mcp").MCPManager;
361
345
  readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
362
346
 
363
347
  readonly #btwController: BtwController;
348
+ readonly #tanCommandController: TanCommandController;
364
349
  readonly #omfgController: OmfgController;
365
350
  readonly #commandController: CommandController;
366
351
  readonly #todoCommandController: TodoCommandController;
@@ -379,6 +364,7 @@ export class InteractiveMode implements InteractiveModeContext {
379
364
  #eventBus?: EventBus;
380
365
  #eventBusUnsubscribers: Array<() => void> = [];
381
366
  #welcomeComponent?: WelcomeComponent;
367
+ readonly #chatHost: ChatBlockHost = { requestRender: () => this.ui.requestRender() };
382
368
 
383
369
  constructor(
384
370
  session: AgentSession,
@@ -484,6 +470,7 @@ export class InteractiveMode implements InteractiveModeContext {
484
470
 
485
471
  this.#uiHelpers = new UiHelpers(this);
486
472
  this.#btwController = new BtwController(this);
473
+ this.#tanCommandController = new TanCommandController(this);
487
474
  this.#omfgController = new OmfgController(this);
488
475
  this.#extensionUiController = new ExtensionUiController(this);
489
476
  this.#eventController = new EventController(this);
@@ -613,8 +600,9 @@ export class InteractiveMode implements InteractiveModeContext {
613
600
  // Load initial todos
614
601
  await this.#loadTodoList();
615
602
 
616
- // Start the UI
617
- this.ui.start();
603
+ // Start the UI. Cold `omp` launch opts into clearing on the first paint so
604
+ // the initial welcome frame does not append over the previous run's scrollback.
605
+ this.ui.start({ clearScrollback: options.clearInitialTerminalHistory === true });
618
606
  pushTerminalTitle();
619
607
  setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
620
608
  this.updateEditorBorderColor();
@@ -1536,22 +1524,15 @@ export class InteractiveMode implements InteractiveModeContext {
1536
1524
  if (!state?.enabled) {
1537
1525
  throw new ToolError("Plan mode is not active.");
1538
1526
  }
1539
- const planFilePath = state.planFilePath;
1540
- const planContent = await this.#readPlanFile(planFilePath);
1541
- if (planContent === null) {
1542
- throw new ToolError(
1543
- `Plan file not found at ${planFilePath}. Write the finalized plan to ${planFilePath} before requesting approval.`,
1544
- );
1545
- }
1546
- const normalized = resolvePlanTitle({
1527
+ const { planFilePath, title } = await resolveApprovedPlan({
1547
1528
  suppliedTitle: extra?.title,
1548
- planContent,
1549
- planFilePath,
1529
+ statePlanFilePath: state.planFilePath,
1530
+ readPlan: url => this.#readPlanFile(url),
1531
+ listPlanFiles: () => this.#listLocalPlanFiles(),
1550
1532
  });
1551
1533
  const details: PlanApprovalDetails = {
1552
1534
  planFilePath,
1553
- finalPlanFilePath: `local://${normalized.fileName}`,
1554
- title: normalized.title,
1535
+ title,
1555
1536
  planExists: true,
1556
1537
  };
1557
1538
  return {
@@ -1691,22 +1672,87 @@ export class InteractiveMode implements InteractiveModeContext {
1691
1672
  }
1692
1673
  }
1693
1674
 
1694
- #renderPlanPreview(planContent: string, options?: { append?: boolean }): void {
1695
- const existingContainer = this.#planReviewContainer;
1696
- const replaceExisting = options?.append !== true && existingContainer !== undefined;
1697
- const planReviewContainer = replaceExisting ? existingContainer : new PlanReviewBlock();
1698
- planReviewContainer.clear();
1699
- planReviewContainer.addChild(new Spacer(1));
1700
- planReviewContainer.addChild(new DynamicBorder());
1701
- planReviewContainer.addChild(new Text(theme.bold(theme.fg("accent", "Plan Review")), 1, 1));
1702
- planReviewContainer.addChild(new Spacer(1));
1703
- planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
1704
- planReviewContainer.addChild(new DynamicBorder());
1705
- if (!replaceExisting) {
1706
- this.chatContainer.addChild(planReviewContainer);
1675
+ /** `local://` URLs of plan files in the session-local root, newest first.
1676
+ * A fallback for `resolveApprovedPlan` when the agent dropped `extra.title`,
1677
+ * so the plan it wrote is still found by scanning recent `*-plan.md` files. */
1678
+ async #listLocalPlanFiles(): Promise<string[]> {
1679
+ const localRoot = this.#resolvePlanFilePath("local://");
1680
+ try {
1681
+ const entries = await fs.readdir(localRoot, { withFileTypes: true });
1682
+ const plans = await Promise.all(
1683
+ entries
1684
+ .filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
1685
+ .map(async name => {
1686
+ const stat = await fs.stat(path.join(localRoot, name.name)).catch(() => null);
1687
+ return { url: `local://${name.name}`, mtime: stat?.mtimeMs ?? 0 };
1688
+ }),
1689
+ );
1690
+ return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
1691
+ } catch {
1692
+ return [];
1707
1693
  }
1708
- this.#planReviewContainer = planReviewContainer;
1694
+ }
1695
+
1696
+ showPlanReview(
1697
+ planContent: string,
1698
+ title: string,
1699
+ options: string[],
1700
+ dialogOptions?: {
1701
+ helpText?: string;
1702
+ disabledIndices?: number[];
1703
+ onExternalEditor?: () => void;
1704
+ onPlanEdited?: (content: string) => void;
1705
+ onFeedbackChange?: (feedback: string) => void;
1706
+ initialIndex?: number;
1707
+ },
1708
+ extra?: { slider?: HookSelectorSlider },
1709
+ ): Promise<string | undefined> {
1710
+ this.#hidePlanReview();
1711
+ const { promise, resolve } = Promise.withResolvers<string | undefined>();
1712
+ let settled = false;
1713
+ const finish = (choice: string | undefined): void => {
1714
+ if (settled) return;
1715
+ settled = true;
1716
+ this.#hidePlanReview();
1717
+ this.ui.requestRender();
1718
+ resolve(choice);
1719
+ };
1720
+ const overlay = new PlanReviewOverlay(
1721
+ planContent,
1722
+ {
1723
+ promptTitle: title,
1724
+ options,
1725
+ disabledIndices: dialogOptions?.disabledIndices,
1726
+ helpText: dialogOptions?.helpText,
1727
+ initialIndex: dialogOptions?.initialIndex,
1728
+ slider: extra?.slider,
1729
+ externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
1730
+ },
1731
+ {
1732
+ onPick: choice => finish(choice),
1733
+ onCancel: () => finish(undefined),
1734
+ onExternalEditor: dialogOptions?.onExternalEditor,
1735
+ onPlanEdited: dialogOptions?.onPlanEdited,
1736
+ onFeedbackChange: dialogOptions?.onFeedbackChange,
1737
+ },
1738
+ );
1739
+ this.#planReviewOverlay = overlay;
1740
+ this.#planReviewOverlayHandle = this.ui.showOverlay(overlay, {
1741
+ anchor: "bottom-center",
1742
+ width: "100%",
1743
+ maxHeight: "100%",
1744
+ margin: 0,
1745
+ fullscreen: true,
1746
+ });
1747
+ this.ui.setFocus(overlay);
1709
1748
  this.ui.requestRender();
1749
+ return promise;
1750
+ }
1751
+
1752
+ #hidePlanReview(): void {
1753
+ this.#planReviewOverlayHandle?.hide();
1754
+ this.#planReviewOverlayHandle = undefined;
1755
+ this.#planReviewOverlay = undefined;
1710
1756
  }
1711
1757
 
1712
1758
  #getEditorTerminalPath(): string | null {
@@ -1728,14 +1774,6 @@ export class InteractiveMode implements InteractiveModeContext {
1728
1774
  }
1729
1775
  }
1730
1776
 
1731
- #getPlanReviewHelpText(): string {
1732
- const externalEditorKey = this.keybindings.getDisplayString("app.editor.external");
1733
- if (!externalEditorKey) {
1734
- return "up/down navigate enter select esc cancel";
1735
- }
1736
- return `up/down navigate enter select ${externalEditorKey.toLowerCase()} open in editor esc cancel`;
1737
- }
1738
-
1739
1777
  #getPlanApprovalContextUsage(): ContextUsage | undefined {
1740
1778
  const executionModel = this.#planModePreviousModelState?.model ?? this.session.model;
1741
1779
  const contextWindow = executionModel?.contextWindow;
@@ -1794,7 +1832,7 @@ export class InteractiveMode implements InteractiveModeContext {
1794
1832
  });
1795
1833
  if (result !== null) {
1796
1834
  await Bun.write(resolvedPath, result);
1797
- this.#renderPlanPreview(result);
1835
+ this.#planReviewOverlay?.setPlanContent(result);
1798
1836
  this.showStatus("Plan updated in external editor.");
1799
1837
  }
1800
1838
  } catch (error) {
@@ -1826,19 +1864,12 @@ export class InteractiveMode implements InteractiveModeContext {
1826
1864
  planContent: string,
1827
1865
  options: {
1828
1866
  planFilePath: string;
1829
- finalPlanFilePath: string;
1830
1867
  title: string;
1831
1868
  preserveContext?: boolean;
1832
1869
  compactBeforeExecute?: boolean;
1833
1870
  executionModel?: ResolvedRoleModel;
1834
1871
  },
1835
1872
  ): Promise<void> {
1836
- await renameApprovedPlanFile({
1837
- planFilePath: options.planFilePath,
1838
- finalPlanFilePath: options.finalPlanFilePath,
1839
- getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
1840
- getSessionId: () => this.sessionManager.getSessionId(),
1841
- });
1842
1873
  const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
1843
1874
 
1844
1875
  // Mark the pending abort caused by the plan-mode → compaction transition as
@@ -1857,8 +1888,8 @@ export class InteractiveMode implements InteractiveModeContext {
1857
1888
  if (!options.preserveContext) {
1858
1889
  await this.handleClearCommand();
1859
1890
  // The new session has a fresh local:// root — persist the approved plan there
1860
- // so `local://<title>.md` resolves correctly in the execution session.
1861
- const newLocalPath = resolveLocalUrlToPath(options.finalPlanFilePath, {
1891
+ // so `local://<slug>-plan.md` resolves correctly in the execution session.
1892
+ const newLocalPath = resolveLocalUrlToPath(options.planFilePath, {
1862
1893
  getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
1863
1894
  getSessionId: () => this.sessionManager.getSessionId(),
1864
1895
  });
@@ -1872,7 +1903,7 @@ export class InteractiveMode implements InteractiveModeContext {
1872
1903
  // Cancellation skips the synthetic-prompt dispatch (operator's explicit
1873
1904
  // abort is honored); failure proceeds best-effort — approval intent stands.
1874
1905
  const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
1875
- planFilePath: options.finalPlanFilePath,
1906
+ planFilePath: options.planFilePath,
1876
1907
  });
1877
1908
  // Pin the plan reference path BEFORE compaction so any user messages
1878
1909
  // queued during the compaction await (which `handleCompactCommand`
@@ -1880,7 +1911,7 @@ export class InteractiveMode implements InteractiveModeContext {
1880
1911
  // approved plan in `#buildPlanReferenceMessage`. Reassignment after
1881
1912
  // the try/finally is idempotent and kept for the !compactBeforeExecute
1882
1913
  // branch.
1883
- this.session.setPlanReferencePath(options.finalPlanFilePath);
1914
+ this.session.setPlanReferencePath(options.planFilePath);
1884
1915
  compactOutcome = await this.handleCompactCommand(compactionPrompt);
1885
1916
  }
1886
1917
  } finally {
@@ -1896,7 +1927,7 @@ export class InteractiveMode implements InteractiveModeContext {
1896
1927
  if (previousTools.length > 0) {
1897
1928
  await this.session.setActiveToolsByName(previousTools);
1898
1929
  }
1899
- this.session.setPlanReferencePath(options.finalPlanFilePath);
1930
+ this.session.setPlanReferencePath(options.planFilePath);
1900
1931
 
1901
1932
  if (compactOutcome === "cancelled") {
1902
1933
  // Explicit abort: honor it. `executeCompaction` already surfaced
@@ -1933,7 +1964,7 @@ export class InteractiveMode implements InteractiveModeContext {
1933
1964
  this.session.markPlanReferenceSent();
1934
1965
  const planModePrompt = prompt.render(planModeApprovedPrompt, {
1935
1966
  planContent,
1936
- finalPlanFilePath: options.finalPlanFilePath,
1967
+ planFilePath: options.planFilePath,
1937
1968
  contextPreserved: options.preserveContext === true,
1938
1969
  });
1939
1970
  await this.session.prompt(planModePrompt, { synthetic: true });
@@ -2223,7 +2254,6 @@ export class InteractiveMode implements InteractiveModeContext {
2223
2254
  return;
2224
2255
  }
2225
2256
 
2226
- this.#renderPlanPreview(planContent, { append: true });
2227
2257
  const contextUsage = this.#getPlanApprovalContextUsage();
2228
2258
  const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
2229
2259
  const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
@@ -2253,23 +2283,40 @@ export class InteractiveMode implements InteractiveModeContext {
2253
2283
  },
2254
2284
  }
2255
2285
  : undefined;
2256
- const helpText = slider ? `${this.#getPlanReviewHelpText()} ◂/▸ model` : this.#getPlanReviewHelpText();
2257
-
2258
- const choice = await this.showHookSelector(
2286
+ // The overlay now owns the dynamic, focus-aware help line; the caller only
2287
+ // supplies the trailing cancel hint.
2288
+ const helpText = "esc cancel";
2289
+ // In-overlay edits (section deletes/undo) and section annotations. Deletes
2290
+ // update `editedContent` (and mirror to disk); annotations build `feedback`
2291
+ // that the Refine branch re-prompts the model with.
2292
+ let editedContent: string | undefined;
2293
+ let feedback = "";
2294
+
2295
+ const choice = await this.showPlanReview(
2296
+ planContent,
2259
2297
  "Plan mode - next step",
2260
2298
  ["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
2261
2299
  {
2262
2300
  helpText,
2263
2301
  onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
2302
+ onPlanEdited: content => {
2303
+ editedContent = content;
2304
+ void Bun.write(this.#resolvePlanFilePath(planFilePath), content);
2305
+ },
2306
+ onFeedbackChange: value => {
2307
+ feedback = value;
2308
+ },
2264
2309
  disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
2265
2310
  },
2266
2311
  { slider },
2267
2312
  );
2268
2313
 
2269
2314
  if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
2270
- const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
2271
2315
  try {
2272
- const latestPlanContent = await this.#readPlanFile(planFilePath);
2316
+ // Prefer in-overlay edits (already in memory) over a disk re-read; the
2317
+ // `onPlanEdited` write is fire-and-forget, so reading the file here could
2318
+ // race ahead of it.
2319
+ const latestPlanContent = editedContent ?? (await this.#readPlanFile(planFilePath));
2273
2320
  if (!latestPlanContent) {
2274
2321
  this.showError(`Plan file not found at ${planFilePath}`);
2275
2322
  return;
@@ -2287,7 +2334,6 @@ export class InteractiveMode implements InteractiveModeContext {
2287
2334
  cycle && selectedTierIndex !== cycle.currentIndex ? cycle.models[selectedTierIndex] : undefined;
2288
2335
  await this.#approvePlan(latestPlanContent, {
2289
2336
  planFilePath,
2290
- finalPlanFilePath,
2291
2337
  title: details.title,
2292
2338
  preserveContext: choice !== "Approve and execute",
2293
2339
  compactBeforeExecute: choice === "Approve and compact context",
@@ -2300,6 +2346,16 @@ export class InteractiveMode implements InteractiveModeContext {
2300
2346
  }
2301
2347
  return;
2302
2348
  }
2349
+
2350
+ if (choice === "Refine plan") {
2351
+ // Section annotations entered in the overlay become a refinement prompt
2352
+ // re-submitted to the model. With no annotations, fall back to today's
2353
+ // behavior: close the overlay and let the operator type their own.
2354
+ if (feedback.trim() && this.onInputCallback) {
2355
+ this.onInputCallback(this.startPendingSubmission({ text: feedback }));
2356
+ }
2357
+ return;
2358
+ }
2303
2359
  }
2304
2360
 
2305
2361
  /**
@@ -2453,9 +2509,6 @@ export class InteractiveMode implements InteractiveModeContext {
2453
2509
  initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void {
2454
2510
  this.#extensionUiController.initializeHookRunner(uiContext, hasUI);
2455
2511
  }
2456
- createBackgroundUiContext(): ExtensionUIContext {
2457
- return this.#extensionUiController.createBackgroundUiContext();
2458
- }
2459
2512
 
2460
2513
  setEditorComponent(
2461
2514
  factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
@@ -2497,12 +2550,26 @@ export class InteractiveMode implements InteractiveModeContext {
2497
2550
  this.ui.requestRender();
2498
2551
  }
2499
2552
 
2500
- // Event handling
2501
- async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
2502
- await this.#eventController.handleBackgroundEvent(event);
2553
+ // UI helpers
2554
+ present(content: Component | readonly Component[]): void {
2555
+ if (Array.isArray(content)) {
2556
+ for (const item of content) this.#mountChatChild(item);
2557
+ } else {
2558
+ this.#mountChatChild(content as Component);
2559
+ }
2560
+ this.ui.requestRender();
2561
+ }
2562
+
2563
+ #mountChatChild(item: Component): void {
2564
+ this.chatContainer.addChild(item);
2565
+ if (item instanceof ChatBlock) item.mount(this.#chatHost);
2566
+ }
2567
+
2568
+ resetTranscript(): void {
2569
+ this.chatContainer.dispose();
2570
+ this.chatContainer.clear();
2503
2571
  }
2504
2572
 
2505
- // UI helpers
2506
2573
  showStatus(message: string, options?: { dim?: boolean }): void {
2507
2574
  this.#uiHelpers.showStatus(message, options);
2508
2575
  }
@@ -2522,7 +2589,6 @@ export class InteractiveMode implements InteractiveModeContext {
2522
2589
  }
2523
2590
 
2524
2591
  showPinnedError(message: string): void {
2525
- if (this.isBackgrounded) return;
2526
2592
  this.errorBannerContainer.clear();
2527
2593
  this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
2528
2594
  this.ui.requestRender();
@@ -2593,13 +2659,18 @@ export class InteractiveMode implements InteractiveModeContext {
2593
2659
  ensureLoadingAnimation(): void {
2594
2660
  if (!this.loadingAnimation) {
2595
2661
  this.statusContainer.clear();
2662
+ const messageColorFn = ((message: string) =>
2663
+ renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
2664
+ animated: true;
2665
+ };
2666
+ messageColorFn.animated = true;
2596
2667
  this.loadingAnimation = new Loader(
2597
2668
  this.ui,
2598
2669
  spinner => {
2599
2670
  const accent = this.#getWorkingMessageAccent();
2600
2671
  return accent ? `${accent.main}${spinner}\x1b[39m` : theme.fg("accent", spinner);
2601
2672
  },
2602
- message => renderWorkingMessage(message, this.#getWorkingMessageAccent()),
2673
+ messageColorFn,
2603
2674
  this.#defaultWorkingMessage,
2604
2675
  getSymbolTheme().spinnerFrames,
2605
2676
  );
@@ -2751,7 +2822,7 @@ export class InteractiveMode implements InteractiveModeContext {
2751
2822
  this.#omfgController.dispose();
2752
2823
  this.#extensionUiController.clearExtensionTerminalInputListeners();
2753
2824
  this.clearPinnedError();
2754
- this.#planReviewContainer = undefined;
2825
+ this.#hidePlanReview();
2755
2826
  }
2756
2827
 
2757
2828
  handleClearCommand(): Promise<void> {
@@ -2759,6 +2830,10 @@ export class InteractiveMode implements InteractiveModeContext {
2759
2830
  return this.#commandController.handleClearCommand();
2760
2831
  }
2761
2832
 
2833
+ handleFreshCommand(): Promise<void> {
2834
+ return this.#commandController.handleFreshCommand();
2835
+ }
2836
+
2762
2837
  handleDropCommand(): Promise<void> {
2763
2838
  this.#prepareSessionSwitch();
2764
2839
  return this.#commandController.handleDropCommand();
@@ -2994,10 +3069,6 @@ export class InteractiveMode implements InteractiveModeContext {
2994
3069
  this.#inputController.handleDequeue();
2995
3070
  }
2996
3071
 
2997
- handleBackgroundCommand(): void {
2998
- this.#inputController.handleBackgroundCommand();
2999
- }
3000
-
3001
3072
  handleImagePaste(): Promise<boolean> {
3002
3073
  return this.#inputController.handleImagePaste();
3003
3074
  }
@@ -3006,6 +3077,10 @@ export class InteractiveMode implements InteractiveModeContext {
3006
3077
  return this.#btwController.start(question);
3007
3078
  }
3008
3079
 
3080
+ handleTanCommand(work: string): Promise<void> {
3081
+ return this.#tanCommandController.start(work);
3082
+ }
3083
+
3009
3084
  hasActiveBtw(): boolean {
3010
3085
  return this.#btwController.hasActiveRequest();
3011
3086
  }
@@ -82,7 +82,7 @@ export class SetupWizardComponent implements Component {
82
82
  }
83
83
 
84
84
  invalidate(): void {
85
- this.#activeScene?.invalidate();
85
+ this.#activeScene?.invalidate?.();
86
86
  }
87
87
 
88
88
  handleInput(data: string): void {
@@ -406,7 +406,7 @@
406
406
  }
407
407
  },
408
408
  "spinnerFrames": {
409
- "description": "Override the spinner frames. Use a flat array to set both `status` and `activity`, or an object to override each independently. Frames are advanced ~12.5fps for status spinners and ~60fps for activity spinners.",
409
+ "description": "Override the spinner frames. Use a flat array to set both `status` and `activity`, or an object to override each independently. Frames are advanced ~12.5fps for status spinners and ~30fps for activity spinners.",
410
410
  "oneOf": [
411
411
  {
412
412
  "type": "array",
@@ -95,6 +95,7 @@ export type SymbolKey =
95
95
  | "icon.pause"
96
96
  | "icon.loop"
97
97
  | "icon.folder"
98
+ | "icon.search"
98
99
  | "icon.scratchFolder"
99
100
  | "icon.file"
100
101
  | "icon.git"
@@ -264,6 +265,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
264
265
  "icon.pause": "⏸",
265
266
  "icon.loop": "↻",
266
267
  "icon.folder": "📁",
268
+ "icon.search": "🔍",
267
269
  "icon.scratchFolder": "🗑",
268
270
  "icon.file": "📄",
269
271
  "icon.git": "⎇",
@@ -488,6 +490,7 @@ const NERD_SYMBOLS: SymbolMap = {
488
490
  "icon.loop": "\uf021",
489
491
  // pick:  | alt:  
490
492
  "icon.folder": "\uf115",
493
+ "icon.search": "\uf002",
491
494
  // pick: | alt:
492
495
  "icon.scratchFolder": "\uf014",
493
496
  // pick:  | alt:  
@@ -701,6 +704,7 @@ const ASCII_SYMBOLS: SymbolMap = {
701
704
  "icon.pause": "||",
702
705
  "icon.loop": "loop",
703
706
  "icon.folder": "[D]",
707
+ "icon.search": "[/]",
704
708
  "icon.scratchFolder": "[T]",
705
709
  "icon.file": "[F]",
706
710
  "icon.git": "git:",
@@ -2439,10 +2443,10 @@ function getHighlightColors(t: Theme): NativeHighlightColors {
2439
2443
  * switch (which always reassigns `theme`) must invalidate every entry.
2440
2444
  *
2441
2445
  * Why this exists: animated tool blocks (eval/bash) repaint their box on every
2442
- * ~16ms border-shimmer frame, and markdown re-lexes on every streamed delta.
2443
- * Without memoization each frame re-tokenizes an unchanged code body through the
2444
- * Rust FFI — ~26ms for 100 lines, ~40ms for 150 — overrunning the 16ms frame
2445
- * budget and starving the spinner/render timers (the "TUI freeze").
2446
+ * ~33ms border-shimmer frame, and markdown re-lexes on every streamed delta.
2447
+ * Without memoization each frame can re-tokenize an unchanged code body through
2448
+ * the Rust FFI — ~26ms for 100 lines, ~40ms for 150 — consuming or overrunning
2449
+ * the 33ms frame budget and starving the spinner/render timers (the "TUI freeze").
2446
2450
  */
2447
2451
  const HIGHLIGHT_CACHE_MAX = 256;
2448
2452
  const highlightCache = new LRUCache<string, string>({ max: HIGHLIGHT_CACHE_MAX });
@@ -14,7 +14,7 @@ import type {
14
14
  import type { CompactOptions } from "../extensibility/extensions/types";
15
15
  import type { MCPManager } from "../mcp";
16
16
  import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
17
- import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
17
+ import type { AgentSession } from "../session/agent-session";
18
18
  import type { HistoryStorage } from "../session/history-storage";
19
19
  import type { SessionContext, SessionManager } from "../session/session-manager";
20
20
  import type { ShakeMode } from "../session/shake-types";
@@ -63,6 +63,7 @@ export type TodoPhase = {
63
63
 
64
64
  export interface InteractiveModeInitOptions {
65
65
  suppressWelcomeIntro?: boolean;
66
+ clearInitialTerminalHistory?: boolean;
66
67
  }
67
68
 
68
69
  export type InteractiveSelectorDialogOptions = ExtensionUIDialogOptions & Pick<HookSelectorOptions, "disabledIndices">;
@@ -95,7 +96,6 @@ export interface InteractiveModeContext {
95
96
 
96
97
  // State
97
98
  isInitialized: boolean;
98
- isBackgrounded: boolean;
99
99
  isBashMode: boolean;
100
100
  toolOutputExpanded: boolean;
101
101
  todoExpanded: boolean;
@@ -149,15 +149,25 @@ export interface InteractiveModeContext {
149
149
  // Extension UI integration
150
150
  setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
151
151
  initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
152
- createBackgroundUiContext(): ExtensionUIContext;
153
152
  setEditorComponent(
154
153
  factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
155
154
  ): void;
156
155
 
157
- // Event handling
158
- handleBackgroundEvent(event: AgentSessionEvent): Promise<void>;
159
-
160
156
  // UI helpers
157
+ /**
158
+ * Mount transcript content and repaint once. The single sink for "show this in
159
+ * chat": producers build and return a `Component` (or a `ChatBlock` carrying
160
+ * its own lifecycle) and hand it here instead of touching `chatContainer` /
161
+ * `ui.requestRender()` directly. `ChatBlock`s are mounted (their `onMount`
162
+ * runs) so their timers/subscriptions start.
163
+ */
164
+ present(content: Component | readonly Component[]): void;
165
+ /**
166
+ * Dispose every live block in the transcript (stopping timers/subscriptions)
167
+ * and clear it. Used before a full rebuild so animated/streaming blocks do not
168
+ * leak.
169
+ */
170
+ resetTranscript(): void;
161
171
  showStatus(message: string, options?: { dim?: boolean }): void;
162
172
  showError(message: string): void;
163
173
  showPinnedError(message: string): void;
@@ -233,6 +243,7 @@ export interface InteractiveModeContext {
233
243
  handleDumpCommand(): void;
234
244
  handleDebugTranscriptCommand(): Promise<void>;
235
245
  handleClearCommand(): Promise<void>;
246
+ handleFreshCommand(): Promise<void>;
236
247
  handleDropCommand(): Promise<void>;
237
248
  handleForkCommand(): Promise<void>;
238
249
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
@@ -278,9 +289,9 @@ export interface InteractiveModeContext {
278
289
  handleCtrlD(): void;
279
290
  handleCtrlZ(): void;
280
291
  handleDequeue(): void;
281
- handleBackgroundCommand(): void;
282
292
  handleImagePaste(): Promise<boolean>;
283
293
  handleBtwCommand(question: string): Promise<void>;
294
+ handleTanCommand(work: string): Promise<void>;
284
295
  hasActiveBtw(): boolean;
285
296
  handleBtwEscape(): boolean;
286
297
  handleOmfgCommand(complaint: string): Promise<void>;