@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4685 -4614
  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 +374 -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/legacy-pi-coding-agent-shim.d.ts +3 -0
  25. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  26. package/dist/types/live/attestation.d.ts +2 -0
  27. package/dist/types/live/controller.d.ts +48 -0
  28. package/dist/types/live/protocol.d.ts +85 -0
  29. package/dist/types/live/protocol.test.d.ts +1 -0
  30. package/dist/types/live/transport.d.ts +35 -0
  31. package/dist/types/live/visualizer.d.ts +30 -0
  32. package/dist/types/lsp/client.d.ts +16 -1
  33. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  34. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  35. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  36. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  37. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  38. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  39. package/dist/types/modes/components/custom-message.d.ts +1 -1
  40. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  41. package/dist/types/modes/components/message-frame.d.ts +8 -4
  42. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  43. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  44. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  45. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  46. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  48. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  49. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +4 -1
  54. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  55. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  56. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  57. package/dist/types/registry/agent-registry.d.ts +11 -4
  58. package/dist/types/sdk.d.ts +15 -1
  59. package/dist/types/secrets/index.d.ts +15 -2
  60. package/dist/types/secrets/obfuscator.d.ts +113 -15
  61. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  62. package/dist/types/session/agent-session-events.d.ts +84 -0
  63. package/dist/types/session/agent-session-types.d.ts +321 -0
  64. package/dist/types/session/agent-session.d.ts +135 -753
  65. package/dist/types/session/async-job-delivery.d.ts +28 -0
  66. package/dist/types/session/auth-storage.d.ts +1 -1
  67. package/dist/types/session/bash-runner.d.ts +75 -0
  68. package/dist/types/session/blob-store.d.ts +11 -1
  69. package/dist/types/session/blob-store.test.d.ts +1 -0
  70. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  71. package/dist/types/session/eval-runner.d.ts +51 -0
  72. package/dist/types/session/irc-bridge.d.ts +46 -0
  73. package/dist/types/session/messages.d.ts +24 -0
  74. package/dist/types/session/model-controls.d.ts +180 -0
  75. package/dist/types/session/prewalk.d.ts +46 -0
  76. package/dist/types/session/queued-messages.d.ts +22 -0
  77. package/dist/types/session/redis-session-storage.d.ts +1 -0
  78. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  79. package/dist/types/session/role-models.d.ts +15 -0
  80. package/dist/types/session/session-advisors.d.ts +228 -0
  81. package/dist/types/session/session-entries.d.ts +8 -0
  82. package/dist/types/session/session-handoff.d.ts +70 -0
  83. package/dist/types/session/session-history-format.d.ts +6 -1
  84. package/dist/types/session/session-maintenance.d.ts +246 -0
  85. package/dist/types/session/session-manager.d.ts +41 -0
  86. package/dist/types/session/session-memory.d.ts +56 -0
  87. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  88. package/dist/types/session/session-stats.d.ts +48 -0
  89. package/dist/types/session/session-tools.d.ts +158 -0
  90. package/dist/types/session/session-workspace.d.ts +32 -0
  91. package/dist/types/session/stream-guards.d.ts +44 -0
  92. package/dist/types/session/streaming-output.d.ts +8 -0
  93. package/dist/types/session/todo-tracker.d.ts +56 -0
  94. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  95. package/dist/types/session/turn-recovery.d.ts +225 -0
  96. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  97. package/dist/types/stt/index.d.ts +0 -2
  98. package/dist/types/stt/stt-controller.d.ts +7 -0
  99. package/dist/types/system-prompt.d.ts +2 -0
  100. package/dist/types/task/executor.d.ts +4 -1
  101. package/dist/types/task/output-manager.d.ts +3 -1
  102. package/dist/types/tiny/title-client.d.ts +10 -0
  103. package/dist/types/tools/approval.d.ts +1 -0
  104. package/dist/types/tools/browser/launch.d.ts +5 -0
  105. package/dist/types/tools/builtin-names.d.ts +1 -1
  106. package/dist/types/tools/computer/protocol.d.ts +43 -0
  107. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  108. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  109. package/dist/types/tools/computer/worker.d.ts +15 -0
  110. package/dist/types/tools/computer-renderer.d.ts +22 -0
  111. package/dist/types/tools/computer.d.ts +71 -0
  112. package/dist/types/tools/context.d.ts +2 -0
  113. package/dist/types/tools/default-renderer.d.ts +21 -0
  114. package/dist/types/tools/image-gen.d.ts +4 -4
  115. package/dist/types/tools/image-providers.d.ts +38 -0
  116. package/dist/types/tools/index.d.ts +7 -0
  117. package/dist/types/tools/path-utils.d.ts +8 -0
  118. package/dist/types/tools/todo.d.ts +10 -4
  119. package/dist/types/tools/xdev.d.ts +5 -1
  120. package/dist/types/tts/streaming-player.d.ts +10 -29
  121. package/dist/types/tts/vocalizer.d.ts +5 -0
  122. package/dist/types/utils/jj.d.ts +29 -0
  123. package/dist/types/utils/lang-from-path.d.ts +1 -0
  124. package/dist/types/utils/title-generator.d.ts +30 -0
  125. package/dist/types/utils/tools-manager.d.ts +1 -2
  126. package/dist/types/vibe/runtime.d.ts +36 -8
  127. package/dist/types/web/search/index.d.ts +1 -1
  128. package/dist/types/web/search/provider.d.ts +15 -5
  129. package/dist/types/web/search/types.d.ts +94 -0
  130. package/package.json +12 -12
  131. package/scripts/generate-share-viewer.ts +4 -6
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +3 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/setup-cli.ts +2 -14
  141. package/src/cli/usage-cli.ts +10 -1
  142. package/src/cli.ts +8 -0
  143. package/src/collab/host.ts +3 -2
  144. package/src/commands/bench.ts +18 -4
  145. package/src/commands/launch.ts +4 -0
  146. package/src/config/model-registry.ts +16 -2
  147. package/src/config/models-config-schema.ts +15 -4
  148. package/src/config/provider-globals.ts +9 -9
  149. package/src/config/service-tier.ts +26 -1
  150. package/src/config/settings-schema.ts +270 -51
  151. package/src/config/settings.ts +81 -2
  152. package/src/dap/client.ts +37 -4
  153. package/src/edit/index.ts +49 -12
  154. package/src/eval/executor-base.ts +1 -0
  155. package/src/eval/js/executor.ts +2 -0
  156. package/src/eval/py/executor.ts +103 -33
  157. package/src/exec/bash-executor.ts +90 -6
  158. package/src/exec/direnv.ts +145 -0
  159. package/src/export/html/args.ts +20 -0
  160. package/src/export/html/index.ts +45 -47
  161. package/src/export/html/template.css +19 -1
  162. package/src/export/html/template.html +6 -0
  163. package/src/export/html/template.js +21 -0
  164. package/src/export/html/tool-views.generated.js +31 -31
  165. package/src/export/html/web-palette.ts +116 -132
  166. package/src/export/share.ts +249 -49
  167. package/src/extensibility/extensions/loader.ts +29 -1
  168. package/src/extensibility/extensions/runner.ts +6 -0
  169. package/src/extensibility/extensions/types.ts +33 -2
  170. package/src/extensibility/extensions/wrapper.ts +68 -12
  171. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  172. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  173. package/src/hindsight/state.ts +64 -6
  174. package/src/internal-urls/local-protocol.ts +2 -1
  175. package/src/internal-urls/memory-protocol.ts +2 -2
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/attestation.ts +91 -0
  180. package/src/live/controller.ts +517 -0
  181. package/src/live/prompts/agent-final-message.md +3 -0
  182. package/src/live/prompts/live-instructions.md +23 -0
  183. package/src/live/protocol.test.ts +140 -0
  184. package/src/live/protocol.ts +233 -0
  185. package/src/live/transport.ts +422 -0
  186. package/src/live/visualizer.ts +214 -0
  187. package/src/lsp/client.ts +29 -9
  188. package/src/lsp/index.ts +19 -4
  189. package/src/main.ts +8 -1
  190. package/src/mcp/config-writer.test.ts +43 -0
  191. package/src/mcp/config-writer.ts +109 -82
  192. package/src/mcp/oauth-discovery.ts +10 -2
  193. package/src/mcp/smithery-auth.test.ts +29 -0
  194. package/src/mcp/smithery-auth.ts +3 -2
  195. package/src/memories/index.ts +40 -20
  196. package/src/memory-backend/tool-names.ts +2 -0
  197. package/src/mnemopi/backend.ts +24 -7
  198. package/src/modes/acp/acp-agent.ts +56 -10
  199. package/src/modes/acp/acp-event-mapper.ts +8 -1
  200. package/src/modes/components/agent-hub.ts +1 -1
  201. package/src/modes/components/assistant-message.ts +7 -2
  202. package/src/modes/components/custom-message.ts +4 -1
  203. package/src/modes/components/login-dialog.ts +14 -4
  204. package/src/modes/components/message-frame.ts +14 -8
  205. package/src/modes/components/plan-review-overlay.ts +350 -35
  206. package/src/modes/components/session-account-selector.ts +62 -0
  207. package/src/modes/components/settings-defs.ts +23 -1
  208. package/src/modes/components/settings-selector.ts +169 -1
  209. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  210. package/src/modes/components/status-line/component.ts +123 -2
  211. package/src/modes/components/tool-execution.ts +28 -112
  212. package/src/modes/controllers/command-controller.ts +56 -15
  213. package/src/modes/controllers/event-controller.ts +111 -16
  214. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  215. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  216. package/src/modes/controllers/input-controller.ts +47 -39
  217. package/src/modes/controllers/live-command-controller.ts +255 -0
  218. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  219. package/src/modes/controllers/omfg-controller.ts +44 -40
  220. package/src/modes/controllers/selector-controller.ts +80 -10
  221. package/src/modes/controllers/session-focus-controller.ts +6 -1
  222. package/src/modes/interactive-mode.ts +176 -38
  223. package/src/modes/runtime-init.ts +2 -0
  224. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  225. package/src/modes/theme/theme.ts +1 -1
  226. package/src/modes/types.ts +4 -1
  227. package/src/modes/utils/context-usage.ts +23 -7
  228. package/src/modes/utils/ui-helpers.ts +10 -5
  229. package/src/plan-mode/plan-files.ts +40 -0
  230. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  231. package/src/prompts/bench/cache-prefix.md +3 -0
  232. package/src/prompts/bench/cache-suffix.md +1 -0
  233. package/src/prompts/system/computer-safety.md +14 -0
  234. package/src/prompts/system/custom-system-prompt.md +1 -1
  235. package/src/prompts/system/project-prompt.md +8 -1
  236. package/src/prompts/system/subagent-async-pending.md +6 -0
  237. package/src/prompts/system/system-prompt.md +1 -1
  238. package/src/prompts/system/workflow-notice.md +48 -40
  239. package/src/prompts/system/xdev-mount-notice.md +4 -0
  240. package/src/prompts/tools/computer.md +26 -0
  241. package/src/prompts/tools/todo.md +3 -1
  242. package/src/registry/agent-lifecycle.ts +116 -46
  243. package/src/registry/agent-registry.ts +35 -8
  244. package/src/registry/persisted-agents.ts +26 -2
  245. package/src/sdk.ts +215 -136
  246. package/src/secrets/index.ts +127 -4
  247. package/src/secrets/obfuscator.ts +2328 -105
  248. package/src/session/acp-permission-gate.ts +165 -0
  249. package/src/session/agent-session-error-log.test.ts +1 -1
  250. package/src/session/agent-session-events.ts +66 -0
  251. package/src/session/agent-session-types.ts +343 -0
  252. package/src/session/agent-session.ts +1940 -12218
  253. package/src/session/artifacts.ts +6 -5
  254. package/src/session/async-job-delivery.ts +74 -0
  255. package/src/session/auth-storage.ts +1 -0
  256. package/src/session/bash-runner.ts +326 -0
  257. package/src/session/blob-store.test.ts +56 -0
  258. package/src/session/blob-store.ts +18 -2
  259. package/src/session/checkpoint-entries.ts +81 -0
  260. package/src/session/eval-runner.ts +217 -0
  261. package/src/session/indexed-session-storage.ts +9 -2
  262. package/src/session/irc-bridge.ts +203 -0
  263. package/src/session/messages.ts +266 -1
  264. package/src/session/model-controls.ts +714 -0
  265. package/src/session/prewalk.ts +252 -0
  266. package/src/session/queued-messages.ts +93 -0
  267. package/src/session/redis-session-storage.ts +51 -11
  268. package/src/session/retry-fallback-chains.ts +455 -0
  269. package/src/session/role-models.ts +85 -0
  270. package/src/session/session-advisors.ts +1679 -0
  271. package/src/session/session-context.ts +6 -3
  272. package/src/session/session-entries.ts +8 -0
  273. package/src/session/session-handoff.ts +308 -0
  274. package/src/session/session-history-format.ts +20 -9
  275. package/src/session/session-loader.ts +0 -46
  276. package/src/session/session-maintenance.ts +2983 -0
  277. package/src/session/session-manager.ts +390 -30
  278. package/src/session/session-memory.ts +222 -0
  279. package/src/session/session-provider-boundary.ts +307 -0
  280. package/src/session/session-stats.ts +293 -0
  281. package/src/session/session-tools.ts +980 -0
  282. package/src/session/session-workspace.ts +53 -0
  283. package/src/session/stream-guards.ts +417 -0
  284. package/src/session/streaming-output.ts +52 -5
  285. package/src/session/todo-tracker.ts +380 -0
  286. package/src/session/ttsr-coordinator.ts +496 -0
  287. package/src/session/turn-recovery.ts +1629 -0
  288. package/src/slash-commands/builtin-registry.ts +257 -20
  289. package/src/slash-commands/helpers/session-pin.ts +44 -0
  290. package/src/slash-commands/helpers/usage-report.ts +22 -3
  291. package/src/stt/downloader.ts +0 -2
  292. package/src/stt/index.ts +0 -2
  293. package/src/stt/stt-controller.ts +57 -146
  294. package/src/system-prompt.ts +45 -17
  295. package/src/task/executor.ts +285 -70
  296. package/src/task/index.ts +98 -105
  297. package/src/task/output-manager.ts +25 -3
  298. package/src/task/persisted-revive.ts +4 -3
  299. package/src/task/structured-subagent.ts +1 -0
  300. package/src/tiny/title-client.ts +22 -0
  301. package/src/tools/approval.ts +55 -10
  302. package/src/tools/bash-interactive.ts +116 -86
  303. package/src/tools/bash-skill-urls.ts +28 -2
  304. package/src/tools/bash.ts +339 -123
  305. package/src/tools/browser/launch.ts +9 -1
  306. package/src/tools/browser/tab-supervisor.ts +8 -5
  307. package/src/tools/browser.ts +94 -47
  308. package/src/tools/builtin-names.ts +1 -0
  309. package/src/tools/computer/protocol.ts +28 -0
  310. package/src/tools/computer/supervisor.ts +258 -0
  311. package/src/tools/computer/worker-entry.ts +25 -0
  312. package/src/tools/computer/worker.ts +135 -0
  313. package/src/tools/computer-renderer.ts +108 -0
  314. package/src/tools/computer.ts +433 -0
  315. package/src/tools/context.ts +2 -0
  316. package/src/tools/default-renderer.ts +139 -0
  317. package/src/tools/essential-tools.ts +1 -0
  318. package/src/tools/image-gen.ts +20 -28
  319. package/src/tools/image-providers.ts +50 -0
  320. package/src/tools/index.ts +14 -0
  321. package/src/tools/path-utils.ts +49 -2
  322. package/src/tools/read.ts +156 -88
  323. package/src/tools/renderers.ts +9 -1
  324. package/src/tools/todo.ts +101 -11
  325. package/src/tools/vibe.ts +1 -2
  326. package/src/tools/write.ts +51 -1
  327. package/src/tools/xdev.ts +121 -29
  328. package/src/tts/streaming-player.ts +82 -283
  329. package/src/tts/vocalizer.ts +18 -1
  330. package/src/utils/clipboard.ts +1 -30
  331. package/src/utils/jj.ts +125 -4
  332. package/src/utils/lang-from-path.ts +7 -0
  333. package/src/utils/mac-file-urls.applescript +37 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/utils/tool-choice.ts +14 -0
  336. package/src/utils/tools-manager.ts +1 -19
  337. package/src/vibe/runtime.ts +852 -72
  338. package/src/web/search/index.ts +3 -6
  339. package/src/web/search/provider.ts +31 -16
  340. package/src/web/search/providers/xai.ts +7 -1
  341. package/src/web/search/types.ts +3 -0
  342. package/dist/types/stt/recorder.d.ts +0 -30
  343. package/dist/types/stt/transcriber.d.ts +0 -14
  344. package/dist/types/stt/wav.d.ts +0 -29
  345. package/dist/types/tts/player.d.ts +0 -32
  346. package/src/stt/recorder.ts +0 -551
  347. package/src/stt/transcriber.ts +0 -60
  348. package/src/stt/wav.ts +0 -173
  349. package/src/tts/player.ts +0 -137
@@ -834,45 +834,6 @@ export class InputController {
834
834
  // First, move any pending bash components to chat
835
835
  this.ctx.flushPendingBashComponents();
836
836
 
837
- // AgentSession.prompt() consumes registered extension commands locally.
838
- // Classify them here because title generation starts before prompt dispatch.
839
- const extensionCommandSpace = text.indexOf(" ");
840
- const isLocalExtensionCommand =
841
- text.startsWith("/") &&
842
- runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
843
- undefined;
844
-
845
- // Auto-generate a session title while the session is still unnamed.
846
- // Greetings / acknowledgements / empty input carry no task, so they are
847
- // skipped deterministically (no model invoked, no download-progress UI)
848
- // and the session stays unnamed — the next user message gets a fresh
849
- // chance, so titling defers past "hi" instead of latching onto it.
850
- if (
851
- !isLocalExtensionCommand &&
852
- !this.ctx.sessionManager.getSessionName() &&
853
- !$env.PI_NO_TITLE &&
854
- !isLowSignalTitleInput(text)
855
- ) {
856
- this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
857
- this.ctx.session
858
- .generateTitle(text)
859
- .then(async title => {
860
- // Re-check: a concurrent attempt for an earlier message may have
861
- // already named the session. Don't clobber it. Terminal title and
862
- // accent updates fire from the onSessionNameChanged listener.
863
- if (title && !this.ctx.sessionManager.getSessionName()) {
864
- await this.ctx.sessionManager.setSessionName(title, "auto");
865
- }
866
- })
867
- .catch(err => {
868
- logger.warn("title-generator: uncaught auto-title error", {
869
- sessionId: this.ctx.session.sessionId,
870
- reason: "uncaught-auto-title-error",
871
- error: err instanceof Error ? err.message : String(err),
872
- });
873
- });
874
- }
875
-
876
837
  if (this.ctx.onInputCallback) {
877
838
  // Include any pending images from clipboard paste
878
839
  this.ctx.editor.imageLinks = undefined;
@@ -892,6 +853,9 @@ export class InputController {
892
853
  imageLinks: inputImageLinks,
893
854
  streamingBehavior: "steer",
894
855
  });
856
+ // Start titling only after the optimistic row painted, so the local
857
+ // tiny-title worker's subprocess spawn never blocks the first frame.
858
+ this.#maybeStartTitleGeneration(text);
895
859
 
896
860
  this.ctx.onInputCallback(submission);
897
861
  } else {
@@ -906,6 +870,7 @@ export class InputController {
906
870
  const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
907
871
  this.ctx.editor.pendingImages = [];
908
872
  this.ctx.editor.pendingImageLinks = [];
873
+ this.#maybeStartTitleGeneration(text);
909
874
  try {
910
875
  await this.ctx.withLocalSubmission(
911
876
  text,
@@ -935,6 +900,49 @@ export class InputController {
935
900
  };
936
901
  }
937
902
 
903
+ /**
904
+ * Kick off session-title generation while the session is still unnamed.
905
+ * Invoked AFTER the optimistic user row is painted so the local tiny-title
906
+ * worker's subprocess spawn never lands ahead of the first frame (issue #6462).
907
+ * Skips slash extension commands (consumed locally by AgentSession.prompt()),
908
+ * already-named sessions, PI_NO_TITLE, and low-signal greetings — no model or
909
+ * download UI for those.
910
+ */
911
+ #maybeStartTitleGeneration(text: string): void {
912
+ const runner = this.ctx.session.extensionRunner;
913
+ const extensionCommandSpace = text.indexOf(" ");
914
+ const isLocalExtensionCommand =
915
+ text.startsWith("/") &&
916
+ runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
917
+ undefined;
918
+ if (
919
+ isLocalExtensionCommand ||
920
+ this.ctx.sessionManager.getSessionName() ||
921
+ $env.PI_NO_TITLE ||
922
+ isLowSignalTitleInput(text)
923
+ ) {
924
+ return;
925
+ }
926
+ this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
927
+ this.ctx.session
928
+ .generateTitle(text)
929
+ .then(async title => {
930
+ // Re-check: a concurrent attempt for an earlier message may have
931
+ // already named the session. Don't clobber it. Terminal title and
932
+ // accent updates fire from the onSessionNameChanged listener.
933
+ if (title && !this.ctx.sessionManager.getSessionName()) {
934
+ await this.ctx.sessionManager.setSessionName(title, "auto");
935
+ }
936
+ })
937
+ .catch(err => {
938
+ logger.warn("title-generator: uncaught auto-title error", {
939
+ sessionId: this.ctx.session.sessionId,
940
+ reason: "uncaught-auto-title-error",
941
+ error: err instanceof Error ? err.message : String(err),
942
+ });
943
+ });
944
+ }
945
+
938
946
  /** Submit editor text to the focused subagent session (chat-only focus policy). */
939
947
  async #submitToFocusedSession(text: string, streamingBehavior: "steer" | "followUp"): Promise<void> {
940
948
  const target = this.ctx.viewSession;
@@ -0,0 +1,255 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import { logger } from "@oh-my-pi/pi-utils";
3
+ import { LiveSessionController, type LiveTranscript } from "../../live/controller";
4
+ import { LIVE_MODEL } from "../../live/protocol";
5
+ import { LiveVisualizer } from "../../live/visualizer";
6
+ import { vocalizer } from "../../tts/vocalizer";
7
+ import type { AssistantMessageComponent } from "../components/assistant-message";
8
+ import type { CustomEditor } from "../components/custom-editor";
9
+ import { theme } from "../theme/theme";
10
+ import type { InteractiveModeContext } from "../types";
11
+ import { createAssistantMessageComponent } from "../utils/interactive-context-helpers";
12
+
13
+ const ANIMATION_INTERVAL_MS = 80;
14
+
15
+ const LIVE_MESSAGE_USAGE: AssistantMessage["usage"] = {
16
+ input: 0,
17
+ output: 0,
18
+ cacheRead: 0,
19
+ cacheWrite: 0,
20
+ totalTokens: 0,
21
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
22
+ };
23
+ function errorFrom(cause: unknown): Error {
24
+ return cause instanceof Error ? cause : new Error(String(cause));
25
+ }
26
+
27
+ /** Owns the editor-replacing visualizer and realtime session lifecycle for `/live`. */
28
+ export class LiveCommandController {
29
+ readonly #ctx: InteractiveModeContext;
30
+
31
+ #session: LiveSessionController | undefined;
32
+ #settling: Promise<void> | undefined;
33
+ #visualizer: LiveVisualizer | undefined;
34
+ #detachedEditor: CustomEditor | undefined;
35
+ #animationInterval: NodeJS.Timeout | undefined;
36
+ #previousShowHardwareCursor: boolean | undefined;
37
+ #previousUseTerminalCursor: boolean | undefined;
38
+ #resumeVocalizer: (() => void) | undefined;
39
+ #assistantTranscriptComponent: AssistantMessageComponent | undefined;
40
+ #assistantTranscriptTurn = 0;
41
+ #assistantTranscriptStartedAt = 0;
42
+
43
+ constructor(ctx: InteractiveModeContext) {
44
+ this.#ctx = ctx;
45
+ }
46
+
47
+ /** Whether a live session is connected, connecting, or closing. */
48
+ get active(): boolean {
49
+ return this.#session !== undefined || this.#settling !== undefined;
50
+ }
51
+
52
+ /** Start live mode, or stop the currently active session. */
53
+ async handleCommand(): Promise<void> {
54
+ if (this.#session) {
55
+ await this.stop();
56
+ return;
57
+ }
58
+ if (this.#settling) await this.#settling;
59
+ await this.#start();
60
+ }
61
+
62
+ /** Stop the active live session and restore the editor. */
63
+ async stop(): Promise<void> {
64
+ const session = this.#session;
65
+ if (!session) {
66
+ if (this.#settling) await this.#settling;
67
+ return;
68
+ }
69
+ try {
70
+ await session.stop();
71
+ } catch (cause) {
72
+ this.#finish(session, errorFrom(cause));
73
+ } finally {
74
+ this.#finish(session);
75
+ }
76
+ }
77
+
78
+ /** Release UI resources during synchronous InteractiveMode teardown. */
79
+ dispose(): void {
80
+ const session = this.#session;
81
+ if (session) {
82
+ this.#finish(session);
83
+ void session.stop().catch(cause => {
84
+ logger.debug("Live session teardown failed", { error: errorFrom(cause).message });
85
+ });
86
+ } else {
87
+ this.#restoreEditor();
88
+ }
89
+ }
90
+
91
+ async #start(): Promise<void> {
92
+ this.#assistantTranscriptTurn = 0;
93
+ this.#assistantTranscriptStartedAt = 0;
94
+ const visualizer = new LiveVisualizer({
95
+ onStop: () => {
96
+ void this.stop().catch(cause => this.#ctx.showError(errorFrom(cause).message));
97
+ },
98
+ onToggleMute: () => this.#session?.toggleMute(),
99
+ });
100
+ this.#mountVisualizer(visualizer);
101
+
102
+ let session: LiveSessionController;
103
+ session = new LiveSessionController({
104
+ session: this.#ctx.session,
105
+ extractAssistantText: message => this.#ctx.extractAssistantText(message),
106
+ callbacks: {
107
+ onPhase: phase => {
108
+ if (this.#visualizer !== visualizer) return;
109
+ visualizer.setPhase(phase);
110
+ this.#ctx.ui.requestComponentRender(visualizer);
111
+ },
112
+ onLevels: input => {
113
+ if (this.#visualizer !== visualizer) return;
114
+ visualizer.setInputLevel(input);
115
+ this.#ctx.ui.requestComponentRender(visualizer);
116
+ },
117
+ onTranscript: transcript => {
118
+ if (this.#visualizer !== visualizer) return;
119
+ if (!transcript) {
120
+ visualizer.clearTranscript();
121
+ this.#ctx.ui.requestComponentRender(visualizer);
122
+ } else if (transcript.role === "user") {
123
+ visualizer.setTranscript(transcript.text);
124
+ this.#ctx.ui.requestComponentRender(visualizer);
125
+ } else {
126
+ this.#presentAssistantTranscript(transcript);
127
+ }
128
+ },
129
+ onTerminal: error => this.#finish(session, error),
130
+ },
131
+ });
132
+ this.#session = session;
133
+
134
+ try {
135
+ await session.start();
136
+ } catch (cause) {
137
+ if (this.#session === session) {
138
+ await session.stop();
139
+ this.#finish(session, errorFrom(cause));
140
+ }
141
+ }
142
+ }
143
+
144
+ #presentAssistantTranscript(transcript: LiveTranscript): void {
145
+ if (
146
+ transcript.turn < this.#assistantTranscriptTurn ||
147
+ (transcript.turn === this.#assistantTranscriptTurn && !this.#assistantTranscriptComponent)
148
+ ) {
149
+ return;
150
+ }
151
+ if (transcript.turn > this.#assistantTranscriptTurn) {
152
+ this.#finalizeAssistantTranscript();
153
+ this.#assistantTranscriptTurn = transcript.turn;
154
+ }
155
+
156
+ let component = this.#assistantTranscriptComponent;
157
+ if (!component) {
158
+ component = createAssistantMessageComponent(this.#ctx);
159
+ component.setTextColorTransform(text => theme.fg("borderAccent", text));
160
+ this.#assistantTranscriptComponent = component;
161
+ this.#assistantTranscriptStartedAt = Date.now();
162
+ }
163
+ const message: AssistantMessage = {
164
+ role: "assistant",
165
+ content: [{ type: "text", text: transcript.text }],
166
+ api: "openai-codex-responses",
167
+ provider: "openai-codex",
168
+ model: LIVE_MODEL,
169
+ usage: { ...LIVE_MESSAGE_USAGE },
170
+ stopReason: "stop",
171
+ timestamp: this.#assistantTranscriptStartedAt,
172
+ };
173
+ component.updateContent(message, { transient: !transcript.final });
174
+ if (transcript.final) {
175
+ component.markTranscriptBlockFinalized();
176
+ this.#assistantTranscriptComponent = undefined;
177
+ this.#assistantTranscriptStartedAt = 0;
178
+ }
179
+ if (!this.#ctx.chatContainer.children.includes(component)) {
180
+ this.#ctx.present(component);
181
+ } else {
182
+ this.#ctx.ui.requestComponentRender(component);
183
+ }
184
+ }
185
+
186
+ #finalizeAssistantTranscript(): void {
187
+ const component = this.#assistantTranscriptComponent;
188
+ if (!component) return;
189
+ component.markTranscriptBlockFinalized();
190
+ this.#assistantTranscriptComponent = undefined;
191
+ this.#assistantTranscriptStartedAt = 0;
192
+ this.#ctx.ui.requestComponentRender(component);
193
+ }
194
+
195
+ #mountVisualizer(visualizer: LiveVisualizer): void {
196
+ this.#visualizer = visualizer;
197
+ this.#detachedEditor = this.#ctx.editor;
198
+ this.#previousShowHardwareCursor = this.#ctx.ui.getShowHardwareCursor();
199
+ this.#previousUseTerminalCursor = this.#ctx.editor.getUseTerminalCursor();
200
+ this.#ctx.ui.setShowHardwareCursor(false);
201
+ this.#ctx.editor.setUseTerminalCursor(false);
202
+ this.#ctx.editorContainer.clear();
203
+ this.#ctx.editorContainer.addChild(visualizer);
204
+ this.#ctx.ui.setFocus(visualizer);
205
+ this.#resumeVocalizer = vocalizer.suspend();
206
+ let frame = 0;
207
+ this.#animationInterval = setInterval(() => {
208
+ if (this.#visualizer !== visualizer) return;
209
+ frame += 1;
210
+ visualizer.setFrame(frame);
211
+ this.#ctx.ui.requestComponentRender(visualizer);
212
+ }, ANIMATION_INTERVAL_MS);
213
+ this.#ctx.ui.requestRender();
214
+ }
215
+
216
+ #finish(session: LiveSessionController, error?: Error): void {
217
+ if (this.#session !== session) return;
218
+ this.#session = undefined;
219
+ this.#restoreEditor();
220
+ if (error) this.#ctx.showError(error.message);
221
+ const settling = session.stop().catch(cause => {
222
+ logger.debug("Live session cleanup failed", { error: errorFrom(cause).message });
223
+ });
224
+ this.#settling = settling;
225
+ void settling.finally(() => {
226
+ if (this.#settling === settling) this.#settling = undefined;
227
+ });
228
+ }
229
+
230
+ #restoreEditor(): void {
231
+ this.#finalizeAssistantTranscript();
232
+ if (this.#animationInterval) {
233
+ clearInterval(this.#animationInterval);
234
+ this.#animationInterval = undefined;
235
+ }
236
+ this.#resumeVocalizer?.();
237
+ this.#resumeVocalizer = undefined;
238
+ const editor = this.#detachedEditor;
239
+ this.#detachedEditor = undefined;
240
+ this.#visualizer = undefined;
241
+ if (!editor) return;
242
+ this.#ctx.editorContainer.clear();
243
+ this.#ctx.editorContainer.addChild(editor);
244
+ if (this.#previousShowHardwareCursor !== undefined) {
245
+ this.#ctx.ui.setShowHardwareCursor(this.#previousShowHardwareCursor);
246
+ }
247
+ if (this.#previousUseTerminalCursor !== undefined) {
248
+ editor.setUseTerminalCursor(this.#previousUseTerminalCursor);
249
+ }
250
+ this.#previousShowHardwareCursor = undefined;
251
+ this.#previousUseTerminalCursor = undefined;
252
+ this.#ctx.ui.setFocus(editor);
253
+ this.#ctx.ui.requestRender();
254
+ }
255
+ }
@@ -38,6 +38,7 @@ import {
38
38
  getSmitheryApiKey,
39
39
  getSmitheryLoginUrl,
40
40
  pollSmitheryCliAuthSession,
41
+ type SmitheryCliPollResponse,
41
42
  saveSmitheryApiKey,
42
43
  } from "../../mcp/smithery-auth";
43
44
  import { SmitheryConnectError } from "../../mcp/smithery-connect";
@@ -51,6 +52,7 @@ import type { MCPAuthChallenge, MCPAuthConfig, MCPServerConfig, MCPServerConnect
51
52
  import { shortenPath } from "../../tools/render-utils";
52
53
  import { urlHyperlinkAlways } from "../../tui";
53
54
  import { copyToClipboard } from "../../utils/clipboard";
55
+ import { isTimeoutError } from "../../utils/fetch-timeout";
54
56
  import { openPath } from "../../utils/open";
55
57
  import { ChatBlock } from "../components/chat-block";
56
58
  import { MCPAddWizard } from "../components/mcp-add-wizard";
@@ -2121,7 +2123,14 @@ export class MCPCommandController {
2121
2123
  if (Date.now() - startedAt >= timeoutMs) {
2122
2124
  throw new Error("Smithery authorization timed out after 5 minutes.");
2123
2125
  }
2124
- const response = await pollSmitheryCliAuthSession(sessionId, signal);
2126
+ let response: SmitheryCliPollResponse;
2127
+ try {
2128
+ response = await pollSmitheryCliAuthSession(sessionId, signal);
2129
+ } catch (error) {
2130
+ // A single hung/slow poll aborts with TimeoutError; retry until the deadline.
2131
+ if (isTimeoutError(error)) continue;
2132
+ throw error;
2133
+ }
2125
2134
  if (response.status === "success" && response.apiKey) {
2126
2135
  return response.apiKey;
2127
2136
  }
@@ -192,56 +192,60 @@ export class OmfgController {
192
192
 
193
193
  async #saveCandidate(request: OmfgRequest, candidate: OmfgCandidate): Promise<SaveCandidateResult> {
194
194
  if (this.#shouldStop(request)) return { kind: "aborted" };
195
- request.component.setStatus("saving", "Choose where to save or amend the TTSR rule…");
196
- const location = await this.ctx.showHookSelector("Save TTSR rule where?", [
197
- PROJECT_OPTION,
198
- GLOBAL_OPTION,
199
- AMEND_OPTION,
200
- ]);
201
- if (!this.#isActiveRequest(request)) return { kind: "aborted" };
202
- if (!location) {
203
- request.component.markAborted();
204
- this.#closeActiveRequest({ abort: false });
205
- return { kind: "aborted" };
206
- }
207
195
 
208
- if (location === AMEND_OPTION) {
209
- request.component.setStatus("confirming", "Describe how to amend the rule…");
210
- const amendment = await this.ctx.showHookInput(
211
- "Amend TTSR rule",
212
- "e.g. Make it specific to Ruby string eval in tool:write(*.rb)",
213
- );
196
+ for (;;) {
197
+ request.component.setStatus("saving", "Choose where to save or amend the TTSR rule…");
198
+ const location = await this.ctx.showHookSelector("Save TTSR rule where?", [
199
+ PROJECT_OPTION,
200
+ GLOBAL_OPTION,
201
+ AMEND_OPTION,
202
+ ]);
214
203
  if (!this.#isActiveRequest(request)) return { kind: "aborted" };
215
- const feedback = amendment?.trim();
216
- if (!feedback) {
204
+ if (!location) {
217
205
  request.component.markAborted();
218
206
  this.#closeActiveRequest({ abort: false });
219
207
  return { kind: "aborted" };
220
208
  }
221
- return { kind: "amend", feedback };
222
- }
223
209
 
224
- const target = this.#resolveTarget(location, candidate.rule.name);
225
- if (await Bun.file(target.filePath).exists()) {
226
- const shouldOverwrite = await this.ctx.showHookConfirm(
227
- "Overwrite TTSR rule?",
228
- `${shortenPath(target.filePath)} already exists. Overwrite it?`,
229
- );
230
- if (!this.#isActiveRequest(request)) return { kind: "aborted" };
231
- if (!shouldOverwrite) {
232
- request.component.markRejected();
233
- return { kind: "rejected" };
210
+ if (location === AMEND_OPTION) {
211
+ request.component.setStatus("confirming", "Describe how to amend the rule…");
212
+ const amendment = await this.ctx.showHookInput(
213
+ "Amend TTSR rule",
214
+ "e.g. Make it specific to Ruby string eval in tool:write(*.rb)",
215
+ );
216
+ if (!this.#isActiveRequest(request)) return { kind: "aborted" };
217
+ const feedback = amendment?.trim();
218
+ if (!feedback) continue;
219
+ return { kind: "amend", feedback };
234
220
  }
235
- }
236
221
 
237
- request.component.setStatus("saving", `Saving ${candidate.rule.name}…`);
238
- await Bun.write(target.filePath, candidate.fileContent);
239
- if (!this.#isActiveRequest(request)) return { kind: "aborted" };
222
+ const target = this.#resolveTarget(location, candidate.rule.name);
223
+ if (await Bun.file(target.filePath).exists()) {
224
+ const shouldOverwrite = await this.ctx.showHookConfirm(
225
+ "Overwrite TTSR rule?",
226
+ `${shortenPath(target.filePath)} already exists. Overwrite it?`,
227
+ );
228
+ if (!this.#isActiveRequest(request)) return { kind: "aborted" };
229
+ if (!shouldOverwrite) {
230
+ request.component.markRejected();
231
+ return { kind: "rejected" };
232
+ }
233
+ }
240
234
 
241
- const savedRule = buildOmfgRuleForPath(candidate.rule.name, candidate.fileContent, target.filePath, target.level);
242
- this.#registerLive(savedRule);
243
- request.component.markSaved(shortenPath(target.filePath));
244
- return { kind: "saved" };
235
+ request.component.setStatus("saving", `Saving ${candidate.rule.name}…`);
236
+ await Bun.write(target.filePath, candidate.fileContent);
237
+ if (!this.#isActiveRequest(request)) return { kind: "aborted" };
238
+
239
+ const savedRule = buildOmfgRuleForPath(
240
+ candidate.rule.name,
241
+ candidate.fileContent,
242
+ target.filePath,
243
+ target.level,
244
+ );
245
+ this.#registerLive(savedRule);
246
+ request.component.markSaved(shortenPath(target.filePath));
247
+ return { kind: "saved" };
248
+ }
245
249
  }
246
250
 
247
251
  #resolveTarget(location: string, ruleName: string): { filePath: string; level: OmfgRuleSourceLevel } {
@@ -40,6 +40,7 @@ import {
40
40
  theme,
41
41
  } from "../../modes/theme/theme";
42
42
  import type { InteractiveModeContext } from "../../modes/types";
43
+ import type { SessionOAuthAccountList } from "../../session/agent-session-types";
43
44
  import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome } from "../../session/auth-storage";
44
45
  import type { SessionInfo } from "../../session/session-listing";
45
46
  import { SessionManager } from "../../session/session-manager";
@@ -50,6 +51,7 @@ import {
50
51
  type ResetUsageAccount,
51
52
  toResetUsageAccounts,
52
53
  } from "../../slash-commands/helpers/reset-usage";
54
+ import { toSessionPinAccounts } from "../../slash-commands/helpers/session-pin";
53
55
  import {
54
56
  AUTO_THINKING,
55
57
  type ConfiguredThinkingLevel,
@@ -57,12 +59,10 @@ import {
57
59
  parseConfiguredThinkingLevel,
58
60
  } from "../../thinking";
59
61
  import {
60
- isImageProviderPreference,
61
62
  isSearchProviderId,
62
- isSearchProviderPreference,
63
63
  setExcludedSearchProviders,
64
- setPreferredImageProvider,
65
- setPreferredSearchProvider,
64
+ setImageProviderOrder,
65
+ setSearchProviderOrder,
66
66
  type ToolSession,
67
67
  } from "../../tools";
68
68
  import { AskTool, type AskToolDetails, type AskToolInput } from "../../tools/ask";
@@ -86,6 +86,7 @@ import { OAuthSelectorComponent } from "../components/oauth-selector";
86
86
  import { PluginSelectorComponent } from "../components/plugin-selector";
87
87
  import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
88
88
  import { renderSegmentTrack } from "../components/segment-track";
89
+ import { SessionAccountSelectorComponent } from "../components/session-account-selector";
89
90
  import { SessionSelectorComponent } from "../components/session-selector";
90
91
  import { SettingsSelectorComponent } from "../components/settings-selector";
91
92
  import { ToolExecutionComponent } from "../components/tool-execution";
@@ -439,6 +440,16 @@ export class SelectorController {
439
440
  this.ctx.showError(`Failed to apply personality: ${err}`);
440
441
  });
441
442
  break;
443
+ case "tools.xdevDocs":
444
+ void this.ctx.session.refreshBaseSystemPrompt().catch(err => {
445
+ this.ctx.showError(`Failed to apply xd:// prompt docs setting: ${err}`);
446
+ });
447
+ break;
448
+ case "memory.backend":
449
+ void this.ctx.session.applyMemoryBackend().catch(err => {
450
+ this.ctx.showError(`Failed to apply memory backend: ${err}`);
451
+ });
452
+ break;
442
453
 
443
454
  case "autocompleteMaxVisible":
444
455
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
@@ -609,9 +620,9 @@ export class SelectorController {
609
620
  }
610
621
 
611
622
  // Provider settings - update runtime preferences
612
- case "providers.webSearch":
613
- if (typeof value === "string" && isSearchProviderPreference(value)) {
614
- setPreferredSearchProvider(value);
623
+ case "providers.webSearchOrder":
624
+ if (Array.isArray(value)) {
625
+ setSearchProviderOrder(value.filter(isSearchProviderId));
615
626
  }
616
627
  break;
617
628
  case "providers.webSearchExclude":
@@ -619,9 +630,9 @@ export class SelectorController {
619
630
  setExcludedSearchProviders(value.filter(isSearchProviderId));
620
631
  }
621
632
  break;
622
- case "providers.image":
623
- if (isImageProviderPreference(value)) {
624
- setPreferredImageProvider(value);
633
+ case "providers.imageOrder":
634
+ if (Array.isArray(value)) {
635
+ setImageProviderOrder(value.filter((entry): entry is string => typeof entry === "string"));
625
636
  }
626
637
  break;
627
638
 
@@ -1735,6 +1746,65 @@ export class SelectorController {
1735
1746
  });
1736
1747
  }
1737
1748
 
1749
+ async showSessionPinSelector(): Promise<void> {
1750
+ const session = this.ctx.session;
1751
+ if (session.isStreaming) {
1752
+ this.ctx.showStatus("Cannot pin an account while the session is streaming.");
1753
+ return;
1754
+ }
1755
+ this.ctx.showStatus("Loading provider accounts…", { dim: true });
1756
+ let accountList: SessionOAuthAccountList | undefined;
1757
+ try {
1758
+ accountList = await session.listCurrentProviderOAuthAccounts();
1759
+ } catch (error) {
1760
+ this.ctx.showError(
1761
+ `Could not load provider accounts: ${error instanceof Error ? error.message : String(error)}`,
1762
+ );
1763
+ return;
1764
+ }
1765
+ if (!accountList) {
1766
+ this.ctx.showStatus("Select a model before pinning a provider account.");
1767
+ return;
1768
+ }
1769
+ const provider = getOAuthProviders().find(candidate => candidate.id === accountList.provider);
1770
+ const providerName = provider?.name ?? accountList.provider;
1771
+ const accounts = toSessionPinAccounts(accountList.accounts);
1772
+ if (accounts.length === 0) {
1773
+ const source = session.modelRegistry.authStorage.describeCredentialSource(
1774
+ accountList.provider,
1775
+ session.sessionId,
1776
+ );
1777
+ this.ctx.showStatus(
1778
+ source
1779
+ ? `No stored OAuth accounts for ${providerName}. Current auth comes from ${source}.`
1780
+ : `No stored OAuth accounts for ${providerName}. Use /login to add one.`,
1781
+ );
1782
+ return;
1783
+ }
1784
+
1785
+ this.showSelector(done => {
1786
+ const selector = new SessionAccountSelectorComponent(
1787
+ providerName,
1788
+ accounts,
1789
+ account => {
1790
+ done();
1791
+ if (!session.pinCurrentProviderOAuthAccount(account.credentialId)) {
1792
+ this.ctx.showWarning(`${account.label} is no longer available to pin.`);
1793
+ return;
1794
+ }
1795
+ this.ctx.showStatus(`Pinned ${account.label} to this session for ${providerName}.`);
1796
+ this.ctx.statusLine.invalidate();
1797
+ this.ctx.ui.requestRender();
1798
+ },
1799
+ () => {
1800
+ done();
1801
+ this.ctx.ui.requestRender();
1802
+ },
1803
+ );
1804
+ return { component: selector, focus: selector };
1805
+ });
1806
+ }
1807
+
1738
1808
  async showResetUsageSelector(): Promise<void> {
1739
1809
  const session = this.ctx.session;
1740
1810
  this.ctx.showStatus("Checking saved rate-limit resets…", { dim: true });
@@ -12,6 +12,7 @@
12
12
  import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
13
13
  import { AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "../../registry/agent-registry";
14
14
  import type { AgentSession } from "../../session/agent-session";
15
+ import { setTerminalTitleState } from "../../utils/title-generator";
15
16
  import type { InteractiveModeContext } from "../types";
16
17
 
17
18
  export class SessionFocusController {
@@ -104,8 +105,12 @@ export class SessionFocusController {
104
105
  });
105
106
  this.ctx.statusLine.setSession(target, this.#focusedAgentId);
106
107
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
107
- // Mid-turn attach: no agent_start will arrive; arm the loader/turn state manually.
108
+ // Sync the run-state title to the attached target: a streaming target has no
109
+ // agent_start incoming, so arm the loader/working title manually; an idle
110
+ // target would otherwise inherit the previous session's stuck spinner, so
111
+ // reset it to idle (agent_end teardown already ran via clearTransientSessionUi).
108
112
  if (target.isStreaming) await this.ctx.eventController.handleEvent({ type: "agent_start" });
113
+ else setTerminalTitleState("idle");
109
114
  this.ctx.updateEditorBorderColor();
110
115
  this.ctx.ui.requestRender();
111
116
  }