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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's `SubagentHandback`: the tool every child session (an Agent
|
|
3
|
+
* run, a fork, a workflow agent) returns its result through, and the closing
|
|
4
|
+
* reminder that tells it so (decisions/subagents-workflows.md, "Every child
|
|
5
|
+
* hands back through SubagentHandback"). Claude Code gives it to Opus and
|
|
6
|
+
* Sonnet children only; One Code gives it to every child on every model.
|
|
7
|
+
*
|
|
8
|
+
* The tool records the report in the run's `HandbackSlot` and ends the run
|
|
9
|
+
* (`terminate`, so no further request goes out after a batch of hand-backs).
|
|
10
|
+
* The runner then reports the recorded text as the child's result, and falls
|
|
11
|
+
* back to the child's final message when the tool was never called.
|
|
12
|
+
*
|
|
13
|
+
* The tool keeps Claude Code's exact name, like `Agent` and `SendMessage`,
|
|
14
|
+
* because the harness frames that carry its report already name it ("its
|
|
15
|
+
* SubagentHandback call", lib/notifications.ts). It is harness plumbing: the
|
|
16
|
+
* child permission and hook gates never gate it, and an agent file's `tools`
|
|
17
|
+
* allowlist keeps it.
|
|
18
|
+
*
|
|
19
|
+
* In a fork the tool is deferred: a fork sends the parent's tool list so it
|
|
20
|
+
* reads the parent's prompt cache (subagents/fork-cache.ts), and an eager tool
|
|
21
|
+
* of its own would be dropped from that list. A deferred one reaches the
|
|
22
|
+
* request through a `tool_search` load, as the fork's `SendMessage` does.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { Type } from "typebox";
|
|
27
|
+
import { DEFER_CHANNEL } from "./deferred.ts";
|
|
28
|
+
import { REMINDER_CHANNEL, type ReminderPayload } from "./reminders.ts";
|
|
29
|
+
|
|
30
|
+
export const SUBAGENT_HANDBACK = "SubagentHandback";
|
|
31
|
+
|
|
32
|
+
/** Claude Code's description of the tool. */
|
|
33
|
+
export const SUBAGENT_HANDBACK_DESCRIPTION =
|
|
34
|
+
"Deliver your final report to the agent that spawned you (your caller). Use it once, for that hand-off only: when your work is complete, call SubagentHandback({message: <your full report>}). The call ends your run, so do everything else first and put everything your caller needs in that one report. It is not a messaging channel: do not use it for progress updates or questions.\n\nOnly a report delivered through SubagentHandback reaches your caller; plain text you write at the end of your run is NOT delivered. There is no recipient parameter: the report can only go to your caller.";
|
|
35
|
+
|
|
36
|
+
/** Claude Code's closing reminder on the child's task message. */
|
|
37
|
+
export const SUBAGENT_HANDBACK_REMINDER =
|
|
38
|
+
"Your final report is delivered through SubagentHandback: when your work is complete, call SubagentHandback({message: <your full report>}). The call ends your run, so make it your last step. Only a SubagentHandback call reaches your caller as your result; plain text you write at the end is not delivered.";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One Code's line before the reminder in a fork, where the tool is deferred
|
|
42
|
+
* (the fork sends its parent's tool list): it names the load step, which the
|
|
43
|
+
* frozen deferred listing it inherits does not.
|
|
44
|
+
*/
|
|
45
|
+
export const SUBAGENT_HANDBACK_LOAD_FIRST =
|
|
46
|
+
'SubagentHandback is deferred here: load it first with tool_search (query "select:SubagentHandback").';
|
|
47
|
+
|
|
48
|
+
/** Claude Code's result texts for the call. */
|
|
49
|
+
export const HANDBACK_DELIVERED = "Report delivered to your caller.";
|
|
50
|
+
export const HANDBACK_ALREADY_DELIVERED =
|
|
51
|
+
"Nothing was sent: your report was already delivered (SubagentHandback delivers one report). Use SendMessage for anything further, then stop.";
|
|
52
|
+
export const HANDBACK_EMPTY = "message must not be empty: pass your full report as `message`.";
|
|
53
|
+
|
|
54
|
+
/** Where a run keeps the report its child handed back. */
|
|
55
|
+
export interface HandbackSlot {
|
|
56
|
+
/** Record the report; false when this turn already delivered one. */
|
|
57
|
+
recordHandback(message: string): boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A slot for a one-turn run (a workflow agent): the first report wins. */
|
|
61
|
+
export function oneTurnHandbackSlot(): HandbackSlot & { report?: string } {
|
|
62
|
+
const slot: HandbackSlot & { report?: string } = {
|
|
63
|
+
report: undefined,
|
|
64
|
+
recordHandback(message) {
|
|
65
|
+
if (slot.report !== undefined) return false;
|
|
66
|
+
slot.report = message;
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return slot;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function subagentHandbackTool(slot: HandbackSlot): ToolDefinition {
|
|
74
|
+
return {
|
|
75
|
+
name: SUBAGENT_HANDBACK,
|
|
76
|
+
label: "Hand back",
|
|
77
|
+
description: SUBAGENT_HANDBACK_DESCRIPTION,
|
|
78
|
+
parameters: Type.Object({ message: Type.String({ description: "Your full report for your caller" }) }, { additionalProperties: false }) as never,
|
|
79
|
+
async execute(_toolCallId: string, params: unknown) {
|
|
80
|
+
const message = (params as { message?: unknown } | undefined)?.message;
|
|
81
|
+
// Fail loud (decisions/tools.md): an empty report would end the run with nothing for the caller.
|
|
82
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
83
|
+
return { content: [{ type: "text" as const, text: HANDBACK_EMPTY }], details: {}, isError: true };
|
|
84
|
+
}
|
|
85
|
+
if (!slot.recordHandback(message)) {
|
|
86
|
+
return { content: [{ type: "text" as const, text: HANDBACK_ALREADY_DELIVERED }], details: {}, isError: true };
|
|
87
|
+
}
|
|
88
|
+
return { content: [{ type: "text" as const, text: HANDBACK_DELIVERED }], details: { handback: true }, terminate: true };
|
|
89
|
+
},
|
|
90
|
+
} as ToolDefinition;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The inline extension a child session loads for the hand-back: the tool,
|
|
95
|
+
* deferred in a fork, and the closing reminder. The reminder is a standing
|
|
96
|
+
* block (`sticky-append`) switched on at session start, so it rides the
|
|
97
|
+
* child's task message and every later message of the run (a resident's
|
|
98
|
+
* next turns hand back too), and never an inherited message of a fork.
|
|
99
|
+
*/
|
|
100
|
+
export function subagentHandbackExtension(slot: HandbackSlot, options: { deferred?: boolean } = {}) {
|
|
101
|
+
return (pi: ExtensionAPI) => {
|
|
102
|
+
pi.registerTool(subagentHandbackTool(slot));
|
|
103
|
+
if (options.deferred) pi.events.emit(DEFER_CHANNEL, { name: SUBAGENT_HANDBACK, keywords: ["handback", "report", "result", "final", "caller"] });
|
|
104
|
+
pi.on("session_start", () => {
|
|
105
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
106
|
+
text: options.deferred ? `${SUBAGENT_HANDBACK_LOAD_FIRST}\n${SUBAGENT_HANDBACK_REMINDER}` : SUBAGENT_HANDBACK_REMINDER,
|
|
107
|
+
scope: "every-turn",
|
|
108
|
+
key: "subagent-handback",
|
|
109
|
+
placement: "sticky-append",
|
|
110
|
+
} satisfies ReminderPayload);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which models get Claude Code's mid-conversation `role: "system"` message,
|
|
3
|
+
* and in which wire shape (pure). One function for the system-prompt builder,
|
|
4
|
+
* the reminder hook and the tool hook, so all three agree on every request.
|
|
5
|
+
*
|
|
6
|
+
* The gate is pi's model catalog flag `compat.supportsMidConvoSystemMessages`
|
|
7
|
+
* alone, no list of our own: pi owns the per-model facts and adds models as it
|
|
8
|
+
* confirms them (`working-docs/decisions/tools.md`, "Claude Code's request
|
|
9
|
+
* shape, on every model pi flags for it"). Haiku before 5 is never in it:
|
|
10
|
+
* Anthropic rejects the role there (findings §52); Haiku 5.5 takes it
|
|
11
|
+
* (findings §67). Every unflagged model keeps the context in the first user
|
|
12
|
+
* message.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseClaudeId } from "./claude-id.ts";
|
|
16
|
+
|
|
17
|
+
/** The wire shape a system message takes: Anthropic `system`, OpenAI Responses `input`, or Chat Completions `messages`. */
|
|
18
|
+
export type SystemRoleLayout = "anthropic" | "responses" | "completions";
|
|
19
|
+
|
|
20
|
+
/** The fields of a pi `Model` the gate reads. */
|
|
21
|
+
export interface SystemRoleModel {
|
|
22
|
+
id: string;
|
|
23
|
+
api: string;
|
|
24
|
+
compat?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** pi-ai's Responses APIs: requests carry `input` items and flat `{ type, name }` tools. */
|
|
28
|
+
export const RESPONSES_APIS: ReadonlySet<string> = new Set(["openai-responses", "azure-openai-responses", "openai-codex-responses"]);
|
|
29
|
+
|
|
30
|
+
/** The wire shape of a pi API, for the three shapes One Code rewrites; undefined for the rest (Google, Bedrock). */
|
|
31
|
+
export function wireShape(api: string): SystemRoleLayout | undefined {
|
|
32
|
+
if (api === "anthropic-messages") return "anthropic";
|
|
33
|
+
if (RESPONSES_APIS.has(api)) return "responses";
|
|
34
|
+
if (api === "openai-completions") return "completions";
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The payload field that holds the conversation for a wire shape. */
|
|
39
|
+
export function messagesKey(shape: SystemRoleLayout): "input" | "messages" {
|
|
40
|
+
return shape === "responses" ? "input" : "messages";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function systemRoleLayout(model: SystemRoleModel | undefined): SystemRoleLayout | undefined {
|
|
44
|
+
if (!model) return undefined;
|
|
45
|
+
const compat = model.compat as { supportsMidConvoSystemMessages?: unknown } | undefined;
|
|
46
|
+
if (compat?.supportsMidConvoSystemMessages !== true) return undefined;
|
|
47
|
+
if (model.api === "anthropic-messages" && isHaikuBeforeFive(model.id)) return undefined;
|
|
48
|
+
return wireShape(model.api);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A Haiku older than Haiku 5 in any provider's spelling; the old `claude-3-5-haiku` order parses as none. */
|
|
52
|
+
function isHaikuBeforeFive(id: string): boolean {
|
|
53
|
+
const claude = parseClaudeId(id);
|
|
54
|
+
return claude ? claude.family === "haiku" && claude.major < 5 : id.includes("haiku");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One context block that moves: its text as injected on the user message, and its text inside the system message. */
|
|
58
|
+
export interface MovedBlock {
|
|
59
|
+
/** The block exactly as the user message carries it (`<system-reminder>` frame and suffix included). */
|
|
60
|
+
framed: string;
|
|
61
|
+
/** The unwrapped text, as Claude Code's system message joins it. */
|
|
62
|
+
inner: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The role a system message takes on the OpenAI APIs: pi's own rule per API
|
|
67
|
+
* (`developer` for a reasoning model that accepts it, else `system`).
|
|
68
|
+
*/
|
|
69
|
+
export function instructionRole(layout: SystemRoleLayout, model: { reasoning?: boolean; compat?: unknown }): "system" | "developer" {
|
|
70
|
+
if (layout === "anthropic") return "system";
|
|
71
|
+
const developer = (model.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole;
|
|
72
|
+
const accepts = layout === "responses" ? developer !== false : developer === true;
|
|
73
|
+
return model.reasoning && accepts ? "developer" : "system";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A wire message and content part, as far as the payload rewrites read them. */
|
|
77
|
+
export type WireMessage = { role?: unknown; type?: unknown; content?: unknown; output?: unknown; call_id?: unknown; tool_call_id?: unknown };
|
|
78
|
+
export type WirePart = { type?: unknown; text?: unknown; content?: unknown; tool_use_id?: unknown; cache_control?: unknown };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The parts with those `drop` accepts removed, keeping pi's cache mark: when a
|
|
82
|
+
* removed part carried it, it moves to the new last part. Undefined when every
|
|
83
|
+
* part would go (a message cannot be left empty) or nothing is removed.
|
|
84
|
+
*/
|
|
85
|
+
export function withoutParts(parts: readonly WirePart[], drop: (part: WirePart, index: number) => boolean): WirePart[] | undefined {
|
|
86
|
+
let mark: unknown;
|
|
87
|
+
const kept = parts.filter((part, index) => {
|
|
88
|
+
if (!drop(part, index)) return true;
|
|
89
|
+
if (part?.cache_control !== undefined) mark = part.cache_control;
|
|
90
|
+
return false;
|
|
91
|
+
});
|
|
92
|
+
if (kept.length === parts.length || kept.length === 0) return undefined;
|
|
93
|
+
if (mark === undefined || kept[kept.length - 1].cache_control !== undefined) return kept;
|
|
94
|
+
return [...kept.slice(0, -1), { ...kept[kept.length - 1], cache_control: mark }];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The value stored under a pi call id for the id a request carries: the same
|
|
99
|
+
* id, the Responses call id before pi's `|item` suffix, or the id pi-ai's
|
|
100
|
+
* Anthropic conversion rewrote (any other character to `_`, at most 64), as it
|
|
101
|
+
* does for a call made on another provider before a model switch.
|
|
102
|
+
*/
|
|
103
|
+
export function byWireCallId<V>(stored: ReadonlyMap<string, V>, wireId: string): V | undefined {
|
|
104
|
+
if (stored.has(wireId)) return stored.get(wireId);
|
|
105
|
+
for (const [id, value] of stored) {
|
|
106
|
+
if (id.split("|")[0] === wireId || id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) === wireId) return value;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A system message with `text` in the API's own shape. */
|
|
112
|
+
export function systemMessage(layout: SystemRoleLayout, role: "system" | "developer", text: string): WireMessage {
|
|
113
|
+
return layout === "anthropic" ? { role: "system", content: [{ type: "text", text }] } : { role, content: text };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The payload with the moved context blocks lifted off the user message that
|
|
118
|
+
* carries them and joined, unwrapped and a blank line apart, into one system
|
|
119
|
+
* message placed right after it, in the API's own shape (Claude Code's layout,
|
|
120
|
+
* `decisions/tools.md`). Blocks are found by their exact text, never by
|
|
121
|
+
* position: a fork's request drops the parent's first message, and then nothing
|
|
122
|
+
* moves. The cache mark is reseated afterwards (anthropic-payload.ts
|
|
123
|
+
* reseatMessageMark). Undefined when nothing changes.
|
|
124
|
+
*/
|
|
125
|
+
export function withSystemRoleContext(
|
|
126
|
+
payload: Record<string, unknown>,
|
|
127
|
+
layout: SystemRoleLayout,
|
|
128
|
+
moved: readonly MovedBlock[],
|
|
129
|
+
role: "system" | "developer",
|
|
130
|
+
): Record<string, unknown> | undefined {
|
|
131
|
+
if (moved.length === 0) return undefined;
|
|
132
|
+
const key = messagesKey(layout);
|
|
133
|
+
const messages = payload[key];
|
|
134
|
+
if (!Array.isArray(messages)) return undefined;
|
|
135
|
+
const wanted = new Set(moved.map((block) => block.framed));
|
|
136
|
+
const index = messages.findIndex(
|
|
137
|
+
(message: WireMessage) => message?.role === "user" && Array.isArray(message.content) && (message.content as WirePart[]).some((part) => wanted.has(part?.text as string)),
|
|
138
|
+
);
|
|
139
|
+
if (index === -1) return undefined;
|
|
140
|
+
|
|
141
|
+
const carrier = messages[index] as WireMessage & { content: WirePart[] };
|
|
142
|
+
const found = new Set<string>();
|
|
143
|
+
const kept = carrier.content.filter((part) => {
|
|
144
|
+
const text = part?.text;
|
|
145
|
+
if (typeof text !== "string" || !wanted.has(text) || found.has(text)) return true;
|
|
146
|
+
found.add(text);
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
const text = moved
|
|
150
|
+
.filter((block) => found.has(block.framed))
|
|
151
|
+
.map((block) => block.inner)
|
|
152
|
+
.join("\n\n");
|
|
153
|
+
|
|
154
|
+
const user: WireMessage = { ...carrier, content: kept };
|
|
155
|
+
return { ...payload, [key]: [...messages.slice(0, index), user, systemMessage(layout, role, text), ...messages.slice(index + 1)] };
|
|
156
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control characters in text that reaches the terminal (pure).
|
|
3
|
+
*
|
|
4
|
+
* Tool results, tool arguments, notification bodies, MCP results, fetched
|
|
5
|
+
* pages and the session title all carry text One Code did not write. Sent to
|
|
6
|
+
* the terminal raw, an escape sequence in it acts: OSC 0 retitles the
|
|
7
|
+
* terminal, OSC 52 rewrites the clipboard in kitty, WezTerm, Alacritty and
|
|
8
|
+
* Windows Terminal, cursor movement rewrites the rows above, SGR conceal hides
|
|
9
|
+
* text, and a carriage return overwrites the start of the row.
|
|
10
|
+
*
|
|
11
|
+
* `sanitizeDisplayText` applies pi's own rule for a tool result
|
|
12
|
+
* (`render-utils.js getTextOutput`: `stripAnsi`, then `sanitizeBinaryOutput`,
|
|
13
|
+
* then drop `\r`), and also drops C1 controls, which some terminals act on.
|
|
14
|
+
* It keeps `\n` and `\t` and never touches styling One Code adds afterwards,
|
|
15
|
+
* because it runs on the text before painting. `escapeControlText` shows each
|
|
16
|
+
* control character as a visible `\xHH` escape instead, for consent dialogs,
|
|
17
|
+
* where the user must see exactly what would run. `sanitizeTitle` is for the
|
|
18
|
+
* one-line terminal title.
|
|
19
|
+
*
|
|
20
|
+
* Display only: the model-facing text is never passed through these.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// pi's `ansiRegex` (utils/ansi.js, from chalk's ansi-regex) matches an OSC up
|
|
24
|
+
// to the first ST, else CSI and the related ESC forms. Its OSC branch is a lazy
|
|
25
|
+
// scan to the end of the text from every `ESC ]`, quadratic on many
|
|
26
|
+
// unterminated ones (50,000 took 1.8 s), so `stripAnsi` walks the text once
|
|
27
|
+
// with the same alternation and remembers where no terminator is left.
|
|
28
|
+
const CSI_AT = /[\u001B\u009B][[\]()#;?]*(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]/y;
|
|
29
|
+
|
|
30
|
+
/** The index just past the first string terminator (BEL, `ESC \`, or C1 ST) at or after `from`, else -1. */
|
|
31
|
+
function stringTerminatorEnd(text: string, from: number): number {
|
|
32
|
+
for (let i = from; i < text.length; i++) {
|
|
33
|
+
const ch = text.charCodeAt(i);
|
|
34
|
+
if (ch === 0x07 || ch === 0x9c) return i + 1;
|
|
35
|
+
if (ch === 0x1b && text.charCodeAt(i + 1) === 0x5c) return i + 2;
|
|
36
|
+
}
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `text.replace(ansiRegex, "")` in linear time: the same matches, left to right. */
|
|
41
|
+
function stripAnsi(text: string): string {
|
|
42
|
+
let out = "";
|
|
43
|
+
let from = 0;
|
|
44
|
+
// No string terminator exists at or after this index (a failed search holds for every later start).
|
|
45
|
+
let noTerminatorFrom = Number.POSITIVE_INFINITY;
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < text.length) {
|
|
48
|
+
const ch = text.charCodeAt(i);
|
|
49
|
+
if (ch !== 0x1b && ch !== 0x9b) {
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === 0x1b && text.charCodeAt(i + 1) === 0x5d && i + 2 < noTerminatorFrom) {
|
|
54
|
+
const end = stringTerminatorEnd(text, i + 2);
|
|
55
|
+
if (end !== -1) {
|
|
56
|
+
out += text.slice(from, i);
|
|
57
|
+
from = i = end;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
noTerminatorFrom = i + 2;
|
|
61
|
+
}
|
|
62
|
+
CSI_AT.lastIndex = i;
|
|
63
|
+
const match = CSI_AT.exec(text);
|
|
64
|
+
if (match) {
|
|
65
|
+
out += text.slice(from, i);
|
|
66
|
+
from = i = i + match[0].length;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
return out + text.slice(from);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** C0 controls other than tab and newline, DEL, C1 controls, and U+FFF9 to U+FFFB (interlinear annotation). */
|
|
75
|
+
const UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f-]/g;
|
|
76
|
+
const HAS_UNSAFE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f-]/;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Text safe to write to the terminal: escape sequences removed, then every
|
|
80
|
+
* control character other than `\n` and `\t` (carriage returns included, as
|
|
81
|
+
* pi does). Plain text comes back unchanged (same string).
|
|
82
|
+
*/
|
|
83
|
+
export function sanitizeDisplayText(text: string): string {
|
|
84
|
+
if (!HAS_UNSAFE.test(text)) return text;
|
|
85
|
+
return stripAnsi(text).replace(UNSAFE, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `label` as an OSC 8 hyperlink to `url` (pi-tui's `hyperlink`). Terminals
|
|
90
|
+
* without OSC 8, and tmux without its `hyperlinks` feature, show the label
|
|
91
|
+
* alone, so the label must read on its own. Both parts are sanitized, so
|
|
92
|
+
* neither can end the sequence early.
|
|
93
|
+
*/
|
|
94
|
+
export function terminalLink(label: string, url: string): string {
|
|
95
|
+
return `\x1b]8;;${sanitizeDisplayText(url)}\x1b\\${sanitizeDisplayText(label)}\x1b]8;;\x1b\\`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Every control character other than `\n` and `\t` shown as a visible
|
|
100
|
+
* `\xHH` escape (`\x1b[8m` for an SGR conceal), so nothing in the text can
|
|
101
|
+
* act on the terminal or hide part of itself.
|
|
102
|
+
*/
|
|
103
|
+
export function escapeControlText(text: string): string {
|
|
104
|
+
if (!HAS_UNSAFE.test(text)) return text;
|
|
105
|
+
return text.replace(UNSAFE, (ch) => {
|
|
106
|
+
const code = ch.charCodeAt(0);
|
|
107
|
+
return code > 0xff ? `\\u${code.toString(16).padStart(4, "0")}` : `\\x${code.toString(16).padStart(2, "0")}`;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One line for the terminal title: escape sequences and every control character removed, whitespace collapsed. */
|
|
112
|
+
export function sanitizeTitle(text: string): string {
|
|
113
|
+
return sanitizeDisplayText(text).replace(/\s+/g, " ").trim();
|
|
114
|
+
}
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
* 2-vs-1 decision is byte-identical to pi's without importing pi or adding a
|
|
18
18
|
* package. Grapheme segmentation and the mark/emoji classification use V8's
|
|
19
19
|
* built-in `Intl.Segmenter` and Unicode property regexes.
|
|
20
|
+
*
|
|
21
|
+
* Synced with pi-tui 0.99.2. `text-width.test.ts` compares this measure with
|
|
22
|
+
* pi-tui's over every code point to U+3FFFF and over random strings with
|
|
23
|
+
* escapes, so a pi bump that changes pi-tui's measure fails the suite.
|
|
20
24
|
*/
|
|
21
25
|
|
|
22
26
|
// --- East Asian wide/fullwidth lookup (get-east-asian-width v1.6.0, Unicode 16) ---
|
|
@@ -127,6 +131,7 @@ export function graphemeWidth(segment: string): number {
|
|
|
127
131
|
} else if (!nonPrintingCharRegex.test(char)) {
|
|
128
132
|
const c = char.codePointAt(0)!;
|
|
129
133
|
if (followsMark || (c >= 0xff00 && c <= 0xffef)) width += eastAsianWidth(c);
|
|
134
|
+
else if (c === 0x0e33 || c === 0x0eb3) width += 1; // Thai SARA AM, Lao AM
|
|
130
135
|
followsMark = false;
|
|
131
136
|
}
|
|
132
137
|
}
|
|
@@ -134,36 +139,35 @@ export function graphemeWidth(segment: string): number {
|
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
// --- ANSI / OSC / APC escape handling ---
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
142
|
+
// The length of the terminal control sequence starting at index `i`, or 0.
|
|
143
|
+
// Mirrors pi-tui's `extractAnsiCode` exactly, because pi-tui validates a line
|
|
144
|
+
// with it: a CSI runs to the first `m`, `G`, `K`, `H` or `J`; an OSC or APC
|
|
145
|
+
// runs to BEL or ST; an unterminated sequence, or any other ESC form, is not
|
|
146
|
+
// a sequence, so its bytes count as text. A broader rule here measured such
|
|
147
|
+
// text narrower than pi-tui and could emit a line pi-tui rejects. Display text
|
|
148
|
+
// never carries those forms once `sanitizeDisplayText` has run.
|
|
140
149
|
function escapeAt(s: string, i: number): number {
|
|
141
150
|
if (s.charCodeAt(i) !== 0x1b) return 0;
|
|
142
151
|
const next = s[i + 1];
|
|
143
152
|
if (next === "[") {
|
|
144
|
-
// CSI: ESC [ ... final byte 0x40–0x7E
|
|
145
153
|
let j = i + 2;
|
|
146
|
-
while (j < s.length)
|
|
147
|
-
|
|
148
|
-
j++;
|
|
149
|
-
if (c >= 0x40 && c <= 0x7e) return j - i;
|
|
150
|
-
}
|
|
151
|
-
return s.length - i;
|
|
154
|
+
while (j < s.length && !CSI_END.test(s[j])) j++;
|
|
155
|
+
return j < s.length ? j + 1 - i : 0;
|
|
152
156
|
}
|
|
153
|
-
if (next === "]" || next === "_"
|
|
154
|
-
// OSC / APC / DCS / PM: ESC <intro> ... terminated by BEL or ST (ESC \)
|
|
157
|
+
if (next === "]" || next === "_") {
|
|
155
158
|
let j = i + 2;
|
|
156
159
|
while (j < s.length) {
|
|
157
160
|
if (s.charCodeAt(j) === 0x07) return j + 1 - i; // BEL
|
|
158
161
|
if (s.charCodeAt(j) === 0x1b && s[j + 1] === "\\") return j + 2 - i; // ST
|
|
159
162
|
j++;
|
|
160
163
|
}
|
|
161
|
-
return
|
|
164
|
+
return 0;
|
|
162
165
|
}
|
|
163
|
-
|
|
164
|
-
return next ? 2 : 1;
|
|
166
|
+
return 0;
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
const CSI_END = /[mGKHJ]/;
|
|
170
|
+
|
|
167
171
|
const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
168
172
|
|
|
169
173
|
/**
|
|
@@ -174,6 +178,9 @@ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
|
174
178
|
export function visibleWidth(text: string): number {
|
|
175
179
|
if (text.length === 0) return 0;
|
|
176
180
|
if (PRINTABLE_ASCII.test(text)) return text.length;
|
|
181
|
+
// pi-tui expands tabs before segmenting, so a mark after a tab joins the
|
|
182
|
+
// last space's cluster; doing the same keeps the two measures identical.
|
|
183
|
+
if (text.includes("\t")) text = text.replace(/\t/g, " ");
|
|
177
184
|
let clean = "";
|
|
178
185
|
for (let i = 0; i < text.length; ) {
|
|
179
186
|
const esc = escapeAt(text, i);
|
|
@@ -204,6 +211,29 @@ export function sliceColumns(text: string, maxCols: number): { text: string; wid
|
|
|
204
211
|
return fitPainted(text, maxCols);
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Longest suffix of PLAIN (escape-free) text whose visible width is ≤
|
|
216
|
+
* `maxCols`, cut on a grapheme boundary: the counterpart of `sliceColumns` for
|
|
217
|
+
* text that keeps its tail, such as a path whose deepest folders matter most.
|
|
218
|
+
*/
|
|
219
|
+
export function tailColumns(text: string, maxCols: number): { text: string; width: number } {
|
|
220
|
+
if (maxCols <= 0) return { text: "", width: 0 };
|
|
221
|
+
if (PRINTABLE_ASCII.test(text)) {
|
|
222
|
+
const tail = text.slice(Math.max(0, text.length - maxCols));
|
|
223
|
+
return { text: tail, width: tail.length };
|
|
224
|
+
}
|
|
225
|
+
const graphemes = [...graphemeSegmenter.segment(text)].map((s) => s.segment);
|
|
226
|
+
let width = 0;
|
|
227
|
+
let start = graphemes.length;
|
|
228
|
+
while (start > 0) {
|
|
229
|
+
const w = graphemeWidth(graphemes[start - 1]);
|
|
230
|
+
if (width + w > maxCols) break;
|
|
231
|
+
width += w;
|
|
232
|
+
start--;
|
|
233
|
+
}
|
|
234
|
+
return { text: graphemes.slice(start).join(""), width };
|
|
235
|
+
}
|
|
236
|
+
|
|
207
237
|
/**
|
|
208
238
|
* Hard-wrap text into chunks each ≤ `columns` visible columns, on grapheme
|
|
209
239
|
* boundaries. A single glyph wider than the whole budget occupies its own chunk
|
|
@@ -253,7 +283,8 @@ export function fitPainted(line: string, maxCols: number): { text: string; width
|
|
|
253
283
|
continue;
|
|
254
284
|
}
|
|
255
285
|
// Consume the visible run up to the next escape, segment it into graphemes.
|
|
256
|
-
|
|
286
|
+
// An ESC that starts no sequence is text (width 0), so the run starts past it.
|
|
287
|
+
let end = line.charCodeAt(i) === 0x1b ? i + 1 : i;
|
|
257
288
|
while (end < line.length && line.charCodeAt(end) !== 0x1b) end++;
|
|
258
289
|
for (const { segment } of graphemeSegmenter.segment(line.slice(i, end))) {
|
|
259
290
|
const w = graphemeWidth(segment);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An injectable timer (pure), so an idle-timer state machine is unit-tested
|
|
3
|
+
* with fake timers, and the real adapter both recap and idle-compact run on.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TimerOps {
|
|
7
|
+
/** Schedule `cb` after `ms`; return a handle for `clear`. */
|
|
8
|
+
set(cb: () => void, ms: number): unknown;
|
|
9
|
+
clear(handle: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `setTimeout` with the handle unref'd, so a pending idle timer never keeps a one-shot process alive. */
|
|
13
|
+
export const unrefTimers: TimerOps = {
|
|
14
|
+
set: (cb, ms) => {
|
|
15
|
+
const handle = setTimeout(cb, ms);
|
|
16
|
+
handle.unref?.();
|
|
17
|
+
return handle;
|
|
18
|
+
},
|
|
19
|
+
clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
20
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's `tool_addition` for tools that arrive mid-session (pure). One
|
|
3
|
+
* Code stores the arrival as the deferred-tool addendum one-shot
|
|
4
|
+
* (lib/deferred.ts deferredAddendumText), which reads correctly on any model.
|
|
5
|
+
* On a first-party model that takes mid-conversation tool changes, the
|
|
6
|
+
* addendum leaves the message that carries it for a system message right
|
|
7
|
+
* after it: Claude Code's sentence and names, then one reference-form
|
|
8
|
+
* `tool_addition` block per tool, each declared deferred in `tools`. The model
|
|
9
|
+
* can then call the tool without a `tool_search` round trip (findings §55
|
|
10
|
+
* P1). Elsewhere the addendum stays as it is.
|
|
11
|
+
*
|
|
12
|
+
* An addendum is matched as a whole block, never inside other text: in a tool
|
|
13
|
+
* result at `context` time, while the result's blocks are still separate and
|
|
14
|
+
* keyed by its call id (`liftAddenda`), and on a user message as a whole part
|
|
15
|
+
* (a one-shot pinned there).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { MID_CONVERSATION_SYSTEM_BETA, MID_CONVERSATION_TOOL_CHANGES_BETA, reseatMessageMark } from "./anthropic-payload.ts";
|
|
19
|
+
import { byWireCallId, systemRoleLayout, type WireMessage, type WirePart, withoutParts } from "./system-role.ts";
|
|
20
|
+
|
|
21
|
+
/** The first line of the stored addendum; the names follow, one per line. */
|
|
22
|
+
export const ADDENDUM_HEAD =
|
|
23
|
+
"Additional deferred tools became available via tool_search since this conversation started (they registered after the first request). Same rules: load with tool_search \"select:<name>\" before calling.";
|
|
24
|
+
|
|
25
|
+
/** Claude Code's sentence before the names, in its tool-addition message. */
|
|
26
|
+
export const TOOLS_AVAILABLE = "The following tools just became available and are ready to use:";
|
|
27
|
+
|
|
28
|
+
/** Claude Code's betas for a reference-form tool addition in a system message. */
|
|
29
|
+
export const TOOL_ADDITION_BETAS = [MID_CONVERSATION_SYSTEM_BETA, MID_CONVERSATION_TOOL_CHANGES_BETA];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the model takes Claude Code's reference-form `tool_addition`:
|
|
33
|
+
* first-party Anthropic on the system-role layout, with pi's catalog flag for
|
|
34
|
+
* mid-conversation tool changes (Opus 5.5, Sonnet 5.5 and Fable 5.1 among them).
|
|
35
|
+
*/
|
|
36
|
+
export function supportsToolAdditions(model: { id?: string; provider?: string; api?: string; compat?: unknown } | undefined): boolean {
|
|
37
|
+
if (!model || model.provider !== "anthropic" || systemRoleLayout({ id: model.id ?? "", api: model.api ?? "", compat: model.compat }) !== "anthropic") return false;
|
|
38
|
+
return (model.compat as { supportsMidConvoToolChanges?: boolean } | undefined)?.supportsMidConvoToolChanges === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const OPEN = `<system-reminder>\n${ADDENDUM_HEAD}\n`;
|
|
42
|
+
const CLOSE = "\n</system-reminder>";
|
|
43
|
+
|
|
44
|
+
/** The names a whole framed addendum block lists; undefined for any other text. */
|
|
45
|
+
export function addendumNames(text: string): string[] | undefined {
|
|
46
|
+
if (!text.startsWith(OPEN) || !text.endsWith(CLOSE)) return undefined;
|
|
47
|
+
return text
|
|
48
|
+
.slice(OPEN.length, -CLOSE.length)
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter((line) => line.length > 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type StoredBlock = { type?: string; text?: string };
|
|
54
|
+
type StoredMessage = { role: string; toolCallId?: string; content?: unknown };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The context-time step: each tool result's addendum block leaves the result,
|
|
58
|
+
* its names kept under the call id, for the payload step to send as an
|
|
59
|
+
* addition after that result. Returns the input when nothing changed.
|
|
60
|
+
*/
|
|
61
|
+
export function liftAddenda<T extends StoredMessage>(messages: T[]): { messages: T[]; byCall: Map<string, string[]> } {
|
|
62
|
+
const byCall = new Map<string, string[]>();
|
|
63
|
+
let changed = false;
|
|
64
|
+
const out = messages.map((message) => {
|
|
65
|
+
if (message.role !== "toolResult" || !Array.isArray(message.content) || !message.toolCallId) return message;
|
|
66
|
+
const names: string[] = [];
|
|
67
|
+
const kept = (message.content as StoredBlock[]).filter((block) => {
|
|
68
|
+
const found = block?.type === "text" && typeof block.text === "string" ? addendumNames(block.text) : undefined;
|
|
69
|
+
if (found) names.push(...found);
|
|
70
|
+
return !found;
|
|
71
|
+
});
|
|
72
|
+
if (names.length === 0) return message;
|
|
73
|
+
changed = true;
|
|
74
|
+
byCall.set(message.toolCallId, names);
|
|
75
|
+
return { ...message, content: kept };
|
|
76
|
+
});
|
|
77
|
+
return { messages: changed ? out : messages, byCall };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The Anthropic payload with a tool-addition system message after each user
|
|
82
|
+
* message that answers a call `liftAddenda` lifted an addendum from, or that
|
|
83
|
+
* carries a pinned addendum part (removed). Also returns every name added: they
|
|
84
|
+
* must be declared deferred in `tools`. With `references: false` the message
|
|
85
|
+
* is Claude Code's sentence and names alone, for a request whose tools cannot
|
|
86
|
+
* be referenced (OAuth renames them): the tools are active, so the model can
|
|
87
|
+
* still call them. Undefined when there is none.
|
|
88
|
+
*/
|
|
89
|
+
export function withToolAdditions(
|
|
90
|
+
payload: Record<string, unknown>,
|
|
91
|
+
byCall: ReadonlyMap<string, readonly string[]>,
|
|
92
|
+
references = true,
|
|
93
|
+
): { payload: Record<string, unknown>; names: string[] } | undefined {
|
|
94
|
+
const messages = payload.messages;
|
|
95
|
+
if (!Array.isArray(messages)) return undefined;
|
|
96
|
+
const out: WireMessage[] = [];
|
|
97
|
+
const all = new Set<string>();
|
|
98
|
+
for (const message of messages as WireMessage[]) {
|
|
99
|
+
if (message?.role !== "user" || !Array.isArray(message.content)) {
|
|
100
|
+
out.push(message);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const names: string[] = [];
|
|
104
|
+
const parts = message.content as WirePart[];
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
const called = part?.type === "tool_result" && typeof part.tool_use_id === "string" ? byWireCallId(byCall, part.tool_use_id) : undefined;
|
|
107
|
+
if (called) names.push(...called);
|
|
108
|
+
}
|
|
109
|
+
const pinnedNames = (part: WirePart) => (part?.type === "text" && typeof part.text === "string" ? addendumNames(part.text) : undefined);
|
|
110
|
+
// A pinned addendum part goes, keeping pi's cache mark; a message it would empty keeps it.
|
|
111
|
+
const kept = withoutParts(parts, (part) => pinnedNames(part) !== undefined) ?? parts;
|
|
112
|
+
if (kept !== parts) for (const part of parts) names.push(...(pinnedNames(part) ?? []));
|
|
113
|
+
if (names.length === 0) {
|
|
114
|
+
out.push(message);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const unique = [...new Set(names)];
|
|
118
|
+
for (const name of unique) all.add(name);
|
|
119
|
+
out.push(kept.length === parts.length ? message : { ...message, content: kept }, {
|
|
120
|
+
role: "system",
|
|
121
|
+
content: [
|
|
122
|
+
{ type: "text", text: [TOOLS_AVAILABLE, ...unique].join("\n") },
|
|
123
|
+
...(references ? unique.map((name) => ({ type: "tool_addition", tool: { type: "tool_reference", name } })) : []),
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (all.size === 0) return undefined;
|
|
128
|
+
return { payload: { ...payload, messages: reseatMessageMark(out) }, names: [...all] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A stored message as far as the addendum scan is concerned. */
|
|
132
|
+
type BranchEntry = { type?: string; message?: { role?: string; content?: unknown } };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The names every addendum persisted into a tool result on the branch
|
|
136
|
+
* announced: on resume they are re-activated, since their addition blocks
|
|
137
|
+
* are rebuilt from the same stored blocks and the model may call them.
|
|
138
|
+
*/
|
|
139
|
+
export function addendumNamesOnBranch(branch: readonly BranchEntry[]): string[] {
|
|
140
|
+
const names = new Set<string>();
|
|
141
|
+
for (const entry of branch) {
|
|
142
|
+
const message = entry?.type === "message" ? entry.message : undefined;
|
|
143
|
+
if (message?.role !== "toolResult" || !Array.isArray(message.content)) continue;
|
|
144
|
+
for (const block of message.content as StoredBlock[]) {
|
|
145
|
+
for (const name of (typeof block?.text === "string" ? addendumNames(block.text) : undefined) ?? []) names.add(name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...names];
|
|
149
|
+
}
|