one-code-extension 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -2,24 +2,30 @@
2
2
  * skill extension — Claude Code's Skill tool.
3
3
  *
4
4
  * pi's own mechanism lists skills in the system prompt and expects the model to
5
- * `read` the SKILL.md path. Claude Code instead exposes a `skill` tool that
6
- * returns the skill's instructions as a tool result. This adds that tool, so a
7
- * skill can be invoked by name — including plugin skills as `<plugin>:<skill>`.
5
+ * `read` the SKILL.md path. Claude Code instead exposes a `skill` tool, which
6
+ * answers "Launching skill: <name>" with the skill's instructions beside it.
7
+ * This adds that tool, so a skill can be invoked by name — including plugin
8
+ * skills as `<plugin>:<skill>` — and expands a skill the user types into its
9
+ * instructions before the model sees the message, as Claude Code does.
8
10
  *
9
11
  * Skills discovered by pi (which includes `~/.claude/skills` and
10
12
  * `.claude/skills` thanks to the claude-compat extension) are read from
11
13
  * `before_agent_start`'s systemPromptOptions rather than rediscovered here.
12
14
  */
13
15
 
14
- import { readFileSync } from "node:fs";
16
+ import { readFileSync, statSync } from "node:fs";
15
17
  import os from "node:os";
18
+ import { dirname, join, resolve, sep } from "node:path";
19
+ import type { Api, Model } from "@earendil-works/pi-ai";
16
20
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
- import { getAgentDir, stripFrontmatter } from "@earendil-works/pi-coding-agent";
21
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
18
22
  import { Type } from "typebox";
23
+ import { canShowCustomUi, notifyOrPrint, notifyRpcReadOnly } from "../lib/headless-output.ts";
19
24
  import { pluginRoot } from "../lib/plugin-root.ts";
20
25
  import { defaultDiscoverRoots, discoverPlugins } from "../lib/plugins.ts";
21
- import { awaitOneShotTurn } from "../lib/notifications.ts";
22
- import { CONTEXT_ORDER, REMINDER_CHANNEL } from "../lib/reminders.ts";
26
+ import { awaitOneShotTurn, sessionOutlivesTurn } from "../lib/notifications.ts";
27
+ import { PROMPT_OPTIONS_CHANNEL, type PromptOptionsAnnouncement } from "../lib/prompt-options.ts";
28
+ import { CONTEXT_ORDER, REMINDER_CHANNEL, RENDER_CHANNEL, type RenderRequest } from "../lib/reminders.ts";
23
29
  import {
24
30
  nextSkillState,
25
31
  readSkillStates,
@@ -30,22 +36,34 @@ import {
30
36
  skillStateFor,
31
37
  } from "../lib/skill-overrides.ts";
32
38
  import { BUNDLED_SKILLS_DIR, estimateSkillTokens, promptTemplateNames, scanSkills, scopeForPath } from "../lib/skill-scan.ts";
33
- import { recordUsage } from "../lib/usage-tracker.ts";
39
+ import { sessionModelTier } from "../lib/session-model-tier.ts";
40
+ import { readUsage, recordUsage, usageKey } from "../lib/usage-tracker.ts";
34
41
  import { boundedDockHeight, ccToolRenderers, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
35
42
  import {
36
43
  bareSkillMatches,
37
- buildSkillBlock,
44
+ commandDisplayLine,
45
+ launchingSkill,
46
+ missingArgumentsNote,
38
47
  parseSkillCommand,
39
- redactOffSkillMessages,
40
48
  resolveSkill,
49
+ SKILL_INVOCATION_TYPE,
50
+ type SkillInvocationDetails,
41
51
  skillCommandCandidates,
52
+ skillPromptText,
53
+ typedSkillContent,
54
+ withoutDuplicateSkillCommands,
42
55
  } from "./invoke.ts";
56
+ import { codeReviewBody } from "./code-review.ts";
43
57
  import { decodeSkillsKey } from "./panel/keys.ts";
44
- import { skillListingText } from "./listing.ts";
58
+ import { frontmatterFlag, readableSkillListing, skillListingBudget, skillListingText, usageScore, withoutPiSkillsBlock } from "./listing.ts";
45
59
  import { renderSkillsPanel, type SkillsPaint } from "./panel/render.ts";
46
- import { applySkillsKey, initialSkillsState, type SkillsRow, visibleRows } from "./panel/state.ts";
60
+ import { listableSkillNames, SKILL_LISTING_HEADER } from "./announce.ts";
61
+ import { applySkillsKey, followRow, initialSkillsState, type SkillsRow, visibleRows } from "./panel/state.ts";
62
+ import { announceArgumentHint, type CommandHint, frontmatterCommandHint } from "../lib/argument-hints.ts";
63
+ import { parseSlashCommand, SKILL_BODY_CHANNEL, type SkillBodyQuery, SLASH_EXPAND_CHANNEL, type SlashExpandQuery } from "../lib/skill-body.ts";
47
64
  import { parseFrontmatterLoosely } from "../lib/frontmatter.ts";
48
65
  import { registerLocalCommand } from "../lib/local-command.ts";
66
+ import { panelUi } from "../lib/panel-focus.ts";
49
67
 
50
68
  interface IndexedSkill {
51
69
  name: string;
@@ -55,22 +73,102 @@ interface IndexedSkill {
55
73
  scope: SkillScope;
56
74
  state: SkillState;
57
75
  pluginName?: string;
76
+ /** The `when_to_use` frontmatter, which Claude Code lists after the description. */
77
+ whenToUse?: string;
78
+ /**
79
+ * A plugin command (`<plugin>:<command>`): listed for the model and
80
+ * loadable through the tool, as in Claude Code; typed, it stays the
81
+ * plugins extension's command.
82
+ */
83
+ kind?: "command";
84
+ /**
85
+ * Frontmatter `disable-model-invocation: true`: only the user starts it (a
86
+ * typed `/<name>`); it is left out of the model's listing and the `skill`
87
+ * tool refuses it, as Claude Code's Skill tool does.
88
+ */
89
+ disableModelInvocation?: boolean;
58
90
  }
59
91
 
92
+
60
93
  /** Bounded dock like the /plugins panel — keeps the transcript visible above. */
61
94
  const SKILLS_PANEL_MAX_HEIGHT = 24;
62
95
 
96
+ const BUNDLED_ROOT = resolve(BUNDLED_SKILLS_DIR);
97
+ /** A skill in One Code's own catalog, like one built into Claude Code: listed last, kept whole over budget, no folder named. */
98
+ const isBundled = (path: string): boolean => resolve(path).startsWith(BUNDLED_ROOT + sep);
99
+ const CODE_REVIEW_PATH = join(BUNDLED_ROOT, "code-review", "SKILL.md");
100
+ const CODE_REVIEW_CELLS = join(BUNDLED_ROOT, "code-review", "cells");
101
+
63
102
  export default function skillExtension(pi: ExtensionAPI) {
103
+ const requestTier = sessionModelTier(pi);
104
+ // pi draws a user message as markdown, so a typed skill, `/init` and
105
+ // `/doctor` showed their whole expanded prompt; show the command line.
106
+ pi.registerMarkdownTransformer((markdown, context) =>
107
+ context.messageType === "user" ? (commandDisplayLine(markdown) ?? markdown) : markdown,
108
+ );
64
109
  /** pi resolves skills per turn; cache the latest list for the tool to use. */
65
110
  let piSkills: IndexedSkill[] = [];
66
111
  /** Session cwd, so project-level enabledPlugins settings apply to plugin skills. */
67
112
  let sessionCwd: string | undefined;
113
+ /** The session's model: its context window sizes the listing, its tier picks code-review's body. */
114
+ let sessionModel: Model<Api> | undefined;
68
115
 
69
- pi.on("before_agent_start", (event, ctx) => {
70
- sessionCwd = ctx.cwd;
71
- const skills = event.systemPromptOptions.skills ?? [];
116
+ /**
117
+ * True once a `prompt()` has run (the only path that emits
118
+ * before_agent_start). Until then an idle skill delivery goes out as a user
119
+ * message: pi's `sendMessage(…, {triggerTurn})` skips that preamble, so a
120
+ * session opening with `/loop …` or `/simplify` ran its first request
121
+ * without our system prompt or reminder stack (lib/notifications.ts,
122
+ * "First turn of a session"; upstream_prs.md #17). Later turns get the
123
+ * prompt from system-prompt's context_with_system handler (idle-turn.ts).
124
+ */
125
+ let prompted = false;
126
+ /**
127
+ * A one-shot run (`-p`, `--mode json`) ends when its prompt returns, so the
128
+ * bare `/<name>` skill forms are taken by the `input` hook there instead of
129
+ * registered as commands: a command can only start a second prompt, which
130
+ * the run does not wait for.
131
+ */
132
+ let oneShot = false;
133
+ /** The bare skill names a one-shot run takes in the `input` hook. */
134
+ const oneShotSkillNames = new Set<string>();
135
+
136
+ // A factory re-run builds each session's instance, so this is belt and braces
137
+ // (lib/notifications.ts resets its twin the same way).
138
+ pi.on("session_start", (_event, ctx) => {
139
+ prompted = false;
140
+ oneShot = !sessionOutlivesTurn(ctx.mode);
141
+ announcedSkills = false;
142
+ sessionUsage = undefined;
143
+ sessionModel = ctx.model;
144
+ });
145
+ /** The index as the current run's first request read it; a new run, or pi's skills for it, reads it again. */
146
+ let runIndex: IndexedSkill[] | undefined;
147
+ pi.on("agent_start", () => {
148
+ runIndex = undefined;
149
+ });
150
+ // Inside an epoch the listing on message 1 is the record: a skill that became
151
+ // listable since rides the request's tail with only its own line, decided
152
+ // when the request goes out from what the epoch has told.
153
+ pi.events.on(RENDER_CHANNEL, (data) => {
154
+ const request = data as RenderRequest;
155
+ if (request.epoch) return;
156
+ const told = request.told.get("skills") ?? new Set<string>();
157
+ // Read once per run: a run makes many requests, and the index reads every skill file.
158
+ const all = (runIndex ??= index());
159
+ const added = new Set(listableNow(all).filter((name) => !told.has(name)));
160
+ if (added.size === 0) return;
161
+ const text = listingText(all.filter((skill) => added.has(skill.name)));
162
+ if (text) request.blocks.push({ text, facts: [{ kind: "skills", names: [...added] }] });
163
+ });
164
+ pi.on("model_select", (event) => {
165
+ sessionModel = event.model;
166
+ });
167
+ /** Index the skills pi resolved for this turn and list them for the model. */
168
+ const adoptPiSkills = (skills: unknown[], cwd: string) => {
169
+ sessionCwd = cwd;
72
170
  piSkills = skills.map((skill) => {
73
- const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string };
171
+ const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string; disableModelInvocation?: boolean };
74
172
  const path = record.path ?? record.filePath ?? "";
75
173
  return {
76
174
  name: record.name,
@@ -79,20 +177,65 @@ export default function skillExtension(pi: ExtensionAPI) {
79
177
  source: "project" as const,
80
178
  scope: scopeForPath(path, os.homedir(), getAgentDir()),
81
179
  state: "on" as const, // resolved per index() call below
180
+ // pi reads only a YAML `true`; Claude Code also takes the string "true".
181
+ disableModelInvocation: record.disableModelInvocation === true || (path ? readModelInvocationDisabled(path) : false),
82
182
  };
83
183
  });
84
- // Claude Code lists skills as a <system-reminder> on the first user message
85
- // (its block 3), framed for the Skill tool — not in the system prompt.
86
- const listing = describe();
87
- if (listing !== "(no skills available)") {
88
- pi.events.emit(REMINDER_CHANNEL, {
89
- text: `The following skills are available for use with the Skill tool:\n\n${listing}`,
90
- scope: "every-turn",
91
- key: "skills",
92
- placement: "first-prepend",
93
- order: CONTEXT_ORDER.skills,
94
- });
184
+ announceSkills();
185
+ };
186
+ /**
187
+ * Claude Code lists skills as a <system-reminder> on the first user message
188
+ * (its block 3), framed for the Skill tool — not in the system prompt. A
189
+ * session without that tool (an agent limited to read and bash, a main
190
+ * session started with fewer tools) gets the same block framed for reading
191
+ * the file, since pi's own section is dropped from every prompt below.
192
+ * `skills` is the whole index or, for a later notice, only the new ones.
193
+ */
194
+ const listingText = (skills: IndexedSkill[]): string | undefined => {
195
+ const tools = pi.getActiveTools();
196
+ if (!tools.includes("skill")) return readableSkillListing(skills, tools.includes("read") ? "the read tool" : "the shell");
197
+ const listing = describe(skills);
198
+ return listing === "(no skills available)" ? undefined : `${SKILL_LISTING_HEADER}\n\n${listing}`;
199
+ };
200
+ const listableNow = (skills: IndexedSkill[]) => listableSkillNames(skills, !pi.getActiveTools().includes("skill"));
201
+ /** The listing as live state: what an epoch's message 1 says (the render step above announces later skills). */
202
+ const announceSkills = () => {
203
+ const all = index();
204
+ const text = listingText(all);
205
+ if (!text) {
206
+ pi.events.emit(REMINDER_CHANNEL, { key: "skills", remove: true });
207
+ return;
95
208
  }
209
+ pi.events.emit(REMINDER_CHANNEL, {
210
+ text,
211
+ key: "skills",
212
+ placement: "first-prepend",
213
+ order: CONTEXT_ORDER.skills,
214
+ facts: [{ kind: "skills", names: listableNow(all) }],
215
+ });
216
+ };
217
+ pi.on("before_agent_start", (event, ctx) => {
218
+ prompted = true;
219
+ sessionModel = ctx.model ?? sessionModel;
220
+ adoptPiSkills(event.systemPromptOptions.skills ?? [], ctx.cwd);
221
+ runIndex = undefined;
222
+ // The listing above is the one skills instruction: drop pi's own section
223
+ // from the prompt (a child agent's own prompt carries it; the main
224
+ // session's prompt is rebuilt without it).
225
+ if (typeof event.systemPrompt !== "string") return;
226
+ const stripped = withoutPiSkillsBlock(event.systemPrompt);
227
+ return stripped === event.systemPrompt ? undefined : { systemPrompt: stripped };
228
+ });
229
+ // The first turn a background completion opens (lib/prompt-options.ts)
230
+ // carries the same listing a typed first prompt would.
231
+ // The first announcement wins, as in system-prompt: both describe one turn.
232
+ let announcedSkills = false;
233
+ pi.events.on(PROMPT_OPTIONS_CHANNEL, (data) => {
234
+ const announced = data as PromptOptionsAnnouncement | undefined;
235
+ const skills = (announced?.options as { skills?: unknown[] } | undefined)?.skills;
236
+ if (prompted || announcedSkills || !announced?.cwd || !Array.isArray(skills)) return;
237
+ announcedSkills = true;
238
+ adoptPiSkills(skills, announced.cwd);
96
239
  });
97
240
 
98
241
  /**
@@ -102,24 +245,57 @@ export default function skillExtension(pi: ExtensionAPI) {
102
245
  * stable for the session anyway (the listing must stay byte-stable for the
103
246
  * cache prefix).
104
247
  */
105
- const descriptionCache = new Map<string, string | undefined>();
248
+ const frontmatterCache = new Map<string, Record<string, unknown> | undefined>();
249
+ /** A SKILL.md's frontmatter, read once per path: the description and the argument hint come from it. */
250
+ const readFrontmatter = (path: string): Record<string, unknown> | undefined => {
251
+ if (frontmatterCache.has(path)) return frontmatterCache.get(path);
252
+ let frontmatter: Record<string, unknown> | undefined;
253
+ try {
254
+ frontmatter = parseFrontmatterLoosely(readFileSync(path, "utf-8")).frontmatter;
255
+ } catch {
256
+ frontmatter = undefined;
257
+ }
258
+ frontmatterCache.set(path, frontmatter);
259
+ return frontmatter;
260
+ };
106
261
  const readDescription = (path: string): string | undefined => {
107
- if (descriptionCache.has(path)) return descriptionCache.get(path);
108
- const description = readDescriptionUncached(path);
109
- descriptionCache.set(path, description);
110
- return description;
262
+ const description = readFrontmatter(path)?.description;
263
+ return typeof description === "string" ? description : undefined;
264
+ };
265
+ const readWhenToUse = (path: string): string | undefined => {
266
+ const whenToUse = readFrontmatter(path)?.when_to_use;
267
+ return typeof whenToUse === "string" && whenToUse.trim() ? whenToUse : undefined;
111
268
  };
112
- const readDescriptionUncached = (path: string): string | undefined => {
269
+
270
+ /**
271
+ * A SKILL.md's `disable-model-invocation` flag, read apart from the
272
+ * description cache and again whenever the file changes: a flag added
273
+ * mid-session must stop the model loading the skill, while the listing's
274
+ * descriptions stay byte-stable.
275
+ */
276
+ const invocationFlagCache = new Map<string, { mtimeMs: number; disabled: boolean }>();
277
+ const readModelInvocationDisabled = (path: string): boolean => {
278
+ let mtimeMs = -1;
113
279
  try {
114
- const { frontmatter } = parseFrontmatterLoosely(readFileSync(path, "utf-8")) as {
115
- frontmatter?: { description?: unknown };
116
- };
117
- return typeof frontmatter?.description === "string" ? frontmatter.description : undefined;
280
+ mtimeMs = statSync(path).mtimeMs;
281
+ } catch {
282
+ // Unreadable: parsed (and failed) below, cached under -1.
283
+ }
284
+ const cached = invocationFlagCache.get(path);
285
+ if (cached && cached.mtimeMs === mtimeMs) return cached.disabled;
286
+ let disabled = false;
287
+ try {
288
+ disabled = frontmatterFlag(parseFrontmatterLoosely(readFileSync(path, "utf-8")).frontmatter?.["disable-model-invocation"]);
118
289
  } catch {
119
- return undefined;
290
+ // No file, no flag.
120
291
  }
292
+ invocationFlagCache.set(path, { mtimeMs, disabled });
293
+ return disabled;
121
294
  };
122
295
 
296
+ /** A SKILL.md's `argument-hint`, the prompt's placeholder after its command (lib/argument-hints.ts). */
297
+ const readArgumentHint = (path: string): CommandHint | undefined => frontmatterCommandHint(readFrontmatter(path));
298
+
123
299
  // Per-skill availability comes from the skill-overrides store (the /skills
124
300
  // panel cycles it, the /plugins panel manages plugin skills). The listing
125
301
  // and the tool both re-read it, so a change applies live.
@@ -130,7 +306,11 @@ export default function skillExtension(pi: ExtensionAPI) {
130
306
  // the /skills command/panel falls back to a disk scan (reading each
131
307
  // SKILL.md's frontmatter for its description) — that's the gap that made a
132
308
  // fresh session's /skills show only plugin skills.
133
- const index = (cwd = sessionCwd): IndexedSkill[] => {
309
+ //
310
+ // The order is Claude Code's: project and user skills, then plugin commands
311
+ // and plugin skills, then the bundled catalog.
312
+ /** Every listable entry; `withCommands: false` leaves out plugin commands (and their frontmatter reads). */
313
+ const index = (cwd = sessionCwd, withCommands = true): IndexedSkill[] => {
134
314
  const agentDir = getAgentDir();
135
315
  const home = os.homedir();
136
316
  const states = readSkillStates(pluginRoot(agentDir));
@@ -145,34 +325,78 @@ export default function skillExtension(pi: ExtensionAPI) {
145
325
  source: "project" as const,
146
326
  scope: skill.scope,
147
327
  state: "on" as const,
328
+ disableModelInvocation: readModelInvocationDisabled(skill.path),
148
329
  }));
149
330
  const project = base.map((skill) => ({
150
331
  ...skill,
332
+ whenToUse: readWhenToUse(skill.path),
151
333
  state: skillStateFor(states, skillOverrideKey(skill.scope, skill.name)),
152
334
  }));
153
335
  // Plugin skills from discoverPlugins are already filtered to enabled by
154
336
  // the same store; anything it returns is fully available (plugin skills
155
337
  // aren't governed by skillOverrides — managed via /plugins).
156
- const plugin = discoverPlugins(defaultDiscoverRoots(agentDir, cwd, home)).skills.map((skill) => ({
338
+ const discovered = discoverPlugins(defaultDiscoverRoots(agentDir, cwd, home));
339
+ const plugin = discovered.skills.map((skill) => ({
157
340
  name: skill.name,
158
341
  description: readDescription(skill.path),
342
+ whenToUse: readWhenToUse(skill.path),
159
343
  path: skill.path,
160
344
  source: "plugin" as const,
161
345
  scope: "plugin" as const,
162
346
  state: "on" as const,
163
347
  pluginName: skill.plugin,
348
+ disableModelInvocation: readModelInvocationDisabled(skill.path),
164
349
  }));
165
- return [...project, ...plugin];
350
+ // A plugin command is listed when it describes itself, as in Claude Code.
351
+ const commands = !withCommands ? [] : discovered.commands.flatMap((command) => {
352
+ const description = readDescription(command.path);
353
+ const whenToUse = readWhenToUse(command.path);
354
+ if (!description?.trim() && !whenToUse) return [];
355
+ return [
356
+ {
357
+ name: command.name,
358
+ description,
359
+ whenToUse,
360
+ path: command.path,
361
+ source: "plugin" as const,
362
+ scope: "plugin" as const,
363
+ state: "on" as const,
364
+ pluginName: command.plugin,
365
+ kind: "command" as const,
366
+ disableModelInvocation: readModelInvocationDisabled(command.path),
367
+ },
368
+ ];
369
+ });
370
+ return [...project.filter((skill) => !isBundled(skill.path)), ...commands, ...plugin, ...project.filter((skill) => isBundled(skill.path))];
166
371
  };
372
+ /** Skills proper: the /skills panel and the typed forms leave plugin commands to the plugins extension. */
373
+ const skillsOnly = (cwd = sessionCwd): IndexedSkill[] => index(cwd, false);
167
374
 
168
- const describe = () => skillListingText(index());
375
+ /**
376
+ * Usage as the session found it: read once, so the listing's ranking (and
377
+ * with it message 1 of the cached prefix) does not move when a skill is
378
+ * used mid-session or a day's decay crosses a threshold.
379
+ */
380
+ let sessionUsage: { usage: ReturnType<typeof readUsage>; now: Date } | undefined;
381
+
382
+ /** The listing, within Claude Code's budget for the session model's context window. */
383
+ const describe = (source: IndexedSkill[] = index()) => {
384
+ sessionUsage ??= { usage: readUsage(pluginRoot(getAgentDir())), now: new Date() };
385
+ const { usage, now } = sessionUsage;
386
+ const skills = source.map((skill) => ({
387
+ ...skill,
388
+ bundled: isBundled(skill.path),
389
+ usage: usageScore(usage[usageKey(skill.kind ?? "skill", skill.name)], now),
390
+ }));
391
+ return skillListingText(skills, skillListingBudget(sessionModel?.contextWindow));
392
+ };
169
393
 
170
394
  pi.registerTool({
171
395
  name: "skill",
172
396
  label: "Skill",
173
397
  ...ccToolRenderers<{ skill?: string; args?: string }>("Skill", {
174
398
  title: (a) => (a ? [a.skill, a.args].filter(Boolean).join(" ") : undefined),
175
- // The full instruction text goes to the model; the transcript needs one line.
399
+ // The skill's text goes to the model; the transcript needs one line.
176
400
  result: (_r, a, isError) => (isError ? undefined : a?.skill ? `Loaded ${a.skill}` : undefined),
177
401
  }),
178
402
  description:
@@ -183,12 +407,12 @@ export default function skillExtension(pi: ExtensionAPI) {
183
407
  args: Type.Optional(Type.String({ description: "Arguments to pass through to the skill" })),
184
408
  list: Type.Optional(Type.Boolean({ description: "List available skills instead of invoking one" })),
185
409
  }),
186
- async execute(_toolCallId, params) {
410
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
187
411
  // `list`, or a bare call with no invoke signal, browses the catalog. A
188
412
  // call that passed `args` but no `skill` is an invocation that forgot to
189
413
  // name the skill — fail loudly rather than silently returning the
190
414
  // catalog, which a weak model reads as a non-sequitur (same archetype as
191
- // the subagent tool; see docs/decisions/subagents-workflows.md).
415
+ // the subagent tool; see working-docs/decisions/subagents-workflows.md).
192
416
  const all = index();
193
417
  if (params.list || (!params.skill && params.args == null)) {
194
418
  return {
@@ -228,10 +452,11 @@ export default function skillExtension(pi: ExtensionAPI) {
228
452
  };
229
453
  }
230
454
 
231
- // "off" is the only state that refuses invocation (matching Claude Code,
232
- // where invoking an off skill by name returns the skillOverrides error).
233
- // "user-only" is invocable — it's just hidden from the model's listing,
234
- // so the model won't auto-trigger it, but /skill-name still runs it.
455
+ // An off skill is refused (matching Claude Code, where invoking an off
456
+ // skill by name returns the skillOverrides error). A user-only skill is
457
+ // hidden from the model and runs only as the user's `/<name>`, which does
458
+ // not come through this tool; a listing frozen before the user hid it can
459
+ // still name it, so the tool refuses it too (./announce.ts).
235
460
  if (found.state === "off") {
236
461
  const where = found.source === "plugin" ? "/plugins" : "/skills";
237
462
  return {
@@ -240,11 +465,29 @@ export default function skillExtension(pi: ExtensionAPI) {
240
465
  isError: true,
241
466
  };
242
467
  }
468
+ if (found.state === "user-only") {
469
+ return {
470
+ content: [{ type: "text", text: `Skill "${found.name}" is set to user-only — only the user can run it, by typing /${found.name}.` }],
471
+ details: { skill: found.name, state: found.state } as Record<string, unknown>,
472
+ isError: true,
473
+ };
474
+ }
243
475
 
244
- let body: string;
476
+ // Only the user may start a skill marked `disable-model-invocation`
477
+ // (a typed `/<name>` runs through deliverSkill, not this tool).
478
+ if (found.disableModelInvocation || readModelInvocationDisabled(found.path)) {
479
+ return {
480
+ content: [{ type: "text", text: `Skill ${wanted} cannot be used with Skill tool due to disable-model-invocation` }],
481
+ details: { skill: found.name } as Record<string, unknown>,
482
+ isError: true,
483
+ };
484
+ }
485
+
486
+ let promptText: string;
245
487
  try {
246
488
  const parsed = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")) as { body: string };
247
- body = parsed.body.trim();
489
+ const { body, args } = skillBody(found, parsed.body.trim(), params.args?.trim() ?? "", ctx.cwd);
490
+ promptText = skillPromptText(body, args, baseDirFor(found));
248
491
  } catch (error) {
249
492
  return {
250
493
  content: [{ type: "text", text: `Could not read skill "${found.name}": ${(error as Error).message}` }],
@@ -253,76 +496,130 @@ export default function skillExtension(pi: ExtensionAPI) {
253
496
  };
254
497
  }
255
498
 
256
- recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
499
+ recordUsage(pluginRoot(getAgentDir()), found.kind ?? "skill", found.name);
257
500
 
258
- // Resource paths in a skill are relative to its own directory, so the
259
- // model needs to know where it lives to read references/ or scripts/.
260
- const header = [
261
- `Skill: ${found.name}`,
262
- `Location: ${found.path}`,
263
- params.args ? `Arguments: ${params.args}` : undefined,
264
- "Follow these instructions for the current task.",
265
- ]
266
- .filter(Boolean)
267
- .join("\n");
501
+ // A skill that takes arguments, called without any, returns its
502
+ // no-argument body; say so, since a model previewing the skill would
503
+ // otherwise follow it (invoke.ts missingArgumentsNote).
504
+ const hint = params.args?.trim() ? undefined : readArgumentHint(found.path);
505
+ const hintText = hint?.hint ?? hint?.argNames?.map((name) => `<${name}>`).join(" ");
506
+ const note = hintText ? missingArgumentsNote(hintText) : undefined;
268
507
 
508
+ // Claude Code's shape: the result says the skill is launching, and its
509
+ // text follows as a block of its own.
269
510
  return {
270
- content: [{ type: "text", text: `${header}\n\n---\n\n${body}` }],
511
+ content: [
512
+ { type: "text", text: launchingSkill(found.name) },
513
+ { type: "text", text: note ? `${note.before}\n\n${promptText}\n\n${note.after}` : promptText },
514
+ ],
271
515
  details: { skill: found.name, path: found.path } as Record<string, unknown>,
272
516
  };
273
517
  },
274
518
  });
275
519
 
276
520
  /**
277
- * Surface a user-facing skill message: a TUI/RPC toast where there is UI, or
278
- * stderr in a one-shot run (`-p`, `--mode json`), where `ctx.ui.notify` is a
279
- * no-op (pi wires no UI context there) so the message would otherwise vanish
280
- * and the command would exit silently. `console.error` writes to stderr, so it
281
- * never corrupts the `--mode json` event stream on stdout.
521
+ * A skill's body and the arguments still to append: a generated body
522
+ * (lib/skill-body.ts, `/loop`) carries its arguments, a file's does not.
523
+ * The bundled code-review picks its body by tier and effort (code-review.ts).
282
524
  */
283
- const notifyOrPrint = (
284
- ctx: Pick<ExtensionContext, "hasUI" | "ui">,
285
- message: string,
286
- level: "warning" | "error",
287
- ): void => {
288
- if (ctx.hasUI) ctx.ui.notify(message, level);
289
- else console.error(message);
525
+ const skillBody = (found: IndexedSkill, fileBody: string, args: string, cwd: string): { body: string; args: string } => {
526
+ if (found.kind === "command") return { body: fileBody, args };
527
+ if (resolve(found.path) === CODE_REVIEW_PATH) {
528
+ try {
529
+ const review = codeReviewBody(CODE_REVIEW_CELLS, requestTier(), pi.getThinkingLevel(), args);
530
+ return { body: review.body, args: review.args };
531
+ } catch {
532
+ // A cell file missing from an install: the SKILL.md body (the medium cell) still reviews.
533
+ }
534
+ }
535
+ const query: SkillBodyQuery = { skill: found.name, path: found.path, args, cwd };
536
+ pi.events.emit(SKILL_BODY_CHANNEL, query);
537
+ return query.body !== undefined ? { body: query.body, args: "" } : { body: fileBody, args };
290
538
  };
539
+ /** The folder Claude Code names before a skill's text; none for One Code's own catalog. */
540
+ const baseDirFor = (found: IndexedSkill): string | undefined => (found.kind === "command" || isBundled(found.path) ? undefined : dirname(found.path));
541
+
542
+ // A fired scheduled prompt that is a skill's slash command runs that skill,
543
+ // as Claude Code's queue runs a fired `/babysit-prs` (lib/skill-body.ts).
544
+ pi.events.on(SLASH_EXPAND_CHANNEL, (data) => {
545
+ const query = data as SlashExpandQuery;
546
+ const command = parseSlashCommand(query.text);
547
+ if (!command) return;
548
+ const found = resolveSkill(skillsOnly(query.cwd), command.name.replace(/^skill:/, ""));
549
+ if (!found || found.state === "off") return;
550
+ let fileBody: string;
551
+ try {
552
+ fileBody = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")).body.trim();
553
+ } catch {
554
+ return;
555
+ }
556
+ recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
557
+ const { body, args } = skillBody(found, fileBody, command.args, query.cwd);
558
+ query.expanded = typedSkillContent(found.name, command.args, skillPromptText(body, args, baseDirFor(found))).join("");
559
+ });
560
+
561
+ /** The `input` hook's answer that replaces the typed prompt with the skill's message. */
562
+ type SkillTransform = { action: "transform"; text: string; images?: Array<{ type: "image"; data: string; mimeType: string }> };
291
563
 
292
564
  /**
293
565
  * Run a resolved skill the way a user-typed command does: refuse an "off"
294
566
  * skill (in EVERY mode — falling through would hand `/skill:` to pi's native
295
567
  * expansion, which knows nothing of the overrides store and would run it; a
296
568
  * one-shot run gets the refusal on stderr instead of a UI notice, never as
297
- * silent execution), else re-deliver pi's exact `<skill>` block as a hidden
298
- * custom message. The model receives the same bytes pi's own expansion would
299
- * submit as a user turn (convertToLlm maps a custom message to a user message
300
- * regardless of `display`), but nothing renders. Shared by the `/skill:<name>`
301
- * interception and the bare `/<name>` commands.
569
+ * silent execution), else deliver Claude Code's typed-skill message (the
570
+ * command breadcrumb and the skill's text) as a hidden custom message: the
571
+ * model receives it as a user message (convertToLlm maps a custom message
572
+ * to one regardless of `display`), but nothing renders and no `skill` call
573
+ * is needed. Shared by the `/skill:<name>` interception, the typed
574
+ * `/<plugin>:<skill>` form and the bare `/<name>` commands.
302
575
  */
303
576
  const deliverSkill = async (
304
577
  found: IndexedSkill,
305
578
  args: string,
306
579
  ctx: ExtensionContext & { waitForIdle?: () => Promise<void> },
307
- extra: { images?: Array<{ type: "image"; data: string; mimeType: string }>; streamingBehavior?: "steer" | "followUp" } = {},
308
- ): Promise<"handled" | "unavailable"> => {
580
+ extra: {
581
+ images?: Array<{ type: "image"; data: string; mimeType: string }>;
582
+ streamingBehavior?: "steer" | "followUp";
583
+ input?: string;
584
+ /** Called from the `input` hook, which can hand the message back as the prompt's own text. */
585
+ fromInput?: boolean;
586
+ } = {},
587
+ ): Promise<"handled" | "unavailable" | SkillTransform> => {
309
588
  if (found.state === "off") {
310
589
  const where = found.source === "plugin" ? "/plugins" : "/skills";
311
590
  notifyOrPrint(ctx, `Skill "${found.name}" is turned off — enable it from ${where} to run it.`, "warning");
312
591
  return "handled";
313
592
  }
314
- let body: string;
593
+ let fileBody: string;
315
594
  try {
316
- body = stripFrontmatter(readFileSync(found.path, "utf-8")).trim();
595
+ fileBody = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")).body.trim();
317
596
  } catch {
318
597
  return "unavailable";
319
598
  }
320
599
  recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
321
- const block = buildSkillBlock({ name: found.name, filePath: found.path }, body, args);
322
- // Carry any attached images alongside the block, as pi's native path would.
323
- const content = extra.images?.length ? [{ type: "text" as const, text: block }, ...extra.images] : block;
600
+ const { body, args: bodyArgs } = skillBody(found, fileBody, args, ctx.cwd);
601
+ // Claude Code's typed-skill message: the command breadcrumb, then the
602
+ // skill's text; any attached images ride after them.
603
+ const [breadcrumb, text] = typedSkillContent(found.name, args, skillPromptText(body, bodyArgs, baseDirFor(found)));
604
+ const content = [{ type: "text" as const, text: breadcrumb }, { type: "text" as const, text }, ...(extra.images ?? [])];
605
+ if (!prompted && !extra.streamingBehavior) {
606
+ // From the input hook the message becomes the typed prompt's own text
607
+ // (the same bytes sendUserMessage would send: its text parts joined by a
608
+ // newline). sendUserMessage starts a second prompt that is still in its
609
+ // async preflight when this handler returns, so the session looks idle,
610
+ // and a one-shot run disposed the session before that turn ran.
611
+ if (extra.fromInput) return { action: "transform", text: `${breadcrumb}\n${text}`, images: extra.images };
612
+ pi.sendUserMessage(content, { deliverAs: "followUp" });
613
+ await awaitOneShotTurn(ctx);
614
+ return "handled";
615
+ }
324
616
  pi.sendMessage(
325
- { customType: "one-code:skill-invocation", content, display: false, details: { skill: found.name, args } },
617
+ {
618
+ customType: SKILL_INVOCATION_TYPE,
619
+ content,
620
+ display: false,
621
+ details: { skill: found.name, args, ...(extra.input !== undefined ? { input: extra.input } : {}) } satisfies SkillInvocationDetails,
622
+ },
326
623
  { triggerTurn: true, ...(extra.streamingBehavior ? { deliverAs: extra.streamingBehavior } : {}) },
327
624
  );
328
625
  // The turn we just triggered rides a fire-and-forget pi.sendMessage. In a
@@ -339,21 +636,52 @@ export default function skillExtension(pi: ExtensionAPI) {
339
636
  // the transcript as a new user turn. Intercept it here, suppress pi's
340
637
  // expansion (return "handled"), and deliver through deliverSkill. An unknown
341
638
  // name, an ambiguous plugin match, or an unreadable file falls through to
342
- // pi's native handling. This hook only covers prompt(); paths that expand
343
- // without an `input` event (steer/followUp) are caught by the context-hook
344
- // redaction below.
639
+ // pi's native handling. Since pi 0.86.0 (faa9863cb) this also runs for a
640
+ // message queued mid-turn and for RPC steer/followUp (`streamingBehavior` is
641
+ // set), so an off skill is refused before it enters the queue. On older pi
642
+ // those paths skip `input`; the context-hook redaction below covers them.
643
+ //
644
+ // A typed `/<plugin>:<skill>` has no command of its own (plugin skills get
645
+ // no bare alias), so it reached the model as text and the model called
646
+ // `skill`; it is expanded here too, as Claude Code expands it. Only an exact
647
+ // plugin-skill name is taken; anything else stays the user's text.
648
+ //
649
+ // In a one-shot run the bare `/<name>` forms are not commands (see
650
+ // registerSkillCommands) and are taken here too, so the first skill becomes
651
+ // the prompt's own text and the run waits for its turn.
345
652
  pi.on("input", async (event, ctx) => {
346
- const cmd = parseSkillCommand(event.text);
653
+ const typed = parseSkillCommand(event.text) ?? oneShotBareSkill(event.text);
654
+ const plugin = typed ? undefined : typedPluginSkill(event.text);
655
+ const cmd = typed ?? plugin?.command;
347
656
  if (!cmd) return { action: "continue" };
348
- const found = resolveSkill(index(), cmd.name);
657
+ const found = plugin?.skill ?? resolveSkill(skillsOnly(ctx.cwd), cmd.name);
349
658
  if (!found) return { action: "continue" };
350
659
  const outcome = await deliverSkill(found, cmd.args, ctx, {
351
660
  images: event.images as Array<{ type: "image"; data: string; mimeType: string }> | undefined,
352
661
  streamingBehavior: event.streamingBehavior,
662
+ input: event.text,
663
+ fromInput: true,
353
664
  });
665
+ if (typeof outcome === "object") return outcome;
354
666
  return { action: outcome === "handled" ? "handled" : "continue" };
355
667
  });
356
668
 
669
+ /** A typed bare `/<name> [args]` naming a skill whose command a one-shot run left unregistered. */
670
+ const oneShotBareSkill = (text: string): { name: string; args: string } | undefined => {
671
+ if (!oneShot || !text.startsWith("/")) return undefined;
672
+ const command = parseSlashCommand(text);
673
+ return command && oneShotSkillNames.has(command.name) ? command : undefined;
674
+ };
675
+
676
+ /** A typed `/<plugin>:<skill> [args]` naming a plugin skill exactly, and that skill. */
677
+ const typedPluginSkill = (text: string): { command: { name: string; args: string }; skill: IndexedSkill } | undefined => {
678
+ if (!text.startsWith("/")) return undefined;
679
+ const command = parseSlashCommand(text);
680
+ if (!command?.name.includes(":")) return undefined;
681
+ const skill = skillsOnly().find((candidate) => candidate.source === "plugin" && candidate.name === command.name);
682
+ return skill ? { command, skill } : undefined;
683
+ };
684
+
357
685
  // Claude Code invokes a user/project skill as a bare `/<name>` (only plugin
358
686
  // skills carry a `<plugin>:` prefix); pi registers `/skill:<name>`. Register
359
687
  // each project/user/bundled skill as a bare extension command too, so
@@ -370,7 +698,7 @@ export default function skillExtension(pi: ExtensionAPI) {
370
698
  // be unregistered, so the handler resolves the skill afresh at invocation:
371
699
  // a skill deleted or turned off since registration is refused, never run.
372
700
  const registeredSkillCommands = new Set<string>();
373
- const registerSkillCommands = (cwd: string | undefined) => {
701
+ const registerSkillCommands = (cwd: string | undefined, skills: IndexedSkill[] = skillsOnly(cwd)) => {
374
702
  let taken: string[];
375
703
  try {
376
704
  taken = pi.getCommands().map((command) => command.name);
@@ -378,12 +706,18 @@ export default function skillExtension(pi: ExtensionAPI) {
378
706
  return; // not bound yet (load time) — session_start retries
379
707
  }
380
708
  const templates = promptTemplateNames(cwd ?? process.cwd(), os.homedir(), getAgentDir());
381
- for (const skill of skillCommandCandidates(index(cwd), [...taken, ...templates, ...registeredSkillCommands])) {
709
+ for (const skill of skillCommandCandidates(skills, [...taken, ...templates, ...registeredSkillCommands, ...oneShotSkillNames])) {
710
+ if (oneShot) {
711
+ oneShotSkillNames.add(skill.name);
712
+ continue;
713
+ }
382
714
  registeredSkillCommands.add(skill.name);
715
+ const hint = readArgumentHint(skill.path);
716
+ if (hint) announceArgumentHint(pi, skill.name, hint);
383
717
  pi.registerCommand(skill.name, {
384
718
  description: skill.description ? `${skill.description} (skill)` : `Run the ${skill.name} skill`,
385
719
  handler: async (args, ctx) => {
386
- const found = resolveSkill(index(ctx.cwd), skill.name);
720
+ const found = resolveSkill(skillsOnly(ctx.cwd), skill.name);
387
721
  if (!found || (await deliverSkill(found, args.trim(), ctx)) === "unavailable") {
388
722
  notifyOrPrint(ctx, `Skill "${skill.name}" is no longer available (its SKILL.md was removed or is unreadable).`, "error");
389
723
  }
@@ -391,40 +725,45 @@ export default function skillExtension(pi: ExtensionAPI) {
391
725
  });
392
726
  }
393
727
  };
394
- pi.on("session_start", (_event, ctx) => registerSkillCommands(ctx.cwd));
728
+ pi.on("session_start", (_event, ctx) => {
729
+ const skills = skillsOnly(ctx.cwd);
730
+ registerSkillCommands(ctx.cwd, skills);
731
+ // pi lists every skill a second time as `/skill:<name>`; drop that entry
732
+ // where the bare command exists (withoutDuplicateSkillCommands).
733
+ if (ctx.hasUI && ctx.mode === "tui") {
734
+ ctx.ui.addAutocompleteProvider((current) => ({
735
+ ...current,
736
+ getSuggestions: async (lines, cursorLine, cursorCol, options) => {
737
+ const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
738
+ if (!suggestions) return suggestions;
739
+ const items = withoutDuplicateSkillCommands(suggestions.items, suggestions.prefix, registeredSkillCommands);
740
+ return items.length > 0 ? { ...suggestions, items } : null;
741
+ },
742
+ applyCompletion: (...args) => current.applyCompletion(...args),
743
+ ...(current.shouldTriggerFileCompletion ? { shouldTriggerFileCompletion: (...args) => current.shouldTriggerFileCompletion!(...args) } : {}),
744
+ }));
745
+ }
746
+ // pi's own `/skill:<name>` form, which pi lists only after the first turn.
747
+ for (const skill of skills) {
748
+ const hint = skill.source === "plugin" ? undefined : readArgumentHint(skill.path);
749
+ if (hint) announceArgumentHint(pi, `skill:${skill.name}`, hint);
750
+ }
751
+ });
395
752
  // pi's per-turn resolution (frontmatter `name`, description required) can
396
753
  // surface skills the pre-turn disk scan missed; late registrations still
397
754
  // execute (autocomplete lists them after a /reload). Gated on an unseen
398
755
  // name so the steady state costs one Set lookup per skill per turn, not a
399
756
  // re-index (before_agent_start runs every turn).
400
757
  pi.on("before_agent_start", (_event, ctx) => {
401
- if (piSkills.some((skill) => !registeredSkillCommands.has(skill.name))) registerSkillCommands(ctx.cwd);
402
- });
403
-
404
- // Fail-closed backstop for the `input` interception above: pi's steer() and
405
- // followUp() expand `/skill:<name>` natively WITHOUT firing an `input` event
406
- // (queued interactive messages after the first, RPC steer/followUp), so a
407
- // turned-off skill's instructions can land in the session history anyway.
408
- // Strip them from every outgoing request instead — the wire copy carries the
409
- // refusal notice, the session file keeps the original bytes, and untouched
410
- // requests return undefined so the message array stays byte-identical
411
- // (prompt-cache stable). Override states are read lazily, only when a
412
- // message actually contains a `<skill>` block.
413
- pi.on("context", (event) => {
414
- let states: Map<string, string> | undefined;
415
- const isOff = (name: string) => {
416
- states ??= new Map(index().map((skill) => [skill.name, skill.state]));
417
- return states.get(name) === "off";
418
- };
419
- const redacted = redactOffSkillMessages(event.messages, isOff);
420
- return redacted ? { messages: redacted } : undefined;
758
+ if (piSkills.some((skill) => !registeredSkillCommands.has(skill.name) && !oneShotSkillNames.has(skill.name))) registerSkillCommands(ctx.cwd);
421
759
  });
422
760
 
423
761
  const buildSkillsRows = (cwd: string | undefined): SkillsRow[] =>
424
- index(cwd).map((skill) => ({
762
+ skillsOnly(cwd).map((skill) => ({
425
763
  key: skillOverrideKey(skill.scope, skill.name),
426
764
  name: skill.name,
427
- scope: skill.scope,
765
+ // The override key keeps the path's scope; the row names the catalog.
766
+ scope: isBundled(skill.path) ? ("bundled" as const) : skill.scope,
428
767
  tokens: estimateSkillTokens(skill.path),
429
768
  state: skill.state,
430
769
  locked: skill.source === "plugin",
@@ -436,7 +775,7 @@ export default function skillExtension(pi: ExtensionAPI) {
436
775
  // the skill-overrides store live. Plugin skills show locked (managed via
437
776
  // /plugins). Pure state/render live in ./panel; this owns repaint + writes.
438
777
  const openSkillsPanel = async (ctx: ExtensionContext): Promise<void> => {
439
- await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
778
+ await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
440
779
  const paint: SkillsPaint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
441
780
  const state = initialSkillsState();
442
781
  let rows = buildSkillsRows(ctx.cwd);
@@ -462,7 +801,6 @@ export default function skillExtension(pi: ExtensionAPI) {
462
801
  if (!key) return;
463
802
  const before = visibleRows(rows, state);
464
803
  const anchorKey = before[state.cursor]?.key;
465
- const sortBefore = state.sort;
466
804
  const effect = applySkillsKey(state, key, before);
467
805
  notice = undefined;
468
806
  if (effect?.kind === "close") {
@@ -478,13 +816,8 @@ export default function skillExtension(pi: ExtensionAPI) {
478
816
  } else if (effect?.kind === "locked") {
479
817
  notice = `${effect.row.name} is a plugin skill — manage it from /plugins.`;
480
818
  }
481
- // A sort toggle reorders the list; keep the same skill selected so a
482
- // following Space acts on the row the user was looking at, not the one
483
- // that slid into its index.
484
- if (state.sort !== sortBefore && anchorKey) {
485
- const idx = visibleRows(rows, state).findIndex((row) => row.key === anchorKey);
486
- if (idx >= 0) state.cursor = idx;
487
- }
819
+ // A sort toggle or a cycle under the state sort reorders the list.
820
+ if ((key.kind === "text" || effect?.kind === "cycle") && anchorKey) followRow(state, visibleRows(rows, state), anchorKey);
488
821
  repaint();
489
822
  },
490
823
  invalidate: () => {
@@ -495,17 +828,20 @@ export default function skillExtension(pi: ExtensionAPI) {
495
828
  };
496
829
 
497
830
  registerLocalCommand(pi, "skills", {
831
+ reportsResult: true,
498
832
  description: "View and manage skills (on / name-only / user-only / off)",
499
833
  handler: async (args, ctx) => {
500
- if (ctx.hasUI) {
834
+ if (canShowCustomUi(ctx)) {
501
835
  await openSkillsPanel(ctx);
502
- return;
836
+ return "Skills dialog dismissed";
503
837
  }
838
+ const readOnly = notifyRpcReadOnly(ctx, "/skills", "change skill states");
839
+ const withNotice = (text: string) => (readOnly ? `${readOnly}\n${text}` : text);
504
840
  // Non-interactive fallback: a flat listing with each skill's state.
505
- const all = index(ctx.cwd);
841
+ const all = skillsOnly(ctx.cwd);
506
842
  if (all.length === 0) {
507
843
  ctx.ui.notify("No skills available.", "info");
508
- return;
844
+ return withNotice("No skills available.");
509
845
  }
510
846
  const lines = all.map((skill) => {
511
847
  const label = skill.source === "plugin" ? "plugin" : skill.state;
@@ -513,6 +849,7 @@ export default function skillExtension(pi: ExtensionAPI) {
513
849
  return `- ${skill.name} [${label}]${desc}`;
514
850
  });
515
851
  ctx.ui.notify(`Skills:\n${lines.join("\n")}`, "info");
852
+ return withNotice(`Skills:\n${lines.join("\n")}`);
516
853
  },
517
854
  });
518
855
  }