@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  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/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  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 +2 -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/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  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/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -60,6 +60,8 @@ export class OAuthSelectorComponent extends Container {
60
60
  /** First provider index of the visible ScrollView window (last #updateList). */
61
61
  #scrollStart = 0;
62
62
  #visibleCount = 0;
63
+ /** Visible list window, shrunk by {@link setMaxHeight} on short screens. */
64
+ #maxVisible = OAUTH_SELECTOR_MAX_VISIBLE;
63
65
  #mode: "login" | "logout";
64
66
  #authStorage: AuthStorage;
65
67
  #onSelectCallback: (providerId: string) => void;
@@ -111,6 +113,24 @@ export class OAuthSelectorComponent extends Container {
111
113
  this.#validationGeneration += 1;
112
114
  this.#stopSpinner();
113
115
  }
116
+
117
+ /**
118
+ * Fit the selector into `lines` rendered rows by shrinking the visible list
119
+ * window (the window is centered on the selection, so the selected row is
120
+ * always visible at any height). Prefers keeping the full chrome — borders,
121
+ * spacers, title, search status — but sacrifices the trailing spacer/border
122
+ * (clipped by the host) before dropping below three visible rows.
123
+ */
124
+ setMaxHeight(lines: number): void {
125
+ // Above the rows: LIST_ROW_OFFSET; below: search status + spacer + border.
126
+ const strict = lines - LIST_ROW_OFFSET - 3;
127
+ // Keeps only the rows + search status inside `lines`.
128
+ const relaxed = lines - LIST_ROW_OFFSET - 1;
129
+ const rows = Math.min(OAUTH_SELECTOR_MAX_VISIBLE, Math.max(1, strict, Math.min(relaxed, 3)));
130
+ if (rows === this.#maxVisible) return;
131
+ this.#maxVisible = rows;
132
+ this.#updateList();
133
+ }
114
134
  #hasSelectableAuth(providerId: string): boolean {
115
135
  return this.#mode === "logout" ? this.#authStorage.has(providerId) : this.#authStorage.hasAuth(providerId);
116
136
  }
@@ -227,7 +247,7 @@ export class OAuthSelectorComponent extends Container {
227
247
  }
228
248
 
229
249
  #isSearchEnabled(): boolean {
230
- return this.#allProviders.length > OAUTH_SELECTOR_MAX_VISIBLE;
250
+ return this.#allProviders.length > this.#maxVisible;
231
251
  }
232
252
 
233
253
  #shouldRenderSearchStatus(): boolean {
@@ -286,7 +306,7 @@ export class OAuthSelectorComponent extends Container {
286
306
  this.#listContainer.clear();
287
307
 
288
308
  const total = this.#filteredProviders.length;
289
- const maxVisible = OAUTH_SELECTOR_MAX_VISIBLE;
309
+ const maxVisible = this.#maxVisible;
290
310
  const startIndex =
291
311
  total <= maxVisible
292
312
  ? 0
@@ -381,7 +401,7 @@ export class OAuthSelectorComponent extends Container {
381
401
  // Page up - jump up by one visible page
382
402
  else if (matchesKey(keyData, "pageUp")) {
383
403
  if (this.#filteredProviders.length > 0) {
384
- this.#selectedIndex = Math.max(0, this.#selectedIndex - OAUTH_SELECTOR_MAX_VISIBLE);
404
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#maxVisible);
385
405
  }
386
406
  this.#statusMessage = undefined;
387
407
  this.#updateList();
@@ -389,10 +409,7 @@ export class OAuthSelectorComponent extends Container {
389
409
  // Page down - jump down by one visible page
390
410
  else if (matchesKey(keyData, "pageDown")) {
391
411
  if (this.#filteredProviders.length > 0) {
392
- this.#selectedIndex = Math.min(
393
- this.#filteredProviders.length - 1,
394
- this.#selectedIndex + OAUTH_SELECTOR_MAX_VISIBLE,
395
- );
412
+ this.#selectedIndex = Math.min(this.#filteredProviders.length - 1, this.#selectedIndex + this.#maxVisible);
396
413
  }
397
414
  this.#statusMessage = undefined;
398
415
  this.#updateList();
@@ -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(" · "))];
@@ -66,18 +66,27 @@ export interface SubmenuSettingDef extends BaseSettingDef {
66
66
 
67
67
  export interface TextInputSettingDef extends BaseSettingDef {
68
68
  type: "text";
69
+ secret: boolean;
69
70
  }
70
71
 
71
72
  export interface ProviderLimitsSettingDef extends BaseSettingDef {
72
73
  type: "providerLimits";
73
74
  }
74
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
+
75
83
  export type SettingDef =
76
84
  | BooleanSettingDef
77
85
  | EnumSettingDef
78
86
  | SubmenuSettingDef
79
87
  | TextInputSettingDef
80
- | ProviderLimitsSettingDef;
88
+ | ProviderLimitsSettingDef
89
+ | MultiSelectSettingDef;
81
90
 
82
91
  // ═══════════════════════════════════════════════════════════════════════════
83
92
  // Condition Functions
@@ -120,6 +129,13 @@ const CONDITIONS: Record<string, () => boolean> = {
120
129
  return false;
121
130
  }
122
131
  },
132
+ usageAwareFallbackEnabled: () => {
133
+ try {
134
+ return Settings.instance.get("retry.usageAwareFallback") === true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ },
123
139
  planModeEnabled: () => {
124
140
  try {
125
141
  return Settings.instance.get("plan.enabled");
@@ -176,11 +192,20 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
176
192
  if (options) {
177
193
  return { ...base, type: "submenu", options };
178
194
  }
179
- return { ...base, type: "text" };
195
+ return { ...base, type: "text", secret: ui.secret === true };
196
+ }
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 };
180
203
  }
181
204
 
182
205
  if (schemaType === "record") {
183
- return path === "providers.maxInFlightRequests" ? { ...base, type: "providerLimits" } : { ...base, type: "text" };
206
+ return path === "providers.maxInFlightRequests"
207
+ ? { ...base, type: "providerLimits" }
208
+ : { ...base, type: "text", secret: false };
184
209
  }
185
210
 
186
211
  return null;