@crouton-kit/crouter 0.3.82 → 0.3.84
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-pi-packages/pi-mode-switch/README.md +3 -3
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +21 -4
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +17 -44
- package/dist/clients/attach/attach-cmd.d.ts +2 -3
- package/dist/clients/attach/attach-cmd.js +689 -689
- package/dist/clients/attach/attach-def.d.ts +2 -0
- package/dist/clients/attach/attach-def.js +93 -0
- package/dist/clients/attach/canvas-panels.d.ts +10 -3
- package/dist/clients/attach/canvas-panels.js +42 -16
- package/dist/clients/attach/chat-view.d.ts +20 -1
- package/dist/clients/attach/chat-view.js +85 -2
- package/dist/commands/__tests__/human.test.js +7 -22
- package/dist/commands/attention.js +31 -30
- package/dist/commands/human/prompts.js +57 -38
- package/dist/commands/human/queue.d.ts +1 -28
- package/dist/commands/human/queue.js +156 -520
- package/dist/commands/human/shared.d.ts +28 -68
- package/dist/commands/human/shared.js +37 -120
- package/dist/commands/human.js +15 -17
- package/dist/commands/node.js +5 -2
- package/dist/commands/surface.js +1 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
- package/dist/commands/sys/__tests__/setup-front-door.test.js +85 -0
- package/dist/commands/sys/setup-core.d.ts +10 -0
- package/dist/commands/sys/setup-core.js +39 -1
- package/dist/commands/sys/setup.js +10 -1
- package/dist/core/__tests__/boot.test.js +141 -6
- package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
- package/dist/core/__tests__/human-deliver.test.js +332 -0
- package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
- package/dist/core/__tests__/pid-identity-match.test.js +145 -0
- package/dist/core/__tests__/tmux-surface.test.js +180 -2
- package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/attention.test.js +126 -0
- package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
- package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
- package/dist/core/canvas/attention.d.ts +16 -14
- package/dist/core/canvas/attention.js +34 -29
- package/dist/core/canvas/boot.d.ts +45 -0
- package/dist/core/canvas/boot.js +36 -3
- package/dist/core/canvas/browse/app.js +3 -3
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +65 -1
- package/dist/core/canvas/pid.d.ts +86 -17
- package/dist/core/canvas/pid.js +152 -37
- package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
- package/dist/core/canvas/remote-canvas-source.js +4 -2
- package/dist/core/canvas/render.d.ts +5 -5
- package/dist/core/canvas/render.js +14 -14
- package/dist/core/canvas/source.d.ts +2 -2
- package/dist/core/canvas/source.js +3 -3
- package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/inbox-affordance.test.js +38 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +2 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +4 -2
- package/dist/core/keybindings/inbox.d.ts +21 -0
- package/dist/core/keybindings/inbox.js +35 -0
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/preview-registry.js +23 -29
- package/dist/core/profiles/select.js +4 -4
- package/dist/core/runtime/pi-vendored.js +15 -1
- package/dist/core/runtime/placement.d.ts +1 -10
- package/dist/core/runtime/placement.js +43 -26
- package/dist/core/runtime/session-list-cache.d.ts +22 -2
- package/dist/core/runtime/session-list-cache.js +92 -24
- package/dist/core/runtime/spawn.js +2 -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 +1 -0
- package/dist/core/runtime/tmux.js +55 -1
- package/dist/core/spawn.d.ts +0 -59
- package/dist/core/spawn.js +5 -163
- package/dist/daemon/crtrd.js +36 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +2 -2
- package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
- package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
- package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
- package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
- package/dist/core/__tests__/human-surface-target.test.js +0 -98
- package/dist/web-client/assets/index--md2ylfi.js +0 -78
- /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
- /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
- /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// attach-def.ts — the LIGHT `crtr surface attach` command definition.
|
|
2
|
+
//
|
|
3
|
+
// This module holds only the command metadata (help schema, flags) and the
|
|
4
|
+
// thin `run` entry. It is deliberately kept OFF the heavy pi-coding-agent module
|
|
5
|
+
// graph so that registering the `surface` subtree — for `surface view`,
|
|
6
|
+
// `surface web`, `surface workspace`, `surface tmux-spread`, and every
|
|
7
|
+
// `surface -h` render — costs a cheap command-framework load, not the ~363ms
|
|
8
|
+
// ESM load of the esbuild-bundled attach viewer (`attach-cmd.ts`, which pulls
|
|
9
|
+
// pi-coding-agent's babel/highlight.js). The viewer runtime is dynamically
|
|
10
|
+
// imported inside `run`, so the bundle loads ONLY when a viewer actually opens.
|
|
11
|
+
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
12
|
+
const attachToLeaf = defineLeaf({
|
|
13
|
+
name: 'to',
|
|
14
|
+
description: 'attach an interactive terminal viewer to a node\'s running broker',
|
|
15
|
+
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. The configured detach shortcuts leave the engine running',
|
|
16
|
+
help: {
|
|
17
|
+
name: 'attach to',
|
|
18
|
+
summary: 'attach a terminal viewer to a headless node\'s running broker (controller by default, --observer for read-only); configured detach shortcuts leave the engine running',
|
|
19
|
+
params: [
|
|
20
|
+
{
|
|
21
|
+
kind: 'positional',
|
|
22
|
+
name: 'node',
|
|
23
|
+
required: true,
|
|
24
|
+
constraint: 'Node id to attach to. Must already have a running headless broker.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
kind: 'flag',
|
|
28
|
+
name: 'observer',
|
|
29
|
+
type: 'bool',
|
|
30
|
+
required: false,
|
|
31
|
+
default: false,
|
|
32
|
+
constraint: 'Attach READ-ONLY: never claim control even if it is free. Default: drive (claim control if available, else fall back to read-only).',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
kind: 'flag',
|
|
36
|
+
name: 'canvas',
|
|
37
|
+
type: 'string',
|
|
38
|
+
required: false,
|
|
39
|
+
constraint: 'Attach against a remote canvas target configured via `crtr canvas config add` instead of the local canvas — connects over a relayed WebSocket to the target\'s preview endpoint using the relay token from the 0600 secrets store. Omit for local (default): connects to the node\'s local unix socket, which must already exist in THIS machine\'s canvas.db. Does NOT honor `crtr canvas use` — this flag is the only way to attach remotely.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
output: [
|
|
43
|
+
{
|
|
44
|
+
name: 'note',
|
|
45
|
+
type: 'string',
|
|
46
|
+
required: false,
|
|
47
|
+
constraint: 'Only set on the non-TTY/piped path (a static notice); the interactive path returns nothing.',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
outputKind: 'object',
|
|
51
|
+
effects: [
|
|
52
|
+
'Takes over the current pane in raw mode and renders the node\'s live engine stream until you use a configured detach shortcut or the broker exits.',
|
|
53
|
+
'As controller: sends prompts/steers/dialog answers to the engine over the socket. As observer: read-only.',
|
|
54
|
+
'NEVER spawns pi and NEVER writes the session — it holds only a socket to the existing broker.',
|
|
55
|
+
'Outside a TTY (piped): prints a short notice and exits 0 — attach is an interactive program, not a pipe stage.',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
run: async (input) => {
|
|
59
|
+
const nodeId = input['node'];
|
|
60
|
+
const observer = input['observer'] ?? false;
|
|
61
|
+
const canvasName = input['canvas'];
|
|
62
|
+
// attach IS the in-pane interactive program — it only needs a TTY (crtr is
|
|
63
|
+
// tmux-only, but the pane itself is the surface). Non-TTY (piped) → static
|
|
64
|
+
// notice + exit, never a non-tmux interactive fallback. Guarded BEFORE the
|
|
65
|
+
// heavy import so a piped invocation never pays the viewer's load cost.
|
|
66
|
+
if (!process.stdout.isTTY) {
|
|
67
|
+
return {
|
|
68
|
+
note: `crtr surface attach is an interactive terminal viewer — run it in a tmux pane (a TTY), not a pipe. Node: ${nodeId}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// The viewer runtime (the pi-coding-agent-heavy bundle) loads only here.
|
|
72
|
+
const { runAttach } = await import('./attach-cmd.js');
|
|
73
|
+
await runAttach(nodeId, observer, canvasName);
|
|
74
|
+
return;
|
|
75
|
+
},
|
|
76
|
+
render: (result) => (result['note'] !== undefined ? String(result['note']) : ''),
|
|
77
|
+
});
|
|
78
|
+
export function registerAttach() {
|
|
79
|
+
return defineBranch({
|
|
80
|
+
name: 'attach',
|
|
81
|
+
rootEntry: {
|
|
82
|
+
concept: 'a terminal viewer for a headless node — connect over its socket and drive (or watch) the live engine',
|
|
83
|
+
desc: 'attach a terminal viewer to a headless node',
|
|
84
|
+
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. Configured detach shortcuts leave the engine running.',
|
|
85
|
+
},
|
|
86
|
+
help: {
|
|
87
|
+
name: 'attach',
|
|
88
|
+
summary: 'attach a terminal viewer to a headless node\'s running broker',
|
|
89
|
+
model: 'The branch opens a viewer in the current pane. A TTY is required; piped invocation prints a notice and exits. By default it claims control and drives the engine; --observer follows read-only. One controller plus any number of observers may watch a node. Configured detach shortcuts detach cleanly and the engine runs on. If the broker exits, the viewer holds the pane and auto-reconnects — through a yield/refresh relaunch, and across dormancy until the node wakes again — reporting "broker gone" and exiting only when the node is terminal (done/dead/canceled) or deleted. attach NEVER spawns pi or writes the session — it holds only a socket to the broker, which must already be running. --canvas <name> is the escape hatch for a node hosted on a different machine\'s canvas — see the leaf\'s own -h.',
|
|
90
|
+
},
|
|
91
|
+
children: [attachToLeaf],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -3,10 +3,17 @@ import type { AttachPalette } from './config-load.js';
|
|
|
3
3
|
export interface CanvasPanelLines {
|
|
4
4
|
/** Above-editor manager line(s) — empty for a root node (no manager). */
|
|
5
5
|
managers: string[];
|
|
6
|
-
/** Below-editor report row(s) + self's trailing ⚑
|
|
6
|
+
/** Below-editor report row(s) + self's trailing ⚑ pending-ticket line — empty when none. */
|
|
7
7
|
reports: string[];
|
|
8
|
+
/** Node ids backing the manager row(s), in render order — the roster's UP leg
|
|
9
|
+
* (toward main). Drives the attach viewer's inline scroll+swap selection. */
|
|
10
|
+
managerIds: string[];
|
|
11
|
+
/** Node ids backing the report row(s), in render order — the roster's DOWN leg
|
|
12
|
+
* (the live subagents). Excludes the trailing ⚑ self-asks line. */
|
|
13
|
+
reportIds: string[];
|
|
8
14
|
}
|
|
9
15
|
/** Build the BASE panel line stacks for `nodeId`. `asks` is the per-node
|
|
10
|
-
* pending-
|
|
16
|
+
* pending human-ticket count map the caller polls (deck/review/notify); pass
|
|
17
|
+
* `{}` to omit the ⚑ badges. `palette`
|
|
11
18
|
* drives the headers + dim cells; omit it and the panels render uncolored. */
|
|
12
|
-
export declare function buildCanvasPanelLines(nodeId: string, asks: Record<string, number>, palette?: AttachPalette, source?: CanvasSource): Promise<CanvasPanelLines>;
|
|
19
|
+
export declare function buildCanvasPanelLines(nodeId: string, asks: Record<string, number>, palette?: AttachPalette, source?: CanvasSource, selectedId?: string): Promise<CanvasPanelLines>;
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// canvas-panels.ts — the subscribed-node panel for `crtr surface attach` (Unit Q).
|
|
2
2
|
//
|
|
3
3
|
// Reads through the CanvasSource seam so the viewer chrome and graph overlay
|
|
4
|
-
// share the same canvas read authority. Produces the
|
|
4
|
+
// share the same canvas read authority. Produces the line stacks the viewer paints
|
|
5
5
|
// into Containers around the editor:
|
|
6
|
-
// managers → ABOVE the editor
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// managers → always empty: nothing is shown ABOVE the editor. The manager is
|
|
7
|
+
// folded into the one list below (see reports), so the roster is a
|
|
8
|
+
// single stable list, not a split above/below layout.
|
|
9
|
+
// reports → BELOW the editor: the ONE stable family roster — the subagent
|
|
10
|
+
// set's root (self's non-human manager, or self when it is the
|
|
11
|
+
// root) followed by that root's live (active|idle) children — plus
|
|
12
|
+
// a trailing ⚑ line for self's own pending human tickets (asks,
|
|
13
|
+
// reviews, notifications). The SAME list shows for every family
|
|
14
|
+
// member; the one this viewer is attached to is the highlighted
|
|
15
|
+
// row. Backs the attach viewer's inline scroll+swap.
|
|
9
16
|
//
|
|
10
|
-
// Pure: given a node id + the per-node
|
|
17
|
+
// Pure: given a node id + the per-node ticket-count map it returns ANSI strings; the
|
|
11
18
|
// caller owns the Containers and the refresh trigger. beginFrame() clears the
|
|
12
19
|
// nav-model telemetry cache for this one build pass so token/activity cells are
|
|
13
20
|
// read fresh for every chrome frame.
|
|
14
|
-
import { beginFrame, navLabel, nodeGlyph, truncate, tokensCell, cycleBadge, askBadge, activityCell, YELLOW, RESET, DIM, GREEN, } from '../../core/canvas/nav-model.js';
|
|
21
|
+
import { beginFrame, navLabel, nodeGlyph, truncate, tokensCell, cycleBadge, askBadge, activityCell, fillBar, fillWidth, YELLOW, RESET, DIM, GREEN, REVERSE, BG_ATTACHED, } from '../../core/canvas/nav-model.js';
|
|
15
22
|
import { localCanvasSource } from '../../core/canvas/source.js';
|
|
16
23
|
import { RemoteCanvasSource } from '../../core/canvas/remote-canvas-source.js';
|
|
17
24
|
/** Column widths for the aligned panel rows (visible columns). name is sized to
|
|
@@ -30,9 +37,10 @@ function padStartVisible(s, w) {
|
|
|
30
37
|
return s.length >= w ? s.slice(0, w) : ' '.repeat(w - s.length) + s;
|
|
31
38
|
}
|
|
32
39
|
/** Build the BASE panel line stacks for `nodeId`. `asks` is the per-node
|
|
33
|
-
* pending-
|
|
40
|
+
* pending human-ticket count map the caller polls (deck/review/notify); pass
|
|
41
|
+
* `{}` to omit the ⚑ badges. `palette`
|
|
34
42
|
* drives the headers + dim cells; omit it and the panels render uncolored. */
|
|
35
|
-
export async function buildCanvasPanelLines(nodeId, asks, palette, source = localCanvasSource) {
|
|
43
|
+
export async function buildCanvasPanelLines(nodeId, asks, palette, source = localCanvasSource, selectedId) {
|
|
36
44
|
// A remote source provides only graph structure + node metadata; tokens/
|
|
37
45
|
// activity/status-glyph adornments are local disk reads (telemetry.json,
|
|
38
46
|
// fault state, pid liveness) and are suppressed to safe defaults below —
|
|
@@ -126,9 +134,20 @@ export async function buildCanvasPanelLines(nodeId, asks, palette, source = loca
|
|
|
126
134
|
const muted = palette?.muted ?? ((s) => s);
|
|
127
135
|
const faint = palette?.faint ?? ((s) => s);
|
|
128
136
|
await computeActivity(await climbRoot(nodeId));
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
// ONE stable family roster, shown BELOW the editor and NOTHING above it
|
|
138
|
+
// (Claude-Code style): the root of this subagent set — self's (non-human)
|
|
139
|
+
// manager, or self when it is the root — followed by that root's live children.
|
|
140
|
+
// The SAME list appears no matter which family member you're viewing; the one
|
|
141
|
+
// you're in is the highlighted row. The manager is folded in as the top entry
|
|
142
|
+
// (root-first), so "up to main" is just scrolling up the same list — no
|
|
143
|
+
// separate parent panel above the box. A solo node with no family shows nothing.
|
|
144
|
+
const mgrRaw = await managerOf(nodeId);
|
|
145
|
+
const mgr = mgrRaw !== undefined && (await node(mgrRaw))?.kind !== 'human' ? mgrRaw : undefined;
|
|
146
|
+
const rosterRoot = mgr ?? nodeId;
|
|
147
|
+
const kids = await liveReports(rosterRoot);
|
|
148
|
+
const family = [rosterRoot, ...kids.filter((id) => id !== rosterRoot)];
|
|
149
|
+
const live = family.length > 1 ? family : [];
|
|
150
|
+
const gridIds = live;
|
|
132
151
|
const gridNodes = await Promise.all(gridIds.map((id) => node(id)));
|
|
133
152
|
const nameW = gridIds.length > 0
|
|
134
153
|
? Math.min(NAME_CAP, Math.max(...gridIds.map((id, i) => navLabel(gridNodes[i] ?? null, id).length)))
|
|
@@ -139,16 +158,23 @@ export async function buildCanvasPanelLines(nodeId, asks, palette, source = loca
|
|
|
139
158
|
const kind = faint(nameCell(n?.kind ?? '', KIND_W));
|
|
140
159
|
const tokens = muted(padStartVisible(tokensCell(id, remote), TOK_W));
|
|
141
160
|
const badges = `${cycleBadge(n)}${await childBadgeFor(n)}${liveBelowBadgeFor(n)}${askBadge(id, asks)}${activityCell(id, n, remote)}`;
|
|
142
|
-
|
|
161
|
+
const line = ` ${nodeGlyph(n, remote)} ${name} ${kind} ${tokens}${badges}`;
|
|
162
|
+
// Two roster highlights: the roving nav cursor (strong REVERSE, mirroring the
|
|
163
|
+
// graph overlay's cursor) wins; otherwise the node this viewer is CURRENTLY
|
|
164
|
+
// attached to — self, when it sits among its siblings — gets the subtler
|
|
165
|
+
// you-are-here bar so you always see which subagent you're in.
|
|
166
|
+
if (id === selectedId)
|
|
167
|
+
return fillBar(line, fillWidth(), REVERSE);
|
|
168
|
+
if (id === nodeId)
|
|
169
|
+
return fillBar(line, fillWidth(), BG_ATTACHED);
|
|
170
|
+
return truncate(line);
|
|
143
171
|
};
|
|
144
|
-
const managers = [];
|
|
145
|
-
if (mgr !== undefined)
|
|
146
|
-
managers.push(await row(mgr));
|
|
147
172
|
const reports = [];
|
|
148
173
|
for (const id of live)
|
|
149
174
|
reports.push(await row(id));
|
|
150
175
|
const selfAsks = asks[nodeId] ?? 0;
|
|
151
176
|
if (selfAsks > 0)
|
|
152
177
|
reports.push(` ${YELLOW}⚑ ${selfAsks} pending${RESET}`);
|
|
153
|
-
|
|
178
|
+
// Nothing above the editor: the whole family lives in the one list below.
|
|
179
|
+
return { managers: [], reports, managerIds: [], reportIds: [...live] };
|
|
154
180
|
}
|
|
@@ -59,6 +59,17 @@ export declare class ChatView {
|
|
|
59
59
|
private bashComponent;
|
|
60
60
|
/** The single active activity loader (working/compaction/retry), if any. */
|
|
61
61
|
private activityLoader;
|
|
62
|
+
/** True while applySnapshot is mid-build; live broker frames defer instead of
|
|
63
|
+
* mutating the partially-rebuilt view. */
|
|
64
|
+
private applyingSnapshot;
|
|
65
|
+
/** Bumped on every applySnapshot. A build suspended at a yield compares this
|
|
66
|
+
* after resuming and abandons itself when a newer welcome has bumped it, so
|
|
67
|
+
* the newest welcome wins and stale state never overwrites the live view. */
|
|
68
|
+
private snapshotGeneration;
|
|
69
|
+
/** Live frames captured during an in-flight build (bound closures that re-run
|
|
70
|
+
* the original entry point), replayed in order when the build completes and
|
|
71
|
+
* dropped when a newer welcome supersedes the build. */
|
|
72
|
+
private readonly deferredFrames;
|
|
62
73
|
/** Text of the most recent assistant message, kept current from the welcome
|
|
63
74
|
* snapshot AND the live event stream so `/copy` (getLastAssistantText) works
|
|
64
75
|
* whether the message arrived before or after attach. */
|
|
@@ -69,7 +80,15 @@ export declare class ChatView {
|
|
|
69
80
|
* tool-execution components matched to their toolResult messages; everything
|
|
70
81
|
* else routes through `addMessageToChat`. The TUI's differential renderer +
|
|
71
82
|
* reflow-on-resize is automatic — no scrollback reconstruction needed. */
|
|
72
|
-
applySnapshot(snapshot: BrokerSnapshot): void
|
|
83
|
+
applySnapshot(snapshot: BrokerSnapshot): Promise<void>;
|
|
84
|
+
/** Defer a live broker frame when a snapshot build is in flight. Returns true
|
|
85
|
+
* (and enqueues the replay closure) while building so the caller returns
|
|
86
|
+
* without mutating the half-rebuilt view; false when the caller may proceed.
|
|
87
|
+
* This is the single-writer gate every live-frame entry point passes through. */
|
|
88
|
+
private deferIfBuilding;
|
|
89
|
+
/** Replay deferred live frames in wire order. The gate is already open, so each
|
|
90
|
+
* closure re-enters its entry point and dispatches immediately. */
|
|
91
|
+
private replayDeferredFrames;
|
|
73
92
|
/** Stop any spinning activity loader. A one-shot `crtr surface attach` process lets its
|
|
74
93
|
* timers die with the process, but if T7 reuses a ChatView across reconnects in
|
|
75
94
|
* one process it should call this on detach to avoid a leaked interval. */
|
|
@@ -40,6 +40,10 @@ const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
|
|
|
40
40
|
* themselves still use `getMarkdownTheme()` for full-fidelity rendering. */
|
|
41
41
|
const defaultSpinnerStyle = (s) => `\x1b[2m${s}\x1b[22m`;
|
|
42
42
|
const defaultDimStyle = (s) => `\x1b[2m${s}\x1b[22m`;
|
|
43
|
+
/** How many catch-up messages `applySnapshot` builds between event-loop yields.
|
|
44
|
+
* Keeps large non-streaming snapshots from monopolizing the main thread so
|
|
45
|
+
* queued keystrokes are serviced during construction. */
|
|
46
|
+
const SNAPSHOT_YIELD_EVERY = 50;
|
|
43
47
|
/** A component supports the Ctrl+O global tool-output toggle. Mirrors pi's
|
|
44
48
|
* `isExpandable` duck-type (interactive-mode.js): any rendered child carrying a
|
|
45
49
|
* `setExpanded(boolean)` method — tool executions, custom/compaction/branch/skill
|
|
@@ -125,6 +129,25 @@ export class ChatView {
|
|
|
125
129
|
bashComponent;
|
|
126
130
|
/** The single active activity loader (working/compaction/retry), if any. */
|
|
127
131
|
activityLoader;
|
|
132
|
+
// -- Single-writer snapshot ordering -------------------------------------
|
|
133
|
+
// A non-streaming catch-up yields to the event loop mid-build (so queued stdin
|
|
134
|
+
// drains), which opens a window where later broker frames could otherwise
|
|
135
|
+
// mutate a half-rebuilt transcript. These three fields make the ChatView a
|
|
136
|
+
// single ordered writer: applySnapshot is the only thing that mutates the
|
|
137
|
+
// view while a build is in flight, live frames are deferred and replayed in
|
|
138
|
+
// wire order once it finishes, and a newer welcome always supersedes an older
|
|
139
|
+
// build. See applySnapshot / deferIfBuilding.
|
|
140
|
+
/** True while applySnapshot is mid-build; live broker frames defer instead of
|
|
141
|
+
* mutating the partially-rebuilt view. */
|
|
142
|
+
applyingSnapshot = false;
|
|
143
|
+
/** Bumped on every applySnapshot. A build suspended at a yield compares this
|
|
144
|
+
* after resuming and abandons itself when a newer welcome has bumped it, so
|
|
145
|
+
* the newest welcome wins and stale state never overwrites the live view. */
|
|
146
|
+
snapshotGeneration = 0;
|
|
147
|
+
/** Live frames captured during an in-flight build (bound closures that re-run
|
|
148
|
+
* the original entry point), replayed in order when the build completes and
|
|
149
|
+
* dropped when a newer welcome supersedes the build. */
|
|
150
|
+
deferredFrames = [];
|
|
128
151
|
/** Text of the most recent assistant message, kept current from the welcome
|
|
129
152
|
* snapshot AND the live event stream so `/copy` (getLastAssistantText) works
|
|
130
153
|
* whether the message arrived before or after attach. */
|
|
@@ -163,12 +186,34 @@ export class ChatView {
|
|
|
163
186
|
* tool-execution components matched to their toolResult messages; everything
|
|
164
187
|
* else routes through `addMessageToChat`. The TUI's differential renderer +
|
|
165
188
|
* reflow-on-resize is automatic — no scrollback reconstruction needed. */
|
|
166
|
-
applySnapshot(snapshot) {
|
|
189
|
+
async applySnapshot(snapshot) {
|
|
190
|
+
// Open a new build generation. Any older build still suspended at a yield
|
|
191
|
+
// abandons itself when it resumes (generation check below), and live frames
|
|
192
|
+
// that were deferred against the transcript we are about to discard are
|
|
193
|
+
// dropped — a newer welcome always wins.
|
|
194
|
+
const generation = ++this.snapshotGeneration;
|
|
195
|
+
this.applyingSnapshot = true;
|
|
196
|
+
this.deferredFrames.length = 0;
|
|
167
197
|
this.resetChat();
|
|
168
198
|
const renderedPendingTools = new Map();
|
|
169
199
|
const messages = snapshot.messages;
|
|
170
200
|
const lastIndex = messages.length - 1;
|
|
171
201
|
for (let i = 0; i < messages.length; i++) {
|
|
202
|
+
// Yield to the event loop every SNAPSHOT_YIELD_EVERY messages so queued
|
|
203
|
+
// stdin drains (keystrokes are handled instead of bursting after a long
|
|
204
|
+
// catch-up) while a big transcript constructs. Skipped for a STREAMING
|
|
205
|
+
// snapshot: its in-flight tail + pending tools must be bound before any
|
|
206
|
+
// relayed message_update/message_end lands, so it builds synchronously
|
|
207
|
+
// (no await → no broker frame can interleave and freeze the live bubble).
|
|
208
|
+
if (!snapshot.state.isStreaming &&
|
|
209
|
+
i > 0 &&
|
|
210
|
+
i % SNAPSHOT_YIELD_EVERY === 0) {
|
|
211
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
212
|
+
// A newer welcome arrived while we were suspended — abandon this stale
|
|
213
|
+
// build without touching the view (or the gate) it no longer owns.
|
|
214
|
+
if (generation !== this.snapshotGeneration)
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
172
217
|
const message = messages[i];
|
|
173
218
|
if (message.role === 'assistant') {
|
|
174
219
|
// Keep the `/copy` source current from the catch-up transcript too, not
|
|
@@ -231,6 +276,28 @@ export class ChatView {
|
|
|
231
276
|
if (snapshot.state.isStreaming)
|
|
232
277
|
this.setActivity(this.makeLoader('Working...'));
|
|
233
278
|
this.tui.requestRender();
|
|
279
|
+
// Build complete and still current (a mismatch would have returned above):
|
|
280
|
+
// open the gate and replay, in wire order, any live frames that arrived
|
|
281
|
+
// during construction so they land on the finished transcript.
|
|
282
|
+
this.applyingSnapshot = false;
|
|
283
|
+
this.replayDeferredFrames();
|
|
284
|
+
}
|
|
285
|
+
/** Defer a live broker frame when a snapshot build is in flight. Returns true
|
|
286
|
+
* (and enqueues the replay closure) while building so the caller returns
|
|
287
|
+
* without mutating the half-rebuilt view; false when the caller may proceed.
|
|
288
|
+
* This is the single-writer gate every live-frame entry point passes through. */
|
|
289
|
+
deferIfBuilding(replay) {
|
|
290
|
+
if (!this.applyingSnapshot)
|
|
291
|
+
return false;
|
|
292
|
+
this.deferredFrames.push(replay);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
/** Replay deferred live frames in wire order. The gate is already open, so each
|
|
296
|
+
* closure re-enters its entry point and dispatches immediately. */
|
|
297
|
+
replayDeferredFrames() {
|
|
298
|
+
const frames = this.deferredFrames.splice(0);
|
|
299
|
+
for (const replay of frames)
|
|
300
|
+
replay();
|
|
234
301
|
}
|
|
235
302
|
/** Stop any spinning activity loader. A one-shot `crtr surface attach` process lets its
|
|
236
303
|
* timers die with the process, but if T7 reuses a ChatView across reconnects in
|
|
@@ -274,6 +341,10 @@ export class ChatView {
|
|
|
274
341
|
// exactly as interactive pi falls through default for those two).
|
|
275
342
|
// -------------------------------------------------------------------------
|
|
276
343
|
handleEvent(event) {
|
|
344
|
+
// Order behind an in-flight catch-up: a live turn that starts while a large
|
|
345
|
+
// welcome is still building is replayed after it, never interleaved into it.
|
|
346
|
+
if (this.deferIfBuilding(() => this.handleEvent(event)))
|
|
347
|
+
return;
|
|
277
348
|
switch (event.type) {
|
|
278
349
|
case 'agent_start': {
|
|
279
350
|
this.pendingTools.clear();
|
|
@@ -444,7 +515,13 @@ export class ChatView {
|
|
|
444
515
|
default:
|
|
445
516
|
break;
|
|
446
517
|
}
|
|
447
|
-
|
|
518
|
+
// No unconditional markDirty here: history-mutating branches already
|
|
519
|
+
// invalidate the cache the moment they touch the container — appends/removes
|
|
520
|
+
// go through RenderCacheContainer.addChild/removeChild (which markDirty), and
|
|
521
|
+
// every in-place child mutation (message_update/message_end, tool_execution_*)
|
|
522
|
+
// runs while the container is volatile (agent_start set it), where the cache
|
|
523
|
+
// is bypassed entirely. Footer-only, ignored (turn_start/turn_end), and
|
|
524
|
+
// activity-only events thus no longer nuke the settled-history render cache.
|
|
448
525
|
this.tui.requestRender();
|
|
449
526
|
}
|
|
450
527
|
// -------------------------------------------------------------------------
|
|
@@ -454,6 +531,8 @@ export class ChatView {
|
|
|
454
531
|
// also rides the next snapshot, so this only drives the LIVE view.
|
|
455
532
|
// -------------------------------------------------------------------------
|
|
456
533
|
bashStart(command, excludeFromContext) {
|
|
534
|
+
if (this.deferIfBuilding(() => this.bashStart(command, excludeFromContext)))
|
|
535
|
+
return;
|
|
457
536
|
const component = new BashExecutionComponent(command, this.tui, excludeFromContext);
|
|
458
537
|
this.bashComponent = component;
|
|
459
538
|
this.historyContainer.setVolatile(true);
|
|
@@ -461,11 +540,15 @@ export class ChatView {
|
|
|
461
540
|
this.tui.requestRender();
|
|
462
541
|
}
|
|
463
542
|
bashOutput(chunk) {
|
|
543
|
+
if (this.deferIfBuilding(() => this.bashOutput(chunk)))
|
|
544
|
+
return;
|
|
464
545
|
this.bashComponent?.appendOutput(chunk);
|
|
465
546
|
this.historyContainer.markDirty();
|
|
466
547
|
this.tui.requestRender();
|
|
467
548
|
}
|
|
468
549
|
bashEnd(result) {
|
|
550
|
+
if (this.deferIfBuilding(() => this.bashEnd(result)))
|
|
551
|
+
return;
|
|
469
552
|
this.bashComponent?.setComplete(result.exitCode, result.cancelled, result.truncated ? { truncated: true } : undefined, result.fullOutputPath);
|
|
470
553
|
this.bashComponent = undefined;
|
|
471
554
|
this.historyContainer.setVolatile(false);
|
|
@@ -220,12 +220,15 @@ describe('human cancel: behavior', () => {
|
|
|
220
220
|
assert.equal(r['canceled'], false);
|
|
221
221
|
assert.equal(r['reason'], 'already_resolved');
|
|
222
222
|
});
|
|
223
|
-
test('live node with no
|
|
223
|
+
test('live node with no request descriptor → already_resolved (nothing to cancel)', async () => {
|
|
224
|
+
// A live bridge whose interaction dir holds no deck.json/review.json has no
|
|
225
|
+
// humanloop ticket to race a cancellation against, so the !hasDescriptor
|
|
226
|
+
// guard short-circuits to already_resolved and never transitions the node.
|
|
224
227
|
createNode(humanNode('live-1', { status: 'active' }));
|
|
225
228
|
const r = await humanCancel.run({ job_id: 'live-1' });
|
|
226
|
-
assert.equal(r['canceled'],
|
|
227
|
-
assert.equal(r['
|
|
228
|
-
assert.equal(getNode('live-1')?.status, '
|
|
229
|
+
assert.equal(r['canceled'], false);
|
|
230
|
+
assert.equal(r['reason'], 'already_resolved');
|
|
231
|
+
assert.equal(getNode('live-1')?.status, 'active');
|
|
229
232
|
});
|
|
230
233
|
});
|
|
231
234
|
// ---------------------------------------------------------------------------
|
|
@@ -253,21 +256,3 @@ describe('human list: params', () => {
|
|
|
253
256
|
assert.equal(result['cursor'], 'tok_abc123');
|
|
254
257
|
});
|
|
255
258
|
});
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// human _run — no params (reads CRTR_HUMAN_DIR from env)
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
describe('human _run: no params', () => {
|
|
260
|
-
const params = [];
|
|
261
|
-
test('empty argv yields empty result (no params declared)', async () => {
|
|
262
|
-
const result = await parseArgv(params, []);
|
|
263
|
-
assert.deepEqual(result, {});
|
|
264
|
-
});
|
|
265
|
-
test('positional token throws bad_invocation (no positionals declared)', async () => {
|
|
266
|
-
await assert.rejects(() => parseArgv(params, ['some-value']), (err) => { assert.match(err.message, /takes no positional/); return true; });
|
|
267
|
-
});
|
|
268
|
-
test('CRTR_HUMAN_DIR env var contract: _run reads it from env, not argv', () => {
|
|
269
|
-
// Structural check: the params array is empty, confirming no argv surface.
|
|
270
|
-
// The actual env-var read is in the run handler and verified by inspection.
|
|
271
|
-
assert.equal(params.length, 0);
|
|
272
|
-
});
|
|
273
|
-
});
|
|
@@ -1,51 +1,52 @@
|
|
|
1
|
-
// `crtr canvas attention` — aggregate pending human
|
|
1
|
+
// `crtr canvas attention` — aggregate pending human tickets across the canvas.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// A pending "ticket" is any humanloop interaction awaiting a human: a deck
|
|
4
|
+
// (ask), a notify deck, or an anchored review. They are stored per-cwd by
|
|
5
|
+
// humanloop. This subtree surfaces two public views:
|
|
5
6
|
// count — a single integer (stdout.count is machine-parseable)
|
|
6
|
-
// list — itemised entries with cwd, node id, and per-cwd
|
|
7
|
+
// list — itemised entries with cwd, node id, and per-cwd ticket count
|
|
7
8
|
// `map` (hidden) is the batched per-node form the attach viewer's nav chrome
|
|
8
9
|
// polls; it is not an agent-facing decision.
|
|
9
10
|
//
|
|
10
11
|
// All three scope modes share the same underlying helpers in attention.ts:
|
|
11
|
-
// --node <id> →
|
|
12
|
-
// --view <id> →
|
|
13
|
-
// (neither) →
|
|
12
|
+
// --node <id> → countTickets(id) (one cwd)
|
|
13
|
+
// --view <id> → pendingTicketsForView(id) (sub-DAG from root)
|
|
14
|
+
// (neither) → ticketsAcrossCanvas() (whole canvas)
|
|
14
15
|
//
|
|
15
16
|
// Exported as a branch; `crtr canvas` (canvas.ts) mounts it.
|
|
16
17
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
17
18
|
import { InputError } from '../core/io.js';
|
|
18
19
|
import { getNode, view } from '../core/canvas/index.js';
|
|
19
|
-
import {
|
|
20
|
+
import { countTickets, pendingTicketsForView, ticketsAcrossCanvas, ticketCountsForNodes, } from '../core/canvas/attention.js';
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// attention count
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
const attentionCount = defineLeaf({
|
|
24
25
|
name: 'count',
|
|
25
|
-
description: 'total pending
|
|
26
|
+
description: 'total pending human-ticket count (machine-parseable stdout.count)',
|
|
26
27
|
whenToUse: 'a lightweight summary when you just need the number, not which cwds are blocked — scope it to a node or a sub-DAG, or default to canvas-wide. Use `list` when you need to act on where to go answer.',
|
|
27
28
|
help: {
|
|
28
29
|
name: 'canvas attention count',
|
|
29
30
|
// stdout.count is parsed directly by the nav chrome — keep the contract.
|
|
30
|
-
summary: 'return the number of pending human asks; stdout.count is machine-parseable',
|
|
31
|
+
summary: 'return the number of pending human tickets (asks, reviews, notifications); stdout.count is machine-parseable',
|
|
31
32
|
params: [
|
|
32
33
|
{
|
|
33
34
|
kind: 'flag',
|
|
34
35
|
name: 'node',
|
|
35
36
|
type: 'string',
|
|
36
37
|
required: false,
|
|
37
|
-
constraint: 'Count
|
|
38
|
+
constraint: 'Count tickets only for this node\'s cwd. Mutually exclusive with --view.',
|
|
38
39
|
},
|
|
39
40
|
{
|
|
40
41
|
kind: 'flag',
|
|
41
42
|
name: 'view',
|
|
42
43
|
type: 'string',
|
|
43
44
|
required: false,
|
|
44
|
-
constraint: 'Sum
|
|
45
|
+
constraint: 'Sum tickets for all nodes in the sub-DAG rooted at this id. Mutually exclusive with --node.',
|
|
45
46
|
},
|
|
46
47
|
],
|
|
47
48
|
output: [
|
|
48
|
-
{ name: 'count', type: 'integer', required: true, constraint: 'Total pending
|
|
49
|
+
{ name: 'count', type: 'integer', required: true, constraint: 'Total pending human tickets in the requested scope.' },
|
|
49
50
|
],
|
|
50
51
|
outputKind: 'object',
|
|
51
52
|
effects: ['Read-only: scans humanloop interaction dirs.'],
|
|
@@ -69,7 +70,7 @@ const attentionCount = defineLeaf({
|
|
|
69
70
|
next: 'List nodes with `crtr node inspect list`.',
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
|
-
return { count:
|
|
73
|
+
return { count: countTickets(nodeId) };
|
|
73
74
|
}
|
|
74
75
|
if (viewId !== undefined) {
|
|
75
76
|
if (getNode(viewId) === null) {
|
|
@@ -79,12 +80,12 @@ const attentionCount = defineLeaf({
|
|
|
79
80
|
next: 'List nodes with `crtr node inspect list`.',
|
|
80
81
|
});
|
|
81
82
|
}
|
|
82
|
-
const items =
|
|
83
|
+
const items = pendingTicketsForView(viewId);
|
|
83
84
|
const total = items.reduce((s, e) => s + e.count, 0);
|
|
84
85
|
return { count: total };
|
|
85
86
|
}
|
|
86
87
|
// Canvas-wide.
|
|
87
|
-
const items =
|
|
88
|
+
const items = ticketsAcrossCanvas();
|
|
88
89
|
const total = items.reduce((s, e) => s + e.count, 0);
|
|
89
90
|
return { count: total };
|
|
90
91
|
},
|
|
@@ -94,11 +95,11 @@ const attentionCount = defineLeaf({
|
|
|
94
95
|
// ---------------------------------------------------------------------------
|
|
95
96
|
const attentionList = defineLeaf({
|
|
96
97
|
name: 'list',
|
|
97
|
-
description: 'itemised list of cwds with pending
|
|
98
|
-
whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending asks, oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
|
|
98
|
+
description: 'itemised list of cwds with pending human tickets',
|
|
99
|
+
whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending tickets (asks, reviews, notifications), oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when you just need the number.',
|
|
99
100
|
help: {
|
|
100
101
|
name: 'canvas attention list',
|
|
101
|
-
summary: 'list nodes with pending human
|
|
102
|
+
summary: 'list nodes with pending human tickets, grouped by cwd, oldest first',
|
|
102
103
|
params: [
|
|
103
104
|
{
|
|
104
105
|
kind: 'flag',
|
|
@@ -131,10 +132,10 @@ const attentionList = defineLeaf({
|
|
|
131
132
|
next: 'List nodes with `crtr node inspect list`.',
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
|
-
items =
|
|
135
|
+
items = pendingTicketsForView(viewId);
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
|
-
items =
|
|
138
|
+
items = ticketsAcrossCanvas();
|
|
138
139
|
}
|
|
139
140
|
const total = items.reduce((s, e) => s + e.count, 0);
|
|
140
141
|
return { items, total };
|
|
@@ -145,12 +146,12 @@ const attentionList = defineLeaf({
|
|
|
145
146
|
// ---------------------------------------------------------------------------
|
|
146
147
|
const attentionMap = defineLeaf({
|
|
147
148
|
name: 'map',
|
|
148
|
-
description: 'per-node
|
|
149
|
-
whenToUse: 'labelling every node in a UI with its pending-
|
|
149
|
+
description: 'per-node ticket counts for a visible set, batched in one pass',
|
|
150
|
+
whenToUse: 'labelling every node in a UI with its pending-ticket count in one batched pass — the form nav chrome polls (one process, one JSON blob) instead of N count shell-outs',
|
|
150
151
|
tier: 'hidden',
|
|
151
152
|
help: {
|
|
152
153
|
name: 'canvas attention map',
|
|
153
|
-
summary: 'per-node pending-
|
|
154
|
+
summary: 'per-node pending human-ticket counts for a visible set of nodes in ONE pass — the batched form the nav chrome polls (one process, one JSON blob) instead of N count shell-outs',
|
|
154
155
|
params: [
|
|
155
156
|
{
|
|
156
157
|
kind: 'flag',
|
|
@@ -172,7 +173,7 @@ const attentionMap = defineLeaf({
|
|
|
172
173
|
name: 'counts',
|
|
173
174
|
type: 'object',
|
|
174
175
|
required: true,
|
|
175
|
-
constraint: 'Map of node_id → pending
|
|
176
|
+
constraint: 'Map of node_id → pending human-ticket count. Every requested id is present (0 when none).',
|
|
176
177
|
},
|
|
177
178
|
],
|
|
178
179
|
outputKind: 'object',
|
|
@@ -205,7 +206,7 @@ const attentionMap = defineLeaf({
|
|
|
205
206
|
for (const id of nodesRaw.split(',').map((s) => s.trim()).filter((s) => s !== ''))
|
|
206
207
|
ids.add(id);
|
|
207
208
|
}
|
|
208
|
-
return { counts:
|
|
209
|
+
return { counts: ticketCountsForNodes([...ids]) };
|
|
209
210
|
},
|
|
210
211
|
});
|
|
211
212
|
// ---------------------------------------------------------------------------
|
|
@@ -213,12 +214,12 @@ const attentionMap = defineLeaf({
|
|
|
213
214
|
// ---------------------------------------------------------------------------
|
|
214
215
|
export const attentionBranch = defineBranch({
|
|
215
216
|
name: 'attention',
|
|
216
|
-
description: 'count/list pending human
|
|
217
|
-
whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending asks or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
|
|
217
|
+
description: 'count/list pending human tickets across the graph',
|
|
218
|
+
whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending tickets (asks, reviews, notifications) or list the cwds that have them. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
|
|
218
219
|
help: {
|
|
219
220
|
name: 'canvas attention',
|
|
220
|
-
summary: 'aggregate pending human
|
|
221
|
-
model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised entries — the cwds with pending
|
|
221
|
+
summary: 'aggregate pending human tickets across the canvas',
|
|
222
|
+
model: '`count` returns a single integer (stdout.count is machine-parseable); `list` returns itemised entries — the cwds with pending tickets, oldest first. Scope with --node (one node) or --view (sub-DAG) — default is canvas-wide. A ticket is any humanloop interaction awaiting a human: an ask, a review, or a notification.',
|
|
222
223
|
},
|
|
223
224
|
children: [attentionCount, attentionList, attentionMap],
|
|
224
225
|
});
|