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