one-code-extension 0.4.1 → 0.6.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 (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -0,0 +1,199 @@
1
+ /**
2
+ * An artifact's viewer page, `<id>/view.html` (pure, no fs): the stored page in
3
+ * a frame under a slim bar, One Code's version of the header Claude Code's web
4
+ * viewer puts over an artifact. The bar keeps what works without a server: a
5
+ * home link to the gallery, a title menu (details, Download HTML, Copy file
6
+ * path, Reload, the page without the bar, the version list, All artifacts) and
7
+ * a Download button. Share, Chat, Rename, Duplicate and Pin need claude.ai and
8
+ * are left out.
9
+ *
10
+ * `view.html#v<N>` shows an earlier version with a banner back to the latest.
11
+ * Download loads the version's payload script (`download.js`, or
12
+ * `versions/v<N>.js`) and saves it through a `blob:` link, for the same reason
13
+ * the gallery does (gallery.ts).
14
+ */
15
+
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { tildify } from "../lib/paths.ts";
19
+ import { CLIENT_RUNTIME, escapeHtml, THEME_TOKENS, utcStamp } from "./gallery.ts";
20
+ import type { ArtifactMeta } from "./store.ts";
21
+
22
+ /** Inline SVG icons; `currentColor` so they follow the theme. */
23
+ const HOME_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 7.5 8 3l5.5 4.5V13a.5.5 0 0 1-.5.5H9.5V10h-3v3.5H3a.5.5 0 0 1-.5-.5z"/></svg>`;
24
+ const CHEVRON_ICON = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m3 4.5 3 3 3-3"/></svg>`;
25
+ const DOWNLOAD_ICON = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 2v7m-3-3 3 3 3-3M2.5 11.5h9"/></svg>`;
26
+
27
+ /** `<id>/view.html` for `meta`, whose folder is `dir`. */
28
+ export function renderViewer(meta: ArtifactMeta, dir: string, home: string = homedir()): string {
29
+ const title = escapeHtml(meta.title);
30
+ const versions: string[] = [`<a role="menuitem" href="#" data-version="latest">Version ${meta.version} (latest)</a>`];
31
+ for (let n = meta.version - 1; n >= 1; n--) versions.push(`<a role="menuitem" href="#v${n}" data-version="${n}">Version ${n}</a>`);
32
+ const details = [
33
+ `Version ${meta.version}`,
34
+ `Updated <time datetime="${escapeHtml(meta.updatedAt)}">${escapeHtml(utcStamp(meta.updatedAt))}</time>`,
35
+ meta.project ? escapeHtml(tildify(meta.project, home)) : "",
36
+ ].filter(Boolean);
37
+ // The page paths the script copies, for the current version and each earlier one.
38
+ const paths = JSON.stringify({ latest: join(dir, "index.html"), versions: join(dir, "versions") }).replace(/</g, "\\u003c");
39
+ return `<!doctype html>
40
+ <html lang="en">
41
+ <head>
42
+ <meta charset="utf-8">
43
+ <meta name="viewport" content="width=device-width, initial-scale=1">
44
+ <title>${title}</title>
45
+ <style>
46
+ ${THEME_TOKENS}
47
+ * { box-sizing: border-box; }
48
+ html, body { margin: 0; height: 100%; }
49
+ body {
50
+ background: var(--bg); color: var(--text);
51
+ font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
52
+ display: flex; flex-direction: column;
53
+ }
54
+ .bar {
55
+ display: flex; align-items: center; gap: 6px; padding: 6px 16px; min-height: 44px;
56
+ background: var(--surface); border-bottom: 1px solid var(--line); position: relative; z-index: 2;
57
+ }
58
+ .icon-link, .title-button, .primary, .menu [role="menuitem"] {
59
+ font: inherit; color: var(--text); background: transparent; border: 0; border-radius: 6px; cursor: pointer; text-decoration: none;
60
+ }
61
+ .icon-link { display: inline-flex; padding: 6px; color: var(--muted); }
62
+ .icon-link:hover, .title-button:hover, .title-button[aria-expanded="true"] { background: var(--hover); color: var(--text); }
63
+ .title-button { display: inline-flex; align-items: center; gap: 6px; padding: 5px 8px; max-width: min(60vw, 480px); }
64
+ .title-button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
65
+ .spacer { flex: 1; }
66
+ .primary {
67
+ display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; font-weight: 600; font-size: 13px;
68
+ border: 1px solid var(--line);
69
+ }
70
+ .primary:hover { background: var(--hover); }
71
+ .menu {
72
+ position: absolute; top: calc(100% - 2px); left: 44px; min-width: 250px; max-width: calc(100vw - 32px);
73
+ background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 6px;
74
+ box-shadow: 0 8px 24px rgb(0 0 0 / 0.18); display: flex; flex-direction: column;
75
+ }
76
+ .menu[hidden] { display: none; }
77
+ .menu .details { padding: 6px 10px 8px; color: var(--muted); font-size: 13px; display: flex; flex-direction: column; gap: 2px; font-variant-numeric: tabular-nums; }
78
+ .menu .details span:last-child { overflow-wrap: anywhere; }
79
+ .menu [role="menuitem"] { display: block; text-align: left; padding: 7px 10px; }
80
+ .menu [role="menuitem"]:hover, .menu [role="menuitem"]:focus-visible { background: var(--hover); outline: none; }
81
+ .menu [aria-current="true"] { font-weight: 600; }
82
+ .menu .label { padding: 8px 10px 2px; color: var(--muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
83
+ .menu hr { border: 0; border-top: 1px solid var(--line); margin: 6px 4px; }
84
+ .versions { max-height: 180px; overflow-y: auto; display: flex; flex-direction: column; }
85
+ .banner { padding: 8px 16px; background: var(--hover); border-bottom: 1px solid var(--line); font-size: 13px; }
86
+ .banner[hidden] { display: none; }
87
+ .banner a { color: var(--accent); }
88
+ .toast {
89
+ position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 3;
90
+ background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: 6px; font-size: 13px;
91
+ }
92
+ .toast[hidden] { display: none; }
93
+ iframe { flex: 1; width: 100%; border: 0; display: block; background: var(--bg); }
94
+ a:focus-visible, button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <header class="bar">
99
+ <a class="icon-link" href="../index.html" title="All artifacts" aria-label="All artifacts">${HOME_ICON}</a>
100
+ <button type="button" class="title-button" id="title-button" aria-haspopup="menu" aria-expanded="false" aria-controls="menu"><span>${title}</span>${CHEVRON_ICON}</button>
101
+ <div class="menu" id="menu" role="menu" hidden>
102
+ <div class="details">${details.map((line) => `<span>${line}</span>`).join("")}</div>
103
+ <hr>
104
+ <button type="button" role="menuitem" data-action="download">Download HTML</button>
105
+ <button type="button" role="menuitem" data-action="copy-path">Copy file path</button>
106
+ <button type="button" role="menuitem" data-action="reload">Reload</button>
107
+ <a role="menuitem" id="raw-link" href="index.html">Open without this bar</a>
108
+ ${meta.version > 1 ? `<hr><div class="label">Versions</div><div class="versions">${versions.join("")}</div>` : ""}
109
+ <hr>
110
+ <a role="menuitem" href="../index.html">All artifacts</a>
111
+ </div>
112
+ <span class="spacer"></span>
113
+ <button type="button" class="primary" data-action="download">${DOWNLOAD_ICON}Download</button>
114
+ </header>
115
+ <div class="banner" id="banner" hidden>You are viewing version <span id="banner-version"></span> of ${meta.version}. <a href="#">Show the latest</a></div>
116
+ <iframe id="page" src="index.html" title="${title}"></iframe>
117
+ <div class="toast" id="toast" role="status" hidden></div>
118
+ <script>
119
+ (function () {
120
+ var LATEST = ${meta.version};
121
+ var PATHS = ${paths};
122
+ var SEP = PATHS.latest.indexOf("\\\\") !== -1 ? "\\\\" : "/";
123
+ var frame = document.getElementById("page");
124
+ var menu = document.getElementById("menu");
125
+ var button = document.getElementById("title-button");
126
+ var banner = document.getElementById("banner");
127
+ var toast = document.getElementById("toast");
128
+ var shown = null;
129
+
130
+ function current() {
131
+ var match = /^#v(\\d+)$/.exec(location.hash);
132
+ var n = match ? parseInt(match[1], 10) : NaN;
133
+ return n >= 1 && n < LATEST ? n : null;
134
+ }
135
+ function show() {
136
+ shown = current();
137
+ var src = shown ? "versions/v" + shown + ".html" : "index.html";
138
+ if (frame.getAttribute("src") !== src) frame.setAttribute("src", src);
139
+ document.getElementById("raw-link").setAttribute("href", src);
140
+ banner.hidden = !shown;
141
+ if (shown) document.getElementById("banner-version").textContent = shown;
142
+ menu.querySelectorAll("[data-version]").forEach(function (link) {
143
+ var v = link.getAttribute("data-version");
144
+ link.setAttribute("aria-current", String(v === "latest" ? !shown : Number(v) === shown));
145
+ });
146
+ }
147
+ function note(text) {
148
+ toast.textContent = text;
149
+ toast.hidden = false;
150
+ clearTimeout(note.timer);
151
+ note.timer = setTimeout(function () { toast.hidden = true; }, 1800);
152
+ }
153
+ function setMenu(open) {
154
+ menu.hidden = !open;
155
+ button.setAttribute("aria-expanded", String(open));
156
+ }
157
+ ${CLIENT_RUNTIME}
158
+ function download() {
159
+ var script = document.createElement("script");
160
+ script.src = shown ? "versions/v" + shown + ".js" : "download.js";
161
+ script.onload = function () { script.remove(); };
162
+ script.onerror = function () { script.remove(); note("Download failed. Use Open without this bar, then save the page."); };
163
+ document.head.appendChild(script);
164
+ }
165
+ function copyPath() {
166
+ var path = shown ? PATHS.versions + SEP + "v" + shown + ".html" : PATHS.latest;
167
+ var done = function () { note("Copied " + path); };
168
+ var fallback = function () { window.prompt("File path", path); };
169
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(path).then(done, fallback);
170
+ else fallback();
171
+ }
172
+
173
+ button.addEventListener("click", function () { setMenu(menu.hidden); });
174
+ document.addEventListener("click", function (event) {
175
+ var action = event.target.closest && event.target.closest("[data-action]");
176
+ if (action) {
177
+ var name = action.getAttribute("data-action");
178
+ if (name === "download") download();
179
+ else if (name === "copy-path") copyPath();
180
+ else if (name === "reload") location.reload();
181
+ setMenu(false);
182
+ return;
183
+ }
184
+ if (event.target.closest && event.target.closest("[data-version], #raw-link")) { setMenu(false); return; }
185
+ if (!menu.hidden && !menu.contains(event.target) && !button.contains(event.target)) setMenu(false);
186
+ });
187
+ document.addEventListener("keydown", function (event) {
188
+ if (event.key === "Escape" && !menu.hidden) { setMenu(false); button.focus(); }
189
+ });
190
+ // A click in the frame never reaches this document; losing focus to it closes the menu.
191
+ window.addEventListener("blur", function () { setMenu(false); });
192
+ window.addEventListener("hashchange", show);
193
+ show();
194
+ })();
195
+ </script>
196
+ </body>
197
+ </html>
198
+ `;
199
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ask_user_question's model-facing texts: Claude Code's AskUserQuestion
3
+ * description and parameter texts (pure).
4
+ *
5
+ * The short form goes to frontier and workhorse models; the long form, the
6
+ * same text without the "Reserve this for decisions…" paragraph, to cheap
7
+ * and tiny ones (lib/tool-variants.ts). The plan-mode note names One Code's
8
+ * tools and drops "the user cannot see the plan until you call ExitPlanMode":
9
+ * One Code's plan is a file the user can open at any time.
10
+ */
11
+
12
+ import type { DescriptionForm } from "../lib/tool-variants.ts";
13
+
14
+ export const ASK_SHORT_DESCRIPTION = `Use this tool only when you are blocked on a decision that is genuinely the user's to make: one you cannot resolve from the request, the code, or sensible defaults.
15
+
16
+ Usage notes:
17
+ - Users will always be able to select "Other" to provide custom text input
18
+ - Use multiSelect: true to allow multiple answers to be selected for a question
19
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
20
+
21
+ Plan mode note: To switch into plan mode, use enter_plan_mode (not this tool). Once in plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?", "Should I proceed?", or otherwise reference "the plan" in questions — plan approval goes through exit_plan_mode.
22
+
23
+ Reserve this for decisions where the user's answer changes what you do next — not for choices with a conventional default or facts you can verify in the codebase yourself. In those cases pick the obvious option, mention it in your response, and proceed.
24
+
25
+ Preview feature:
26
+ Use the optional \`preview\` field on options when presenting concrete artifacts that users need to visually compare:
27
+ - ASCII mockups of UI layouts or components
28
+ - Code snippets showing different implementations
29
+ - Diagram variations
30
+ - Configuration examples
31
+
32
+ Preview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).
33
+ `;
34
+
35
+ export const ASK_LONG_DESCRIPTION = `Use this tool only when you are blocked on a decision that is genuinely the user's to make: one you cannot resolve from the request, the code, or sensible defaults.
36
+
37
+ Usage notes:
38
+ - Users will always be able to select "Other" to provide custom text input
39
+ - Use multiSelect: true to allow multiple answers to be selected for a question
40
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
41
+
42
+ Plan mode note: To switch into plan mode, use enter_plan_mode (not this tool). Once in plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?", "Should I proceed?", or otherwise reference "the plan" in questions — plan approval goes through exit_plan_mode.
43
+
44
+ Preview feature:
45
+ Use the optional \`preview\` field on options when presenting concrete artifacts that users need to visually compare:
46
+ - ASCII mockups of UI layouts or components
47
+ - Code snippets showing different implementations
48
+ - Diagram variations
49
+ - Configuration examples
50
+
51
+ Preview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).
52
+ `;
53
+
54
+ export function askDescription(form: DescriptionForm): string {
55
+ return form === "long" ? ASK_LONG_DESCRIPTION : ASK_SHORT_DESCRIPTION;
56
+ }
57
+
58
+ export const ASK_PARAMS = {
59
+ questions: `Questions to ask the user (1-4 questions)`,
60
+ question: `The complete question to ask the user. Should be clear, specific, and end with a question mark. Example: "Which library should we use for date formatting?" If multiSelect is true, phrase it accordingly, e.g. "Which features do you want to enable?"`,
61
+ header: `Very short label displayed as a chip/tag (max 12 chars). Examples: "Auth method", "Library", "Approach".`,
62
+ options: `The available choices for this question. Must have 2-4 options. Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). There should be no 'Other' option, that will be provided automatically.`,
63
+ multiSelect: `Set to true to allow the user to select multiple options instead of just one. Use when choices are not mutually exclusive.`,
64
+ label: `The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.`,
65
+ optionDescription: `Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.`,
66
+ preview: `Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons that help users compare options. See the tool description for the expected content format.`,
67
+ } as const;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ask_user_question through pi's plain dialogs (pure).
3
+ *
4
+ * pi's RPC mode returns `undefined` from `ctx.ui.custom()` ("Custom UI not
5
+ * supported in RPC mode"), so the tabbed widget never reaches an RPC client and
6
+ * the tool used to report "The user cancelled without answering." RPC does
7
+ * forward `select` and `input` as `extension_ui_request`s, so this asks each
8
+ * question with them and returns the widget's result shape: the same tool
9
+ * result the TUI produces. A dismissed dialog cancels with the answers given so
10
+ * far, as Esc does in the widget.
11
+ */
12
+
13
+ import type { Answer, Question } from "./questions.ts";
14
+ import type { WidgetResult } from "./widget.ts";
15
+
16
+ export const TYPE_OWN = "Type my own answer";
17
+ export const CHAT = "Chat about this";
18
+ export const DONE = "Done";
19
+
20
+ export interface DialogUI {
21
+ select(title: string, options: string[], opts?: { signal?: AbortSignal }): Promise<string | undefined>;
22
+ input(title: string, placeholder?: string, opts?: { signal?: AbortSignal }): Promise<string | undefined>;
23
+ }
24
+
25
+ /** One option's row: its label, and its description after a dash when it has one. */
26
+ export function optionRow(option: { label: string; description?: string }): string {
27
+ return option.description ? `${option.label} — ${option.description}` : option.label;
28
+ }
29
+
30
+ export async function askThroughDialogs(questions: Question[], ui: DialogUI, signal?: AbortSignal): Promise<WidgetResult> {
31
+ const answers: Answer[] = [];
32
+ for (const question of questions) {
33
+ const plainRows = question.options.map(optionRow);
34
+ // RPC identifies a selection by its displayed string. Number every option
35
+ // when a row collides with another option or one of our control rows.
36
+ const numbered = new Set(plainRows).size !== plainRows.length || plainRows.some((row) => [DONE, TYPE_OWN, CHAT].includes(row));
37
+ const rows = plainRows.map((row, index) => (numbered ? `${index + 1}. ${row}` : row));
38
+ const previews = question.options.flatMap((option, index) => (option.preview ? [`${rows[index]}:\n${option.preview}`] : []));
39
+ const title = [`${question.header}: ${question.question}`, ...previews].join("\n\n");
40
+ // By option index: two options may share a label (with different descriptions).
41
+ const selected: number[] = [];
42
+ const labels = () => selected.map((index) => question.options[index].label);
43
+ let typed: string | undefined;
44
+ for (;;) {
45
+ const choices = [
46
+ ...rows.flatMap((row, index) => (selected.includes(index) ? [] : [row])),
47
+ ...(question.multiSelect && selected.length > 0 ? [DONE] : []),
48
+ TYPE_OWN,
49
+ CHAT,
50
+ ];
51
+ const picked = await ui.select(question.multiSelect && selected.length > 0 ? `${title} (selected: ${labels().join(", ")})` : title, choices, { signal });
52
+ if (signal?.aborted || picked === undefined || !choices.includes(picked)) return { kind: "cancel", answers };
53
+ if (picked === CHAT) return { kind: "chat" };
54
+ if (picked === DONE) break;
55
+ if (picked === TYPE_OWN) {
56
+ const text = (await ui.input(title, "Type something.", { signal }))?.trim();
57
+ if (signal?.aborted || text === undefined) return { kind: "cancel", answers };
58
+ // A blank answer is no answer: ask the question again.
59
+ if (!text) continue;
60
+ typed = text;
61
+ break;
62
+ }
63
+ const index = rows.indexOf(picked);
64
+ if (index < 0) return { kind: "cancel", answers };
65
+ selected.push(index);
66
+ if (!question.multiSelect || selected.length === question.options.length) break;
67
+ }
68
+ const all = typed === undefined ? labels() : [...labels(), typed];
69
+ const single = !question.multiSelect && selected.length === 1 ? question.options[selected[0]] : undefined;
70
+ answers.push({
71
+ question: question.question,
72
+ header: question.header,
73
+ selected: all,
74
+ freeform: typed !== undefined && selected.length === 0,
75
+ ...(typed !== undefined ? { typed: true } : {}),
76
+ ...(single?.preview ? { preview: single.preview } : {}),
77
+ });
78
+ }
79
+ return { kind: "submit", answers };
80
+ }
@@ -11,57 +11,60 @@
11
11
  * pulled in a second, conflicting TypeBox.
12
12
  */
13
13
 
14
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
15
  import { Type } from "typebox";
16
+ import { canShowCustomUi } from "../lib/headless-output.ts";
16
17
  import { ccToolRenderers, safeThemeBold, safeThemeInverse, safeThemePaint } from "../lib/tui-render.ts";
17
- import { type Answer, formatAnswers, formatDecline, type Question } from "./questions.ts";
18
+ import { registerFormTool } from "../lib/tool-variants.ts";
19
+ import { ASK_PARAMS, askDescription } from "./description.ts";
20
+ import { askThroughDialogs } from "./dialogs.ts";
21
+ import { type Answer, answerPairs, formatAnswers, formatDecline, type Question } from "./questions.ts";
18
22
  import { applyWidgetKey, createWidgetState, decodeWidgetKey, renderWidget, type WidgetResult } from "./widget.ts";
23
+ import { panelUi } from "../lib/panel-focus.ts";
19
24
 
20
25
  const NON_INTERACTIVE =
21
26
  "This session has no interactive UI, so the user cannot be shown a dialog. Ask your question in your reply instead and stop, or proceed under a stated assumption.";
22
27
 
28
+ const AskParams = Type.Object({
29
+ questions: Type.Array(
30
+ Type.Object({
31
+ question: Type.String({ description: ASK_PARAMS.question }),
32
+ header: Type.String({ description: ASK_PARAMS.header }),
33
+ options: Type.Array(
34
+ Type.Object({
35
+ label: Type.String({ description: ASK_PARAMS.label }),
36
+ description: Type.Optional(Type.String({ description: ASK_PARAMS.optionDescription })),
37
+ preview: Type.Optional(Type.String({ description: ASK_PARAMS.preview })),
38
+ }),
39
+ { minItems: 2, maxItems: 4, description: ASK_PARAMS.options },
40
+ ),
41
+ multiSelect: Type.Optional(Type.Boolean({ description: ASK_PARAMS.multiSelect })),
42
+ }),
43
+ { minItems: 1, maxItems: 4, description: ASK_PARAMS.questions },
44
+ ),
45
+ });
46
+
23
47
  export default function askUserExtension(pi: ExtensionAPI) {
24
- pi.registerTool({
48
+ // Claude Code's AskUserQuestion text, short or long by the model's tier
49
+ // (description.ts, lib/tool-variants.ts); registered again when the form changes.
50
+ const tool = defineTool({
25
51
  name: "ask_user_question",
26
52
  label: "Ask User",
27
53
  ...ccToolRenderers<{ questions?: Array<{ question?: string }> }>("Ask User", {
28
54
  title: (a) => a?.questions?.[0]?.question,
29
55
  }),
30
- description:
31
- "Ask the user to choose between options when you are blocked on a decision that is genuinely theirs — one you cannot resolve from the request, the code, or a sensible default. Ask up to four questions in one call; the user answers them in one tabbed dialog and can decline into a chat discussion instead. Options on single-select questions may carry a `preview` — an ASCII mockup, diagram, or code snippet rendered beside the list while the user compares choices; use previews when seeing the alternatives helps (UI layouts, code style variants), never for plain preference questions. Non-preview questions get a free-text 'Other' row automatically, so never add an 'Other' option yourself. If you recommend a specific option, make it the first in the list and end its label with '(Recommended)'. To switch into plan mode use enter_plan_mode (not this tool); once in plan mode, use this tool only to clarify requirements or choose between approaches before finalizing the plan. Do not use this tool for choices with an obvious default or for facts you can verify yourself.",
56
+ description: askDescription("short"),
32
57
  promptSnippet: "Ask the user to decide between options when genuinely blocked",
33
- parameters: Type.Object({
34
- questions: Type.Array(
35
- Type.Object({
36
- question: Type.String({ description: "The question, ending in a question mark" }),
37
- header: Type.String({ description: "Very short label for the question (a few words)" }),
38
- multiSelect: Type.Optional(
39
- Type.Boolean({ description: "Allow choosing several options (default false)" }),
40
- ),
41
- options: Type.Array(
42
- Type.Object({
43
- label: Type.String({ description: "Short choice text" }),
44
- description: Type.Optional(Type.String({ description: "What choosing this means" })),
45
- preview: Type.Optional(
46
- Type.String({
47
- description:
48
- "Preview shown beside the options while this option is focused: an ASCII mockup, diagram, or code snippet that helps the user compare choices. Single-select questions only.",
49
- }),
50
- ),
51
- }),
52
- { minItems: 2, maxItems: 4 },
53
- ),
54
- }),
55
- { minItems: 1, maxItems: 4, description: "Questions to ask (1-4)" },
56
- ),
57
- }),
58
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
58
+ parameters: AskParams,
59
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
59
60
  if (!ctx.hasUI) {
60
61
  return { content: [{ type: "text", text: NON_INTERACTIVE }], details: {}, isError: true };
61
62
  }
62
63
 
63
64
  const questions = params.questions as Question[];
64
- const outcome = await ctx.ui.custom<WidgetResult>((tui, theme, _keybindings, done) => {
65
+ // RPC has no custom UI (its custom() resolves undefined); ask through the
66
+ // select/input requests it forwards to the client instead (dialogs.ts).
67
+ const outcome = !canShowCustomUi(ctx) ? await askThroughDialogs(questions, ctx.ui, signal) : await panelUi(pi.events, ctx.ui).custom<WidgetResult>((tui, theme, _keybindings, done) => {
65
68
  const style = {
66
69
  paint: safeThemePaint(theme),
67
70
  bold: safeThemeBold(theme),
@@ -94,7 +97,7 @@ export default function askUserExtension(pi: ExtensionAPI) {
94
97
  const partial = outcome?.kind === "cancel" ? outcome.answers : [];
95
98
  const text =
96
99
  partial.length > 0
97
- ? `The user cancelled without submitting. They had made these selections before cancelling (NOT submitted — do not treat them as final answers):\n\n${formatAnswers(partial)}`
100
+ ? `The user cancelled without submitting. They had made these selections before cancelling (NOT submitted — do not treat them as final answers): ${answerPairs(partial)}.`
98
101
  : "The user cancelled without answering.";
99
102
  return {
100
103
  content: [{ type: "text", text }],
@@ -115,4 +118,5 @@ export default function askUserExtension(pi: ExtensionAPI) {
115
118
  };
116
119
  },
117
120
  });
121
+ registerFormTool(pi, tool, askDescription);
118
122
  }
@@ -22,20 +22,45 @@ export interface Answer {
22
22
  selected: string[];
23
23
  /** True when the user typed their own answer instead of picking. */
24
24
  freeform: boolean;
25
+ /** True when the answer includes text the user typed (the "Other" row), alone or beside picked options. */
26
+ typed?: boolean;
27
+ /** The preview of the single option picked, when it carries one. */
28
+ preview?: string;
25
29
  /** Free-text notes the user attached to this answer. */
26
30
  notes?: string;
27
31
  }
28
32
 
29
- export function formatAnswers(answers: Answer[]): string {
30
- if (answers.length === 0) return "The user did not answer.";
33
+ /**
34
+ * Claude Code's answer list: `"question"="answer"` per answered question, with
35
+ * the picked option's preview and the user's notes, joined by ", ". A question
36
+ * left unanswered is listed only when it carries notes.
37
+ */
38
+ export function answerPairs(answers: Answer[]): string {
31
39
  return answers
32
40
  .map((answer) => {
33
- const value = answer.selected.length > 0 ? answer.selected.join(", ") : "(no answer)";
34
- const typed = answer.freeform ? " (typed by the user)" : "";
35
- const notes = answer.notes ? `\n→ notes: ${answer.notes}` : "";
36
- return `${answer.question}\n→ ${value}${typed}${notes}`;
41
+ const answered = answer.selected.length > 0;
42
+ if (!answered && !answer.notes) return undefined;
43
+ const parts = [answered ? `"${answer.question}"="${answer.selected.join(", ")}"` : `"${answer.question}"=(no option selected)`];
44
+ if (answer.preview) parts.push(`selected preview:\n${answer.preview}`);
45
+ if (answer.notes) parts.push(`notes: ${answer.notes}`);
46
+ return parts.join(" ");
37
47
  })
38
- .join("\n\n");
48
+ .filter((pair): pair is string => pair !== undefined)
49
+ .join(", ");
50
+ }
51
+
52
+ /**
53
+ * ask_user_question's result, in Claude Code's words. Answers picked from the
54
+ * offered options are confirmed; an answer the user typed, or one with notes,
55
+ * may redirect the task, so the model is told to read it carefully.
56
+ */
57
+ export function formatAnswers(answers: Answer[]): string {
58
+ const pairs = answerPairs(answers);
59
+ if (!pairs) return "The user did not answer the questions.";
60
+ const fromOptions = answers.every((answer) => !answer.notes && !answer.typed && !answer.freeform);
61
+ return fromOptions
62
+ ? `Your questions have been answered: ${pairs}. You can now continue with these answers in mind.`
63
+ : `The user answered: ${pairs}. Read the answers carefully — they may request clarification, changes, or that you not proceed — and follow what they actually say.`;
39
64
  }
40
65
 
41
66
  /** Tool result when the user picks "Chat about this" instead of answering. */
@@ -14,8 +14,9 @@
14
14
  * painting wherever it can along the way.
15
15
  */
16
16
 
17
- import { cutPlainText, padPlainText, truncateLine, visibleWidth, wrapPlainText } from "../lib/tui-render.ts";
17
+ import { cutPlainText, padPlainText, truncateLine, visibleWidth, wrapHints, wrapPlainText } from "../lib/tui-render.ts";
18
18
  import type { Answer, Question } from "./questions.ts";
19
+ import { keyId, keyText } from "../lib/key-input.ts";
19
20
 
20
21
  export type Paint = (color: string, text: string) => string;
21
22
  export type Bold = (text: string) => string;
@@ -124,35 +125,29 @@ export type WidgetKey =
124
125
  | { kind: "text"; text: string };
125
126
 
126
127
  export function decodeWidgetKey(data: string): WidgetKey | undefined {
127
- switch (data) {
128
- case "\x1b[A":
129
- case "\x1bOA":
128
+ switch (keyId(data)) {
129
+ case "up":
130
130
  return { kind: "up" };
131
- case "\x1b[B":
132
- case "\x1bOB":
131
+ case "down":
133
132
  return { kind: "down" };
134
- case "\x1b[D":
135
- case "\x1bOD":
136
- case "\x1b[Z": // shift+tab
133
+ case "left":
134
+ case "shift+tab":
137
135
  return { kind: "prevTab" };
138
- case "\x1b[C":
139
- case "\x1bOC":
140
- case "\t":
136
+ case "right":
137
+ case "tab":
141
138
  return { kind: "nextTab" };
142
- case "\r":
143
- case "\n":
139
+ case "enter":
144
140
  return { kind: "enter" };
145
- case "\x1b":
146
- case "\x03": // ctrl+c — same intent as escape while the dialog is focused
141
+ case "escape":
142
+ case "ctrl+c": // same intent as escape while the dialog is focused
147
143
  return { kind: "esc" };
148
- case "\x7f":
149
- case "\b":
144
+ case "backspace":
145
+ case "ctrl+backspace":
150
146
  return { kind: "backspace" };
151
147
  default: {
152
- // Unrecognized escape sequences (home/end/F-keys) must not leak their
153
- // tail characters into a text draft.
154
- if (data.startsWith("\x1b")) return undefined;
155
- const text = [...data].filter((char) => char >= " " && char !== "\x7f").join("");
148
+ // Unrecognized escape sequences (home/end/F-keys) type nothing, so
149
+ // their tail characters never leak into a text draft.
150
+ const text = keyText(data);
156
151
  return text ? { kind: "text", text } : undefined;
157
152
  }
158
153
  }
@@ -204,6 +199,12 @@ export function applyWidgetKey(state: WidgetState, key: WidgetKey): WidgetResult
204
199
  case "enter":
205
200
  return activateRow(state, rows[questionState.cursor]);
206
201
  case "text": {
202
+ // The free-text row is an input: typing on it starts the answer, as in
203
+ // Claude Code, instead of being dropped until Enter opens it.
204
+ if (rows[questionState.cursor]?.kind === "other") {
205
+ state.editing = { field: "other", draft: questionState.otherText + key.text };
206
+ return;
207
+ }
207
208
  if (key.text === "n" && hasPreviews(question)) {
208
209
  state.editing = { field: "notes", draft: questionState.notes };
209
210
  return;
@@ -303,11 +304,14 @@ function advance(state: WidgetState): void {
303
304
  function collectAnswer(question: Question, state: QuestionState): Answer {
304
305
  const labels = [...state.selected].sort((a, b) => a - b).map((index) => question.options[index].label);
305
306
  if (state.otherChosen) labels.push(state.otherText);
307
+ const picked = !question.multiSelect && state.selected.length === 1 && !state.otherChosen ? question.options[state.selected[0]] : undefined;
306
308
  return {
307
309
  question: question.question,
308
310
  header: question.header,
309
311
  selected: labels,
310
312
  freeform: labels.length > 0 && state.selected.length === 0,
313
+ ...(state.otherChosen ? { typed: true } : {}),
314
+ ...(picked?.preview?.trim() ? { preview: picked.preview } : {}),
311
315
  notes: state.notes || undefined,
312
316
  };
313
317
  }
@@ -333,7 +337,9 @@ export function renderWidget(state: WidgetState, style: WidgetStyle, width: numb
333
337
  if (state.tab === state.questions.length) renderSubmitTab(state, paint, width, out);
334
338
  else renderQuestionTab(state, style, width, out);
335
339
  out.push("");
336
- out.push(paint("dim", cutPlainText(footerFor(state), Math.max(1, width - 1))));
340
+ // The key hints wrap at their separators on a narrow terminal, so "Esc to
341
+ // cancel" is never the part cut off.
342
+ for (const line of wrapHints(footerFor(state), Math.max(1, width - 1))) out.push(paint("dim", line));
337
343
  // Final ANSI-aware guard: pi-tui crashes the app on an overwide line, and
338
344
  // the painted tab bar can exceed narrow widths.
339
345
  return out.map((line) => truncateLine(line, width));
@@ -440,7 +446,9 @@ function renderStackedRows(state: WidgetState, style: WidgetStyle, width: number
440
446
  const editing = state.editing?.field === "other";
441
447
  const check = question.multiSelect ? (questionState.otherChosen ? "[✔] " : "[ ] ") : "";
442
448
  if (editing) {
443
- out.push(paint("accent", cut(`❯ ${number} ${check}${state.editing?.draft ?? ""}▏`)));
449
+ // A long answer wraps under its row while typing; nothing is cut.
450
+ const prefix = `❯ ${number} ${check}`;
451
+ for (const line of wrapUnder(prefix, `${state.editing?.draft ?? ""}▏`, width)) out.push(paint("accent", line));
444
452
  } else if (questionState.otherText) {
445
453
  out.push(labelRow(`${pointer} ${number} ${check}`, questionState.otherText, focused || questionState.otherChosen));
446
454
  } else {
@@ -505,7 +513,7 @@ function renderPreviewRows(state: WidgetState, style: WidgetStyle, width: number
505
513
  const notesIndent = " ".repeat(sideBySide ? leftWidth : 2);
506
514
  out.push("");
507
515
  if (state.editing?.field === "notes") {
508
- out.push(`${notesIndent}${paint("accent", "Notes: ")}${cut(state.editing.draft)}▏`);
516
+ out.push(...wrapUnder(`${notesIndent}Notes: `, `${state.editing.draft}▏`, width, `${notesIndent}${paint("accent", "Notes: ")}`));
509
517
  } else if (questionState.notes) {
510
518
  out.push(`${notesIndent}${paint("accent", "Notes: ")}${cut(questionState.notes)}`);
511
519
  } else {
@@ -516,6 +524,18 @@ function renderPreviewRows(state: WidgetState, style: WidgetStyle, width: number
516
524
  pushChatRow(state, rows, style, width, out);
517
525
  }
518
526
 
527
+ /**
528
+ * `prefix` then `text`, wrapped to the row width with continuation lines
529
+ * indented under the text, so a typed answer shows whole. `painted` replaces
530
+ * the plain prefix on the first line (same width, with colour).
531
+ */
532
+ function wrapUnder(prefix: string, text: string, width: number, painted = prefix): string[] {
533
+ const indent = visibleWidth(prefix);
534
+ const room = Math.max(4, width - 1 - indent);
535
+ const wrapped = wrapPlainText(text, room);
536
+ return wrapped.map((line, i) => (i === 0 ? painted : " ".repeat(indent)) + line);
537
+ }
538
+
519
539
  function previewBox(preview: string, maxOuter: number, paint: Paint): string[] {
520
540
  const inner = Math.max(8, maxOuter - 4);
521
541
  const wrapped = wrapPlainText(preview.replace(/\t/g, " "), inner);