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

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. 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,
@@ -50,7 +50,7 @@ import chalk from "chalk";
50
50
  import { reset as resetCapabilities } from "../capability";
51
51
  import { KeybindingsManager } from "../config/keybindings";
52
52
  import { MODEL_ROLES, type ModelRole } from "../config/model-registry";
53
- import { isSettingsInitialized, Settings, settings } from "../config/settings";
53
+ import { isSettingsInitialized, onStatusLineSessionAccentChanged, Settings, settings } from "../config/settings";
54
54
  import { clearClaudePluginRootsCache } from "../discovery/helpers";
55
55
  import type {
56
56
  ContextUsage,
@@ -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,
@@ -127,7 +125,7 @@ import {
127
125
  import { OAuthManualInputManager } from "./oauth-manual-input";
128
126
  import { SessionObserverRegistry } from "./session-observer-registry";
129
127
  import { interruptHint } from "./shared";
130
- import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
128
+ import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
131
129
  import type { Theme } from "./theme/theme";
132
130
  import {
133
131
  getEditorTheme,
@@ -159,6 +157,12 @@ interface WorkingMessageAccent {
159
157
  dim: string;
160
158
  }
161
159
 
160
+ interface WorkingMessageAccentCacheKey {
161
+ sessionName: string | undefined;
162
+ accentSurfaceLuminance: number | undefined;
163
+ sessionAccentEnabled: boolean;
164
+ }
165
+
162
166
  function renderWorkingMessage(message: string, accent?: WorkingMessageAccent): string {
163
167
  const palette = accent
164
168
  ? ({
@@ -250,20 +254,6 @@ export interface InteractiveModeOptions {
250
254
  initialMessages?: string[];
251
255
  }
252
256
 
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
257
  export class InteractiveMode implements InteractiveModeContext {
268
258
  session: AgentSession;
269
259
  sessionManager: SessionManager;
@@ -287,7 +277,6 @@ export class InteractiveMode implements InteractiveModeContext {
287
277
  statusLine: StatusLineComponent;
288
278
 
289
279
  isInitialized = false;
290
- isBackgrounded = false;
291
280
  isBashMode = false;
292
281
  toolOutputExpanded = false;
293
282
  todoExpanded = false;
@@ -318,6 +307,9 @@ export class InteractiveMode implements InteractiveModeContext {
318
307
  autoCompactionLoader: Loader | undefined = undefined;
319
308
  retryLoader: Loader | undefined = undefined;
320
309
  #pendingWorkingMessage: string | undefined;
310
+ #workingMessageAccentCacheKey?: WorkingMessageAccentCacheKey;
311
+ #workingMessageAccentCacheValue?: WorkingMessageAccent;
312
+ #workingMessageAccentCacheHasValue = false;
321
313
  get #defaultWorkingMessage(): string {
322
314
  return `Working…${interruptHint()}`;
323
315
  }
@@ -355,12 +347,14 @@ export class InteractiveMode implements InteractiveModeContext {
355
347
  #planModePreviousModelState: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
356
348
  #pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
357
349
  #planModeHasEntered = false;
358
- #planReviewContainer: Container | undefined;
350
+ #planReviewOverlay: PlanReviewOverlay | undefined;
351
+ #planReviewOverlayHandle: OverlayHandle | undefined;
359
352
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
360
353
  mcpManager?: import("../mcp").MCPManager;
361
354
  readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
362
355
 
363
356
  readonly #btwController: BtwController;
357
+ readonly #tanCommandController: TanCommandController;
364
358
  readonly #omfgController: OmfgController;
365
359
  readonly #commandController: CommandController;
366
360
  readonly #todoCommandController: TodoCommandController;
@@ -379,6 +373,7 @@ export class InteractiveMode implements InteractiveModeContext {
379
373
  #eventBus?: EventBus;
380
374
  #eventBusUnsubscribers: Array<() => void> = [];
381
375
  #welcomeComponent?: WelcomeComponent;
376
+ readonly #chatHost: ChatBlockHost = { requestRender: () => this.ui.requestRender() };
382
377
 
383
378
  constructor(
384
379
  session: AgentSession,
@@ -484,6 +479,7 @@ export class InteractiveMode implements InteractiveModeContext {
484
479
 
485
480
  this.#uiHelpers = new UiHelpers(this);
486
481
  this.#btwController = new BtwController(this);
482
+ this.#tanCommandController = new TanCommandController(this);
487
483
  this.#omfgController = new OmfgController(this);
488
484
  this.#extensionUiController = new ExtensionUiController(this);
489
485
  this.#eventController = new EventController(this);
@@ -613,8 +609,9 @@ export class InteractiveMode implements InteractiveModeContext {
613
609
  // Load initial todos
614
610
  await this.#loadTodoList();
615
611
 
616
- // Start the UI
617
- this.ui.start();
612
+ // Start the UI. Cold `omp` launch opts into clearing on the first paint so
613
+ // the initial welcome frame does not append over the previous run's scrollback.
614
+ this.ui.start({ clearScrollback: options.clearInitialTerminalHistory === true });
618
615
  pushTerminalTitle();
619
616
  setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
620
617
  this.updateEditorBorderColor();
@@ -650,9 +647,17 @@ export class InteractiveMode implements InteractiveModeContext {
650
647
  this.session.subscribe(event => {
651
648
  void this.#handleGoalSessionEvent(event);
652
649
  }),
650
+ this.sessionManager.onSessionNameChanged(() => {
651
+ this.#handleSessionAccentInputsChanged();
652
+ }),
653
+ onStatusLineSessionAccentChanged(() => {
654
+ this.#syncStatusLineSettings();
655
+ this.#handleSessionAccentInputsChanged();
656
+ }),
653
657
  );
654
658
  // Set up theme file watcher
655
659
  onThemeChange(() => {
660
+ this.#clearWorkingMessageAccentCache();
656
661
  clearRenderCache();
657
662
  this.ui.invalidate();
658
663
  this.updateEditorBorderColor();
@@ -977,9 +982,7 @@ export class InteractiveMode implements InteractiveModeContext {
977
982
  this.#goalContinuationTurnInFlight = false;
978
983
  }
979
984
  if (this.loadingAnimation) {
980
- this.loadingAnimation.stop();
981
- this.loadingAnimation = undefined;
982
- this.statusContainer.clear();
985
+ this.#stopLoadingAnimation(true);
983
986
  }
984
987
  if (!submission.customType) {
985
988
  this.pendingImages = submission.images ? [...submission.images] : [];
@@ -1017,9 +1020,7 @@ export class InteractiveMode implements InteractiveModeContext {
1017
1020
  pendingSubmissionDispose?.();
1018
1021
  this.#pendingWorkingMessage = undefined;
1019
1022
  if (this.loadingAnimation) {
1020
- this.loadingAnimation.stop();
1021
- this.loadingAnimation = undefined;
1022
- this.statusContainer.clear();
1023
+ this.#stopLoadingAnimation(true);
1023
1024
  }
1024
1025
  }
1025
1026
  }
@@ -1035,6 +1036,24 @@ export class InteractiveMode implements InteractiveModeContext {
1035
1036
  this.editor.setMaxHeight(this.#computeEditorMaxHeight());
1036
1037
  }
1037
1038
 
1039
+ #syncStatusLineSettings(): void {
1040
+ this.statusLine.updateSettings({
1041
+ preset: settings.get("statusLine.preset"),
1042
+ leftSegments: settings.get("statusLine.leftSegments"),
1043
+ rightSegments: settings.get("statusLine.rightSegments"),
1044
+ separator: settings.get("statusLine.separator"),
1045
+ showHookStatus: settings.get("statusLine.showHookStatus"),
1046
+ sessionAccent: settings.get("statusLine.sessionAccent"),
1047
+ segmentOptions: settings.get("statusLine.segmentOptions"),
1048
+ });
1049
+ }
1050
+
1051
+ #handleSessionAccentInputsChanged(): void {
1052
+ this.#clearWorkingMessageAccentCache();
1053
+ this.statusLine.invalidate();
1054
+ this.updateEditorBorderColor();
1055
+ }
1056
+
1038
1057
  updateEditorBorderColor(): void {
1039
1058
  if (this.isBashMode) {
1040
1059
  this.editor.borderColor = theme.getBashModeBorderColor();
@@ -1536,22 +1555,15 @@ export class InteractiveMode implements InteractiveModeContext {
1536
1555
  if (!state?.enabled) {
1537
1556
  throw new ToolError("Plan mode is not active.");
1538
1557
  }
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({
1558
+ const { planFilePath, title } = await resolveApprovedPlan({
1547
1559
  suppliedTitle: extra?.title,
1548
- planContent,
1549
- planFilePath,
1560
+ statePlanFilePath: state.planFilePath,
1561
+ readPlan: url => this.#readPlanFile(url),
1562
+ listPlanFiles: () => this.#listLocalPlanFiles(),
1550
1563
  });
1551
1564
  const details: PlanApprovalDetails = {
1552
1565
  planFilePath,
1553
- finalPlanFilePath: `local://${normalized.fileName}`,
1554
- title: normalized.title,
1566
+ title,
1555
1567
  planExists: true,
1556
1568
  };
1557
1569
  return {
@@ -1691,22 +1703,87 @@ export class InteractiveMode implements InteractiveModeContext {
1691
1703
  }
1692
1704
  }
1693
1705
 
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);
1706
+ /** `local://` URLs of plan files in the session-local root, newest first.
1707
+ * A fallback for `resolveApprovedPlan` when the agent dropped `extra.title`,
1708
+ * so the plan it wrote is still found by scanning recent `*-plan.md` files. */
1709
+ async #listLocalPlanFiles(): Promise<string[]> {
1710
+ const localRoot = this.#resolvePlanFilePath("local://");
1711
+ try {
1712
+ const entries = await fs.readdir(localRoot, { withFileTypes: true });
1713
+ const plans = await Promise.all(
1714
+ entries
1715
+ .filter(entry => entry.isFile() && /plan\.md$/i.test(entry.name))
1716
+ .map(async name => {
1717
+ const stat = await fs.stat(path.join(localRoot, name.name)).catch(() => null);
1718
+ return { url: `local://${name.name}`, mtime: stat?.mtimeMs ?? 0 };
1719
+ }),
1720
+ );
1721
+ return plans.sort((a, b) => b.mtime - a.mtime).map(plan => plan.url);
1722
+ } catch {
1723
+ return [];
1707
1724
  }
1708
- this.#planReviewContainer = planReviewContainer;
1725
+ }
1726
+
1727
+ showPlanReview(
1728
+ planContent: string,
1729
+ title: string,
1730
+ options: string[],
1731
+ dialogOptions?: {
1732
+ helpText?: string;
1733
+ disabledIndices?: number[];
1734
+ onExternalEditor?: () => void;
1735
+ onPlanEdited?: (content: string) => void;
1736
+ onFeedbackChange?: (feedback: string) => void;
1737
+ initialIndex?: number;
1738
+ },
1739
+ extra?: { slider?: HookSelectorSlider },
1740
+ ): Promise<string | undefined> {
1741
+ this.#hidePlanReview();
1742
+ const { promise, resolve } = Promise.withResolvers<string | undefined>();
1743
+ let settled = false;
1744
+ const finish = (choice: string | undefined): void => {
1745
+ if (settled) return;
1746
+ settled = true;
1747
+ this.#hidePlanReview();
1748
+ this.ui.requestRender();
1749
+ resolve(choice);
1750
+ };
1751
+ const overlay = new PlanReviewOverlay(
1752
+ planContent,
1753
+ {
1754
+ promptTitle: title,
1755
+ options,
1756
+ disabledIndices: dialogOptions?.disabledIndices,
1757
+ helpText: dialogOptions?.helpText,
1758
+ initialIndex: dialogOptions?.initialIndex,
1759
+ slider: extra?.slider,
1760
+ externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
1761
+ },
1762
+ {
1763
+ onPick: choice => finish(choice),
1764
+ onCancel: () => finish(undefined),
1765
+ onExternalEditor: dialogOptions?.onExternalEditor,
1766
+ onPlanEdited: dialogOptions?.onPlanEdited,
1767
+ onFeedbackChange: dialogOptions?.onFeedbackChange,
1768
+ },
1769
+ );
1770
+ this.#planReviewOverlay = overlay;
1771
+ this.#planReviewOverlayHandle = this.ui.showOverlay(overlay, {
1772
+ anchor: "bottom-center",
1773
+ width: "100%",
1774
+ maxHeight: "100%",
1775
+ margin: 0,
1776
+ fullscreen: true,
1777
+ });
1778
+ this.ui.setFocus(overlay);
1709
1779
  this.ui.requestRender();
1780
+ return promise;
1781
+ }
1782
+
1783
+ #hidePlanReview(): void {
1784
+ this.#planReviewOverlayHandle?.hide();
1785
+ this.#planReviewOverlayHandle = undefined;
1786
+ this.#planReviewOverlay = undefined;
1710
1787
  }
1711
1788
 
1712
1789
  #getEditorTerminalPath(): string | null {
@@ -1728,14 +1805,6 @@ export class InteractiveMode implements InteractiveModeContext {
1728
1805
  }
1729
1806
  }
1730
1807
 
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
1808
  #getPlanApprovalContextUsage(): ContextUsage | undefined {
1740
1809
  const executionModel = this.#planModePreviousModelState?.model ?? this.session.model;
1741
1810
  const contextWindow = executionModel?.contextWindow;
@@ -1794,7 +1863,7 @@ export class InteractiveMode implements InteractiveModeContext {
1794
1863
  });
1795
1864
  if (result !== null) {
1796
1865
  await Bun.write(resolvedPath, result);
1797
- this.#renderPlanPreview(result);
1866
+ this.#planReviewOverlay?.setPlanContent(result);
1798
1867
  this.showStatus("Plan updated in external editor.");
1799
1868
  }
1800
1869
  } catch (error) {
@@ -1826,19 +1895,12 @@ export class InteractiveMode implements InteractiveModeContext {
1826
1895
  planContent: string,
1827
1896
  options: {
1828
1897
  planFilePath: string;
1829
- finalPlanFilePath: string;
1830
1898
  title: string;
1831
1899
  preserveContext?: boolean;
1832
1900
  compactBeforeExecute?: boolean;
1833
1901
  executionModel?: ResolvedRoleModel;
1834
1902
  },
1835
1903
  ): 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
1904
  const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
1843
1905
 
1844
1906
  // Mark the pending abort caused by the plan-mode → compaction transition as
@@ -1857,8 +1919,8 @@ export class InteractiveMode implements InteractiveModeContext {
1857
1919
  if (!options.preserveContext) {
1858
1920
  await this.handleClearCommand();
1859
1921
  // 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, {
1922
+ // so `local://<slug>-plan.md` resolves correctly in the execution session.
1923
+ const newLocalPath = resolveLocalUrlToPath(options.planFilePath, {
1862
1924
  getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
1863
1925
  getSessionId: () => this.sessionManager.getSessionId(),
1864
1926
  });
@@ -1872,7 +1934,7 @@ export class InteractiveMode implements InteractiveModeContext {
1872
1934
  // Cancellation skips the synthetic-prompt dispatch (operator's explicit
1873
1935
  // abort is honored); failure proceeds best-effort — approval intent stands.
1874
1936
  const compactionPrompt = prompt.render(planModeCompactInstructionsPrompt, {
1875
- planFilePath: options.finalPlanFilePath,
1937
+ planFilePath: options.planFilePath,
1876
1938
  });
1877
1939
  // Pin the plan reference path BEFORE compaction so any user messages
1878
1940
  // queued during the compaction await (which `handleCompactCommand`
@@ -1880,7 +1942,7 @@ export class InteractiveMode implements InteractiveModeContext {
1880
1942
  // approved plan in `#buildPlanReferenceMessage`. Reassignment after
1881
1943
  // the try/finally is idempotent and kept for the !compactBeforeExecute
1882
1944
  // branch.
1883
- this.session.setPlanReferencePath(options.finalPlanFilePath);
1945
+ this.session.setPlanReferencePath(options.planFilePath);
1884
1946
  compactOutcome = await this.handleCompactCommand(compactionPrompt);
1885
1947
  }
1886
1948
  } finally {
@@ -1896,7 +1958,7 @@ export class InteractiveMode implements InteractiveModeContext {
1896
1958
  if (previousTools.length > 0) {
1897
1959
  await this.session.setActiveToolsByName(previousTools);
1898
1960
  }
1899
- this.session.setPlanReferencePath(options.finalPlanFilePath);
1961
+ this.session.setPlanReferencePath(options.planFilePath);
1900
1962
 
1901
1963
  if (compactOutcome === "cancelled") {
1902
1964
  // Explicit abort: honor it. `executeCompaction` already surfaced
@@ -1933,7 +1995,7 @@ export class InteractiveMode implements InteractiveModeContext {
1933
1995
  this.session.markPlanReferenceSent();
1934
1996
  const planModePrompt = prompt.render(planModeApprovedPrompt, {
1935
1997
  planContent,
1936
- finalPlanFilePath: options.finalPlanFilePath,
1998
+ planFilePath: options.planFilePath,
1937
1999
  contextPreserved: options.preserveContext === true,
1938
2000
  });
1939
2001
  await this.session.prompt(planModePrompt, { synthetic: true });
@@ -2223,7 +2285,6 @@ export class InteractiveMode implements InteractiveModeContext {
2223
2285
  return;
2224
2286
  }
2225
2287
 
2226
- this.#renderPlanPreview(planContent, { append: true });
2227
2288
  const contextUsage = this.#getPlanApprovalContextUsage();
2228
2289
  const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
2229
2290
  const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
@@ -2253,23 +2314,40 @@ export class InteractiveMode implements InteractiveModeContext {
2253
2314
  },
2254
2315
  }
2255
2316
  : undefined;
2256
- const helpText = slider ? `${this.#getPlanReviewHelpText()} ◂/▸ model` : this.#getPlanReviewHelpText();
2257
-
2258
- const choice = await this.showHookSelector(
2317
+ // The overlay now owns the dynamic, focus-aware help line; the caller only
2318
+ // supplies the trailing cancel hint.
2319
+ const helpText = "esc cancel";
2320
+ // In-overlay edits (section deletes/undo) and section annotations. Deletes
2321
+ // update `editedContent` (and mirror to disk); annotations build `feedback`
2322
+ // that the Refine branch re-prompts the model with.
2323
+ let editedContent: string | undefined;
2324
+ let feedback = "";
2325
+
2326
+ const choice = await this.showPlanReview(
2327
+ planContent,
2259
2328
  "Plan mode - next step",
2260
2329
  ["Approve and execute", "Approve and compact context", keepContextLabel, "Refine plan"],
2261
2330
  {
2262
2331
  helpText,
2263
2332
  onExternalEditor: () => void this.#openPlanInExternalEditor(planFilePath),
2333
+ onPlanEdited: content => {
2334
+ editedContent = content;
2335
+ void Bun.write(this.#resolvePlanFilePath(planFilePath), content);
2336
+ },
2337
+ onFeedbackChange: value => {
2338
+ feedback = value;
2339
+ },
2264
2340
  disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
2265
2341
  },
2266
2342
  { slider },
2267
2343
  );
2268
2344
 
2269
2345
  if (choice === "Approve and execute" || choice === "Approve and compact context" || choice === keepContextLabel) {
2270
- const finalPlanFilePath = details.finalPlanFilePath || planFilePath;
2271
2346
  try {
2272
- const latestPlanContent = await this.#readPlanFile(planFilePath);
2347
+ // Prefer in-overlay edits (already in memory) over a disk re-read; the
2348
+ // `onPlanEdited` write is fire-and-forget, so reading the file here could
2349
+ // race ahead of it.
2350
+ const latestPlanContent = editedContent ?? (await this.#readPlanFile(planFilePath));
2273
2351
  if (!latestPlanContent) {
2274
2352
  this.showError(`Plan file not found at ${planFilePath}`);
2275
2353
  return;
@@ -2287,7 +2365,6 @@ export class InteractiveMode implements InteractiveModeContext {
2287
2365
  cycle && selectedTierIndex !== cycle.currentIndex ? cycle.models[selectedTierIndex] : undefined;
2288
2366
  await this.#approvePlan(latestPlanContent, {
2289
2367
  planFilePath,
2290
- finalPlanFilePath,
2291
2368
  title: details.title,
2292
2369
  preserveContext: choice !== "Approve and execute",
2293
2370
  compactBeforeExecute: choice === "Approve and compact context",
@@ -2300,6 +2377,16 @@ export class InteractiveMode implements InteractiveModeContext {
2300
2377
  }
2301
2378
  return;
2302
2379
  }
2380
+
2381
+ if (choice === "Refine plan") {
2382
+ // Section annotations entered in the overlay become a refinement prompt
2383
+ // re-submitted to the model. With no annotations, fall back to today's
2384
+ // behavior: close the overlay and let the operator type their own.
2385
+ if (feedback.trim() && this.onInputCallback) {
2386
+ this.onInputCallback(this.startPendingSubmission({ text: feedback }));
2387
+ }
2388
+ return;
2389
+ }
2303
2390
  }
2304
2391
 
2305
2392
  /**
@@ -2360,8 +2447,7 @@ export class InteractiveMode implements InteractiveModeContext {
2360
2447
 
2361
2448
  stop(): void {
2362
2449
  if (this.loadingAnimation) {
2363
- this.loadingAnimation.stop();
2364
- this.loadingAnimation = undefined;
2450
+ this.#stopLoadingAnimation(false);
2365
2451
  }
2366
2452
  this.#cleanupMicAnimation();
2367
2453
  this.#cancelTodoAutoClearTimer();
@@ -2453,9 +2539,6 @@ export class InteractiveMode implements InteractiveModeContext {
2453
2539
  initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void {
2454
2540
  this.#extensionUiController.initializeHookRunner(uiContext, hasUI);
2455
2541
  }
2456
- createBackgroundUiContext(): ExtensionUIContext {
2457
- return this.#extensionUiController.createBackgroundUiContext();
2458
- }
2459
2542
 
2460
2543
  setEditorComponent(
2461
2544
  factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
@@ -2497,12 +2580,26 @@ export class InteractiveMode implements InteractiveModeContext {
2497
2580
  this.ui.requestRender();
2498
2581
  }
2499
2582
 
2500
- // Event handling
2501
- async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
2502
- await this.#eventController.handleBackgroundEvent(event);
2583
+ // UI helpers
2584
+ present(content: Component | readonly Component[]): void {
2585
+ if (Array.isArray(content)) {
2586
+ for (const item of content) this.#mountChatChild(item);
2587
+ } else {
2588
+ this.#mountChatChild(content as Component);
2589
+ }
2590
+ this.ui.requestRender();
2591
+ }
2592
+
2593
+ #mountChatChild(item: Component): void {
2594
+ this.chatContainer.addChild(item);
2595
+ if (item instanceof ChatBlock) item.mount(this.#chatHost);
2596
+ }
2597
+
2598
+ resetTranscript(): void {
2599
+ this.chatContainer.dispose();
2600
+ this.chatContainer.clear();
2503
2601
  }
2504
2602
 
2505
- // UI helpers
2506
2603
  showStatus(message: string, options?: { dim?: boolean }): void {
2507
2604
  this.#uiHelpers.showStatus(message, options);
2508
2605
  }
@@ -2514,15 +2611,12 @@ export class InteractiveMode implements InteractiveModeContext {
2514
2611
  this.#pendingSubmissionDispose = undefined;
2515
2612
  this.#pendingWorkingMessage = undefined;
2516
2613
  if (this.loadingAnimation) {
2517
- this.loadingAnimation.stop();
2518
- this.loadingAnimation = undefined;
2519
- this.statusContainer.clear();
2614
+ this.#stopLoadingAnimation(true);
2520
2615
  }
2521
2616
  this.#uiHelpers.showError(message);
2522
2617
  }
2523
2618
 
2524
2619
  showPinnedError(message: string): void {
2525
- if (this.isBackgrounded) return;
2526
2620
  this.errorBannerContainer.clear();
2527
2621
  this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
2528
2622
  this.ui.requestRender();
@@ -2580,26 +2674,76 @@ export class InteractiveMode implements InteractiveModeContext {
2580
2674
  this.ui.requestRender();
2581
2675
  }
2582
2676
 
2677
+ #clearWorkingMessageAccentCache(): void {
2678
+ this.#workingMessageAccentCacheKey = undefined;
2679
+ this.#workingMessageAccentCacheValue = undefined;
2680
+ this.#workingMessageAccentCacheHasValue = false;
2681
+ }
2682
+
2683
+ #buildWorkingMessageAccentCacheKey(): WorkingMessageAccentCacheKey {
2684
+ const sessionAccentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
2685
+ return {
2686
+ sessionAccentEnabled,
2687
+ sessionName: sessionAccentEnabled ? this.sessionManager.getSessionName() : undefined,
2688
+ accentSurfaceLuminance: theme.accentSurfaceLuminance,
2689
+ };
2690
+ }
2691
+
2692
+ #workingMessageAccentCacheKeyEquals(a: WorkingMessageAccentCacheKey, b: WorkingMessageAccentCacheKey): boolean {
2693
+ return (
2694
+ a.sessionName === b.sessionName &&
2695
+ a.accentSurfaceLuminance === b.accentSurfaceLuminance &&
2696
+ a.sessionAccentEnabled === b.sessionAccentEnabled
2697
+ );
2698
+ }
2699
+
2700
+ #cacheWorkingMessageAccent(
2701
+ key: WorkingMessageAccentCacheKey,
2702
+ value: WorkingMessageAccent | undefined,
2703
+ ): WorkingMessageAccent | undefined {
2704
+ this.#workingMessageAccentCacheKey = key;
2705
+ this.#workingMessageAccentCacheValue = value;
2706
+ this.#workingMessageAccentCacheHasValue = true;
2707
+ return value;
2708
+ }
2709
+
2583
2710
  #getWorkingMessageAccent(): WorkingMessageAccent | undefined {
2584
- const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
2585
- const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
2586
- if (!sessionName) return undefined;
2587
- const hex = getSessionAccentHex(sessionName, theme.accentSurfaceLuminance);
2711
+ const key = this.#buildWorkingMessageAccentCacheKey();
2712
+ if (
2713
+ this.#workingMessageAccentCacheHasValue &&
2714
+ this.#workingMessageAccentCacheKey &&
2715
+ this.#workingMessageAccentCacheKeyEquals(key, this.#workingMessageAccentCacheKey)
2716
+ ) {
2717
+ return this.#workingMessageAccentCacheValue;
2718
+ }
2719
+ if (!key.sessionAccentEnabled || !key.sessionName) {
2720
+ return this.#cacheWorkingMessageAccent(key, undefined);
2721
+ }
2722
+ const hex = getSessionAccentHex(key.sessionName, key.accentSurfaceLuminance);
2588
2723
  const main = getSessionAccentAnsi(hex);
2589
2724
  const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
2590
- return main && dim ? { main, dim } : undefined;
2725
+ return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
2591
2726
  }
2592
2727
 
2593
2728
  ensureLoadingAnimation(): void {
2594
2729
  if (!this.loadingAnimation) {
2730
+ this.#clearWorkingMessageAccentCache();
2595
2731
  this.statusContainer.clear();
2732
+ const messageColorFn = ((message: string) =>
2733
+ renderWorkingMessage(message, this.#getWorkingMessageAccent())) as LoaderMessageColorFn & {
2734
+ animated?: true;
2735
+ };
2736
+ // Shimmer drives the 30fps redraw; when it is disabled the working
2737
+ // message is static, so leave `animated` unset and let the loader use
2738
+ // the spinner-only ~12.5fps cadence instead of repainting a frozen line.
2739
+ if (shimmerEnabled()) messageColorFn.animated = true;
2596
2740
  this.loadingAnimation = new Loader(
2597
2741
  this.ui,
2598
2742
  spinner => {
2599
2743
  const accent = this.#getWorkingMessageAccent();
2600
2744
  return accent ? `${accent.main}${spinner}\x1b[39m` : theme.fg("accent", spinner);
2601
2745
  },
2602
- message => renderWorkingMessage(message, this.#getWorkingMessageAccent()),
2746
+ messageColorFn,
2603
2747
  this.#defaultWorkingMessage,
2604
2748
  getSymbolTheme().spinnerFrames,
2605
2749
  );
@@ -2609,6 +2753,16 @@ export class InteractiveMode implements InteractiveModeContext {
2609
2753
  this.applyPendingWorkingMessage();
2610
2754
  }
2611
2755
 
2756
+ #stopLoadingAnimation(clearStatusContainer: boolean): void {
2757
+ if (!this.loadingAnimation) return;
2758
+ this.loadingAnimation.stop();
2759
+ this.loadingAnimation = undefined;
2760
+ this.#clearWorkingMessageAccentCache();
2761
+ if (clearStatusContainer) {
2762
+ this.statusContainer.clear();
2763
+ }
2764
+ }
2765
+
2612
2766
  setWorkingMessage(message?: string): void {
2613
2767
  if (message === undefined) {
2614
2768
  this.#pendingWorkingMessage = undefined;
@@ -2636,6 +2790,10 @@ export class InteractiveMode implements InteractiveModeContext {
2636
2790
  this.setWorkingMessage(message);
2637
2791
  }
2638
2792
 
2793
+ notifyInterrupting(): void {
2794
+ this.#eventController.notifyInterrupting();
2795
+ }
2796
+
2639
2797
  showNewVersionNotification(newVersion: string): void {
2640
2798
  this.#uiHelpers.showNewVersionNotification(newVersion);
2641
2799
  }
@@ -2751,7 +2909,7 @@ export class InteractiveMode implements InteractiveModeContext {
2751
2909
  this.#omfgController.dispose();
2752
2910
  this.#extensionUiController.clearExtensionTerminalInputListeners();
2753
2911
  this.clearPinnedError();
2754
- this.#planReviewContainer = undefined;
2912
+ this.#hidePlanReview();
2755
2913
  }
2756
2914
 
2757
2915
  handleClearCommand(): Promise<void> {
@@ -2759,6 +2917,10 @@ export class InteractiveMode implements InteractiveModeContext {
2759
2917
  return this.#commandController.handleClearCommand();
2760
2918
  }
2761
2919
 
2920
+ handleFreshCommand(): Promise<void> {
2921
+ return this.#commandController.handleFreshCommand();
2922
+ }
2923
+
2762
2924
  handleDropCommand(): Promise<void> {
2763
2925
  this.#prepareSessionSwitch();
2764
2926
  return this.#commandController.handleDropCommand();
@@ -2994,10 +3156,6 @@ export class InteractiveMode implements InteractiveModeContext {
2994
3156
  this.#inputController.handleDequeue();
2995
3157
  }
2996
3158
 
2997
- handleBackgroundCommand(): void {
2998
- this.#inputController.handleBackgroundCommand();
2999
- }
3000
-
3001
3159
  handleImagePaste(): Promise<boolean> {
3002
3160
  return this.#inputController.handleImagePaste();
3003
3161
  }
@@ -3006,6 +3164,10 @@ export class InteractiveMode implements InteractiveModeContext {
3006
3164
  return this.#btwController.start(question);
3007
3165
  }
3008
3166
 
3167
+ handleTanCommand(work: string): Promise<void> {
3168
+ return this.#tanCommandController.start(work);
3169
+ }
3170
+
3009
3171
  hasActiveBtw(): boolean {
3010
3172
  return this.#btwController.hasActiveRequest();
3011
3173
  }