@crouton-kit/crouter 0.3.70 → 0.3.79

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 (193) hide show
  1. package/README.md +1 -1
  2. package/dist/build-root.d.ts +12 -4
  3. package/dist/build-root.js +25 -6
  4. package/dist/builtin-memory/00-runtime-base.md +3 -10
  5. package/dist/builtin-memory/04-base-worker.md +18 -0
  6. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +82 -5
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
  14. package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
  15. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
  16. package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
  17. package/dist/builtin-views/canvas/tui.mjs +8 -9
  18. package/dist/builtin-views/chat/tui.mjs +12 -12
  19. package/dist/builtin-views/git-pr/tui.mjs +7 -8
  20. package/dist/builtin-views/inbox/tui.mjs +27 -41
  21. package/dist/builtin-views/linkedin/tui.mjs +23 -37
  22. package/dist/builtin-views/prompt-review/tui.mjs +11 -11
  23. package/dist/builtin-views/settings/tui.mjs +11 -11
  24. package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
  25. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
  26. package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
  27. package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
  28. package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
  29. package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
  30. package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
  31. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
  32. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +9 -1
  34. package/dist/clients/attach/attach-cmd.js +841 -792
  35. package/dist/clients/attach/auth-pickers.d.ts +0 -12
  36. package/dist/clients/attach/auth-pickers.js +64 -15
  37. package/dist/clients/attach/canvas-panels.js +2 -3
  38. package/dist/clients/attach/chat-view.d.ts +7 -8
  39. package/dist/clients/attach/chat-view.js +119 -79
  40. package/dist/clients/attach/edit-diff-render.d.ts +6 -0
  41. package/dist/clients/attach/edit-diff-render.js +263 -0
  42. package/dist/clients/attach/graph-overlay.d.ts +12 -2
  43. package/dist/clients/attach/graph-overlay.js +83 -33
  44. package/dist/clients/attach/input-controller.d.ts +28 -2
  45. package/dist/clients/attach/input-controller.js +38 -5
  46. package/dist/clients/attach/mermaid-render.js +5 -2
  47. package/dist/clients/attach/pickers.d.ts +8 -7
  48. package/dist/clients/attach/pickers.js +11 -17
  49. package/dist/clients/attach/slash-commands.d.ts +9 -0
  50. package/dist/clients/attach/slash-commands.js +127 -7
  51. package/dist/clients/attach/titled-editor.d.ts +12 -1
  52. package/dist/clients/attach/titled-editor.js +103 -8
  53. package/dist/commands/canvas-browse.js +2 -2
  54. package/dist/commands/human/queue.js +3 -4
  55. package/dist/commands/memory/lint.js +39 -5
  56. package/dist/commands/memory/write.js +1 -0
  57. package/dist/commands/node.js +9 -2
  58. package/dist/commands/pkg/plugin-inspect.js +22 -1
  59. package/dist/commands/pkg/plugin-manage.js +31 -9
  60. package/dist/commands/surface-tmux-spread.js +1 -3
  61. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
  62. package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
  63. package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
  64. package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
  65. package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
  66. package/dist/commands/sys/config.js +18 -21
  67. package/dist/commands/sys/doctor.js +42 -4
  68. package/dist/commands/sys/setup-core.d.ts +49 -1
  69. package/dist/commands/sys/setup-core.js +161 -4
  70. package/dist/commands/sys/setup.d.ts +88 -0
  71. package/dist/commands/sys/setup.js +943 -169
  72. package/dist/commands/view-pick.d.ts +4 -0
  73. package/dist/commands/view-pick.js +17 -7
  74. package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
  75. package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
  76. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
  77. package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
  78. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
  79. package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
  80. package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
  81. package/dist/core/__tests__/command-plugins.test.js +444 -0
  82. package/dist/core/__tests__/fault-classifier.test.js +15 -0
  83. package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
  84. package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
  85. package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
  86. package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
  87. package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
  88. package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
  89. package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
  90. package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
  91. package/dist/core/__tests__/stream-watchdog.test.js +70 -0
  92. package/dist/core/__tests__/tmux-surface.test.js +72 -0
  93. package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
  94. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  95. package/dist/core/canvas/browse/app.d.ts +6 -0
  96. package/dist/core/canvas/browse/app.js +230 -41
  97. package/dist/core/canvas/browse/model.d.ts +10 -6
  98. package/dist/core/canvas/browse/model.js +20 -12
  99. package/dist/core/canvas/browse/pins.d.ts +4 -0
  100. package/dist/core/canvas/browse/pins.js +29 -0
  101. package/dist/core/canvas/browse/render.d.ts +13 -1
  102. package/dist/core/canvas/browse/render.js +72 -15
  103. package/dist/core/canvas/nav-model.js +5 -11
  104. package/dist/core/canvas/paths.d.ts +5 -0
  105. package/dist/core/canvas/paths.js +7 -0
  106. package/dist/core/command-plugins/adapter.d.ts +15 -0
  107. package/dist/core/command-plugins/adapter.js +145 -0
  108. package/dist/core/command-plugins/compose.d.ts +5 -0
  109. package/dist/core/command-plugins/compose.js +56 -0
  110. package/dist/core/command-plugins/discovery.d.ts +104 -0
  111. package/dist/core/command-plugins/discovery.js +565 -0
  112. package/dist/core/config.d.ts +48 -0
  113. package/dist/core/config.js +228 -110
  114. package/dist/core/fault-classifier.js +1 -1
  115. package/dist/core/fs-utils.d.ts +1 -0
  116. package/dist/core/fs-utils.js +15 -1
  117. package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
  118. package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
  119. package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
  120. package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
  121. package/dist/core/keybindings/catalog.d.ts +14 -0
  122. package/dist/core/keybindings/catalog.js +257 -0
  123. package/dist/core/keybindings/index.d.ts +5 -0
  124. package/dist/core/keybindings/index.js +4 -0
  125. package/dist/core/keybindings/match.d.ts +29 -0
  126. package/dist/core/keybindings/match.js +99 -0
  127. package/dist/core/keybindings/persistence.d.ts +14 -0
  128. package/dist/core/keybindings/persistence.js +31 -0
  129. package/dist/core/keybindings/resolve.d.ts +14 -0
  130. package/dist/core/keybindings/resolve.js +305 -0
  131. package/dist/core/keybindings/types.d.ts +37 -0
  132. package/dist/core/keybindings/types.js +1 -0
  133. package/dist/core/memory-resolver.d.ts +1 -1
  134. package/dist/core/memory-resolver.js +10 -3
  135. package/dist/core/predicate.d.ts +5 -3
  136. package/dist/core/predicate.js +5 -3
  137. package/dist/core/profiles/select.d.ts +6 -0
  138. package/dist/core/profiles/select.js +86 -52
  139. package/dist/core/provider-management.d.ts +12 -0
  140. package/dist/core/provider-management.js +24 -0
  141. package/dist/core/runtime/broker-protocol.d.ts +33 -5
  142. package/dist/core/runtime/broker.js +164 -14
  143. package/dist/core/runtime/launch.d.ts +39 -6
  144. package/dist/core/runtime/launch.js +78 -20
  145. package/dist/core/runtime/naming.js +3 -3
  146. package/dist/core/runtime/pi-cli.d.ts +6 -0
  147. package/dist/core/runtime/pi-cli.js +16 -2
  148. package/dist/core/runtime/pi-vendored.d.ts +8 -0
  149. package/dist/core/runtime/pi-vendored.js +14 -0
  150. package/dist/core/runtime/placement.d.ts +2 -2
  151. package/dist/core/runtime/placement.js +4 -1
  152. package/dist/core/runtime/promote.js +4 -0
  153. package/dist/core/runtime/recap.d.ts +3 -3
  154. package/dist/core/runtime/recap.js +75 -47
  155. package/dist/core/runtime/recycle.js +6 -1
  156. package/dist/core/runtime/reset.js +5 -0
  157. package/dist/core/runtime/session-list-cache.d.ts +33 -0
  158. package/dist/core/runtime/session-list-cache.js +338 -0
  159. package/dist/core/runtime/spawn.js +52 -45
  160. package/dist/core/runtime/stream-watchdog.d.ts +26 -0
  161. package/dist/core/runtime/stream-watchdog.js +75 -0
  162. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  163. package/dist/core/runtime/tmux-chrome.js +2 -2
  164. package/dist/core/runtime/tmux.d.ts +18 -15
  165. package/dist/core/runtime/tmux.js +217 -110
  166. package/dist/core/scope.js +27 -4
  167. package/dist/core/subscription-state.d.ts +90 -0
  168. package/dist/core/subscription-state.js +762 -0
  169. package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
  170. package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
  171. package/dist/core/tui/host.d.ts +30 -1
  172. package/dist/core/tui/host.js +148 -34
  173. package/dist/core/view/contract.d.ts +17 -3
  174. package/dist/daemon/crtrd.js +253 -12
  175. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
  176. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
  177. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
  178. package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
  179. package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
  180. package/dist/pi-extensions/canvas-recap.d.ts +4 -0
  181. package/dist/pi-extensions/canvas-recap.js +73 -46
  182. package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
  183. package/dist/pi-extensions/canvas-stophook.js +33 -4
  184. package/dist/types.d.ts +9 -26
  185. package/dist/types.js +1 -28
  186. package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
  187. package/dist/web-client/index.html +1 -1
  188. package/dist/web-client/sw.js +1 -1
  189. package/package.json +8 -4
  190. package/dist/pi-extensions/canvas-nav.d.ts +0 -43
  191. package/dist/pi-extensions/canvas-nav.js +0 -640
  192. package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
  193. package/dist/pi-extensions/widget-order-bus.js +0 -34
@@ -29,6 +29,7 @@ import { openFocusRow } from '../../../core/canvas/focuses.js';
29
29
  import { jobDir } from '../../../core/canvas/paths.js';
30
30
  import { recordFault } from '../../../core/runtime/fault.js';
31
31
  import { BG_ATTACHED } from '../../../core/canvas/nav-model.js';
32
+ import { resolveKeybindings } from '../../../core/keybindings/index.js';
32
33
  import { dispatchSlashCommand, slashCommandList } from '../slash-commands.js';
33
34
  import { resolveInitialMode, requestModeSwitch, tagAttachPane, untagAttachPane, reapOnDetach, } from '../attach-cmd.js';
34
35
  import { buildCanvasPanelLines } from '../canvas-panels.js';
@@ -198,7 +199,7 @@ test('GraphOverlay suppresses every local-read adornment for a RemoteCanvasSourc
198
199
  t.after(() => server.close());
199
200
  const source = new RemoteCanvasSource(server.url, 'tok');
200
201
  const { tui } = fakeTui();
201
- const overlay = new GraphOverlay(tui, 'root', () => ({}), IDENTITY_PALETTE, source);
202
+ const overlay = new GraphOverlay(tui, 'root', () => ({}), IDENTITY_PALETTE, resolveKeybindings(), source);
202
203
  overlay.open();
203
204
  // buildSnapshot() is async (chased through the mock server); poll for it to land.
204
205
  for (let i = 0; i < 100 && overlay.snapshot === undefined; i++) {
@@ -217,17 +218,21 @@ test('GraphOverlay suppresses every local-read adornment for a RemoteCanvasSourc
217
218
  // effect of a real `focusTarget` dispatch) — it must no-op instead.
218
219
  overlay.handleInput('\r');
219
220
  assert.equal(overlay.isOpen(), true, 'Enter must not act (close) on a remote graph row');
220
- // x must NOT arm the y/n kill confirm render() would show it in the hint.
221
+ // x must NOT arm the close confirm on a remote row. The confirmation UI never
222
+ // literally contains legacy `y/n`, so assert the real state directly:
223
+ // pendingConfirm stays unset and no `kill …?` accept/cancel prompt renders.
221
224
  overlay.handleInput('x');
225
+ assert.equal(overlay.pendingConfirm, undefined, 'x must not arm pendingConfirm on a remote graph row');
222
226
  const afterX = overlay.render(100).join('\n');
223
- assert.ok(!afterX.includes('y/n'), `x must not arm a kill confirm on a remote graph row: ${afterX}`);
227
+ assert.ok(!afterX.includes('kill'), `x must not surface a kill confirm prompt on a remote graph row: ${afterX}`);
228
+ assert.ok(!/accept|cancel/.test(afterX), `x must not surface an accept/cancel confirm on a remote graph row: ${afterX}`);
224
229
  // m must NOT close the overlay either (it would focusTarget the manager).
225
230
  overlay.handleInput('m');
226
231
  assert.equal(overlay.isOpen(), true, 'm must not act (close) on a remote graph row');
227
232
  });
228
233
  // ---------------------------------------------------------------------------
229
234
  // Phase 3 review 2 — the ENCLOSING attach viewer (slash commands, pane tag,
230
- // detach-reap, Alt+M mode-switch), not just the GraphOverlay/panel rendering
235
+ // detach-reap, Alt+Shift+P mode-switch), not just the GraphOverlay/panel rendering
231
236
  // the fix above already covers. Same adversarial shape: seed colliding LOCAL
232
237
  // state for the remote node id `a`, then assert the remote path never touches
233
238
  // it — and, symmetrically, that the LOCAL path is byte-for-byte unchanged
@@ -332,10 +337,10 @@ test('resolveInitialMode/requestModeSwitch never touch the local mode-switch rea
332
337
  lastWrite = { cwd, nodeId, mode };
333
338
  };
334
339
  const remoteOk = requestModeSwitch('/tmp/cwd', 'a', true, 'spec', writeRequest);
335
- assert.equal(remoteOk, false, 'remote Alt+M must no-op (return false)');
340
+ assert.equal(remoteOk, false, 'remote Alt+Shift+P must no-op (return false)');
336
341
  assert.equal(writes, 0, 'remote must never write the local mode-switch request file');
337
342
  const localOk = requestModeSwitch('/tmp/cwd', 'a', false, 'spec', writeRequest);
338
- assert.equal(localOk, true, 'local Alt+M must still write through');
343
+ assert.equal(localOk, true, 'local Alt+Shift+P must still write through');
339
344
  assert.equal(writes, 1, 'local must call the injected local writer exactly once');
340
345
  assert.deepEqual(lastWrite, { cwd: '/tmp/cwd', nodeId: 'a', mode: 'spec' });
341
346
  });
@@ -0,0 +1,113 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { resolveKeybindings } from '../../../core/keybindings/index.js';
4
+ import { detachHint, dispatchAttachBinding, matchesAttachBinding, refreshAttachBindingSnapshot, } from '../attach-cmd.js';
5
+ import { GraphOverlay } from '../graph-overlay.js';
6
+ const PALETTE = {
7
+ accent: (s) => s,
8
+ active: (s) => s,
9
+ info: (s) => s,
10
+ muted: (s) => s,
11
+ faint: (s) => s,
12
+ border: (s) => s,
13
+ bold: (s) => s,
14
+ error: (s) => s,
15
+ warning: (s) => s,
16
+ bashMode: (s) => s,
17
+ bashModeAlt: (s) => s,
18
+ surface: (s) => s,
19
+ };
20
+ function fakeTui() {
21
+ return {
22
+ showOverlay: () => ({ hide: () => { } }),
23
+ requestRender: () => { },
24
+ };
25
+ }
26
+ test('attach input dispatches only the current configured action', () => {
27
+ const bindings = resolveKeybindings({
28
+ 'crtr.attach.detach': ['ctrl+x'],
29
+ 'crtr.attach.clear-or-detach': [],
30
+ 'crtr.attach.graph.toggle': [],
31
+ 'crtr.mode.cycle': ['alt+x'],
32
+ 'crtr.attach.model-ladder.next': ['alt+z'],
33
+ });
34
+ const calls = [];
35
+ const actions = {
36
+ 'crtr.attach.detach': () => calls.push('detach'),
37
+ 'crtr.attach.clear-or-detach': () => calls.push('clear'),
38
+ 'crtr.attach.graph.toggle': () => calls.push('graph'),
39
+ 'crtr.mode.cycle': () => calls.push('mode'),
40
+ 'crtr.attach.model-ladder.next': () => calls.push('ladder'),
41
+ };
42
+ assert.equal(dispatchAttachBinding(bindings, '\x18', actions), true);
43
+ assert.equal(dispatchAttachBinding(bindings, '\x04', actions), false, 'old detach must not reach the listener action');
44
+ assert.equal(dispatchAttachBinding(bindings, '\x1bg', actions), false, 'disabled graph must not reach the listener action');
45
+ assert.equal(dispatchAttachBinding(bindings, '\x1bx', actions), true);
46
+ assert.equal(dispatchAttachBinding(bindings, '\x1bm', actions), false, 'old mode key must not reach the listener action');
47
+ 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(detachHint(bindings), 'Ctrl+X', 'disabled clear-or-detach must fall back to enabled detach');
51
+ const noDetach = resolveKeybindings({ 'crtr.attach.detach': [], 'crtr.attach.clear-or-detach': [] });
52
+ assert.equal(detachHint(noDetach), undefined, 'a disabled pair must not render "Disabled to detach"');
53
+ });
54
+ test('attach startup and reload replace the dispatch/GRAPH snapshots and install each tmux snapshot', () => {
55
+ const first = resolveKeybindings({ 'crtr.attach.detach': ['ctrl+x'], 'crtr.graph.focus': ['f'] });
56
+ const second = resolveKeybindings({ 'crtr.attach.detach': ['ctrl+z'], 'crtr.graph.focus': ['r'] });
57
+ const snapshots = [first, second];
58
+ const graphSnapshots = [];
59
+ const installed = [];
60
+ let active = refreshAttachBindingSnapshot(() => snapshots.shift(), (bindings) => graphSnapshots.push(bindings), () => true, (bindings) => installed.push(bindings));
61
+ assert.equal(matchesAttachBinding(active, 'crtr.attach.detach', '\x18'), true, 'startup snapshot drives attach dispatch');
62
+ assert.equal(active.gestures('crtr.graph.focus')[0], 'f', 'startup snapshot reaches GRAPH');
63
+ active = refreshAttachBindingSnapshot(() => snapshots.shift(), (bindings) => graphSnapshots.push(bindings), () => true, (bindings) => installed.push(bindings));
64
+ assert.equal(matchesAttachBinding(active, 'crtr.attach.detach', '\x18'), false, 'reload replaces the old attach snapshot');
65
+ assert.equal(matchesAttachBinding(active, 'crtr.attach.detach', '\x1a'), true, 'reload dispatches through the new attach snapshot');
66
+ assert.equal(active.gestures('crtr.graph.focus')[0], 'r', 'reload replaces GRAPH bindings too');
67
+ assert.deepEqual(graphSnapshots, [first, second]);
68
+ assert.deepEqual(installed, [first, second], 'the unified tmux installer receives startup and reload snapshots');
69
+ });
70
+ test('GRAPH navigation and hints use effective bindings rather than default literals', () => {
71
+ const bindings = resolveKeybindings({
72
+ 'crtr.graph.down': ['z'],
73
+ 'crtr.graph.up': [],
74
+ 'crtr.graph.focus': ['f'],
75
+ 'crtr.graph.close-subtree': [],
76
+ 'crtr.graph.dismiss': ['q'],
77
+ 'crtr.graph.confirm.accept': ['t'],
78
+ 'crtr.graph.confirm.cancel': ['c'],
79
+ });
80
+ const overlay = new GraphOverlay(fakeTui(), 'a', () => ({}), PALETTE, bindings);
81
+ const internal = overlay;
82
+ internal.cursorId = 'a';
83
+ internal.snapshot = {
84
+ rows: [
85
+ { id: 'a', hasKids: false, isSelf: true, branch: '', cycle: false, collapsed: false },
86
+ { id: 'b', hasKids: false, isSelf: false, branch: '', cycle: false, collapsed: false },
87
+ ],
88
+ activeBelow: new Map(),
89
+ nodes: new Map([['a', null], ['b', null]]),
90
+ childIds: new Map(),
91
+ sortedChildIds: new Map(),
92
+ managerOf: new Map(),
93
+ focused: new Set(),
94
+ };
95
+ overlay.handleInput('j');
96
+ assert.equal(internal.cursorId, 'a', 'replaced default must stop matching');
97
+ overlay.handleInput('z');
98
+ assert.equal(internal.cursorId, 'b', 'replacement must drive navigation');
99
+ const rendered = overlay.render(58).join('\n');
100
+ assert.match(rendered, /F focus/, 'narrow GRAPH footer must prioritize remapped local focus');
101
+ assert.match(rendered, /Q dismiss/, 'narrow GRAPH footer must prioritize remapped dismiss');
102
+ assert.doesNotMatch(rendered, /close/);
103
+ assert.doesNotMatch(rendered, /J down/);
104
+ assert.doesNotMatch(rendered, /Disabled/);
105
+ let closed = 0;
106
+ internal.pendingConfirm = { label: 'close b?', action: () => { closed++; } };
107
+ assert.match(overlay.render(80).join('\n'), /T accept · C cancel/);
108
+ overlay.handleInput('y');
109
+ assert.equal(closed, 0, 'replaced accept default must stop confirming');
110
+ internal.pendingConfirm = { label: 'close b?', action: () => { closed++; } };
111
+ overlay.handleInput('t');
112
+ assert.equal(closed, 1, 'replacement must confirm close');
113
+ });
@@ -1996,7 +1996,7 @@ const SYS_SETUP_OUTPUT = [
1996
1996
  '',
1997
1997
  'Packages:',
1998
1998
  '- pi-crtr-extensions — crtr companion extensions. (installed)',
1999
- '- pi-mode-switch — Alt+M mode cycling. (installed)',
1999
+ '- pi-mode-switch — Alt+Shift+P mode cycling. (installed)',
2000
2000
  '- pi-claude-oauth-adapter — OAuth adapter. (not installed)',
2001
2001
  '',
2002
2002
  'System deps:',
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { ensureRenderer, isRendererReady } from '@crouton-kit/humanloop';
4
+ import { transformMermaidFences } from '../mermaid-render.js';
5
+ // This asserts a REAL termrender render (box-drawing glyphs), so it only holds
6
+ // once the managed renderer is provisioned — true in the full tier and on a dev
7
+ // box, but the fast tier is deliberately termrender-free (no uv to provision the
8
+ // venv). Skip cleanly when the renderer is unavailable, exactly as the tmux
9
+ // tests skip when tmux is absent, rather than failing on the environment.
10
+ ensureRenderer();
11
+ const SKIP = !isRendererReady() ? 'termrender renderer unavailable' : false;
12
+ const APPLET_CONCIERGE_FLOW = `\`\`\`mermaid
13
+ flowchart LR
14
+ U[User] --> C[Applet concierge]
15
+ C --> B[Applet-builder developer node<br/>cwd = app repo]
16
+ B --> D[Platform deploy command]
17
+ D --> B
18
+ B --> C
19
+ C --> U
20
+ \`\`\``;
21
+ test('renders the applet concierge flowchart after provisioning termrender', { skip: SKIP }, () => {
22
+ const rendered = transformMermaidFences(APPLET_CONCIERGE_FLOW, 200);
23
+ assert.match(rendered, /Applet-builder/);
24
+ assert.match(rendered, /cwd = app repo/);
25
+ assert.match(rendered, /Platform deploy/);
26
+ assert.match(rendered, /[\u2500-\u259F\u25A0-\u25FF]/);
27
+ assert.doesNotMatch(rendered, /```mermaid/);
28
+ });
@@ -0,0 +1,110 @@
1
+ // oauth-dialog-lifecycle.test.ts — review mrf68z4h (OAuth dialog lifecycle).
2
+ //
3
+ // Two attach-viewer regressions, proven against a REAL InputController built from
4
+ // minimal fakes:
5
+ //
6
+ // A. An unrelated extension `notify` must NOT tear down a blocking dialog whose
7
+ // broker request is still pending. (The old code dismissed the active dialog
8
+ // on ANY notify — leaving the request unanswered in the broker and the user
9
+ // with no way to answer it.)
10
+ // B. A broker-driven `extension_ui_dismiss` (the correlated cleanup emitted when
11
+ // a dialog's request is aborted/timed out out-of-band) tears down ONLY the
12
+ // overlay whose id matches, never an unrelated one.
13
+ import { test } from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import { initTheme } from '@earendil-works/pi-coding-agent';
16
+ import { InputController } from '../input-controller.js';
17
+ // pi's ExtensionInputComponent reads the process-global theme in its constructor
18
+ // (the broker calls initTheme() at boot); initialize it once so renderDialog can
19
+ // build a real dialog component here.
20
+ initTheme(undefined, false);
21
+ function fakeEditor() {
22
+ const actions = new Map();
23
+ const obj = {
24
+ onSubmit: undefined,
25
+ onEscape: undefined,
26
+ onPasteImage: undefined,
27
+ onAction: (name, cb) => { actions.set(name, cb); },
28
+ getText: () => '',
29
+ setText: () => { },
30
+ addToHistory: () => { },
31
+ insertTextAtCursor: () => { },
32
+ };
33
+ return { obj };
34
+ }
35
+ /** A TUI fake whose `showOverlay` returns a handle whose `hide()` is observable,
36
+ * so a test can tell whether a dialog was actually torn down. */
37
+ function fakeTui() {
38
+ const hides = [];
39
+ let seq = 0;
40
+ const tui = {
41
+ requestRender: () => { },
42
+ setFocus: () => { },
43
+ showOverlay: () => {
44
+ const label = `overlay-${seq++}`;
45
+ return { hide: () => hides.push(label), label };
46
+ },
47
+ start: () => { },
48
+ stop: () => { },
49
+ };
50
+ return { tui, hides };
51
+ }
52
+ function buildController() {
53
+ const notices = [];
54
+ const editor = fakeEditor();
55
+ const { tui, hides } = fakeTui();
56
+ const ic = new InputController(tui, editor.obj, {}, {
57
+ onCommand: () => { },
58
+ onDialogResponse: () => { },
59
+ onNotice: (m) => notices.push(m),
60
+ });
61
+ return { ic, notices, hides };
62
+ }
63
+ const inputDialog = (id) => ({
64
+ type: 'extension_ui_request',
65
+ id,
66
+ method: 'input',
67
+ title: 'Paste the redirect URL or code',
68
+ placeholder: 'http://localhost/callback?...',
69
+ });
70
+ const notify = (message) => ({
71
+ type: 'extension_ui_request',
72
+ id: 'notify-1',
73
+ method: 'notify',
74
+ message,
75
+ notifyType: 'info',
76
+ });
77
+ test('an unrelated notify does NOT dismiss an active blocking dialog (its broker request stays live)', () => {
78
+ const { ic, notices, hides } = buildController();
79
+ ic.attachDialog(inputDialog('oauth-manual-1'));
80
+ assert.equal(hides.length, 0, 'the dialog overlay is up, not torn down');
81
+ // An unrelated extension fires a notify while the blocking dialog is showing.
82
+ ic.attachDialog(notify('Some unrelated background progress'));
83
+ assert.equal(hides.length, 0, 'the blocking dialog must remain on screen — a notify is NOT a dismissal signal');
84
+ assert.ok(notices.some((n) => /unrelated background progress/i.test(n)), `the notify still shows as a notice line, got: ${JSON.stringify(notices)}`);
85
+ });
86
+ test('extension_ui_dismiss tears down ONLY the overlay whose request id matches', () => {
87
+ const { ic, hides } = buildController();
88
+ ic.attachDialog(inputDialog('oauth-manual-1'));
89
+ assert.equal(hides.length, 0);
90
+ // A dismiss for a DIFFERENT request id must be a no-op.
91
+ ic.dismissDialog('some-other-request');
92
+ assert.equal(hides.length, 0, 'a non-matching dismiss must not tear down the current dialog');
93
+ // The correlated dismiss for THIS dialog's id tears it down.
94
+ ic.dismissDialog('oauth-manual-1');
95
+ assert.equal(hides.length, 1, 'the matching correlated dismiss tears down exactly the current overlay');
96
+ // Idempotent: a repeat dismiss (or a late one after teardown) is inert.
97
+ ic.dismissDialog('oauth-manual-1');
98
+ assert.equal(hides.length, 1, 'a repeat dismiss must not double-tear-down');
99
+ });
100
+ test('a new blocking dialog still supersedes the previous one (control-handoff re-route unaffected)', () => {
101
+ const { ic, hides } = buildController();
102
+ ic.attachDialog(inputDialog('dialog-a'));
103
+ ic.attachDialog(inputDialog('dialog-b'));
104
+ assert.equal(hides.length, 1, 'the first dialog is superseded (dismissed) when a second blocking dialog arrives');
105
+ // Now a correlated dismiss for the OLD id must not touch the current one.
106
+ ic.dismissDialog('dialog-a');
107
+ assert.equal(hides.length, 1, 'a stale dismiss for the superseded dialog does not tear down the current one');
108
+ ic.dismissDialog('dialog-b');
109
+ assert.equal(hides.length, 2, 'the current dialog is torn down by its own correlated dismiss');
110
+ });
@@ -1,12 +1,19 @@
1
1
  import { CombinedAutocompleteProvider } from '@earendil-works/pi-tui';
2
2
  import type { BranchDef } from '../../core/command.js';
3
3
  import type { BrokerSnapshot, ClientRole } from '../../core/runtime/broker-protocol.js';
4
+ import { type BindingId, type BindingResolution } from '../../core/keybindings/index.js';
4
5
  import { type AttachPalette } from './config-load.js';
5
6
  export type Mode = 'normal' | 'spec' | 'plan';
7
+ type AttachBindingId = Extract<BindingId, `crtr.attach.${string}` | 'crtr.mode.cycle'>;
8
+ type AttachBindings = BindingResolution<BindingId>;
9
+ export declare function matchesAttachBinding(bindings: AttachBindings, id: AttachBindingId, data: string): boolean;
10
+ export declare function dispatchAttachBinding(bindings: AttachBindings, data: string, actions: Record<AttachBindingId, () => void>): boolean;
11
+ export declare function detachHint(bindings: AttachBindings): string | undefined;
12
+ export declare function refreshAttachBindingSnapshot(resolve: () => AttachBindings, setGraphBindings: (bindings: AttachBindings) => void, inTmuxNow: () => boolean, install: (bindings: AttachBindings) => void): AttachBindings;
6
13
  /** Resolve the mode badge's initial state. Remote never reads the local
7
14
  * mode-switch `current` file for `nodeId` — always starts 'normal'. */
8
15
  export declare function resolveInitialMode(cwd: string, nodeId: string, remote: boolean, readMode?: (cwd: string, nodeId: string) => Mode | undefined): Mode;
9
- /** Handle Alt+M's mode-cycle request. Remote no-ops entirely (returns `false`,
16
+ /** Handle a configured mode-cycle request. Remote no-ops entirely (returns `false`,
10
17
  * so the caller leaves `modeState`/the badge untouched) — there is no
11
18
  * remote-safe mode-switch protocol, and writing the local `request.json` for a
12
19
  * remote node id would target the wrong (or a colliding local) node. */
@@ -46,3 +53,4 @@ export declare function buildAttachAutocompleteProvider(cwd: string, fdPath: str
46
53
  description?: string;
47
54
  }>, cwd: string, fdPath: string | null) => CombinedAutocompleteProvider): CombinedAutocompleteProvider;
48
55
  export declare function registerAttach(): BranchDef;
56
+ export {};