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