@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
@@ -23,11 +23,14 @@ import {
23
23
  Markdown,
24
24
  type MarkdownTheme,
25
25
  matchesKey,
26
+ replaceTabs,
26
27
  routeSgrMouseInput,
27
28
  ScrollView,
28
29
  truncateToWidth,
29
30
  visibleWidth,
30
31
  } from "@oh-my-pi/pi-tui";
32
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
33
+ import { sanitizeStatusText } from "../shared";
31
34
  import { getMarkdownTheme, theme } from "../theme/theme";
32
35
  import {
33
36
  matchesAppExternalEditor,
@@ -58,22 +61,59 @@ const MIN_BODY_ROWS = 3;
58
61
  const SIDEBAR_MIN_HEADINGS = 2;
59
62
  const SIDEBAR_MIN_TOTAL_WIDTH = 64;
60
63
  const SIDEBAR_MIN_BODY_WIDTH = 40;
64
+ /** Persisted line-context cap; render-time captions clamp again to the viewport. */
65
+ const MAX_ANNOTATION_CONTEXT_WIDTH = 120;
61
66
 
62
67
  type Focus = "toc" | "body" | "actions";
63
68
 
69
+ type AnnotationTarget = { kind: "section" } | { kind: "line"; row: number; context: string; contextTruncated: boolean };
70
+
71
+ interface OverlayAnnotation {
72
+ note: string;
73
+ target: AnnotationTarget;
74
+ }
75
+
64
76
  interface OverlaySection {
65
77
  level: number;
66
78
  title: string;
67
79
  raw: string;
68
80
  md: Markdown;
69
- annotations: string[];
81
+ annotations: OverlayAnnotation[];
82
+ }
83
+
84
+ interface LineAnchorContext {
85
+ text: string;
86
+ truncated: boolean;
87
+ }
88
+
89
+ interface BodyRowAnchor {
90
+ sectionIndex: number;
91
+ row: number;
92
+ context: string;
93
+ contextTruncated: boolean;
94
+ }
95
+
96
+ /** Serializable annotations retained by the plan-review owner between overlays. */
97
+ export interface PlanReviewAnnotationState {
98
+ annotations: Array<{
99
+ section: {
100
+ index: number;
101
+ title: string;
102
+ /** Heading ancestry from the document root, when emitted by this overlay. */
103
+ path?: string[];
104
+ /** Hash of the section source, used to reject ambiguous moved headings. */
105
+ contentHash?: string;
106
+ };
107
+ target: { kind: "section" } | { kind: "line"; row: number; context: string; contextTruncated?: boolean };
108
+ note: string;
109
+ }>;
70
110
  }
71
111
 
72
112
  /** Undo snapshot: joined plan text, annotations aligned by section, and the
73
113
  * accumulated deleted-section feedback at the time of the snapshot. */
74
114
  interface UndoEntry {
75
115
  text: string;
76
- annotations: string[][];
116
+ annotations: OverlayAnnotation[][];
77
117
  deleted: string[];
78
118
  }
79
119
 
@@ -92,6 +132,8 @@ export interface PlanReviewOverlayCallbacks {
92
132
  onPlanEdited?: (content: string) => void;
93
133
  /** Invoked with the Refine feedback markdown whenever annotations change. */
94
134
  onFeedbackChange?: (feedback: string) => void;
135
+ /** Invoked with a serializable annotation snapshot whenever annotations change. */
136
+ onAnnotationStateChange?: (state: PlanReviewAnnotationState) => void;
95
137
  }
96
138
 
97
139
  export interface PlanReviewOverlayOptions {
@@ -108,6 +150,8 @@ export interface PlanReviewOverlayOptions {
108
150
  slider?: HookSelectorSlider;
109
151
  /** Display label for the external-editor key, surfaced in the footer help. */
110
152
  externalEditorLabel?: string;
153
+ /** Serializable annotations restored into this overlay instance. */
154
+ annotationState?: PlanReviewAnnotationState;
111
155
  }
112
156
 
113
157
  /** Default trailing footer hint when the caller supplies none. */
@@ -121,6 +165,8 @@ export class PlanReviewOverlay implements Component {
121
165
  /** Shallowest level among ToC entries, used to flatten indentation. */
122
166
  #tocBaseLevel = 1;
123
167
  #sectionOffsets: number[] = [];
168
+ /** Rendered body row to underlying plan row; callouts retain their owner's anchor. */
169
+ #bodyRowAnchors: BodyRowAnchor[] = [];
124
170
  #undo: UndoEntry[] = [];
125
171
  /** Titles of sections deleted in the overlay, surfaced as Refine feedback. */
126
172
  #deleted: string[] = [];
@@ -162,6 +208,7 @@ export class PlanReviewOverlay implements Component {
162
208
  #committedLabel: string | undefined;
163
209
  #annotating = false;
164
210
  #input: Input;
211
+ #annotationTarget: BodyRowAnchor | { sectionIndex: number; row: null; context: null } | undefined;
165
212
 
166
213
  constructor(
167
214
  planContent: string,
@@ -194,6 +241,10 @@ export class PlanReviewOverlay implements Component {
194
241
  this.#input.onSubmit = value => this.#submitAnnotation(value);
195
242
  this.#input.onEscape = () => this.#exitAnnotate();
196
243
  this.#setSections(planContent);
244
+ this.#restoreAnnotationState(options.annotationState);
245
+ if (Array.isArray(options.annotationState?.annotations) && options.annotationState.annotations.length > 0) {
246
+ this.#recomputeFeedback();
247
+ }
197
248
  }
198
249
 
199
250
  invalidate(): void {
@@ -204,7 +255,9 @@ export class PlanReviewOverlay implements Component {
204
255
  * reset scroll/focus so the operator starts at the top. Does not emit
205
256
  * `onPlanEdited` (the editor round-trip already persisted the file). */
206
257
  setPlanContent(planContent: string): void {
258
+ const annotations = this.#annotationState();
207
259
  this.#setSections(planContent);
260
+ this.#restoreAnnotationState(annotations);
208
261
  this.#scrollView.scrollToTop();
209
262
  this.#scrollProgress = 0;
210
263
  this.#tocCursor = 0;
@@ -220,11 +273,148 @@ export class PlanReviewOverlay implements Component {
220
273
  title: section.title,
221
274
  raw: section.raw,
222
275
  md: new Markdown(section.raw, 1, 0, this.#mdTheme),
223
- annotations: [] as string[],
276
+ annotations: [],
224
277
  }));
225
278
  this.#rebuildToc();
226
279
  this.#tocCursor = Math.min(this.#tocCursor, Math.max(0, this.#toc.length - 1));
227
280
  }
281
+ #cloneAnnotation(annotation: OverlayAnnotation): OverlayAnnotation {
282
+ return {
283
+ note: annotation.note,
284
+ target:
285
+ annotation.target.kind === "section"
286
+ ? { kind: "section" }
287
+ : {
288
+ kind: "line",
289
+ row: annotation.target.row,
290
+ context: annotation.target.context,
291
+ contextTruncated: annotation.target.contextTruncated,
292
+ },
293
+ };
294
+ }
295
+
296
+ #sectionPaths(): string[][] {
297
+ const stack: Array<{ level: number; title: string }> = [];
298
+ return this.#sections.map(section => {
299
+ if (section.level < 1) return [];
300
+ while (stack.length > 0 && stack[stack.length - 1]!.level >= section.level) stack.pop();
301
+ stack.push({ level: section.level, title: section.title });
302
+ return stack.map(entry => entry.title);
303
+ });
304
+ }
305
+
306
+ #sectionContentHash(section: OverlaySection): string {
307
+ return `${section.raw.length}:${Bun.hash(section.raw).toString(16)}`;
308
+ }
309
+
310
+ #annotationState(): PlanReviewAnnotationState {
311
+ const annotations: PlanReviewAnnotationState["annotations"] = [];
312
+ const sectionPaths = this.#sectionPaths();
313
+ for (let sectionIndex = 0; sectionIndex < this.#sections.length; sectionIndex++) {
314
+ const section = this.#sections[sectionIndex]!;
315
+ for (const annotation of section.annotations) {
316
+ annotations.push({
317
+ section: {
318
+ index: sectionIndex,
319
+ title: section.title,
320
+ path: sectionPaths[sectionIndex]!,
321
+ contentHash: this.#sectionContentHash(section),
322
+ },
323
+ target:
324
+ annotation.target.kind === "section"
325
+ ? { kind: "section" }
326
+ : {
327
+ kind: "line",
328
+ row: annotation.target.row,
329
+ context: annotation.target.context,
330
+ contextTruncated: annotation.target.contextTruncated,
331
+ },
332
+ note: annotation.note,
333
+ });
334
+ }
335
+ }
336
+ return { annotations };
337
+ }
338
+
339
+ #restoreAnnotationState(state: PlanReviewAnnotationState | undefined): void {
340
+ if (!state || !Array.isArray(state.annotations)) return;
341
+ const sectionPaths = this.#sectionPaths();
342
+ const contentHashes = this.#sections.map(section => this.#sectionContentHash(section));
343
+ for (const entry of state.annotations) {
344
+ if (
345
+ !entry ||
346
+ typeof entry.note !== "string" ||
347
+ !entry.section ||
348
+ !entry.target ||
349
+ typeof entry.section.title !== "string"
350
+ ) {
351
+ continue;
352
+ }
353
+ const note = entry.note.trim();
354
+ if (!note) continue;
355
+ const storedIndex = Number.isInteger(entry.section.index) ? entry.section.index : -1;
356
+ const storedPath = entry.section.path;
357
+ let matchingSections: number[];
358
+ if (
359
+ Array.isArray(storedPath) &&
360
+ storedPath.every(segment => typeof segment === "string") &&
361
+ typeof entry.section.contentHash === "string"
362
+ ) {
363
+ matchingSections = [];
364
+ for (let i = 0; i < this.#sections.length; i++) {
365
+ const path = sectionPaths[i]!;
366
+ if (
367
+ this.#sections[i]!.title === entry.section.title &&
368
+ contentHashes[i] === entry.section.contentHash &&
369
+ path.length === storedPath.length &&
370
+ path.every((segment, pathIndex) => segment === storedPath[pathIndex])
371
+ ) {
372
+ matchingSections.push(i);
373
+ }
374
+ }
375
+ } else {
376
+ matchingSections =
377
+ storedIndex >= 0 &&
378
+ storedIndex < this.#sections.length &&
379
+ this.#sections[storedIndex]!.title === entry.section.title
380
+ ? [storedIndex]
381
+ : [];
382
+ }
383
+ if (matchingSections.length === 0) continue;
384
+ const sectionIndex = matchingSections.reduce((best, candidate) =>
385
+ Math.abs(candidate - storedIndex) < Math.abs(best - storedIndex) ? candidate : best,
386
+ );
387
+ const section = this.#sections[sectionIndex]!;
388
+ if (entry.target.kind === "section") {
389
+ if (section.level >= 1) section.annotations.push({ note, target: { kind: "section" } });
390
+ continue;
391
+ }
392
+ if (
393
+ entry.target.kind !== "line" ||
394
+ !Number.isFinite(entry.target.row) ||
395
+ typeof entry.target.context !== "string"
396
+ ) {
397
+ continue;
398
+ }
399
+ const contexts = section.md.render(MAX_ANNOTATION_CONTEXT_WIDTH).map(line => this.#lineContext(line));
400
+ const row = this.#resolveLineRow(
401
+ entry.target.row,
402
+ { text: entry.target.context, truncated: entry.target.contextTruncated === true },
403
+ contexts,
404
+ );
405
+ if (row < 0) continue;
406
+ const context = contexts[row]!;
407
+ section.annotations.push({
408
+ note,
409
+ target: {
410
+ kind: "line",
411
+ row,
412
+ context: context.text,
413
+ contextTruncated: context.truncated,
414
+ },
415
+ });
416
+ }
417
+ }
228
418
 
229
419
  #rebuildToc(): void {
230
420
  const headings: number[] = [];
@@ -443,6 +633,10 @@ export class PlanReviewOverlay implements Component {
443
633
  }
444
634
 
445
635
  #handleBody(data: string): void {
636
+ if (data === "a") {
637
+ this.#startBodyAnnotate();
638
+ return;
639
+ }
446
640
  if (matchesKey(data, "left") || matchesKey(data, "h")) {
447
641
  if (this.#sidebarShown) this.#setFocus("toc");
448
642
  return;
@@ -529,7 +723,7 @@ export class PlanReviewOverlay implements Component {
529
723
  return;
530
724
  }
531
725
  if (data === "a") {
532
- this.#startAnnotate();
726
+ this.#startSectionAnnotate();
533
727
  return;
534
728
  }
535
729
  if (data === "u") {
@@ -577,7 +771,9 @@ export class PlanReviewOverlay implements Component {
577
771
  #pushUndo(): void {
578
772
  this.#undo.push({
579
773
  text: joinPlanSections(this.#sections),
580
- annotations: this.#sections.map(section => [...section.annotations]),
774
+ annotations: this.#sections.map(section =>
775
+ section.annotations.map(annotation => this.#cloneAnnotation(annotation)),
776
+ ),
581
777
  deleted: [...this.#deleted],
582
778
  });
583
779
  }
@@ -607,7 +803,8 @@ export class PlanReviewOverlay implements Component {
607
803
  if (!entry) return;
608
804
  this.#setSections(entry.text);
609
805
  for (let i = 0; i < this.#sections.length; i++) {
610
- this.#sections[i]!.annotations = entry.annotations[i] ? [...entry.annotations[i]!] : [];
806
+ this.#sections[i]!.annotations =
807
+ entry.annotations[i]?.map(annotation => this.#cloneAnnotation(annotation)) ?? [];
611
808
  }
612
809
  this.#deleted = [...entry.deleted];
613
810
  this.#tocCursor = Math.min(this.#tocCursor, Math.max(0, this.#toc.length - 1));
@@ -616,8 +813,21 @@ export class PlanReviewOverlay implements Component {
616
813
  this.#recomputeFeedback();
617
814
  }
618
815
 
619
- #startAnnotate(): void {
620
- if (this.#toc[this.#tocCursor] === undefined) return;
816
+ #startSectionAnnotate(): void {
817
+ const sectionIndex = this.#toc[this.#tocCursor];
818
+ if (sectionIndex === undefined) return;
819
+ this.#startAnnotate({ sectionIndex, row: null, context: null });
820
+ }
821
+
822
+ #startBodyAnnotate(): void {
823
+ const maxRow = this.#bodyRowAnchors.length - 1;
824
+ if (maxRow < 0) return;
825
+ const topRow = Math.max(0, Math.min(maxRow, Math.floor(this.#scrollView.getScrollOffset())));
826
+ this.#startAnnotate(this.#bodyRowAnchors[topRow]!);
827
+ }
828
+
829
+ #startAnnotate(target: BodyRowAnchor | { sectionIndex: number; row: null; context: null }): void {
830
+ this.#annotationTarget = target;
621
831
  this.#annotating = true;
622
832
  this.#input.setValue("");
623
833
  }
@@ -625,10 +835,23 @@ export class PlanReviewOverlay implements Component {
625
835
  #submitAnnotation(value: string): void {
626
836
  this.#annotating = false;
627
837
  const note = value.trim();
628
- const sectionIndex = this.#toc[this.#tocCursor];
629
- if (note && sectionIndex !== undefined) {
838
+ const target = this.#annotationTarget;
839
+ this.#annotationTarget = undefined;
840
+ const section = target ? this.#sections[target.sectionIndex] : undefined;
841
+ if (note && section && target) {
630
842
  this.#pushUndo();
631
- this.#sections[sectionIndex]!.annotations.push(note);
843
+ section.annotations.push({
844
+ note,
845
+ target:
846
+ target.row === null
847
+ ? { kind: "section" }
848
+ : {
849
+ kind: "line",
850
+ row: target.row,
851
+ context: target.context,
852
+ contextTruncated: target.contextTruncated,
853
+ },
854
+ });
632
855
  this.#recomputeFeedback();
633
856
  }
634
857
  this.#input.setValue("");
@@ -636,11 +859,13 @@ export class PlanReviewOverlay implements Component {
636
859
 
637
860
  #exitAnnotate(): void {
638
861
  this.#annotating = false;
862
+ this.#annotationTarget = undefined;
639
863
  this.#input.setValue("");
640
864
  }
641
865
 
642
866
  #recomputeFeedback(): void {
643
- const annotated = this.#sections.filter(section => section.level >= 1 && section.annotations.length > 0);
867
+ this.callbacks.onAnnotationStateChange?.(this.#annotationState());
868
+ const annotated = this.#sections.filter(section => section.annotations.length > 0);
644
869
  if (annotated.length === 0 && this.#deleted.length === 0) {
645
870
  this.callbacks.onFeedbackChange?.("");
646
871
  return;
@@ -651,8 +876,11 @@ export class PlanReviewOverlay implements Component {
651
876
  for (const title of this.#deleted) feedback += `- ${title}\n`;
652
877
  }
653
878
  for (const section of annotated) {
654
- feedback += `\n## ${section.title}\n`;
655
- for (const note of section.annotations) feedback += this.#formatAnnotationFeedback(note);
879
+ feedback += `\n## ${section.title || "Plan preamble"}\n`;
880
+ for (const annotation of section.annotations) {
881
+ if (annotation.target.kind === "line") feedback += `> Line: ${annotation.target.context}\n`;
882
+ feedback += this.#formatAnnotationFeedback(annotation.note);
883
+ }
656
884
  }
657
885
  this.callbacks.onFeedbackChange?.(feedback);
658
886
  }
@@ -717,7 +945,7 @@ export class PlanReviewOverlay implements Component {
717
945
  parts.push("↑↓ section", "⏎ open", "a annotate", "d delete", "u undo");
718
946
  break;
719
947
  case "body":
720
- parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends");
948
+ parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends", "a annotate");
721
949
  break;
722
950
  }
723
951
  if (this.callbacks.onCopyPlan) parts.push("c copy");
@@ -744,35 +972,113 @@ export class PlanReviewOverlay implements Component {
744
972
  if (maxOffset > 0) this.#scrollView.setScrollOffset(Math.round(this.#scrollProgress * maxOffset));
745
973
  }
746
974
 
747
- /** Build the concatenated body lines and record each section's start row. */
975
+ /** Build the concatenated body lines and map each rendered row back to a plan anchor. */
748
976
  #buildBody(bodyContentWidth: number): string[] {
749
977
  const lines: string[] = [];
978
+ const anchors: BodyRowAnchor[] = [];
750
979
  const offsets: number[] = new Array(this.#sections.length);
751
- for (let i = 0; i < this.#sections.length; i++) {
752
- const section = this.#sections[i]!;
753
- offsets[i] = lines.length;
980
+ for (let sectionIndex = 0; sectionIndex < this.#sections.length; sectionIndex++) {
981
+ const section = this.#sections[sectionIndex]!;
982
+ offsets[sectionIndex] = lines.length;
754
983
  const rendered = section.md.render(bodyContentWidth);
755
- if (section.level >= 1 && section.annotations.length > 0 && rendered.length > 0) {
756
- lines.push(rendered[0]!);
757
- for (const note of section.annotations) {
758
- const noteLines = note.split(/\r?\n/);
759
- for (let j = 0; j < noteLines.length; j++) {
760
- const prefix =
761
- j === 0
762
- ? `${theme.fg("warning", "▎ ")}${theme.fg("dim", "note: ")}`
763
- : `${theme.fg("warning", "▎ ")}${theme.fg("dim", " ")}`;
764
- lines.push(`${prefix}${theme.fg("accent", noteLines[j] ?? "")}`);
984
+ const contexts = rendered.map(line => this.#lineContext(line));
985
+ for (let row = 0; row < rendered.length; row++) {
986
+ const context = contexts[row]!;
987
+ const anchor = {
988
+ sectionIndex,
989
+ row,
990
+ context: context.text,
991
+ contextTruncated: context.truncated,
992
+ };
993
+ lines.push(rendered[row]!);
994
+ anchors.push(anchor);
995
+ for (const annotation of section.annotations) {
996
+ const annotationRow =
997
+ annotation.target.kind === "section"
998
+ ? 0
999
+ : this.#resolveLineRow(
1000
+ annotation.target.row,
1001
+ {
1002
+ text: annotation.target.context,
1003
+ truncated: annotation.target.contextTruncated,
1004
+ },
1005
+ contexts,
1006
+ );
1007
+ if (annotationRow === row) {
1008
+ this.#appendAnnotationCallout(lines, anchors, annotation.note, anchor, bodyContentWidth);
765
1009
  }
766
1010
  }
767
- for (let k = 1; k < rendered.length; k++) lines.push(rendered[k]!);
768
- } else {
769
- for (const line of rendered) lines.push(line);
770
1011
  }
771
1012
  }
772
1013
  this.#sectionOffsets = offsets;
1014
+ this.#bodyRowAnchors = anchors;
773
1015
  return lines;
774
1016
  }
775
1017
 
1018
+ #lineContext(line: string): LineAnchorContext {
1019
+ const sanitized = sanitizeStatusText(line);
1020
+ const truncated = visibleWidth(sanitized) > MAX_ANNOTATION_CONTEXT_WIDTH;
1021
+ const text = truncateToWidth(sanitized, MAX_ANNOTATION_CONTEXT_WIDTH, Ellipsis.Unicode);
1022
+ return { text: text || "(blank line)", truncated };
1023
+ }
1024
+
1025
+ #resolveLineRow(
1026
+ storedRow: number,
1027
+ storedContext: LineAnchorContext,
1028
+ contexts: readonly LineAnchorContext[],
1029
+ ): number {
1030
+ if (contexts.length === 0) return -1;
1031
+ const targetRow = Math.max(0, Math.floor(storedRow));
1032
+ const normalize = (context: LineAnchorContext): string => {
1033
+ const normalized = sanitizeStatusText(context.text).replace(/\s+/g, " ").trim();
1034
+ return context.truncated && normalized.endsWith("…") ? normalized.slice(0, -1) : normalized;
1035
+ };
1036
+ const normalizedStoredContext = normalize(storedContext);
1037
+ if (!normalizedStoredContext) return -1;
1038
+ let best = -1;
1039
+ let bestDistance = Number.POSITIVE_INFINITY;
1040
+ for (let row = 0; row < contexts.length; row++) {
1041
+ const normalizedContext = normalize(contexts[row]!);
1042
+ if (
1043
+ normalizedContext !== normalizedStoredContext &&
1044
+ !normalizedContext.includes(normalizedStoredContext) &&
1045
+ !normalizedStoredContext.includes(normalizedContext)
1046
+ ) {
1047
+ continue;
1048
+ }
1049
+ const distance = Math.abs(row - targetRow);
1050
+ if (distance < bestDistance) {
1051
+ best = row;
1052
+ bestDistance = distance;
1053
+ }
1054
+ }
1055
+ return best;
1056
+ }
1057
+
1058
+ #appendAnnotationCallout(
1059
+ lines: string[],
1060
+ anchors: BodyRowAnchor[],
1061
+ note: string,
1062
+ anchor: BodyRowAnchor,
1063
+ bodyContentWidth: number,
1064
+ ): void {
1065
+ const noteLines = note.split(/\r?\n/);
1066
+ for (let i = 0; i < noteLines.length; i++) {
1067
+ const prefix =
1068
+ i === 0
1069
+ ? `${theme.fg("warning", "▎ ")}${theme.fg("dim", "note: ")}`
1070
+ : `${theme.fg("warning", "▎ ")}${theme.fg("dim", " ")}`;
1071
+ const available = Math.max(0, bodyContentWidth - visibleWidth(prefix));
1072
+ const displayLine = truncateToWidth(
1073
+ replaceTabs(sanitizeText(noteLines[i] ?? "")),
1074
+ available,
1075
+ Ellipsis.Unicode,
1076
+ );
1077
+ lines.push(truncateToWidth(`${prefix}${theme.fg("accent", displayLine)}`, bodyContentWidth));
1078
+ anchors.push(anchor);
1079
+ }
1080
+ }
1081
+
776
1082
  #sidebarWidthFor(width: number): number {
777
1083
  return Math.max(18, Math.min(30, Math.round(width * 0.24)));
778
1084
  }
@@ -832,9 +1138,18 @@ export class PlanReviewOverlay implements Component {
832
1138
 
833
1139
  #renderFooterLines(innerWidth: number): string[] {
834
1140
  if (this.#annotating) {
835
- const section = this.#sections[this.#toc[this.#tocCursor]!];
836
- const title = section?.title ?? "";
837
- const caption = `${theme.fg("dim", "Annotate")} ${theme.fg("accent", `‹${title}›`)}`;
1141
+ const target = this.#annotationTarget;
1142
+ const section = target ? this.#sections[target.sectionIndex] : undefined;
1143
+ const title = sanitizeStatusText(section?.title || "Plan preamble");
1144
+ const location =
1145
+ target?.row === null
1146
+ ? `‹${title}›`
1147
+ : `‹${title}› · ${truncateToWidth(target?.context ?? "", Math.max(1, innerWidth - 16), Ellipsis.Unicode)}`;
1148
+ const caption = truncateToWidth(
1149
+ `${theme.fg("dim", "Annotate")} ${theme.fg("accent", location)}`,
1150
+ innerWidth,
1151
+ Ellipsis.Unicode,
1152
+ );
838
1153
  const hintParts = ["enter save", "esc cancel"];
839
1154
  if (this.#externalEditorLabel) hintParts.push(`${this.#externalEditorLabel} editor`);
840
1155
  return [caption, this.#input.render(innerWidth)[0] ?? "", theme.fg("dim", hintParts.join(" · "))];
@@ -0,0 +1,62 @@
1
+ import { Container, type SelectItem, SelectList, type SgrMouseEvent, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
2
+ import { getSelectListTheme, theme } from "../../modes/theme/theme";
3
+ import type { SessionPinAccount } from "../../slash-commands/helpers/session-pin";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+
6
+ const ACCOUNT_SELECTOR_MAX_VISIBLE = 10;
7
+ const ACCOUNT_LIST_ROW_OFFSET = 4;
8
+
9
+ /** Account picker opened by `/session pin` for the current model provider. */
10
+ export class SessionAccountSelectorComponent extends Container {
11
+ #selectList: SelectList;
12
+
13
+ constructor(
14
+ providerName: string,
15
+ accounts: readonly SessionPinAccount[],
16
+ onSelect: (account: SessionPinAccount) => void,
17
+ onCancel: () => void,
18
+ ) {
19
+ super();
20
+ const accountsByValue = new Map<string, SessionPinAccount>();
21
+ const items: SelectItem[] = accounts.map(account => {
22
+ const value = String(account.credentialId);
23
+ accountsByValue.set(value, account);
24
+ return {
25
+ value,
26
+ label: account.label,
27
+ description: account.active ? "active for this session" : undefined,
28
+ };
29
+ });
30
+
31
+ this.addChild(new DynamicBorder());
32
+ this.addChild(new Spacer(1));
33
+ this.addChild(new TruncatedText(theme.bold(`Select a ${providerName} account for this session:`)));
34
+ this.addChild(new Spacer(1));
35
+
36
+ this.#selectList = new SelectList(
37
+ items,
38
+ Math.min(Math.max(items.length, 1), ACCOUNT_SELECTOR_MAX_VISIBLE),
39
+ getSelectListTheme(),
40
+ );
41
+ const activeIndex = accounts.findIndex(account => account.active);
42
+ if (activeIndex >= 0) this.#selectList.setSelectedIndex(activeIndex);
43
+ this.#selectList.onSelect = item => {
44
+ const account = accountsByValue.get(item.value);
45
+ if (account) onSelect(account);
46
+ };
47
+ this.#selectList.onCancel = onCancel;
48
+ this.addChild(this.#selectList);
49
+ this.addChild(new Spacer(1));
50
+ this.addChild(new DynamicBorder());
51
+ }
52
+
53
+ /** Forward keyboard navigation and cancellation when the wrapper owns focus. */
54
+ handleInput(keyData: string): void {
55
+ this.#selectList.handleInput(keyData);
56
+ }
57
+
58
+ /** Route mouse selection through the title rows into the account list. */
59
+ routeMouse(event: SgrMouseEvent, line: number, col: number): void {
60
+ this.#selectList.routeMouse(event, line - ACCOUNT_LIST_ROW_OFFSET, col);
61
+ }
62
+ }
@@ -73,12 +73,20 @@ export interface ProviderLimitsSettingDef extends BaseSettingDef {
73
73
  type: "providerLimits";
74
74
  }
75
75
 
76
+ /** Array-of-enum setting edited as a toggle list; `ordered` lists render positions and support reordering. */
77
+ export interface MultiSelectSettingDef extends BaseSettingDef {
78
+ type: "multiselect";
79
+ options: OptionList;
80
+ ordered: boolean;
81
+ }
82
+
76
83
  export type SettingDef =
77
84
  | BooleanSettingDef
78
85
  | EnumSettingDef
79
86
  | SubmenuSettingDef
80
87
  | TextInputSettingDef
81
- | ProviderLimitsSettingDef;
88
+ | ProviderLimitsSettingDef
89
+ | MultiSelectSettingDef;
82
90
 
83
91
  // ═══════════════════════════════════════════════════════════════════════════
84
92
  // Condition Functions
@@ -121,6 +129,13 @@ const CONDITIONS: Record<string, () => boolean> = {
121
129
  return false;
122
130
  }
123
131
  },
132
+ usageAwareFallbackEnabled: () => {
133
+ try {
134
+ return Settings.instance.get("retry.usageAwareFallback") === true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ },
124
139
  planModeEnabled: () => {
125
140
  try {
126
141
  return Settings.instance.get("plan.enabled");
@@ -180,6 +195,13 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
180
195
  return { ...base, type: "text", secret: ui.secret === true };
181
196
  }
182
197
 
198
+ if (schemaType === "array") {
199
+ // Arrays without declared options stay config-file only (free-form lists
200
+ // like extension paths have no finite choice set to toggle).
201
+ if (!options || options === "runtime") return null;
202
+ return { ...base, type: "multiselect", options, ordered: ui.ordered === true };
203
+ }
204
+
183
205
  if (schemaType === "record") {
184
206
  return path === "providers.maxInFlightRequests"
185
207
  ? { ...base, type: "providerLimits" }