@north-light/crouter 0.3.162 → 0.3.164
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/dist/builtin-memory/00-runtime-base.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
- package/dist/builtin-memory/init.md +38 -0
- package/dist/builtin-memory/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/memory-loading.md +45 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +34 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +14 -3
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
- package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
- package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/edit-diff.test.js +35 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
- package/dist/clients/attach/render/chat-view.js +54 -38
- package/dist/clients/attach/render/crtr-output.d.ts +2 -1
- package/dist/clients/attach/render/crtr-output.js +33 -4
- package/dist/clients/attach/render/edit-diff.d.ts +21 -6
- package/dist/clients/attach/render/edit-diff.js +104 -81
- package/dist/clients/attach/render/tool-calls.d.ts +21 -20
- package/dist/clients/attach/render/tool-calls.js +89 -43
- package/dist/clients/attach/session/connection.d.ts +1 -1
- package/dist/clients/attach/session/editor-frame.d.ts +5 -1
- package/dist/clients/attach/session/editor-frame.js +21 -15
- package/dist/clients/attach/session/identity.d.ts +1 -1
- package/dist/clients/attach/viewer.js +628 -624
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
- package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
- package/dist/commands/__tests__/human.test.js +2 -0
- package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
- package/dist/commands/__tests__/search-contents.test.js +52 -0
- package/dist/commands/api-client.js +1 -0
- package/dist/commands/human/prompts.js +2 -2
- package/dist/commands/human/shared.d.ts +1 -0
- package/dist/commands/human/shared.js +7 -4
- package/dist/commands/memory/lint.d.ts +6 -0
- package/dist/commands/memory/lint.js +177 -35
- package/dist/commands/memory/read.js +22 -1
- package/dist/commands/memory/write.js +18 -4
- package/dist/commands/memory.js +1 -1
- package/dist/commands/search/contents.js +2 -2
- package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
- package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
- package/dist/commands/sys/setup-wizard.d.ts +8 -1
- package/dist/commands/sys/setup-wizard.js +209 -58
- package/dist/commands/sys/sync-deps.js +14 -18
- package/dist/commands/sys/sync-project-guidance.js +16 -19
- package/dist/core/__tests__/migration.test.js +8 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
- package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
- package/dist/core/__tests__/on-read-identity.test.js +8 -15
- package/dist/core/__tests__/revive.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -4
- package/dist/core/__tests__/worktree.test.js +3 -3
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +18 -11
- package/dist/core/canvas/labels.d.ts +4 -5
- package/dist/core/canvas/labels.js +9 -9
- package/dist/core/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- package/dist/core/configured-clis/invoker.js +2 -0
- package/dist/core/help.d.ts +3 -1
- package/dist/core/help.js +5 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +3 -2
- package/dist/core/keybindings/catalog.js +34 -25
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/resolve.js +8 -0
- package/dist/core/keybindings/types.d.ts +6 -1
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +110 -0
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +12 -1
- package/dist/core/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.js +3 -13
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- package/dist/core/substrate/ceiling.d.ts +3 -6
- package/dist/core/substrate/ceiling.js +13 -15
- package/dist/core/substrate/index.d.ts +2 -3
- package/dist/core/substrate/index.js +2 -2
- package/dist/core/substrate/on-read.d.ts +4 -13
- package/dist/core/substrate/on-read.js +142 -262
- package/dist/core/substrate/render.js +4 -4
- package/dist/core/substrate/schema.d.ts +3 -2
- package/dist/core/substrate/schema.js +5 -5
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
- package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
- package/dist/pi-extensions/canvas-tool-guide.js +77 -0
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -9,6 +9,7 @@ rationale: >-
|
|
|
9
9
|
"Say what actually happens" exists because an approval request called a root a person had created an "attended root" — an invented category with no referent in the product, which forced Silas to halt the decision and ask what the term meant (2026-07-28). Agents coin taxonomies to compress a distinction; the reader pays by decoding a word that names nothing real.
|
|
10
10
|
|
|
11
11
|
"Waiting is a way to end a turn" lived in its own ungated all-node doc until 2026-07-28. Same gate, same audience, never independently readable — so the split bought no routing and cost a stub cross-reference in this file pointing at a section spliced a few hundred tokens later. Split it back out only if it ever needs a gate of its own.
|
|
12
|
+
lint-ignore: length
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
You are a **node** in a live agent graph (the crtr canvas). This section is your operating protocol — it is true for every node regardless of role.
|
|
@@ -6,6 +6,7 @@ file-read-visibility: none
|
|
|
6
6
|
gate: {mode: orchestrator}
|
|
7
7
|
rationale: >-
|
|
8
8
|
Two observed orchestration failures set this kernel's stopping rules. A sole-writer feature lane produced a 5-deep 1:1 developer/orchestrator chain by repeatedly delegating the whole assignment; separately, the kernel's “idle capacity,” “maximum agents,” and “when in doubt, more rigor” objective helped produce review-only subtrees as large as 87 nodes and five levels deep. Coordination must optimize new evidence toward the goal rather than node count or process length.
|
|
9
|
+
lint-ignore: length
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
## You are an orchestrator
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When bootstrapping a project's memory store — the user invoked /init or a workspace has no .crouter/memory/INDEX.md front door — this knowledge should be read because a lean routed front door gives every future agent in that workspace its operating contract for a fraction of the standing context cost an unstructured guidance dump would charge each session forever.
|
|
4
|
+
short-form: "Bootstrap a project memory store: survey the repo, then author a lean INDEX front door plus a linked dev doc — commands, map, non-default rules, pointers — and lint. Depth splits into linked docs; every always-loaded line must earn its keep."
|
|
5
|
+
system-prompt-visibility: none
|
|
6
|
+
file-read-visibility: none
|
|
7
|
+
slash: true
|
|
8
|
+
rationale: Unguided agents bootstrap project memory badly in both directions — either no front door at all, or a monolithic CLAUDE.md-style manual (copied README, generic framework advice, exhaustive command catalogs) that taxes every future session and decays. Distilled 2026-07-31 from ecosystem research across Anthropic/OpenAI/Cursor/GitHub guidance and production AGENTS.md files — the strongest evidence being Sentry's consolidation postmortem (critical commands buried in nested files got missed) and Anthropic's under-200-line target with adherence falling as always-loaded files grow.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# /init — bootstrap this project's memory store
|
|
12
|
+
|
|
13
|
+
Give this workspace its memory front door: a root INDEX every future agent boots with when the project mounts, plus a small graph of linked docs carrying the depth. The front door is an operating contract and routing layer, never a manual — every line it carries is paid by every session here forever, so a line earns its place only by changing behavior: a fact every task needs, a non-obvious hazard, or a rule an agent would otherwise get wrong.
|
|
14
|
+
|
|
15
|
+
## 1. Survey before writing
|
|
16
|
+
|
|
17
|
+
Learn the project from its own artifacts: README, package manifests and lockfiles, build/CI config, scripts, existing CLAUDE.md/AGENTS.md or docs. Extract the real commands (setup, run, build, targeted test, full verification) and RUN the important ones — record only commands you have seen work, with any required ordering, environment, or package-manager constraint. Note the architecture in a few sentences: major components, dependency direction, where generated or externally-owned code lives. Collect the conventions that differ from what a competent agent would assume — those deviations are the highest-value lines you will write.
|
|
18
|
+
|
|
19
|
+
## 2. Read the contract, then author the front door
|
|
20
|
+
|
|
21
|
+
Run `crtr memory write -h` first — it carries the substrate frontmatter contract and the exact root-INDEX shape, including the workspace-mount route that `crtr memory lint` validates. Then author the INDEX with these sections, total well under the length lint's cap:
|
|
22
|
+
|
|
23
|
+
- **Project in one paragraph** — purpose, major components, dependency direction.
|
|
24
|
+
- **Common commands** — the 3–8 commands most changes need, copyable, verified.
|
|
25
|
+
- **Where to work** — a task-to-path map; mark generated or externally-owned areas.
|
|
26
|
+
- **Non-default rules and gotchas** — only high-impact deviations from defaults. Skip anything a formatter, linter, or typechecker already enforces; point at the check instead.
|
|
27
|
+
- **Done** — the minimum credible gate for an ordinary change.
|
|
28
|
+
- **Pointers** — a `[[link]]` per deeper doc with a one-clause read trigger (e.g. "debugging the daemon → the dev doc").
|
|
29
|
+
|
|
30
|
+
Exclude generic language/framework advice, copied docs or style guides, volatile version detail, rare edge cases, and multi-step procedures — depth belongs behind links, not in the standing load.
|
|
31
|
+
|
|
32
|
+
## 3. Split depth into linked docs
|
|
33
|
+
|
|
34
|
+
Author `dev` alongside the INDEX by default: validated setup/run/build/test/debug commands with required order and environment, targeted-vs-full verification, and common failure modes. Add `architecture` only when the tree cannot be understood from the INDEX's brief map, and a conventions/gotchas doc only once real project-specific rules outgrow a few INDEX bullets — never pre-populate generic security or style manuals. These reference docs take `none` visibility on both axes; the INDEX's links are how they are found, so they cost nothing until followed.
|
|
35
|
+
|
|
36
|
+
## 4. Validate
|
|
37
|
+
|
|
38
|
+
Run `crtr memory lint` — it strict-parses the store, checks the INDEX front-door contract, and fails on dangling links or over-length bodies. Fix findings rather than suppressing them. The store is a living contract: when commands, architecture, or conventions change, rewrite the affected doc in place and delete superseded guidance — an always-loaded line that has stopped being true is worse than no line at all.
|
|
@@ -14,6 +14,7 @@ Open this dir whenever a task turns on understanding the runtime itself or chang
|
|
|
14
14
|
|
|
15
15
|
- **nodes-and-canvas** — the agent-runtime model: nodes on the canvas graph, spawn/delegate, the push/feed spine, lifecycle (mode + lifecycle axes), and revive (manual + daemon auto-revive).
|
|
16
16
|
- **storage-tiers** — where every kind of state lives: the two tiers (scope root and canvas home) and their durability/ownership contracts.
|
|
17
|
+
- **memory-loading** — the memory load model: the two hooks (boot catalog, file-read), the four-rung ladder, gates, applies-to/read-when routing, boot-render ordering, and store mounting/precedence — read when diagnosing why a doc did or didn't load.
|
|
17
18
|
- **agent-shaping** — the when-to-use-which layer over the four dials that shape a node: kinds (the builtin roster, sub-kinds, and custom personas), modes (base vs orchestrator), profiles, and the memory tiers (node/profile/project/user/builtin).
|
|
18
19
|
- **workflow-codification** — the codification loop for repeatable tasks: do the work hands-on under a capture session, mine the HAR, then codify it as scripts (scope-root `scripts/`) + a slash-invokable memory doc, with the self-heal rule that a failed script is re-derived and rewritten.
|
|
19
20
|
- **plugins** — authoring a crtr plugin: the plugin.json manifest, directory layout, scopes, install mechanics, versioning, command plugins (contributing top-level CLI commands via commands.json + one executable), and configured CLIs (contributing commands as definition + HTTP from a remote manifest, no executable).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When you need to know why a memory doc did or didn't load — or are deciding how a new doc should surface — this reference should be read because it names the hook, rung, gate, and ordering that produced the behavior, so you fix loading by turning the right dial instead of guessing at frontmatter.
|
|
4
|
+
short-form: The complete load model — two hooks (boot catalog, file-read), the four-rung ladder, gates, applies-to/read-when routing, boot-render ordering, and store mounting/precedence.
|
|
5
|
+
system-prompt-visibility: name
|
|
6
|
+
file-read-visibility: none
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# How memory loads
|
|
10
|
+
|
|
11
|
+
Every memory doc declares its own loading policy in frontmatter; the runtime never guesses. Loading is two hooks, one four-rung dial per hook, an optional gate, and structural ordering. The authoring contract (flags, routing-line craft) is `crtr memory write -h`; which scope to write to is `internal/agent-shaping`; physical paths are `internal/storage-tiers`. This doc is the mechanics between those: what actually fires, when, and in what order.
|
|
12
|
+
|
|
13
|
+
## The two hooks
|
|
14
|
+
|
|
15
|
+
- **System prompt** (`system-prompt-visibility`) — the boot catalog assembled into every node's system prompt at revive.
|
|
16
|
+
- **File read** (`file-read-visibility`) — context attached to the workspace or to files. Two events: **workspace mount** during first-message assembly (docs routed `applies-to: "."`), and a later **matching file read** (docs routed by glob). Nothing positional fires from where the doc happens to sit on disk — only from its declared route.
|
|
17
|
+
|
|
18
|
+
Each doc sets both rungs explicitly; there is no kind-based default. Usually one axis carries a real rung and the other is `none`.
|
|
19
|
+
|
|
20
|
+
## The rung ladder
|
|
21
|
+
|
|
22
|
+
`none` → `name` → `preview` → `content`, per hook:
|
|
23
|
+
|
|
24
|
+
- `none` — invisible on that hook; findable only by search/read. For archival docs and for the unused axis.
|
|
25
|
+
- `name` — the bare title. The practical floor: an agent can't reach for a doc it has never seen named.
|
|
26
|
+
- `preview` — name + the `when-and-why-to-read` routing line, rendered verbatim. The heart of progressive disclosure: one sentence that lets an agent decide whether to spend the read.
|
|
27
|
+
- `content` — the full body inlined. Reserved for always-relevant docs that are either a bullet's worth of text or a wholly-important operating guide (the root INDEX shape below).
|
|
28
|
+
|
|
29
|
+
`short-form` is **not** a rung and never enters agent context — it exists for humans browsing `crtr memory list`. Disclosure is name → routing line → whole thing; there is deliberately no "just the summary" level, because agents satisfice on abbreviations and never read the rest.
|
|
30
|
+
|
|
31
|
+
## Gates and read-when
|
|
32
|
+
|
|
33
|
+
An optional `gate` predicates visibility on the node's own config — kind, mode, orchestration depth, scope, cwd — using the standard matcher vocabulary (`crtr memory write -h`). No gate means always eligible. Persona prose is just gated content-rung docs (`gate: {kind: developer, mode: base}`); guidance that should scale with effort is one predicate (`orchestration.depth: {gte: 2}`), not a mechanism. `read-when` additionally matches the *read file's* frontmatter on the file-read hook; it refines a route but never replaces the required `applies-to` boundary.
|
|
34
|
+
|
|
35
|
+
## Store mounting and precedence
|
|
36
|
+
|
|
37
|
+
At boot/first-message assembly the runtime mounts: builtin docs, the user store (`~/.crouter/memory/`), the selected profile's store, and every project store — ancestor `.crouter/memory/` dirs walking up from cwd plus each project in the profile's purview. Physical duplicates are deduplicated; name collisions resolve nearest-first (project over profile over user over builtin), which is what lets a project doc shadow a builtin one.
|
|
38
|
+
|
|
39
|
+
A root `INDEX.md` is a workspace's front door: `system-prompt-visibility: none`, `file-read-visibility: content`, `applies-to: "."` — the operating guide loads when that workspace mounts, not in every boot catalog. Multiple mounted roots render broad-to-specific, each under its own envelope name.
|
|
40
|
+
|
|
41
|
+
## Ordering
|
|
42
|
+
|
|
43
|
+
The boot render is structural, never a per-doc knob: docs group by rung (content bodies as prose, then previews, then names), and within a group order general-to-specific — scope first (builtin → user → profile → outermost project root → nearest), then tree position (higher directories before deeper), then filename. A numeric `NN-` filename prefix (stripped from the doc's name) is the sparing escape hatch when an exact sequence must be pinned.
|
|
44
|
+
|
|
45
|
+
`crtr memory lint` is the validator for all of the above: frontmatter schema, both rungs present, routes on every non-`none` file-read rung, rung-scaled body length, dangling `[[links]]`, and each profile-managed project's front door.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
kind: knowledge
|
|
3
3
|
when-and-why-to-read: When a node is marked wedged or remains active without progress while its inbox accumulates, this reference should be read so recovery restores progress without losing context or killing the wrong process.
|
|
4
4
|
short-form: a child stuck mid-turn (runaway bash, or a stalled broker with no subprocess) is AUTO-DETECTED by the daemon (⚠ wedged + a doctrine wake) — kill the subprocess if one exists; if none, the daemon SIGTERM-kicks the broker and automatically starts a fresh recovery cycle
|
|
5
|
-
system-prompt-visibility:
|
|
6
|
-
file-read-visibility:
|
|
5
|
+
system-prompt-visibility: preview
|
|
6
|
+
file-read-visibility: none
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
A base node can wedge INDEFINITELY on a single runaway bash command (classic: `grep -rln "..." /` — a recursive grep from filesystem root that scans all of disk and never returns; also any unbounded find/scan over `/` or a network mount). The node's pi turn is blocked waiting on the bash call, so it never `push`es and never finishes.
|
|
@@ -32,7 +32,6 @@ directory's `node_modules/`.
|
|
|
32
32
|
| Extension | What it does |
|
|
33
33
|
|-----------|--------------|
|
|
34
34
|
| `claude-plugin-commands.ts` | Surfaces commands and skills from `~/.claude`, the cwd, every project in the selected crouter profile, and installed plugins as pi slash commands with arg/shell-block execution. Writes shims to `~/.pi/agent/.plugin-command-shims/`. Skips any name pi already serves natively from `~/.pi/agent/prompts/` (e.g. crtr exports a slash command to both `~/.claude/commands` and `~/.pi/agent/prompts`), so the shim never collides with the native prompt. |
|
|
35
|
-
| `crouter-help.ts` | Help integration for the `crtr` CLI. |
|
|
36
35
|
| `provider-rotation.ts` | Subscription credential rotation across Anthropic / OpenAI-Codex: does its own OAuth login/refresh, rotates on rate-limits, falls back across the model ladder. Registers `/provider-sub <provider> <list\|add\|select\|rm>`. |
|
|
37
36
|
| `crtr-commands/` | Auto-generates a slash command per `crtr` CLI node. The tree is derived in-process from crtr's own `buildRoot()` (no subprocesses, no cache on disk); `filters.json` controls which nodes are exposed. |
|
|
38
37
|
| `sysprompt-window.ts` | Registers `/sysprompt`, which runs `crtr sys sysprompt --window` without injecting the prompt into context. |
|
package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts
CHANGED
|
@@ -965,6 +965,7 @@ function makeRotationCtx(modelIntent?: string) {
|
|
|
965
965
|
},
|
|
966
966
|
} as any;
|
|
967
967
|
const setModelCalls: any[] = [];
|
|
968
|
+
const thinkingLevelCalls: string[] = [];
|
|
968
969
|
const providers: Record<string, { streamSimple: (...args: any[]) => AsyncIterable<any> }> = {};
|
|
969
970
|
const events: Record<string, (...args: any[]) => unknown> = {};
|
|
970
971
|
const commands: Record<string, { handler: (args: string, ctx: any) => unknown }> = {};
|
|
@@ -992,11 +993,42 @@ function makeRotationCtx(modelIntent?: string) {
|
|
|
992
993
|
ctx.model = model;
|
|
993
994
|
return true;
|
|
994
995
|
},
|
|
995
|
-
setThinkingLevel(level: string) {
|
|
996
|
+
setThinkingLevel(level: string) {
|
|
997
|
+
thinkingLevelCalls.push(level);
|
|
998
|
+
ctx.thinkingLevel = level;
|
|
999
|
+
},
|
|
996
1000
|
} as any;
|
|
997
|
-
return { ctx, setModelCalls, providers, events, commands, pi };
|
|
1001
|
+
return { ctx, setModelCalls, thinkingLevelCalls, providers, events, commands, pi };
|
|
998
1002
|
}
|
|
999
1003
|
|
|
1004
|
+
// A live route carries the session's exact thinking level through every provider
|
|
1005
|
+
// request, including the requests that continue a turn after tool results.
|
|
1006
|
+
test("preserves a manually selected thinking level across provider requests", async () => {
|
|
1007
|
+
await resetRotationState();
|
|
1008
|
+
writeLadderConfig();
|
|
1009
|
+
await helpers.writeSubscriptionPool("openai-codex", [credential("codex")]);
|
|
1010
|
+
|
|
1011
|
+
const { ctx, thinkingLevelCalls, providers, events, pi } = makeRotationCtx("openai/strong");
|
|
1012
|
+
ctx.thinkingLevel = "max";
|
|
1013
|
+
rotationModule.__setStreamForProviderForTest(() => (async function* () {
|
|
1014
|
+
yield { type: "text_delta", text: "continued after tool result" };
|
|
1015
|
+
})() as any);
|
|
1016
|
+
|
|
1017
|
+
try {
|
|
1018
|
+
await providerRotationExtension(pi);
|
|
1019
|
+
await events.session_start?.({}, ctx);
|
|
1020
|
+
const stream = providers["openai-codex"].streamSimple(ctx.model, ctx);
|
|
1021
|
+
for await (const _event of stream) {
|
|
1022
|
+
// Drain the managed-provider request to completion.
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
assert.deepEqual(thinkingLevelCalls, ["max"]);
|
|
1026
|
+
assert.equal(ctx.thinkingLevel, "max");
|
|
1027
|
+
} finally {
|
|
1028
|
+
rotationModule.__setStreamForProviderForTest(undefined);
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1000
1032
|
// Codex exhaustion switches the model and serves the same turn on the Anthropic fallback.
|
|
1001
1033
|
test("falls back transparently and serves the turn on Anthropic when Codex is exhausted", async () => {
|
|
1002
1034
|
await resetRotationState();
|
|
@@ -512,6 +512,8 @@ function emitTerminalError(stream: AssistantMessageEventStream, model: Model<any
|
|
|
512
512
|
// turn-local cooling state.
|
|
513
513
|
type RouteCandidate = {
|
|
514
514
|
routeId: string;
|
|
515
|
+
credentialSource: string;
|
|
516
|
+
rank: number;
|
|
515
517
|
providerId: string;
|
|
516
518
|
model: Model<any>;
|
|
517
519
|
thinkingLevel: string | undefined;
|
|
@@ -549,6 +551,12 @@ async function switchToRoute(pi: ExtensionAPI, ctx: ExtensionContext, candidate:
|
|
|
549
551
|
return ctx.model?.provider === candidate.providerId && ctx.model.id === candidate.model.id;
|
|
550
552
|
}
|
|
551
553
|
|
|
554
|
+
function logRouteQueueAdvance(from: RouteCandidate, to: RouteCandidate | undefined, result: AttemptResult): void {
|
|
555
|
+
logRotation(
|
|
556
|
+
`from=${from.providerId}/${from.model.id} from_route_id=${from.routeId} from_credential_source=${from.credentialSource} from_rank=${from.rank} to=${to ? `${to.providerId}/${to.model.id}` : "none"} to_route_id=${to?.routeId ?? "none"} to_credential_source=${to?.credentialSource ?? "none"} to_rank=${to?.rank ?? "none"} reason=${result.classification?.reason ?? result.result}; advancing route queue`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
552
560
|
// Drive the deterministic route queue from the live rank. Managed routes walk
|
|
553
561
|
// their pool rows; every other declared route is one configured credential.
|
|
554
562
|
async function runManagedProvider(
|
|
@@ -570,7 +578,7 @@ async function runManagedProvider(
|
|
|
570
578
|
if (!registry || !routeModel) return undefined;
|
|
571
579
|
const availability = probeRouteAvailability(route, registry);
|
|
572
580
|
if (!availability.registered || (!isManagedProvider(route.providerId) && !availability.authenticated)) return undefined;
|
|
573
|
-
return { routeId: route.routeId, providerId: route.providerId, model: routeModel, thinkingLevel: route.thinkingLevel };
|
|
581
|
+
return { routeId: route.routeId, credentialSource: route.credentialSource, rank: route.rank, providerId: route.providerId, model: routeModel, thinkingLevel: route.thinkingLevel };
|
|
574
582
|
})
|
|
575
583
|
.filter((candidate): candidate is RouteCandidate => candidate !== undefined);
|
|
576
584
|
const isLiveRoute = (candidate: RouteCandidate): boolean =>
|
|
@@ -579,7 +587,7 @@ async function runManagedProvider(
|
|
|
579
587
|
// later re-enter when a credential becomes ready, but it is never selected now.
|
|
580
588
|
const candidates = routeCandidates.filter((candidate) => isLiveRoute(candidate) || !isManagedProvider(candidate.providerId) || readSubscriptionPool(candidate.providerId).some((entry) => entry.authFailure === undefined && (entry.rateLimitedUntil || 0) <= Date.now()));
|
|
581
589
|
if (!candidates.some(isLiveRoute)) {
|
|
582
|
-
candidates.unshift({ routeId: `live:${model.provider}/${model.id}`, providerId: model.provider, model, thinkingLevel:
|
|
590
|
+
candidates.unshift({ routeId: `live:${model.provider}/${model.id}`, credentialSource: 'live', rank: liveRank, providerId: model.provider, model, thinkingLevel: liveThinking });
|
|
583
591
|
}
|
|
584
592
|
|
|
585
593
|
const queuedRouteIds = new Set(candidates.map((candidate) => candidate.routeId));
|
|
@@ -609,7 +617,7 @@ async function runManagedProvider(
|
|
|
609
617
|
if (result.result === 'fatal') { stream.end(); return; }
|
|
610
618
|
const cooldownMs = result.result === 'rate_limited' ? result.retryAfterMs ?? DEFAULT_RATE_LIMIT_BACKOFF_MS : TRANSIENT_ROTATE_COOLDOWN_MS;
|
|
611
619
|
localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
|
|
612
|
-
|
|
620
|
+
logRouteQueueAdvance(candidate, candidates[0], result);
|
|
613
621
|
continue;
|
|
614
622
|
}
|
|
615
623
|
|
|
@@ -620,6 +628,7 @@ async function runManagedProvider(
|
|
|
620
628
|
for (const credential of available) seenLabels.add(credential.label);
|
|
621
629
|
seenReadyLabels.set(routeId, seenLabels);
|
|
622
630
|
|
|
631
|
+
let advanceResult: AttemptResult | undefined;
|
|
623
632
|
for (const credential of available) {
|
|
624
633
|
let freshCredential: ResolvedCredential;
|
|
625
634
|
try {
|
|
@@ -654,9 +663,11 @@ async function runManagedProvider(
|
|
|
654
663
|
} else {
|
|
655
664
|
localCoolingDeadlines.set(routeId, Math.max(localCoolingDeadlines.get(routeId) ?? 0, Date.now() + cooldownMs));
|
|
656
665
|
}
|
|
666
|
+
advanceResult = result;
|
|
657
667
|
}
|
|
658
668
|
|
|
659
669
|
enqueueFreshRouteCandidates(candidates, queuedRouteIds, knownRoutes, seenReadyLabels, cooldownAttempts, localCoolingDeadlines);
|
|
670
|
+
if (advanceResult !== undefined) logRouteQueueAdvance(candidate, candidates[0], advanceResult);
|
|
660
671
|
}
|
|
661
672
|
|
|
662
673
|
const coolingDeadline = earliestCoolingDeadline([...knownRoutes.values()], localCoolingDeadlines);
|
|
@@ -50,7 +50,7 @@ test('the attach viewer covers crtr bash tool calls and crtr-output messages nat
|
|
|
50
50
|
const chatView = read(join(ROOT, 'clients', 'attach', 'render/chat-view.ts'));
|
|
51
51
|
assert.match(chatView, /CRTR_OUTPUT_CUSTOM_TYPE/);
|
|
52
52
|
assert.match(chatView, /new CrtrOutputMessageComponent\(/);
|
|
53
|
-
assert.match(chatView, /createCrtrBashToolDefinition\(args\)/);
|
|
53
|
+
assert.match(chatView, /createCrtrBashToolDefinition\(args,\s*fold\)/);
|
|
54
54
|
// MinimizableToolComponent is the viewer's ToolExecutionComponent subclass (it
|
|
55
55
|
// adds the Ctrl+O minimized state); the crtr tool definition still threads through it.
|
|
56
56
|
assert.match(chatView, /new MinimizableToolComponent\([\s\S]*toolDefinition as never/);
|
|
@@ -16,6 +16,11 @@ const plainTheme = {
|
|
|
16
16
|
bg(_name, text) { return text; },
|
|
17
17
|
bold(text) { return text; },
|
|
18
18
|
};
|
|
19
|
+
/** pi calls tool renderers with a shared state bag; a normal (unfolded) call has an empty one. */
|
|
20
|
+
function renderCrtrCall(definition, args) {
|
|
21
|
+
const definitionWithContext = definition;
|
|
22
|
+
return definitionWithContext.renderCall(args, theme, { state: {} });
|
|
23
|
+
}
|
|
19
24
|
function stripAnsi(text) {
|
|
20
25
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
21
26
|
}
|
|
@@ -31,7 +36,7 @@ test('returns a viewer tool definition only for crtr bash commands', () => {
|
|
|
31
36
|
assert.equal(createCrtrBashToolDefinition({ command: 'echo hi' }), undefined);
|
|
32
37
|
const def = createCrtrBashToolDefinition({ command: 'crtr canvas dashboard' });
|
|
33
38
|
assert.equal(typeof def.renderCall, 'function');
|
|
34
|
-
const rendered = def
|
|
39
|
+
const rendered = renderCrtrCall(def, { command: 'crtr canvas dashboard' }).render(80).join('\n');
|
|
35
40
|
assert.equal(rendered.trimEnd().split('\n').length, 1);
|
|
36
41
|
assert.match(rendered, new RegExp(iconForPath(['canvas', 'dashboard'])));
|
|
37
42
|
assert.match(rendered, /crtr/);
|
|
@@ -165,7 +170,7 @@ test('a branch help preview with more subcommands than the line cap truncates wi
|
|
|
165
170
|
// displayed crtr path and help detection include only the command segment.
|
|
166
171
|
test('a shell redirect on the outer bash call never leaks into the displayed crtr path or help detection', () => {
|
|
167
172
|
const def = createCrtrBashToolDefinition({ command: 'crtr push -h 2>&1' });
|
|
168
|
-
const rendered = def
|
|
173
|
+
const rendered = renderCrtrCall(def, { command: 'crtr push -h 2>&1' }).render(80).join('\n');
|
|
169
174
|
assert.match(rendered, new RegExp(HELP_ICON));
|
|
170
175
|
assert.match(rendered, /crtr help/);
|
|
171
176
|
assert.match(rendered, /<b>push<\/b>/);
|
|
@@ -175,18 +180,18 @@ test('a shell redirect on the outer bash call never leaks into the displayed crt
|
|
|
175
180
|
// Other redirect shapes on the outer call, and mid-argv redirects, must
|
|
176
181
|
// never survive into the path either.
|
|
177
182
|
const other = createCrtrBashToolDefinition({ command: 'crtr node new --name foo > /tmp/log 2>&1' });
|
|
178
|
-
const otherRendered = other
|
|
183
|
+
const otherRendered = renderCrtrCall(other, { command: 'crtr node new --name foo > /tmp/log 2>&1' }).render(80).join('\n');
|
|
179
184
|
assert.match(otherRendered, /node new --name foo/);
|
|
180
185
|
assert.doesNotMatch(otherRendered, /tmp\/log/);
|
|
181
186
|
// Redirect operators glued to their target with NO space (`2>/tmp/out`,
|
|
182
187
|
// `>/tmp/log`) are still shell plumbing and never displayed as path text.
|
|
183
188
|
const noSpace = createCrtrBashToolDefinition({ command: 'crtr push -h 2>/tmp/out' });
|
|
184
|
-
const noSpaceRendered = noSpace
|
|
189
|
+
const noSpaceRendered = renderCrtrCall(noSpace, { command: 'crtr push -h 2>/tmp/out' }).render(80).join('\n');
|
|
185
190
|
assert.match(noSpaceRendered, /<b>push<\/b>/);
|
|
186
191
|
assert.doesNotMatch(noSpaceRendered, /2>/);
|
|
187
192
|
assert.doesNotMatch(noSpaceRendered, /tmp\/out/);
|
|
188
193
|
const noSpaceMid = createCrtrBashToolDefinition({ command: 'crtr node new --name foo >/tmp/log' });
|
|
189
|
-
const noSpaceMidRendered = noSpaceMid
|
|
194
|
+
const noSpaceMidRendered = renderCrtrCall(noSpaceMid, { command: 'crtr node new --name foo >/tmp/log' }).render(80).join('\n');
|
|
190
195
|
assert.match(noSpaceMidRendered, /node new --name foo/);
|
|
191
196
|
assert.doesNotMatch(noSpaceMidRendered, /tmp\/log/);
|
|
192
197
|
});
|
|
@@ -194,7 +199,7 @@ test('a shell redirect on the outer bash call never leaks into the displayed crt
|
|
|
194
199
|
// body contains only synthesized preview content, never a repeated header.
|
|
195
200
|
test('a collapsed help card never shows the help label twice (header + body)', () => {
|
|
196
201
|
const def = createCrtrBashToolDefinition({ command: 'crtr canvas dashboard -h' });
|
|
197
|
-
const header = def
|
|
202
|
+
const header = renderCrtrCall(def, { command: 'crtr canvas dashboard -h' }).render(80).join('\n');
|
|
198
203
|
const body = def
|
|
199
204
|
.renderResult({ content: [{ type: 'text', text: LEAF_SCHEMA }] }, { expanded: false }, theme, {})
|
|
200
205
|
.render(80)
|
|
@@ -414,7 +419,7 @@ test('a crtr memory lint with findings collapses to a findings count and warning
|
|
|
414
419
|
// (possibly collapsed) result renders.
|
|
415
420
|
test('renderCall shows a help-specific header for a -h/--help invocation, no bash chrome', () => {
|
|
416
421
|
const def = createCrtrBashToolDefinition({ command: 'crtr node config -h' });
|
|
417
|
-
const rendered = def
|
|
422
|
+
const rendered = renderCrtrCall(def, { command: 'crtr node config -h' }).render(80).join('\n');
|
|
418
423
|
assert.equal(rendered.trimEnd().split('\n').length, 1);
|
|
419
424
|
assert.match(rendered, new RegExp(HELP_ICON));
|
|
420
425
|
assert.match(rendered, /crtr help/);
|
|
@@ -447,7 +452,7 @@ test('renderCall uses a mapped Nerd Font glyph and one line for every mapped roo
|
|
|
447
452
|
// strip for width purposes), so the visible-width accounting used by the
|
|
448
453
|
// real Text component's word-wrap counts that markup as content width —
|
|
449
454
|
// a generous width keeps every case here well clear of an unrelated wrap.
|
|
450
|
-
const rendered = def
|
|
455
|
+
const rendered = renderCrtrCall(def, { command }).render(200).join('\n');
|
|
451
456
|
assert.equal(rendered.trimEnd().split('\n').length, 1, `expected one line for ${command}`);
|
|
452
457
|
assert.match(rendered, new RegExp(iconForPath(iconTokens)), `expected the ${iconTokens.join(' ')} glyph in ${command}`);
|
|
453
458
|
assert.match(rendered, /crtr/, `expected "crtr" in ${command}`);
|
|
@@ -457,7 +462,7 @@ test('renderCall uses a mapped Nerd Font glyph and one line for every mapped roo
|
|
|
457
462
|
});
|
|
458
463
|
test('renderCall for a normal invocation previews the crtr path, not a raw bash echo', () => {
|
|
459
464
|
const def = createCrtrBashToolDefinition({ command: 'crtr node config set foo bar' });
|
|
460
|
-
const rendered = def
|
|
465
|
+
const rendered = renderCrtrCall(def, { command: 'crtr node config set foo bar' }).render(200).join('\n');
|
|
461
466
|
assert.equal(rendered.trimEnd().split('\n').length, 1);
|
|
462
467
|
assert.match(rendered, new RegExp(iconForPath(['node', 'config'])));
|
|
463
468
|
assert.match(rendered, /crtr/);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createCrtrEditToolDefinition } from '../render/edit-diff.js';
|
|
4
|
+
import { FoldedToolCallController } from '../render/tool-calls.js';
|
|
5
|
+
const theme = {
|
|
6
|
+
fg: (_name, text) => text,
|
|
7
|
+
bg: (_name, text) => text,
|
|
8
|
+
bold: (text) => text,
|
|
9
|
+
getFgAnsi: (_name) => '',
|
|
10
|
+
getBgAnsi: (_name) => '',
|
|
11
|
+
getColorMode: () => 'truecolor',
|
|
12
|
+
};
|
|
13
|
+
test('edit cards stream argument diffs, then keep the executed result authoritative', () => {
|
|
14
|
+
const fold = new FoldedToolCallController();
|
|
15
|
+
const view = createCrtrEditToolDefinition(process.cwd(), fold);
|
|
16
|
+
const definition = view.definition;
|
|
17
|
+
const context = { cwd: process.cwd(), isError: false };
|
|
18
|
+
const streamedArgs = { path: 'example.ts', edits: [{ oldText: 'before', newText: 'streaming' }] };
|
|
19
|
+
const streamed = definition.renderCall(streamedArgs, theme, context).render(80).join('\n');
|
|
20
|
+
assert.match(streamed, /before/);
|
|
21
|
+
assert.match(streamed, /streaming/);
|
|
22
|
+
view.observeResult({
|
|
23
|
+
content: [{ type: 'text', text: 'edited' }],
|
|
24
|
+
details: { diff: '-10 before\n+10 executed\n+11 extra' },
|
|
25
|
+
isError: false,
|
|
26
|
+
}, false);
|
|
27
|
+
const laterArgs = { path: 'example.ts', edits: [{ oldText: 'before', newText: 'stale preview' }] };
|
|
28
|
+
const settled = definition.renderCall(laterArgs, theme, context).render(80).join('\n');
|
|
29
|
+
assert.match(settled, /executed/);
|
|
30
|
+
assert.doesNotMatch(settled, /stale preview/);
|
|
31
|
+
fold.setFolded(true);
|
|
32
|
+
const folded = definition.renderCall(laterArgs, theme, context).render(80).join('\n');
|
|
33
|
+
assert.match(folded, /\+2/);
|
|
34
|
+
assert.match(folded, /-1/);
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { editorLabel } from '../../../core/canvas/labels.js';
|
|
4
|
+
import { displayEditorTitle } from '../session/editor-frame.js';
|
|
5
|
+
test('a reloaded renamed session still shows its generated icon', () => {
|
|
6
|
+
const identity = { kind: 'general', mode: 'base', icon: '\uf188' };
|
|
7
|
+
assert.equal(displayEditorTitle('test', identity), '\uf188 test');
|
|
8
|
+
assert.equal(displayEditorTitle('\uf188 test', identity), '\uf188 test');
|
|
9
|
+
});
|
|
10
|
+
test('a node restart restores the explicit editor name instead of the generated description', () => {
|
|
11
|
+
const meta = {
|
|
12
|
+
name: 'test',
|
|
13
|
+
kind: 'general',
|
|
14
|
+
mode: 'base',
|
|
15
|
+
description: 'preserve-icon-on-rename',
|
|
16
|
+
icon: '\uf188',
|
|
17
|
+
};
|
|
18
|
+
assert.equal(editorLabel(meta), '\uf188 test');
|
|
19
|
+
});
|
|
@@ -30,7 +30,7 @@ import { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../../core/runtime/situation
|
|
|
30
30
|
import { transformMessageDiagrams } from './diagram.js';
|
|
31
31
|
import { styleAttachMarkdownHeadings, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-headings.js';
|
|
32
32
|
import { createCrtrEditToolDefinition } from './edit-diff.js';
|
|
33
|
-
import {
|
|
33
|
+
import { FoldedToolCallController, createCrtrPlainBashToolDefinition, createCrtrReadToolDefinition, createCrtrWriteToolDefinition, } from './tool-calls.js';
|
|
34
34
|
import { ringCompletionBell } from '../chrome/completion-bell.js';
|
|
35
35
|
import { extractFilePaths } from '../visible-paths.js';
|
|
36
36
|
import { FrozenHistoryComponent, keepWhenCondensed, liveRegionStart } from './frozen-history.js';
|
|
@@ -125,10 +125,15 @@ function stripBackground(line) {
|
|
|
125
125
|
const kept = [];
|
|
126
126
|
for (let i = 0; i < parts.length; i++) {
|
|
127
127
|
const code = Number(parts[i] === '' ? 0 : parts[i]);
|
|
128
|
-
// Extended
|
|
129
|
-
|
|
128
|
+
// Extended colors (`38` foreground, `48` background, `58` underline):
|
|
129
|
+
// consume the full sequence atomically so an RGB channel in the 40-47 or
|
|
130
|
+
// 100-107 range is not mistaken for a standalone background code.
|
|
131
|
+
if (code === 38 || code === 48 || code === 58) {
|
|
130
132
|
const mode = Number(parts[i + 1]);
|
|
131
|
-
i
|
|
133
|
+
const end = i + (mode === 5 ? 2 : mode === 2 ? 4 : 1);
|
|
134
|
+
if (code !== 48)
|
|
135
|
+
kept.push(...parts.slice(i, end + 1));
|
|
136
|
+
i = end;
|
|
132
137
|
continue;
|
|
133
138
|
}
|
|
134
139
|
// Standard (40-47) and bright (100-107) backgrounds. `49` (reset to the
|
|
@@ -142,13 +147,10 @@ function stripBackground(line) {
|
|
|
142
147
|
});
|
|
143
148
|
}
|
|
144
149
|
/** A tool component that can fold itself down to its CALL. pi's `ToolExecutionComponent`
|
|
145
|
-
* only knows expanded/collapsed, so
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* component (result and Box chrome dropped), so a wrapped path keeps its wrap rows
|
|
150
|
-
* naturally — no measuring or slicing of rendered ANSI rows. Tools without a crtr
|
|
151
|
-
* definition are not foldable and always render in full. */
|
|
150
|
+
* only knows expanded/collapsed, so each crtr-owned definition receives a local fold
|
|
151
|
+
* controller and captures the compact call component it renders. Folded render paints
|
|
152
|
+
* that captured call directly, dropping result and Box chrome while letting a wrapped
|
|
153
|
+
* path keep its natural rows. Tools without a crtr definition remain in full. */
|
|
152
154
|
/** A one-line separator that appears only while its adjacent tool group is
|
|
153
155
|
* folded. It stays in the transcript so toggling folded tools reflows the same
|
|
154
156
|
* message/tool boundary without rebuilding the chat. */
|
|
@@ -169,30 +171,36 @@ class FoldedToolSeparator extends Spacer {
|
|
|
169
171
|
}
|
|
170
172
|
class MinimizableToolComponent extends ToolExecutionComponent {
|
|
171
173
|
minimized = false;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
expandedState = false;
|
|
175
|
+
foldController;
|
|
176
|
+
resultObserver;
|
|
177
|
+
/** A foldable definition owns its folded call component; an edit definition
|
|
178
|
+
* may also observe results before the imported shell renders them. */
|
|
179
|
+
setToolView(foldController, resultObserver) {
|
|
180
|
+
this.foldController = foldController;
|
|
181
|
+
this.resultObserver = resultObserver;
|
|
177
182
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
updateResult(...args) {
|
|
184
|
+
this.resultObserver?.(args[0], args[1] ?? false);
|
|
185
|
+
super.updateResult(...args);
|
|
186
|
+
}
|
|
187
|
+
setExpanded(expanded) {
|
|
188
|
+
this.expandedState = expanded;
|
|
189
|
+
super.setExpanded(expanded);
|
|
190
|
+
}
|
|
191
|
+
/** Flip the local fold state, then use the imported component's public
|
|
192
|
+
* expansion setter to rebuild its call renderer at the current setting. */
|
|
183
193
|
setMinimized(minimized) {
|
|
184
|
-
if (!this.
|
|
194
|
+
if (!this.foldController || this.minimized === minimized)
|
|
185
195
|
return;
|
|
186
196
|
this.minimized = minimized;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
internals.updateDisplay();
|
|
197
|
+
this.foldController.setFolded(minimized);
|
|
198
|
+
super.setExpanded(this.expandedState);
|
|
190
199
|
}
|
|
191
200
|
render(width) {
|
|
192
201
|
if (!this.minimized)
|
|
193
202
|
return super.render(width);
|
|
194
|
-
const call = this
|
|
195
|
-
.callRendererComponent;
|
|
203
|
+
const call = this.foldController?.callComponent();
|
|
196
204
|
if (!call)
|
|
197
205
|
return super.render(width);
|
|
198
206
|
// trimEnd drops right-hand padding, which only existed to carry the
|
|
@@ -719,7 +727,7 @@ export class ChatView {
|
|
|
719
727
|
this.pendingTools.clear();
|
|
720
728
|
}
|
|
721
729
|
else {
|
|
722
|
-
//
|
|
730
|
+
// Finalize renderer caches now that every tool argument is complete.
|
|
723
731
|
for (const component of this.pendingTools.values()) {
|
|
724
732
|
component.setArgsComplete();
|
|
725
733
|
this.historyContainer.markDirty(component);
|
|
@@ -1015,18 +1023,26 @@ export class ChatView {
|
|
|
1015
1023
|
}
|
|
1016
1024
|
/** Tool component with native viewer renderers for crouter-owned tool output. */
|
|
1017
1025
|
makeToolComponent(name, id, args) {
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1026
|
+
const fold = new FoldedToolCallController();
|
|
1027
|
+
let resultObserver;
|
|
1028
|
+
let toolDefinition;
|
|
1029
|
+
if (name === 'bash') {
|
|
1030
|
+
toolDefinition = createCrtrBashToolDefinition(args, fold) ?? createCrtrPlainBashToolDefinition(this.cwd, fold);
|
|
1031
|
+
}
|
|
1032
|
+
else if (name === 'edit') {
|
|
1033
|
+
const edit = createCrtrEditToolDefinition(this.cwd, fold);
|
|
1034
|
+
toolDefinition = edit.definition;
|
|
1035
|
+
resultObserver = edit.observeResult;
|
|
1036
|
+
}
|
|
1037
|
+
else if (name === 'read') {
|
|
1038
|
+
toolDefinition = createCrtrReadToolDefinition(this.cwd, fold);
|
|
1039
|
+
}
|
|
1040
|
+
else if (name === 'write') {
|
|
1041
|
+
toolDefinition = createCrtrWriteToolDefinition(this.cwd, fold);
|
|
1042
|
+
}
|
|
1027
1043
|
let component;
|
|
1028
1044
|
component = new MinimizableToolComponent(name, id, args, { showImages: this.showImages, imageWidthCells: this.imageWidthCells }, toolDefinition, this.componentTui(() => component), this.cwd);
|
|
1029
|
-
component.
|
|
1045
|
+
component.setToolView(toolDefinition === undefined ? undefined : fold, resultObserver);
|
|
1030
1046
|
component.setExpanded(this.toolOutputExpanded);
|
|
1031
1047
|
return component;
|
|
1032
1048
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Box } from '@earendil-works/pi-tui';
|
|
2
|
+
import { FoldedToolCallController } from './tool-calls.js';
|
|
2
3
|
type Theme = {
|
|
3
4
|
fg: (name: string, text: string) => string;
|
|
4
5
|
bg: (name: string, text: string) => string;
|
|
@@ -44,5 +45,5 @@ export declare class CrtrOutputMessageComponent extends Box {
|
|
|
44
45
|
constructor(content: string, expanded: boolean, theme: Theme);
|
|
45
46
|
setExpanded(expanded: boolean): void;
|
|
46
47
|
}
|
|
47
|
-
export declare function createCrtrBashToolDefinition(args: unknown): unknown | undefined;
|
|
48
|
+
export declare function createCrtrBashToolDefinition(args: unknown, fold?: FoldedToolCallController): unknown | undefined;
|
|
48
49
|
export {};
|