@crouton-kit/crouter 0.3.81 → 0.3.83

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.
Files changed (84) hide show
  1. package/dist/builtin-pi-packages/pi-mode-switch/README.md +3 -3
  2. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +12 -4
  3. package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -3
  4. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +104 -0
  5. package/dist/clients/attach/__tests__/crtr-output-render.test.js +16 -44
  6. package/dist/clients/attach/attach-cmd.d.ts +1 -2
  7. package/dist/clients/attach/attach-cmd.js +960 -961
  8. package/dist/clients/attach/attach-def.d.ts +2 -0
  9. package/dist/clients/attach/attach-def.js +93 -0
  10. package/dist/clients/attach/canvas-panels.d.ts +10 -3
  11. package/dist/clients/attach/canvas-panels.js +42 -16
  12. package/dist/clients/attach/chat-view.d.ts +20 -1
  13. package/dist/clients/attach/chat-view.js +85 -2
  14. package/dist/commands/__tests__/human.test.js +7 -22
  15. package/dist/commands/attention.js +31 -30
  16. package/dist/commands/human/prompts.js +51 -38
  17. package/dist/commands/human/queue.d.ts +1 -28
  18. package/dist/commands/human/queue.js +156 -520
  19. package/dist/commands/human/shared.d.ts +28 -68
  20. package/dist/commands/human/shared.js +37 -120
  21. package/dist/commands/human.js +13 -17
  22. package/dist/commands/node.js +4 -2
  23. package/dist/commands/surface.js +1 -1
  24. package/dist/commands/sys/__tests__/setup-core.test.js +1 -1
  25. package/dist/commands/sys/__tests__/setup-front-door.test.js +98 -0
  26. package/dist/commands/sys/setup-core.d.ts +22 -0
  27. package/dist/commands/sys/setup-core.js +62 -0
  28. package/dist/commands/sys/setup.js +8 -1
  29. package/dist/core/__tests__/human-deliver-e2e.test.js +95 -0
  30. package/dist/core/__tests__/human-deliver.test.js +332 -0
  31. package/dist/core/__tests__/human-node-not-supervised.test.js +5 -4
  32. package/dist/core/__tests__/pid-identity-match.test.js +83 -0
  33. package/dist/core/canvas/__tests__/attention.test.d.ts +1 -0
  34. package/dist/core/canvas/__tests__/attention.test.js +126 -0
  35. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +4 -4
  36. package/dist/core/canvas/__tests__/render-remote.test.js +1 -1
  37. package/dist/core/canvas/attention.d.ts +16 -14
  38. package/dist/core/canvas/attention.js +34 -29
  39. package/dist/core/canvas/boot.d.ts +1 -0
  40. package/dist/core/canvas/boot.js +1 -1
  41. package/dist/core/canvas/browse/app.js +3 -3
  42. package/dist/core/canvas/pid.d.ts +43 -17
  43. package/dist/core/canvas/pid.js +104 -35
  44. package/dist/core/canvas/remote-canvas-source.d.ts +1 -1
  45. package/dist/core/canvas/remote-canvas-source.js +4 -2
  46. package/dist/core/canvas/render.d.ts +5 -5
  47. package/dist/core/canvas/render.js +14 -14
  48. package/dist/core/canvas/source.d.ts +2 -2
  49. package/dist/core/canvas/source.js +3 -3
  50. package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
  51. package/dist/core/keybindings/catalog.d.ts +2 -2
  52. package/dist/core/keybindings/catalog.js +3 -2
  53. package/dist/core/preview-registry.js +22 -29
  54. package/dist/core/profiles/select.js +4 -4
  55. package/dist/core/runtime/front-door.js +19 -6
  56. package/dist/core/runtime/package-health.d.ts +13 -0
  57. package/dist/core/runtime/package-health.js +18 -0
  58. package/dist/core/runtime/pi-vendored.js +15 -1
  59. package/dist/core/runtime/placement.d.ts +1 -10
  60. package/dist/core/runtime/placement.js +43 -26
  61. package/dist/core/runtime/resume.js +72 -4
  62. package/dist/core/runtime/session-list-cache.d.ts +22 -2
  63. package/dist/core/runtime/session-list-cache.js +92 -24
  64. package/dist/core/runtime/spawn.js +9 -0
  65. package/dist/core/spawn.d.ts +0 -59
  66. package/dist/core/spawn.js +5 -163
  67. package/dist/daemon/crtrd.js +7 -6
  68. package/dist/index.d.ts +2 -2
  69. package/dist/index.js +1 -1
  70. package/dist/web-client/assets/index-DZdKS-FB.js +78 -0
  71. package/dist/web-client/index.html +1 -1
  72. package/dist/web-client/sw.js +1 -1
  73. package/package.json +2 -2
  74. package/dist/core/__tests__/full/dead-pane-regression.test.js +0 -154
  75. package/dist/core/__tests__/full/human-new-window-regression.test.js +0 -104
  76. package/dist/core/__tests__/full/review-render-pane-regression.test.js +0 -133
  77. package/dist/core/__tests__/human-stranded-deliver.test.js +0 -145
  78. package/dist/core/__tests__/human-surface-target.test.js +0 -98
  79. package/dist/web-client/assets/index--md2ylfi.js +0 -78
  80. /package/dist/{core/__tests__/full/dead-pane-regression.test.d.ts → clients/attach/__tests__/chat-view-snapshot-ordering.test.d.ts} +0 -0
  81. /package/dist/{core/__tests__/full/human-new-window-regression.test.d.ts → commands/sys/__tests__/setup-front-door.test.d.ts} +0 -0
  82. /package/dist/core/__tests__/{full/review-render-pane-regression.test.d.ts → human-deliver-e2e.test.d.ts} +0 -0
  83. /package/dist/core/__tests__/{human-stranded-deliver.test.d.ts → human-deliver.test.d.ts} +0 -0
  84. /package/dist/core/__tests__/{human-surface-target.test.d.ts → pid-identity-match.test.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  # pi-mode-switch
2
2
 
3
- Cycle between operating modes with crouter's configurable **Cycle mode** binding (default **Alt+M**) — the way Claude's Shift+Tab cycles plan / accept-edits. Whenever the mode changes, the new mode's operating instructions are injected invisibly before your next message.
3
+ Cycle between operating modes with crouter's configurable **Cycle mode** binding (default **Alt+Shift+P**) — the way Claude's Shift+Tab cycles plan / accept-edits. Whenever the mode changes, the new mode's operating instructions are injected invisibly before your next message.
4
4
 
5
5
  ## Modes
6
6
 
@@ -12,7 +12,7 @@ Cycle between operating modes with crouter's configurable **Cycle mode** binding
12
12
 
13
13
  ## Usage
14
14
 
15
- - the configured `crtr.mode.cycle` binding (default `Alt+M`) — cycle `normal → spec → plan → normal`
15
+ - the configured `crtr.mode.cycle` binding (default `Alt+Shift+P`) — cycle `normal → spec → plan → normal`
16
16
  - `/mode` — cycle without using the shortcut
17
17
  - `/mode <normal\|spec\|plan>` — jump to a specific mode
18
18
  - the agent can switch its own mode by running the bundled `bin/mode` CLI through its bash tool — e.g. `bash <plugin>/bin/mode normal` after a spec/plan is approved. The command prints that mode's guidance, so the text only enters context at the moment of the switch (no always-on tool)
@@ -33,4 +33,4 @@ The input badge is drawn by wrapping whatever editor is already installed (for e
33
33
 
34
34
  This package ships inside crouter. `crtr sys setup` registers its built package path with pi; there is no separate repository or package to install.
35
35
 
36
- Run `/reload` in pi after setup. The extension resolves `crtr.mode.cycle` from the sparse `keybindings` object in `~/.crouter/config.json`; missing overrides use Alt+M, multiple gestures all register, and an empty list disables the shortcut. `/mode` remains available when the shortcut is disabled. Pi's stock actions and `~/.pi/agent/keybindings.json` remain untouched.
36
+ Run `/reload` in pi after setup. The extension resolves `crtr.mode.cycle` from the sparse `keybindings` object in `~/.crouter/config.json`; missing overrides use Alt+Shift+P, multiple gestures all register, and an empty list disables the shortcut. `/mode` remains available when the shortcut is disabled. Pi's stock actions and `~/.pi/agent/keybindings.json` remain untouched.
@@ -172,13 +172,21 @@ test('buildCanvasPanelLines suppresses every local-read adornment for a RemoteCa
172
172
  // (see the CI-hang postmortem on rebuild-coalescer.test.ts).
173
173
  t.after(() => server.close());
174
174
  const source = new RemoteCanvasSource(server.url, 'tok');
175
- // 'root' has 'a' as a live report row('a') renders into `reports`.
176
- const { reports } = await buildCanvasPanelLines('root', {}, IDENTITY_PALETTE, source);
177
- assert.equal(reports.length, 1);
178
- const line = reports[0];
175
+ // The stable family roster shown when attached to 'root' is root-first:
176
+ // the root row followed by its one live child 'a' (root has no non-human
177
+ // manager, so it is its own roster root). The poisoned local node id is 'a',
178
+ // so the load-bearing assertions must target the 'a' ROW specifically —
179
+ // `reportIds` maps each rendered row to its node id in render order.
180
+ const { reports, reportIds } = await buildCanvasPanelLines('root', {}, IDENTITY_PALETTE, source);
181
+ assert.deepEqual(reportIds, ['root', 'a'], 'roster is root-first: the attached root then its live child');
182
+ assert.equal(reports.length, 2);
183
+ const line = reports[reportIds.indexOf('a')];
179
184
  assert.ok(!line.includes('1000k'), `local telemetry (999999 tokens) must not leak into a remote row: ${line}`);
180
185
  assert.ok(!line.includes('POISON'), `poisoned local text must never appear in a remote row: ${line}`);
181
186
  assert.ok(!line.includes('⚠'), `a local active fault must not surface as the hanging glyph on a remote row: ${line}`);
187
+ // The 'a' row is NOT the attached node ('root' is), so a legitimate
188
+ // you-are-here bar never applies to it — any BG_ATTACHED here could only
189
+ // come from the poisoned LOCAL focus/attach on id 'a', which must be ignored.
182
190
  assert.ok(!line.includes(BG_ATTACHED), `a local focus/attach must not paint a remote row as attached: ${line}`);
183
191
  });
184
192
  test('GraphOverlay suppresses every local-read adornment for a RemoteCanvasSource AND makes Enter/m/x no-ops, even with conflicting local state for the same node id', async (t) => {
@@ -30,6 +30,7 @@ test('attach input dispatches only the current configured action', () => {
30
30
  'crtr.attach.graph.toggle': [],
31
31
  'crtr.mode.cycle': ['alt+x'],
32
32
  'crtr.attach.model-ladder.next': ['alt+z'],
33
+ 'crtr.attach.model-ladder.previous': ['alt+y'],
33
34
  });
34
35
  const calls = [];
35
36
  const actions = {
@@ -38,15 +39,18 @@ test('attach input dispatches only the current configured action', () => {
38
39
  'crtr.attach.graph.toggle': () => calls.push('graph'),
39
40
  'crtr.mode.cycle': () => calls.push('mode'),
40
41
  'crtr.attach.model-ladder.next': () => calls.push('ladder'),
42
+ 'crtr.attach.model-ladder.previous': () => calls.push('ladder-back'),
41
43
  };
42
44
  assert.equal(dispatchAttachBinding(bindings, '\x18', actions), true);
43
45
  assert.equal(dispatchAttachBinding(bindings, '\x04', actions), false, 'old detach must not reach the listener action');
44
46
  assert.equal(dispatchAttachBinding(bindings, '\x1bg', actions), false, 'disabled graph must not reach the listener action');
45
47
  assert.equal(dispatchAttachBinding(bindings, '\x1bx', actions), true);
46
- assert.equal(dispatchAttachBinding(bindings, '\x1bm', actions), false, 'old mode key must not reach the listener action');
48
+ assert.equal(dispatchAttachBinding(bindings, '\x1bP', actions), false, 'old mode key must not reach the listener action');
47
49
  assert.equal(dispatchAttachBinding(bindings, '\x1bz', actions), true);
48
- assert.equal(dispatchAttachBinding(bindings, '\x1bM', actions), false, 'old ladder key must not reach the listener action');
49
- assert.deepEqual(calls, ['detach', 'mode', 'ladder']);
50
+ assert.equal(dispatchAttachBinding(bindings, '\x1bm', actions), false, 'old ladder key must not reach the listener action');
51
+ assert.equal(dispatchAttachBinding(bindings, '\x1by', actions), true);
52
+ assert.equal(dispatchAttachBinding(bindings, '\x1bM', actions), false, 'old ladder-back key must not reach the listener action');
53
+ assert.deepEqual(calls, ['detach', 'mode', 'ladder', 'ladder-back']);
50
54
  assert.equal(detachHint(bindings), 'Ctrl+X', 'disabled clear-or-detach must fall back to enabled detach');
51
55
  const noDetach = resolveKeybindings({ 'crtr.attach.detach': [], 'crtr.attach.clear-or-detach': [] });
52
56
  assert.equal(detachHint(noDetach), undefined, 'a disabled pair must not render "Disabled to detach"');
@@ -0,0 +1,104 @@
1
+ // Regression coverage for the single-writer snapshot ordering in ChatView
2
+ // (chat-view.ts). A large NON-streaming catch-up yields to the event loop
3
+ // mid-build so queued stdin drains; that yield window used to let later broker
4
+ // frames mutate a half-rebuilt transcript. Two races are pinned here:
5
+ //
6
+ // 1. Live turn frames arriving during a yielded welcome must be replayed
7
+ // AFTER the build finishes (not interleaved), and their streaming/volatility
8
+ // state must survive — the resuming build's stale non-streaming completion
9
+ // must not overwrite it.
10
+ // 2. A second welcome delivered while the first build is suspended must
11
+ // supersede it outright: the newest snapshot wins and the stale build
12
+ // abandons itself instead of appending into the replaced transcript.
13
+ import assert from 'node:assert/strict';
14
+ import test from 'node:test';
15
+ import { Container, TUI } from '@earendil-works/pi-tui';
16
+ import { AssistantMessageComponent, UserMessageComponent, } from '@earendil-works/pi-coding-agent';
17
+ import { ChatView } from '../chat-view.js';
18
+ /** A headless terminal so TUI.requestRender's scheduled writes are no-ops (the
19
+ * test only inspects component state, never real output). */
20
+ function fakeTerminal() {
21
+ return {
22
+ start() { },
23
+ stop() { },
24
+ async drainInput() { },
25
+ write() { },
26
+ get columns() { return 80; },
27
+ get rows() { return 24; },
28
+ get kittyProtocolActive() { return false; },
29
+ moveBy() { },
30
+ hideCursor() { },
31
+ showCursor() { },
32
+ clearLine() { },
33
+ clearFromCursor() { },
34
+ clearScreen() { },
35
+ setTitle() { },
36
+ setProgress() { },
37
+ };
38
+ }
39
+ function makeChatView() {
40
+ const tui = new TUI(fakeTerminal());
41
+ const container = new Container();
42
+ const view = new ChatView(tui, container);
43
+ const internals = view;
44
+ return { view, historyChildren: () => internals.historyContainer.children, internals };
45
+ }
46
+ /** A non-streaming snapshot of `count` assistant text messages. Enough messages
47
+ * (>2× SNAPSHOT_YIELD_EVERY) to guarantee the build suspends at least once. */
48
+ function assistantSnapshot(count, tag) {
49
+ const messages = Array.from({ length: count }, (_, i) => ({
50
+ role: 'assistant',
51
+ content: [{ type: 'text', text: `${tag}-${i}` }],
52
+ stopReason: 'endTurn',
53
+ }));
54
+ return {
55
+ messages,
56
+ stats: {},
57
+ state: { isStreaming: false },
58
+ };
59
+ }
60
+ const countOf = (children, ctor) => children.filter((c) => c instanceof ctor).length;
61
+ test('live turn frames arriving during a yielded welcome replay after the build and keep streaming state', async () => {
62
+ const { view, historyChildren, internals } = makeChatView();
63
+ // Start a large non-streaming catch-up: it appends the first chunk of
64
+ // components synchronously, then suspends at the first event-loop yield.
65
+ const build = view.applySnapshot(assistantSnapshot(120, 'snap'));
66
+ assert.equal(internals.applyingSnapshot, true, 'build is in flight after the first synchronous chunk');
67
+ // Live turn frames arrive while the build is suspended — these must be
68
+ // deferred, not applied to the half-rebuilt view.
69
+ view.handleEvent({ type: 'agent_start' });
70
+ view.handleEvent({ type: 'message_start', message: { role: 'user', content: 'LIVE' } });
71
+ await build;
72
+ const children = historyChildren();
73
+ // All 120 snapshot assistant components are present and precede the one live
74
+ // user message that was deferred and replayed at the tail.
75
+ assert.equal(countOf(children, AssistantMessageComponent), 120, 'every snapshot message survived the build');
76
+ assert.equal(countOf(children, UserMessageComponent), 1, 'exactly the one deferred live user message landed');
77
+ const lastReal = [...children].reverse().find((c) => c instanceof UserMessageComponent || c instanceof AssistantMessageComponent);
78
+ assert.ok(lastReal instanceof UserMessageComponent, 'the live message is appended AFTER the whole snapshot, not interleaved');
79
+ // The replayed agent_start owns the final volatility/activity state — the
80
+ // resuming build's stale non-streaming completion did NOT overwrite it.
81
+ assert.equal(internals.applyingSnapshot, false, 'gate released after the build');
82
+ assert.equal(internals.historyContainer.volatile, true, 'live turn keeps history volatile (cache bypassed)');
83
+ assert.notEqual(internals.activityLoader, undefined, 'the working loader from the live agent_start is present');
84
+ // Cleanup: the live loader runs a spinner interval that would otherwise hold
85
+ // the event loop (and the test process) open forever.
86
+ internals.setActivity(undefined);
87
+ });
88
+ test('a second welcome delivered mid-build supersedes the first and the stale build abandons itself', async () => {
89
+ const { view, historyChildren, internals } = makeChatView();
90
+ // First large build suspends at its first yield.
91
+ const first = view.applySnapshot(assistantSnapshot(120, 'first'));
92
+ assert.equal(internals.snapshotGeneration, 1);
93
+ // A newer welcome arrives while the first is suspended — resets and rebuilds
94
+ // synchronously (small, no yield), bumping the generation.
95
+ const second = view.applySnapshot(assistantSnapshot(3, 'second'));
96
+ assert.equal(internals.snapshotGeneration, 2, 'the newer welcome opened a new generation');
97
+ await Promise.all([first, second]);
98
+ // Only the second snapshot's transcript remains: the stale first build saw the
99
+ // generation change after resuming and returned without appending its tail.
100
+ const children = historyChildren();
101
+ assert.equal(countOf(children, AssistantMessageComponent), 3, 'newest welcome wins; no interleaved stale tail');
102
+ assert.equal(internals.applyingSnapshot, false, 'gate released');
103
+ assert.equal(internals.historyContainer.volatile, false, 'second (non-streaming) snapshot set the final state');
104
+ });
@@ -1318,84 +1318,56 @@ test('crtr push urgent/final cards collapse to a one-line outcome + report basen
1318
1318
  // ---------------------------------------------------------------------------
1319
1319
  // Batch C — human/memory/pkg summarizers (preview-registry.ts "Batch C")
1320
1320
  // ---------------------------------------------------------------------------
1321
- const HUMAN_ASK_DELIVERED_OUTPUT = [
1321
+ // Ask and review now share the single QUEUED_FOLLOW_UP road-sign (prompts.ts):
1322
+ // arrival is always just durable queue state, so there is no drained/delivered
1323
+ // fork — the card collapses to one posted line.
1324
+ const QUEUED_FOLLOW_UP = "Queued for the human. Their answer is pushed to your inbox and wakes you when they respond — never poll, verify it opened, or block. Press ⌥I (or run `hl inbox open` at a terminal) to see the queue.";
1325
+ const HUMAN_ASK_QUEUED_OUTPUT = [
1322
1326
  '- job_id: mrASK0001',
1323
1327
  '',
1324
1328
  '**dir:** /Users/silas/Code/cli/crouter',
1325
1329
  '',
1326
- "**follow_up:** The human's answer is delivered to your inbox when they respond — no need to poll.",
1330
+ `**follow_up:** ${QUEUED_FOLLOW_UP}`,
1327
1331
  ].join('\n');
1328
- const HUMAN_ASK_DRAINED_OUTPUT = [
1329
- '- job_id: mrASK0002',
1330
- '',
1331
- '**dir:** /Users/silas/Code/cli/crouter',
1332
- '',
1333
- '**follow_up:** Not in tmux: a human must drain it — run `crtr human inbox` (or re-run inside tmux). The answer then arrives in your inbox.',
1334
- ].join('\n');
1335
- test('a crtr human ask card collapses to a posted/drained status + question teaser, not the raw fields', () => {
1332
+ test('a crtr human ask card collapses to a posted status + question teaser, not the raw fields', () => {
1336
1333
  const def = createCrtrBashToolDefinition({ command: 'crtr human ask "Should we ship the preview rollout now?"' });
1337
1334
  const collapsed = def
1338
- .renderResult({ content: [{ type: 'text', text: HUMAN_ASK_DELIVERED_OUTPUT }] }, { expanded: false }, theme, {})
1335
+ .renderResult({ content: [{ type: 'text', text: HUMAN_ASK_QUEUED_OUTPUT }] }, { expanded: false }, theme, {})
1339
1336
  .render(140)
1340
1337
  .join('\n');
1341
1338
  assert.match(collapsed, /<accent>ask posted → waiting on human<\/accent>/);
1342
1339
  assert.match(collapsed, /<muted>Should we ship the preview rollout now\?<\/muted>/);
1343
1340
  assert.doesNotMatch(collapsed, /- job_id:/);
1344
1341
  const expanded = def
1345
- .renderResult({ content: [{ type: 'text', text: HUMAN_ASK_DELIVERED_OUTPUT }] }, { expanded: true }, theme, {})
1342
+ .renderResult({ content: [{ type: 'text', text: HUMAN_ASK_QUEUED_OUTPUT }] }, { expanded: true }, theme, {})
1346
1343
  .render(140)
1347
1344
  .join('\n');
1348
1345
  assert.match(expanded, /job_id: mrASK0001/);
1349
- assert.match(expanded, /no need to poll/);
1350
- const drained = def
1351
- .renderResult({ content: [{ type: 'text', text: HUMAN_ASK_DRAINED_OUTPUT }] }, { expanded: false }, theme, {})
1352
- .render(140)
1353
- .join('\n');
1354
- assert.match(drained, /<warning>ask posted — drain via `human inbox`<\/warning>/);
1355
- assert.match(drained, /<muted>Should we ship the preview rollout now\?<\/muted>/);
1356
- const drainedExpanded = def
1357
- .renderResult({ content: [{ type: 'text', text: HUMAN_ASK_DRAINED_OUTPUT }] }, { expanded: true }, theme, {})
1358
- .render(140)
1359
- .join('\n');
1360
- assert.match(drainedExpanded, /Not in tmux: a human must drain it/);
1346
+ assert.match(expanded, /wakes you when they respond/);
1361
1347
  });
1362
- const HUMAN_REVIEW_LIVE_OUTPUT = [
1348
+ const HUMAN_REVIEW_QUEUED_OUTPUT = [
1363
1349
  '- job_id: mrREV0001',
1364
1350
  '',
1365
1351
  '**dir:** /Users/silas/Code/cli/crouter',
1366
1352
  '',
1367
1353
  '**output:** /Users/silas/Code/cli/crouter/spec.review.md',
1368
1354
  '',
1369
- "**follow_up:** The document is live on the human's screen for anchored, line-by-line review. The pane tracks the file — edit the .md in place and it re-renders on save, so never cancel and re-present just to show a change. Do not poll, verify it opened, or background this call; end your turn. The human reviews on their own time, and their comments (with any line edits) arrive in your inbox and wake you when they submit.",
1355
+ `**follow_up:** ${QUEUED_FOLLOW_UP}`,
1370
1356
  ].join('\n');
1371
- const HUMAN_REVIEW_DRAINED_OUTPUT = [
1372
- '- job_id: mrREV0002',
1373
- '',
1374
- '**dir:** /Users/silas/Code/cli/crouter',
1375
- '',
1376
- '**output:** /Users/silas/Code/cli/crouter/spec.review.md',
1377
- '',
1378
- '**follow_up:** Not in tmux: a human must drain it — run `crtr human inbox` (or re-run inside tmux). The answer then arrives in your inbox.',
1379
- ].join('\n');
1380
- test('a crtr human review card collapses to a posted/drained status + file basename, not the raw fields', () => {
1357
+ test('a crtr human review card collapses to a posted status + file basename, not the raw fields', () => {
1381
1358
  const def = createCrtrBashToolDefinition({ command: 'crtr human review /Users/silas/Code/cli/crouter/spec.md' });
1382
1359
  const collapsed = def
1383
- .renderResult({ content: [{ type: 'text', text: HUMAN_REVIEW_LIVE_OUTPUT }] }, { expanded: false }, theme, {})
1360
+ .renderResult({ content: [{ type: 'text', text: HUMAN_REVIEW_QUEUED_OUTPUT }] }, { expanded: false }, theme, {})
1384
1361
  .render(140)
1385
1362
  .join('\n');
1386
1363
  assert.match(collapsed, /<accent>review posted: spec\.md → waiting on human<\/accent>/);
1387
1364
  assert.doesNotMatch(collapsed, /- job_id:/);
1388
1365
  const expanded = def
1389
- .renderResult({ content: [{ type: 'text', text: HUMAN_REVIEW_LIVE_OUTPUT }] }, { expanded: true }, theme, {})
1366
+ .renderResult({ content: [{ type: 'text', text: HUMAN_REVIEW_QUEUED_OUTPUT }] }, { expanded: true }, theme, {})
1390
1367
  .render(140)
1391
1368
  .join('\n');
1392
1369
  assert.match(expanded, /job_id: mrREV0001/);
1393
- assert.match(expanded, /anchored, line-by-line review/);
1394
- const drained = def
1395
- .renderResult({ content: [{ type: 'text', text: HUMAN_REVIEW_DRAINED_OUTPUT }] }, { expanded: false }, theme, {})
1396
- .render(140)
1397
- .join('\n');
1398
- assert.match(drained, /<warning>review posted: spec\.md — drain via `human inbox`<\/warning>/);
1370
+ assert.match(expanded, /wakes you when they respond/);
1399
1371
  });
1400
1372
  const HUMAN_LIST_OUTPUT = [
1401
1373
  '- total: 2',
@@ -1,5 +1,4 @@
1
1
  import { CombinedAutocompleteProvider } from '@earendil-works/pi-tui';
2
- import type { BranchDef } from '../../core/command.js';
3
2
  import type { BrokerSnapshot, ClientRole } from '../../core/runtime/broker-protocol.js';
4
3
  import { type BindingId, type BindingResolution } from '../../core/keybindings/index.js';
5
4
  import { type AttachPalette } from './config-load.js';
@@ -52,5 +51,5 @@ export declare function buildAttachAutocompleteProvider(cwd: string, fdPath: str
52
51
  name: string;
53
52
  description?: string;
54
53
  }>, cwd: string, fdPath: string | null) => CombinedAutocompleteProvider): CombinedAutocompleteProvider;
55
- export declare function registerAttach(): BranchDef;
54
+ export declare function runAttach(nodeId: string, observer: boolean, canvasName?: string): Promise<void>;
56
55
  export {};