@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -64,10 +64,12 @@ import type {
64
64
  } from "../extensibility/extensions";
65
65
  import type { CompactOptions } from "../extensibility/extensions/types";
66
66
  import { loadSlashCommands } from "../extensibility/slash-commands";
67
+ import { type GuidedGoalMessage, runGuidedGoalTurn } from "../goals/guided-setup";
67
68
  import type { Goal, GoalModeState } from "../goals/state";
68
69
  import { resolveLocalUrlToPath } from "../internal-urls";
69
70
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
70
71
  import type { MCPManager } from "../mcp";
72
+ import { formatMCPConnectingMessage, isMcpConnectingEvent, MCP_CONNECTING_EVENT_CHANNEL } from "../mcp/startup-events";
71
73
  import {
72
74
  humanizePlanTitle,
73
75
  type PlanApprovalDetails,
@@ -80,8 +82,9 @@ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compa
80
82
  };
81
83
  import type { AgentSession, AgentSessionEvent, ResolvedRoleModel } from "../session/agent-session";
82
84
  import { HistoryStorage } from "../session/history-storage";
83
- import type { SessionContext, SessionManager } from "../session/session-manager";
84
- import { getRecentSessions } from "../session/session-manager";
85
+ import type { SessionContext } from "../session/session-context";
86
+ import { getRecentSessions } from "../session/session-listing";
87
+ import type { SessionManager } from "../session/session-manager";
85
88
  import type { ShakeMode } from "../session/shake-types";
86
89
  import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES, BUILTIN_SLASH_COMMANDS } from "../slash-commands/builtin-registry";
87
90
  import { formatDuration } from "../slash-commands/helpers/format";
@@ -95,6 +98,7 @@ import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
95
98
  import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
96
99
  import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
97
100
  import { ToolError } from "../tools/tool-errors";
101
+ import { vocalizer } from "../tts/vocalizer";
98
102
  import type { EventBus } from "../utils/event-bus";
99
103
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
100
104
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
@@ -210,6 +214,25 @@ const EDITOR_MAX_HEIGHT_MIN = 6;
210
214
  const EDITOR_MAX_HEIGHT_MAX = 18;
211
215
  const EDITOR_RESERVED_ROWS = 12;
212
216
  const EDITOR_FALLBACK_ROWS = 24;
217
+ const EDITOR_MIN_CHROME_ROWS = 4; // rows reserved for transcript + status on small terms
218
+ const EDITOR_MIN_RENDERED_ROWS = 3; // bordered editor floor: top+bottom border + 1 content row
219
+
220
+ /**
221
+ * Editor max-height cap for a terminal of `terminalRows` rows.
222
+ *
223
+ * Roomy terminals get the comfortable [6, 18] band. Small terminals shrink the
224
+ * cap so the editor leaves at least EDITOR_MIN_CHROME_ROWS rows for the
225
+ * transcript + status line. The editor is bordered, so it never renders fewer
226
+ * than EDITOR_MIN_RENDERED_ROWS rows; once the terminal is too small for both
227
+ * (terminalRows < EDITOR_MIN_RENDERED_ROWS + EDITOR_MIN_CHROME_ROWS) the cap is
228
+ * pinned to that floor — returning a smaller number would not shrink the editor
229
+ * any further, it would only misreport the rows it actually occupies.
230
+ */
231
+ export function computeEditorMaxHeight(terminalRows: number): number {
232
+ const rows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : EDITOR_FALLBACK_ROWS;
233
+ const comfortable = Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, rows - EDITOR_RESERVED_ROWS));
234
+ return Math.max(EDITOR_MIN_RENDERED_ROWS, Math.min(comfortable, rows - EDITOR_MIN_CHROME_ROWS));
235
+ }
213
236
 
214
237
  const HUD_NOTE_SUP_DIGITS: Record<string, string> = {
215
238
  "0": "\u2070",
@@ -282,6 +305,10 @@ class StatusContainer extends Container implements NativeScrollbackLiveRegion {
282
305
  }
283
306
  }
284
307
 
308
+ /** How long the ctrl+p model-role cycle chip track lingers above the editor
309
+ * before it auto-clears, mirroring the todo HUD's auto-clear timer. */
310
+ const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
311
+
285
312
  /**
286
313
  * Build the anchored subagent HUD block: a bold accent "Subagents" header plus
287
314
  * one hooked row per running agent in the same `Id: description` shape the
@@ -340,6 +367,7 @@ export class InteractiveMode implements InteractiveModeContext {
340
367
  btwContainer: Container;
341
368
  omfgContainer: Container;
342
369
  errorBannerContainer: Container;
370
+ modelCycleContainer: Container;
343
371
  editor: CustomEditor;
344
372
  editorContainer: Container;
345
373
  hookWidgetContainerAbove: Container;
@@ -360,6 +388,7 @@ export class InteractiveMode implements InteractiveModeContext {
360
388
  loopLimit: LoopLimitRuntime | undefined = undefined;
361
389
  #loopAutoSubmitTimer: NodeJS.Timeout | undefined;
362
390
  #todoAutoClearTimer: NodeJS.Timeout | undefined;
391
+ #modelCycleClearTimer: NodeJS.Timeout | undefined;
363
392
  todoPhases: TodoPhase[] = [];
364
393
  hideThinkingBlock = false;
365
394
  pendingImages: ImageContent[] = [];
@@ -462,6 +491,8 @@ export class InteractiveMode implements InteractiveModeContext {
462
491
  }
463
492
  this.statusContainer.clear();
464
493
  this.pendingMessagesContainer.clear();
494
+ this.#cancelModelCycleClearTimer();
495
+ this.modelCycleContainer.clear();
465
496
  this.compactionQueuedMessages = [];
466
497
  this.streamingComponent = undefined;
467
498
  this.streamingMessage = undefined;
@@ -508,6 +539,15 @@ export class InteractiveMode implements InteractiveModeContext {
508
539
  this.#handleLspStartupEvent(data as LspStartupEvent);
509
540
  }),
510
541
  );
542
+ this.#eventBusUnsubscribers.push(
543
+ eventBus.on(MCP_CONNECTING_EVENT_CHANNEL, data => {
544
+ if (!isMcpConnectingEvent(data)) {
545
+ logger.warn("Ignoring malformed mcp:connecting event", { data });
546
+ return;
547
+ }
548
+ this.showStatus(formatMCPConnectingMessage(data.serverNames));
549
+ }),
550
+ );
511
551
  }
512
552
 
513
553
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
@@ -524,6 +564,7 @@ export class InteractiveMode implements InteractiveModeContext {
524
564
  this.btwContainer = new Container();
525
565
  this.omfgContainer = new Container();
526
566
  this.errorBannerContainer = new Container();
567
+ this.modelCycleContainer = new Container();
527
568
  this.editor = new CustomEditor(getEditorTheme());
528
569
  this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
529
570
  this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
@@ -533,6 +574,7 @@ export class InteractiveMode implements InteractiveModeContext {
533
574
  this.editor.onAutocompleteUpdate = () => {
534
575
  this.ui.requestRender();
535
576
  };
577
+ this.editor.setShimmerRepaintHandler(() => this.ui.requestComponentRender(this.editor));
536
578
  this.#syncEditorMaxHeight();
537
579
  this.#resizeHandler = () => {
538
580
  this.#syncEditorMaxHeight();
@@ -692,6 +734,7 @@ export class InteractiveMode implements InteractiveModeContext {
692
734
  this.ui.addChild(this.btwContainer);
693
735
  this.ui.addChild(this.omfgContainer);
694
736
  this.ui.addChild(this.errorBannerContainer);
737
+ this.ui.addChild(this.modelCycleContainer);
695
738
  this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
696
739
  this.ui.addChild(this.hookWidgetContainerAbove);
697
740
  this.ui.addChild(this.editorContainer);
@@ -810,8 +853,30 @@ export class InteractiveMode implements InteractiveModeContext {
810
853
  name: cmd.name,
811
854
  description: cmd.description,
812
855
  }));
856
+ // Surface discovered prompt templates in the picker. AgentSession.prompt() expands
857
+ // `expandSlashCommand` before `expandPromptTemplate`, and builtin command
858
+ // execution resolves aliases before template expansion. Mirror that command
859
+ // resolution order by skipping templates whose names already appear in any
860
+ // builtin/hook/custom/skill/file command token.
861
+ const reservedNames = new Set<string>();
862
+ for (const command of this.#pendingSlashCommands) {
863
+ reservedNames.add(command.name);
864
+ for (const alias of command.aliases ?? []) reservedNames.add(alias);
865
+ }
866
+ for (const command of fileSlashCommands) {
867
+ reservedNames.add(command.name);
868
+ for (const alias of command.aliases ?? []) reservedNames.add(alias);
869
+ }
870
+ const promptTemplateCommands: SlashCommand[] = this.session.promptTemplates
871
+ .filter(template => !reservedNames.has(template.name))
872
+ .map(template => ({
873
+ name: template.name,
874
+ // `PromptTemplate.description` from `loadTemplatesFromDir` already includes the
875
+ // source suffix (e.g. "Review code (project)"), so pass it through verbatim.
876
+ description: template.description,
877
+ }));
813
878
  const autocompleteProvider = this.#inputController.createAutocompleteProvider(
814
- [...this.#pendingSlashCommands, ...fileSlashCommands],
879
+ [...this.#pendingSlashCommands, ...fileSlashCommands, ...promptTemplateCommands],
815
880
  basePath,
816
881
  );
817
882
  this.editor.setAutocompleteProvider(autocompleteProvider);
@@ -905,6 +970,14 @@ export class InteractiveMode implements InteractiveModeContext {
905
970
  this.#goalContinuationTimer = undefined;
906
971
  if (!this.onInputCallback) return;
907
972
  if (!this.goalModeEnabled || this.goalModePaused) return;
973
+ // The 800ms timer can outlive the idle window that scheduled it: a
974
+ // `/goal set` taken via the streaming branch (or any extension/hook
975
+ // path that starts a turn while we wait) leaves the agent busy. Firing
976
+ // the continuation now would route through `submitInteractiveInput` →
977
+ // `promptCustomMessage` with no `streamingBehavior` and resurface
978
+ // `AgentBusyError`. Drop this tick; `#handleGoalSessionEvent` reschedules
979
+ // on the next `agent_end`.
980
+ if (this.#isAutoSubmitBlocked()) return;
908
981
  if (this.#pendingSubmittedInput) return;
909
982
  if (this.editor.getText().trim().length > 0) return;
910
983
  if ((this.pendingImages?.length ?? 0) > 0) return;
@@ -928,7 +1001,7 @@ export class InteractiveMode implements InteractiveModeContext {
928
1001
  }
929
1002
  }
930
1003
 
931
- #isLoopAutoSubmitBlocked(): boolean {
1004
+ #isAutoSubmitBlocked(): boolean {
932
1005
  return this.session.isStreaming || this.session.isCompacting || this.session.hasPostPromptWork;
933
1006
  }
934
1007
 
@@ -938,7 +1011,7 @@ export class InteractiveMode implements InteractiveModeContext {
938
1011
  this.disableLoopMode("Loop time limit reached. Loop mode disabled.");
939
1012
  return;
940
1013
  }
941
- if (this.#isLoopAutoSubmitBlocked()) {
1014
+ if (this.#isAutoSubmitBlocked()) {
942
1015
  this.#deferLoopAutoSubmit(() => this.#submitLoopPromptWhenReady(prompt));
943
1016
  return;
944
1017
  }
@@ -947,7 +1020,7 @@ export class InteractiveMode implements InteractiveModeContext {
947
1020
 
948
1021
  async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
949
1022
  if (!this.loopModeEnabled || this.loopPrompt !== prompt || !this.onInputCallback) return;
950
- if (this.#isLoopAutoSubmitBlocked()) {
1023
+ if (this.#isAutoSubmitBlocked()) {
951
1024
  this.#deferLoopAutoSubmit(() => {
952
1025
  void this.#runLoopIteration(action, prompt);
953
1026
  });
@@ -1140,10 +1213,7 @@ export class InteractiveMode implements InteractiveModeContext {
1140
1213
  }
1141
1214
 
1142
1215
  #computeEditorMaxHeight(): number {
1143
- const rows = this.ui.terminal.rows;
1144
- const terminalRows = Number.isFinite(rows) && rows > 0 ? rows : EDITOR_FALLBACK_ROWS;
1145
- const maxHeight = terminalRows - EDITOR_RESERVED_ROWS;
1146
- return Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, maxHeight));
1216
+ return computeEditorMaxHeight(this.ui.terminal.rows);
1147
1217
  }
1148
1218
 
1149
1219
  #syncEditorMaxHeight(): void {
@@ -1343,6 +1413,41 @@ export class InteractiveMode implements InteractiveModeContext {
1343
1413
  this.#todoAutoClearTimer.unref?.();
1344
1414
  }
1345
1415
 
1416
+ /**
1417
+ * Render the ctrl+p model-role cycle chip track into its own anchored
1418
+ * container (just above the editor), mirroring the todo HUD: the container is
1419
+ * cleared and rebuilt in place on every cycle, so rapid presses or concurrent
1420
+ * chat activity can never stack duplicate tracks into the scrollback.
1421
+ */
1422
+ showModelCycleTrack(track: string): void {
1423
+ this.#renderModelCycleTrack(track);
1424
+ this.#syncModelCycleClearTimer();
1425
+ this.ui.requestRender();
1426
+ }
1427
+
1428
+ #renderModelCycleTrack(track: string | null): void {
1429
+ this.modelCycleContainer.clear();
1430
+ if (!track) return;
1431
+ this.modelCycleContainer.addChild(new Spacer(1));
1432
+ this.modelCycleContainer.addChild(new Text(track, 1, 0));
1433
+ }
1434
+
1435
+ #cancelModelCycleClearTimer(): void {
1436
+ if (!this.#modelCycleClearTimer) return;
1437
+ clearTimeout(this.#modelCycleClearTimer);
1438
+ this.#modelCycleClearTimer = undefined;
1439
+ }
1440
+
1441
+ #syncModelCycleClearTimer(): void {
1442
+ this.#cancelModelCycleClearTimer();
1443
+ this.#modelCycleClearTimer = setTimeout(() => {
1444
+ this.#modelCycleClearTimer = undefined;
1445
+ this.#renderModelCycleTrack(null);
1446
+ this.ui.requestRender();
1447
+ }, MODEL_CYCLE_TRACK_CLEAR_MS);
1448
+ this.#modelCycleClearTimer.unref?.();
1449
+ }
1450
+
1346
1451
  #getActivePhase(phases: TodoPhase[]): TodoPhase | undefined {
1347
1452
  const nonEmpty = phases.filter(phase => phase.tasks.length > 0);
1348
1453
  const active = nonEmpty.find(phase =>
@@ -1736,7 +1841,40 @@ export class InteractiveMode implements InteractiveModeContext {
1736
1841
  });
1737
1842
  }
1738
1843
 
1739
- async #exitPlanMode(options?: { silent?: boolean; paused?: boolean }): Promise<void> {
1844
+ async #restorePlanPreviousModel(prev: { model: Model; thinkingLevel?: ThinkingLevel }): Promise<void> {
1845
+ if (modelsAreEqual(this.session.model, prev.model)) {
1846
+ // Same model — only thinking level may differ. Avoid setModelTemporary()
1847
+ // which would reset provider-side sessions and break continuity.
1848
+ this.session.setThinkingLevel(prev.thinkingLevel);
1849
+ } else if (this.session.isStreaming) {
1850
+ this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
1851
+ } else {
1852
+ await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
1853
+ }
1854
+ }
1855
+
1856
+ /**
1857
+ * Idempotent post-compaction model transition for the plan-approval compact
1858
+ * path. The deferred pre-plan state is consumed on first application, so a
1859
+ * second call (the before-flush hook vs. the short-circuit fallback) is a
1860
+ * no-op. "failed" intentionally stays on the plan model — the context is
1861
+ * intact and we dispatch best-effort.
1862
+ */
1863
+ async #applyDeferredPlanModelTransition(
1864
+ outcome: CompactionOutcome | undefined,
1865
+ executionModel: ResolvedRoleModel | undefined,
1866
+ ): Promise<void> {
1867
+ const deferredPrev = this.#planModePreviousModelState;
1868
+ if (deferredPrev === undefined || outcome === "failed") return;
1869
+ this.#planModePreviousModelState = undefined;
1870
+ if (executionModel) {
1871
+ await this.#applyPlanExecutionModel(executionModel);
1872
+ } else {
1873
+ await this.#restorePlanPreviousModel(deferredPrev);
1874
+ }
1875
+ }
1876
+
1877
+ async #exitPlanMode(options?: { silent?: boolean; paused?: boolean; deferModelRestore?: boolean }): Promise<void> {
1740
1878
  if (!this.planModeEnabled) {
1741
1879
  return;
1742
1880
  }
@@ -1746,23 +1884,18 @@ export class InteractiveMode implements InteractiveModeContext {
1746
1884
  await this.session.setActiveToolsByName(previousTools);
1747
1885
  }
1748
1886
  if (this.#planModePreviousModelState) {
1749
- const prev = this.#planModePreviousModelState;
1750
- if (modelsAreEqual(this.session.model, prev.model)) {
1751
- // Same model — only thinking level may differ. Avoid setModelTemporary()
1752
- // which would reset provider-side sessions (openai-responses/Codex) and
1753
- // break conversation continuity.
1754
- this.session.setThinkingLevel(prev.thinkingLevel);
1755
- } else if (this.session.isStreaming) {
1756
- this.#pendingModelSwitch = { model: prev.model, thinkingLevel: prev.thinkingLevel };
1757
- } else {
1758
- await this.session.setModelTemporary(prev.model, prev.thinkingLevel);
1887
+ if (!options?.deferModelRestore) {
1888
+ await this.#restorePlanPreviousModel(this.#planModePreviousModelState);
1759
1889
  }
1760
1890
  // If #applyPlanModeModel queued a deferred switch to the plan-role model
1761
1891
  // (because the session was streaming on entry), drop it now: we are
1762
1892
  // leaving plan mode, so flushing it on the next agent_end would land the
1763
1893
  // session on the plan-role model after the user has exited plan mode
1764
- // (issue #816). Only clear when the pending target matches the plan-role
1765
- // model leave any unrelated user-queued switch intact.
1894
+ // (issue #816). This runs even when deferModelRestore is set
1895
+ // (compact-approval path): otherwise the stale plan switch survives and
1896
+ // flushPendingModelSwitch() later clobbers the restored/execution model.
1897
+ // Only clear when the pending target matches the plan-role model — leave
1898
+ // any unrelated user-queued switch intact.
1766
1899
  const pending = this.#pendingModelSwitch;
1767
1900
  if (pending) {
1768
1901
  const planResolution = this.session.resolveRoleModelWithThinking("plan");
@@ -1777,7 +1910,7 @@ export class InteractiveMode implements InteractiveModeContext {
1777
1910
  this.planModePaused = options?.paused ?? false;
1778
1911
  this.planModePlanFilePath = undefined;
1779
1912
  this.#planModePreviousTools = undefined;
1780
- this.#planModePreviousModelState = undefined;
1913
+ if (!options?.deferModelRestore) this.#planModePreviousModelState = undefined;
1781
1914
  this.#updatePlanModeStatus();
1782
1915
  const paused = options?.paused ?? false;
1783
1916
  this.sessionManager.appendModeChange(paused ? "plan_paused" : "none");
@@ -2117,7 +2250,11 @@ export class InteractiveMode implements InteractiveModeContext {
2117
2250
  }
2118
2251
  let compactOutcome: CompactionOutcome | undefined;
2119
2252
  try {
2120
- await this.#exitPlanMode({ silent: true, paused: false });
2253
+ await this.#exitPlanMode({
2254
+ silent: true,
2255
+ paused: false,
2256
+ deferModelRestore: options.compactBeforeExecute === true,
2257
+ });
2121
2258
 
2122
2259
  if (!options.preserveContext) {
2123
2260
  await this.handleClearCommand();
@@ -2146,7 +2283,9 @@ export class InteractiveMode implements InteractiveModeContext {
2146
2283
  // the try/finally is idempotent and kept for the !compactBeforeExecute
2147
2284
  // branch.
2148
2285
  this.session.setPlanReferencePath(options.planFilePath);
2149
- compactOutcome = await this.handleCompactCommand(compactionPrompt);
2286
+ compactOutcome = await this.handleCompactCommand(compactionPrompt, outcome =>
2287
+ this.#applyDeferredPlanModelTransition(outcome, options.executionModel),
2288
+ );
2150
2289
  }
2151
2290
  } finally {
2152
2291
  // Unconditional clear. Idempotent: a no-op when the flag was never set
@@ -2163,22 +2302,33 @@ export class InteractiveMode implements InteractiveModeContext {
2163
2302
  }
2164
2303
  this.session.setPlanReferencePath(options.planFilePath);
2165
2304
 
2305
+ // Resolve the deferred plan-approval model transition. On the compact path
2306
+ // the before-flush hook passed to handleCompactCommand already ran this (so
2307
+ // any input queued during compaction executed on the post-compaction
2308
+ // model); the re-run here is idempotent and covers the short-circuit where
2309
+ // compaction never executed. It runs for "cancelled" too — the operator
2310
+ // aborted only the compaction, not the approval — so the next turn no longer
2311
+ // lands on the plan model. "failed" stays on the plan model (context
2312
+ // intact) and dispatches best-effort.
2313
+ if (options.compactBeforeExecute) {
2314
+ await this.#applyDeferredPlanModelTransition(compactOutcome, options.executionModel);
2315
+ } else {
2316
+ await this.#applyPlanExecutionModel(options.executionModel);
2317
+ }
2318
+
2166
2319
  if (compactOutcome === "cancelled") {
2167
2320
  // Explicit abort: honor it. `executeCompaction` already surfaced
2168
- // `showError("Compaction cancelled")` to the operator; we add the
2169
- // deferred-dispatch warning and exit. `markPlanReferenceSent` is
2170
- // intentionally skipped here: `#planReferenceSent` stays false, so
2171
- // `AgentSession.#buildPlanReferenceMessage` will inject the plan
2172
- // reference on the operator's next `prompt()` call. If we marked it
2173
- // sent here, the executor's first turn would have no plan context.
2321
+ // `showError("Compaction cancelled")`; we add the deferred-dispatch
2322
+ // warning and exit without dispatching the synthetic plan-approved
2323
+ // prompt. `markPlanReferenceSent` stays unset so
2324
+ // `AgentSession.#buildPlanReferenceMessage` injects the plan reference
2325
+ // on the operator's next `prompt()` call.
2174
2326
  this.showWarning(
2175
2327
  "Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
2176
2328
  );
2177
2329
  return;
2178
2330
  }
2179
2331
 
2180
- await this.#applyPlanExecutionModel(options.executionModel);
2181
-
2182
2332
  // Approved plans land in a fresh (or compacted) session whose first user-visible
2183
2333
  // turn is the synthetic plan-approved prompt — that path bypasses the
2184
2334
  // input-controller's title generation. Seed an auto-name from the plan title
@@ -2201,6 +2351,15 @@ export class InteractiveMode implements InteractiveModeContext {
2201
2351
  planFilePath: options.planFilePath,
2202
2352
  contextPreserved: options.preserveContext === true,
2203
2353
  });
2354
+ // The executor's first turn must start on an idle session. The agent may still
2355
+ // be streaming the post-`resolve` continuation (Agent.#emit is fire-and-forget)
2356
+ // or a turn kicked off by the compaction/clear above; prompt() would then throw
2357
+ // AgentBusyError ("Failed to finalize approved plan"). Abort the now-irrelevant
2358
+ // in-flight turn first — abort() bumps the prompt generation and cancels pending
2359
+ // continuations, so nothing re-streams in the synchronous gap before prompt().
2360
+ if (this.session.isStreaming) {
2361
+ await this.session.abort();
2362
+ }
2204
2363
  await this.session.prompt(planModePrompt, { synthetic: true });
2205
2364
  }
2206
2365
 
@@ -2221,6 +2380,19 @@ export class InteractiveMode implements InteractiveModeContext {
2221
2380
  await this.#exitPlanMode({ paused: true });
2222
2381
  return;
2223
2382
  }
2383
+ if (this.planModePaused && !initialPrompt) {
2384
+ // No-arg third toggle: paused → off. Tools, model, and plan state were
2385
+ // already restored by the prior #exitPlanMode({ paused: true }); only the
2386
+ // paused flag, the reentry marker, and the session mode entry remain.
2387
+ // Prompted /plan invocations fall through to #enterPlanMode below so the
2388
+ // supplied prompt is still submitted as the first plan-mode turn.
2389
+ this.planModePaused = false;
2390
+ this.#planModeHasEntered = false;
2391
+ this.#updatePlanModeStatus();
2392
+ this.sessionManager.appendModeChange("none");
2393
+ this.showStatus("Plan mode disabled.");
2394
+ return;
2395
+ }
2224
2396
  if (!this.session.settings.get("plan.enabled")) {
2225
2397
  this.showWarning("Plan mode is disabled. Enable it in settings (plan.enabled).");
2226
2398
  return;
@@ -2302,6 +2474,70 @@ export class InteractiveMode implements InteractiveModeContext {
2302
2474
  this.showError(error instanceof Error ? error.message : String(error));
2303
2475
  }
2304
2476
  }
2477
+ async handleGuidedGoalCommand(rest?: string): Promise<void> {
2478
+ try {
2479
+ if (this.planModeEnabled || this.planModePaused) {
2480
+ this.showWarning("Exit plan mode first.");
2481
+ return;
2482
+ }
2483
+ if (!this.session.settings.get("goal.enabled")) {
2484
+ this.showWarning("Goal mode is disabled. Enable it in settings (goal.enabled).");
2485
+ return;
2486
+ }
2487
+ if (this.goalModeEnabled) {
2488
+ this.showStatus("Goal mode is already active. Use /goal to manage it, or /goal drop to start over.");
2489
+ return;
2490
+ }
2491
+ if (this.#getPausedGoalState()) {
2492
+ this.showWarning("Resume the current goal first, or drop it before setting a new objective.");
2493
+ return;
2494
+ }
2495
+
2496
+ const initial = rest?.trim()
2497
+ ? rest.trim()
2498
+ : (await this.showHookEditor("Guided goal", undefined, undefined, { promptStyle: true }))?.trim();
2499
+ if (!initial) return;
2500
+
2501
+ const messages: GuidedGoalMessage[] = [{ role: "user", content: initial }];
2502
+ let latestDraftObjective: string | undefined;
2503
+ for (let turn = 0; turn < 6; turn++) {
2504
+ const result = await runGuidedGoalTurn(this.session, { messages });
2505
+ if (result.objective?.trim()) latestDraftObjective = result.objective.trim();
2506
+ if (result.kind === "question") {
2507
+ messages.push({ role: "assistant", content: result.question });
2508
+ const answer = (
2509
+ await this.showHookEditor(result.question, undefined, undefined, { promptStyle: true })
2510
+ )?.trim();
2511
+ if (!answer) return;
2512
+ messages.push({ role: "user", content: answer });
2513
+ continue;
2514
+ }
2515
+
2516
+ const finalObjective = (
2517
+ await this.showHookEditor("Review guided goal", result.objective, undefined, { promptStyle: true })
2518
+ )?.trim();
2519
+ if (!finalObjective) return;
2520
+ await this.#startGoalFromObjective(finalObjective);
2521
+ return;
2522
+ }
2523
+
2524
+ // Hit the turn cap without an explicit `ready`. Rather than discard the whole interview,
2525
+ // salvage the latest non-empty model objective draft seen on any earlier turn. A final
2526
+ // question turn may omit `objective`; that must not erase a usable draft.
2527
+ if (latestDraftObjective) {
2528
+ const finalObjective = (
2529
+ await this.showHookEditor("Review guided goal", latestDraftObjective, undefined, { promptStyle: true })
2530
+ )?.trim();
2531
+ if (finalObjective) {
2532
+ await this.#startGoalFromObjective(finalObjective);
2533
+ return;
2534
+ }
2535
+ }
2536
+ this.showWarning("Guided goal setup needs more detail. Run /guided-goal again with a narrower objective.");
2537
+ } catch (error) {
2538
+ this.showError(error instanceof Error ? error.message : String(error));
2539
+ }
2540
+ }
2305
2541
 
2306
2542
  async #dispatchGoalSubcommand(sub: GoalSubcommand, rest: string): Promise<void> {
2307
2543
  switch (sub) {
@@ -2585,11 +2821,13 @@ export class InteractiveMode implements InteractiveModeContext {
2585
2821
  return;
2586
2822
  }
2587
2823
  // Capture the operator's tier choice and hand it to #approvePlan, which
2588
- // applies it AFTER #exitPlanMode. #exitPlanMode restores
2824
+ // applies it AFTER #exitPlanMode. #exitPlanMode normally restores
2589
2825
  // #planModePreviousModelState (the model from before plan mode), so
2590
2826
  // applying the slider choice any earlier would be silently reverted —
2591
2827
  // the bug that made "continue with slow" keep executing on the default
2592
- // model. Deferred application also survives newSession()/compaction.
2828
+ // model. For compact-context approval, the plan model is kept through
2829
+ // compaction, then a successful compaction transitions to the slider model
2830
+ // (or restores the pre-plan model when no slider choice was made).
2593
2831
  // `cycle.currentIndex` is exactly that restored model, so any chosen tier
2594
2832
  // differing from it needs an explicit executionModel — this also covers
2595
2833
  // leaving the slider on its `default` anchor while planning ran elsewhere.
@@ -2790,6 +3028,7 @@ export class InteractiveMode implements InteractiveModeContext {
2790
3028
  nextEditor.onAutocompleteUpdate = () => {
2791
3029
  this.ui.requestRender();
2792
3030
  };
3031
+ nextEditor.setShimmerRepaintHandler(() => this.ui.requestComponentRender(this.editor));
2793
3032
  nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
2794
3033
  if (this.historyStorage) {
2795
3034
  nextEditor.setHistoryStorage(this.historyStorage);
@@ -3181,7 +3420,11 @@ export class InteractiveMode implements InteractiveModeContext {
3181
3420
  await this.#sttController.toggle(this.editor, {
3182
3421
  showWarning: (msg: string) => this.showWarning(msg),
3183
3422
  showStatus: (msg: string) => this.showStatus(msg),
3423
+ requestRender: () => this.ui.requestRender(),
3184
3424
  onStateChange: (state: SttState) => {
3425
+ // Duck assistant speech while the user is talking (push-to-talk); restore after.
3426
+ if (state === "recording") vocalizer.duck();
3427
+ else vocalizer.unduck();
3185
3428
  if (state === "recording") {
3186
3429
  this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
3187
3430
  this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
@@ -3252,8 +3495,8 @@ export class InteractiveMode implements InteractiveModeContext {
3252
3495
  await this.#selectorController.showDebugSelector();
3253
3496
  }
3254
3497
 
3255
- showAgentHub(): void {
3256
- this.#selectorController.showAgentHub(this.#observerRegistry);
3498
+ showAgentHub(options?: { requireContent?: boolean }): void {
3499
+ this.#selectorController.showAgentHub(this.#observerRegistry, options);
3257
3500
  }
3258
3501
 
3259
3502
  resetObserverRegistry(): void {
@@ -3279,8 +3522,11 @@ export class InteractiveMode implements InteractiveModeContext {
3279
3522
  await controller.handle(text);
3280
3523
  }
3281
3524
 
3282
- handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
3283
- return this.#commandController.handleCompactCommand(customInstructions);
3525
+ handleCompactCommand(
3526
+ customInstructions?: string,
3527
+ beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
3528
+ ): Promise<CompactionOutcome> {
3529
+ return this.#commandController.handleCompactCommand(customInstructions, beforeFlush);
3284
3530
  }
3285
3531
 
3286
3532
  handleHandoffCommand(customInstructions?: string): Promise<void> {
@@ -1,6 +1,6 @@
1
- import { highlightOrchestrate } from "./orchestrate";
2
- import { highlightUltrathink } from "./ultrathink";
3
- import { highlightWorkflow } from "./workflow";
1
+ import { containsOrchestrate, highlightOrchestrate } from "./orchestrate";
2
+ import { containsUltrathink, highlightUltrathink } from "./ultrathink";
3
+ import { containsWorkflow, highlightWorkflow } from "./workflow";
4
4
 
5
5
  /**
6
6
  * Gradient-highlight every magic keyword ("ultrathink", "orchestrate",
@@ -14,7 +14,29 @@ import { highlightWorkflow } from "./workflow";
14
14
  * pass the surrounding text color when decorating already-colored content (e.g.
15
15
  * a themed message bubble) so the gradient does not bleed into the rest of the
16
16
  * line. Defaults to a plain foreground reset for default-colored editor text.
17
+ *
18
+ * `phase` ∈ [0, 1) cyclically rotates each gradient — the editor passes a
19
+ * `Date.now()`-derived value to animate a Claude-Code-style shimmer while a
20
+ * keyword is on screen and the prompt is focused; sent message bubbles omit it
21
+ * to keep the static gradient.
22
+ */
23
+ export function highlightMagicKeywords(text: string, resetTo?: string, phase?: number): string {
24
+ return highlightWorkflow(
25
+ highlightOrchestrate(highlightUltrathink(text, resetTo, phase), resetTo, phase),
26
+ resetTo,
27
+ phase,
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Cheap test for "does this text contain any magic keyword as standalone prose?".
33
+ * Short-circuits on a substring probe before paying for the markdown-aware
34
+ * prose check, so the common "no keyword in buffer" path is just three
35
+ * `String#indexOf`s. Used by the live editor to gate the shimmer timer.
17
36
  */
18
- export function highlightMagicKeywords(text: string, resetTo?: string): string {
19
- return highlightWorkflow(highlightOrchestrate(highlightUltrathink(text, resetTo), resetTo), resetTo);
37
+ export function hasMagicKeyword(text: string): boolean {
38
+ if (!text.includes("ultrathink") && !text.includes("orchestrate") && !text.includes("workflowz")) {
39
+ return false;
40
+ }
41
+ return containsUltrathink(text) || containsOrchestrate(text) || containsWorkflow(text);
20
42
  }