@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -55,7 +55,7 @@ import { reset as resetCapabilities } from "../capability";
55
55
  import type { CollabGuestLink } from "../collab/guest";
56
56
  import type { CollabHost } from "../collab/host";
57
57
  import { KeybindingsManager } from "../config/keybindings";
58
- import type { ResolvedModelRoleValue } from "../config/model-resolver";
58
+ import { formatModelString, type ResolvedModelRoleValue } from "../config/model-resolver";
59
59
  import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
60
60
  import {
61
61
  isSettingsInitialized,
@@ -130,8 +130,19 @@ import type { EventBus } from "../utils/event-bus";
130
130
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
131
131
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
132
132
  import { messageHasDisplayableThinking } from "../utils/thinking-display";
133
- import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
134
- import { aggregateVibeWorkerTokensPerSecond, VibeSessionRegistry } from "../vibe/runtime";
133
+ import {
134
+ disposeTerminalTitleState,
135
+ popTerminalTitle,
136
+ pushTerminalTitle,
137
+ setSessionTerminalTitle,
138
+ setTerminalTitleStateEnabled,
139
+ } from "../utils/title-generator";
140
+ import {
141
+ aggregateVibeWorkerTokensPerSecond,
142
+ type VibeOwnerScope,
143
+ type VibeParentSession,
144
+ VibeSessionRegistry,
145
+ } from "../vibe/runtime";
135
146
  import type { AssistantMessageComponent } from "./components/assistant-message";
136
147
  import type { BashExecutionComponent } from "./components/bash-execution";
137
148
  import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
@@ -142,7 +153,7 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
142
153
  import type { HookEditorComponent } from "./components/hook-editor";
143
154
  import type { HookInputComponent } from "./components/hook-input";
144
155
  import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
145
- import { PlanReviewOverlay } from "./components/plan-review-overlay";
156
+ import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
146
157
  import { StatusLineComponent } from "./components/status-line";
147
158
  import type { ToolExecutionHandle } from "./components/tool-execution";
148
159
  import { TranscriptContainer } from "./components/transcript-container";
@@ -152,6 +163,7 @@ import { CommandController } from "./controllers/command-controller";
152
163
  import { EventController } from "./controllers/event-controller";
153
164
  import { ExtensionUiController } from "./controllers/extension-ui-controller";
154
165
  import { InputController } from "./controllers/input-controller";
166
+ import { LiveCommandController } from "./controllers/live-command-controller";
155
167
  import { MCPCommandController } from "./controllers/mcp-command-controller";
156
168
  import { OmfgController } from "./controllers/omfg-controller";
157
169
  import { SelectorController } from "./controllers/selector-controller";
@@ -543,6 +555,8 @@ export class InteractiveMode implements InteractiveModeContext {
543
555
  #planModePreviousTools: string[] | undefined;
544
556
  #goalModePreviousTools: string[] | undefined;
545
557
  #vibeModePreviousTools: string[] | undefined;
558
+ #vibeModeOwnerScope: VibeOwnerScope | undefined;
559
+ #vibeScopeSuspendedForSwitch = false;
546
560
  #goalContinuationTimer: NodeJS.Timeout | undefined;
547
561
  #goalTurnHadToolCalls = false;
548
562
  #goalContinuationTurnInFlight = false;
@@ -555,6 +569,10 @@ export class InteractiveMode implements InteractiveModeContext {
555
569
  #planReviewOverlay: PlanReviewOverlay | undefined;
556
570
  #planReviewOverlayHandle: OverlayHandle | undefined;
557
571
  #planReviewCancel: (() => void) | undefined;
572
+ /** Serializable review annotations keyed by the resolved plan file path. */
573
+ #planReviewAnnotationState = new Map<string, PlanReviewAnnotationState>();
574
+ /** Annotation state held until the associated queued refinement actually starts. */
575
+ #planReviewAnnotationStateBySubmission = new WeakMap<SubmittedUserInput, string>();
558
576
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
559
577
  mcpManager?: MCPManager;
560
578
  readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
@@ -564,6 +582,7 @@ export class InteractiveMode implements InteractiveModeContext {
564
582
  readonly #omfgController: OmfgController;
565
583
  readonly #commandController: CommandController;
566
584
  readonly #todoCommandController: TodoCommandController;
585
+ readonly #liveCommandController: LiveCommandController;
567
586
  readonly #eventController: EventController;
568
587
  get eventController(): EventController {
569
588
  return this.#eventController;
@@ -773,6 +792,7 @@ export class InteractiveMode implements InteractiveModeContext {
773
792
  this.#eventController = new EventController(this);
774
793
  this.#commandController = new CommandController(this);
775
794
  this.#todoCommandController = new TodoCommandController(this);
795
+ this.#liveCommandController = new LiveCommandController(this);
776
796
  this.#selectorController = new SelectorController(this);
777
797
  this.#focusController = new SessionFocusController(this);
778
798
  this.#inputController = new InputController(this);
@@ -976,6 +996,7 @@ export class InteractiveMode implements InteractiveModeContext {
976
996
  // the initial welcome frame does not append over the previous run's scrollback.
977
997
  this.ui.start({ clearScrollback: options.clearInitialTerminalHistory === true });
978
998
  pushTerminalTitle();
999
+ setTerminalTitleStateEnabled(this.settings.get("tui.titleState"));
979
1000
  setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
980
1001
  this.updateEditorBorderColor();
981
1002
  // Single side-effect point for title changes: every setSessionName caller
@@ -997,6 +1018,10 @@ export class InteractiveMode implements InteractiveModeContext {
997
1018
  await this.initHooksAndCustomTools();
998
1019
 
999
1020
  // Restore mode from session (e.g. plan mode on resume)
1021
+ this.session.setSessionBeforeSwitchReconciler?.(async () => {
1022
+ await this.#liveCommandController.stop();
1023
+ await this.#quiesceVibeForSessionSwitch();
1024
+ });
1000
1025
  this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession({ preserveActiveGoal: true }));
1001
1026
  await this.#reconcileModeFromSession();
1002
1027
 
@@ -1566,6 +1591,11 @@ export class InteractiveMode implements InteractiveModeContext {
1566
1591
  return false;
1567
1592
  }
1568
1593
  input.started = true;
1594
+ const annotationStateKey = this.#planReviewAnnotationStateBySubmission.get(input);
1595
+ if (annotationStateKey) {
1596
+ this.#planReviewAnnotationStateBySubmission.delete(input);
1597
+ this.#planReviewAnnotationState.delete(annotationStateKey);
1598
+ }
1569
1599
  return true;
1570
1600
  }
1571
1601
 
@@ -1756,6 +1786,8 @@ export class InteractiveMode implements InteractiveModeContext {
1756
1786
  return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1757
1787
  case "abandoned":
1758
1788
  return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1789
+ case "blocked":
1790
+ return theme.fg("warning", `${prefix}${checkbox.unchecked} ${todo.content} (blocked)`) + marker;
1759
1791
  default:
1760
1792
  if (matched) return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1761
1793
  return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
@@ -1775,12 +1807,15 @@ export class InteractiveMode implements InteractiveModeContext {
1775
1807
  }
1776
1808
 
1777
1809
  /**
1778
- * Auto-complete any pending/in_progress todo whose content matches a
1779
- * subagent that has finished successfully. Fires on every observer
1810
+ * Auto-complete any open todo (pending/in_progress/blocked) whose content
1811
+ * matches a subagent that has finished successfully. Fires on every observer
1780
1812
  * `onChange` so the visual state stays in sync with subagent lifecycle
1781
- * without requiring the agent to issue a follow-up `todo`. Failed
1782
- * and aborted subagents are intentionally NOT auto-completed those
1783
- * stay open so the user (or the next agent turn) can decide what to do.
1813
+ * without requiring the agent to issue a follow-up `todo`. A todo `block`ed
1814
+ * while waiting on a detached subagent is included: that subagent completing
1815
+ * is exactly the unblock signal, and blocked todos are excluded from the stop
1816
+ * reminder, so leaving it blocked would strand it silently. Failed and aborted
1817
+ * subagents are intentionally NOT auto-completed — those stay open so the user
1818
+ * (or the next agent turn) can decide what to do.
1784
1819
  *
1785
1820
  * Idempotent: only flips open tasks, never re-touches completed ones.
1786
1821
  */
@@ -1799,10 +1834,14 @@ export class InteractiveMode implements InteractiveModeContext {
1799
1834
  const next: TodoPhase[] = this.todoPhases.map(phase => ({
1800
1835
  name: phase.name,
1801
1836
  tasks: phase.tasks.map(task => {
1802
- if (task.status !== "pending" && task.status !== "in_progress") return task;
1837
+ if (task.status !== "pending" && task.status !== "in_progress" && task.status !== "blocked") {
1838
+ return task;
1839
+ }
1803
1840
  if (!todoMatchesAnyDescription(task.content, completedDescs)) return task;
1804
1841
  mutated = true;
1805
- return { ...task, status: "completed" as const };
1842
+ // Drop any blocker note along with the blocked status the wait the
1843
+ // note described is over.
1844
+ return { content: task.content, status: "completed" as const };
1806
1845
  }),
1807
1846
  }));
1808
1847
  if (!mutated) return;
@@ -2058,6 +2097,29 @@ export class InteractiveMode implements InteractiveModeContext {
2058
2097
  this.ui.requestRender();
2059
2098
  }
2060
2099
 
2100
+ #vibeParentSession(): VibeParentSession {
2101
+ return {
2102
+ getAgentId: () => this.session.getAgentId() ?? null,
2103
+ getSessionId: () => this.sessionManager.getSessionId(),
2104
+ getSessionFile: () => this.sessionManager.getSessionFile() ?? null,
2105
+ sessionManager: this.sessionManager,
2106
+ asyncJobManager: this.session.asyncJobManager,
2107
+ settings: this.session.settings,
2108
+ // Resolve restored/switched-to workers against this session's active model
2109
+ // (same as the spawn-path ToolSession), not the settings default. This is
2110
+ // the primary fallback in resolveAgentModelPatterns, so the `good` worker's
2111
+ // pi/task inheritance tracks the reopened session's model.
2112
+ getActiveModelString: () => (this.session.model ? formatModelString(this.session.model) : undefined),
2113
+ };
2114
+ }
2115
+
2116
+ async #quiesceVibeForSessionSwitch(): Promise<void> {
2117
+ const ownerScope = this.#vibeModeOwnerScope;
2118
+ if (!this.vibeModeEnabled || !ownerScope) return;
2119
+ await VibeSessionRegistry.global().suspendScope(ownerScope, this.session.asyncJobManager);
2120
+ this.#vibeScopeSuspendedForSwitch = true;
2121
+ }
2122
+
2061
2123
  #updateGoalModeStatus(): void {
2062
2124
  const status =
2063
2125
  this.goalModeEnabled || this.goalModePaused
@@ -2241,7 +2303,10 @@ export class InteractiveMode implements InteractiveModeContext {
2241
2303
  }
2242
2304
  }
2243
2305
 
2244
- async #clearTransientModeState(): Promise<void> {
2306
+ async #clearTransientModeState(options?: {
2307
+ preserveVibe?: boolean;
2308
+ vibeScopeAlreadySuspended?: boolean;
2309
+ }): Promise<void> {
2245
2310
  if (this.planModeEnabled || this.planModePaused) {
2246
2311
  this.session.setPlanModeState(undefined);
2247
2312
  try {
@@ -2277,23 +2342,40 @@ export class InteractiveMode implements InteractiveModeContext {
2277
2342
  this.#updateGoalModeStatus();
2278
2343
  }
2279
2344
 
2280
- if (this.vibeModeEnabled) {
2281
- await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
2345
+ if (this.vibeModeEnabled && !options?.preserveVibe) {
2346
+ const ownerScope = this.#vibeModeOwnerScope;
2347
+ // This runs only from #reconcileModeFromSession, i.e. after switchSession
2348
+ // already loaded and restored the target session's active tools. The
2349
+ // #vibeModePreviousTools snapshot belongs to the SOURCE session, so
2350
+ // applying it here would clobber the target's tools — strip only the
2351
+ // transient vibe tools and keep the target's active set intact.
2352
+ await this.session.removeVibeToolsPreservingActive();
2282
2353
  this.session.setVibeModeState(undefined);
2283
2354
  this.vibeModeEnabled = false;
2284
2355
  this.#vibeModePreviousTools = undefined;
2285
- await VibeSessionRegistry.global().killAll(
2286
- this.session.getAgentId() ?? MAIN_AGENT_ID,
2287
- this.session.asyncJobManager,
2288
- );
2356
+ this.#vibeModeOwnerScope = undefined;
2357
+ if (ownerScope && !options?.vibeScopeAlreadySuspended) {
2358
+ await VibeSessionRegistry.global().suspendScope(ownerScope, this.session.asyncJobManager);
2359
+ }
2289
2360
  this.#updateVibeModeStatus();
2290
2361
  }
2291
2362
  }
2292
2363
 
2293
2364
  /** Reconcile mode state from session entries on resume/switch. */
2294
2365
  async #reconcileModeFromSession(options?: { preserveActiveGoal?: boolean }): Promise<void> {
2295
- await this.#clearTransientModeState();
2366
+ const vibeScopeAlreadySuspended = this.#vibeScopeSuspendedForSwitch;
2367
+ this.#vibeScopeSuspendedForSwitch = false;
2296
2368
  const sessionContext = this.sessionManager.buildSessionContext();
2369
+ const vibeSession = this.#vibeParentSession();
2370
+ const targetVibeScope = VibeSessionRegistry.global().ownerScope(vibeSession);
2371
+ const preserveVibe =
2372
+ this.vibeModeEnabled &&
2373
+ sessionContext.mode === "vibe" &&
2374
+ this.#vibeModeOwnerScope?.ownerId === targetVibeScope.ownerId &&
2375
+ this.#vibeModeOwnerScope.parentSessionId === targetVibeScope.parentSessionId &&
2376
+ this.#vibeModeOwnerScope.parentSessionFile === targetVibeScope.parentSessionFile;
2377
+ await this.#clearTransientModeState({ preserveVibe, vibeScopeAlreadySuspended });
2378
+ await VibeSessionRegistry.global().rehydrate(vibeSession);
2297
2379
  const goalEnabled = this.session.settings.get("goal.enabled");
2298
2380
  if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
2299
2381
  this.session.goalRuntime.clearAccounting();
@@ -2328,7 +2410,7 @@ export class InteractiveMode implements InteractiveModeContext {
2328
2410
  }
2329
2411
  this.session.goalRuntime.clearAccounting();
2330
2412
  if (sessionContext.mode === "vibe") {
2331
- await this.#enterVibeMode();
2413
+ if (!preserveVibe) await this.#enterVibeMode({ persistModeChange: false });
2332
2414
  return;
2333
2415
  }
2334
2416
  if (!this.session.settings.get("plan.enabled")) {
@@ -2646,6 +2728,8 @@ export class InteractiveMode implements InteractiveModeContext {
2646
2728
  onExternalEditor?: () => void;
2647
2729
  onPlanEdited?: (content: string) => void;
2648
2730
  onFeedbackChange?: (feedback: string) => void;
2731
+ annotationState?: PlanReviewAnnotationState;
2732
+ onAnnotationStateChange?: (state: PlanReviewAnnotationState) => void;
2649
2733
  initialIndex?: number;
2650
2734
  },
2651
2735
  extra?: { slider?: HookSelectorSlider },
@@ -2669,6 +2753,7 @@ export class InteractiveMode implements InteractiveModeContext {
2669
2753
  initialIndex: dialogOptions?.initialIndex,
2670
2754
  slider: extra?.slider,
2671
2755
  externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
2756
+ annotationState: dialogOptions?.annotationState,
2672
2757
  },
2673
2758
  {
2674
2759
  onPick: choice => finish(choice),
@@ -2678,6 +2763,7 @@ export class InteractiveMode implements InteractiveModeContext {
2678
2763
  onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
2679
2764
  onPlanEdited: dialogOptions?.onPlanEdited,
2680
2765
  onFeedbackChange: dialogOptions?.onFeedbackChange,
2766
+ onAnnotationStateChange: dialogOptions?.onAnnotationStateChange,
2681
2767
  },
2682
2768
  );
2683
2769
  this.#planReviewOverlay = overlay;
@@ -2907,7 +2993,7 @@ export class InteractiveMode implements InteractiveModeContext {
2907
2993
  compactBeforeExecute?: boolean;
2908
2994
  executionModel?: ResolvedRoleModel;
2909
2995
  },
2910
- ): Promise<void> {
2996
+ ): Promise<boolean> {
2911
2997
  const previousTools = this.#planModePreviousTools ?? this.session.getEnabledToolNames();
2912
2998
 
2913
2999
  // Mark the pending abort caused by the plan-mode → compaction transition as
@@ -3006,7 +3092,7 @@ export class InteractiveMode implements InteractiveModeContext {
3006
3092
  this.showWarning(
3007
3093
  "Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
3008
3094
  );
3009
- return;
3095
+ return false;
3010
3096
  }
3011
3097
 
3012
3098
  // Approved plans land in a fresh (or compacted) session whose first user-visible
@@ -3048,14 +3134,15 @@ export class InteractiveMode implements InteractiveModeContext {
3048
3134
  // noted below), catch `AgentBusyError` and fall back to the same queue.
3049
3135
  if (this.session.isStreaming) {
3050
3136
  await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3051
- return;
3052
- }
3053
- try {
3054
- await this.session.prompt(planModePrompt, { synthetic: true });
3055
- } catch (error) {
3056
- if (!(error instanceof AgentBusyError)) throw error;
3057
- await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3137
+ } else {
3138
+ try {
3139
+ await this.session.prompt(planModePrompt, { synthetic: true });
3140
+ } catch (error) {
3141
+ if (!(error instanceof AgentBusyError)) throw error;
3142
+ await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3143
+ }
3058
3144
  }
3145
+ return true;
3059
3146
  }
3060
3147
  async #abortPlanApprovalTurnSilently(): Promise<void> {
3061
3148
  this.session.markPlanInternalAbortPending();
@@ -3135,7 +3222,7 @@ export class InteractiveMode implements InteractiveModeContext {
3135
3222
  }
3136
3223
  }
3137
3224
 
3138
- async #enterVibeMode(): Promise<void> {
3225
+ async #enterVibeMode(options?: { persistModeChange?: boolean }): Promise<void> {
3139
3226
  if (this.vibeModeEnabled) {
3140
3227
  return;
3141
3228
  }
@@ -3148,9 +3235,13 @@ export class InteractiveMode implements InteractiveModeContext {
3148
3235
  return;
3149
3236
  }
3150
3237
 
3238
+ const vibeRegistry = VibeSessionRegistry.global();
3239
+ const ownerScope = vibeRegistry.ownerScope(this.#vibeParentSession());
3240
+ vibeRegistry.activateScope(ownerScope);
3151
3241
  const previousTools = this.session.getEnabledToolNames();
3152
3242
  await this.session.activateVibeTools(["read"]);
3153
3243
  this.#vibeModePreviousTools = previousTools;
3244
+ this.#vibeModeOwnerScope = ownerScope;
3154
3245
  this.vibeModeEnabled = true;
3155
3246
  // Suppress cache-miss marker on the next turn: vibe mode changes the
3156
3247
  // injected context, which predictably invalidates the cache.
@@ -3160,7 +3251,7 @@ export class InteractiveMode implements InteractiveModeContext {
3160
3251
  await this.session.sendVibeModeContext({ deliverAs: "steer" });
3161
3252
  }
3162
3253
  this.#updateVibeModeStatus();
3163
- this.sessionManager.appendModeChange("vibe");
3254
+ if (options?.persistModeChange !== false) this.sessionManager.appendModeChange("vibe");
3164
3255
  this.showStatus("Vibe mode enabled. You direct fast/good worker sessions; toolset is read + vibe tools.");
3165
3256
  }
3166
3257
 
@@ -3168,17 +3259,15 @@ export class InteractiveMode implements InteractiveModeContext {
3168
3259
  if (!this.vibeModeEnabled) {
3169
3260
  return;
3170
3261
  }
3262
+ const ownerScope = this.#vibeModeOwnerScope;
3263
+ const killed = await VibeSessionRegistry.global().killAll(this.#vibeParentSession(), ownerScope);
3171
3264
  await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
3172
3265
  this.session.setVibeModeState(undefined);
3173
3266
  this.vibeModeEnabled = false;
3174
3267
  this.#vibeModePreviousTools = undefined;
3268
+ this.#vibeModeOwnerScope = undefined;
3175
3269
  this.lastAssistantUsage = undefined;
3176
- const killed = await VibeSessionRegistry.global().killAll(
3177
- this.session.getAgentId() ?? MAIN_AGENT_ID,
3178
- this.session.asyncJobManager,
3179
- );
3180
3270
  this.#updateVibeModeStatus();
3181
- this.sessionManager.appendModeChange("none");
3182
3271
  this.showStatus(
3183
3272
  killed > 0
3184
3273
  ? `Vibe mode disabled. Killed ${killed} worker session${killed === 1 ? "" : "s"}.`
@@ -3582,6 +3671,7 @@ export class InteractiveMode implements InteractiveModeContext {
3582
3671
  // that the Refine branch re-prompts the model with.
3583
3672
  let editedContent: string | undefined;
3584
3673
  let feedback = "";
3674
+ const annotationStateKey = this.#resolvePlanFilePath(planFilePath);
3585
3675
 
3586
3676
  const choice = await this.showPlanReview(
3587
3677
  planContent,
@@ -3597,6 +3687,11 @@ export class InteractiveMode implements InteractiveModeContext {
3597
3687
  onFeedbackChange: value => {
3598
3688
  feedback = value;
3599
3689
  },
3690
+ annotationState: this.#planReviewAnnotationState.get(annotationStateKey),
3691
+ onAnnotationStateChange: state => {
3692
+ if (state.annotations.length > 0) this.#planReviewAnnotationState.set(annotationStateKey, state);
3693
+ else this.#planReviewAnnotationState.delete(annotationStateKey);
3694
+ },
3600
3695
  disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
3601
3696
  },
3602
3697
  { slider },
@@ -3648,13 +3743,14 @@ export class InteractiveMode implements InteractiveModeContext {
3648
3743
  : -1;
3649
3744
  const executionModel =
3650
3745
  slider && cycle && selectedTierIndex !== restoredIndex ? cycle.models[selectedTierIndex] : undefined;
3651
- await this.#approvePlan(latestPlanContent, {
3746
+ const executionDispatched = await this.#approvePlan(latestPlanContent, {
3652
3747
  planFilePath,
3653
3748
  title: details.title,
3654
3749
  preserveContext: choice !== "Approve and execute",
3655
3750
  compactBeforeExecute: choice === "Approve and compact context",
3656
3751
  executionModel,
3657
3752
  });
3753
+ if (executionDispatched) this.#planReviewAnnotationState.delete(annotationStateKey);
3658
3754
  } catch (error) {
3659
3755
  this.showError(
3660
3756
  `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
@@ -3669,9 +3765,12 @@ export class InteractiveMode implements InteractiveModeContext {
3669
3765
  try {
3670
3766
  if (refinement) {
3671
3767
  if (this.onInputCallback) {
3672
- this.onInputCallback(this.startPendingSubmission({ text: feedback }));
3768
+ const input = this.startPendingSubmission({ text: feedback });
3769
+ this.#planReviewAnnotationStateBySubmission.set(input, annotationStateKey);
3770
+ this.onInputCallback(input);
3673
3771
  } else {
3674
3772
  await this.session.prompt(feedback);
3773
+ this.#planReviewAnnotationState.delete(annotationStateKey);
3675
3774
  }
3676
3775
  } else {
3677
3776
  this.showStatus("Refine plan: enter a follow-up prompt.");
@@ -3746,6 +3845,7 @@ export class InteractiveMode implements InteractiveModeContext {
3746
3845
  this.#stopLoadingAnimation(false);
3747
3846
  }
3748
3847
  this.#cleanupMicAnimation();
3848
+ this.#liveCommandController.dispose();
3749
3849
  this.#cancelTodoAutoClearTimer();
3750
3850
  this.#cancelObserverUiSyncTimer();
3751
3851
  this.#cancelGoalContinuation();
@@ -3788,6 +3888,8 @@ export class InteractiveMode implements InteractiveModeContext {
3788
3888
  if (this.#isShuttingDown) return;
3789
3889
  this.#isShuttingDown = true;
3790
3890
 
3891
+ await this.#liveCommandController.stop();
3892
+
3791
3893
  this.#btwController.dispose();
3792
3894
  this.#omfgController.dispose();
3793
3895
  this.#focusController.dispose();
@@ -3825,6 +3927,10 @@ export class InteractiveMode implements InteractiveModeContext {
3825
3927
  // Drain any in-flight Kitty key release events before stopping.
3826
3928
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
3827
3929
  await this.ui.terminal.drainInput(1000);
3930
+ // Stop the run-state spinner interval BEFORE restoring the shell title, so a
3931
+ // pending tick cannot re-emit an OSC title after `popTerminalTitle` hands the
3932
+ // terminal back (which would leave the parent shell with a `π ⠋ …` tab).
3933
+ disposeTerminalTitleState();
3828
3934
  popTerminalTitle();
3829
3935
  this.stop();
3830
3936
 
@@ -4297,6 +4403,10 @@ export class InteractiveMode implements InteractiveModeContext {
4297
4403
  }
4298
4404
 
4299
4405
  async handleSTTToggle(): Promise<void> {
4406
+ if (this.#liveCommandController.active) {
4407
+ this.showWarning("End live mode before using push-to-talk speech input.");
4408
+ return;
4409
+ }
4300
4410
  if (!settings.get("stt.enabled")) {
4301
4411
  this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
4302
4412
  return;
@@ -4329,6 +4439,15 @@ export class InteractiveMode implements InteractiveModeContext {
4329
4439
  });
4330
4440
  }
4331
4441
 
4442
+ /** Start or stop the Codex-backed realtime voice surface. */
4443
+ async handleLiveCommand(): Promise<void> {
4444
+ if (this.#sttController && this.#sttController.state !== "idle") {
4445
+ this.showWarning("Finish the current speech-to-text capture before starting live mode.");
4446
+ return;
4447
+ }
4448
+ await this.#liveCommandController.handleCommand();
4449
+ }
4450
+
4332
4451
  #setMicCursor(color: { r: number; g: number; b: number }): void {
4333
4452
  this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
4334
4453
  // Theme symbols can be wide (for example, 🎤), so measure the rendered override.
@@ -90,6 +90,8 @@ export async function initializeExtensions(session: AgentSession, options: Initi
90
90
  setModel: model => runExtensionSetModel(session, model),
91
91
  getThinkingLevel: () => session.thinkingLevel,
92
92
  setThinkingLevel: level => session.setThinkingLevel(level),
93
+ getServiceTiers: () => session.serviceTierByFamily,
94
+ setServiceTier: (family, tier) => session.setServiceTierFamily(family, tier),
93
95
  getSessionName: () => session.sessionManager.getSessionName(),
94
96
  setSessionName: async name => {
95
97
  await session.sessionManager.setSessionName(name, "user");
@@ -5,16 +5,20 @@ import {
5
5
  type SgrMouseEvent,
6
6
  truncateToWidth,
7
7
  } from "@oh-my-pi/pi-tui";
8
- import { SETTINGS_SCHEMA } from "../../../config/settings-schema";
9
- import { getSearchProvider, setPreferredSearchProvider } from "../../../web/search/provider";
10
- import { isSearchProviderPreference, type SearchProviderId } from "../../../web/search/types";
8
+ import { getSearchProvider, setSearchProviderOrder } from "../../../web/search/provider";
9
+ import {
10
+ isSearchProviderId,
11
+ SEARCH_PROVIDER_OPTIONS,
12
+ SEARCH_PROVIDER_ORDER,
13
+ type SearchProviderId,
14
+ } from "../../../web/search/types";
11
15
  import { getSelectListTheme, theme } from "../../theme/theme";
12
16
  import type { SetupSceneHost, SetupTab } from "./types";
13
17
 
14
18
  const MAX_VISIBLE = 8;
15
19
 
16
- /** Reuse the settings schema as the single source of truth for labels/descriptions. */
17
- const WEB_SEARCH_ITEMS: readonly SelectItem[] = SETTINGS_SCHEMA["providers.webSearch"].ui.options.map(option => ({
20
+ /** Reuse the shared provider options as the single source of truth for labels/descriptions. */
21
+ const WEB_SEARCH_ITEMS: readonly SelectItem[] = SEARCH_PROVIDER_OPTIONS.map(option => ({
18
22
  value: option.value,
19
23
  label: option.label,
20
24
  description: option.description,
@@ -42,7 +46,8 @@ export class WebSearchTab implements SetupTab {
42
46
 
43
47
  constructor(private readonly host: SetupSceneHost) {
44
48
  this.#list = new SelectList(WEB_SEARCH_ITEMS, MAX_VISIBLE, getSelectListTheme());
45
- const current = host.ctx.settings.get("providers.webSearch");
49
+ const order = host.ctx.settings.get("providers.webSearchOrder");
50
+ const current = Array.isArray(order) && typeof order[0] === "string" ? order[0] : "auto";
46
51
  const index = WEB_SEARCH_ITEMS.findIndex(item => item.value === current);
47
52
  if (index >= 0) this.#list.setSelectedIndex(index);
48
53
  this.#list.onSelectionChange = item => this.#onHighlight(item.value);
@@ -119,9 +124,12 @@ export class WebSearchTab implements SetupTab {
119
124
  }
120
125
 
121
126
  #apply(value: string): void {
122
- if (!isSearchProviderPreference(value)) return;
123
- this.host.ctx.settings.set("providers.webSearch", value);
124
- setPreferredSearchProvider(value);
127
+ if (value !== "auto" && !isSearchProviderId(value)) return;
128
+ // The wizard picks one favorite; persist it as the head of the priority
129
+ // list with the remaining providers in their built-in order (auto = reset).
130
+ const order = value === "auto" ? [] : [value, ...SEARCH_PROVIDER_ORDER.filter(id => id !== value)];
131
+ this.host.ctx.settings.set("providers.webSearchOrder", order);
132
+ setSearchProviderOrder(order);
125
133
  const label = WEB_SEARCH_ITEMS.find(item => item.value === value)?.label ?? value;
126
134
  this.#status = [theme.fg("success", `${theme.status.success} Web search set to ${label}`)];
127
135
  if (value !== "auto" && this.#availability.get(value as SearchProviderId) === false) {
@@ -20,7 +20,7 @@ import { defaultThemes } from "./defaults";
20
20
  import lightThemeJson from "./light.json" with { type: "json" };
21
21
  import { resolveMermaidAscii } from "./mermaid-cache";
22
22
 
23
- export { getLanguageFromPath } from "../../utils/lang-from-path";
23
+ export { getLanguageFromPath, isMarkdownPath } from "../../utils/lang-from-path";
24
24
 
25
25
  // ============================================================================
26
26
  // Symbol Presets
@@ -71,7 +71,7 @@ export type SubmittedUserInput = {
71
71
  started: boolean;
72
72
  };
73
73
 
74
- export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
74
+ export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned" | "blocked";
75
75
 
76
76
  export type TodoItem = {
77
77
  content: string;
@@ -356,6 +356,8 @@ export interface InteractiveModeContext {
356
356
  handleRenameCommand(title: string): Promise<void>;
357
357
  handleMemoryCommand(text: string): Promise<void>;
358
358
  handleSTTToggle(): Promise<void>;
359
+ /** Start or stop the Codex-backed realtime voice session. */
360
+ handleLiveCommand(): Promise<void>;
359
361
  executeCompaction(
360
362
  customInstructionsOrOptions?: string | CompactOptions,
361
363
  isAuto?: boolean,
@@ -41,7 +41,18 @@ export interface ContextBreakdown {
41
41
  snapcompact?: SnapcompactSavingsEstimate;
42
42
  }
43
43
 
44
- const EMPTY_STRING_PARTS: readonly string[] = [];
44
+ /** Stable inputs used to cache non-message token estimates. */
45
+ export interface NonMessageTokenSource {
46
+ readonly systemPrompt?: string[];
47
+ readonly agent?: {
48
+ readonly state?: {
49
+ readonly tools?: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>;
50
+ };
51
+ };
52
+ readonly skills?: readonly Skill[];
53
+ }
54
+
55
+ const EMPTY_STRING_PARTS: string[] = [];
45
56
  const EMPTY_TOOLS: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">> = [];
46
57
  const EMPTY_SKILLS: readonly Skill[] = [];
47
58
 
@@ -111,13 +122,18 @@ interface NonMessageTokenCache {
111
122
  | undefined;
112
123
  }
113
124
 
114
- const nonMessageTokenCache = new WeakMap<AgentSession, NonMessageTokenCache>();
125
+ const NON_MESSAGE_TOKEN_CACHE = Symbol("non-message-token-cache");
126
+
127
+ interface CachedNonMessageTokenSource extends NonMessageTokenSource {
128
+ [NON_MESSAGE_TOKEN_CACHE]?: NonMessageTokenCache;
129
+ }
115
130
 
116
- function nonMessageTokenCacheEntry(session: AgentSession): NonMessageTokenCache {
131
+ function nonMessageTokenCacheEntry(session: NonMessageTokenSource): NonMessageTokenCache {
132
+ const cachedSession: CachedNonMessageTokenSource = session;
117
133
  const systemPromptRef = session.systemPrompt ?? EMPTY_STRING_PARTS;
118
134
  const toolsRef = session.agent?.state?.tools ?? EMPTY_TOOLS;
119
135
  const skillsRef = session.skills ?? EMPTY_SKILLS;
120
- let entry = nonMessageTokenCache.get(session);
136
+ let entry = cachedSession[NON_MESSAGE_TOKEN_CACHE];
121
137
  if (
122
138
  entry &&
123
139
  entry.systemPromptRef === systemPromptRef &&
@@ -127,11 +143,11 @@ function nonMessageTokenCacheEntry(session: AgentSession): NonMessageTokenCache
127
143
  return entry;
128
144
  }
129
145
  entry = { systemPromptRef, toolsRef, skillsRef, tokens: undefined, breakdown: undefined };
130
- nonMessageTokenCache.set(session, entry);
146
+ cachedSession[NON_MESSAGE_TOKEN_CACHE] = entry;
131
147
  return entry;
132
148
  }
133
149
 
134
- export function computeNonMessageTokens(session: AgentSession): number {
150
+ export function computeNonMessageTokens(session: NonMessageTokenSource): number {
135
151
  const entry = nonMessageTokenCacheEntry(session);
136
152
  if (entry.tokens !== undefined) return entry.tokens;
137
153
  const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
@@ -147,7 +163,7 @@ export function computeNonMessageTokens(session: AgentSession): number {
147
163
  * the status-line fast path intentionally uses the equivalent collapsed total
148
164
  * in `computeNonMessageTokens`.
149
165
  */
150
- export function computeNonMessageBreakdown(session: AgentSession): {
166
+ export function computeNonMessageBreakdown(session: NonMessageTokenSource): {
151
167
  skillsTokens: number;
152
168
  toolsTokens: number;
153
169
  systemContextTokens: number;