one-code-extension 0.4.1 → 0.5.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 (419) 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 +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -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 +123 -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 +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -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 +52 -0
  63. package/extensions/background/wakeup.ts +159 -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 +88 -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 +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +18 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Claude Code's `SubagentHandback`: the tool every child session (an Agent
3
+ * run, a fork, a workflow agent) returns its result through, and the closing
4
+ * reminder that tells it so (decisions/subagents-workflows.md, "Every child
5
+ * hands back through SubagentHandback"). Claude Code gives it to Opus and
6
+ * Sonnet children only; One Code gives it to every child on every model.
7
+ *
8
+ * The tool records the report in the run's `HandbackSlot` and ends the run
9
+ * (`terminate`, so no further request goes out after a batch of hand-backs).
10
+ * The runner then reports the recorded text as the child's result, and falls
11
+ * back to the child's final message when the tool was never called.
12
+ *
13
+ * The tool keeps Claude Code's exact name, like `Agent` and `SendMessage`,
14
+ * because the harness frames that carry its report already name it ("its
15
+ * SubagentHandback call", lib/notifications.ts). It is harness plumbing: the
16
+ * child permission and hook gates never gate it, and an agent file's `tools`
17
+ * allowlist keeps it.
18
+ *
19
+ * In a fork the tool is deferred: a fork sends the parent's tool list so it
20
+ * reads the parent's prompt cache (subagents/fork-cache.ts), and an eager tool
21
+ * of its own would be dropped from that list. A deferred one reaches the
22
+ * request through a `tool_search` load, as the fork's `SendMessage` does.
23
+ */
24
+
25
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
26
+ import { Type } from "typebox";
27
+ import { DEFER_CHANNEL } from "./deferred.ts";
28
+ import { REMINDER_CHANNEL, type ReminderPayload } from "./reminders.ts";
29
+
30
+ export const SUBAGENT_HANDBACK = "SubagentHandback";
31
+
32
+ /** Claude Code's description of the tool. */
33
+ export const SUBAGENT_HANDBACK_DESCRIPTION =
34
+ "Deliver your final report to the agent that spawned you (your caller). Use it once, for that hand-off only: when your work is complete, call SubagentHandback({message: <your full report>}). The call ends your run, so do everything else first and put everything your caller needs in that one report. It is not a messaging channel: do not use it for progress updates or questions.\n\nOnly a report delivered through SubagentHandback reaches your caller; plain text you write at the end of your run is NOT delivered. There is no recipient parameter: the report can only go to your caller.";
35
+
36
+ /** Claude Code's closing reminder on the child's task message. */
37
+ export const SUBAGENT_HANDBACK_REMINDER =
38
+ "Your final report is delivered through SubagentHandback: when your work is complete, call SubagentHandback({message: <your full report>}). The call ends your run, so make it your last step. Only a SubagentHandback call reaches your caller as your result; plain text you write at the end is not delivered.";
39
+
40
+ /**
41
+ * One Code's line before the reminder in a fork, where the tool is deferred
42
+ * (the fork sends its parent's tool list): it names the load step, which the
43
+ * frozen deferred listing it inherits does not.
44
+ */
45
+ export const SUBAGENT_HANDBACK_LOAD_FIRST =
46
+ 'SubagentHandback is deferred here: load it first with tool_search (query "select:SubagentHandback").';
47
+
48
+ /** Claude Code's result texts for the call. */
49
+ export const HANDBACK_DELIVERED = "Report delivered to your caller.";
50
+ export const HANDBACK_ALREADY_DELIVERED =
51
+ "Nothing was sent: your report was already delivered (SubagentHandback delivers one report). Use SendMessage for anything further, then stop.";
52
+ export const HANDBACK_EMPTY = "message must not be empty: pass your full report as `message`.";
53
+
54
+ /** Where a run keeps the report its child handed back. */
55
+ export interface HandbackSlot {
56
+ /** Record the report; false when this turn already delivered one. */
57
+ recordHandback(message: string): boolean;
58
+ }
59
+
60
+ /** A slot for a one-turn run (a workflow agent): the first report wins. */
61
+ export function oneTurnHandbackSlot(): HandbackSlot & { report?: string } {
62
+ const slot: HandbackSlot & { report?: string } = {
63
+ report: undefined,
64
+ recordHandback(message) {
65
+ if (slot.report !== undefined) return false;
66
+ slot.report = message;
67
+ return true;
68
+ },
69
+ };
70
+ return slot;
71
+ }
72
+
73
+ export function subagentHandbackTool(slot: HandbackSlot): ToolDefinition {
74
+ return {
75
+ name: SUBAGENT_HANDBACK,
76
+ label: "Hand back",
77
+ description: SUBAGENT_HANDBACK_DESCRIPTION,
78
+ parameters: Type.Object({ message: Type.String({ description: "Your full report for your caller" }) }, { additionalProperties: false }) as never,
79
+ async execute(_toolCallId: string, params: unknown) {
80
+ const message = (params as { message?: unknown } | undefined)?.message;
81
+ // Fail loud (decisions/tools.md): an empty report would end the run with nothing for the caller.
82
+ if (typeof message !== "string" || !message.trim()) {
83
+ return { content: [{ type: "text" as const, text: HANDBACK_EMPTY }], details: {}, isError: true };
84
+ }
85
+ if (!slot.recordHandback(message)) {
86
+ return { content: [{ type: "text" as const, text: HANDBACK_ALREADY_DELIVERED }], details: {}, isError: true };
87
+ }
88
+ return { content: [{ type: "text" as const, text: HANDBACK_DELIVERED }], details: { handback: true }, terminate: true };
89
+ },
90
+ } as ToolDefinition;
91
+ }
92
+
93
+ /**
94
+ * The inline extension a child session loads for the hand-back: the tool,
95
+ * deferred in a fork, and the closing reminder. The reminder is a standing
96
+ * block (`sticky-append`) switched on at session start, so it rides the
97
+ * child's task message and every later message of the run (a resident's
98
+ * next turns hand back too), and never an inherited message of a fork.
99
+ */
100
+ export function subagentHandbackExtension(slot: HandbackSlot, options: { deferred?: boolean } = {}) {
101
+ return (pi: ExtensionAPI) => {
102
+ pi.registerTool(subagentHandbackTool(slot));
103
+ if (options.deferred) pi.events.emit(DEFER_CHANNEL, { name: SUBAGENT_HANDBACK, keywords: ["handback", "report", "result", "final", "caller"] });
104
+ pi.on("session_start", () => {
105
+ pi.events.emit(REMINDER_CHANNEL, {
106
+ text: options.deferred ? `${SUBAGENT_HANDBACK_LOAD_FIRST}\n${SUBAGENT_HANDBACK_REMINDER}` : SUBAGENT_HANDBACK_REMINDER,
107
+ scope: "every-turn",
108
+ key: "subagent-handback",
109
+ placement: "sticky-append",
110
+ } satisfies ReminderPayload);
111
+ });
112
+ };
113
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Which models get Claude Code's mid-conversation `role: "system"` message,
3
+ * and in which wire shape (pure). One function for the system-prompt builder,
4
+ * the reminder hook and the tool hook, so all three agree on every request.
5
+ *
6
+ * The gate is pi's model catalog flag `compat.supportsMidConvoSystemMessages`
7
+ * alone, no list of our own: pi owns the per-model facts and adds models as it
8
+ * confirms them (`working-docs/decisions/tools.md`, "Claude Code's request
9
+ * shape, on every model pi flags for it"). Haiku before 5 is never in it:
10
+ * Anthropic rejects the role there (findings §52); Haiku 5.5 takes it
11
+ * (findings §67). Every unflagged model keeps the context in the first user
12
+ * message.
13
+ */
14
+
15
+ import { parseClaudeId } from "./claude-id.ts";
16
+
17
+ /** The wire shape a system message takes: Anthropic `system`, OpenAI Responses `input`, or Chat Completions `messages`. */
18
+ export type SystemRoleLayout = "anthropic" | "responses" | "completions";
19
+
20
+ /** The fields of a pi `Model` the gate reads. */
21
+ export interface SystemRoleModel {
22
+ id: string;
23
+ api: string;
24
+ compat?: unknown;
25
+ }
26
+
27
+ /** pi-ai's Responses APIs: requests carry `input` items and flat `{ type, name }` tools. */
28
+ export const RESPONSES_APIS: ReadonlySet<string> = new Set(["openai-responses", "azure-openai-responses", "openai-codex-responses"]);
29
+
30
+ /** The wire shape of a pi API, for the three shapes One Code rewrites; undefined for the rest (Google, Bedrock). */
31
+ export function wireShape(api: string): SystemRoleLayout | undefined {
32
+ if (api === "anthropic-messages") return "anthropic";
33
+ if (RESPONSES_APIS.has(api)) return "responses";
34
+ if (api === "openai-completions") return "completions";
35
+ return undefined;
36
+ }
37
+
38
+ /** The payload field that holds the conversation for a wire shape. */
39
+ export function messagesKey(shape: SystemRoleLayout): "input" | "messages" {
40
+ return shape === "responses" ? "input" : "messages";
41
+ }
42
+
43
+ export function systemRoleLayout(model: SystemRoleModel | undefined): SystemRoleLayout | undefined {
44
+ if (!model) return undefined;
45
+ const compat = model.compat as { supportsMidConvoSystemMessages?: unknown } | undefined;
46
+ if (compat?.supportsMidConvoSystemMessages !== true) return undefined;
47
+ if (model.api === "anthropic-messages" && isHaikuBeforeFive(model.id)) return undefined;
48
+ return wireShape(model.api);
49
+ }
50
+
51
+ /** A Haiku older than Haiku 5 in any provider's spelling; the old `claude-3-5-haiku` order parses as none. */
52
+ function isHaikuBeforeFive(id: string): boolean {
53
+ const claude = parseClaudeId(id);
54
+ return claude ? claude.family === "haiku" && claude.major < 5 : id.includes("haiku");
55
+ }
56
+
57
+ /** One context block that moves: its text as injected on the user message, and its text inside the system message. */
58
+ export interface MovedBlock {
59
+ /** The block exactly as the user message carries it (`<system-reminder>` frame and suffix included). */
60
+ framed: string;
61
+ /** The unwrapped text, as Claude Code's system message joins it. */
62
+ inner: string;
63
+ }
64
+
65
+ /**
66
+ * The role a system message takes on the OpenAI APIs: pi's own rule per API
67
+ * (`developer` for a reasoning model that accepts it, else `system`).
68
+ */
69
+ export function instructionRole(layout: SystemRoleLayout, model: { reasoning?: boolean; compat?: unknown }): "system" | "developer" {
70
+ if (layout === "anthropic") return "system";
71
+ const developer = (model.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole;
72
+ const accepts = layout === "responses" ? developer !== false : developer === true;
73
+ return model.reasoning && accepts ? "developer" : "system";
74
+ }
75
+
76
+ /** A wire message and content part, as far as the payload rewrites read them. */
77
+ export type WireMessage = { role?: unknown; type?: unknown; content?: unknown; output?: unknown; call_id?: unknown; tool_call_id?: unknown };
78
+ export type WirePart = { type?: unknown; text?: unknown; content?: unknown; tool_use_id?: unknown; cache_control?: unknown };
79
+
80
+ /**
81
+ * The parts with those `drop` accepts removed, keeping pi's cache mark: when a
82
+ * removed part carried it, it moves to the new last part. Undefined when every
83
+ * part would go (a message cannot be left empty) or nothing is removed.
84
+ */
85
+ export function withoutParts(parts: readonly WirePart[], drop: (part: WirePart, index: number) => boolean): WirePart[] | undefined {
86
+ let mark: unknown;
87
+ const kept = parts.filter((part, index) => {
88
+ if (!drop(part, index)) return true;
89
+ if (part?.cache_control !== undefined) mark = part.cache_control;
90
+ return false;
91
+ });
92
+ if (kept.length === parts.length || kept.length === 0) return undefined;
93
+ if (mark === undefined || kept[kept.length - 1].cache_control !== undefined) return kept;
94
+ return [...kept.slice(0, -1), { ...kept[kept.length - 1], cache_control: mark }];
95
+ }
96
+
97
+ /**
98
+ * The value stored under a pi call id for the id a request carries: the same
99
+ * id, the Responses call id before pi's `|item` suffix, or the id pi-ai's
100
+ * Anthropic conversion rewrote (any other character to `_`, at most 64), as it
101
+ * does for a call made on another provider before a model switch.
102
+ */
103
+ export function byWireCallId<V>(stored: ReadonlyMap<string, V>, wireId: string): V | undefined {
104
+ if (stored.has(wireId)) return stored.get(wireId);
105
+ for (const [id, value] of stored) {
106
+ if (id.split("|")[0] === wireId || id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) === wireId) return value;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ /** A system message with `text` in the API's own shape. */
112
+ export function systemMessage(layout: SystemRoleLayout, role: "system" | "developer", text: string): WireMessage {
113
+ return layout === "anthropic" ? { role: "system", content: [{ type: "text", text }] } : { role, content: text };
114
+ }
115
+
116
+ /**
117
+ * The payload with the moved context blocks lifted off the user message that
118
+ * carries them and joined, unwrapped and a blank line apart, into one system
119
+ * message placed right after it, in the API's own shape (Claude Code's layout,
120
+ * `decisions/tools.md`). Blocks are found by their exact text, never by
121
+ * position: a fork's request drops the parent's first message, and then nothing
122
+ * moves. The cache mark is reseated afterwards (anthropic-payload.ts
123
+ * reseatMessageMark). Undefined when nothing changes.
124
+ */
125
+ export function withSystemRoleContext(
126
+ payload: Record<string, unknown>,
127
+ layout: SystemRoleLayout,
128
+ moved: readonly MovedBlock[],
129
+ role: "system" | "developer",
130
+ ): Record<string, unknown> | undefined {
131
+ if (moved.length === 0) return undefined;
132
+ const key = messagesKey(layout);
133
+ const messages = payload[key];
134
+ if (!Array.isArray(messages)) return undefined;
135
+ const wanted = new Set(moved.map((block) => block.framed));
136
+ const index = messages.findIndex(
137
+ (message: WireMessage) => message?.role === "user" && Array.isArray(message.content) && (message.content as WirePart[]).some((part) => wanted.has(part?.text as string)),
138
+ );
139
+ if (index === -1) return undefined;
140
+
141
+ const carrier = messages[index] as WireMessage & { content: WirePart[] };
142
+ const found = new Set<string>();
143
+ const kept = carrier.content.filter((part) => {
144
+ const text = part?.text;
145
+ if (typeof text !== "string" || !wanted.has(text) || found.has(text)) return true;
146
+ found.add(text);
147
+ return false;
148
+ });
149
+ const text = moved
150
+ .filter((block) => found.has(block.framed))
151
+ .map((block) => block.inner)
152
+ .join("\n\n");
153
+
154
+ const user: WireMessage = { ...carrier, content: kept };
155
+ return { ...payload, [key]: [...messages.slice(0, index), user, systemMessage(layout, role, text), ...messages.slice(index + 1)] };
156
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Control characters in text that reaches the terminal (pure).
3
+ *
4
+ * Tool results, tool arguments, notification bodies, MCP results, fetched
5
+ * pages and the session title all carry text One Code did not write. Sent to
6
+ * the terminal raw, an escape sequence in it acts: OSC 0 retitles the
7
+ * terminal, OSC 52 rewrites the clipboard in kitty, WezTerm, Alacritty and
8
+ * Windows Terminal, cursor movement rewrites the rows above, SGR conceal hides
9
+ * text, and a carriage return overwrites the start of the row.
10
+ *
11
+ * `sanitizeDisplayText` applies pi's own rule for a tool result
12
+ * (`render-utils.js getTextOutput`: `stripAnsi`, then `sanitizeBinaryOutput`,
13
+ * then drop `\r`), and also drops C1 controls, which some terminals act on.
14
+ * It keeps `\n` and `\t` and never touches styling One Code adds afterwards,
15
+ * because it runs on the text before painting. `escapeControlText` shows each
16
+ * control character as a visible `\xHH` escape instead, for consent dialogs,
17
+ * where the user must see exactly what would run. `sanitizeTitle` is for the
18
+ * one-line terminal title.
19
+ *
20
+ * Display only: the model-facing text is never passed through these.
21
+ */
22
+
23
+ // pi's `ansiRegex` (utils/ansi.js, from chalk's ansi-regex) matches an OSC up
24
+ // to the first ST, else CSI and the related ESC forms. Its OSC branch is a lazy
25
+ // scan to the end of the text from every `ESC ]`, quadratic on many
26
+ // unterminated ones (50,000 took 1.8 s), so `stripAnsi` walks the text once
27
+ // with the same alternation and remembers where no terminator is left.
28
+ const CSI_AT = /[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]/y;
29
+
30
+ /** The index just past the first string terminator (BEL, `ESC \`, or C1 ST) at or after `from`, else -1. */
31
+ function stringTerminatorEnd(text: string, from: number): number {
32
+ for (let i = from; i < text.length; i++) {
33
+ const ch = text.charCodeAt(i);
34
+ if (ch === 0x07 || ch === 0x9c) return i + 1;
35
+ if (ch === 0x1b && text.charCodeAt(i + 1) === 0x5c) return i + 2;
36
+ }
37
+ return -1;
38
+ }
39
+
40
+ /** `text.replace(ansiRegex, "")` in linear time: the same matches, left to right. */
41
+ function stripAnsi(text: string): string {
42
+ let out = "";
43
+ let from = 0;
44
+ // No string terminator exists at or after this index (a failed search holds for every later start).
45
+ let noTerminatorFrom = Number.POSITIVE_INFINITY;
46
+ let i = 0;
47
+ while (i < text.length) {
48
+ const ch = text.charCodeAt(i);
49
+ if (ch !== 0x1b && ch !== 0x9b) {
50
+ i++;
51
+ continue;
52
+ }
53
+ if (ch === 0x1b && text.charCodeAt(i + 1) === 0x5d && i + 2 < noTerminatorFrom) {
54
+ const end = stringTerminatorEnd(text, i + 2);
55
+ if (end !== -1) {
56
+ out += text.slice(from, i);
57
+ from = i = end;
58
+ continue;
59
+ }
60
+ noTerminatorFrom = i + 2;
61
+ }
62
+ CSI_AT.lastIndex = i;
63
+ const match = CSI_AT.exec(text);
64
+ if (match) {
65
+ out += text.slice(from, i);
66
+ from = i = i + match[0].length;
67
+ continue;
68
+ }
69
+ i++;
70
+ }
71
+ return out + text.slice(from);
72
+ }
73
+
74
+ /** C0 controls other than tab and newline, DEL, C1 controls, and U+FFF9 to U+FFFB (interlinear annotation). */
75
+ const UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f-]/g;
76
+ const HAS_UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f-]/;
77
+
78
+ /**
79
+ * Text safe to write to the terminal: escape sequences removed, then every
80
+ * control character other than `\n` and `\t` (carriage returns included, as
81
+ * pi does). Plain text comes back unchanged (same string).
82
+ */
83
+ export function sanitizeDisplayText(text: string): string {
84
+ if (!HAS_UNSAFE.test(text)) return text;
85
+ return stripAnsi(text).replace(UNSAFE, "");
86
+ }
87
+
88
+ /**
89
+ * `label` as an OSC 8 hyperlink to `url` (pi-tui's `hyperlink`). Terminals
90
+ * without OSC 8, and tmux without its `hyperlinks` feature, show the label
91
+ * alone, so the label must read on its own. Both parts are sanitized, so
92
+ * neither can end the sequence early.
93
+ */
94
+ export function terminalLink(label: string, url: string): string {
95
+ return `\x1b]8;;${sanitizeDisplayText(url)}\x1b\\${sanitizeDisplayText(label)}\x1b]8;;\x1b\\`;
96
+ }
97
+
98
+ /**
99
+ * Every control character other than `\n` and `\t` shown as a visible
100
+ * `\xHH` escape (`\x1b[8m` for an SGR conceal), so nothing in the text can
101
+ * act on the terminal or hide part of itself.
102
+ */
103
+ export function escapeControlText(text: string): string {
104
+ if (!HAS_UNSAFE.test(text)) return text;
105
+ return text.replace(UNSAFE, (ch) => {
106
+ const code = ch.charCodeAt(0);
107
+ return code > 0xff ? `\\u${code.toString(16).padStart(4, "0")}` : `\\x${code.toString(16).padStart(2, "0")}`;
108
+ });
109
+ }
110
+
111
+ /** One line for the terminal title: escape sequences and every control character removed, whitespace collapsed. */
112
+ export function sanitizeTitle(text: string): string {
113
+ return sanitizeDisplayText(text).replace(/\s+/g, " ").trim();
114
+ }
@@ -17,6 +17,10 @@
17
17
  * 2-vs-1 decision is byte-identical to pi's without importing pi or adding a
18
18
  * package. Grapheme segmentation and the mark/emoji classification use V8's
19
19
  * built-in `Intl.Segmenter` and Unicode property regexes.
20
+ *
21
+ * Synced with pi-tui 0.99.2. `text-width.test.ts` compares this measure with
22
+ * pi-tui's over every code point to U+3FFFF and over random strings with
23
+ * escapes, so a pi bump that changes pi-tui's measure fails the suite.
20
24
  */
21
25
 
22
26
  // --- East Asian wide/fullwidth lookup (get-east-asian-width v1.6.0, Unicode 16) ---
@@ -127,6 +131,7 @@ export function graphemeWidth(segment: string): number {
127
131
  } else if (!nonPrintingCharRegex.test(char)) {
128
132
  const c = char.codePointAt(0)!;
129
133
  if (followsMark || (c >= 0xff00 && c <= 0xffef)) width += eastAsianWidth(c);
134
+ else if (c === 0x0e33 || c === 0x0eb3) width += 1; // Thai SARA AM, Lao AM
130
135
  followsMark = false;
131
136
  }
132
137
  }
@@ -134,36 +139,35 @@ export function graphemeWidth(segment: string): number {
134
139
  }
135
140
 
136
141
  // --- ANSI / OSC / APC escape handling ---
137
- // A single terminal control sequence starting at index 0 of `s`, or null.
138
- // Covers CSI (styling/cursor), OSC (hyperlinks, prompt markers, terminated by
139
- // BEL or ST) and APC (terminated by ST) — the same families pi-tui strips.
142
+ // The length of the terminal control sequence starting at index `i`, or 0.
143
+ // Mirrors pi-tui's `extractAnsiCode` exactly, because pi-tui validates a line
144
+ // with it: a CSI runs to the first `m`, `G`, `K`, `H` or `J`; an OSC or APC
145
+ // runs to BEL or ST; an unterminated sequence, or any other ESC form, is not
146
+ // a sequence, so its bytes count as text. A broader rule here measured such
147
+ // text narrower than pi-tui and could emit a line pi-tui rejects. Display text
148
+ // never carries those forms once `sanitizeDisplayText` has run.
140
149
  function escapeAt(s: string, i: number): number {
141
150
  if (s.charCodeAt(i) !== 0x1b) return 0;
142
151
  const next = s[i + 1];
143
152
  if (next === "[") {
144
- // CSI: ESC [ ... final byte 0x40–0x7E
145
153
  let j = i + 2;
146
- while (j < s.length) {
147
- const c = s.charCodeAt(j);
148
- j++;
149
- if (c >= 0x40 && c <= 0x7e) return j - i;
150
- }
151
- return s.length - i;
154
+ while (j < s.length && !CSI_END.test(s[j])) j++;
155
+ return j < s.length ? j + 1 - i : 0;
152
156
  }
153
- if (next === "]" || next === "_" || next === "P" || next === "^") {
154
- // OSC / APC / DCS / PM: ESC <intro> ... terminated by BEL or ST (ESC \)
157
+ if (next === "]" || next === "_") {
155
158
  let j = i + 2;
156
159
  while (j < s.length) {
157
160
  if (s.charCodeAt(j) === 0x07) return j + 1 - i; // BEL
158
161
  if (s.charCodeAt(j) === 0x1b && s[j + 1] === "\\") return j + 2 - i; // ST
159
162
  j++;
160
163
  }
161
- return s.length - i;
164
+ return 0;
162
165
  }
163
- // Other two-byte escapes (ESC c, ESC =, …)
164
- return next ? 2 : 1;
166
+ return 0;
165
167
  }
166
168
 
169
+ const CSI_END = /[mGKHJ]/;
170
+
167
171
  const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
168
172
 
169
173
  /**
@@ -174,6 +178,9 @@ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
174
178
  export function visibleWidth(text: string): number {
175
179
  if (text.length === 0) return 0;
176
180
  if (PRINTABLE_ASCII.test(text)) return text.length;
181
+ // pi-tui expands tabs before segmenting, so a mark after a tab joins the
182
+ // last space's cluster; doing the same keeps the two measures identical.
183
+ if (text.includes("\t")) text = text.replace(/\t/g, " ");
177
184
  let clean = "";
178
185
  for (let i = 0; i < text.length; ) {
179
186
  const esc = escapeAt(text, i);
@@ -204,6 +211,29 @@ export function sliceColumns(text: string, maxCols: number): { text: string; wid
204
211
  return fitPainted(text, maxCols);
205
212
  }
206
213
 
214
+ /**
215
+ * Longest suffix of PLAIN (escape-free) text whose visible width is ≤
216
+ * `maxCols`, cut on a grapheme boundary: the counterpart of `sliceColumns` for
217
+ * text that keeps its tail, such as a path whose deepest folders matter most.
218
+ */
219
+ export function tailColumns(text: string, maxCols: number): { text: string; width: number } {
220
+ if (maxCols <= 0) return { text: "", width: 0 };
221
+ if (PRINTABLE_ASCII.test(text)) {
222
+ const tail = text.slice(Math.max(0, text.length - maxCols));
223
+ return { text: tail, width: tail.length };
224
+ }
225
+ const graphemes = [...graphemeSegmenter.segment(text)].map((s) => s.segment);
226
+ let width = 0;
227
+ let start = graphemes.length;
228
+ while (start > 0) {
229
+ const w = graphemeWidth(graphemes[start - 1]);
230
+ if (width + w > maxCols) break;
231
+ width += w;
232
+ start--;
233
+ }
234
+ return { text: graphemes.slice(start).join(""), width };
235
+ }
236
+
207
237
  /**
208
238
  * Hard-wrap text into chunks each ≤ `columns` visible columns, on grapheme
209
239
  * boundaries. A single glyph wider than the whole budget occupies its own chunk
@@ -253,7 +283,8 @@ export function fitPainted(line: string, maxCols: number): { text: string; width
253
283
  continue;
254
284
  }
255
285
  // Consume the visible run up to the next escape, segment it into graphemes.
256
- let end = i;
286
+ // An ESC that starts no sequence is text (width 0), so the run starts past it.
287
+ let end = line.charCodeAt(i) === 0x1b ? i + 1 : i;
257
288
  while (end < line.length && line.charCodeAt(end) !== 0x1b) end++;
258
289
  for (const { segment } of graphemeSegmenter.segment(line.slice(i, end))) {
259
290
  const w = graphemeWidth(segment);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * An injectable timer (pure), so an idle-timer state machine is unit-tested
3
+ * with fake timers, and the real adapter both recap and idle-compact run on.
4
+ */
5
+
6
+ export interface TimerOps {
7
+ /** Schedule `cb` after `ms`; return a handle for `clear`. */
8
+ set(cb: () => void, ms: number): unknown;
9
+ clear(handle: unknown): void;
10
+ }
11
+
12
+ /** `setTimeout` with the handle unref'd, so a pending idle timer never keeps a one-shot process alive. */
13
+ export const unrefTimers: TimerOps = {
14
+ set: (cb, ms) => {
15
+ const handle = setTimeout(cb, ms);
16
+ handle.unref?.();
17
+ return handle;
18
+ },
19
+ clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
20
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Claude Code's `tool_addition` for tools that arrive mid-session (pure). One
3
+ * Code stores the arrival as the deferred-tool addendum one-shot
4
+ * (lib/deferred.ts deferredAddendumText), which reads correctly on any model.
5
+ * On a first-party model that takes mid-conversation tool changes, the
6
+ * addendum leaves the message that carries it for a system message right
7
+ * after it: Claude Code's sentence and names, then one reference-form
8
+ * `tool_addition` block per tool, each declared deferred in `tools`. The model
9
+ * can then call the tool without a `tool_search` round trip (findings §55
10
+ * P1). Elsewhere the addendum stays as it is.
11
+ *
12
+ * An addendum is matched as a whole block, never inside other text: in a tool
13
+ * result at `context` time, while the result's blocks are still separate and
14
+ * keyed by its call id (`liftAddenda`), and on a user message as a whole part
15
+ * (a one-shot pinned there).
16
+ */
17
+
18
+ import { MID_CONVERSATION_SYSTEM_BETA, MID_CONVERSATION_TOOL_CHANGES_BETA, reseatMessageMark } from "./anthropic-payload.ts";
19
+ import { byWireCallId, systemRoleLayout, type WireMessage, type WirePart, withoutParts } from "./system-role.ts";
20
+
21
+ /** The first line of the stored addendum; the names follow, one per line. */
22
+ export const ADDENDUM_HEAD =
23
+ "Additional deferred tools became available via tool_search since this conversation started (they registered after the first request). Same rules: load with tool_search \"select:<name>\" before calling.";
24
+
25
+ /** Claude Code's sentence before the names, in its tool-addition message. */
26
+ export const TOOLS_AVAILABLE = "The following tools just became available and are ready to use:";
27
+
28
+ /** Claude Code's betas for a reference-form tool addition in a system message. */
29
+ export const TOOL_ADDITION_BETAS = [MID_CONVERSATION_SYSTEM_BETA, MID_CONVERSATION_TOOL_CHANGES_BETA];
30
+
31
+ /**
32
+ * Whether the model takes Claude Code's reference-form `tool_addition`:
33
+ * first-party Anthropic on the system-role layout, with pi's catalog flag for
34
+ * mid-conversation tool changes (Opus 5.5, Sonnet 5.5 and Fable 5.1 among them).
35
+ */
36
+ export function supportsToolAdditions(model: { id?: string; provider?: string; api?: string; compat?: unknown } | undefined): boolean {
37
+ if (!model || model.provider !== "anthropic" || systemRoleLayout({ id: model.id ?? "", api: model.api ?? "", compat: model.compat }) !== "anthropic") return false;
38
+ return (model.compat as { supportsMidConvoToolChanges?: boolean } | undefined)?.supportsMidConvoToolChanges === true;
39
+ }
40
+
41
+ const OPEN = `<system-reminder>\n${ADDENDUM_HEAD}\n`;
42
+ const CLOSE = "\n</system-reminder>";
43
+
44
+ /** The names a whole framed addendum block lists; undefined for any other text. */
45
+ export function addendumNames(text: string): string[] | undefined {
46
+ if (!text.startsWith(OPEN) || !text.endsWith(CLOSE)) return undefined;
47
+ return text
48
+ .slice(OPEN.length, -CLOSE.length)
49
+ .split("\n")
50
+ .filter((line) => line.length > 0);
51
+ }
52
+
53
+ type StoredBlock = { type?: string; text?: string };
54
+ type StoredMessage = { role: string; toolCallId?: string; content?: unknown };
55
+
56
+ /**
57
+ * The context-time step: each tool result's addendum block leaves the result,
58
+ * its names kept under the call id, for the payload step to send as an
59
+ * addition after that result. Returns the input when nothing changed.
60
+ */
61
+ export function liftAddenda<T extends StoredMessage>(messages: T[]): { messages: T[]; byCall: Map<string, string[]> } {
62
+ const byCall = new Map<string, string[]>();
63
+ let changed = false;
64
+ const out = messages.map((message) => {
65
+ if (message.role !== "toolResult" || !Array.isArray(message.content) || !message.toolCallId) return message;
66
+ const names: string[] = [];
67
+ const kept = (message.content as StoredBlock[]).filter((block) => {
68
+ const found = block?.type === "text" && typeof block.text === "string" ? addendumNames(block.text) : undefined;
69
+ if (found) names.push(...found);
70
+ return !found;
71
+ });
72
+ if (names.length === 0) return message;
73
+ changed = true;
74
+ byCall.set(message.toolCallId, names);
75
+ return { ...message, content: kept };
76
+ });
77
+ return { messages: changed ? out : messages, byCall };
78
+ }
79
+
80
+ /**
81
+ * The Anthropic payload with a tool-addition system message after each user
82
+ * message that answers a call `liftAddenda` lifted an addendum from, or that
83
+ * carries a pinned addendum part (removed). Also returns every name added: they
84
+ * must be declared deferred in `tools`. With `references: false` the message
85
+ * is Claude Code's sentence and names alone, for a request whose tools cannot
86
+ * be referenced (OAuth renames them): the tools are active, so the model can
87
+ * still call them. Undefined when there is none.
88
+ */
89
+ export function withToolAdditions(
90
+ payload: Record<string, unknown>,
91
+ byCall: ReadonlyMap<string, readonly string[]>,
92
+ references = true,
93
+ ): { payload: Record<string, unknown>; names: string[] } | undefined {
94
+ const messages = payload.messages;
95
+ if (!Array.isArray(messages)) return undefined;
96
+ const out: WireMessage[] = [];
97
+ const all = new Set<string>();
98
+ for (const message of messages as WireMessage[]) {
99
+ if (message?.role !== "user" || !Array.isArray(message.content)) {
100
+ out.push(message);
101
+ continue;
102
+ }
103
+ const names: string[] = [];
104
+ const parts = message.content as WirePart[];
105
+ for (const part of parts) {
106
+ const called = part?.type === "tool_result" && typeof part.tool_use_id === "string" ? byWireCallId(byCall, part.tool_use_id) : undefined;
107
+ if (called) names.push(...called);
108
+ }
109
+ const pinnedNames = (part: WirePart) => (part?.type === "text" && typeof part.text === "string" ? addendumNames(part.text) : undefined);
110
+ // A pinned addendum part goes, keeping pi's cache mark; a message it would empty keeps it.
111
+ const kept = withoutParts(parts, (part) => pinnedNames(part) !== undefined) ?? parts;
112
+ if (kept !== parts) for (const part of parts) names.push(...(pinnedNames(part) ?? []));
113
+ if (names.length === 0) {
114
+ out.push(message);
115
+ continue;
116
+ }
117
+ const unique = [...new Set(names)];
118
+ for (const name of unique) all.add(name);
119
+ out.push(kept.length === parts.length ? message : { ...message, content: kept }, {
120
+ role: "system",
121
+ content: [
122
+ { type: "text", text: [TOOLS_AVAILABLE, ...unique].join("\n") },
123
+ ...(references ? unique.map((name) => ({ type: "tool_addition", tool: { type: "tool_reference", name } })) : []),
124
+ ],
125
+ });
126
+ }
127
+ if (all.size === 0) return undefined;
128
+ return { payload: { ...payload, messages: reseatMessageMark(out) }, names: [...all] };
129
+ }
130
+
131
+ /** A stored message as far as the addendum scan is concerned. */
132
+ type BranchEntry = { type?: string; message?: { role?: string; content?: unknown } };
133
+
134
+ /**
135
+ * The names every addendum persisted into a tool result on the branch
136
+ * announced: on resume they are re-activated, since their addition blocks
137
+ * are rebuilt from the same stored blocks and the model may call them.
138
+ */
139
+ export function addendumNamesOnBranch(branch: readonly BranchEntry[]): string[] {
140
+ const names = new Set<string>();
141
+ for (const entry of branch) {
142
+ const message = entry?.type === "message" ? entry.message : undefined;
143
+ if (message?.role !== "toolResult" || !Array.isArray(message.content)) continue;
144
+ for (const block of message.content as StoredBlock[]) {
145
+ for (const name of (typeof block?.text === "string" ? addendumNames(block.text) : undefined) ?? []) names.add(name);
146
+ }
147
+ }
148
+ return [...names];
149
+ }