one-code-extension 0.4.0 → 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 +219 -219
  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 +66 -34
  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 +19 -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,66 @@
1
+ /**
2
+ * Claude Code's result and error texts for pi's built-in read, write and edit
3
+ * (pure). pi's tools keep their call shapes (`path`, `edits[]`) and their own
4
+ * behavior; only what the model reads back changes, by rewriting the first
5
+ * text block of a result whose text is pi's exact wording. Anything else
6
+ * (pi's truncation notes, an image, a block another hook appended) passes
7
+ * through untouched.
8
+ *
9
+ * The success texts say the file state is current, which is true here: the
10
+ * file-tracker observes the file after every successful write or edit, so the
11
+ * next edit is not refused as unread or stale.
12
+ */
13
+
14
+ type Block = { type: string; text?: string };
15
+
16
+ export const FILE_STATE_CURRENT = " (file state is current in your context — no need to read it back)";
17
+
18
+ /** Claude Code's missing-file error for read and edit. */
19
+ export function fileDoesNotExist(cwd: string): string {
20
+ return `File does not exist. Note: your current working directory is ${cwd}.`;
21
+ }
22
+
23
+ /** Claude Code's empty-file read result. */
24
+ export const EMPTY_FILE_WARNING = "<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>";
25
+
26
+ export interface FileToolResult {
27
+ toolName: string;
28
+ isError: boolean;
29
+ content: Block[];
30
+ /** The absolute path the tool touched. */
31
+ path: string;
32
+ cwd: string;
33
+ /** For a write: whether the file existed before the call (Claude Code says "updated" then, "created" otherwise). */
34
+ existedBefore?: boolean;
35
+ /** For a read: whether the file is empty on disk (asked only for an empty result). */
36
+ isEmptyFile?: () => boolean;
37
+ }
38
+
39
+ /** The first block's replacement text, or undefined to leave the result as pi wrote it. */
40
+ function rewrite(result: FileToolResult, text: string): string | undefined {
41
+ const { toolName, isError, path, cwd } = result;
42
+ if (toolName === "write" && !isError && text.startsWith("Successfully wrote to ")) {
43
+ return result.existedBefore
44
+ ? `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`
45
+ : `File created successfully at: ${path}${FILE_STATE_CURRENT}`;
46
+ }
47
+ if (toolName === "edit" && !isError && /^Successfully replaced \d+ block\(s\) in /.test(text)) {
48
+ return `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`;
49
+ }
50
+ if (toolName === "edit" && isError && /^Could not edit file: .*\. Error code: ENOENT\.$/s.test(text)) return fileDoesNotExist(cwd);
51
+ if (toolName === "read" && isError) {
52
+ if (/^ENOENT: no such file or directory, \w+ '/.test(text)) return fileDoesNotExist(cwd);
53
+ if (text === "EISDIR: illegal operation on a directory, read") return `${text} '${path}'`;
54
+ }
55
+ if (toolName === "read" && !isError && text === "" && result.isEmptyFile?.()) return EMPTY_FILE_WARNING;
56
+ return undefined;
57
+ }
58
+
59
+ /** The result's content with Claude Code's text in place of pi's, or undefined when nothing changes. */
60
+ export function fileToolResultContent<B extends Block>(result: FileToolResult & { content: B[] }): B[] | undefined {
61
+ if (!Array.isArray(result.content)) return undefined;
62
+ const [first, ...rest] = result.content;
63
+ if (first?.type !== "text" || typeof first.text !== "string") return undefined;
64
+ const text = rewrite(result, first.text);
65
+ return text === undefined ? undefined : [{ ...first, text }, ...rest];
66
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Files a shell command showed the model in full (pure apart from the bash
3
+ * grammar, which the caller awaits).
4
+ *
5
+ * The auto-mode note tells the model to read files with cat, head or sed, and
6
+ * GPT-6 models do. Before this, the edit that followed was refused as
7
+ * "not read in this conversation" although the whole file was in the
8
+ * transcript: 16 refused edits in one 72-run battery (2026-10-04). A file now
9
+ * counts as read when a line that runs a reader names it and the output holds
10
+ * the file's entire current content. That one check covers every way the
11
+ * model could have seen less: a pipe into `head`, `cat -n`, a slice, an output
12
+ * persisted or clipped for size, a write later in the same command. The
13
+ * stale-edit guard is unchanged, since the file is observed with that content.
14
+ */
15
+
16
+ import { isAbsolute } from "node:path";
17
+ import { toAbsoluteBash } from "../auto-mode/paths.ts";
18
+ import { globComponentRegex, isUnknownTilde, movesDirectory, resolvePayload } from "../auto-mode/shell-analysis.ts";
19
+ import { LOOPS, parseCommand, type ParseResult, scopedTracker, type Segment, type Token } from "../auto-mode/shell-parse.ts";
20
+
21
+ /** Commands whose file arguments may print a whole file. */
22
+ const READERS = new Set(["cat", "head", "tail", "sed"]);
23
+
24
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
25
+
26
+ /** A word a reader may have printed, with the absolute directory the shell resolves it in. */
27
+ export interface ShellReadCandidate {
28
+ word: string;
29
+ dir: string;
30
+ }
31
+
32
+ /**
33
+ * The files `command` may have printed, as written: the plain arguments of
34
+ * its reader commands, plus every path-like word of a line that runs a reader
35
+ * at all. Candidates only, each still to be checked with {@link shownInFull}.
36
+ * Each comes with the directory its reader runs in, starting from `cwd` and
37
+ * following a literal `cd` ({@link segmentDirectories}); a reader whose
38
+ * directory cannot be known offers nothing. Empty when no reader runs or the
39
+ * command does not parse (the grammar not loaded included).
40
+ */
41
+ export function shellReadCandidates(command: string, cwd: string, home: string): ShellReadCandidate[] {
42
+ const parsed = parseCommand(command);
43
+ if (parsed.parseFailed) return [];
44
+ const readers = parsed.segments.flatMap((segment, index) => {
45
+ const first = segment.tokens.findIndex((token) => !ASSIGNMENT.test(token.value));
46
+ if (first === -1) return [];
47
+ const [head, ...args] = segment.tokens.slice(first);
48
+ return head && !head.dynamic && READERS.has(head.value.split("/").pop() ?? "") ? [{ segment, index, args }] : [];
49
+ });
50
+ // Most commands run no reader: skip the directory walk for them.
51
+ if (readers.length === 0) return [];
52
+ const dirs = segmentDirectories(parsed, cwd, home);
53
+ const candidates = new Map<string, ShellReadCandidate>();
54
+ const add = (word: string, dir: string) => candidates.set(`${dir}\0${word}`, { word, dir });
55
+ const readerDirs = new Set<string>();
56
+ for (const { segment, index, args } of readers) {
57
+ const dir = dirs[index];
58
+ if (dir === undefined) continue;
59
+ readerDirs.add(dir);
60
+ if (segment.substitution !== undefined) continue;
61
+ for (const arg of args) if (isPlainPath(arg)) add(arg.value, dir);
62
+ }
63
+ // A reader fed from a variable (`for f in a.py b.py; do cat "$f"; done`)
64
+ // names its files elsewhere in the line. Every plain path-like word is a
65
+ // candidate then, in each directory a reader ran in: the full-content
66
+ // check, not the parse, decides.
67
+ if (readerDirs.size > 0) {
68
+ const words = new Set<string>();
69
+ for (const word of [...(command.match(PATH_WORD) ?? []), ...(command.match(GLOB_WORD) ?? [])]) if (words.size < MAX_CANDIDATES) words.add(word);
70
+ for (const dir of [...readerDirs].slice(0, MAX_READER_DIRS)) for (const word of words) add(word, dir);
71
+ }
72
+ return [...candidates.values()];
73
+ }
74
+
75
+ /**
76
+ * The absolute paths `command` may have printed: its candidates
77
+ * ({@link shellReadCandidates}) resolved as the shell resolves them, from
78
+ * the directory each one's reader runs in, with a glob expanded through
79
+ * `readdir` (given an absolute directory). The shell's own resolution
80
+ * (`toAbsoluteBash`), not the file tools' (`resolveToolPath` strips a leading
81
+ * `@`, which `cat @x` keeps): `~/` and the shell's directory only.
82
+ */
83
+ export function shellReadPaths(command: string, cwd: string, home: string, readdir: (dir: string) => string[]): string[] {
84
+ const paths = new Set<string>();
85
+ for (const { word, dir } of shellReadCandidates(command, cwd, home)) {
86
+ for (const name of expandCandidate(word, (sub) => readdir(toAbsoluteBash(dir, sub, home)))) paths.add(toAbsoluteBash(dir, name, home));
87
+ }
88
+ return [...paths];
89
+ }
90
+
91
+ /**
92
+ * The directory each segment runs in, or undefined where it cannot be known.
93
+ * Only a plain `cd <literal>` (or `cd -- <literal>`) that surely ran moves
94
+ * the commands after it in its own subshell scope (`scopedTracker`). One that
95
+ * may not run moves only the later members of its own `&&` list
96
+ * (`Segment.andList`), which run only once it succeeded (`cd src && cat x`
97
+ * inside the worktree's `cd <wt> && ( … )`). Anything else that may change
98
+ * the directory makes the commands after it unknown, never guessed: a
99
+ * dynamic, glob, `-` or `~user` target, an option, `pushd` and `popd`, a
100
+ * wrapper, assignment or `!` in front, a `cd` that may not run (an `if`
101
+ * branch, after `||`) for everything outside its `&&` list, a `cd` last in a
102
+ * pipeline (`lastpipe` runs it in the shell around), a backgrounded line,
103
+ * `eval` and `source`, and a line that defines a function (which may replace
104
+ * `cd`). A loop body runs more than once, so after a move in any loop every
105
+ * command in a loop is unknown too. A miss costs one re-read; a wrong
106
+ * directory would certify a read that never happened.
107
+ */
108
+ function segmentDirectories(parsed: ParseResult, cwd: string, home: string): (string | undefined)[] {
109
+ const { segments } = parsed;
110
+ const dirs = scopedTracker<string | undefined>(cwd);
111
+ /** Where a `cd` that may not have run moved the later members of its `&&` list, by list. */
112
+ const listDirs = new Map<number, string | undefined>();
113
+ const inLoop = (segment: Segment) => segment.enclosing.some((construct) => LOOPS.has(construct));
114
+ const moves = segments.map(mayMove);
115
+ const loopMoves = segments.some((segment, index) => moves[index] && inLoop(segment));
116
+ return segments.map((segment, index) => {
117
+ const inList = segment.andList !== undefined && listDirs.has(segment.andList);
118
+ const dir = loopMoves && inLoop(segment) ? undefined : inList ? listDirs.get(segment.andList!) : dirs.get(segment);
119
+ if (!moves[index]) return dir;
120
+ const certain = !parsed.background && !parsed.definesFunction && !segment.pipelineShell && !segment.enclosing.includes("negated_command");
121
+ const target = certain ? literalCdTarget(segment) : undefined;
122
+ // A relative target from an unknown directory stays unknown; an absolute one re-anchors.
123
+ const next = target === undefined ? undefined : dir === undefined && !isAbsoluteTarget(target) ? undefined : toAbsoluteBash(dir ?? cwd, target, home);
124
+ if (segment.pipelineShell) dirs.set({ scopes: segment.pipelineShell }, undefined);
125
+ if (segment.conditional) {
126
+ dirs.set(segment, undefined);
127
+ if (segment.andList !== undefined) listDirs.set(segment.andList, next);
128
+ } else {
129
+ dirs.set(segment, next);
130
+ }
131
+ return dir;
132
+ });
133
+ }
134
+
135
+ /** Whether a segment may change the shell's directory: a `cd`/`pushd`/`popd` in any spelling, or a script run in this shell. */
136
+ function mayMove(segment: Segment): boolean {
137
+ return movesDirectory(segment) || ["eval", "source", "."].includes(resolvePayload(segment.tokens).command);
138
+ }
139
+
140
+ /** The target of `cd <word>` or `cd -- <word>` written plainly, or undefined for any other form. */
141
+ function literalCdTarget(segment: Segment): string | undefined {
142
+ const [head, ...args] = segment.tokens;
143
+ if (head?.value !== "cd" || head.dynamic) return undefined;
144
+ const rest = args[0]?.value === "--" ? args.slice(1) : args;
145
+ if (rest.length > 1) return undefined;
146
+ const target = rest[0];
147
+ // A bare `cd` goes home.
148
+ if (!target) return "~";
149
+ if (target.dynamic || target.glob || target.value === "" || target.value.startsWith("-") || isUnknownTilde(target.value)) return undefined;
150
+ return target.value;
151
+ }
152
+
153
+ /** Whether a `cd` target lands in the same place from any directory. */
154
+ function isAbsoluteTarget(target: string): boolean {
155
+ return isAbsolute(target) || target === "~" || target.startsWith("~/");
156
+ }
157
+
158
+ /**
159
+ * The files a candidate names: itself, or for a glob in its last component
160
+ * (`src/*.py`, a loop's word list), the matching entries of that directory,
161
+ * listed through `readdir` (no shell). Globs in a directory component are not
162
+ * expanded.
163
+ */
164
+ export function expandCandidate(word: string, readdir: (dir: string) => string[]): string[] {
165
+ if (!/[*?[]/.test(word)) return [word];
166
+ const slash = word.lastIndexOf("/");
167
+ const dir = slash === -1 ? "" : word.slice(0, slash);
168
+ if (/[*?[]/.test(dir)) return [];
169
+ const pattern = globComponentRegex(word.slice(slash + 1));
170
+ if (!pattern) return [];
171
+ let names: string[];
172
+ try {
173
+ names = readdir(dir || ".");
174
+ } catch {
175
+ return [];
176
+ }
177
+ return names.filter((name) => !name.startsWith(".") && pattern.test(name)).map((name) => (dir ? `${dir}/${name}` : name)).slice(0, MAX_CANDIDATES);
178
+ }
179
+
180
+ /** A word that could be a file path as written, absolute or relative: no quoting, expansion or glob characters. */
181
+ const PATH_WORD = /(?<![\w$\-/])\/?[\w.@+~][\w.@+~/,-]*/g;
182
+ /** A word with a glob character, which the shell would expand (`src/*.py`). */
183
+ const GLOB_WORD = /(?<![\w$\-])[\w.@+~/,-]*[*?[][\w.@+~/,*?[\]-]*/g;
184
+ const MAX_CANDIDATES = 50;
185
+ /** The path-like words of a line are tried in at most this many reader directories. */
186
+ const MAX_READER_DIRS = 4;
187
+
188
+ function isPlainPath(token: Token): boolean {
189
+ return !token.dynamic && !token.glob && token.value !== "" && !token.value.startsWith("-");
190
+ }
191
+
192
+ /**
193
+ * Whether `output` shows the whole of `content`. Trailing whitespace is
194
+ * ignored because the shell tool trims it. A tiny one-line file is never
195
+ * counted: its text turns up in outputs by chance (an adversarial GPT-6 Astra
196
+ * run matched `completed` to "(bash completed with no output)", `1` to
197
+ * "Exit code 1", an error line, a persisted-output tag), so the match would
198
+ * not show that the file was printed. What the guard protects still holds for
199
+ * a larger match under another file's name: every byte of the file was in
200
+ * front of the model, so an edit cannot discard content it has not seen.
201
+ */
202
+ export function shownInFull(output: string, content: string): boolean {
203
+ const body = content.trimEnd();
204
+ if (body.length < MIN_SHOWN_CHARS && !(body.includes("\n") && body.length >= MIN_SHOWN_LINES_CHARS)) return false;
205
+ return output.includes(body);
206
+ }
207
+
208
+ /** A one-line file shorter than this is never counted as shown. */
209
+ const MIN_SHOWN_CHARS = 40;
210
+ /** A multi-line file shorter than this is never counted as shown. */
211
+ const MIN_SHOWN_LINES_CHARS = 16;
@@ -13,8 +13,15 @@
13
13
  export type FreshnessStatus = "fresh" | "unread" | "stale" | "absent";
14
14
 
15
15
  export interface TrackedFile {
16
- /** Content as of the last read or write we performed. */
16
+ /** Content as of the last read or write we performed ("" when `oversized`). */
17
17
  content: string;
18
+ /**
19
+ * Too large to keep: we saw the file but hold no content to compare. An
20
+ * explicit flag, never the empty string, so a file read while EMPTY
21
+ * compares like any other and cannot be overwritten after someone fills
22
+ * it (TOOLS-REVIEW-2026-09-26 M2).
23
+ */
24
+ oversized: boolean;
18
25
  at: number;
19
26
  }
20
27
 
@@ -54,9 +61,9 @@ export class FileTracker {
54
61
  this.notified.delete(path);
55
62
  if (content.length > MAX_TRACKED_BYTES) {
56
63
  // Too large to diff usefully; remember that we saw it, not its content.
57
- this.files.set(path, { content: "", at });
64
+ this.files.set(path, { content: "", oversized: true, at });
58
65
  } else {
59
- this.files.set(path, { content, at });
66
+ this.files.set(path, { content, oversized: false, at });
60
67
  }
61
68
  if (stamp) this.stamps.set(path, stamp);
62
69
  else this.stamps.delete(path);
@@ -103,8 +110,10 @@ export class FileTracker {
103
110
  return this.files.has(path);
104
111
  }
105
112
 
113
+ /** The content last seen, or undefined when untracked or tracked without content (oversized). */
106
114
  lastSeen(path: string): string | undefined {
107
- return this.files.get(path)?.content;
115
+ const known = this.files.get(path);
116
+ return known && !known.oversized ? known.content : undefined;
108
117
  }
109
118
 
110
119
  get tracked(): string[] {
@@ -123,7 +132,7 @@ export class FileTracker {
123
132
  if (!known) return "unread";
124
133
  // Oversized files are tracked without content; treat them as fresh rather
125
134
  // than blocking edits we cannot reason about.
126
- if (known.content === "" && currentContent !== "") return "fresh";
135
+ if (known.oversized) return "fresh";
127
136
  return known.content === currentContent ? "fresh" : "stale";
128
137
  }
129
138
  }
@@ -173,20 +182,28 @@ export function describeChanges(
173
182
  }
174
183
 
175
184
  export const UNREAD_REASON = (path: string, tool: string) =>
176
- `Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen.`;
185
+ `Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen. A shell read counts only when it printed the whole file as it is on disk (a plain \`cat\`); after a partial one (head, sed -n, a pipe, or output cut off at the size limit) or another version of it (such as \`git show HEAD:<file>\`), use the read tool.`;
186
+
187
+ /**
188
+ * The same notice when one of this session's subagents or workflow agents
189
+ * changed the file (lib/child-writes.ts): saying "someone else, a formatter or
190
+ * a command" there sent the model looking for an outside cause.
191
+ */
192
+ export const CHILD_CHANGE_REMINDER = (path: string, agent: string | undefined, excerpt: ChangeExcerpt) =>
193
+ `Note: ${path} was changed by ${agent ? `the agent "${agent}"` : "an agent"} you started in this session since you last read it. Take it as the current state. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
177
194
 
178
195
  export const STALE_REASON = (path: string) =>
179
196
  `${path} has changed on disk since you last read it — someone else, a formatter, or a command may have modified it. Read it again before editing, or your change would overwrite theirs.`;
180
197
 
181
198
  /**
182
- * Claude Code's `edited_text_file` attachment text, with two additions: the
183
- * cause list also names a command (our tracker sees bash writes too), and the
184
- * closing clause tells the model to re-read before editing — CC marks the file
185
- * as read when it attaches the snippet, we deliberately do not (the stale-edit
186
- * guard keeps firing), so without that clause the model's next edit is blocked
187
- * and costs a round trip. The "intentional / don't revert / don't tell the
188
- * user" steer is CC's: without it a model mid-edit tends to restore its own
189
- * version and narrate linter noise (STEERING-REVIEW-2026-09-05 M5).
199
+ * Claude Code's `edited_text_file` attachment text, plus One Code's closing
200
+ * clause telling the model to re-read before editing: Claude Code marks the
201
+ * file as read when it attaches the snippet, we deliberately do not (the
202
+ * stale-edit guard keeps firing), so without that clause the model's next
203
+ * edit is blocked and costs a round trip. The "take it as the current state
204
+ * rather than reverting it" steer is Claude Code's: without it a model
205
+ * mid-edit tends to restore its own version and narrate linter noise
206
+ * (STEERING-REVIEW-2026-09-05 M5).
190
207
  */
191
208
  export const EXTERNAL_CHANGE_REMINDER = (path: string, excerpt: ChangeExcerpt) =>
192
- `Note: ${path} was modified, either by the user, a linter, or a command, after you last read it. This change was intentional, so make sure to take it into account as you proceed (ie. don't revert it unless the user asks you to). Don't tell the user this, since they are already aware. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
209
+ `Note: ${path} changed on disk since you last read it. That's usually deliberate, so take it as the current state rather than reverting it; if the change looks wrong, say so rather than undoing it yourself — otherwise no need to call it out. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
@@ -7,10 +7,13 @@
7
7
  * Everything here is pure and paint-injected so it unit-tests with an identity
8
8
  * paint. `paint(color, text)` matches `safeThemePaint`; in tests it returns the
9
9
  * text unchanged, so assertions see the plain line. Inclusion decisions use
10
- * plain lengths only, so the painted result (ANSI adds zero display width)
11
- * always fits the width it was built for.
10
+ * the plain text's terminal columns (`visibleWidth`, never `.length`, so a CJK
11
+ * path or branch counts two columns a glyph), so the painted result (ANSI adds
12
+ * zero display width) always fits the width it was built for.
12
13
  */
13
14
 
15
+ import { cutPlainText } from "../lib/tui-render.ts";
16
+ import { tailColumns, visibleWidth } from "../lib/text-width.ts";
14
17
  import { costOf, usageEntryCost } from "../lib/usage-bus.ts";
15
18
  import type { WorktreeLocation } from "../lib/worktree-channel.ts";
16
19
 
@@ -21,8 +24,12 @@ export interface FooterData {
21
24
  cwd: string;
22
25
  home: string;
23
26
  branch?: string;
24
- /** Context tokens in use and the model's window; both needed to show fill. */
25
- contextTokens?: number;
27
+ /**
28
+ * Context tokens in use and the model's window. Tokens are unknown right
29
+ * after a compaction until the next reply; the fill then reads `?/window`,
30
+ * as pi's own footer shows it.
31
+ */
32
+ contextTokens?: number | null;
26
33
  contextWindow?: number;
27
34
  /** Context percentage, shown in parens after the fill; `null`/undefined omits it
28
35
  * (e.g. right after compaction, unknown until the next reply). */
@@ -73,12 +80,13 @@ function tilde(cwd: string, home: string): string {
73
80
 
74
81
  /**
75
82
  * Fit a path into `max` columns, keeping the tail (the deepest, most specific
76
- * folders) with a leading `…`. `max <= 1` collapses to just `…`.
83
+ * folders) with a leading `…`. `max <= 1` collapses to just `…`. Cut by
84
+ * columns on a grapheme boundary, so a wide glyph or an emoji stays whole.
77
85
  */
78
86
  function truncatePath(path: string, max: number): string {
79
- if (path.length <= max) return path;
87
+ if (visibleWidth(path) <= max) return path;
80
88
  if (max <= 1) return "…";
81
- return `…${path.slice(path.length - (max - 1))}`;
89
+ return `…${tailColumns(path, max - 1).text}`;
82
90
  }
83
91
 
84
92
  interface Part {
@@ -86,6 +94,8 @@ interface Part {
86
94
  painted: string;
87
95
  /** Higher drops first when the line is too narrow; 0 = never dropped. */
88
96
  dropPriority: number;
97
+ /** The model name: the one core part cut (with an ellipsis) when even the core overflows. */
98
+ model: boolean;
89
99
  }
90
100
 
91
101
  const SEP = " · ";
@@ -103,17 +113,19 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
103
113
  // A labelled metric: the label is muted, the value takes `color`. The plain
104
114
  // form (label + value) drives the width/drop math; the painted form colours
105
115
  // the two halves separately.
106
- const push = (label: string, value: string, color: string, dropPriority = 0) => {
116
+ const push = (label: string, value: string, color: string, dropPriority = 0, model = false) => {
107
117
  if (!value) return;
108
118
  const plain = label ? `${label} ${value}` : value;
109
119
  const painted = label ? `${paint("muted", label)} ${paint(color, value)}` : paint(color, value);
110
- parts.push({ plain, painted, dropPriority });
120
+ parts.push({ plain, painted, dropPriority, model });
111
121
  };
112
122
 
113
- // Context fill as "usage: tokens/window (pct%)".
114
- if (typeof data.contextTokens === "number" && typeof data.contextWindow === "number" && data.contextWindow > 0) {
115
- const pct = typeof data.contextPercent === "number" ? ` (${data.contextPercent.toFixed(0)}%)` : "";
116
- push("usage:", `${formatTokens(data.contextTokens)}/${formatTokens(data.contextWindow)}${pct}`, "dim");
123
+ // Context fill as "usage: tokens/window (pct%)", or "?/window" while the
124
+ // tokens are unknown (right after a compaction).
125
+ if (typeof data.contextWindow === "number" && data.contextWindow > 0) {
126
+ const tokens = typeof data.contextTokens === "number" ? formatTokens(data.contextTokens) : "?";
127
+ const pct = typeof data.contextTokens === "number" && typeof data.contextPercent === "number" ? ` (${data.contextPercent.toFixed(0)}%)` : "";
128
+ push("usage:", `${tokens}/${formatTokens(data.contextWindow)}${pct}`, "dim");
117
129
  }
118
130
  // Total cost, always shown once known.
119
131
  push("cost:", formatCost(data.cost), "dim");
@@ -123,7 +135,7 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
123
135
  push("cache-hit:", `${data.cacheHitPercent.toFixed(0)}%`, color, 2);
124
136
  }
125
137
  // Model, then effort — no labels; both self-describing.
126
- if (data.model) push("", data.model, "dim");
138
+ if (data.model) push("", data.model, "dim", 0, true);
127
139
  if (data.effort) push("", data.effort, "accent");
128
140
 
129
141
  // Left chunk: path + branch, with the open PR attached to the branch as
@@ -132,7 +144,7 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
132
144
  // branch (bare-branch fallback), and only when there is no room even for the
133
145
  // bare branch does the whole chunk go, leaving metrics alone. The three
134
146
  // segments paint separately — path dim, branch in accent, PR in the link
135
- // colour — but every width/gap decision uses their plain lengths.
147
+ // colour — but every width/gap decision uses their plain columns.
136
148
  const path = tilde(data.cwd, data.home);
137
149
  const branchSeg = data.branch ? ` ⎇ ${data.branch}` : "";
138
150
  const prSeg = data.branch && typeof data.pr === "number" ? ` ← PR #${data.pr}` : "";
@@ -140,7 +152,7 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
140
152
  // Drop optional right parts (highest priority first) until the metrics fit.
141
153
  let kept = parts.slice();
142
154
  const rightPlain = () => kept.map((p) => p.plain).join(SEP);
143
- while (rightPlain().length > width) {
155
+ while (visibleWidth(rightPlain()) > width) {
144
156
  const droppable = kept
145
157
  .map((p, i) => ({ p, i }))
146
158
  .filter((x) => x.p.dropPriority > 0)
@@ -149,25 +161,34 @@ export function buildFooterLines(data: FooterData, width: number, paint: Paint):
149
161
  kept = kept.filter((_, i) => i !== droppable.i);
150
162
  }
151
163
 
152
- const rightLen = rightPlain().length;
153
-
154
- // Even the core overflows: last resort, truncate the plain metrics to width.
155
- if (rightLen > width) {
156
- return [paint("dim", rightPlain().slice(0, width))];
164
+ // Even the core overflows: cut the model name with an ellipsis so the
165
+ // effort after it stays; when that is not enough, cut the whole line.
166
+ const overflow = visibleWidth(rightPlain()) - width;
167
+ if (overflow > 0) {
168
+ const model = kept.find((p) => p.model);
169
+ const room = model ? visibleWidth(model.plain) - overflow : 0;
170
+ if (model && room >= 4) {
171
+ const shortened = cutPlainText(model.plain, room);
172
+ kept = kept.map((p) => (p === model ? { ...p, plain: shortened, painted: paint("dim", shortened) } : p));
173
+ } else {
174
+ return [paint("dim", cutPlainText(rightPlain(), width))];
175
+ }
157
176
  }
158
177
 
178
+ const rightLen = visibleWidth(rightPlain());
179
+
159
180
  const rightPaintedStr = kept.map((p) => p.painted).join(paint("dim", SEP));
160
181
 
161
182
  // Room for the left chunk after the metrics and at least one space of gap.
162
183
  // Keep the PR only if the branch+PR both fit; otherwise drop it and keep the
163
184
  // bare branch.
164
185
  const room = width - rightLen - 1;
165
- const keptPr = room >= branchSeg.length + prSeg.length + 1 ? prSeg : "";
166
- const suffixLen = branchSeg.length + keptPr.length;
186
+ const keptPr = room >= visibleWidth(branchSeg) + visibleWidth(prSeg) + 1 ? prSeg : "";
187
+ const suffixLen = visibleWidth(branchSeg) + visibleWidth(keptPr);
167
188
  if (room >= suffixLen + 1) {
168
189
  const shownPath = truncatePath(path, room - suffixLen);
169
190
  const leftPlain = `${shownPath}${branchSeg}${keptPr}`;
170
- const gap = " ".repeat(width - leftPlain.length - rightLen);
191
+ const gap = " ".repeat(Math.max(0, width - visibleWidth(leftPlain) - rightLen));
171
192
  const leftPainted = `${paint("dim", shownPath)}${paint("accent", branchSeg)}${paint("mdLink", keptPr)}`;
172
193
  return [`${leftPainted}${gap}${rightPaintedStr}`];
173
194
  }
@@ -24,7 +24,7 @@ import { ULTRACODE_STATUS_KEY } from "../effort/slider.ts";
24
24
  import { USAGE_CHANNEL } from "../lib/usage-bus.ts";
25
25
  import { buildFooterLines, computeMainUsage, footerLocation, type FooterData } from "./footer-line.ts";
26
26
  import { WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
27
- import { fetchPrNumber } from "./pr.ts";
27
+ import { fetchPrNumber, nextPrPollDelay, PR_SLOW_LOOKUP_MS } from "./pr.ts";
28
28
 
29
29
  export default function footerExtension(pi: ExtensionAPI) {
30
30
  if (process.env.CC_FOOTER === "0") return;
@@ -38,6 +38,27 @@ export default function footerExtension(pi: ExtensionAPI) {
38
38
  let pr: number | undefined;
39
39
  /** Bumped per branch change so a slow gh lookup for an old branch is ignored. */
40
40
  let prToken = 0;
41
+ /**
42
+ * The PR is looked up again once a minute while the session is in use, so a
43
+ * PR merged or closed mid-session disappears (pr.ts nextPrPollDelay). A slow
44
+ * or missing `gh` turns polling off; branch changes still look up.
45
+ */
46
+ const prPoll = { lastLookupAt: 0, lastInputAt: Date.now(), disabled: false };
47
+ let prPollTimer: ReturnType<typeof setTimeout> | undefined;
48
+ const stopPrPoll = () => {
49
+ clearTimeout(prPollTimer);
50
+ prPollTimer = undefined;
51
+ };
52
+ const schedulePrPoll = () => {
53
+ stopPrPoll();
54
+ const delay = nextPrPollDelay(prPoll, Date.now());
55
+ if (delay === undefined) return;
56
+ prPollTimer = setTimeout(() => {
57
+ prPollTimer = undefined;
58
+ refreshLocation();
59
+ }, delay);
60
+ prPollTimer.unref?.();
61
+ };
41
62
 
42
63
  /** Invalidate the memoized line and repaint; set once the footer mounts. */
43
64
  let repaint = () => {};
@@ -71,20 +92,43 @@ export default function footerExtension(pi: ExtensionAPI) {
71
92
 
72
93
  const refreshPr = (cwd: string, branch: string | null) => {
73
94
  const token = ++prToken;
95
+ stopPrPoll();
74
96
  if (!branch) {
75
97
  pr = undefined;
76
98
  repaint();
77
99
  return;
78
100
  }
101
+ const startedAt = Date.now();
102
+ prPoll.lastLookupAt = startedAt;
79
103
  void fetchPrNumber(cwd, branch).then((found) => {
80
104
  if (token !== prToken) return; // a newer branch superseded this lookup
81
- pr = found;
105
+ if (found.ghMissing || Date.now() - startedAt > PR_SLOW_LOOKUP_MS) prPoll.disabled = true;
106
+ pr = found.pr;
82
107
  repaint();
108
+ schedulePrPoll();
83
109
  });
84
110
  };
85
111
 
112
+ // Input keeps polling alive; after an hour without any, the next input
113
+ // resumes it. A turn boundary asks again, never sooner than a minute after
114
+ // the last lookup.
115
+ pi.on("input", () => {
116
+ prPoll.lastInputAt = Date.now();
117
+ if (!prPollTimer) schedulePrPoll();
118
+ });
119
+ pi.on("agent_settled", () => {
120
+ if (!prPollTimer) schedulePrPoll();
121
+ });
122
+ pi.on("session_shutdown", () => {
123
+ stopPrPoll();
124
+ prToken++;
125
+ });
126
+
86
127
  pi.on("session_start", (_event, ctx) => {
87
128
  recomputeMain(ctx);
129
+ // A slow or missing gh turns polling off for one session, not the process.
130
+ prPoll.disabled = false;
131
+ prPoll.lastInputAt = Date.now();
88
132
  if (!ctx.hasUI) return;
89
133
 
90
134
  ctx.ui.setFooter((tui: unknown, theme: unknown, footerData: unknown) => {
@@ -144,6 +188,7 @@ export default function footerExtension(pi: ExtensionAPI) {
144
188
  return Object.assign(component, {
145
189
  dispose: () => {
146
190
  stopFollowingBranch();
191
+ stopPrPoll();
147
192
  repaint = () => {};
148
193
  refreshLocation = () => {};
149
194
  },