one-code-extension 0.1.4 → 0.3.1
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/LICENSE +20 -0
- package/README.md +222 -177
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +128 -0
- package/docs/guide/configuration.md +173 -0
- package/docs/guide/differences-from-claude-code.md +152 -0
- package/docs/guide/doctor.md +88 -0
- package/docs/guide/hooks.md +111 -0
- package/docs/guide/installation.md +126 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
- package/docs/guide/providers-and-models.md +196 -0
- package/docs/guide/reference.md +226 -0
- package/docs/guide/sessions-and-context.md +192 -0
- package/docs/guide/skills-plugins-and-mcp.md +178 -0
- package/docs/guide/subagents-and-workflows.md +195 -0
- package/docs/guide/tasks-and-background-work.md +113 -0
- package/docs/guide/terminal-interface.md +157 -0
- package/docs/guide/tools.md +177 -0
- package/docs/guide/troubleshooting.md +177 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -16,15 +16,30 @@
|
|
|
16
16
|
* child is spawned with a concrete `provider/id`:
|
|
17
17
|
*
|
|
18
18
|
* - Claude Code aliases (`sonnet`/`opus`/`haiku`/`fable`) resolve **within the
|
|
19
|
-
* session's provider** (and contained route/model family on gateways)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
19
|
+
* session's provider** (and contained route/model family on gateways): by
|
|
20
|
+
* name where a model carries it, else as the Claude Code *tier* the alias
|
|
21
|
+
* names — `haiku` the cheapest capable contained model, `sonnet` the cheapest
|
|
22
|
+
* workhorse-class one, `opus`/`fable` the session model — and the parent says
|
|
23
|
+
* so (2026-09-11; before, an off-family alias landed on the session model,
|
|
24
|
+
* the most expensive answer available, and a global "use Sonnet for
|
|
25
|
+
* subagents" instruction did exactly that on every non-Anthropic provider).
|
|
26
|
+
* - An exact per-call reference (the main model's `model` field) or the user's
|
|
27
|
+
* own `subagentModel` setting resolves anywhere — naming a model is choosing
|
|
28
|
+
* it — but crossing providers is announced, never silent.
|
|
29
|
+
* - An `.claude/agents` model field is a Claude Code convention: honored on a
|
|
30
|
+
* Claude-family session, but on any other provider a cross-provider one is
|
|
31
|
+
* *not* honored (a fork inherits the parent transcript, so subagents stay on
|
|
32
|
+
* the session's provider) — the configured default or the automatic
|
|
33
|
+
* same-provider profile serves instead.
|
|
34
|
+
* - A configured default or agent-file model that cannot resolve degrades, with
|
|
35
|
+
* a notice naming the knob, to the automatic same-provider pick — the
|
|
36
|
+
* cheapest contained model at the session's capability floor, the SAME floor
|
|
37
|
+
* the auto-mode classifier applies (`capableContainedCandidates`) — and only
|
|
38
|
+
* then to the session model. Only an explicit `inherit` names the session
|
|
39
|
+
* model outright. A bad *per-call* request first falls through to the agent's
|
|
40
|
+
* own model and the configured default (running the subagent on its intended
|
|
41
|
+
* model, with a notice); only when nothing downstream resolves does it error,
|
|
42
|
+
* so the model can read the menu and retry.
|
|
28
43
|
*
|
|
29
44
|
* The menu keeps the main model informed without dumping a 300-model gateway
|
|
30
45
|
* catalog into every request: vendor-contained, variant- and unpriced-filtered,
|
|
@@ -33,16 +48,19 @@
|
|
|
33
48
|
*/
|
|
34
49
|
|
|
35
50
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
51
|
+
import type { SubagentDefault } from "./default-model.ts";
|
|
36
52
|
import {
|
|
37
|
-
|
|
53
|
+
crossesProvider,
|
|
38
54
|
findConfigured,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
isClaudeFamilyModel,
|
|
56
|
+
isDatedDuplicate,
|
|
57
|
+
isSnapshotDatedId,
|
|
58
|
+
isStaleContainmentStamp,
|
|
43
59
|
modelsContainedToSession,
|
|
60
|
+
modelSpec as spec,
|
|
44
61
|
pricedInput,
|
|
45
62
|
} from "../lib/model-policy.ts";
|
|
63
|
+
import { atLeastTier, capableContainedCandidates, economicalContainedCandidates, intrinsicTier, type PromptTier } from "../lib/model-tier.ts";
|
|
46
64
|
|
|
47
65
|
/**
|
|
48
66
|
* Cross-extension channel carrying the resolved default subagent model, so the
|
|
@@ -82,8 +100,13 @@ export function subagentStatusModel(
|
|
|
82
100
|
return { model: spec(resolution.model), via };
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Claude Code's Agent-tool aliases, read as the tiers they name: `haiku` the
|
|
105
|
+
* cheap line, `sonnet` the workhorse line, `opus`/`fable` the frontier. An alias
|
|
106
|
+
* resolves by NAME within the session's provider family when a model carries it
|
|
107
|
+
* (an Anthropic session), else as its tier there — see `resolveAlias`.
|
|
108
|
+
*/
|
|
109
|
+
const ALIAS_TIER: Record<string, PromptTier> = { haiku: "cheap", sonnet: "workhorse", opus: "frontier", fable: "frontier" };
|
|
87
110
|
|
|
88
111
|
export type SubagentModelSource = "call" | "agent" | "default" | "automatic" | "session";
|
|
89
112
|
|
|
@@ -92,8 +115,14 @@ export interface ResolveInput {
|
|
|
92
115
|
requested?: string;
|
|
93
116
|
/** The agent definition's frontmatter `model`. */
|
|
94
117
|
agentModel?: string;
|
|
95
|
-
/**
|
|
96
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The `subagentModel` setting / `CLAUDE_CODE_SUBAGENT_MODEL`, with its knob
|
|
120
|
+
* (`source`) and, for the setting, the session provider it was stamped for
|
|
121
|
+
* (`setForProvider`). A cross-provider setting stamped for a provider since
|
|
122
|
+
* left — or unstamped (hand-edited) — is stale and skipped so a same-provider
|
|
123
|
+
* model runs; see the resolve loop.
|
|
124
|
+
*/
|
|
125
|
+
configuredDefault?: SubagentDefault;
|
|
97
126
|
sessionModel?: Model<Api>;
|
|
98
127
|
available: Model<Api>[];
|
|
99
128
|
}
|
|
@@ -111,28 +140,81 @@ export interface SubagentModelResolution {
|
|
|
111
140
|
notices: string[];
|
|
112
141
|
}
|
|
113
142
|
|
|
114
|
-
|
|
143
|
+
/**
|
|
144
|
+
* The model-facing form of a resolution's notices, for the spawn tool's result.
|
|
145
|
+
*
|
|
146
|
+
* Every notice above says what was NOT honored ("falling back to the agent's
|
|
147
|
+
* configured model or the session default"); none of them names where the walk
|
|
148
|
+
* actually landed, because it is written before the chain finishes. The user
|
|
149
|
+
* can see the answer in the banner, but the model that asked for `sonnet` could
|
|
150
|
+
* not: on a non-Anthropic session — where a global "use Sonnet for subagents"
|
|
151
|
+
* memory shared with Claude Code makes that the standing request — it spawned
|
|
152
|
+
* children believing they ran on Sonnet and reasoned about their cost and
|
|
153
|
+
* capability on that basis (WEAK-MODEL-REVIEW-2026-09-06 L4).
|
|
154
|
+
*
|
|
155
|
+
* So the concrete spec is appended whenever there is a notice at all. It is
|
|
156
|
+
* stated rather than inferred even when a notice already mentions that spec:
|
|
157
|
+
* the notices name models in every position — the session ("no sonnet model in
|
|
158
|
+
* this session's provider family (openai/gpt-5.1)"), the one refused, the one
|
|
159
|
+
* honored — and a reader cannot tell which mention is the answer.
|
|
160
|
+
*
|
|
161
|
+
* The source rides along because it is the question the answer raises. The
|
|
162
|
+
* notices used to end by listing which knob might serve next; naming the one
|
|
163
|
+
* that actually did is the same information after the fact, and true.
|
|
164
|
+
*/
|
|
165
|
+
const SOURCE_LABEL: Record<SubagentModelSource, string> = {
|
|
166
|
+
call: "the model this call named",
|
|
167
|
+
agent: "the agent's own model",
|
|
168
|
+
default: "the configured default",
|
|
169
|
+
automatic: "the automatic smaller default",
|
|
170
|
+
session: "this session's model",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export function subagentModelNotes(resolution: Pick<SubagentModelResolution, "model" | "notices" | "source">): string[] {
|
|
174
|
+
if (resolution.notices.length === 0) return [];
|
|
175
|
+
if (!resolution.model) return [...resolution.notices];
|
|
176
|
+
return [...resolution.notices, `This subagent runs on ${spec(resolution.model)} (${SOURCE_LABEL[resolution.source]}).`];
|
|
177
|
+
}
|
|
115
178
|
|
|
116
|
-
|
|
179
|
+
interface AliasResolution {
|
|
180
|
+
model: Model<Api>;
|
|
181
|
+
/** "name": a contained model carries the alias; "tier": the alias's Claude Code tier, resolved here. */
|
|
182
|
+
how: "name" | "tier";
|
|
183
|
+
}
|
|
117
184
|
|
|
118
185
|
/**
|
|
119
|
-
* Resolve a Claude Code alias
|
|
120
|
-
* undated alias ids
|
|
121
|
-
*
|
|
122
|
-
*
|
|
186
|
+
* Resolve a Claude Code alias within the session's provider family: by name
|
|
187
|
+
* first (preferring undated alias ids, then the newest id), else as the tier
|
|
188
|
+
* the alias names — the cheapest contained model at or above it
|
|
189
|
+
* (`economicalContainedCandidates` order: never tiny, prior-generation or
|
|
190
|
+
* tool-less; cheap → workhorse → frontier, then price), `frontier` meaning the
|
|
191
|
+
* session model itself, the strongest model the user chose on this provider.
|
|
192
|
+
* Never off-family: "sonnet" on a Codex session is the cheapest workhorse-class
|
|
193
|
+
* Codex model, not Anthropic's Sonnet. The aliases ARE Claude Code's tier
|
|
194
|
+
* names, so the tier reading honors what the alias meant rather than
|
|
195
|
+
* substituting a model nobody described. Undefined only when the provider has
|
|
196
|
+
* nothing capable to offer (unpriced/opaque) or there is no session model.
|
|
123
197
|
*/
|
|
124
|
-
function resolveAlias(
|
|
198
|
+
function resolveAlias(
|
|
199
|
+
alias: string,
|
|
200
|
+
contained: Model<Api>[],
|
|
201
|
+
available: Model<Api>[],
|
|
202
|
+
sessionModel: Model<Api> | undefined,
|
|
203
|
+
): AliasResolution | undefined {
|
|
125
204
|
const matches = contained.filter((model) => model.id.toLowerCase().includes(alias));
|
|
126
|
-
if (matches.length
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
205
|
+
if (matches.length > 0) {
|
|
206
|
+
const undated = matches.filter((model) => !isSnapshotDatedId(model.id));
|
|
207
|
+
const pool = undated.length > 0 ? undated : matches;
|
|
208
|
+
return { model: pool.sort((a, b) => b.id.localeCompare(a.id))[0], how: "name" };
|
|
209
|
+
}
|
|
210
|
+
if (!sessionModel) return undefined;
|
|
211
|
+
const tier = ALIAS_TIER[alias];
|
|
212
|
+
if (tier === "frontier") return { model: sessionModel, how: "tier" };
|
|
213
|
+
// Name-class only, deliberately: an explicit alias asks for a *class* of model,
|
|
214
|
+
// so a measured pass (which lets a strong flash serve the automatic default)
|
|
215
|
+
// does not lift a lean-named model into "sonnet" here.
|
|
216
|
+
const byTier = economicalContainedCandidates(available, sessionModel, contained).find((model) => atLeastTier(intrinsicTier(model), tier));
|
|
217
|
+
return byTier ? { model: byTier, how: "tier" } : undefined;
|
|
136
218
|
}
|
|
137
219
|
|
|
138
220
|
export function resolveSubagentModel(input: ResolveInput): SubagentModelResolution {
|
|
@@ -143,11 +225,23 @@ export function resolveSubagentModel(input: ResolveInput): SubagentModelResoluti
|
|
|
143
225
|
if (input.requested) chain.push({ value: input.requested, source: "call", knob: "the model field" });
|
|
144
226
|
if (input.agentModel) chain.push({ value: input.agentModel, source: "agent", knob: "the agent file's model" });
|
|
145
227
|
if (input.configuredDefault) {
|
|
146
|
-
chain.push({ value: input.configuredDefault, source: "default", knob: "the configured subagent default" });
|
|
228
|
+
chain.push({ value: input.configuredDefault.spec, source: "default", knob: "the configured subagent default" });
|
|
147
229
|
}
|
|
148
230
|
|
|
149
231
|
const contained = sessionModel ? modelsContainedToSession(available, sessionModel) : [];
|
|
150
232
|
let suppressAutomatic = false;
|
|
233
|
+
// A per-call `model` that did not resolve — deferred, not errored (see the
|
|
234
|
+
// "per-call request" paragraph in the module doc above).
|
|
235
|
+
let callFailed: string | undefined;
|
|
236
|
+
|
|
237
|
+
// The `subagentModel` setting is stale when it was stamped for a different
|
|
238
|
+
// provider than this session (or never stamped — a hand-edited setting): the
|
|
239
|
+
// user switched the session's provider since setting it, so a cross-provider
|
|
240
|
+
// value is not honored and a same-provider model runs instead. A deliberate
|
|
241
|
+
// choice made on this provider (stamp matches) is still honored + announced.
|
|
242
|
+
const settingIsStale =
|
|
243
|
+
input.configuredDefault?.source === "subagentModel setting" &&
|
|
244
|
+
isStaleContainmentStamp(input.configuredDefault.setForContainment, sessionModel);
|
|
151
245
|
|
|
152
246
|
for (const entry of chain) {
|
|
153
247
|
const wanted = entry.value.trim();
|
|
@@ -157,19 +251,57 @@ export function resolveSubagentModel(input: ResolveInput): SubagentModelResoluti
|
|
|
157
251
|
}
|
|
158
252
|
|
|
159
253
|
const alias = wanted.toLowerCase();
|
|
160
|
-
if (
|
|
161
|
-
const resolved = resolveAlias(alias, contained);
|
|
162
|
-
if (resolved) return { model: resolved, source: entry.source, notices };
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
254
|
+
if (alias in ALIAS_TIER) {
|
|
255
|
+
const resolved = resolveAlias(alias, contained, available, sessionModel);
|
|
256
|
+
if (resolved?.how === "name") return { model: resolved.model, source: entry.source, notices };
|
|
257
|
+
// SCOPE the claim. "No sonnet model exists" is false at machine level
|
|
258
|
+
// whenever Anthropic is also authenticated, and a model relaying it tells
|
|
259
|
+
// the user "Sonnet is not available" — wrong, and the everyday case given
|
|
260
|
+
// a global "use Sonnet for subagents" memory on another provider. What is
|
|
261
|
+
// true is narrower: the alias cannot leave this session's containment.
|
|
262
|
+
// "Provider family" covers both a plain provider and a gateway, where
|
|
263
|
+
// containment is the model-creator namespace (`modelsContainedToSession`).
|
|
264
|
+
const family = sessionModel ? ` (${spec(sessionModel)})` : "";
|
|
265
|
+
if (resolved) {
|
|
266
|
+
// The tier reading: what the alias meant, on this provider. Stated so
|
|
267
|
+
// the model that asked for "sonnet" knows which model actually runs
|
|
268
|
+
// (WEAK-MODEL-REVIEW-2026-09-06 L4) — `subagentModelNotes` repeats the
|
|
269
|
+
// spec, but this line says WHY it is not Sonnet.
|
|
270
|
+
notices.push(`No "${alias}" model in this session's provider family${family}; read as its Claude Code tier, "${alias}" resolves to ${spec(resolved.model)} here.`);
|
|
271
|
+
return { model: resolved.model, source: entry.source, notices };
|
|
272
|
+
}
|
|
273
|
+
// Nothing capable contained at all (an unpriced/opaque provider, or no
|
|
274
|
+
// session model): keep walking the chain — the agent's model / configured
|
|
275
|
+
// default may still resolve — then the automatic pick or the session
|
|
276
|
+
// model serve below. Never `unresolved`: unlike a literal typo, an
|
|
277
|
+
// off-family alias is a naming mismatch, not a retryable mistake.
|
|
278
|
+
notices.push(`No "${alias}" model in this session's provider family${family}.`);
|
|
279
|
+
continue;
|
|
168
280
|
}
|
|
169
281
|
|
|
170
282
|
const resolved = findConfigured(available, wanted);
|
|
171
283
|
if (resolved) {
|
|
172
284
|
if (sessionModel && crossesProvider(resolved, sessionModel)) {
|
|
285
|
+
// An `.claude/agents` model is a Claude Code convention; on a
|
|
286
|
+
// non-Claude session it does not get to move a subagent (which
|
|
287
|
+
// inherits the parent transcript) onto another provider. Skip it and
|
|
288
|
+
// let the configured default or the automatic same-provider profile
|
|
289
|
+
// serve. Per-call `model` and the user's own `subagentModel` setting
|
|
290
|
+
// are explicit choices, so their crossings are still honored + announced.
|
|
291
|
+
if (entry.source === "agent" && !isClaudeFamilyModel(sessionModel)) {
|
|
292
|
+
notices.push(
|
|
293
|
+
`The agent file's model ${spec(resolved)} is a different provider than this session (${spec(sessionModel)}); ` +
|
|
294
|
+
"on a non-Claude session subagents stay on this provider, so a same-provider model runs this subagent instead.",
|
|
295
|
+
);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (entry.source === "default" && settingIsStale) {
|
|
299
|
+
notices.push(
|
|
300
|
+
`The configured subagent default ${spec(resolved)} was set for a different provider than this session (${spec(sessionModel)}); ` +
|
|
301
|
+
"a same-provider model runs this subagent instead. Re-set it with /subagent on this session to use it here.",
|
|
302
|
+
);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
173
305
|
notices.push(
|
|
174
306
|
`Subagent model ${spec(resolved)} is a different provider/family than this session (${spec(sessionModel)}) — it was named via ${entry.knob}, so it is honored.`,
|
|
175
307
|
);
|
|
@@ -178,45 +310,53 @@ export function resolveSubagentModel(input: ResolveInput): SubagentModelResoluti
|
|
|
178
310
|
}
|
|
179
311
|
|
|
180
312
|
if (entry.source === "call") {
|
|
181
|
-
//
|
|
182
|
-
|
|
313
|
+
// Defer to the rest of the chain (see the module doc): record the
|
|
314
|
+
// failure and keep walking, erroring after the loop only if nothing
|
|
315
|
+
// downstream resolves.
|
|
316
|
+
callFailed = wanted;
|
|
317
|
+
notices.push(
|
|
318
|
+
`Requested subagent model "${wanted}" is not available — falling back to the agent's configured model or the session default.`,
|
|
319
|
+
);
|
|
320
|
+
continue;
|
|
183
321
|
}
|
|
184
|
-
// An explicit choice failed
|
|
185
|
-
//
|
|
186
|
-
|
|
322
|
+
// An explicit choice failed: the rest of the chain, then the automatic
|
|
323
|
+
// same-provider pick, then the session model serve. The automatic pick is
|
|
324
|
+
// no more "a model nobody described" than the session model is, and it is
|
|
325
|
+
// the cheaper of the two (2026-09-11; before, this dropped straight to the
|
|
326
|
+
// session model).
|
|
187
327
|
notices.push(
|
|
188
328
|
entry.source === "agent"
|
|
189
|
-
? `Subagent model "${wanted}" (from ${entry.knob}) is not available — falling back to the configured default or the session model.`
|
|
190
|
-
: `Subagent model "${wanted}" (from ${entry.knob}) is not available — the session model runs this subagent instead.`,
|
|
329
|
+
? `Subagent model "${wanted}" (from ${entry.knob}) is not available — falling back to the configured default, the automatic same-provider pick, or the session model.`
|
|
330
|
+
: `Subagent model "${wanted}" (from ${entry.knob}) is not available — the automatic same-provider pick or the session model runs this subagent instead.`,
|
|
191
331
|
);
|
|
192
332
|
}
|
|
193
333
|
|
|
334
|
+
// Per-call model failed and nothing downstream resolved: surface it so the
|
|
335
|
+
// caller shows the menu and the model retries, rather than silently dropping
|
|
336
|
+
// to the automatic/session pick for a model the main model named.
|
|
337
|
+
if (callFailed) return { source: "call", unresolved: callFailed, notices };
|
|
338
|
+
|
|
194
339
|
// Automatic selection is a cost optimisation, so it needs price evidence:
|
|
195
|
-
// with
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
.filter((entry) => spec(entry.model) !== spec(sessionModel))
|
|
216
|
-
.filter((entry) => entry.price < sessionPrice && hasSmallModelName(entry.model))
|
|
217
|
-
.sort((a, b) => a.rank - b.rank || a.price - b.price)[0]?.model;
|
|
218
|
-
if (cheaper) return { model: cheaper, source: "automatic", notices };
|
|
219
|
-
}
|
|
340
|
+
// with the session price unknown there is no demonstrable saving, and picking
|
|
341
|
+
// a cheap-tier model could silently *upgrade* an (unpriced) cheap session. The
|
|
342
|
+
// floor-gated selector — the cheapest same-provider model at the session's
|
|
343
|
+
// capability floor (workhorse-or-better for a workhorse-or-better session,
|
|
344
|
+
// Claude Code's min(main, sonnet); never `tiny`) — is the SAME one the
|
|
345
|
+
// auto-mode classifier uses, so a session screens and delegates on one
|
|
346
|
+
// model. A delegated worker writes code and calls tools for many turns; a
|
|
347
|
+
// weaker one spends the saving on retries (2026-09-11 — before, subagents
|
|
348
|
+
// took the cheapest capable model with no floor). It excludes unpriced/opaque
|
|
349
|
+
// providers by construction, which subsumes the old dynamic-selection gate;
|
|
350
|
+
// those degrade to the session model below.
|
|
351
|
+
if (sessionModel && !suppressAutomatic) {
|
|
352
|
+
// `strict` requires a genuinely cheaper model and yields nothing when the
|
|
353
|
+
// session price is unknown, so a cheap-tier pick never silently upgrades an
|
|
354
|
+
// unpriced session. Reuse the `contained` set computed above for the hot path.
|
|
355
|
+
// With an Artificial Analysis snapshot (lib/capability-index.ts) the floor
|
|
356
|
+
// is measured — coding index ≥ min(session, Sonnet 5), no tolerance —
|
|
357
|
+
// and passers rank by price; unscored candidates are judged by name-class tier.
|
|
358
|
+
const cheaper = capableContainedCandidates(available, sessionModel, "subagent", { strict: true, contained })[0];
|
|
359
|
+
if (cheaper) return { model: cheaper, source: "automatic", notices };
|
|
220
360
|
}
|
|
221
361
|
|
|
222
362
|
if (sessionModel) return { model: sessionModel, source: "session", notices };
|
|
@@ -259,6 +399,13 @@ export interface MenuOptions {
|
|
|
259
399
|
defaultSource?: SubagentModelSource;
|
|
260
400
|
/** How many cheaper-option lines to include. */
|
|
261
401
|
maxCheaper?: number;
|
|
402
|
+
/**
|
|
403
|
+
* The configured default and which knob set it, so the reminder can tell the
|
|
404
|
+
* main model when the user has *manually* pinned the subagent model via the
|
|
405
|
+
* `subagentModel` setting (as opposed to Claude Code's env var or automatic
|
|
406
|
+
* selection). Only the setting triggers the notice.
|
|
407
|
+
*/
|
|
408
|
+
configured?: SubagentDefault;
|
|
262
409
|
}
|
|
263
410
|
|
|
264
411
|
const price = (model: Model<Api>): string => {
|
|
@@ -297,11 +444,7 @@ export function subagentModelMenu({ available, sessionModel, defaultModel, defau
|
|
|
297
444
|
.map((model) => ({ model, input: pricedInput(model) }))
|
|
298
445
|
.filter((entry): entry is { model: Model<Api>; input: number } => entry.input !== undefined)
|
|
299
446
|
// Collapse dated duplicates when the undated alias is also present.
|
|
300
|
-
.filter(
|
|
301
|
-
(entry) =>
|
|
302
|
-
!isDated(entry.model.id) ||
|
|
303
|
-
!contained.some((other) => other.id !== entry.model.id && entry.model.id.startsWith(other.id)),
|
|
304
|
-
)
|
|
447
|
+
.filter((entry) => !isDatedDuplicate(entry.model, contained))
|
|
305
448
|
.sort((a, b) => a.input - b.input);
|
|
306
449
|
const sessionPrice = pricedInput(sessionModel);
|
|
307
450
|
for (const entry of priced) {
|
|
@@ -332,15 +475,42 @@ export function defaultCostsMoreWarning({ sessionModel, defaultModel }: MenuOpti
|
|
|
332
475
|
);
|
|
333
476
|
}
|
|
334
477
|
|
|
478
|
+
/**
|
|
479
|
+
* One line informing the main model when the user has manually pinned the
|
|
480
|
+
* subagent default via the `subagentModel` setting — so the model knows what
|
|
481
|
+
* subagents run on, and (when that model is a different provider than the
|
|
482
|
+
* session) that it can keep a subagent's inherited transcript on this provider
|
|
483
|
+
* by passing a listed same-provider model. Silent for the automatic default and
|
|
484
|
+
* for Claude Code's env var, which are not the user's manual per-harness choice.
|
|
485
|
+
*/
|
|
486
|
+
export function settingOverrideNotice({ sessionModel, defaultModel, defaultSource, configured }: MenuOptions): string | undefined {
|
|
487
|
+
// Only when the setting is actually in effect: a stale cross-provider setting
|
|
488
|
+
// that was overridden resolves to an automatic/session model (defaultSource
|
|
489
|
+
// != "default"), and its user-facing "set for a different provider" warning
|
|
490
|
+
// already explains the override — no "runs on it" line for the model then.
|
|
491
|
+
if (configured?.source !== "subagentModel setting" || defaultSource !== "default" || !defaultModel) return undefined;
|
|
492
|
+
const cross = sessionModel && crossesProvider(defaultModel, sessionModel);
|
|
493
|
+
return (
|
|
494
|
+
`The user pinned the subagent default to ${spec(defaultModel)} via the subagentModel setting; ` +
|
|
495
|
+
"subagents run on it unless you pass a different model in the `model` field" +
|
|
496
|
+
(cross
|
|
497
|
+
? ` — it is a different provider than this session (${spec(sessionModel as Model<Api>)}), so a subagent's inherited transcript goes to that provider; pass a listed same-provider model to keep it here.`
|
|
498
|
+
: ".")
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
335
502
|
/** The every-turn reminder body. Kept short: its tokens are paid on every call. */
|
|
336
503
|
export function subagentModelsReminder(options: MenuOptions): string {
|
|
337
504
|
const menu = subagentModelMenu(options);
|
|
505
|
+
const setting = settingOverrideNotice(options);
|
|
338
506
|
const warning = defaultCostsMoreWarning(options);
|
|
339
507
|
return [
|
|
340
508
|
"Models for the subagent/workflow `model` field (omit it to use the default; set the default with /subagent):",
|
|
341
509
|
...menu,
|
|
510
|
+
...(setting ? [setting] : []),
|
|
342
511
|
...(warning ? [warning] : []),
|
|
343
|
-
|
|
512
|
+
"Aliases sonnet|opus|haiku|fable resolve within this session's provider — by name when a model carries it, else as the tier it names " +
|
|
513
|
+
'(haiku: the cheapest capable model, sonnet: the cheapest workhorse-class model, opus/fable: this session\'s model); "inherit" means the session model. ' +
|
|
344
514
|
"Any exact provider/model-id the user asked for also works, even from another provider (that is announced to the user) or unlisted here — this is a menu, not a whitelist.",
|
|
345
515
|
].join("\n");
|
|
346
516
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared result/handle contracts for a subagent run, plus the fork framing.
|
|
3
|
+
* (These outlived child.ts, the retired process-spawning runner; the runner is
|
|
4
|
+
* now in-process — see runner.ts.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ChildAction } from "../auto-mode/actions.ts";
|
|
8
|
+
import type { UsageTotals } from "./usage.ts";
|
|
9
|
+
|
|
10
|
+
export const OUTPUT_CAP = 50_000;
|
|
11
|
+
|
|
12
|
+
/** A fork running in an isolation worktree: where it is, and where the inherited paths pointed. */
|
|
13
|
+
export interface ForkIsolation {
|
|
14
|
+
worktreePath: string;
|
|
15
|
+
parentCwd: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Framing wrapped around a fork child's task. A fork inherits the parent's
|
|
20
|
+
* entire transcript; without this, a weaker model tends to abandon its assigned
|
|
21
|
+
* task and continue (or confabulate about) the inherited topic — then its output
|
|
22
|
+
* returns to the parent looking like independent confirmation. See
|
|
23
|
+
* docs/features/tools/records/tool-ambiguity-hardening.md (fork confabulation).
|
|
24
|
+
*
|
|
25
|
+
* With `isolation`, the fork is told that its working directory moved: the
|
|
26
|
+
* inherited system prompt and transcript name the parent's checkout in every
|
|
27
|
+
* path, so without this line a worktree fork edits the main tree (the guard in
|
|
28
|
+
* lib/worktree-isolation.ts then refuses the write, but the model needs to know
|
|
29
|
+
* where to write instead — SUBAGENT-REVIEW H3).
|
|
30
|
+
*/
|
|
31
|
+
export function forkTaskMessage(task: string, isolation?: ForkIsolation): string {
|
|
32
|
+
const lines = [
|
|
33
|
+
"You are a forked subagent. The conversation above is inherited context, for reference only — do NOT continue its open threads, verify its claims, or act on its plans. Do ONLY the task below; your final message is returned to the parent conversation verbatim, as data. You cannot see the parent's background tasks: its task ids are not addressable from here.",
|
|
34
|
+
];
|
|
35
|
+
if (isolation) {
|
|
36
|
+
lines.push(
|
|
37
|
+
"",
|
|
38
|
+
`You are isolated in your own git worktree: your working directory is now ${isolation.worktreePath}, a checkout of the same repository at HEAD. Every path under ${isolation.parentCwd} in the inherited context refers to the shared checkout, which you must NOT modify — read, edit and run things at the corresponding path under ${isolation.worktreePath} instead (writes into the shared checkout are refused). Your changes stay on this worktree's branch for the parent to review.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
lines.push("", "Task:", task);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChildOutcome {
|
|
46
|
+
output: string;
|
|
47
|
+
toolCalls: number;
|
|
48
|
+
usage: UsageTotals;
|
|
49
|
+
failed?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* What the child actually did, names and short subjects only. Auto mode
|
|
52
|
+
* reviews this when the child returns, to catch a sequence whose individual
|
|
53
|
+
* steps each passed. Never carries tool output — see auto-mode/actions.ts.
|
|
54
|
+
*/
|
|
55
|
+
actions: ChildAction[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A blocking run handle: await the result, or kill/snapshot it while it runs. */
|
|
59
|
+
export interface ChildHandle {
|
|
60
|
+
result: Promise<ChildOutcome>;
|
|
61
|
+
kill(): void;
|
|
62
|
+
snapshot(): { toolCalls: number; text: string; usage: UsageTotals };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A resident (background) agent handle: message it live, inspect it, stop it. */
|
|
66
|
+
export interface RpcChildHandle {
|
|
67
|
+
/**
|
|
68
|
+
* Deliver a message. "started" = the agent was idle and this began a new turn
|
|
69
|
+
* (resolves as soon as the turn is dispatched, not when it ends); "steered" =
|
|
70
|
+
* the agent was mid-turn and the message joined it. Rejects when the message
|
|
71
|
+
* could not be queued at all (pi refuses to steer an extension command), so
|
|
72
|
+
* the caller never reports a delivery that did not happen.
|
|
73
|
+
*/
|
|
74
|
+
send(message: string): Promise<"started" | "steered">;
|
|
75
|
+
busy(): boolean;
|
|
76
|
+
exited(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Abort any in-flight turn (reported as terminated) and dispose the session.
|
|
79
|
+
* Resolves once the abort has settled and the session is disposed (so a
|
|
80
|
+
* shutdown can wait for the exit-time cleanup, e.g. worktree removal).
|
|
81
|
+
*/
|
|
82
|
+
kill(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Dispose an IDLE session quietly — no turn outcome, no notification. A
|
|
85
|
+
* no-op mid-turn. Its persisted session file remains, so SendMessage still
|
|
86
|
+
* reaches the agent by resuming from disk instead of live.
|
|
87
|
+
*/
|
|
88
|
+
release(): void;
|
|
89
|
+
snapshot(): { toolCalls: number; text: string; usage: UsageTotals };
|
|
90
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure key decoding for the below-editor panel — Claude Code's agent-tree
|
|
3
|
+
* keys. Their meaning depends on whether a transcript view is open (index.ts
|
|
4
|
+
* owns that split): with no view open, arrows select and Enter opens the
|
|
5
|
+
* selected agent's transcript; while a view IS open the panel is in "read"
|
|
6
|
+
* mode, where arrows scroll it a line at a time, PageUp/PageDown scroll a page,
|
|
7
|
+
* `switch` (Tab) retargets to the next agent, `left` (←) closes the transcript
|
|
8
|
+
* back to agent selection, and Enter closes it. `x` stops, the `ctrl+x ctrl+k`
|
|
9
|
+
* chord stops all, esc leaves. `left` also serves the shell-panel stages (back)
|
|
10
|
+
* and `space` (close); the agents branch uses `left` in read mode (above) and
|
|
11
|
+
* treats `space` like typing. Raw terminal bytes in, intents out — no side
|
|
12
|
+
* effects, fully unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const UP = new Set(["\x1b[A", "\x1bOA"]);
|
|
16
|
+
const DOWN = new Set(["\x1b[B", "\x1bOB"]);
|
|
17
|
+
const LEFT = new Set(["\x1b[D", "\x1bOD"]);
|
|
18
|
+
|
|
19
|
+
export type StripKey = "up" | "down" | "left" | "space" | "switch" | "open" | "leave" | "stop" | "stopAll" | "pageUp" | "pageDown";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decoder with chord state: `ctrl+x` (\x18) arms the chord (consumed, no key);
|
|
23
|
+
* a following `ctrl+k` (\x0b) completes it into `stopAll`; any other key
|
|
24
|
+
* cancels the chord and is decoded normally. A byte that decodes to nothing
|
|
25
|
+
* (typing) is the caller's signal to drop focus and let the byte through.
|
|
26
|
+
*/
|
|
27
|
+
export function decodeStripKey(data: string, chordArmed: boolean): { key?: StripKey; chordArmed: boolean } {
|
|
28
|
+
if (chordArmed && data === "\x0b") return { key: "stopAll", chordArmed: false };
|
|
29
|
+
if (data === "\x18") return { chordArmed: true };
|
|
30
|
+
if (UP.has(data)) return { key: "up", chordArmed: false };
|
|
31
|
+
if (DOWN.has(data)) return { key: "down", chordArmed: false };
|
|
32
|
+
if (LEFT.has(data)) return { key: "left", chordArmed: false };
|
|
33
|
+
if (data === "\t") return { key: "switch", chordArmed: false };
|
|
34
|
+
if (data === " ") return { key: "space", chordArmed: false };
|
|
35
|
+
if (data === "\r" || data === "\n") return { key: "open", chordArmed: false };
|
|
36
|
+
if (data === "\x1b") return { key: "leave", chordArmed: false };
|
|
37
|
+
if (data === "x" || data === "X") return { key: "stop", chordArmed: false };
|
|
38
|
+
if (data === "\x1b[5~") return { key: "pageUp", chordArmed: false };
|
|
39
|
+
if (data === "\x1b[6~") return { key: "pageDown", chordArmed: false };
|
|
40
|
+
return { chordArmed: false };
|
|
41
|
+
}
|