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
@@ -6,18 +6,54 @@
6
6
  *
7
7
  * Every rendered line must stay within the width given: pi-tui crashes the
8
8
  * whole app on an overwide line. Content is wrapped (not truncated — a plan
9
- * must stay readable in full), and chrome lines are cut plain-text *before*
10
- * painting so ANSI escapes never confuse the width accounting.
9
+ * must stay readable in full) and then cut ANSI-aware as a guard, and chrome
10
+ * lines are cut plain-text *before* painting so ANSI escapes never confuse
11
+ * the width accounting.
11
12
  */
12
13
 
13
- import { cutPlainText } from "../lib/tui-render.ts";
14
+ import { cutPlainText, hintLines, truncateLine, wrapProse } from "../lib/tui-render.ts";
15
+ import { keyId } from "../lib/key-input.ts";
14
16
 
15
- /** Hard-wrap plain text to `width` columns, preserving blank lines. */
16
- export { wrapPlainText as wrapPlanText } from "../lib/tui-render.ts";
17
+ /** Renders Markdown to painted lines at a width (pi-tui's Markdown, which the transcript uses). */
18
+ export type PlanMarkdown = (text: string, width: number) => string[];
19
+
20
+ /**
21
+ * The plan's lines at `width`: Markdown rendered the way the transcript renders
22
+ * assistant text when a renderer is available, else plain text wrapped at word
23
+ * boundaries. A renderer that throws falls back to the plain wrap, so a
24
+ * Markdown failure never costs the user the plan.
25
+ */
26
+ export function planLines(plan: string, width: number, markdown?: PlanMarkdown): string[] {
27
+ if (markdown) {
28
+ try {
29
+ return markdown(plan, width);
30
+ } catch {
31
+ // fall through to the plain wrap
32
+ }
33
+ }
34
+ return wrapProse(plan, width);
35
+ }
17
36
 
18
37
  /** What the dialog resolves to; index into the choices list, or null for cancel. */
19
38
  export type PlanChoice = number;
20
39
 
40
+ /**
41
+ * The approval through a plain select, for RPC: its `custom()` resolves
42
+ * undefined (findings §8), which read as "dismissed" and rejected every plan.
43
+ * The plan rides in the title; a reply that is no choice is a dismissal.
44
+ */
45
+ export async function selectPlanChoice(
46
+ ui: { select(title: string, options: string[], opts?: { signal?: AbortSignal }): Promise<string | undefined> },
47
+ plan: string,
48
+ path: string,
49
+ choices: string[],
50
+ signal?: AbortSignal,
51
+ ): Promise<PlanChoice | null> {
52
+ const label = await ui.select(`Plan (${path}):\n\n${plan}\n\nApprove the plan?`, choices, { signal });
53
+ const index = signal?.aborted || label === undefined ? -1 : choices.indexOf(label);
54
+ return index >= 0 ? index : null;
55
+ }
56
+
21
57
  export type ViewerKey =
22
58
  | { kind: "scroll"; delta: number }
23
59
  | { kind: "choice"; delta: number }
@@ -32,33 +68,29 @@ export type ViewerKey =
32
68
  * classifier model is reachable).
33
69
  */
34
70
  export function decodeViewerKey(data: string, page: number): ViewerKey | undefined {
35
- switch (data) {
36
- case "\x1b[A":
37
- case "\x1bOA":
71
+ const id = keyId(data);
72
+ switch (id) {
73
+ case "up":
38
74
  return { kind: "scroll", delta: -1 };
39
- case "\x1b[B":
40
- case "\x1bOB":
75
+ case "down":
41
76
  return { kind: "scroll", delta: 1 };
42
- case "\x1b[5~":
77
+ case "pageUp":
43
78
  return { kind: "scroll", delta: -page };
44
- case "\x1b[6~":
79
+ case "pageDown":
45
80
  return { kind: "scroll", delta: page };
46
- case "\x1b[D":
47
- case "\x1bOD":
81
+ case "left":
48
82
  return { kind: "choice", delta: -1 };
49
- case "\x1b[C":
50
- case "\x1bOC":
51
- case "\t":
83
+ case "right":
84
+ case "tab":
52
85
  return { kind: "choice", delta: 1 };
53
- case "\r":
54
- case "\n":
86
+ case "enter":
55
87
  return { kind: "confirm" };
56
- case "\x1b":
57
- case "\x03": // ctrl+c — same intent as escape while the dialog is focused
88
+ case "escape":
89
+ case "ctrl+c": // same intent as escape while the dialog is focused
58
90
  return { kind: "cancel" };
59
91
  default:
60
- if (data.length === 1 && data >= "1" && data <= "9") {
61
- return { kind: "pick", index: Number(data) - 1 };
92
+ if (id && id.length === 1 && id >= "1" && id <= "9") {
93
+ return { kind: "pick", index: Number(id) - 1 };
62
94
  }
63
95
  return undefined;
64
96
  }
@@ -90,7 +122,7 @@ export function initialPlanChoice(options: readonly { mode?: string; restore?: b
90
122
  export type Paint = (color: string, text: string) => string;
91
123
 
92
124
  export interface PlanViewerView {
93
- /** Pre-wrapped plain-text plan lines (wrapPlanText output for this width). */
125
+ /** The plan's lines for this width (planLines output: painted Markdown or plain wrapped text). */
94
126
  lines: string[];
95
127
  offset: number;
96
128
  choice: PlanChoice;
@@ -103,18 +135,20 @@ export function renderPlanViewer(view: PlanViewerView, paint: Paint, width: numb
103
135
  const maxVisible = view.maxVisible ?? 12;
104
136
  // Cut plain text before painting so escapes never enter the width math.
105
137
  const cut = (line: string) => cutPlainText(line, width);
106
- const rule = paint("dim", "─".repeat(Math.max(0, Math.min(width, 72))));
138
+ const rule = paint("dim", "─".repeat(Math.max(0, width)));
107
139
  const pickHint = view.choices.length > 1 ? `1-${view.choices.length} pick · ` : "";
108
140
 
109
141
  const out: string[] = [];
110
142
  out.push("");
111
143
  out.push(paint("accent", cut("Approve this plan?")));
112
- out.push(paint("dim", cut(`↑/↓ scroll · ←/→ switch choice · ${pickHint}enter confirm · esc keep planning`)));
144
+ // Each hint sits beside what its keys move: scrolling above the plan, choosing under the choices.
145
+ out.push(paint("dim", cut("↑/↓ scroll the plan")));
113
146
  out.push(rule);
114
147
 
148
+ // Plan lines may arrive painted (Markdown), so the guard cut is ANSI-aware.
115
149
  const offset = clampOffset(view.offset, view.lines.length, maxVisible);
116
150
  for (const line of view.lines.slice(offset, offset + maxVisible)) {
117
- out.push(cut(line));
151
+ out.push(truncateLine(line, width));
118
152
  }
119
153
  if (view.lines.length > maxVisible) {
120
154
  const last = Math.min(view.lines.length, offset + maxVisible);
@@ -128,6 +162,37 @@ export function renderPlanViewer(view: PlanViewerView, paint: Paint, width: numb
128
162
  index === view.choice ? paint("accent", cut(`❯ ${numbered}`)) : cut(` ${numbered}`),
129
163
  );
130
164
  }
165
+ // The keys wrap at their separators on a narrow terminal, so "esc cancel" is never cut.
166
+ out.push(...hintLines(`←/→ choose · ${pickHint}enter confirm · esc cancel`, width).map((line) => paint("dim", line)));
131
167
  out.push("");
132
168
  return out;
133
169
  }
170
+
171
+ /** The question the keep-planning choice asks (Claude Code's placeholder, naming the agent). */
172
+ export const KEEP_PLANNING_QUESTION = "Tell the agent what to change";
173
+
174
+ /** How the approval ended: an approving choice, or a rejection carrying the user's words when they gave some. */
175
+ export type PlanDecision = { kind: "approve"; index: PlanChoice } | { kind: "reject"; feedback?: string };
176
+
177
+ /**
178
+ * The approval flow, as in Claude Code: keeping planning asks what to change,
179
+ * and those words go back to the model, which keeps planning. A blank answer
180
+ * returns to the dialog (Claude Code ignores Enter on an empty "keep planning"
181
+ * input). Esc on the dialog or on the question rejects without words, which
182
+ * stops the turn. The option without a mode is the keep-planning one.
183
+ */
184
+ export async function runPlanApproval(
185
+ options: readonly { mode?: string }[],
186
+ showDialog: () => Promise<PlanChoice | null>,
187
+ askFeedback: () => Promise<string | undefined>,
188
+ signal?: AbortSignal,
189
+ ): Promise<PlanDecision> {
190
+ for (;;) {
191
+ const choice = await showDialog();
192
+ if (choice == null || signal?.aborted) return { kind: "reject" };
193
+ if (options[choice]?.mode) return { kind: "approve", index: choice };
194
+ const feedback = await askFeedback();
195
+ if (feedback === undefined || signal?.aborted) return { kind: "reject" };
196
+ if (feedback.trim()) return { kind: "reject", feedback: feedback.trim() };
197
+ }
198
+ }
@@ -48,9 +48,9 @@ export async function fetchInstallCounts(root: string, now: Date = new Date()):
48
48
  if (cached) return cached;
49
49
 
50
50
  try {
51
- const response = await fetchWithTimeout(INSTALL_COUNTS_URL, FETCH_TIMEOUT_MS);
52
- if (!response.ok) return undefined;
53
- const counts = (await response.json()) as CountsCache["counts"];
51
+ const counts = await fetchWithTimeout(INSTALL_COUNTS_URL, FETCH_TIMEOUT_MS, async (response) =>
52
+ response.ok ? ((await response.json()) as CountsCache["counts"]) : undefined,
53
+ );
54
54
  if (!Array.isArray(counts)) return undefined;
55
55
  writeJsonAtomic(join(root, CACHE_FILE), { version: 1, fetchedAt: now.toISOString(), counts } satisfies CountsCache);
56
56
  return toMap(counts);
@@ -16,14 +16,16 @@
16
16
  * ~/.claude is read-only here, always.
17
17
  */
18
18
 
19
- import { execFile } from "node:child_process";
20
19
  import { readFileSync, rmSync } from "node:fs";
21
- import { join } from "node:path";
22
- import { promisify } from "node:util";
20
+ import { dirname, join } from "node:path";
23
21
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
24
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
25
23
  import { readFavorites, toggleFavorite } from "../lib/favorites.ts";
24
+ import { announceArgumentHint, type CommandHint, frontmatterCommandHint } from "../lib/argument-hints.ts";
26
25
  import { MCP_STATUS_CHANNEL, MCP_STATUS_REQUEST_CHANNEL, type McpStatusEvent } from "../lib/mcp-status.ts";
26
+ import { canShowCustomUi, notifyOrPrint, notifyRpcReadOnly } from "../lib/headless-output.ts";
27
+ import { createUserMessageSender, sessionOutlivesTurn } from "../lib/notifications.ts";
28
+ import { ONE_SHOT_COMMAND_FAILED_CHANNEL } from "../lib/interrupt.ts";
27
29
  import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
28
30
  import { setOverride } from "../lib/plugin-overrides.ts";
29
31
  import { pathWithinBase, pluginRoot } from "../lib/plugin-root.ts";
@@ -62,13 +64,15 @@ import { decodePanelKey } from "./panel/keys.ts";
62
64
  import { type DiscoverDetail, renderPanel, type PanelPaint } from "./panel/render.ts";
63
65
  import { buildDiscoverRows, buildInstalledRows, buildMarketplaceRows, type DiscoverRow } from "./panel/rows.ts";
64
66
  import { applyPanelKey, initialPanelState, type PanelEffect, type PanelView } from "./panel/state.ts";
65
- import { findShellPlaceholders, replaceShellPlaceholders, substituteArguments } from "./template.ts";
67
+ import { expandTemplate, runPlaceholderCommand } from "./shell-expand.ts";
68
+ import { watchPermissionBridge } from "../permissions/subagent-gate.ts";
69
+ import { followWorkCwd } from "../lib/worktree-channel.ts";
66
70
  import { parseFrontmatterLoosely } from "../lib/frontmatter.ts";
67
71
  import { sessionAlive } from "../lib/session-lifecycle.ts";
68
72
  import { registerLocalCommand } from "../lib/local-command.ts";
73
+ import { promptCommandText } from "../skill/invoke.ts";
74
+ import { panelUi } from "../lib/panel-focus.ts";
69
75
 
70
- const run = promisify(execFile);
71
- const SHELL_TIMEOUT_MS = 30_000;
72
76
 
73
77
  // The panel is a bounded dock like Claude Code's /plugins, NOT a full-screen
74
78
  // takeover: it swaps the editor container and leaves the transcript visible
@@ -76,36 +80,6 @@ const SHELL_TIMEOUT_MS = 30_000;
76
80
  // terminals still shrink to fit. ~6 discover rows show at this cap.
77
81
  const PANEL_MAX_HEIGHT = 22;
78
82
 
79
- async function expandTemplate(body: string, args: string, cwd: string, persist?: { dir: string; name: string }): Promise<string> {
80
- const withArgs = substituteArguments(body, args);
81
- const commands = [...new Set(findShellPlaceholders(withArgs))];
82
- if (commands.length === 0) return withArgs;
83
-
84
- const outputs = new Map<string, string>();
85
- await Promise.all(
86
- commands.map(async (command, index) => {
87
- let text: string;
88
- try {
89
- const { stdout, stderr } = await run(command, {
90
- cwd,
91
- shell: true,
92
- timeout: SHELL_TIMEOUT_MS,
93
- maxBuffer: 2 * 1024 * 1024,
94
- });
95
- text = (stdout || stderr || "").trim();
96
- } catch (error) {
97
- const detail = error as { stdout?: string; stderr?: string; message?: string };
98
- text = (detail.stdout || detail.stderr || detail.message || "command failed").trim();
99
- }
100
- // A `!`command's output (a `git diff` of a large change) lands in the
101
- // user turn bounded only by maxBuffer — persist it past the cap so the
102
- // model gets a preview plus the path, never a wall of text (review L3).
103
- outputs.set(command, persist ? persistIfLarge(text, { dir: persist.dir, id: `plugin-cmd-${persist.name}-${index}` }) : text);
104
- }),
105
- );
106
- return replaceShellPlaceholders(withArgs, outputs);
107
- }
108
-
109
83
  /** An empty discovery result — the fallback when a scan throws. */
110
84
  function emptyDiscovered(): DiscoveredPlugins {
111
85
  return {
@@ -136,6 +110,9 @@ function safeDiscover(roots: ReturnType<typeof defaultDiscoverRoots>): Discovere
136
110
  }
137
111
 
138
112
  export default function pluginsExtension(pi: ExtensionAPI) {
113
+ const sendUserMessage = createUserMessageSender(pi);
114
+ const getPermissionBridge = watchPermissionBridge(pi);
115
+ const workCwd = followWorkCwd(pi.events);
139
116
  // False once the session is replaced: a panel action still awaiting then
140
117
  // throws on its first pi.* call, which is expected and not an error to show.
141
118
  const alive = sessionAlive(pi);
@@ -155,7 +132,7 @@ export default function pluginsExtension(pi: ExtensionAPI) {
155
132
  plugins.plugins.find((p) => p.name === command.plugin);
156
133
  if (!plugin) continue;
157
134
  registeredCommands.add(command.name);
158
- registerPluginCommand(pi, plugin, command.name, command.path);
135
+ registerPluginCommand(pi, plugin, command.name, command.path, sendUserMessage, getPermissionBridge, workCwd);
159
136
  }
160
137
  };
161
138
 
@@ -183,7 +160,7 @@ export default function pluginsExtension(pi: ExtensionAPI) {
183
160
  panelOpen = true;
184
161
  let restartNotice = false;
185
162
  try {
186
- await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
163
+ await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
187
164
  const paint: PanelPaint = {
188
165
  fg: safeThemePaint(theme),
189
166
  bold: safeThemeBold(theme),
@@ -522,10 +499,11 @@ export default function pluginsExtension(pi: ExtensionAPI) {
522
499
  registerLocalCommand(pi, "plugins", {
523
500
  description: "Browse, install, and manage Claude Code-compatible plugins and marketplaces",
524
501
  handler: async (args, ctx) => {
525
- if (ctx.hasUI) {
502
+ if (canShowCustomUi(ctx)) {
526
503
  await openPanel(ctx);
527
504
  return;
528
505
  }
506
+ notifyRpcReadOnly(ctx, "/plugins", "install or manage plugins and marketplaces");
529
507
  // Non-interactive fallback: the old text listing.
530
508
  if (discovered.plugins.length === 0) {
531
509
  ctx.ui.notify("No plugins installed.", "info");
@@ -547,34 +525,71 @@ export default function pluginsExtension(pi: ExtensionAPI) {
547
525
  });
548
526
  }
549
527
 
550
- function registerPluginCommand(pi: ExtensionAPI, plugin: Plugin, name: string, path: string): void {
528
+ function registerPluginCommand(pi: ExtensionAPI, plugin: Plugin, name: string, path: string, sendUserMessage: ReturnType<typeof createUserMessageSender>, getPermissionBridge: ReturnType<typeof watchPermissionBridge>, workCwd: (sessionCwd: string) => string): void {
551
529
  let description = `Command from the ${plugin.name} plugin`;
552
- let argumentHint: string | undefined;
530
+ let argumentHint: CommandHint | undefined;
553
531
  try {
554
532
  const { frontmatter } = parseFrontmatterLoosely(readFileSync(path, "utf-8")) as {
555
533
  frontmatter?: Record<string, unknown>;
556
534
  };
557
535
  if (typeof frontmatter?.description === "string") description = frontmatter.description;
558
- if (typeof frontmatter?.["argument-hint"] === "string") argumentHint = frontmatter["argument-hint"];
536
+ argumentHint = frontmatterCommandHint(frontmatter);
559
537
  } catch {
560
538
  // Fall back to the generic description.
561
539
  }
562
540
 
541
+ if (argumentHint) announceArgumentHint(pi, name, argumentHint);
563
542
  pi.registerCommand(name, {
564
- description: argumentHint ? `${description} (${argumentHint})` : description,
543
+ description: argumentHint?.hint ? `${description} (${argumentHint.hint})` : description,
565
544
  handler: async (args, ctx) => {
545
+ // Registered commands cannot be removed from pi. Resolve their live
546
+ // owner and path on invocation, after toggles, updates or a cwd change.
547
+ const current = safeDiscover(defaultDiscoverRoots(getAgentDir(), ctx.cwd));
548
+ const command = current.commands.find((entry) => entry.name === name);
549
+ const plugin = command && current.enabledPlugins.find((entry) =>
550
+ entry.name === command.plugin && dirname(command.path) === join(entry.path, "commands"),
551
+ );
552
+ if (!command || !plugin) {
553
+ notifyOrPrint(ctx, `/${name} is unavailable in this project. Enable or reinstall its plugin in /plugins, then retry.`, "error");
554
+ if (!sessionOutlivesTurn(ctx.mode)) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
555
+ return;
556
+ }
557
+ const path = command.path;
566
558
  let body: string;
567
559
  try {
568
560
  const parsed = parseFrontmatterLoosely(readFileSync(path, "utf-8")) as { body: string };
569
561
  body = parsed.body;
570
562
  } catch (error) {
571
- ctx.ui.notify(`Could not read ${path}: ${(error as Error).message}`, "error");
563
+ notifyOrPrint(ctx, `Could not read ${path}: ${(error as Error).message}. Restore the command file or reinstall the ${plugin.name} plugin, then retry.`, "error");
564
+ // A one-shot run must not exit 0 for a command that never ran (exit/index.ts).
565
+ if (!sessionOutlivesTurn(ctx.mode)) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
566
+ return;
567
+ }
568
+ // A `!`command's output (a `git diff` of a large change) lands in the
569
+ // user turn: persist it past the cap so the model gets a preview plus
570
+ // the path, never a wall of text (review L3).
571
+ const dir = sessionResultsDir(ctx);
572
+ let expanded: string;
573
+ try {
574
+ // Placeholders run where the session works: an entered worktree, not the checkout it left.
575
+ expanded = await expandTemplate(body, args, workCwd(ctx.cwd), {
576
+ persist: (text, index) => persistIfLarge(text, { dir, id: `plugin-cmd-${name}-${index}` }),
577
+ run: async (command, cwd) => {
578
+ const decide = getPermissionBridge();
579
+ if (!decide) throw new Error("The permission gate is unavailable. Load the permissions extension before running shell placeholders.");
580
+ const decision = await decide({ toolName: "bash", input: { command }, cwd, model: ctx.model });
581
+ if (decision?.block) throw new Error(`Shell placeholder blocked: ${decision.reason}`);
582
+ return runPlaceholderCommand(command, cwd);
583
+ },
584
+ });
585
+ } catch (error) {
586
+ notifyOrPrint(ctx, `Could not expand /${name}: ${(error as Error).message}`, "error");
587
+ if (!sessionOutlivesTurn(ctx.mode)) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
572
588
  return;
573
589
  }
574
- const expanded = await expandTemplate(body, args, ctx.cwd, { dir: sessionResultsDir(ctx), name });
575
590
  recordUsage(pluginRoot(getAgentDir()), "command", name);
576
591
  // Deliver as a user turn, which is how Claude Code runs a command template.
577
- pi.sendUserMessage(expanded);
592
+ await sendUserMessage(ctx, promptCommandText(name, args.trim(), expanded));
578
593
  },
579
594
  });
580
595
  }
@@ -18,6 +18,7 @@ import { gitClone, gitHeadSha } from "../marketplace/git.ts";
18
18
  import type { MarketplaceEntry } from "../marketplace/types.ts";
19
19
  import { addInstalledPlugin, installedEntry, removeInstalledPlugin } from "./registry.ts";
20
20
  import { pluginCacheDir, versionedCachePath, withinBase } from "./paths.ts";
21
+ import { loadInstalledPlugins, validPluginName } from "../../lib/plugins.ts";
21
22
 
22
23
  export interface InstallResult {
23
24
  id: string;
@@ -82,6 +83,7 @@ export async function installPlugin(
82
83
  contentRoot: string | undefined,
83
84
  ): Promise<InstallResult> {
84
85
  const id = `${entry.name}@${marketplaceName}`;
86
+ if (validPluginName(entry.name) !== entry.name) throw new Error(`${id}: invalid plugin name — refusing to install`);
85
87
  const raw = entry.raw;
86
88
  if (raw.dependencies !== undefined) {
87
89
  throw new Error(`${id}: plugin dependencies are not supported yet — install the dependencies manually first`);
@@ -99,6 +101,9 @@ export async function installPlugin(
99
101
  }
100
102
  sourceDir = resolve(contentRoot, source);
101
103
  if (!existsSync(sourceDir)) throw new Error(`${id}: source path "${source}" does not exist in the marketplace`);
104
+ if (!withinBase(realpathSync(contentRoot), realpathSync(sourceDir))) {
105
+ throw new Error(`${id}: source path "${source}" escapes the marketplace directory through a symlink — refusing to install`);
106
+ }
102
107
  } else if (source.source === "github" || source.source === "url" || source.source === "git-subdir") {
103
108
  if (source.sha) {
104
109
  throw new Error(`${id}: commit-sha pinning is not supported yet — use a ref (branch/tag) instead`);
@@ -121,6 +126,10 @@ export async function installPlugin(
121
126
  rmSync(temp, { recursive: true, force: true });
122
127
  throw new Error(`${id}: "${source.source === "git-subdir" ? source.path : url}" has no content at the expected path`);
123
128
  }
129
+ if (source.source === "git-subdir" && !withinBase(realpathSync(cloneDir), realpathSync(sourceDir))) {
130
+ rmSync(temp, { recursive: true, force: true });
131
+ throw new Error(`${id}: git-subdir path "${source.path}" escapes the repository through a symlink — refusing to install`);
132
+ }
124
133
  } else {
125
134
  throw new Error(`${id}: source kind "${(source as { source: string }).source}" is not supported`);
126
135
  }
@@ -137,6 +146,13 @@ export async function installPlugin(
137
146
  if (!withinBase(pluginCacheBase, installPath)) {
138
147
  throw new Error(`${id}: computed install path "${installPath}" escapes the plugin cache directory — refusing to install`);
139
148
  }
149
+ // Sanitization is lossy (a.b and a-b share a cache segment). Never
150
+ // replace files that another registered plugin still owns.
151
+ if (existsSync(installPath)) {
152
+ const target = realpathSync(installPath);
153
+ const owner = loadInstalledPlugins(root).find((plugin) => plugin.id !== id && realpathSync(plugin.path) === target);
154
+ if (owner) throw new Error(`${id}: cache path "${installPath}" is already owned by ${owner.id} — refusing to overwrite it`);
155
+ }
140
156
  copyIntoCache(sourceDir, installPath);
141
157
  const now = new Date().toISOString();
142
158
  addInstalledPlugin(root, id, {
@@ -46,9 +46,15 @@ function cloneUrl(source: MarketplaceSource): string | undefined {
46
46
  const FETCH_TIMEOUT_MS = 10_000;
47
47
 
48
48
  async function fetchManifestJson(url: string): Promise<unknown> {
49
- const response = await fetchWithTimeout(url, FETCH_TIMEOUT_MS, { headers: { "user-agent": "one-code-plugins" } });
50
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
51
- return await response.json();
49
+ return fetchWithTimeout(
50
+ url,
51
+ FETCH_TIMEOUT_MS,
52
+ async (response) => {
53
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
54
+ return (await response.json()) as unknown;
55
+ },
56
+ { init: { headers: { "user-agent": "one-code-plugins" } } },
57
+ );
52
58
  }
53
59
 
54
60
  /**
@@ -8,6 +8,8 @@
8
8
  * entry never blanks the rest of the marketplace.
9
9
  */
10
10
 
11
+ import { validPluginName } from "../../lib/plugins.ts";
12
+
11
13
  export type MarketplaceSource =
12
14
  | { source: "github"; repo: string; ref?: string }
13
15
  | { source: "git"; url: string; ref?: string }
@@ -81,7 +83,7 @@ export function parseMarketplaceManifest(raw: unknown): { manifest?: Marketplace
81
83
 
82
84
  const plugins: MarketplaceEntry[] = [];
83
85
  for (const [index, entry] of raw.plugins.entries()) {
84
- if (!isRecord(entry) || typeof entry.name !== "string" || entry.name.length === 0 || entry.name.includes(" ")) {
86
+ if (!isRecord(entry) || typeof entry.name !== "string" || validPluginName(entry.name) !== entry.name) {
85
87
  errors.push(`plugins[${index}]: missing or invalid "name" — entry skipped`);
86
88
  continue;
87
89
  }
@@ -1,52 +1,49 @@
1
1
  /**
2
- * Raw-byte key decoding for the /plugins panel (pure).
2
+ * Key decoding for the /plugins panel (pure), named through
3
+ * `lib/key-input.ts` so every terminal encoding of a key decodes alike.
3
4
  *
4
- * Navigation bytes are always controls; any other printable input is a `text`
5
+ * Navigation keys are always controls; any other printable input is a `text`
5
6
  * intent the state layer routes (search box, Add Marketplace draft, or a
6
7
  * single-letter action where the active view has no text field). Unrecognized
7
8
  * escape sequences are swallowed so they never leak into a draft.
8
9
  */
9
10
 
11
+ import { keyId, keyText } from "../../lib/key-input.ts";
12
+
10
13
  export type PanelKey =
11
14
  | { kind: "up" | "down" | "pageUp" | "pageDown" | "nextTab" | "prevTab" | "enter" | "back" | "space" | "backspace" | "close" }
12
15
  | { kind: "text"; text: string };
13
16
 
14
17
  export function decodePanelKey(data: string): PanelKey | undefined {
15
- switch (data) {
16
- case "\x1b[A":
17
- case "\x1bOA":
18
+ switch (keyId(data)) {
19
+ case "up":
18
20
  return { kind: "up" };
19
- case "\x1b[B":
20
- case "\x1bOB":
21
+ case "down":
21
22
  return { kind: "down" };
22
- case "\x1b[5~":
23
+ case "pageUp":
23
24
  return { kind: "pageUp" };
24
- case "\x1b[6~":
25
+ case "pageDown":
25
26
  return { kind: "pageDown" };
26
- case "\x1b[C":
27
- case "\x1bOC":
28
- case "\t":
27
+ case "right":
28
+ case "tab":
29
29
  return { kind: "nextTab" };
30
- case "\x1b[D":
31
- case "\x1bOD":
32
- case "\x1b[Z":
30
+ case "left":
31
+ case "shift+tab":
33
32
  return { kind: "prevTab" };
34
- case "\r":
35
- case "\n":
33
+ case "enter":
36
34
  return { kind: "enter" };
37
- case "\x1b":
35
+ case "escape":
38
36
  return { kind: "back" };
39
- case " ":
37
+ case "space":
40
38
  return { kind: "space" };
41
- case "\x7f":
42
- case "\b":
39
+ case "backspace":
40
+ case "ctrl+backspace":
43
41
  return { kind: "backspace" };
44
- case "\x03": // ctrl+c
42
+ case "ctrl+c":
45
43
  return { kind: "close" };
46
44
  default:
47
45
  break;
48
46
  }
49
- if (data.startsWith("\x1b")) return undefined;
50
- const text = [...data].filter((ch) => ch >= " " && ch !== "\x7f").join("");
47
+ const text = keyText(data);
51
48
  return text ? { kind: "text", text } : undefined;
52
49
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { formatInstallCount } from "../counts.ts";
10
- import { cutPlainText, panelTopRule, type RenderBlock as Block, searchBoxLines, windowBlocks } from "../../lib/tui-render.ts";
10
+ import { cutPlainText, hintLines, panelTopRule, type RenderBlock as Block, searchBoxLines, windowBlocks } from "../../lib/tui-render.ts";
11
11
  import type { DiscoverRow, InstalledRow } from "./rows.ts";
12
12
  import { clampPanelState, type PanelState, type PanelView, resolveDetail, selectableRows, TABS, type Tab } from "./state.ts";
13
13
 
@@ -176,7 +176,7 @@ function detailLines(input: PanelRenderInput, paint: PanelPaint): { lines: strin
176
176
  const discover = row as DiscoverRow;
177
177
  const detail = input.discoverDetails.get(discover.id);
178
178
  push(`${discover.name} · ${discover.marketplace}`, paint.bold);
179
- if (discover.description) push(discover.description);
179
+ if (discover.description) for (const line of wrapNote(discover.description, width - 4)) push(line);
180
180
  push("");
181
181
  if (detail?.version) push(`Version: ${detail.version}`, (t) => paint.fg("dim", t));
182
182
  if (detail?.author) push(`Author: ${detail.author}`, (t) => paint.fg("dim", t));
@@ -250,20 +250,26 @@ const FOOTERS: Record<Tab, string[]> = {
250
250
  /** Installed search hints while the `/` box is open (Esc leaves it). */
251
251
  const INSTALLED_SEARCH_FOOTER = ["Type to search · Enter/Space to toggle · Esc to leave search"];
252
252
 
253
+ /** Footer hint lines wrapped at their separators to the panel's width (one leading space), so no key is cut off. */
254
+ function fitFooter(lines: string[], width: number): string[] {
255
+ return hintLines(lines, width - 1, " ");
256
+ }
257
+
253
258
  export function renderPanel(input: PanelRenderInput, paint: PanelPaint): string[] {
254
259
  const { state, view, width, height } = input;
255
260
  clampPanelState(state, view);
256
261
 
257
262
  const out: string[] = [panelTopRule(paint.fg, width), tabBar(state, view, paint, width), ""];
263
+ // Wrapped to the width where it is set: the list counts its lines for the budget.
258
264
  let footer: string[];
259
265
 
260
266
  if (state.addDialog) {
261
267
  out.push(...addDialogLines(state.addDialog.draft, paint, width));
262
- footer = ["Enter to add · Esc to cancel"];
268
+ footer = fitFooter(["Enter to add · Esc to cancel"], width);
263
269
  } else if (state.detail) {
264
270
  const detail = detailLines(input, paint);
265
271
  out.push(...detail.lines);
266
- footer = [detail.footer];
272
+ footer = fitFooter([detail.footer], width);
267
273
  } else {
268
274
  let blocks: Block[];
269
275
  switch (state.tab) {
@@ -293,7 +299,7 @@ export function renderPanel(input: PanelRenderInput, paint: PanelPaint): string[
293
299
  break;
294
300
  }
295
301
  }
296
- footer = state.tab === "installed" && state.installedSearching ? INSTALLED_SEARCH_FOOTER : FOOTERS[state.tab];
302
+ footer = fitFooter(state.tab === "installed" && state.installedSearching ? INSTALLED_SEARCH_FOOTER : FOOTERS[state.tab], width);
297
303
  const budget = Math.max(3, height - out.length - 2 - footer.length);
298
304
  const windowed = windowBlocks(blocks, state.cursor[state.tab], budget);
299
305
  out.push(...windowed.lines);
@@ -310,6 +316,6 @@ export function renderPanel(input: PanelRenderInput, paint: PanelPaint): string[
310
316
  ),
311
317
  );
312
318
  }
313
- out.push("", ...footer.map((line) => paint.fg("dim", cutPlainText(` ${line}`, width - 1))));
319
+ out.push("", ...footer.map((line) => paint.fg("dim", line)));
314
320
  return out;
315
321
  }