@crouton-kit/crouter 0.3.17 → 0.3.19
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.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// attach-cmd.ts — the `crtr attach` terminal viewer: core + registration (T7).
|
|
2
|
+
//
|
|
3
|
+
// Wires the already-built render layer (T5 ChatView + extension-dialogs) and
|
|
4
|
+
// input layer (T6 InputController + slash/config/clipboard) together over a
|
|
5
|
+
// single socket to a node's running headless broker, and drives the SAME engine
|
|
6
|
+
// as a controller (or read-only observer). The §0 ONE-WRITER INVARIANT holds by
|
|
7
|
+
// construction: this dir has ONLY a socket — no reviveNode, no `pi --session`,
|
|
8
|
+
// no SessionManager, no `.jsonl` write/open. attach does NOT launch the engine;
|
|
9
|
+
// if the broker isn't running it errors and exits (focus/T9 keeps a broker
|
|
10
|
+
// alive, not attach).
|
|
11
|
+
//
|
|
12
|
+
// Command shape (plan §3.6): the CLI path-walker forbids a flat top-level leaf
|
|
13
|
+
// (`defineRoot.subtrees` is `BranchDef[]`, and a top-level leaf crashes
|
|
14
|
+
// listing-completeness), and a branch rejects a bare positional — so the bare
|
|
15
|
+
// `crtr attach <node>` form is not expressible without a framework change. attach
|
|
16
|
+
// is therefore a BRANCH wrapping a single leaf: `crtr attach to <node>`.
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { Container, ProcessTerminal, Text, TUI, matchesKey, } from '@earendil-works/pi-tui';
|
|
19
|
+
import { CustomEditor, getSelectListTheme, } from '@earendil-works/pi-coding-agent';
|
|
20
|
+
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
21
|
+
import { InputError } from '../../core/io.js';
|
|
22
|
+
import { getNode } from '../../core/canvas/index.js';
|
|
23
|
+
import { ChatView } from './chat-view.js';
|
|
24
|
+
import { InputController } from './input-controller.js';
|
|
25
|
+
import { applyTheme, createKeybindingsManager } from './config-load.js';
|
|
26
|
+
import { BrokerUnavailableError, ViewSocketClient } from './view-socket.js';
|
|
27
|
+
/** Run the interactive attach session against `nodeId`. Resolves when the
|
|
28
|
+
* viewer detaches (ctrl+c/ctrl+d), the broker goes away, or a signal lands —
|
|
29
|
+
* always restoring the terminal first. */
|
|
30
|
+
async function runAttach(nodeId, observer) {
|
|
31
|
+
const meta = getNode(nodeId);
|
|
32
|
+
if (meta === null) {
|
|
33
|
+
throw new InputError({
|
|
34
|
+
error: 'not_found',
|
|
35
|
+
message: `no node: ${nodeId}`,
|
|
36
|
+
received: nodeId,
|
|
37
|
+
next: 'List nodes with `crtr node inspect list`.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const socket = new ViewSocketClient(nodeId);
|
|
41
|
+
// Connect FIRST (race connect/error) so a missing broker exits non-zero
|
|
42
|
+
// cleanly, BEFORE the TUI takes over the terminal.
|
|
43
|
+
await new Promise((resolve, reject) => {
|
|
44
|
+
const onConnect = () => {
|
|
45
|
+
cleanup();
|
|
46
|
+
resolve();
|
|
47
|
+
};
|
|
48
|
+
const onErr = (err) => {
|
|
49
|
+
cleanup();
|
|
50
|
+
reject(err);
|
|
51
|
+
};
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
socket.off('connect', onConnect);
|
|
54
|
+
socket.off('error', onErr);
|
|
55
|
+
};
|
|
56
|
+
socket.on('connect', onConnect);
|
|
57
|
+
socket.on('error', onErr);
|
|
58
|
+
socket.connect();
|
|
59
|
+
}).catch((err) => {
|
|
60
|
+
if (err instanceof BrokerUnavailableError) {
|
|
61
|
+
throw new InputError({
|
|
62
|
+
error: 'no_broker',
|
|
63
|
+
message: err.message,
|
|
64
|
+
received: nodeId,
|
|
65
|
+
next: 'Focus or revive the node first — attach needs a running headless broker.',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
throw new InputError({
|
|
69
|
+
error: 'socket_error',
|
|
70
|
+
message: `could not attach to node ${nodeId}: ${String(err?.message ?? err)}`,
|
|
71
|
+
received: nodeId,
|
|
72
|
+
next: 'Check the node has a running headless broker.',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// 1. Theme + keybindings FIRST — pi's components throw "Theme not initialized"
|
|
76
|
+
// otherwise (T5 contract). One KeybindingsManager feeds BOTH editor + input.
|
|
77
|
+
applyTheme({ cwd: meta.cwd });
|
|
78
|
+
const km = createKeybindingsManager();
|
|
79
|
+
// 2. TUI + chat container + editor.
|
|
80
|
+
const tui = new TUI(new ProcessTerminal());
|
|
81
|
+
const chatContainer = new Container();
|
|
82
|
+
const footer = new Container();
|
|
83
|
+
const editorTheme = { borderColor: (s) => s, selectList: getSelectListTheme() };
|
|
84
|
+
const editor = new CustomEditor(tui, editorTheme, km, { paddingX: 1 });
|
|
85
|
+
// 3. Render layer (T5). ChatView owns the chat scroll + activity spinner; the
|
|
86
|
+
// footer/status line below is ours, so onFooterEvent is left unset (footer
|
|
87
|
+
// updates flow through handleBrokerFrame's single update path instead).
|
|
88
|
+
const chatView = new ChatView(tui, chatContainer, { cwd: meta.cwd });
|
|
89
|
+
// 4. Footer/status line — surfaces role + live state + transient notices.
|
|
90
|
+
const clientId = randomUUID();
|
|
91
|
+
let role = observer ? 'observer' : 'controller';
|
|
92
|
+
let liveState;
|
|
93
|
+
let notice = '';
|
|
94
|
+
const renderFooter = () => {
|
|
95
|
+
const bits = [role === 'controller' ? 'drive' : 'read-only'];
|
|
96
|
+
if (liveState?.model)
|
|
97
|
+
bits.push(liveState.model);
|
|
98
|
+
if (liveState?.isStreaming)
|
|
99
|
+
bits.push('streaming');
|
|
100
|
+
if (liveState?.sessionName)
|
|
101
|
+
bits.push(liveState.sessionName);
|
|
102
|
+
if (liveState && liveState.pendingMessageCount > 0)
|
|
103
|
+
bits.push(`queued ${liveState.pendingMessageCount}`);
|
|
104
|
+
let line = `\x1b[2m${bits.join(' · ')}\x1b[22m`;
|
|
105
|
+
if (notice)
|
|
106
|
+
line += ` \x1b[33m${notice}\x1b[39m`;
|
|
107
|
+
footer.clear();
|
|
108
|
+
footer.addChild(new Text(line, 1, 0));
|
|
109
|
+
tui.requestRender();
|
|
110
|
+
};
|
|
111
|
+
const setNotice = (msg) => {
|
|
112
|
+
notice = msg;
|
|
113
|
+
renderFooter();
|
|
114
|
+
};
|
|
115
|
+
// 5. Input layer (T6). onCommand/onDialogResponse → socket; onNotice → footer.
|
|
116
|
+
const input = new InputController(tui, editor, km, {
|
|
117
|
+
onCommand: (frame) => socket.send(frame),
|
|
118
|
+
onDialogResponse: (resp) => socket.send(resp),
|
|
119
|
+
onNotice: (msg) => setNotice(msg),
|
|
120
|
+
});
|
|
121
|
+
// Feed fresh state into the input controller AND the footer. Steer-vs-prompt
|
|
122
|
+
// routing (T6) reads `state.isStreaming`, so this must stay current — the
|
|
123
|
+
// broker only ships full state in `welcome`, so we patch isStreaming/name/etc
|
|
124
|
+
// from the relayed event stream below.
|
|
125
|
+
const setLiveState = (state) => {
|
|
126
|
+
liveState = state;
|
|
127
|
+
input.setState(state);
|
|
128
|
+
renderFooter();
|
|
129
|
+
};
|
|
130
|
+
const patchState = (patch) => {
|
|
131
|
+
if (liveState === undefined)
|
|
132
|
+
return;
|
|
133
|
+
setLiveState({ ...liveState, ...patch });
|
|
134
|
+
};
|
|
135
|
+
// 6. Drive every broker frame into both layers.
|
|
136
|
+
const handleBrokerFrame = (frame) => {
|
|
137
|
+
switch (frame.type) {
|
|
138
|
+
case 'welcome': {
|
|
139
|
+
role = frame.role;
|
|
140
|
+
chatView.applySnapshot(frame.snapshot);
|
|
141
|
+
setLiveState(frame.snapshot.state);
|
|
142
|
+
if (frame.pending_dialog != null)
|
|
143
|
+
input.attachDialog(frame.pending_dialog);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'control_changed': {
|
|
147
|
+
// We learn our own role by matching the broker's controller_id to the
|
|
148
|
+
// client_id we sent in `hello`.
|
|
149
|
+
role = frame.controller_id === clientId ? 'controller' : 'observer';
|
|
150
|
+
renderFooter();
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'error': {
|
|
154
|
+
if (frame.code === 'not_controller') {
|
|
155
|
+
setNotice('read-only — another viewer is the controller');
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
setNotice(`error: ${frame.message}`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'ack': {
|
|
163
|
+
// Dynamic-command autocomplete (feeding get_commands into T6's palette)
|
|
164
|
+
// is DEFERRED for Phase 4 (would require editing T6's input-controller —
|
|
165
|
+
// out of my 3-file scope; the fall-through-to-prompt path already makes
|
|
166
|
+
// extension commands functional). Surface only command failures here.
|
|
167
|
+
if (!frame.ok) {
|
|
168
|
+
setNotice(`command failed: ${frame.for}${frame.detail ? ` — ${frame.detail}` : ''}`);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case 'extension_ui_request': {
|
|
173
|
+
input.attachDialog(frame);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
default: {
|
|
177
|
+
// A relayed AgentSessionEvent — render it, and keep local state fresh.
|
|
178
|
+
const event = frame;
|
|
179
|
+
chatView.handleEvent(event);
|
|
180
|
+
if (event.type === 'agent_start')
|
|
181
|
+
patchState({ isStreaming: true });
|
|
182
|
+
else if (event.type === 'agent_end')
|
|
183
|
+
patchState({ isStreaming: false });
|
|
184
|
+
else if (event.type === 'session_info_changed')
|
|
185
|
+
patchState({ sessionName: event.name });
|
|
186
|
+
else if (event.type === 'thinking_level_changed')
|
|
187
|
+
patchState({ thinkingLevel: event.level });
|
|
188
|
+
else if (event.type === 'queue_update') {
|
|
189
|
+
patchState({ pendingMessageCount: event.steering.length + event.followUp.length });
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
// 7. Lifecycle / teardown — always restore the terminal, exactly once.
|
|
196
|
+
let restored = false;
|
|
197
|
+
const restore = () => {
|
|
198
|
+
if (restored)
|
|
199
|
+
return;
|
|
200
|
+
restored = true;
|
|
201
|
+
try {
|
|
202
|
+
tui.stop();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* best-effort terminal restore */
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
process.once('exit', restore);
|
|
209
|
+
let tornDown = false;
|
|
210
|
+
// Last socket-level error, so a broker-gone teardown can report the precise
|
|
211
|
+
// reason (e.g. an oversized-frame overflow) instead of the generic message.
|
|
212
|
+
let lastSocketError;
|
|
213
|
+
let resolveDone;
|
|
214
|
+
const done = new Promise((r) => {
|
|
215
|
+
resolveDone = r;
|
|
216
|
+
});
|
|
217
|
+
const teardown = (reason) => {
|
|
218
|
+
if (tornDown)
|
|
219
|
+
return;
|
|
220
|
+
tornDown = true;
|
|
221
|
+
removeKeyListener();
|
|
222
|
+
// A clean detach tells the broker to drop this viewer (the engine runs on);
|
|
223
|
+
// on broker-gone the socket is already dead, so skip `bye`.
|
|
224
|
+
if (reason === 'detach')
|
|
225
|
+
socket.send({ type: 'bye' });
|
|
226
|
+
try {
|
|
227
|
+
chatView.dispose();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
/* ignore */
|
|
231
|
+
}
|
|
232
|
+
socket.close();
|
|
233
|
+
restore();
|
|
234
|
+
if (reason === 'broker-gone') {
|
|
235
|
+
const msg = lastSocketError ??
|
|
236
|
+
`broker gone — node ${nodeId} is no longer running its engine. Re-focus to reattach.`;
|
|
237
|
+
process.stderr.write(`\n${msg}\n`);
|
|
238
|
+
}
|
|
239
|
+
resolveDone();
|
|
240
|
+
};
|
|
241
|
+
// Permanent socket wiring (the connect-race listeners are gone).
|
|
242
|
+
socket.on('frame', handleBrokerFrame);
|
|
243
|
+
socket.on('error', (err) => {
|
|
244
|
+
// Stash the reason; a 'close' follows and teardown converges there, where a
|
|
245
|
+
// broker-gone message prefers this over the generic text.
|
|
246
|
+
lastSocketError = err.message;
|
|
247
|
+
});
|
|
248
|
+
socket.on('close', () => teardown('broker-gone'));
|
|
249
|
+
// ctrl+c / ctrl+d → detach (T6 left these unwired; lifecycle is ours). A
|
|
250
|
+
// global TUI input listener fires BEFORE the focused component (and before any
|
|
251
|
+
// dialog overlay), so detach is unconditional and consistent.
|
|
252
|
+
const removeKeyListener = tui.addInputListener((data) => {
|
|
253
|
+
if (matchesKey(data, 'ctrl+c') || matchesKey(data, 'ctrl+d')) {
|
|
254
|
+
teardown('detach');
|
|
255
|
+
return { consume: true };
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
});
|
|
259
|
+
// Pane kill / daemon teardown arrives as a signal (raw mode swallows ctrl+c as
|
|
260
|
+
// data, so SIGINT is unlikely, but handle it for completeness).
|
|
261
|
+
const onSignal = () => teardown('signal');
|
|
262
|
+
process.once('SIGTERM', onSignal);
|
|
263
|
+
process.once('SIGHUP', onSignal);
|
|
264
|
+
process.once('SIGINT', onSignal);
|
|
265
|
+
// 8. Lay out, focus the editor, start, then handshake. Starting before the
|
|
266
|
+
// handshake means the welcome's applySnapshot renders into a live TUI.
|
|
267
|
+
tui.addChild(chatContainer);
|
|
268
|
+
tui.addChild(footer);
|
|
269
|
+
tui.addChild(editor);
|
|
270
|
+
tui.setFocus(editor);
|
|
271
|
+
renderFooter();
|
|
272
|
+
tui.start();
|
|
273
|
+
socket.send({
|
|
274
|
+
type: 'hello',
|
|
275
|
+
role: observer ? 'observer' : 'controller',
|
|
276
|
+
client_id: clientId,
|
|
277
|
+
term: { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 },
|
|
278
|
+
});
|
|
279
|
+
await done;
|
|
280
|
+
}
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Command registration (§3.6)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
const attachToLeaf = defineLeaf({
|
|
285
|
+
name: 'to',
|
|
286
|
+
description: 'attach an interactive terminal viewer to a node\'s running broker',
|
|
287
|
+
whenToUse: 'you want to WATCH or DRIVE a headless node live in this pane — it connects over the node\'s unix socket to the broker that is already hosting the engine and renders the same chat stream, letting you type prompts (as the controller) or follow read-only (as an observer). It does NOT start the engine: the node must already have a running headless broker (focus or revive it first). One controller drives; extra viewers are read-only. ctrl+c / ctrl+d detaches and leaves the engine running',
|
|
288
|
+
help: {
|
|
289
|
+
name: 'attach to',
|
|
290
|
+
summary: 'attach a terminal viewer to a headless node\'s running broker (controller by default, --observer for read-only); detach with ctrl+c/ctrl+d, the engine runs on',
|
|
291
|
+
params: [
|
|
292
|
+
{
|
|
293
|
+
kind: 'positional',
|
|
294
|
+
name: 'node',
|
|
295
|
+
required: true,
|
|
296
|
+
constraint: 'Node id to attach to. Must already have a running headless broker.',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
kind: 'flag',
|
|
300
|
+
name: 'observer',
|
|
301
|
+
type: 'bool',
|
|
302
|
+
required: false,
|
|
303
|
+
default: false,
|
|
304
|
+
constraint: 'Attach READ-ONLY: never claim control even if it is free. Default: drive (claim control if available, else fall back to read-only).',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
output: [
|
|
308
|
+
{
|
|
309
|
+
name: 'note',
|
|
310
|
+
type: 'string',
|
|
311
|
+
required: false,
|
|
312
|
+
constraint: 'Only set on the non-TTY/piped path (a static notice); the interactive path returns nothing.',
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
outputKind: 'object',
|
|
316
|
+
effects: [
|
|
317
|
+
'Takes over the current pane in raw mode and renders the node\'s live engine stream until you detach (ctrl+c/ctrl+d) or the broker exits.',
|
|
318
|
+
'As controller: sends prompts/steers/dialog answers to the engine over the socket. As observer: read-only.',
|
|
319
|
+
'NEVER spawns pi and NEVER writes the session — it holds only a socket to the existing broker.',
|
|
320
|
+
'Outside a TTY (piped): prints a short notice and exits 0 — attach is an interactive program, not a pipe stage.',
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
run: async (input) => {
|
|
324
|
+
const nodeId = input['node'];
|
|
325
|
+
const observer = input['observer'] ?? false;
|
|
326
|
+
// attach IS the in-pane interactive program — it only needs a TTY (crtr is
|
|
327
|
+
// tmux-only, but the pane itself is the surface). Non-TTY (piped) → static
|
|
328
|
+
// notice + exit, never a non-tmux interactive fallback.
|
|
329
|
+
if (!process.stdout.isTTY) {
|
|
330
|
+
return {
|
|
331
|
+
note: `crtr attach is an interactive terminal viewer — run it in a tmux pane (a TTY), not a pipe. Node: ${nodeId}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
await runAttach(nodeId, observer);
|
|
335
|
+
return;
|
|
336
|
+
},
|
|
337
|
+
render: (result) => (result['note'] !== undefined ? String(result['note']) : ''),
|
|
338
|
+
});
|
|
339
|
+
export function registerAttach() {
|
|
340
|
+
return defineBranch({
|
|
341
|
+
name: 'attach',
|
|
342
|
+
rootEntry: {
|
|
343
|
+
concept: 'a terminal viewer for a headless node — connect over its socket and drive (or watch) the live engine',
|
|
344
|
+
desc: 'attach a terminal viewer to a headless node',
|
|
345
|
+
useWhen: 'you want to open a headless node live in this pane — watch its engine stream and drive it by typing, without the node ever running pi in your window. The node must already have a running headless broker; attach connects to it over a unix socket and never starts the engine itself. Detach (ctrl+c/ctrl+d) and the engine keeps running.',
|
|
346
|
+
},
|
|
347
|
+
help: {
|
|
348
|
+
name: 'attach',
|
|
349
|
+
summary: 'attach a terminal viewer to a headless node\'s running broker',
|
|
350
|
+
model: '`to <node>` opens the viewer in the current pane (tmux-only; a TTY is required — piped it prints a notice and exits). By default it claims control and drives the engine (type to prompt; while the engine is streaming a submit steers the running turn); pass --observer to follow read-only. One controller + N observers per node. ctrl+c / ctrl+d detaches cleanly and the engine runs on; if the broker exits the viewer reports "broker gone" and exits. attach NEVER spawns pi or writes the session — it holds only a socket to the broker, which must already be running (focus or revive the node first).',
|
|
351
|
+
},
|
|
352
|
+
children: [attachToLeaf],
|
|
353
|
+
});
|
|
354
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Container, type TUI } from '@earendil-works/pi-tui';
|
|
2
|
+
import { type AgentSessionEvent } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import type { BrokerSnapshot } from '../../core/runtime/broker-protocol.js';
|
|
4
|
+
export interface ChatViewOptions {
|
|
5
|
+
/** Working dir used by `ToolExecutionComponent` for path/diff display. Defaults
|
|
6
|
+
* to the viewer's cwd; T7 may pass the node's working dir for nicer paths. */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Render inline images in tool output (terminal-dependent). Default true. */
|
|
9
|
+
showImages?: boolean;
|
|
10
|
+
/** Inline image width in cells. Default 60 (pi's default). */
|
|
11
|
+
imageWidthCells?: number;
|
|
12
|
+
/** Collapse the assistant thinking block. Default false. */
|
|
13
|
+
hideThinking?: boolean;
|
|
14
|
+
/** Label shown for a hidden thinking block. Default "Thinking...". */
|
|
15
|
+
hiddenThinkingLabel?: string;
|
|
16
|
+
/** Start tool output expanded. Default false. */
|
|
17
|
+
toolOutputExpanded?: boolean;
|
|
18
|
+
/** Sink for footer/header-only events ChatView does not itself render
|
|
19
|
+
* (queue_update, session_info_changed, thinking_level_changed). The attach
|
|
20
|
+
* command (T7) wires a footer here; absent → these events are no-ops. */
|
|
21
|
+
onFooterEvent?: (event: AgentSessionEvent) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare class ChatView {
|
|
24
|
+
private readonly tui;
|
|
25
|
+
/** The chat scroll area handed in by the attach command. */
|
|
26
|
+
private readonly container;
|
|
27
|
+
/** Activity/status area, pinned as the LAST child of `container` (mirrors pi's
|
|
28
|
+
* statusContainer sitting directly below chatContainer). Holds the working
|
|
29
|
+
* spinner / compaction+retry loaders. */
|
|
30
|
+
private readonly statusContainer;
|
|
31
|
+
private readonly cwd;
|
|
32
|
+
private readonly showImages;
|
|
33
|
+
private readonly imageWidthCells;
|
|
34
|
+
private readonly hideThinking;
|
|
35
|
+
private readonly hiddenThinkingLabel;
|
|
36
|
+
private readonly toolOutputExpanded;
|
|
37
|
+
private readonly onFooterEvent;
|
|
38
|
+
/** The assistant message component currently being streamed (between
|
|
39
|
+
* message_start and message_end for an assistant turn). */
|
|
40
|
+
private streamingComponent;
|
|
41
|
+
/** Tool-call id → its execution component, for the duration of the call. */
|
|
42
|
+
private readonly pendingTools;
|
|
43
|
+
/** The single active activity loader (working/compaction/retry), if any. */
|
|
44
|
+
private activityLoader;
|
|
45
|
+
/** While true, `append` skips re-pinning the status container (bulk rebuild). */
|
|
46
|
+
private bulkMode;
|
|
47
|
+
constructor(tui: TUI, container: Container, opts?: ChatViewOptions);
|
|
48
|
+
/** Render the full message history from a `welcome` snapshot. Ports pi's
|
|
49
|
+
* `renderSessionContext` (interactive-mode.js:2553): assistant messages spawn
|
|
50
|
+
* tool-execution components matched to their toolResult messages; everything
|
|
51
|
+
* else routes through `addMessageToChat`. The TUI's differential renderer +
|
|
52
|
+
* reflow-on-resize is automatic — no scrollback reconstruction needed. */
|
|
53
|
+
applySnapshot(snapshot: BrokerSnapshot): void;
|
|
54
|
+
/** Stop any spinning activity loader. A one-shot `crtr attach` process lets its
|
|
55
|
+
* timers die with the process, but if T7 reuses a ChatView across reconnects in
|
|
56
|
+
* one process it should call this on detach to avoid a leaked interval. */
|
|
57
|
+
dispose(): void;
|
|
58
|
+
handleEvent(event: AgentSessionEvent): void;
|
|
59
|
+
private addMessageToChat;
|
|
60
|
+
/** Default-path tool component (toolDef undefined → builtin/default rendering). */
|
|
61
|
+
private makeToolComponent;
|
|
62
|
+
/** Append a child while keeping the activity/status area pinned to the bottom. */
|
|
63
|
+
private append;
|
|
64
|
+
/** Number of chat children excluding the pinned status container. */
|
|
65
|
+
private chatChildCount;
|
|
66
|
+
/** Clear all chat content and re-pin an empty status container. */
|
|
67
|
+
private resetChat;
|
|
68
|
+
private makeLoader;
|
|
69
|
+
/** Swap the single active activity indicator (working/compaction/retry). */
|
|
70
|
+
private setActivity;
|
|
71
|
+
private showStatus;
|
|
72
|
+
private showError;
|
|
73
|
+
/** Extract the tool-call content items from an assistant message. */
|
|
74
|
+
private assistantToolCalls;
|
|
75
|
+
/** Concatenate the text blocks of a user message (string or content array). */
|
|
76
|
+
private userMessageText;
|
|
77
|
+
}
|