@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
@@ -0,0 +1,55 @@
1
+ // The scope ancestor walk (`collectAncestorScopeRoots` → `findProjectScopeRoots`)
2
+ // must never climb at or above `homedir()`. When HOME is relocated and nested
3
+ // under a directory tree that itself contains a `.crouter/` (a mock-VM home
4
+ // under the real host home, or any relocated-HOME sandbox), the unfenced walk
5
+ // climbs ABOVE homedir() and sweeps in the above-HOME `.crouter` as a bogus
6
+ // "project ancestor" — leaking that host's entire user-global memory store into
7
+ // every nested-HOME node's assembled knowledge/preference catalog. This test
8
+ // pins the fence: an ancestor `.crouter/` sitting ABOVE the relocated HOME must
9
+ // NOT be collected as a project scope root, while a legitimate project
10
+ // `.crouter/` BETWEEN the cwd and HOME still is.
11
+ //
12
+ // Mirrors src/core/__tests__/on-read-crouter-home-fence.test.ts.
13
+ //
14
+ // Run: node --import tsx/esm --test src/core/__tests__/scope-crouter-home-fence.test.ts
15
+ import { test, before, beforeEach, after } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { findProjectScopeRoots, resetScopeCache } from '../scope.js';
21
+ let sandbox;
22
+ let prevHomeEnv;
23
+ before(() => {
24
+ prevHomeEnv = process.env['HOME'];
25
+ });
26
+ beforeEach(() => {
27
+ resetScopeCache();
28
+ // A sandbox that CONTAINS the relocated HOME, so the ancestor walk from a cwd
29
+ // under HOME can (without the fence) climb above HOME into the sandbox.
30
+ sandbox = mkdtempSync(join(tmpdir(), 'crtr-scope-fence-'));
31
+ });
32
+ after(() => {
33
+ resetScopeCache();
34
+ rmSync(sandbox, { recursive: true, force: true });
35
+ if (prevHomeEnv === undefined)
36
+ delete process.env['HOME'];
37
+ else
38
+ process.env['HOME'] = prevHomeEnv;
39
+ });
40
+ test('an ancestor .crouter ABOVE a relocated HOME is not collected as a project scope root', () => {
41
+ // Simulated host user-global store, sitting ABOVE the relocated home.
42
+ const hostUserRoot = join(sandbox, '.crouter');
43
+ mkdirSync(join(hostUserRoot, 'memory'), { recursive: true });
44
+ // The relocated HOME, nested one level under the sandbox.
45
+ const home = join(sandbox, 'vm-home');
46
+ process.env['HOME'] = home;
47
+ // A legitimate project `.crouter/` between the node cwd and HOME.
48
+ const projectDir = join(home, 'work', 'proj');
49
+ const projectRoot = join(projectDir, '.crouter');
50
+ mkdirSync(join(projectRoot, 'memory'), { recursive: true });
51
+ resetScopeCache();
52
+ const roots = findProjectScopeRoots(projectDir, null);
53
+ assert.ok(!roots.includes(hostUserRoot), `above-HOME .crouter must NOT be collected as a project ancestor; got: ${JSON.stringify(roots)}`);
54
+ assert.ok(roots.includes(projectRoot), `legitimate project .crouter between cwd and HOME must still be collected; got: ${JSON.stringify(roots)}`);
55
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/stream-watchdog.test.ts
2
+ //
3
+ // Unit-level contract test for StreamWatchdog (see stream-watchdog.ts): a
4
+ // single-timer dead-man's switch the broker pokes while a turn awaits the
5
+ // provider (disarmed during tool execution) and clears at agent_end. This
6
+ // locks the class's arm/clear/single-fire/token contract directly — no broker
7
+ // or fake-engine harness needed (that integration surface is deliberately out
8
+ // of scope; see
9
+ // the roadmap's Fix 3 test-scope note).
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { StreamWatchdog } from '../runtime/stream-watchdog.js';
13
+ const MS = 30;
14
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+ test('poke() arms the watchdog, firing once after ms with no further poke', async () => {
16
+ let calls = 0;
17
+ const wd = new StreamWatchdog(() => calls++, MS);
18
+ wd.poke(0);
19
+ assert.equal(calls, 0, 'must not fire before ms elapses');
20
+ await sleep(MS * 2);
21
+ assert.equal(calls, 1, 'must fire exactly once after ms with no further poke');
22
+ });
23
+ test('repeated pokes rearm the deadline from the LAST poke, not the first', async () => {
24
+ // A cancel-without-reschedule bug (clear the old timer but never arm a new
25
+ // one) would also "never fire while poked", so proving no-fire mid-stream is
26
+ // not enough. Prove the deadline actually REARMS: no fire around the original
27
+ // deadline, then exactly one fire measured from the FINAL poke.
28
+ let calls = 0;
29
+ const wd = new StreamWatchdog(() => calls++, MS);
30
+ wd.poke(0);
31
+ // Re-poke well inside the window, several times, always resetting the clock.
32
+ // The last poke lands at ~t=2.5*MS, so the original t=MS deadline must pass
33
+ // with no fire.
34
+ for (let i = 0; i < 5; i++) {
35
+ await sleep(MS / 2);
36
+ wd.poke(0);
37
+ }
38
+ assert.equal(calls, 0, 'no fire around the original deadline — each poke rearmed it');
39
+ // From the final poke the timer must still be armed and fire exactly once.
40
+ await sleep(MS * 2);
41
+ assert.equal(calls, 1, 'exactly one fire, measured from the final poke — proves rearming');
42
+ });
43
+ test('clear() after poke prevents firing', async () => {
44
+ let calls = 0;
45
+ const wd = new StreamWatchdog(() => calls++, MS);
46
+ wd.poke(0);
47
+ wd.clear();
48
+ await sleep(MS * 2);
49
+ assert.equal(calls, 0, 'a cleared watchdog must never fire');
50
+ });
51
+ test('fires exactly once even if left armed well past ms', async () => {
52
+ let calls = 0;
53
+ const wd = new StreamWatchdog(() => calls++, MS);
54
+ wd.poke(0);
55
+ await sleep(MS * 5);
56
+ assert.equal(calls, 1, 'must fire only once, not repeatedly, past the deadline');
57
+ });
58
+ test('onStall receives the token of the arm that fired (generation binding)', async () => {
59
+ // The broker stamps each poke with the current session generation and no-ops
60
+ // a stall whose token is stale. This pins that the token travels with the
61
+ // arm: a re-poke supersedes the prior token, so a fire after a rebind carries
62
+ // the NEW generation — and an old timer that somehow survived would deliver
63
+ // the OLD one, which the broker guard rejects.
64
+ const tokens = [];
65
+ const wd = new StreamWatchdog((token) => tokens.push(token), MS);
66
+ wd.poke(1);
67
+ wd.poke(2); // supersedes generation 1 before it can fire
68
+ await sleep(MS * 2);
69
+ assert.deepEqual(tokens, [2], 'exactly one fire, carrying the most-recent arm\'s token');
70
+ });
@@ -18,8 +18,11 @@
18
18
  import { test } from 'node:test';
19
19
  import assert from 'node:assert/strict';
20
20
  import { readFileSync, readdirSync, statSync } from 'node:fs';
21
+ import { spawnSync } from 'node:child_process';
21
22
  import { fileURLToPath } from 'node:url';
22
23
  import { dirname, join, basename } from 'node:path';
24
+ import { installTmuxBindings } from '../runtime/tmux-chrome.js';
25
+ import { resolveKeybindings } from '../keybindings/index.js';
23
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
27
  const SRC_ROOT = join(__dirname, '..', '..'); // .../src
25
28
  const TMUX_TS = join(SRC_ROOT, 'core', 'runtime', 'tmux.ts');
@@ -113,3 +116,72 @@ test('§5.1 lint: only placement.ts / tmux-chrome.ts import the tmux driver', ()
113
116
  .map((f) => f.slice(SRC_ROOT.length + 1));
114
117
  assert.deepEqual(offenders, [], `modules importing tmux.ts directly: ${offenders.join(', ')}`);
115
118
  });
119
+ function isolatedTmux(socket, args) {
120
+ const result = spawnSync('tmux', ['-S', socket, ...args], { encoding: 'utf8' });
121
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
122
+ }
123
+ function tmuxBinding(socket, table, key) {
124
+ const result = isolatedTmux(socket, ['list-keys', '-T', table]);
125
+ if (result.status !== 0)
126
+ return null;
127
+ return result.stdout.split('\n').find((line) => {
128
+ const match = line.match(/^bind-key\s+(?:-r\s+)?-T\s+\S+\s+(\S+)\s+/);
129
+ return match?.[1] === key;
130
+ }) ?? null;
131
+ }
132
+ test('tmux installer remaps owned pairs and preserves occupied user bindings', (t) => {
133
+ if (spawnSync('tmux', ['-V']).status !== 0) {
134
+ t.skip('tmux is unavailable');
135
+ return;
136
+ }
137
+ const name = `crtr-keybindings-${process.pid}-${Date.now()}`;
138
+ const boot = spawnSync('tmux', ['-L', name, 'new-session', '-d', '-s', 'test'], { encoding: 'utf8' });
139
+ assert.equal(boot.status, 0, boot.stderr);
140
+ const socketResult = spawnSync('tmux', ['-L', name, 'display-message', '-p', '#{socket_path}'], { encoding: 'utf8' });
141
+ const socket = socketResult.stdout.trim();
142
+ assert.notEqual(socket, '');
143
+ const priorTmux = process.env['TMUX'];
144
+ process.env['TMUX'] = `${socket},1,0`;
145
+ try {
146
+ isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-]', 'run-shell', `crtr node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`]);
147
+ const first = installTmuxBindings(resolveKeybindings());
148
+ assert.equal(first.ok, true, JSON.stringify(first.diagnostics));
149
+ assert.ok(first.installed.some((pair) => pair.table === 'root' && pair.key === 'M-]'));
150
+ assert.match(tmuxBinding(socket, 'root', 'M-c') ?? '', /display-menu/);
151
+ assert.match(tmuxBinding(socket, 'root', 'M-v') ?? '', /switch-client/);
152
+ const remapped = installTmuxBindings(resolveKeybindings({
153
+ 'crtr.tmux.menu.open': ['alt+shift+z'],
154
+ 'crtr.tmux.menu.graph': ['shift+z'],
155
+ 'crtr.tmux.node.next': [],
156
+ 'crtr.tmux.view.next': ['alt+z > n'],
157
+ 'crtr.tmux.view.previous': ['alt+z > p'],
158
+ }));
159
+ assert.equal(remapped.ok, true, JSON.stringify(remapped.diagnostics));
160
+ assert.equal(tmuxBinding(socket, 'root', 'M-c'), null);
161
+ assert.equal(tmuxBinding(socket, 'root', 'M-]'), null);
162
+ const menu = tmuxBinding(socket, 'root', 'M-Z') ?? '';
163
+ assert.match(menu, /graph view/);
164
+ assert.match(menu, /Z/);
165
+ const prefix = tmuxBinding(socket, 'root', 'M-z') ?? '';
166
+ assert.match(prefix, /switch-client/);
167
+ const again = installTmuxBindings(resolveKeybindings({
168
+ 'crtr.tmux.menu.open': ['alt+shift+y'],
169
+ 'crtr.tmux.node.next': [],
170
+ 'crtr.tmux.view.next': ['alt+z > n'],
171
+ 'crtr.tmux.view.previous': ['alt+z > p'],
172
+ }));
173
+ assert.equal(again.ok, true, JSON.stringify(again.diagnostics));
174
+ isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-Z', 'display-message', 'user-owned']);
175
+ const occupied = installTmuxBindings(resolveKeybindings({ 'crtr.tmux.menu.open': ['alt+shift+z'] }));
176
+ assert.equal(occupied.ok, true);
177
+ assert.ok(occupied.diagnostics.some((diagnostic) => diagnostic.code === 'occupied' && diagnostic.bindingId === 'crtr.tmux.menu.open'));
178
+ assert.match(tmuxBinding(socket, 'root', 'M-Z') ?? '', /user-owned/);
179
+ }
180
+ finally {
181
+ if (priorTmux === undefined)
182
+ delete process.env['TMUX'];
183
+ else
184
+ process.env['TMUX'] = priorTmux;
185
+ isolatedTmux(socket, ['kill-server']);
186
+ }
187
+ });
@@ -108,20 +108,38 @@ test('pruneNode: absent id is a no-op', () => {
108
108
  assert.equal(t.nodes.size, before);
109
109
  });
110
110
  // ── tab predicates ───────────────────────────────────────────────────────────
111
- test('tabPredicate: All / Live / Dormant / Flagged', () => {
112
- assert.deepEqual([...TABS], ['All', 'Live', 'Dormant', 'Flagged']);
111
+ test('tabPredicate: All / Live / Dormant / Attention / Pinned', () => {
112
+ assert.deepEqual([...TABS], ['All', 'Live', 'Dormant', 'Attention', 'Pinned']);
113
113
  const active = ROWS[0]; // root1 active
114
114
  const idle = ROWS[1]; // child-a idle
115
115
  const done = ROWS[2]; // grand-x done
116
- const flagged = ROWS[3]; // child-b active asks 2
116
+ const attention = ROWS[3]; // child-b active asks 2
117
117
  assert.equal(tabPredicate('All', done), true);
118
118
  assert.equal(tabPredicate('Live', active), true);
119
119
  assert.equal(tabPredicate('Live', idle), true);
120
120
  assert.equal(tabPredicate('Live', done), false);
121
121
  assert.equal(tabPredicate('Dormant', done), true);
122
122
  assert.equal(tabPredicate('Dormant', active), false);
123
- assert.equal(tabPredicate('Flagged', flagged), true);
124
- assert.equal(tabPredicate('Flagged', active), false);
123
+ // Attention = pending asks OR a fault.
124
+ assert.equal(tabPredicate('Attention', attention), true);
125
+ assert.equal(tabPredicate('Attention', active), false);
126
+ assert.equal(tabPredicate('Attention', { ...active, asks: 0, hanging: { kind: 'provider' } }), true);
127
+ // Pinned reads the isPinned arg, not the row.
128
+ assert.equal(tabPredicate('Pinned', active), false);
129
+ assert.equal(tabPredicate('Pinned', active, true), true);
130
+ });
131
+ // ── flatten: pins ────────────────────────────────────────────────────────────
132
+ test('flatten: Pinned tab shows only pinned rows', () => {
133
+ const pinned = new Set(['child-c']);
134
+ const v = flatten(tree(), { collapsed: new Set(), tab: 'Pinned', query: '', pinned });
135
+ // child-c is directly matched; root2 rides along as its ancestor (context).
136
+ const matched = v.filter((r) => r.matched).map((r) => r.id);
137
+ assert.deepEqual(matched, ['child-c']);
138
+ });
139
+ test('flatten: pinned rows float to the top in a flat sort', () => {
140
+ const pinned = new Set(['lonely']); // an idle straggler that would otherwise sort late
141
+ const v = flatten(tree(), { collapsed: new Set(), tab: 'All', query: '', sort: 'recency', pinned });
142
+ assert.equal(v[0].id, 'lonely');
125
143
  });
126
144
  // ── flatten: collapse / expand ───────────────────────────────────────────────
127
145
  const allCollapsed = () => new Set(['root1', 'child-a', 'root2']); // every node with children
@@ -37,6 +37,7 @@ function state(over = {}) {
37
37
  sort: 'tree',
38
38
  preview: false,
39
39
  residentsOnly: false,
40
+ pinned: new Set(),
40
41
  ...over,
41
42
  };
42
43
  }
@@ -1,6 +1,11 @@
1
1
  import { type CanvasSource } from '../source.js';
2
2
  import type { RemoteEventStream } from '../remote-event-stream.js';
3
3
  import { type CoalescedRebuilder } from './rebuild-coalescer.js';
4
+ import { type BindingId, type BindingResolution, type RawTerminalInput } from '../../keybindings/index.js';
5
+ declare const BROWSE_NAV_ACTIONS: readonly ["crtr.browse.quit", "crtr.browse.down", "crtr.browse.up", "crtr.browse.first", "crtr.browse.last", "crtr.browse.expand-or-child", "crtr.browse.collapse-or-parent", "crtr.browse.toggle-fold", "crtr.browse.tab-next", "crtr.browse.tab-previous", "crtr.browse.tab.1", "crtr.browse.tab.2", "crtr.browse.tab.3", "crtr.browse.tab.4", "crtr.browse.tab.5", "crtr.browse.close-subtree", "crtr.browse.sort", "crtr.browse.scope", "crtr.browse.residents", "crtr.browse.preview", "crtr.browse.search", "crtr.browse.revive-now", "crtr.browse.focus"];
6
+ declare const BROWSE_SEARCH_ACTIONS: readonly ["crtr.browse.search.cancel", "crtr.browse.search.commit", "crtr.browse.search.delete", "crtr.browse.search.quit"];
7
+ export declare function resolveBrowseNavAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_NAV_ACTIONS)[number] | null;
8
+ export declare function resolveBrowseSearchAction(bindings: BindingResolution<BindingId>, raw: RawTerminalInput): (typeof BROWSE_SEARCH_ACTIONS)[number] | null;
4
9
  /** Wires a RemoteEventStream's `nodes`/`inbox` events through the coalescer
5
10
  * (`rebuild-coalescer.ts`) so a burst of SSE events collapses to ONE rebuild
6
11
  * round, never one N+1 remote-subprocess rebuild per event. Exported (rather
@@ -18,3 +23,4 @@ export declare function runBrowse(opts?: {
18
23
  source?: CanvasSource;
19
24
  eventStream?: RemoteEventStream;
20
25
  }): Promise<void>;
26
+ export {};