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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An artifact's viewer page, `<id>/view.html` (pure, no fs): the stored page in
|
|
3
|
+
* a frame under a slim bar, One Code's version of the header Claude Code's web
|
|
4
|
+
* viewer puts over an artifact. The bar keeps what works without a server: a
|
|
5
|
+
* home link to the gallery, a title menu (details, Download HTML, Copy file
|
|
6
|
+
* path, Reload, the page without the bar, the version list, All artifacts) and
|
|
7
|
+
* a Download button. Share, Chat, Rename, Duplicate and Pin need claude.ai and
|
|
8
|
+
* are left out.
|
|
9
|
+
*
|
|
10
|
+
* `view.html#v<N>` shows an earlier version with a banner back to the latest.
|
|
11
|
+
* Download loads the version's payload script (`download.js`, or
|
|
12
|
+
* `versions/v<N>.js`) and saves it through a `blob:` link, for the same reason
|
|
13
|
+
* the gallery does (gallery.ts).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { tildify } from "../lib/paths.ts";
|
|
19
|
+
import { CLIENT_RUNTIME, escapeHtml, THEME_TOKENS, utcStamp } from "./gallery.ts";
|
|
20
|
+
import type { ArtifactMeta } from "./store.ts";
|
|
21
|
+
|
|
22
|
+
/** Inline SVG icons; `currentColor` so they follow the theme. */
|
|
23
|
+
const HOME_ICON = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 7.5 8 3l5.5 4.5V13a.5.5 0 0 1-.5.5H9.5V10h-3v3.5H3a.5.5 0 0 1-.5-.5z"/></svg>`;
|
|
24
|
+
const CHEVRON_ICON = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="m3 4.5 3 3 3-3"/></svg>`;
|
|
25
|
+
const DOWNLOAD_ICON = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 2v7m-3-3 3 3 3-3M2.5 11.5h9"/></svg>`;
|
|
26
|
+
|
|
27
|
+
/** `<id>/view.html` for `meta`, whose folder is `dir`. */
|
|
28
|
+
export function renderViewer(meta: ArtifactMeta, dir: string, home: string = homedir()): string {
|
|
29
|
+
const title = escapeHtml(meta.title);
|
|
30
|
+
const versions: string[] = [`<a role="menuitem" href="#" data-version="latest">Version ${meta.version} (latest)</a>`];
|
|
31
|
+
for (let n = meta.version - 1; n >= 1; n--) versions.push(`<a role="menuitem" href="#v${n}" data-version="${n}">Version ${n}</a>`);
|
|
32
|
+
const details = [
|
|
33
|
+
`Version ${meta.version}`,
|
|
34
|
+
`Updated <time datetime="${escapeHtml(meta.updatedAt)}">${escapeHtml(utcStamp(meta.updatedAt))}</time>`,
|
|
35
|
+
meta.project ? escapeHtml(tildify(meta.project, home)) : "",
|
|
36
|
+
].filter(Boolean);
|
|
37
|
+
// The page paths the script copies, for the current version and each earlier one.
|
|
38
|
+
const paths = JSON.stringify({ latest: join(dir, "index.html"), versions: join(dir, "versions") }).replace(/</g, "\\u003c");
|
|
39
|
+
return `<!doctype html>
|
|
40
|
+
<html lang="en">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="utf-8">
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
44
|
+
<title>${title}</title>
|
|
45
|
+
<style>
|
|
46
|
+
${THEME_TOKENS}
|
|
47
|
+
* { box-sizing: border-box; }
|
|
48
|
+
html, body { margin: 0; height: 100%; }
|
|
49
|
+
body {
|
|
50
|
+
background: var(--bg); color: var(--text);
|
|
51
|
+
font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
52
|
+
display: flex; flex-direction: column;
|
|
53
|
+
}
|
|
54
|
+
.bar {
|
|
55
|
+
display: flex; align-items: center; gap: 6px; padding: 6px 16px; min-height: 44px;
|
|
56
|
+
background: var(--surface); border-bottom: 1px solid var(--line); position: relative; z-index: 2;
|
|
57
|
+
}
|
|
58
|
+
.icon-link, .title-button, .primary, .menu [role="menuitem"] {
|
|
59
|
+
font: inherit; color: var(--text); background: transparent; border: 0; border-radius: 6px; cursor: pointer; text-decoration: none;
|
|
60
|
+
}
|
|
61
|
+
.icon-link { display: inline-flex; padding: 6px; color: var(--muted); }
|
|
62
|
+
.icon-link:hover, .title-button:hover, .title-button[aria-expanded="true"] { background: var(--hover); color: var(--text); }
|
|
63
|
+
.title-button { display: inline-flex; align-items: center; gap: 6px; padding: 5px 8px; max-width: min(60vw, 480px); }
|
|
64
|
+
.title-button span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
65
|
+
.spacer { flex: 1; }
|
|
66
|
+
.primary {
|
|
67
|
+
display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; font-weight: 600; font-size: 13px;
|
|
68
|
+
border: 1px solid var(--line);
|
|
69
|
+
}
|
|
70
|
+
.primary:hover { background: var(--hover); }
|
|
71
|
+
.menu {
|
|
72
|
+
position: absolute; top: calc(100% - 2px); left: 44px; min-width: 250px; max-width: calc(100vw - 32px);
|
|
73
|
+
background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 6px;
|
|
74
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.18); display: flex; flex-direction: column;
|
|
75
|
+
}
|
|
76
|
+
.menu[hidden] { display: none; }
|
|
77
|
+
.menu .details { padding: 6px 10px 8px; color: var(--muted); font-size: 13px; display: flex; flex-direction: column; gap: 2px; font-variant-numeric: tabular-nums; }
|
|
78
|
+
.menu .details span:last-child { overflow-wrap: anywhere; }
|
|
79
|
+
.menu [role="menuitem"] { display: block; text-align: left; padding: 7px 10px; }
|
|
80
|
+
.menu [role="menuitem"]:hover, .menu [role="menuitem"]:focus-visible { background: var(--hover); outline: none; }
|
|
81
|
+
.menu [aria-current="true"] { font-weight: 600; }
|
|
82
|
+
.menu .label { padding: 8px 10px 2px; color: var(--muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
83
|
+
.menu hr { border: 0; border-top: 1px solid var(--line); margin: 6px 4px; }
|
|
84
|
+
.versions { max-height: 180px; overflow-y: auto; display: flex; flex-direction: column; }
|
|
85
|
+
.banner { padding: 8px 16px; background: var(--hover); border-bottom: 1px solid var(--line); font-size: 13px; }
|
|
86
|
+
.banner[hidden] { display: none; }
|
|
87
|
+
.banner a { color: var(--accent); }
|
|
88
|
+
.toast {
|
|
89
|
+
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 3;
|
|
90
|
+
background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: 6px; font-size: 13px;
|
|
91
|
+
}
|
|
92
|
+
.toast[hidden] { display: none; }
|
|
93
|
+
iframe { flex: 1; width: 100%; border: 0; display: block; background: var(--bg); }
|
|
94
|
+
a:focus-visible, button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
95
|
+
</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
<header class="bar">
|
|
99
|
+
<a class="icon-link" href="../index.html" title="All artifacts" aria-label="All artifacts">${HOME_ICON}</a>
|
|
100
|
+
<button type="button" class="title-button" id="title-button" aria-haspopup="menu" aria-expanded="false" aria-controls="menu"><span>${title}</span>${CHEVRON_ICON}</button>
|
|
101
|
+
<div class="menu" id="menu" role="menu" hidden>
|
|
102
|
+
<div class="details">${details.map((line) => `<span>${line}</span>`).join("")}</div>
|
|
103
|
+
<hr>
|
|
104
|
+
<button type="button" role="menuitem" data-action="download">Download HTML</button>
|
|
105
|
+
<button type="button" role="menuitem" data-action="copy-path">Copy file path</button>
|
|
106
|
+
<button type="button" role="menuitem" data-action="reload">Reload</button>
|
|
107
|
+
<a role="menuitem" id="raw-link" href="index.html">Open without this bar</a>
|
|
108
|
+
${meta.version > 1 ? `<hr><div class="label">Versions</div><div class="versions">${versions.join("")}</div>` : ""}
|
|
109
|
+
<hr>
|
|
110
|
+
<a role="menuitem" href="../index.html">All artifacts</a>
|
|
111
|
+
</div>
|
|
112
|
+
<span class="spacer"></span>
|
|
113
|
+
<button type="button" class="primary" data-action="download">${DOWNLOAD_ICON}Download</button>
|
|
114
|
+
</header>
|
|
115
|
+
<div class="banner" id="banner" hidden>You are viewing version <span id="banner-version"></span> of ${meta.version}. <a href="#">Show the latest</a></div>
|
|
116
|
+
<iframe id="page" src="index.html" title="${title}"></iframe>
|
|
117
|
+
<div class="toast" id="toast" role="status" hidden></div>
|
|
118
|
+
<script>
|
|
119
|
+
(function () {
|
|
120
|
+
var LATEST = ${meta.version};
|
|
121
|
+
var PATHS = ${paths};
|
|
122
|
+
var SEP = PATHS.latest.indexOf("\\\\") !== -1 ? "\\\\" : "/";
|
|
123
|
+
var frame = document.getElementById("page");
|
|
124
|
+
var menu = document.getElementById("menu");
|
|
125
|
+
var button = document.getElementById("title-button");
|
|
126
|
+
var banner = document.getElementById("banner");
|
|
127
|
+
var toast = document.getElementById("toast");
|
|
128
|
+
var shown = null;
|
|
129
|
+
|
|
130
|
+
function current() {
|
|
131
|
+
var match = /^#v(\\d+)$/.exec(location.hash);
|
|
132
|
+
var n = match ? parseInt(match[1], 10) : NaN;
|
|
133
|
+
return n >= 1 && n < LATEST ? n : null;
|
|
134
|
+
}
|
|
135
|
+
function show() {
|
|
136
|
+
shown = current();
|
|
137
|
+
var src = shown ? "versions/v" + shown + ".html" : "index.html";
|
|
138
|
+
if (frame.getAttribute("src") !== src) frame.setAttribute("src", src);
|
|
139
|
+
document.getElementById("raw-link").setAttribute("href", src);
|
|
140
|
+
banner.hidden = !shown;
|
|
141
|
+
if (shown) document.getElementById("banner-version").textContent = shown;
|
|
142
|
+
menu.querySelectorAll("[data-version]").forEach(function (link) {
|
|
143
|
+
var v = link.getAttribute("data-version");
|
|
144
|
+
link.setAttribute("aria-current", String(v === "latest" ? !shown : Number(v) === shown));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function note(text) {
|
|
148
|
+
toast.textContent = text;
|
|
149
|
+
toast.hidden = false;
|
|
150
|
+
clearTimeout(note.timer);
|
|
151
|
+
note.timer = setTimeout(function () { toast.hidden = true; }, 1800);
|
|
152
|
+
}
|
|
153
|
+
function setMenu(open) {
|
|
154
|
+
menu.hidden = !open;
|
|
155
|
+
button.setAttribute("aria-expanded", String(open));
|
|
156
|
+
}
|
|
157
|
+
${CLIENT_RUNTIME}
|
|
158
|
+
function download() {
|
|
159
|
+
var script = document.createElement("script");
|
|
160
|
+
script.src = shown ? "versions/v" + shown + ".js" : "download.js";
|
|
161
|
+
script.onload = function () { script.remove(); };
|
|
162
|
+
script.onerror = function () { script.remove(); note("Download failed. Use Open without this bar, then save the page."); };
|
|
163
|
+
document.head.appendChild(script);
|
|
164
|
+
}
|
|
165
|
+
function copyPath() {
|
|
166
|
+
var path = shown ? PATHS.versions + SEP + "v" + shown + ".html" : PATHS.latest;
|
|
167
|
+
var done = function () { note("Copied " + path); };
|
|
168
|
+
var fallback = function () { window.prompt("File path", path); };
|
|
169
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(path).then(done, fallback);
|
|
170
|
+
else fallback();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
button.addEventListener("click", function () { setMenu(menu.hidden); });
|
|
174
|
+
document.addEventListener("click", function (event) {
|
|
175
|
+
var action = event.target.closest && event.target.closest("[data-action]");
|
|
176
|
+
if (action) {
|
|
177
|
+
var name = action.getAttribute("data-action");
|
|
178
|
+
if (name === "download") download();
|
|
179
|
+
else if (name === "copy-path") copyPath();
|
|
180
|
+
else if (name === "reload") location.reload();
|
|
181
|
+
setMenu(false);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.target.closest && event.target.closest("[data-version], #raw-link")) { setMenu(false); return; }
|
|
185
|
+
if (!menu.hidden && !menu.contains(event.target) && !button.contains(event.target)) setMenu(false);
|
|
186
|
+
});
|
|
187
|
+
document.addEventListener("keydown", function (event) {
|
|
188
|
+
if (event.key === "Escape" && !menu.hidden) { setMenu(false); button.focus(); }
|
|
189
|
+
});
|
|
190
|
+
// A click in the frame never reaches this document; losing focus to it closes the menu.
|
|
191
|
+
window.addEventListener("blur", function () { setMenu(false); });
|
|
192
|
+
window.addEventListener("hashchange", show);
|
|
193
|
+
show();
|
|
194
|
+
})();
|
|
195
|
+
</script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask_user_question's model-facing texts: Claude Code's AskUserQuestion
|
|
3
|
+
* description and parameter texts (pure).
|
|
4
|
+
*
|
|
5
|
+
* The short form goes to frontier and workhorse models; the long form, the
|
|
6
|
+
* same text without the "Reserve this for decisions…" paragraph, to cheap
|
|
7
|
+
* and tiny ones (lib/tool-variants.ts). The plan-mode note names One Code's
|
|
8
|
+
* tools and drops "the user cannot see the plan until you call ExitPlanMode":
|
|
9
|
+
* One Code's plan is a file the user can open at any time.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DescriptionForm } from "../lib/tool-variants.ts";
|
|
13
|
+
|
|
14
|
+
export const ASK_SHORT_DESCRIPTION = `Use this tool only when you are blocked on a decision that is genuinely the user's to make: one you cannot resolve from the request, the code, or sensible defaults.
|
|
15
|
+
|
|
16
|
+
Usage notes:
|
|
17
|
+
- Users will always be able to select "Other" to provide custom text input
|
|
18
|
+
- Use multiSelect: true to allow multiple answers to be selected for a question
|
|
19
|
+
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
|
20
|
+
|
|
21
|
+
Plan mode note: To switch into plan mode, use enter_plan_mode (not this tool). Once in plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?", "Should I proceed?", or otherwise reference "the plan" in questions — plan approval goes through exit_plan_mode.
|
|
22
|
+
|
|
23
|
+
Reserve this for decisions where the user's answer changes what you do next — not for choices with a conventional default or facts you can verify in the codebase yourself. In those cases pick the obvious option, mention it in your response, and proceed.
|
|
24
|
+
|
|
25
|
+
Preview feature:
|
|
26
|
+
Use the optional \`preview\` field on options when presenting concrete artifacts that users need to visually compare:
|
|
27
|
+
- ASCII mockups of UI layouts or components
|
|
28
|
+
- Code snippets showing different implementations
|
|
29
|
+
- Diagram variations
|
|
30
|
+
- Configuration examples
|
|
31
|
+
|
|
32
|
+
Preview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export const ASK_LONG_DESCRIPTION = `Use this tool only when you are blocked on a decision that is genuinely the user's to make: one you cannot resolve from the request, the code, or sensible defaults.
|
|
36
|
+
|
|
37
|
+
Usage notes:
|
|
38
|
+
- Users will always be able to select "Other" to provide custom text input
|
|
39
|
+
- Use multiSelect: true to allow multiple answers to be selected for a question
|
|
40
|
+
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
|
|
41
|
+
|
|
42
|
+
Plan mode note: To switch into plan mode, use enter_plan_mode (not this tool). Once in plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask "Is my plan ready?", "Should I proceed?", or otherwise reference "the plan" in questions — plan approval goes through exit_plan_mode.
|
|
43
|
+
|
|
44
|
+
Preview feature:
|
|
45
|
+
Use the optional \`preview\` field on options when presenting concrete artifacts that users need to visually compare:
|
|
46
|
+
- ASCII mockups of UI layouts or components
|
|
47
|
+
- Code snippets showing different implementations
|
|
48
|
+
- Diagram variations
|
|
49
|
+
- Configuration examples
|
|
50
|
+
|
|
51
|
+
Preview content is rendered as markdown in a monospace box. Multi-line text with newlines is supported. When any option has a preview, the UI switches to a side-by-side layout with a vertical option list on the left and preview on the right. Do not use previews for simple preference questions where labels and descriptions suffice. Note: previews are only supported for single-select questions (not multiSelect).
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
export function askDescription(form: DescriptionForm): string {
|
|
55
|
+
return form === "long" ? ASK_LONG_DESCRIPTION : ASK_SHORT_DESCRIPTION;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const ASK_PARAMS = {
|
|
59
|
+
questions: `Questions to ask the user (1-4 questions)`,
|
|
60
|
+
question: `The complete question to ask the user. Should be clear, specific, and end with a question mark. Example: "Which library should we use for date formatting?" If multiSelect is true, phrase it accordingly, e.g. "Which features do you want to enable?"`,
|
|
61
|
+
header: `Very short label displayed as a chip/tag (max 12 chars). Examples: "Auth method", "Library", "Approach".`,
|
|
62
|
+
options: `The available choices for this question. Must have 2-4 options. Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). There should be no 'Other' option, that will be provided automatically.`,
|
|
63
|
+
multiSelect: `Set to true to allow the user to select multiple options instead of just one. Use when choices are not mutually exclusive.`,
|
|
64
|
+
label: `The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.`,
|
|
65
|
+
optionDescription: `Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.`,
|
|
66
|
+
preview: `Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons that help users compare options. See the tool description for the expected content format.`,
|
|
67
|
+
} as const;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask_user_question through pi's plain dialogs (pure).
|
|
3
|
+
*
|
|
4
|
+
* pi's RPC mode returns `undefined` from `ctx.ui.custom()` ("Custom UI not
|
|
5
|
+
* supported in RPC mode"), so the tabbed widget never reaches an RPC client and
|
|
6
|
+
* the tool used to report "The user cancelled without answering." RPC does
|
|
7
|
+
* forward `select` and `input` as `extension_ui_request`s, so this asks each
|
|
8
|
+
* question with them and returns the widget's result shape: the same tool
|
|
9
|
+
* result the TUI produces. A dismissed dialog cancels with the answers given so
|
|
10
|
+
* far, as Esc does in the widget.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Answer, Question } from "./questions.ts";
|
|
14
|
+
import type { WidgetResult } from "./widget.ts";
|
|
15
|
+
|
|
16
|
+
export const TYPE_OWN = "Type my own answer";
|
|
17
|
+
export const CHAT = "Chat about this";
|
|
18
|
+
export const DONE = "Done";
|
|
19
|
+
|
|
20
|
+
export interface DialogUI {
|
|
21
|
+
select(title: string, options: string[], opts?: { signal?: AbortSignal }): Promise<string | undefined>;
|
|
22
|
+
input(title: string, placeholder?: string, opts?: { signal?: AbortSignal }): Promise<string | undefined>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One option's row: its label, and its description after a dash when it has one. */
|
|
26
|
+
export function optionRow(option: { label: string; description?: string }): string {
|
|
27
|
+
return option.description ? `${option.label} — ${option.description}` : option.label;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function askThroughDialogs(questions: Question[], ui: DialogUI, signal?: AbortSignal): Promise<WidgetResult> {
|
|
31
|
+
const answers: Answer[] = [];
|
|
32
|
+
for (const question of questions) {
|
|
33
|
+
const plainRows = question.options.map(optionRow);
|
|
34
|
+
// RPC identifies a selection by its displayed string. Number every option
|
|
35
|
+
// when a row collides with another option or one of our control rows.
|
|
36
|
+
const numbered = new Set(plainRows).size !== plainRows.length || plainRows.some((row) => [DONE, TYPE_OWN, CHAT].includes(row));
|
|
37
|
+
const rows = plainRows.map((row, index) => (numbered ? `${index + 1}. ${row}` : row));
|
|
38
|
+
const previews = question.options.flatMap((option, index) => (option.preview ? [`${rows[index]}:\n${option.preview}`] : []));
|
|
39
|
+
const title = [`${question.header}: ${question.question}`, ...previews].join("\n\n");
|
|
40
|
+
// By option index: two options may share a label (with different descriptions).
|
|
41
|
+
const selected: number[] = [];
|
|
42
|
+
const labels = () => selected.map((index) => question.options[index].label);
|
|
43
|
+
let typed: string | undefined;
|
|
44
|
+
for (;;) {
|
|
45
|
+
const choices = [
|
|
46
|
+
...rows.flatMap((row, index) => (selected.includes(index) ? [] : [row])),
|
|
47
|
+
...(question.multiSelect && selected.length > 0 ? [DONE] : []),
|
|
48
|
+
TYPE_OWN,
|
|
49
|
+
CHAT,
|
|
50
|
+
];
|
|
51
|
+
const picked = await ui.select(question.multiSelect && selected.length > 0 ? `${title} (selected: ${labels().join(", ")})` : title, choices, { signal });
|
|
52
|
+
if (signal?.aborted || picked === undefined || !choices.includes(picked)) return { kind: "cancel", answers };
|
|
53
|
+
if (picked === CHAT) return { kind: "chat" };
|
|
54
|
+
if (picked === DONE) break;
|
|
55
|
+
if (picked === TYPE_OWN) {
|
|
56
|
+
const text = (await ui.input(title, "Type something.", { signal }))?.trim();
|
|
57
|
+
if (signal?.aborted || text === undefined) return { kind: "cancel", answers };
|
|
58
|
+
// A blank answer is no answer: ask the question again.
|
|
59
|
+
if (!text) continue;
|
|
60
|
+
typed = text;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const index = rows.indexOf(picked);
|
|
64
|
+
if (index < 0) return { kind: "cancel", answers };
|
|
65
|
+
selected.push(index);
|
|
66
|
+
if (!question.multiSelect || selected.length === question.options.length) break;
|
|
67
|
+
}
|
|
68
|
+
const all = typed === undefined ? labels() : [...labels(), typed];
|
|
69
|
+
const single = !question.multiSelect && selected.length === 1 ? question.options[selected[0]] : undefined;
|
|
70
|
+
answers.push({
|
|
71
|
+
question: question.question,
|
|
72
|
+
header: question.header,
|
|
73
|
+
selected: all,
|
|
74
|
+
freeform: typed !== undefined && selected.length === 0,
|
|
75
|
+
...(typed !== undefined ? { typed: true } : {}),
|
|
76
|
+
...(single?.preview ? { preview: single.preview } : {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { kind: "submit", answers };
|
|
80
|
+
}
|
|
@@ -11,57 +11,60 @@
|
|
|
11
11
|
* pulled in a second, conflicting TypeBox.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Type } from "typebox";
|
|
16
|
+
import { canShowCustomUi } from "../lib/headless-output.ts";
|
|
16
17
|
import { ccToolRenderers, safeThemeBold, safeThemeInverse, safeThemePaint } from "../lib/tui-render.ts";
|
|
17
|
-
import {
|
|
18
|
+
import { registerFormTool } from "../lib/tool-variants.ts";
|
|
19
|
+
import { ASK_PARAMS, askDescription } from "./description.ts";
|
|
20
|
+
import { askThroughDialogs } from "./dialogs.ts";
|
|
21
|
+
import { type Answer, answerPairs, formatAnswers, formatDecline, type Question } from "./questions.ts";
|
|
18
22
|
import { applyWidgetKey, createWidgetState, decodeWidgetKey, renderWidget, type WidgetResult } from "./widget.ts";
|
|
23
|
+
import { panelUi } from "../lib/panel-focus.ts";
|
|
19
24
|
|
|
20
25
|
const NON_INTERACTIVE =
|
|
21
26
|
"This session has no interactive UI, so the user cannot be shown a dialog. Ask your question in your reply instead and stop, or proceed under a stated assumption.";
|
|
22
27
|
|
|
28
|
+
const AskParams = Type.Object({
|
|
29
|
+
questions: Type.Array(
|
|
30
|
+
Type.Object({
|
|
31
|
+
question: Type.String({ description: ASK_PARAMS.question }),
|
|
32
|
+
header: Type.String({ description: ASK_PARAMS.header }),
|
|
33
|
+
options: Type.Array(
|
|
34
|
+
Type.Object({
|
|
35
|
+
label: Type.String({ description: ASK_PARAMS.label }),
|
|
36
|
+
description: Type.Optional(Type.String({ description: ASK_PARAMS.optionDescription })),
|
|
37
|
+
preview: Type.Optional(Type.String({ description: ASK_PARAMS.preview })),
|
|
38
|
+
}),
|
|
39
|
+
{ minItems: 2, maxItems: 4, description: ASK_PARAMS.options },
|
|
40
|
+
),
|
|
41
|
+
multiSelect: Type.Optional(Type.Boolean({ description: ASK_PARAMS.multiSelect })),
|
|
42
|
+
}),
|
|
43
|
+
{ minItems: 1, maxItems: 4, description: ASK_PARAMS.questions },
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
|
|
23
47
|
export default function askUserExtension(pi: ExtensionAPI) {
|
|
24
|
-
|
|
48
|
+
// Claude Code's AskUserQuestion text, short or long by the model's tier
|
|
49
|
+
// (description.ts, lib/tool-variants.ts); registered again when the form changes.
|
|
50
|
+
const tool = defineTool({
|
|
25
51
|
name: "ask_user_question",
|
|
26
52
|
label: "Ask User",
|
|
27
53
|
...ccToolRenderers<{ questions?: Array<{ question?: string }> }>("Ask User", {
|
|
28
54
|
title: (a) => a?.questions?.[0]?.question,
|
|
29
55
|
}),
|
|
30
|
-
description:
|
|
31
|
-
"Ask the user to choose between options when you are blocked on a decision that is genuinely theirs — one you cannot resolve from the request, the code, or a sensible default. Ask up to four questions in one call; the user answers them in one tabbed dialog and can decline into a chat discussion instead. Options on single-select questions may carry a `preview` — an ASCII mockup, diagram, or code snippet rendered beside the list while the user compares choices; use previews when seeing the alternatives helps (UI layouts, code style variants), never for plain preference questions. Non-preview questions get a free-text 'Other' row automatically, so never add an 'Other' option yourself. If you recommend a specific option, make it the first in the list and end its label with '(Recommended)'. To switch into plan mode use enter_plan_mode (not this tool); once in plan mode, use this tool only to clarify requirements or choose between approaches before finalizing the plan. Do not use this tool for choices with an obvious default or for facts you can verify yourself.",
|
|
56
|
+
description: askDescription("short"),
|
|
32
57
|
promptSnippet: "Ask the user to decide between options when genuinely blocked",
|
|
33
|
-
parameters:
|
|
34
|
-
|
|
35
|
-
Type.Object({
|
|
36
|
-
question: Type.String({ description: "The question, ending in a question mark" }),
|
|
37
|
-
header: Type.String({ description: "Very short label for the question (a few words)" }),
|
|
38
|
-
multiSelect: Type.Optional(
|
|
39
|
-
Type.Boolean({ description: "Allow choosing several options (default false)" }),
|
|
40
|
-
),
|
|
41
|
-
options: Type.Array(
|
|
42
|
-
Type.Object({
|
|
43
|
-
label: Type.String({ description: "Short choice text" }),
|
|
44
|
-
description: Type.Optional(Type.String({ description: "What choosing this means" })),
|
|
45
|
-
preview: Type.Optional(
|
|
46
|
-
Type.String({
|
|
47
|
-
description:
|
|
48
|
-
"Preview shown beside the options while this option is focused: an ASCII mockup, diagram, or code snippet that helps the user compare choices. Single-select questions only.",
|
|
49
|
-
}),
|
|
50
|
-
),
|
|
51
|
-
}),
|
|
52
|
-
{ minItems: 2, maxItems: 4 },
|
|
53
|
-
),
|
|
54
|
-
}),
|
|
55
|
-
{ minItems: 1, maxItems: 4, description: "Questions to ask (1-4)" },
|
|
56
|
-
),
|
|
57
|
-
}),
|
|
58
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
58
|
+
parameters: AskParams,
|
|
59
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
59
60
|
if (!ctx.hasUI) {
|
|
60
61
|
return { content: [{ type: "text", text: NON_INTERACTIVE }], details: {}, isError: true };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const questions = params.questions as Question[];
|
|
64
|
-
|
|
65
|
+
// RPC has no custom UI (its custom() resolves undefined); ask through the
|
|
66
|
+
// select/input requests it forwards to the client instead (dialogs.ts).
|
|
67
|
+
const outcome = !canShowCustomUi(ctx) ? await askThroughDialogs(questions, ctx.ui, signal) : await panelUi(pi.events, ctx.ui).custom<WidgetResult>((tui, theme, _keybindings, done) => {
|
|
65
68
|
const style = {
|
|
66
69
|
paint: safeThemePaint(theme),
|
|
67
70
|
bold: safeThemeBold(theme),
|
|
@@ -94,7 +97,7 @@ export default function askUserExtension(pi: ExtensionAPI) {
|
|
|
94
97
|
const partial = outcome?.kind === "cancel" ? outcome.answers : [];
|
|
95
98
|
const text =
|
|
96
99
|
partial.length > 0
|
|
97
|
-
? `The user cancelled without submitting. They had made these selections before cancelling (NOT submitted — do not treat them as final answers)
|
|
100
|
+
? `The user cancelled without submitting. They had made these selections before cancelling (NOT submitted — do not treat them as final answers): ${answerPairs(partial)}.`
|
|
98
101
|
: "The user cancelled without answering.";
|
|
99
102
|
return {
|
|
100
103
|
content: [{ type: "text", text }],
|
|
@@ -115,4 +118,5 @@ export default function askUserExtension(pi: ExtensionAPI) {
|
|
|
115
118
|
};
|
|
116
119
|
},
|
|
117
120
|
});
|
|
121
|
+
registerFormTool(pi, tool, askDescription);
|
|
118
122
|
}
|
|
@@ -22,20 +22,45 @@ export interface Answer {
|
|
|
22
22
|
selected: string[];
|
|
23
23
|
/** True when the user typed their own answer instead of picking. */
|
|
24
24
|
freeform: boolean;
|
|
25
|
+
/** True when the answer includes text the user typed (the "Other" row), alone or beside picked options. */
|
|
26
|
+
typed?: boolean;
|
|
27
|
+
/** The preview of the single option picked, when it carries one. */
|
|
28
|
+
preview?: string;
|
|
25
29
|
/** Free-text notes the user attached to this answer. */
|
|
26
30
|
notes?: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Claude Code's answer list: `"question"="answer"` per answered question, with
|
|
35
|
+
* the picked option's preview and the user's notes, joined by ", ". A question
|
|
36
|
+
* left unanswered is listed only when it carries notes.
|
|
37
|
+
*/
|
|
38
|
+
export function answerPairs(answers: Answer[]): string {
|
|
31
39
|
return answers
|
|
32
40
|
.map((answer) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
41
|
+
const answered = answer.selected.length > 0;
|
|
42
|
+
if (!answered && !answer.notes) return undefined;
|
|
43
|
+
const parts = [answered ? `"${answer.question}"="${answer.selected.join(", ")}"` : `"${answer.question}"=(no option selected)`];
|
|
44
|
+
if (answer.preview) parts.push(`selected preview:\n${answer.preview}`);
|
|
45
|
+
if (answer.notes) parts.push(`notes: ${answer.notes}`);
|
|
46
|
+
return parts.join(" ");
|
|
37
47
|
})
|
|
38
|
-
.
|
|
48
|
+
.filter((pair): pair is string => pair !== undefined)
|
|
49
|
+
.join(", ");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ask_user_question's result, in Claude Code's words. Answers picked from the
|
|
54
|
+
* offered options are confirmed; an answer the user typed, or one with notes,
|
|
55
|
+
* may redirect the task, so the model is told to read it carefully.
|
|
56
|
+
*/
|
|
57
|
+
export function formatAnswers(answers: Answer[]): string {
|
|
58
|
+
const pairs = answerPairs(answers);
|
|
59
|
+
if (!pairs) return "The user did not answer the questions.";
|
|
60
|
+
const fromOptions = answers.every((answer) => !answer.notes && !answer.typed && !answer.freeform);
|
|
61
|
+
return fromOptions
|
|
62
|
+
? `Your questions have been answered: ${pairs}. You can now continue with these answers in mind.`
|
|
63
|
+
: `The user answered: ${pairs}. Read the answers carefully — they may request clarification, changes, or that you not proceed — and follow what they actually say.`;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
/** Tool result when the user picks "Chat about this" instead of answering. */
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* painting wherever it can along the way.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { cutPlainText, padPlainText, truncateLine, visibleWidth, wrapPlainText } from "../lib/tui-render.ts";
|
|
17
|
+
import { cutPlainText, padPlainText, truncateLine, visibleWidth, wrapHints, wrapPlainText } from "../lib/tui-render.ts";
|
|
18
18
|
import type { Answer, Question } from "./questions.ts";
|
|
19
|
+
import { keyId, keyText } from "../lib/key-input.ts";
|
|
19
20
|
|
|
20
21
|
export type Paint = (color: string, text: string) => string;
|
|
21
22
|
export type Bold = (text: string) => string;
|
|
@@ -124,35 +125,29 @@ export type WidgetKey =
|
|
|
124
125
|
| { kind: "text"; text: string };
|
|
125
126
|
|
|
126
127
|
export function decodeWidgetKey(data: string): WidgetKey | undefined {
|
|
127
|
-
switch (data) {
|
|
128
|
-
case "
|
|
129
|
-
case "\x1bOA":
|
|
128
|
+
switch (keyId(data)) {
|
|
129
|
+
case "up":
|
|
130
130
|
return { kind: "up" };
|
|
131
|
-
case "
|
|
132
|
-
case "\x1bOB":
|
|
131
|
+
case "down":
|
|
133
132
|
return { kind: "down" };
|
|
134
|
-
case "
|
|
135
|
-
case "
|
|
136
|
-
case "\x1b[Z": // shift+tab
|
|
133
|
+
case "left":
|
|
134
|
+
case "shift+tab":
|
|
137
135
|
return { kind: "prevTab" };
|
|
138
|
-
case "
|
|
139
|
-
case "
|
|
140
|
-
case "\t":
|
|
136
|
+
case "right":
|
|
137
|
+
case "tab":
|
|
141
138
|
return { kind: "nextTab" };
|
|
142
|
-
case "
|
|
143
|
-
case "\n":
|
|
139
|
+
case "enter":
|
|
144
140
|
return { kind: "enter" };
|
|
145
|
-
case "
|
|
146
|
-
case "
|
|
141
|
+
case "escape":
|
|
142
|
+
case "ctrl+c": // same intent as escape while the dialog is focused
|
|
147
143
|
return { kind: "esc" };
|
|
148
|
-
case "
|
|
149
|
-
case "
|
|
144
|
+
case "backspace":
|
|
145
|
+
case "ctrl+backspace":
|
|
150
146
|
return { kind: "backspace" };
|
|
151
147
|
default: {
|
|
152
|
-
// Unrecognized escape sequences (home/end/F-keys)
|
|
153
|
-
// tail characters into a text draft.
|
|
154
|
-
|
|
155
|
-
const text = [...data].filter((char) => char >= " " && char !== "\x7f").join("");
|
|
148
|
+
// Unrecognized escape sequences (home/end/F-keys) type nothing, so
|
|
149
|
+
// their tail characters never leak into a text draft.
|
|
150
|
+
const text = keyText(data);
|
|
156
151
|
return text ? { kind: "text", text } : undefined;
|
|
157
152
|
}
|
|
158
153
|
}
|
|
@@ -204,6 +199,12 @@ export function applyWidgetKey(state: WidgetState, key: WidgetKey): WidgetResult
|
|
|
204
199
|
case "enter":
|
|
205
200
|
return activateRow(state, rows[questionState.cursor]);
|
|
206
201
|
case "text": {
|
|
202
|
+
// The free-text row is an input: typing on it starts the answer, as in
|
|
203
|
+
// Claude Code, instead of being dropped until Enter opens it.
|
|
204
|
+
if (rows[questionState.cursor]?.kind === "other") {
|
|
205
|
+
state.editing = { field: "other", draft: questionState.otherText + key.text };
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
207
208
|
if (key.text === "n" && hasPreviews(question)) {
|
|
208
209
|
state.editing = { field: "notes", draft: questionState.notes };
|
|
209
210
|
return;
|
|
@@ -303,11 +304,14 @@ function advance(state: WidgetState): void {
|
|
|
303
304
|
function collectAnswer(question: Question, state: QuestionState): Answer {
|
|
304
305
|
const labels = [...state.selected].sort((a, b) => a - b).map((index) => question.options[index].label);
|
|
305
306
|
if (state.otherChosen) labels.push(state.otherText);
|
|
307
|
+
const picked = !question.multiSelect && state.selected.length === 1 && !state.otherChosen ? question.options[state.selected[0]] : undefined;
|
|
306
308
|
return {
|
|
307
309
|
question: question.question,
|
|
308
310
|
header: question.header,
|
|
309
311
|
selected: labels,
|
|
310
312
|
freeform: labels.length > 0 && state.selected.length === 0,
|
|
313
|
+
...(state.otherChosen ? { typed: true } : {}),
|
|
314
|
+
...(picked?.preview?.trim() ? { preview: picked.preview } : {}),
|
|
311
315
|
notes: state.notes || undefined,
|
|
312
316
|
};
|
|
313
317
|
}
|
|
@@ -333,7 +337,9 @@ export function renderWidget(state: WidgetState, style: WidgetStyle, width: numb
|
|
|
333
337
|
if (state.tab === state.questions.length) renderSubmitTab(state, paint, width, out);
|
|
334
338
|
else renderQuestionTab(state, style, width, out);
|
|
335
339
|
out.push("");
|
|
336
|
-
|
|
340
|
+
// The key hints wrap at their separators on a narrow terminal, so "Esc to
|
|
341
|
+
// cancel" is never the part cut off.
|
|
342
|
+
for (const line of wrapHints(footerFor(state), Math.max(1, width - 1))) out.push(paint("dim", line));
|
|
337
343
|
// Final ANSI-aware guard: pi-tui crashes the app on an overwide line, and
|
|
338
344
|
// the painted tab bar can exceed narrow widths.
|
|
339
345
|
return out.map((line) => truncateLine(line, width));
|
|
@@ -440,7 +446,9 @@ function renderStackedRows(state: WidgetState, style: WidgetStyle, width: number
|
|
|
440
446
|
const editing = state.editing?.field === "other";
|
|
441
447
|
const check = question.multiSelect ? (questionState.otherChosen ? "[✔] " : "[ ] ") : "";
|
|
442
448
|
if (editing) {
|
|
443
|
-
|
|
449
|
+
// A long answer wraps under its row while typing; nothing is cut.
|
|
450
|
+
const prefix = `❯ ${number} ${check}`;
|
|
451
|
+
for (const line of wrapUnder(prefix, `${state.editing?.draft ?? ""}▏`, width)) out.push(paint("accent", line));
|
|
444
452
|
} else if (questionState.otherText) {
|
|
445
453
|
out.push(labelRow(`${pointer} ${number} ${check}`, questionState.otherText, focused || questionState.otherChosen));
|
|
446
454
|
} else {
|
|
@@ -505,7 +513,7 @@ function renderPreviewRows(state: WidgetState, style: WidgetStyle, width: number
|
|
|
505
513
|
const notesIndent = " ".repeat(sideBySide ? leftWidth : 2);
|
|
506
514
|
out.push("");
|
|
507
515
|
if (state.editing?.field === "notes") {
|
|
508
|
-
out.push(`${notesIndent}${paint("accent", "Notes: ")}
|
|
516
|
+
out.push(...wrapUnder(`${notesIndent}Notes: `, `${state.editing.draft}▏`, width, `${notesIndent}${paint("accent", "Notes: ")}`));
|
|
509
517
|
} else if (questionState.notes) {
|
|
510
518
|
out.push(`${notesIndent}${paint("accent", "Notes: ")}${cut(questionState.notes)}`);
|
|
511
519
|
} else {
|
|
@@ -516,6 +524,18 @@ function renderPreviewRows(state: WidgetState, style: WidgetStyle, width: number
|
|
|
516
524
|
pushChatRow(state, rows, style, width, out);
|
|
517
525
|
}
|
|
518
526
|
|
|
527
|
+
/**
|
|
528
|
+
* `prefix` then `text`, wrapped to the row width with continuation lines
|
|
529
|
+
* indented under the text, so a typed answer shows whole. `painted` replaces
|
|
530
|
+
* the plain prefix on the first line (same width, with colour).
|
|
531
|
+
*/
|
|
532
|
+
function wrapUnder(prefix: string, text: string, width: number, painted = prefix): string[] {
|
|
533
|
+
const indent = visibleWidth(prefix);
|
|
534
|
+
const room = Math.max(4, width - 1 - indent);
|
|
535
|
+
const wrapped = wrapPlainText(text, room);
|
|
536
|
+
return wrapped.map((line, i) => (i === 0 ? painted : " ".repeat(indent)) + line);
|
|
537
|
+
}
|
|
538
|
+
|
|
519
539
|
function previewBox(preview: string, maxOuter: number, paint: Paint): string[] {
|
|
520
540
|
const inner = Math.max(8, maxOuter - 4);
|
|
521
541
|
const wrapped = wrapPlainText(preview.replace(/\t/g, " "), inner);
|