@north-light/crouter 0.3.169 → 0.3.171
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/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/insights/capture.md +66 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
- package/dist/clients/attach/__tests__/context-message.test.js +50 -3
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +35 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +7 -18
- package/dist/clients/attach/input/controller.d.ts +10 -0
- package/dist/clients/attach/input/controller.js +3 -0
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/chat-view.d.ts +81 -9
- package/dist/clients/attach/render/chat-view.js +434 -90
- package/dist/clients/attach/render/context-message.js +3 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/measured-container.d.ts +94 -0
- package/dist/clients/attach/render/measured-container.js +307 -0
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +160 -0
- package/dist/clients/attach/session/bindings.d.ts +5 -1
- package/dist/clients/attach/session/bindings.js +16 -0
- package/dist/clients/attach/session/context.d.ts +5 -3
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +173 -0
- package/dist/clients/attach/session/frames.d.ts +2 -0
- package/dist/clients/attach/session/frames.js +3 -0
- package/dist/clients/attach/session/input-wiring.d.ts +10 -4
- package/dist/clients/attach/session/input-wiring.js +13 -13
- package/dist/clients/attach/session/keys.d.ts +4 -1
- package/dist/clients/attach/session/keys.js +9 -4
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +32 -31
- package/dist/clients/attach/session/mouse.d.ts +15 -0
- package/dist/clients/attach/session/mouse.js +61 -0
- package/dist/clients/attach/slash/dispatch.d.ts +10 -0
- package/dist/clients/attach/slash/dispatch.js +35 -2
- package/dist/clients/attach/viewer.js +605 -581
- package/dist/clients/web/dev-server.d.ts +4 -1
- package/dist/clients/web/dev-server.js +8 -19
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/human/prompts.js +16 -6
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
- package/dist/commands/sys/setup-core.js +21 -16
- package/dist/core/__tests__/human-deliver.test.js +62 -2
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +9 -13
- package/dist/core/command.js +12 -17
- package/dist/core/config.js +2 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +8 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -1
- package/dist/core/runtime/broker.js +213 -5
- package/dist/core/runtime/canvas-extensions.d.ts +3 -0
- package/dist/core/runtime/canvas-extensions.js +3 -0
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +5 -2
- package/dist/core/runtime/node-read.js +15 -0
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +117 -0
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +91 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/user-settings.d.ts +14 -0
- package/dist/core/user-settings.js +20 -0
- package/dist/daemon/crtrd.js +5 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -2
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/pi-extensions/summary-tool.d.ts +33 -0
- package/dist/pi-extensions/summary-tool.js +62 -0
- package/dist/shared/generated-context.d.ts +19 -4
- package/dist/shared/generated-context.js +98 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -1
- package/runtime.lock.json +62 -3
- package/scripts/postinstall.mjs +14 -1
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- package/dist/core/runtime/fault-recovery-nudge.js +0 -3
package/dist/build-root.d.ts
CHANGED
|
@@ -24,20 +24,5 @@ export declare function resolveRoot(first: string | undefined): Promise<RootDef>
|
|
|
24
24
|
* Composition is invocation-local and reads only stored manifest bytes — no
|
|
25
25
|
* cache, no persistence, ZERO network — so a plugin enable/update/remove or an
|
|
26
26
|
* HTTP-transport plugin manifest replacement takes effect on the next invocation
|
|
27
|
-
* automatically.
|
|
28
|
-
* isolated in `hydrateAbsentStoresAndRebuild`, off this composition path. */
|
|
27
|
+
* automatically. Composition performs no network I/O. */
|
|
29
28
|
export declare function buildRoot(): Promise<RootDef>;
|
|
30
|
-
/** The ONE unknown-first-token retry step (amended spec §5.1, §8), invoked by
|
|
31
|
-
* dispatch only when an unknown FIRST token misses (not a core command, not
|
|
32
|
-
* any stored external contribution) — never for a deeper unknown token under a
|
|
33
|
-
* known command. Hydrate EVERY registered HTTP-transport plugin whose manifest store
|
|
34
|
-
* is absent exactly once, write one stderr diagnostic per FAILED hydration,
|
|
35
|
-
* and — if anything was hydrated — return a freshly rebuilt root so dispatch
|
|
36
|
-
* can re-walk the original argv once. Returns undefined when nothing was
|
|
37
|
-
* hydrated (no absent stores, or every attempt failed): the caller then
|
|
38
|
-
* surfaces the plain `unknown_path`/USAGE error, with the failed-hydration
|
|
39
|
-
* diagnostics already emitted and NO NETWORK precedence over that outcome.
|
|
40
|
-
* When every registration holds a stored manifest this performs ZERO network
|
|
41
|
-
* I/O and returns undefined. It is the ONLY network the dispatch path can
|
|
42
|
-
* trigger; composition (`buildRoot`) never fetches. */
|
|
43
|
-
export declare function hydrateAbsentStoresAndRebuild(): Promise<RootDef | undefined>;
|
package/dist/build-root.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { defineRoot } from './core/command.js';
|
|
2
|
-
import { diag } from './core/io.js';
|
|
3
2
|
const TAGLINE = 'crtr: agentic runtime.';
|
|
4
3
|
/** Lazy registry: subtree name → dynamic importer that builds its BranchDef.
|
|
5
4
|
* The whole point is that each `import()` only compiles that one subtree's
|
|
@@ -52,8 +51,7 @@ export async function resolveRoot(first) {
|
|
|
52
51
|
* Composition is invocation-local and reads only stored manifest bytes — no
|
|
53
52
|
* cache, no persistence, ZERO network — so a plugin enable/update/remove or an
|
|
54
53
|
* HTTP-transport plugin manifest replacement takes effect on the next invocation
|
|
55
|
-
* automatically.
|
|
56
|
-
* isolated in `hydrateAbsentStoresAndRebuild`, off this composition path. */
|
|
54
|
+
* automatically. Composition performs no network I/O. */
|
|
57
55
|
export async function buildRoot() {
|
|
58
56
|
const core = await Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
|
|
59
57
|
// Dynamic import keeps external command discovery/compose (plugin +
|
|
@@ -71,25 +69,3 @@ export async function buildRoot() {
|
|
|
71
69
|
const external = composeExternalSubtrees(snapshot);
|
|
72
70
|
return defineRoot({ tagline: TAGLINE, globals: [], subtrees: [...core, ...external] });
|
|
73
71
|
}
|
|
74
|
-
/** The ONE unknown-first-token retry step (amended spec §5.1, §8), invoked by
|
|
75
|
-
* dispatch only when an unknown FIRST token misses (not a core command, not
|
|
76
|
-
* any stored external contribution) — never for a deeper unknown token under a
|
|
77
|
-
* known command. Hydrate EVERY registered HTTP-transport plugin whose manifest store
|
|
78
|
-
* is absent exactly once, write one stderr diagnostic per FAILED hydration,
|
|
79
|
-
* and — if anything was hydrated — return a freshly rebuilt root so dispatch
|
|
80
|
-
* can re-walk the original argv once. Returns undefined when nothing was
|
|
81
|
-
* hydrated (no absent stores, or every attempt failed): the caller then
|
|
82
|
-
* surfaces the plain `unknown_path`/USAGE error, with the failed-hydration
|
|
83
|
-
* diagnostics already emitted and NO NETWORK precedence over that outcome.
|
|
84
|
-
* When every registration holds a stored manifest this performs ZERO network
|
|
85
|
-
* I/O and returns undefined. It is the ONLY network the dispatch path can
|
|
86
|
-
* trigger; composition (`buildRoot`) never fetches. */
|
|
87
|
-
export async function hydrateAbsentStoresAndRebuild() {
|
|
88
|
-
const { hydrateAbsentHttpPlugins } = await import('./core/command-plugins/discovery.js');
|
|
89
|
-
const { hydratedAny, diagnostics } = await hydrateAbsentHttpPlugins();
|
|
90
|
-
for (const line of diagnostics)
|
|
91
|
-
diag(line);
|
|
92
|
-
if (!hydratedAny)
|
|
93
|
-
return undefined;
|
|
94
|
-
return buildRoot();
|
|
95
|
-
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When an initialized insight listener or a taste-capture preference identifies a possible reusable principle in user-supplied information, this knowledge should be read because immediate review separates durable user insight from plausible-sounding agent extrapolation before it shapes future decisions.
|
|
4
|
+
short-form: Extract one coherent principle from user input, review terse candidate truths immediately, then create or revise semantic memory only from what the user approves.
|
|
5
|
+
system-prompt-visibility: none
|
|
6
|
+
file-read-visibility: none
|
|
7
|
+
rationale: Agents have both missed deeper user insight hidden in ordinary feedback and saved their own unreviewed interpretation as fundamental truth; the user needs to edit, delete, or sharpen the inferred principle before it becomes standing semantic memory.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Confirm a user-derived insight before saving it
|
|
11
|
+
|
|
12
|
+
The calling memory defines the domain, destination, and final document shape. This workflow owns the extraction and review boundary. No semantic memory is written until the user approves the inferred truth.
|
|
13
|
+
|
|
14
|
+
## Decide whether there is a principle
|
|
15
|
+
|
|
16
|
+
Eligible source material is information supplied or validated by the user: ordinary messages, corrections, humanloop answers, anchored review comments and line edits, and responses to claims an agent surfaced. Model output alone is not user insight.
|
|
17
|
+
|
|
18
|
+
A source episode qualifies only when you can state a coherent, non-obvious principle that would inform materially different future decisions. Routine requirements clarification, implementation feedback, wording changes, plan convergence, and facts already recorded in code or a specification do not qualify.
|
|
19
|
+
|
|
20
|
+
A correction is a signal, not proof. Infer the deeper reason beneath the immediate symptom, but do not save the literal conversational before-and-after as the principle.
|
|
21
|
+
|
|
22
|
+
## Find the existing home
|
|
23
|
+
|
|
24
|
+
Search the destination directory before drafting. If the source refines, bounds, or strengthens an existing principle, propose a rewrite of that document. Create a new document only for a distinct read trigger that can evolve independently. One document may accumulate insight from several source episodes.
|
|
25
|
+
|
|
26
|
+
## Review one coherent principle immediately
|
|
27
|
+
|
|
28
|
+
Write a small Markdown artifact under the current node's absolute context directory and queue it with `crtr human review`. Do not batch it with unrelated principles and do not ask a separate confirmation question first.
|
|
29
|
+
|
|
30
|
+
Use this shape:
|
|
31
|
+
|
|
32
|
+
```markdown
|
|
33
|
+
# Proposed insight: <short principle name>
|
|
34
|
+
|
|
35
|
+
## Source context
|
|
36
|
+
|
|
37
|
+
<Only enough user words or surrounding context to judge the inference. Prefer an exact quote.>
|
|
38
|
+
|
|
39
|
+
## Current truth
|
|
40
|
+
|
|
41
|
+
<Include only when revising an existing memory.>
|
|
42
|
+
|
|
43
|
+
## Proposed durable truth
|
|
44
|
+
|
|
45
|
+
- <terse candidate principle or facet>
|
|
46
|
+
- <another candidate only when it adds a real distinction or boundary>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Candidate bullets are reverse-engineered hypotheses, not polished prose. Generate several when the deeper insight has multiple plausible formulations, and accept that the user may delete most of them. Do not pad to a target count, explain every bullet, or hide uncertainty in a long essay.
|
|
50
|
+
|
|
51
|
+
The review is asynchronous. Continue other work or end the turn and let the submitted review wake this node; never poll it.
|
|
52
|
+
|
|
53
|
+
## Apply only approved truth
|
|
54
|
+
|
|
55
|
+
Apply direct line edits and explicit comments from the review. If feedback requires you to invent a materially new formulation rather than transcribe the user's correction, revise the artifact and review that formulation again. If no candidate survives, save nothing.
|
|
56
|
+
|
|
57
|
+
Then run `crtr memory write -h` and create or rewrite the approved destination document. For an initialized `insights/<topic>/` listener:
|
|
58
|
+
|
|
59
|
+
- store the principle as knowledge unless its actual use is to direct behavior;
|
|
60
|
+
- keep the body to the current distilled truth as short declarative bullets;
|
|
61
|
+
- use `none` visibility on both axes so the topic index is the one router; and
|
|
62
|
+
- add a link to its canonical principle document in the topic index without duplicating the insight there.
|
|
63
|
+
|
|
64
|
+
When the calling preference routes to `taste/` or another established destination, preserve that destination's own body and routing contract while keeping the same review-before-write boundary.
|
|
65
|
+
|
|
66
|
+
Source context and agent reasoning stay in the review artifact. Do not copy them into the approved memory or add provenance machinery. Run `crtr memory lint` after every create or rewrite and fix every finding.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: knowledge
|
|
3
|
+
when-and-why-to-read: When the user invokes /insights:init to begin gathering user-derived insight about a domain, this knowledge should be read because a correctly scoped listener preserves uniquely valuable knowledge without creating a parallel store or background system.
|
|
4
|
+
short-form: Initialize passive insight gathering for a domain at the narrowest durable scope; create one routed topic index that reviews user-derived principles before saving them.
|
|
5
|
+
system-prompt-visibility: none
|
|
6
|
+
file-read-visibility: none
|
|
7
|
+
slash: true
|
|
8
|
+
rationale: Ordinary conversations, corrections, humanloop answers, and review comments carry unique user knowledge that agents inconsistently recognize or save; when agents do infer a deeper principle, they have written it without first letting the user correct the extrapolation.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# /insights:init — begin passive domain listening
|
|
12
|
+
|
|
13
|
+
Initialize an ordinary memory directory that listens for user-derived insight about this domain. This version is passive: do not start research, schedule work, create a standing node, or proactively question the user after initialization.
|
|
14
|
+
|
|
15
|
+
**Requested domain:** $ARGUMENTS
|
|
16
|
+
|
|
17
|
+
## Establish the topic and scope
|
|
18
|
+
|
|
19
|
+
Turn the request into a short path-safe topic slug and a compact domain boundary that preserves the user's meaning rather than reducing it to a single keyword. If the request is empty or too vague to distinguish relevant from unrelated information, ask one focused question through `crtr human ask`.
|
|
20
|
+
|
|
21
|
+
Choose the narrowest durable scope that reaches every future conversation where this domain matters:
|
|
22
|
+
|
|
23
|
+
- project when the insight is meaningful only inside one project;
|
|
24
|
+
- profile when it spans the selected profile's projects but should not follow the user elsewhere; or
|
|
25
|
+
- user when it concerns the person, a market, a craft, or a domain that crosses workspaces.
|
|
26
|
+
|
|
27
|
+
Infer the scope from the request and current workspace. Ask through `crtr human ask` only when more than one scope is genuinely plausible, and settle scope before writing anything. Never use node scope for an ongoing listener.
|
|
28
|
+
|
|
29
|
+
Search the chosen scope before creating. If `insights/<topic>` already represents the same domain, refine that listener rather than creating an overlapping directory.
|
|
30
|
+
|
|
31
|
+
## Create the listener
|
|
32
|
+
|
|
33
|
+
Run `crtr memory write -h`, then create `insights/<topic>/INDEX.md` at the chosen scope as a preference with system-prompt visibility `preview` and file-read visibility `none`.
|
|
34
|
+
|
|
35
|
+
Its routing line must name the actual domain trigger: when user-supplied information, a correction, or a user response relates to this domain, read the preference because recognizing the underlying principle preserves knowledge future decisions can use.
|
|
36
|
+
|
|
37
|
+
Keep the body short. It contains:
|
|
38
|
+
|
|
39
|
+
- the domain boundary, including exclusions needed to prevent false triggers;
|
|
40
|
+
- a directive to follow [[insights/capture]] whenever a source episode may reveal a reusable principle; and
|
|
41
|
+
- an `Approved insights` section containing document links to principle memories as they are approved.
|
|
42
|
+
|
|
43
|
+
Do not copy the capture workflow into the listener. The link keeps that process in one maintained place. Do not create placeholder principle documents.
|
|
44
|
+
|
|
45
|
+
Run `crtr memory lint`, fix every finding, then report the canonical listener name, selected scope, and the domain boundary. Initialization is complete once the routed listener exists; no independent work follows.
|
|
@@ -64,12 +64,12 @@ The `<plugin-name>` directory IS the plugin. The manifest's `name` field must ma
|
|
|
64
64
|
| Field | Required | Notes |
|
|
65
65
|
|---|---|---|
|
|
66
66
|
| `name` | yes | Must match the directory name. Lowercase kebab. |
|
|
67
|
-
| `version` | yes | Semver. Marketplace CI may bump automatically
|
|
67
|
+
| `version` | yes for authored plugins | Semver. Marketplace CI may bump automatically; archive installs synthesize a 12-character content identifier. |
|
|
68
68
|
| `description` | yes | One sentence. |
|
|
69
|
-
| `source` | recommended |
|
|
69
|
+
| `source` | recommended | Human-facing URL where the plugin lives. Source-installed plugin updates derive provenance from their checkout or marketplace registration, not this descriptive field. |
|
|
70
70
|
| `owner` | optional | Author info. |
|
|
71
71
|
| `commands` | optional | Plugin-root-relative path to a `commands.json` command manifest. It must appear with `transport`; together they make the plugin contribute `crtr` commands — see [Plugin commands](#plugin-commands). |
|
|
72
|
-
| `transport` | optional | Required exactly when `commands` is present. `{ "kind": "exec", "executable": "bin/cmd.js" }` runs executable leaves; a passthrough-only exec manifest may omit `executable`. `{ "kind": "http", "endpoint": "https://…", "authEnv": "TOKEN_NAME" }` calls a remote HTTP command surface. |
|
|
72
|
+
| `transport` | optional | Required exactly when `commands` is present. `{ "kind": "exec", "executable": "bin/cmd.js" }` runs executable leaves; a passthrough-only exec manifest may omit `executable`. `{ "kind": "http", "endpoint": "https://…", "authEnv": "TOKEN_NAME" }` calls a remote HTTP command surface. Archive-installed plugins also carry crouter-synthesized `bundle` provenance; authors do not add it. |
|
|
73
73
|
|
|
74
74
|
## Scopes
|
|
75
75
|
|
|
@@ -96,9 +96,10 @@ Four ways a plugin lands in a scope:
|
|
|
96
96
|
- `crtr pkg market update --name <mkt>` refreshes the marketplace and every installed plugin it sources; relative sources follow the checkout, while remote-source checkouts pull their own Git remote.
|
|
97
97
|
- See [[internal/marketplaces]].
|
|
98
98
|
|
|
99
|
-
3. **From an HTTP
|
|
100
|
-
- Fetches
|
|
101
|
-
- The
|
|
99
|
+
3. **From an HTTP directory archive** (`crtr pkg plugin install --endpoint <url> --name <name> --scope user`):
|
|
100
|
+
- Fetches one authenticated uncompressed `application/x-tar` archive containing `bundle.json`, `commands.json`, and `memory/**/*.md`. crouter validates every member before writing, then materializes the complete package with a synthesized `.crouter-plugin/plugin.json`. The endpoint must use `https:`; `http:` is accepted only for `localhost`, `127.0.0.1`, `[::1]`, `::1`, or `host.docker.internal`, with no credentials or fragment.
|
|
101
|
+
- The synthesized `bundle` field records the archive endpoint and optional credential variable name for installs and updates. Its distinct `transport` field records the archive endpoint's origin for command invocation. crouter stores the environment variable name, never its token.
|
|
102
|
+
- Reinstalling a bundle plugin replaces commands and memory together. Fetch or validation failure leaves the prior package intact. A commands-only HTTP plugin predating this archive contract must be removed and reinstalled.
|
|
102
103
|
|
|
103
104
|
4. **Authored in place** (you're writing the plugin in a working repo):
|
|
104
105
|
- Symlink for tight dev loop: `ln -s $(pwd) ~/.crouter/plugins/<name>`.
|
|
@@ -136,7 +137,7 @@ Standard semver:
|
|
|
136
137
|
| New doc, new section, new example | minor (0.1.0 → 0.2.0) |
|
|
137
138
|
| Removed doc, renamed doc, changed manifest schema | major (0.1.0 → 1.0.0) |
|
|
138
139
|
|
|
139
|
-
`crtr pkg plugin update --name <name>` pulls source updates for ordinary
|
|
140
|
+
`crtr pkg plugin update --name <name>` pulls source updates for ordinary plugins, including source-installed plugins whose command transport is HTTP. An archive plugin instead re-fetches its recorded `bundle` endpoint and replaces its complete package atomically; its version is the first 12 hex characters of the fetched archive SHA-256. A failed named update preserves the prior package and exits nonzero; bulk update reports the failure and continues. Plugins published through a marketplace may have their `version` field bumped automatically by CI — see [[internal/marketplaces]].
|
|
140
141
|
|
|
141
142
|
## Enable/disable
|
|
142
143
|
|
|
@@ -180,12 +181,12 @@ Beyond docs, a plugin may contribute **top-level `crtr` commands** — new noun
|
|
|
180
181
|
An HTTP-transport plugin replaces that declaration with:
|
|
181
182
|
|
|
182
183
|
```json
|
|
183
|
-
"transport": { "kind": "http", "endpoint": "https://example.com
|
|
184
|
+
"transport": { "kind": "http", "endpoint": "https://example.com", "authEnv": "DEPLOY_TOKEN" }
|
|
184
185
|
```
|
|
185
186
|
|
|
186
|
-
For `exec`, `executable` is required when the manifest has any executable leaf; a passthrough-only manifest omits it. When declared, it is plugin-root-relative, resolves inside the plugin root to a regular file, and carries the POSIX exec bit. For `http`, `endpoint` is
|
|
187
|
+
For `exec`, `executable` is required when the manifest has any executable leaf; a passthrough-only manifest omits it. When declared, it is plugin-root-relative, resolves inside the plugin root to a regular file, and carries the POSIX exec bit. For `http`, `transport.endpoint` is the backend origin and `authEnv` is optional. crouter stores only the environment variable **name**, never its credential; it reads the credential when invoking a leaf. A crouter-materialized archive plugin additionally records its archive URL separately in `bundle` for installation and update.
|
|
187
188
|
|
|
188
|
-
Only an installed, **enabled** plugin's command manifest contributes. Discovery is per-invocation: enable, disable, update, and remove take effect on the next `crtr` call
|
|
189
|
+
Only an installed, **enabled** plugin's command manifest contributes. Discovery is per-invocation and reads the installed `commands.json` only: enable, disable, update, and remove take effect on the next `crtr` call with no daemon restart, cache clearing, or network fetch.
|
|
189
190
|
|
|
190
191
|
### commands.json shape
|
|
191
192
|
|
|
@@ -239,7 +240,7 @@ For an exec leaf, crtr writes exactly one JSON request to the executable's stdin
|
|
|
239
240
|
|
|
240
241
|
- **Generate an exec plugin's `commands.json` from its command definitions — never hand-write it.** The manifest must stay in lockstep with the executable's command surface; drifted params or output fields surface as a validation issue or `plugin_protocol_error` at invocation.
|
|
241
242
|
- **A required output field must be non-null.** The adapter treats an explicit `null` for a declared-required field as absent → `plugin_protocol_error`. If a value is optional, declare it `required: false`; if it is required, return a real value.
|
|
242
|
-
- **HTTP
|
|
243
|
+
- **HTTP archive plugins are complete package bytes.** `crtr pkg plugin install --endpoint <url> --name <name>` fetches and validates the complete archive before replacing the plugin directory. `crtr pkg plugin update` uses the recorded `bundle` provenance to do the same replacement. The installed `commands.json` and memory tree are authoritative with no TTL, ETag, revalidation, or dispatch-time network path.
|
|
243
244
|
|
|
244
245
|
### Validation and command collisions
|
|
245
246
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { openSync } from 'node:fs';
|
|
|
4
4
|
import { enableCompileCache } from 'node:module';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { runCli } from './core/command.js';
|
|
7
|
-
import { resolveRoot
|
|
7
|
+
import { resolveRoot } from './build-root.js';
|
|
8
8
|
import { maybeBootRoot } from './core/runtime/front-door.js';
|
|
9
9
|
import { maybeAutoUpdate } from './core/auto-update.js';
|
|
10
10
|
import { ensureOfficialMarketplace, ensureProjectScope } from './core/bootstrap.js';
|
|
@@ -57,12 +57,8 @@ async function main() {
|
|
|
57
57
|
ensureOfficialMarketplace(process.argv);
|
|
58
58
|
ensureProjectScope(process.argv);
|
|
59
59
|
maybeAutoUpdate(process.argv);
|
|
60
|
-
// Pass the one-shot unknown-first-token retry seam: on a first-token miss with
|
|
61
|
-
// absent HTTP-plugin stores, hydrate them once, rebuild, and re-walk.
|
|
62
|
-
// A recognized core first token resolves in `resolveRoot`'s fast path and never
|
|
63
|
-
// reaches a first-token miss, so the seam stays off the core fast path.
|
|
64
60
|
mark('cli.dispatch_start');
|
|
65
|
-
await runCli(root, process.argv
|
|
61
|
+
await runCli(root, process.argv);
|
|
66
62
|
mark('cli.dispatch_done');
|
|
67
63
|
}
|
|
68
64
|
main().catch((err) => {
|
|
@@ -28,6 +28,7 @@ test('attach direct and private control input dispatch through the same action m
|
|
|
28
28
|
'crtr.attach.detach': ['ctrl+x'],
|
|
29
29
|
'crtr.attach.clear-or-detach': [],
|
|
30
30
|
'crtr.attach.graph.toggle': [],
|
|
31
|
+
'crtr.attach.help.toggle': ['alt+shift+k'],
|
|
31
32
|
'crtr.mode.cycle': ['alt+x'],
|
|
32
33
|
'crtr.attach.model-ladder.next': ['alt+z'],
|
|
33
34
|
'crtr.attach.model-ladder.previous': ['alt+y'],
|
|
@@ -40,6 +41,7 @@ test('attach direct and private control input dispatch through the same action m
|
|
|
40
41
|
'crtr.attach.detach': () => calls.push('detach'),
|
|
41
42
|
'crtr.attach.clear-or-detach': () => calls.push('clear'),
|
|
42
43
|
'crtr.attach.graph.toggle': () => calls.push('graph'),
|
|
44
|
+
'crtr.attach.help.toggle': () => calls.push('help'),
|
|
43
45
|
'crtr.mode.cycle': () => calls.push('mode'),
|
|
44
46
|
'crtr.attach.model-ladder.next': () => calls.push('ladder'),
|
|
45
47
|
'crtr.attach.model-ladder.previous': () => calls.push('ladder-back'),
|
|
@@ -57,12 +59,16 @@ test('attach direct and private control input dispatch through the same action m
|
|
|
57
59
|
assert.equal(dispatchAttachBinding(bindings, '\x1by', actions), true);
|
|
58
60
|
assert.equal(dispatchAttachBinding(bindings, '\x1bM', actions), false, 'old ladder-back key must not reach the listener action');
|
|
59
61
|
assert.equal(dispatchAttachBinding(bindings, '\x1b[72;4u', actions), true);
|
|
62
|
+
assert.equal(dispatchAttachBinding(bindings, '\x1b[75;4u', actions), true);
|
|
60
63
|
assert.equal(dispatchAttachBinding(bindings, '\x06', actions), false, 'disabled direct binding must stay disabled');
|
|
61
64
|
const privateInput = encodeAttachControlInput('crtr.attach.profile-files');
|
|
62
65
|
const privateAction = decodeAttachControlInput(privateInput);
|
|
63
66
|
assert.equal(privateAction, 'crtr.attach.profile-files');
|
|
64
67
|
dispatchAttachAction(privateAction, actions);
|
|
65
|
-
|
|
68
|
+
const helpPrivateInput = encodeAttachControlInput('crtr.attach.help.toggle');
|
|
69
|
+
assert.equal(decodeAttachControlInput(helpPrivateInput), 'crtr.attach.help.toggle');
|
|
70
|
+
dispatchAttachAction('crtr.attach.help.toggle', actions);
|
|
71
|
+
assert.deepEqual(calls, ['detach', 'mode', 'ladder', 'ladder-back', 'inspect', 'help', 'profile-files', 'help']);
|
|
66
72
|
assert.equal(decodeAttachControlInput(`${privateInput}x`), undefined, 'whole-input decoding must be exact');
|
|
67
73
|
assert.equal(decodeAttachControlInput('\x1b_crtr;crtr.attach.not-real\x1b\\'), undefined, 'unknown actions must stay ordinary input');
|
|
68
74
|
assert.deepEqual(extractAttachControlInput(`before${privateInput}after`), {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Regression coverage for the attach viewer's display-only folding of
|
|
2
2
|
// crouter-generated context. The body must stay agent-visible and expandable,
|
|
3
|
-
// while bearings, situational updates, fresh-revive kickoffs,
|
|
4
|
-
// and node inbox digests share one compact renderer by default.
|
|
3
|
+
// while bearings, situational updates, fresh-revive kickoffs, runtime steering,
|
|
4
|
+
// recovery prompts, and node inbox digests share one compact renderer by default.
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
@@ -13,7 +13,7 @@ import { applyTheme, attachMarkdownTheme } from '../config.js';
|
|
|
13
13
|
import { ChatView } from '../render/chat-view.js';
|
|
14
14
|
import { ContextMessageComponent } from '../render/context-message.js';
|
|
15
15
|
import { styleAttachMarkdownHeadings } from '../render/markdown-headings.js';
|
|
16
|
-
import { CONTEXT_NUDGE_CUSTOM_TYPE, REVIVE_KICKOFF_SENTINEL, generatedContextPresentation, } from '../../../shared/generated-context.js';
|
|
16
|
+
import { AUTH_FAULT_RECOVERY_BODY, CONNECTION_FAULT_RECOVERY_BODY, CONTEXT_NUDGE_CUSTOM_TYPE, PROVIDER_FAULT_RECOVERY_BODY, REVIVE_KICKOFF_SENTINEL, RUNTIME_RESTART_CONTINUATION, STALL_REPROMPT, formatModelFallbackRecovery, formatPersonaTransition, formatStructuredOutputReprompt, generatedContextPresentation, } from '../../../shared/generated-context.js';
|
|
17
17
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const CHAT_VIEW = readFileSync(join(HERE, '..', 'render/chat-view.ts'), 'utf8');
|
|
19
19
|
const identity = (s) => s;
|
|
@@ -51,6 +51,27 @@ test('generated rows add no leading spacer after prior transcript content', asyn
|
|
|
51
51
|
assert.notEqual(generatedIndex, -1);
|
|
52
52
|
assert.match(lines[generatedIndex - 1] ?? '', /before/, 'the collapsed row follows prior content directly');
|
|
53
53
|
});
|
|
54
|
+
test('Ctrl+O expands generated context while showing individual summarized calls', async () => {
|
|
55
|
+
const container = new Container();
|
|
56
|
+
const tui = { requestRender() { }, terminal: { rows: 40 } };
|
|
57
|
+
const view = new ChatView(tui, container, { toolOutputExpanded: false });
|
|
58
|
+
const internals = view;
|
|
59
|
+
internals.summarizeToolCalls = true;
|
|
60
|
+
internals.foldSettledTools = true;
|
|
61
|
+
internals.showGroupSummaries = true;
|
|
62
|
+
await view.applySnapshot({
|
|
63
|
+
messages: [{ role: 'user', content: RUNTIME_RESTART_CONTINUATION }],
|
|
64
|
+
stats: {},
|
|
65
|
+
state: { isStreaming: false },
|
|
66
|
+
});
|
|
67
|
+
assert.doesNotMatch(container.render(80).join('\n'), /runtime-restart-continuation/);
|
|
68
|
+
assert.deepEqual(view.toggleToolsExpanded(), { kind: 'summaries', shown: false });
|
|
69
|
+
assert.equal(internals.toolOutputExpanded, true);
|
|
70
|
+
assert.match(container.render(80).join('\n'), /runtime-restart-continuation/);
|
|
71
|
+
assert.deepEqual(view.toggleToolsExpanded(), { kind: 'summaries', shown: true });
|
|
72
|
+
assert.equal(internals.toolOutputExpanded, false);
|
|
73
|
+
assert.doesNotMatch(container.render(80).join('\n'), /runtime-restart-continuation/);
|
|
74
|
+
});
|
|
54
75
|
test('the crtr-context label/hint keep their defaults when callers omit the new params', () => {
|
|
55
76
|
const bearings = new ContextMessageComponent('some bearings body', false, identity, identity);
|
|
56
77
|
const line = bearings.render(80).join('\n');
|
|
@@ -80,6 +101,32 @@ test('fresh-revive kickoff and both context-nudge roles classify as generated co
|
|
|
80
101
|
assert.equal(inbox?.label, 'crtr inbox');
|
|
81
102
|
assert.equal(inbox?.summary, '2 updates from child-node-123');
|
|
82
103
|
});
|
|
104
|
+
test('runtime steering and recovery prompts classify without hiding their full bodies', () => {
|
|
105
|
+
const personaBody = formatPersonaTransition('You are now an orchestrator.');
|
|
106
|
+
const persona = generatedContextPresentation({ role: 'user', content: personaBody });
|
|
107
|
+
assert.equal(persona?.kind, 'persona-transition');
|
|
108
|
+
assert.equal(persona?.body, personaBody);
|
|
109
|
+
const structuredBody = formatStructuredOutputReprompt(JSON.stringify({ type: 'object' }, null, 2));
|
|
110
|
+
assert.equal(generatedContextPresentation({ role: 'user', content: STALL_REPROMPT })?.summary, 'completion required');
|
|
111
|
+
assert.equal(generatedContextPresentation({ role: 'user', content: structuredBody })?.summary, 'structured output required');
|
|
112
|
+
const recoveries = [
|
|
113
|
+
[AUTH_FAULT_RECOVERY_BODY, 'provider credentials updated'],
|
|
114
|
+
[CONNECTION_FAULT_RECOVERY_BODY, 'network connection restored'],
|
|
115
|
+
[PROVIDER_FAULT_RECOVERY_BODY, 'provider retry'],
|
|
116
|
+
[formatModelFallbackRecovery('old/model', 'new/model:high', 'credential'), 'model route changed'],
|
|
117
|
+
[formatModelFallbackRecovery('custom(provider)/model', 'new/model:high', 'not-found'), 'model route changed'],
|
|
118
|
+
[RUNTIME_RESTART_CONTINUATION, 'runtime restarted'],
|
|
119
|
+
];
|
|
120
|
+
for (const [body, summary] of recoveries) {
|
|
121
|
+
const presentation = generatedContextPresentation({ role: 'user', content: body });
|
|
122
|
+
assert.equal(presentation?.kind, 'recovery');
|
|
123
|
+
assert.equal(presentation?.summary, summary);
|
|
124
|
+
assert.equal(presentation?.body, body);
|
|
125
|
+
}
|
|
126
|
+
assert.equal(generatedContextPresentation({ role: 'user', content: 'continue' }), null, 'a human one-word continue is not mistaken for runtime recovery');
|
|
127
|
+
const unmarkedModelRecovery = formatModelFallbackRecovery('custom(provider)/model', 'new/model:high', 'not-found').split('\n').slice(1, -1).join('\n');
|
|
128
|
+
assert.equal(generatedContextPresentation({ role: 'user', content: unmarkedModelRecovery }), null, 'a human message matching the recovery prose lacks the producer marker');
|
|
129
|
+
});
|
|
83
130
|
test('render/chat-view.ts routes the shared generated-context classification through the foldable component', () => {
|
|
84
131
|
assert.match(CHAT_VIEW, /generatedContextPresentation\(message\)/);
|
|
85
132
|
assert.match(CHAT_VIEW, /new ContextMessageComponent\(/);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import { ensureRenderer, isRendererReady } from '
|
|
3
|
+
import { ensureRenderer, isRendererReady } from '../../../core/termrender/termrender.js';
|
|
4
4
|
import { transformDiagramFences } from '../render/diagram.js';
|
|
5
5
|
// This asserts a REAL termrender render (box-drawing glyphs), so it only holds
|
|
6
6
|
// once the managed renderer is provisioned — true in the full tier and on a dev
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Container } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { SessionState } from '../../../core/session-model/session-state.js';
|
|
3
|
+
import type { AttachSession } from '../session/context.js';
|
|
4
|
+
export interface RecapHeader {
|
|
5
|
+
/** The component ViewerFrame renders as its header region. */
|
|
6
|
+
readonly component: Container;
|
|
7
|
+
/** Repaint from the live `crtr-recap` widget state. Empty widget → 0 rows. */
|
|
8
|
+
render(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function createRecapHeader(ctx: Pick<AttachSession, 'tui' | 'pal'>, state: () => SessionState): RecapHeader;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// chrome/header.ts — the pinned recap header above the transcript.
|
|
2
|
+
//
|
|
3
|
+
// The broker's retained `crtr-recap` widget is painted here rather than in the
|
|
4
|
+
// bottom cockpit so it stays visible while the transcript scrolls.
|
|
5
|
+
import { Container, Text, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
6
|
+
import { DynamicBorder } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
/** A recap line is `Goal:`/`State:` + body — bold the label, faint the body.
|
|
8
|
+
* Styled BEFORE wrapping (wrapTextWithAnsi carries SGR state across wraps), so a
|
|
9
|
+
* wrapped continuation stays faint. Unlabeled lines are all-faint. */
|
|
10
|
+
const styleRecapLine = (line, pal) => {
|
|
11
|
+
const match = /^(Goal|State):\s*/.exec(line);
|
|
12
|
+
if (match === null)
|
|
13
|
+
return pal.faint(line);
|
|
14
|
+
return `${pal.bold(match[1] + ':')} ${pal.faint(line.slice(match[0].length))}`;
|
|
15
|
+
};
|
|
16
|
+
export function createRecapHeader(ctx, state) {
|
|
17
|
+
const { tui, pal } = ctx;
|
|
18
|
+
const component = new Container();
|
|
19
|
+
const render = () => {
|
|
20
|
+
component.clear();
|
|
21
|
+
const widget = state().display.widgets['crtr-recap'];
|
|
22
|
+
if (widget !== undefined && widget.lines.length > 0) {
|
|
23
|
+
const span = Math.max(1, (process.stdout.columns ?? 80) - 2);
|
|
24
|
+
for (const line of widget.lines) {
|
|
25
|
+
const styled = styleRecapLine(line, pal);
|
|
26
|
+
for (const wrapped of wrapTextWithAnsi(styled, span)) {
|
|
27
|
+
component.addChild(new Text(wrapped, 1, 0));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
component.addChild(new DynamicBorder(pal.border));
|
|
31
|
+
}
|
|
32
|
+
tui.requestRender();
|
|
33
|
+
};
|
|
34
|
+
return { component, render };
|
|
35
|
+
}
|
|
@@ -20,6 +20,9 @@ export interface StatusLineHooks {
|
|
|
20
20
|
* a LOCAL in-tmux attach: a remote attach's local inbox registry cannot open
|
|
21
21
|
* the remote canvas's filesystem tickets. */
|
|
22
22
|
inboxAffordance: boolean;
|
|
23
|
+
/** Whether mouse reporting is currently enabled. When false, displays a
|
|
24
|
+
* low-priority 'mouse off' indicator in the status bar. */
|
|
25
|
+
mouseEnabled: () => boolean;
|
|
23
26
|
}
|
|
24
27
|
export interface AttachStatusLine {
|
|
25
28
|
/** Repaint the bar from current state. */
|
|
@@ -76,6 +76,11 @@ export function createStatusLine(s, hooks) {
|
|
|
76
76
|
segs.push(pal.muted(`⚑ ${total} · ${inboxOpenHint(bindings)}`));
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
// Mouse reporting state: show "mouse off" when reporting is disabled, so
|
|
80
|
+
// users are aware that tmux drag-select and copy-mode mouse work again.
|
|
81
|
+
if (!hooks.mouseEnabled()) {
|
|
82
|
+
segs.push(pal.muted('mouse off'));
|
|
83
|
+
}
|
|
79
84
|
// Lowest-priority segment (R4): the Alt+C -> b hint, shown only once the
|
|
80
85
|
// oldest still-foreground bash job has crossed the threshold. Read straight
|
|
81
86
|
// off this node's job files rather than tracked from relayed tool-execution
|
|
@@ -7,33 +7,22 @@ import { DynamicBorder } from '@earendil-works/pi-coding-agent';
|
|
|
7
7
|
export function createChromeWidgets(ctx, state) {
|
|
8
8
|
const { tui, pal } = ctx;
|
|
9
9
|
const { chrome, aboveWidgets, belowWidgets, queued } = ctx.containers;
|
|
10
|
-
/**
|
|
11
|
-
* Styled BEFORE wrapping (wrapTextWithAnsi carries SGR state across wraps),
|
|
12
|
-
* so a wrapped continuation stays faint. Unlabeled lines are all-faint. */
|
|
13
|
-
const styleRecapLine = (line) => {
|
|
14
|
-
const m = /^(Goal|State):\s*/.exec(line);
|
|
15
|
-
if (m === null)
|
|
16
|
-
return pal.faint(line);
|
|
17
|
-
return `${pal.bold(m[1] + ':')} ${pal.faint(line.slice(m[0].length))}`;
|
|
18
|
-
};
|
|
19
|
-
/** Paint serializable extension widgets into their fixed cockpit slots. The
|
|
20
|
-
* recap sits immediately above the editor border and uses the faint palette:
|
|
21
|
-
* persistent orientation, but visually a side note rather than primary UI. */
|
|
10
|
+
/** Paint serializable extension widgets into their fixed cockpit slots. */
|
|
22
11
|
const renderExtensionWidgets = () => {
|
|
23
12
|
const session = state();
|
|
24
13
|
aboveWidgets.clear();
|
|
25
14
|
belowWidgets.clear();
|
|
26
|
-
// The
|
|
27
|
-
//
|
|
15
|
+
// The chrome container is the pinned transcript/cockpit divider. Recap is
|
|
16
|
+
// painted by the pinned header instead of being part of this cockpit slot.
|
|
28
17
|
chrome.clear();
|
|
29
|
-
|
|
30
|
-
chrome.addChild(new DynamicBorder(pal.border));
|
|
18
|
+
chrome.addChild(new DynamicBorder(pal.border));
|
|
31
19
|
const span = Math.max(1, (process.stdout.columns ?? 80) - 2);
|
|
32
20
|
for (const [key, widget] of Object.entries(session.display.widgets)) {
|
|
21
|
+
if (key === 'crtr-recap')
|
|
22
|
+
continue;
|
|
33
23
|
const target = widget.placement === 'belowEditor' ? belowWidgets : aboveWidgets;
|
|
34
24
|
for (const line of widget.lines) {
|
|
35
|
-
const
|
|
36
|
-
for (const wrapped of wrapTextWithAnsi(styled, span)) {
|
|
25
|
+
for (const wrapped of wrapTextWithAnsi(line, span)) {
|
|
37
26
|
target.addChild(new Text(wrapped, 1, 0));
|
|
38
27
|
}
|
|
39
28
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CustomEditor } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { Component, KeybindingsManager, TUI } from '@earendil-works/pi-tui';
|
|
3
3
|
import { type BrokerDataFrame, type ClientRole, type BrokerSnapshot, type ClientToBroker, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from '../../../core/runtime/broker-protocol.js';
|
|
4
|
+
import type { CopyTarget } from '../render/transcript-copy.js';
|
|
4
5
|
import type { ReadOpRequest } from '../../../core/broker-client/index.js';
|
|
5
6
|
import { type Picker, type PickerControls } from '../overlays/pickers.js';
|
|
6
7
|
export interface InputControllerHooks {
|
|
@@ -33,12 +34,21 @@ export interface InputControllerHooks {
|
|
|
33
34
|
/** OPTIONAL: toggle the GRAPH overlay — forwarded to the slash context so
|
|
34
35
|
* `/graph` opens/closes it (Unit Q wires it from runAttach). */
|
|
35
36
|
onGraph?: () => void;
|
|
37
|
+
/** OPTIONAL: toggle the Help overlay — forwarded to the slash context so
|
|
38
|
+
* `/hotkeys` opens/closes it. */
|
|
39
|
+
onHelp?: () => void;
|
|
36
40
|
/** OPTIONAL: detach the viewer — forwarded to the slash context so `/quit`
|
|
37
41
|
* tears down the TUI/socket (the engine keeps running). Wired by viewer.ts. */
|
|
38
42
|
onQuit?: () => void;
|
|
39
43
|
/** OPTIONAL: copy the last assistant message to the clipboard — forwarded to
|
|
40
44
|
* the slash context so `/copy` works. Wired by viewer.ts (it owns ChatView). */
|
|
41
45
|
onCopy?: () => void;
|
|
46
|
+
/** OPTIONAL: copy a viewer-local transcript target — forwarded to the slash
|
|
47
|
+
* context so `/copy-message`, `/copy-tool`, and `/copy-transcript` work. */
|
|
48
|
+
onCopyTarget?: (target: CopyTarget) => void;
|
|
49
|
+
/** OPTIONAL: toggle mouse wheel reporting — forwarded to the slash context so
|
|
50
|
+
* `/mouse` can change viewer-local mouse state. */
|
|
51
|
+
onToggleMouse?: () => void;
|
|
42
52
|
/** OPTIONAL: recolor the editor's name chip — forwarded to the slash context
|
|
43
53
|
* so `/color` paints it (`null` clears). Wired by viewer.ts (it owns the
|
|
44
54
|
* TitledEditor). */
|
|
@@ -240,8 +240,11 @@ export class InputController {
|
|
|
240
240
|
remote: this.hooks.remote,
|
|
241
241
|
role: this.hooks.role,
|
|
242
242
|
onGraph: this.hooks.onGraph,
|
|
243
|
+
onHelp: this.hooks.onHelp,
|
|
243
244
|
onQuit: this.hooks.onQuit,
|
|
244
245
|
onCopy: this.hooks.onCopy,
|
|
246
|
+
onCopyTarget: this.hooks.onCopyTarget,
|
|
247
|
+
onToggleMouse: this.hooks.onToggleMouse,
|
|
245
248
|
onColor: this.hooks.onColor,
|
|
246
249
|
onNodeMetadata: this.hooks.onNodeMetadata,
|
|
247
250
|
onFoldTools: this.hooks.onFoldTools,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Component, type TUI } from '@earendil-works/pi-tui';
|
|
2
|
+
import type { AttachPalette } from '../config.js';
|
|
3
|
+
import type { AttachBindings } from '../session/bindings.js';
|
|
4
|
+
/** A bounded, focused reference for the attach viewer's viewport controls. */
|
|
5
|
+
export declare class HelpOverlay implements Component {
|
|
6
|
+
private readonly tui;
|
|
7
|
+
private readonly pal;
|
|
8
|
+
private handle;
|
|
9
|
+
private bindings;
|
|
10
|
+
private scrollTop;
|
|
11
|
+
private lastContentRows;
|
|
12
|
+
constructor(tui: TUI, pal: AttachPalette);
|
|
13
|
+
/** Re-resolve key spellings after `/reload` (mirrors GraphOverlay.setBindings). */
|
|
14
|
+
setBindings(bindings: AttachBindings): void;
|
|
15
|
+
toggle(): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
isOpen(): boolean;
|
|
18
|
+
invalidate(): void;
|
|
19
|
+
render(width: number): string[];
|
|
20
|
+
handleInput(data: string): boolean;
|
|
21
|
+
private open;
|
|
22
|
+
private matchesBinding;
|
|
23
|
+
private scrollBy;
|
|
24
|
+
private pageSize;
|
|
25
|
+
private body;
|
|
26
|
+
private lines;
|
|
27
|
+
private binding;
|
|
28
|
+
private wrap;
|
|
29
|
+
private style;
|
|
30
|
+
private boxRow;
|
|
31
|
+
private borderRow;
|
|
32
|
+
private renderNarrow;
|
|
33
|
+
}
|