@north-light/crouter 0.3.162 → 0.3.164

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 (100) hide show
  1. package/dist/builtin-memory/00-runtime-base.md +1 -0
  2. package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
  3. package/dist/builtin-memory/init.md +38 -0
  4. package/dist/builtin-memory/internal/INDEX.md +1 -0
  5. package/dist/builtin-memory/internal/memory-loading.md +45 -0
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
  7. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
  8. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +34 -2
  9. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +14 -3
  10. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
  11. package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
  12. package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
  13. package/dist/clients/attach/__tests__/edit-diff.test.js +35 -0
  14. package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
  15. package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
  16. package/dist/clients/attach/render/chat-view.js +54 -38
  17. package/dist/clients/attach/render/crtr-output.d.ts +2 -1
  18. package/dist/clients/attach/render/crtr-output.js +33 -4
  19. package/dist/clients/attach/render/edit-diff.d.ts +21 -6
  20. package/dist/clients/attach/render/edit-diff.js +104 -81
  21. package/dist/clients/attach/render/tool-calls.d.ts +21 -20
  22. package/dist/clients/attach/render/tool-calls.js +89 -43
  23. package/dist/clients/attach/session/connection.d.ts +1 -1
  24. package/dist/clients/attach/session/editor-frame.d.ts +5 -1
  25. package/dist/clients/attach/session/editor-frame.js +21 -15
  26. package/dist/clients/attach/session/identity.d.ts +1 -1
  27. package/dist/clients/attach/viewer.js +628 -624
  28. package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
  29. package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
  30. package/dist/commands/__tests__/human.test.js +2 -0
  31. package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
  32. package/dist/commands/__tests__/search-contents.test.js +52 -0
  33. package/dist/commands/api-client.js +1 -0
  34. package/dist/commands/human/prompts.js +2 -2
  35. package/dist/commands/human/shared.d.ts +1 -0
  36. package/dist/commands/human/shared.js +7 -4
  37. package/dist/commands/memory/lint.d.ts +6 -0
  38. package/dist/commands/memory/lint.js +177 -35
  39. package/dist/commands/memory/read.js +22 -1
  40. package/dist/commands/memory/write.js +18 -4
  41. package/dist/commands/memory.js +1 -1
  42. package/dist/commands/search/contents.js +2 -2
  43. package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
  44. package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
  45. package/dist/commands/sys/setup-wizard.d.ts +8 -1
  46. package/dist/commands/sys/setup-wizard.js +209 -58
  47. package/dist/commands/sys/sync-deps.js +14 -18
  48. package/dist/commands/sys/sync-project-guidance.js +16 -19
  49. package/dist/core/__tests__/migration.test.js +8 -3
  50. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
  51. package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
  52. package/dist/core/__tests__/on-read-identity.test.js +8 -15
  53. package/dist/core/__tests__/revive.test.js +2 -2
  54. package/dist/core/__tests__/tmux-surface.test.js +10 -4
  55. package/dist/core/__tests__/worktree.test.js +3 -3
  56. package/dist/core/canvas/extensions.d.ts +1 -1
  57. package/dist/core/canvas/extensions.js +18 -11
  58. package/dist/core/canvas/labels.d.ts +4 -5
  59. package/dist/core/canvas/labels.js +9 -9
  60. package/dist/core/command-manifests/schema.js +18 -5
  61. package/dist/core/command.js +17 -7
  62. package/dist/core/configured-clis/invoker.js +2 -0
  63. package/dist/core/help.d.ts +3 -1
  64. package/dist/core/help.js +5 -2
  65. package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
  66. package/dist/core/keybindings/catalog.d.ts +3 -2
  67. package/dist/core/keybindings/catalog.js +34 -25
  68. package/dist/core/keybindings/index.d.ts +1 -1
  69. package/dist/core/keybindings/resolve.js +8 -0
  70. package/dist/core/keybindings/types.d.ts +6 -1
  71. package/dist/core/memory/doc-link-grammar.d.ts +20 -0
  72. package/dist/core/memory/doc-link-grammar.js +110 -0
  73. package/dist/core/memory-resolver.d.ts +5 -0
  74. package/dist/core/memory-resolver.js +12 -1
  75. package/dist/core/runtime/bearings.d.ts +8 -8
  76. package/dist/core/runtime/bearings.js +20 -16
  77. package/dist/core/runtime/broker.js +3 -13
  78. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  79. package/dist/core/runtime/canvas-extensions.js +2 -0
  80. package/dist/core/runtime/launch.d.ts +1 -1
  81. package/dist/core/runtime/launch.js +1 -1
  82. package/dist/core/runtime/tmux.js +136 -100
  83. package/dist/core/scope.js +4 -5
  84. package/dist/core/substrate/ceiling.d.ts +3 -6
  85. package/dist/core/substrate/ceiling.js +13 -15
  86. package/dist/core/substrate/index.d.ts +2 -3
  87. package/dist/core/substrate/index.js +2 -2
  88. package/dist/core/substrate/on-read.d.ts +4 -13
  89. package/dist/core/substrate/on-read.js +142 -262
  90. package/dist/core/substrate/render.js +4 -4
  91. package/dist/core/substrate/schema.d.ts +3 -2
  92. package/dist/core/substrate/schema.js +5 -5
  93. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
  94. package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
  95. package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
  96. package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
  97. package/dist/pi-extensions/canvas-tool-guide.js +77 -0
  98. package/package.json +2 -2
  99. package/runtime.lock.json +6 -6
  100. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
@@ -1,17 +1,6 @@
1
- // Bug-regression: on-read doc injection "fires a second time per session".
2
- //
3
- // The on-read substrate hook dedups so a doc surfaces at most once per
4
- // conversation. That dedup set USED to live only in the pi process heap (cleared
5
- // on session_start). But a node's logical session — the .jsonl transcript —
6
- // spans MULTIPLE pi processes: a dormancy → revive(resume) cycle exits the old
7
- // process and launches a fresh `pi --session` that REUSES the same transcript.
8
- // The fresh process started with an empty set, so any doc already injected
9
- // before dormancy got injected AGAIN on the next read.
10
- //
11
- // The fix persists the set to nodes/<id>/injected-docs.json: a resume rehydrates
12
- // it (dedup holds), and only the FRESH-transcript launch paths clear it. These
13
- // tests lock in both halves: the store round-trip, and the dedup surviving a
14
- // simulated revive(resume) while re-surfacing after a fresh launch.
1
+ // On-read context surfaces at most once per conversation. The dedup set lives
2
+ // at nodes/<id>/injected-docs.json so a process resumed onto the same transcript
3
+ // rehydrates it, while a fresh transcript clears it.
15
4
  //
16
5
  // Run: node --import tsx/esm --test src/core/__tests__/on-read-dedup-resume.test.ts
17
6
  import { test, before, beforeEach, after } from 'node:test';
@@ -64,7 +53,7 @@ test('injected-store round-trips and clears', () => {
64
53
  assert.ok(!existsSync(injectedDocsPath(node)), 'clear removes the file');
65
54
  assert.deepEqual([...loadInjectedDocs(node)], [], 'load after clear is empty');
66
55
  });
67
- test('user-global memory docs do not fire positionally for every file under home', () => {
56
+ test('project and user memory docs without applies-to do not fire on file reads', () => {
68
57
  const fakeHome = mkdtempSync(join(tmpdir(), 'crtr-onread-user-home-'));
69
58
  process.env['HOME'] = fakeHome;
70
59
  resetScopeCache();
@@ -72,33 +61,32 @@ test('user-global memory docs do not fire positionally for every file under home
72
61
  const userMemDir = join(fakeHome, '.crouter', 'memory');
73
62
  mkdirSync(userMemDir, { recursive: true });
74
63
  writeFileSync(join(userMemDir, 'noisy-global.md'), '---\nkind: preference\n' +
75
- 'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire positionally\n' +
64
+ 'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n' +
76
65
  'file-read-visibility: content\n---\n' +
77
66
  'NOISY USER BODY\n');
78
67
  const projectMemDir = join(fakeHome, 'work', '.crouter', 'memory');
79
68
  mkdirSync(projectMemDir, { recursive: true });
80
69
  writeFileSync(join(projectMemDir, 'local.md'), '---\nkind: knowledge\n' +
81
- 'when-and-why-to-read: When reading project files, this reference should be read because local project memories remain positional\n' +
70
+ 'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n' +
82
71
  'file-read-visibility: content\n---\n' +
83
72
  'LOCAL PROJECT BODY\n');
84
73
  const readFile = join(fakeHome, 'work', 'ideas.md');
85
74
  writeFileSync(readFile, 'turn off xml closer\n');
86
75
  const rendered = renderOnReadDocs(node, readFile, new Set());
87
- assert.ok(!rendered.includes('NOISY USER BODY'), 'user-global doc is not positional');
88
- assert.ok(rendered.includes('LOCAL PROJECT BODY'), 'project doc still fires positionally');
89
- assert.ok(rendered.includes('<memory kind="knowledge" name="local"'), 'memory docs render as memory tags');
90
- assert.ok(!rendered.includes('triggered-by='), 'read target is not repeated on child items');
91
- assert.ok(!rendered.includes('<auto-loaded-context file='), 'read target is not repeated on the wrapper');
76
+ assert.ok(!rendered.includes('NOISY USER BODY'), 'user-global doc has no explicit file route');
77
+ assert.ok(!rendered.includes('LOCAL PROJECT BODY'), 'project doc also has no implicit file route');
78
+ assert.equal(rendered, '', 'no file context renders without an explicit trigger');
92
79
  rmSync(fakeHome, { recursive: true, force: true });
93
80
  });
94
81
  test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfaces after a fresh launch', () => {
95
82
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
96
- // A positional substrate doc in a `.crouter/memory/` ancestor of the read file.
83
+ // A file-routed substrate doc in a `.crouter/memory/` ancestor.
97
84
  const memDir = join(work, '.crouter', 'memory');
98
85
  mkdirSync(memDir, { recursive: true });
99
86
  writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
100
87
  'when-and-why-to-read: When reading work files, this reference should be read because it is the on-read regression fixture\n' +
101
- 'file-read-visibility: content\n---\n' +
88
+ 'file-read-visibility: content\n' +
89
+ 'applies-to: "src/**"\n---\n' +
102
90
  `${FIXTURE_BODY}\n`);
103
91
  const readFile = join(work, 'src', 'file.ts');
104
92
  mkdirSync(join(work, 'src'), { recursive: true });
@@ -113,7 +101,7 @@ test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfa
113
101
  // --- dormancy → revive(resume): NEW process rehydrates the set from disk ---
114
102
  const seenAfterRevive = loadInjectedDocs(node);
115
103
  assert.deepEqual(seenAfterRevive, seen, 'revive(resume) rehydrates the persisted dedup set');
116
- assert.ok(!renderOnReadDocs(node, readFile, seenAfterRevive).includes(FIXTURE_BODY), 'resumed process does NOT re-inject the doc (the bug)');
104
+ assert.ok(!renderOnReadDocs(node, readFile, seenAfterRevive).includes(FIXTURE_BODY), 'resumed process does not re-inject the doc');
117
105
  // --- fresh launch (revive resume=false, e.g. a refresh-yield) clears the set ---
118
106
  clearInjectedDocs(node);
119
107
  const seenFresh = loadInjectedDocs(node);
@@ -1,13 +1,7 @@
1
- // The on-read POSITIONAL loader resolves a doc's identity with the SAME
1
+ // The explicit file-context loader resolves a doc's identity with the same
2
2
  // explicit-name-then-path-fallback rule the boot resolver uses (schema.ts's
3
3
  // resolveDocName), never deriving `name` from the physical path alone.
4
4
  //
5
- // A migrated AGENTS.md doc (`crtr sys sync project-guidance`'s CLAUDE.md/AGENTS.md conversion)
6
- // is written at `file-read-visibility: content` with an explicit frontmatter
7
- // `name` (the project label, e.g. `crouter`) so sibling project roots do not
8
- // collapse onto the shared `AGENTS` leaf. `loadPositionalDoc` must inject the
9
- // doc under that explicit name, not the path-derived `AGENTS`.
10
- //
11
5
  // Run: node --import tsx/esm --test src/core/__tests__/on-read-identity.test.ts
12
6
  import { test, before, beforeEach, after } from 'node:test';
13
7
  import assert from 'node:assert/strict';
@@ -47,22 +41,21 @@ after(() => {
47
41
  else
48
42
  process.env['HOME'] = prevHomeEnv;
49
43
  });
50
- test('a migrated AGENTS doc injects on-read under its explicit frontmatter name, not the path-derived one', () => {
44
+ test('an explicitly routed doc injects on-read under its frontmatter name, not the path-derived one', () => {
51
45
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
52
- // Shape of a `crtr sys sync project-guidance` CLAUDE.md/AGENTS.md migration: physical filename
53
- // stays AGENTS.md, but frontmatter carries the project's explicit identity.
54
46
  const memDir = join(work, '.crouter', 'memory');
55
47
  mkdirSync(memDir, { recursive: true });
56
- writeFileSync(join(memDir, 'AGENTS.md'), '---\nkind: knowledge\n' +
48
+ writeFileSync(join(memDir, 'physical-guide.md'), '---\nkind: knowledge\n' +
57
49
  'name: acme-project\n' +
58
- 'when-and-why-to-read: When working in acme-project, this knowledge should be read because it is the project\'s operating guide.\n' +
59
- 'file-read-visibility: content\n---\n' +
50
+ 'when-and-why-to-read: When reading acme source, this knowledge should be read because it carries the project-specific implementation constraints.\n' +
51
+ 'file-read-visibility: content\n' +
52
+ 'applies-to: "src/**"\n---\n' +
60
53
  'ACME PROJECT OPERATING GUIDE\n');
61
54
  const readFile = join(work, 'src', 'file.ts');
62
55
  mkdirSync(join(work, 'src'), { recursive: true });
63
56
  writeFileSync(readFile, 'export const x = 1;\n');
64
57
  const rendered = renderOnReadDocs(node, readFile, new Set());
65
- assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the migrated doc fires positionally');
58
+ assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the matching explicit route fires');
66
59
  assert.ok(rendered.includes('<memory kind="knowledge" name="acme-project"'), `expected explicit frontmatter name "acme-project" in the injected envelope, got: ${rendered}`);
67
- assert.ok(!rendered.includes('name="AGENTS"'), 'must not fall back to the path-derived name when frontmatter sets one');
60
+ assert.ok(!rendered.includes('name="physical-guide"'), 'must not fall back to the path-derived name when frontmatter sets one');
68
61
  });
@@ -193,9 +193,9 @@ test('buildPiArgv restores mandatory canvas extensions when persisted launch met
193
193
  }
194
194
  assert.deepEqual(extensions.slice(0, required.size), mandatory, 'mandatory extensions are loaded first and once');
195
195
  });
196
- test('buildPiArgv replaces the historical stophook-first prefix without loading old-generation hooks twice', () => {
196
+ test('buildPiArgv replaces the previous mandatory prefix with the current canvas hooks', () => {
197
197
  const historicalNames = [
198
- 'canvas-stophook', 'canvas-inbox-watcher', 'canvas-recap',
198
+ 'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
199
199
  'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
200
200
  'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
201
201
  ];
@@ -224,6 +224,7 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
224
224
  const remapped = installTmuxBindings(resolveKeybindings({
225
225
  'crtr.tmux.menu.open': ['alt+shift+z'],
226
226
  'crtr.tmux.menu.graph': ['shift+z'],
227
+ 'crtr.tmux.menu-global.fold-tools': ['alt+shift+f'],
227
228
  'crtr.tmux.node.next': [],
228
229
  'crtr.tmux.view.next': ['alt+z > n'],
229
230
  'crtr.tmux.view.previous': ['alt+z > p'],
@@ -231,10 +232,15 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
231
232
  assert.equal(remapped.ok, true, JSON.stringify(remapped.diagnostics));
232
233
  assert.equal(tmuxBinding(socket, 'root', 'M-c'), null);
233
234
  assert.equal(tmuxBinding(socket, 'root', 'M-]'), null);
234
- const menu = tmuxBinding(socket, 'root', 'M-Z') ?? '';
235
+ assert.equal(tmuxBinding(socket, 'root', 'M-F'), null);
236
+ assert.equal(tmuxBinding(socket, 'root', 'M-S-f'), null);
237
+ const foldTools = tmuxBinding(socket, 'root', 'M-S-F') ?? '';
238
+ assert.match(foldTools, /fold-tools/);
239
+ assert.doesNotMatch(foldTools, /#\{pane_id\}/);
240
+ const menu = tmuxBinding(socket, 'root', 'M-S-Z') ?? '';
235
241
  assert.match(menu, /graph view/);
236
242
  assert.match(menu, /Z/);
237
- assert.match(menu, /"Alt\+Shift\+Z to close" M-Z refresh-client/);
243
+ assert.match(menu, /"Alt\+Shift\+Z to close" M-S-Z refresh-client/);
238
244
  const prefix = tmuxBinding(socket, 'root', 'M-z') ?? '';
239
245
  assert.match(prefix, /switch-client/);
240
246
  const again = installTmuxBindings(resolveKeybindings({
@@ -244,11 +250,11 @@ test('tmux installer remaps owned pairs and preserves occupied user bindings', (
244
250
  'crtr.tmux.view.previous': ['alt+z > p'],
245
251
  }));
246
252
  assert.equal(again.ok, true, JSON.stringify(again.diagnostics));
247
- isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-Z', 'display-message', 'user-owned']);
253
+ isolatedTmux(socket, ['bind-key', '-T', 'root', 'M-S-Z', 'display-message', 'user-owned']);
248
254
  const occupied = installTmuxBindings(resolveKeybindings({ 'crtr.tmux.menu.open': ['alt+shift+z'] }));
249
255
  assert.equal(occupied.ok, true);
250
256
  assert.ok(occupied.diagnostics.some((diagnostic) => diagnostic.code === 'occupied' && diagnostic.bindingId === 'crtr.tmux.menu.open'));
251
- assert.match(tmuxBinding(socket, 'root', 'M-Z') ?? '', /user-owned/);
257
+ assert.match(tmuxBinding(socket, 'root', 'M-S-Z') ?? '', /user-owned/);
252
258
  }
253
259
  finally {
254
260
  if (priorTmux === undefined)
@@ -386,15 +386,15 @@ test('close still blocks on real .crouter/memory/ work — a tracked-file edit a
386
386
  run(['config', 'user.email', 'test@example.com'], memoryRepo);
387
387
  run(['config', 'user.name', 'Test'], memoryRepo);
388
388
  mkdirSync(join(memoryRepo, '.crouter', 'memory'), { recursive: true });
389
- writeFileSync(join(memoryRepo, '.crouter', 'memory', 'AGENTS.md'), 'tracked memory doc\n');
389
+ writeFileSync(join(memoryRepo, '.crouter', 'memory', 'tracked.md'), 'tracked memory doc\n');
390
390
  run(['add', '.'], memoryRepo);
391
391
  run(['commit', '-m', 'init with tracked memory'], memoryRepo);
392
392
  const editNodeId = 'wt-real-crouter-memory-edit';
393
393
  const editWt = createManagedWorktree(memoryRepo, editNodeId);
394
394
  createNode(node(editNodeId, { cwd: editWt.path, managed_worktree: editWt }));
395
- writeFileSync(join(editWt.path, '.crouter', 'memory', 'AGENTS.md'), 'edited tracked memory doc\n');
395
+ writeFileSync(join(editWt.path, '.crouter', 'memory', 'tracked.md'), 'edited tracked memory doc\n');
396
396
  const editStatus = gitSync(['status', '--porcelain'], editWt.path).stdout;
397
- assert.ok(editStatus.includes('M .crouter/memory/AGENTS.md'), 'a tracked .crouter/memory/ edit must still show as modified — exclude can never hide a tracked-file change');
397
+ assert.ok(editStatus.includes('M .crouter/memory/tracked.md'), 'a tracked .crouter/memory/ edit must still show as modified — exclude can never hide a tracked-file change');
398
398
  assert.throws(() => closeManagedWorktree(editNodeId), (error) => error instanceof Error && 'code' in error && error.code === 'dirty_worktree');
399
399
  const newDocNodeId = 'wt-real-crouter-memory-new-doc';
400
400
  const newDocWt = createManagedWorktree(memoryRepo, newDocNodeId);
@@ -1,4 +1,4 @@
1
- export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve"];
1
+ export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve"];
2
2
  /** Remove a complete persisted canvas prefix while retaining every genuine
3
3
  * caller/kind extra, including a custom extension whose basename happens to
4
4
  * resemble a canvas hook. */
@@ -1,18 +1,28 @@
1
1
  // Canvas-extension identity is structural, not basename-based: old runtime roots
2
- // are recognized only when they carry the complete shipped canvas prefix.
2
+ // are recognized only when they carry a complete shipped canvas prefix.
3
3
  // canvas-inbox-watcher must load before canvas-stophook: at agent_settled it
4
4
  // commits the cursor the stophook then reads to decide idle release.
5
5
  export const CANVAS_EXTENSION_NAMES = [
6
6
  'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
7
7
  'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
8
- 'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
8
+ 'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
9
+ 'canvas-bash-valve',
9
10
  ];
10
- const LEGACY_CANVAS_EXTENSION_NAMES = [
11
- 'canvas-stophook', 'canvas-inbox-watcher', 'canvas-recap',
12
- 'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
13
- 'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
11
+ const HISTORICAL_CANVAS_EXTENSION_PREFIXES = [
12
+ [
13
+ 'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
14
+ 'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
15
+ 'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
16
+ ],
17
+ [
18
+ 'canvas-stophook', 'canvas-inbox-watcher', 'canvas-recap',
19
+ 'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
20
+ 'canvas-doc-substrate', 'canvas-structured-output', 'canvas-bash-valve',
21
+ ],
14
22
  ];
15
23
  function matchesExtensionPrefix(extensions, names) {
24
+ if (extensions.length < names.length)
25
+ return false;
16
26
  const paths = extensions.slice(0, names.length);
17
27
  const first = paths[0].replaceAll('\\', '/');
18
28
  const marker = '/pi-extensions/';
@@ -24,11 +34,8 @@ function matchesExtensionPrefix(extensions, names) {
24
34
  return names.every((name, index) => paths[index].replaceAll('\\', '/') === `${root}${name}${suffix}`);
25
35
  }
26
36
  function extensionPrefixLength(extensions) {
27
- if (extensions.length < CANVAS_EXTENSION_NAMES.length)
28
- return 0;
29
- return matchesExtensionPrefix(extensions, CANVAS_EXTENSION_NAMES) || matchesExtensionPrefix(extensions, LEGACY_CANVAS_EXTENSION_NAMES)
30
- ? CANVAS_EXTENSION_NAMES.length
31
- : 0;
37
+ const prefixes = [CANVAS_EXTENSION_NAMES, ...HISTORICAL_CANVAS_EXTENSION_PREFIXES];
38
+ return prefixes.find((names) => matchesExtensionPrefix(extensions, names))?.length ?? 0;
32
39
  }
33
40
  /** Remove a complete persisted canvas prefix while retaining every genuine
34
41
  * caller/kind extra, including a custom extension whose basename happens to
@@ -23,9 +23,8 @@ export declare function fullName(node: NamedNode): string;
23
23
  * badge. */
24
24
  export declare function personaLabel(meta: Pick<NodeMeta, 'kind' | 'mode'>): string;
25
25
  /** The pi session display name — the editor label in the top-left. It carries
26
- * ONE name: the pi-generated description of the node's first task once naming
27
- * has run, else the spawn handle. Unlike `fullName` it never prints both, so
28
- * the chip doesn't read as two names side by side. Persona lives in the
29
- * editor's bottom-right badge. Recomputed from meta on every revive and pushed
30
- * live when naming generates the first task description. */
26
+ * ONE name: a person's explicit handle when present, otherwise the generated
27
+ * description of the node's first task. Unlike `fullName` it never prints
28
+ * both, so the chip doesn't read as two names side by side. Persona lives in
29
+ * the editor's bottom-right badge. Recomputed from meta on every revive. */
31
30
  export declare function editorLabel(meta: NodeMeta): string;
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // One source of truth for "what do we call this node on screen". `fullName`
4
4
  // combines the node's explicit handle (meta.name) with the pi-generated
5
- // description of its first task (meta.description) — the same string the editor
6
- // label, the canvas dashboard tree, the nav spine, and the tmux window tab all
7
- // render, so a node reads identically everywhere it appears.
5
+ // description of its first task (meta.description) for canvas and tmux surfaces.
6
+ // `editorLabel` keeps the editor chip compact by choosing the explicit handle
7
+ // when a person supplied one, otherwise the generated description.
8
8
  /** Nerd Font glyph for a node's mode in its persona badge. A base worker reads
9
9
  * as a single figure (nf-fa-user, U+F007); an orchestrator reads as a fan-out
10
10
  * hierarchy (nf-fa-sitemap, U+F0E8). */
@@ -30,14 +30,14 @@ export function personaLabel(meta) {
30
30
  return `${modeIcon(meta.mode)} ${meta.kind}`;
31
31
  }
32
32
  /** The pi session display name — the editor label in the top-left. It carries
33
- * ONE name: the pi-generated description of the node's first task once naming
34
- * has run, else the spawn handle. Unlike `fullName` it never prints both, so
35
- * the chip doesn't read as two names side by side. Persona lives in the
36
- * editor's bottom-right badge. Recomputed from meta on every revive and pushed
37
- * live when naming generates the first task description. */
33
+ * ONE name: a person's explicit handle when present, otherwise the generated
34
+ * description of the node's first task. Unlike `fullName` it never prints
35
+ * both, so the chip doesn't read as two names side by side. Persona lives in
36
+ * the editor's bottom-right badge. Recomputed from meta on every revive. */
38
37
  export function editorLabel(meta) {
39
38
  const icon = (meta.icon ?? '').trim();
39
+ const handle = meta.name && meta.name !== meta.kind ? meta.name.trim() : '';
40
40
  const desc = (meta.description ?? '').trim();
41
- const name = desc !== '' ? desc : fullName(meta);
41
+ const name = handle !== '' ? handle : desc !== '' ? desc : meta.name;
42
42
  return icon !== '' ? `${icon} ${name}` : name;
43
43
  }
@@ -27,7 +27,7 @@ const CONFIGURED_LEAF_KEYS = new Set([...LEAF_KEYS_COMMON, 'rest']);
27
27
  const ROOT_ENTRY_KEYS = new Set(['concept', 'description', 'whenToUse']);
28
28
  const FILE_ENCODINGS = new Set(['text', 'base64']);
29
29
  const PARAM_KEYS_BY_KIND = {
30
- positional: new Set(['kind', 'name', 'type', 'required', 'constraint', 'encoding', 'defaultFromEnv']),
30
+ positional: new Set(['kind', 'name', 'type', 'required', 'constraint', 'repeatable', 'encoding', 'defaultFromEnv']),
31
31
  flag: new Set([
32
32
  'kind',
33
33
  'name',
@@ -137,18 +137,31 @@ function validateParams(raw, path, issue) {
137
137
  issue('command_node_invalid', `positional type must be string|path`, String(p['type']), 'string | path | omitted', 'Fix the positional type.', `${pp}.type`);
138
138
  return null;
139
139
  }
140
+ let repeatable = false;
141
+ if (p['repeatable'] !== undefined) {
142
+ if (typeof p['repeatable'] !== 'boolean') {
143
+ issue('command_node_invalid', `repeatable must be a boolean`, typeName(p['repeatable']), 'true | false | omitted', 'Fix repeatable.', `${pp}.repeatable`);
144
+ return null;
145
+ }
146
+ repeatable = p['repeatable'];
147
+ }
140
148
  const encoding = validateFileEncoding(p, pp, issue);
141
149
  if (encoding === null)
142
150
  return null;
143
151
  const envDefault = validateDefaultFromEnv(p, pp, issue);
144
152
  if (envDefault === null)
145
153
  return null;
154
+ if (repeatable && envDefault !== undefined) {
155
+ issue('command_node_invalid', `repeatable positional must not declare defaultFromEnv`, name, 'no defaultFromEnv on a repeatable positional (values accumulate into an array)', 'Remove defaultFromEnv, or drop repeatable.', `${pp}.defaultFromEnv`);
156
+ return null;
157
+ }
146
158
  out.push({
147
159
  kind: 'positional',
148
160
  name,
149
161
  ...(p['type'] !== undefined ? { type: p['type'] } : {}),
150
162
  required: p['required'],
151
163
  constraint: p['constraint'],
164
+ ...(repeatable ? { repeatable: true } : {}),
152
165
  ...(encoding !== undefined ? { encoding } : {}),
153
166
  ...(envDefault !== undefined ? { defaultFromEnv: envDefault } : {}),
154
167
  });
@@ -251,7 +264,7 @@ function validateFileEncoding(p, pp, issue) {
251
264
  * so it is meaningful only on a string|path param, and it cannot coexist with a
252
265
  * static `default` (two competing sources for one slot is an authoring error,
253
266
  * not a precedence puzzle for the reader). The caller checks `repeatable`
254
- * separately, where that flag is in scope: a repeatable flag accumulates an
267
+ * separately, where that param is in scope: a repeatable param accumulates an
255
268
  * ARRAY, and one env string cannot honestly fill it. Returns the env var name,
256
269
  * undefined when absent, or null when invalid. */
257
270
  function validateDefaultFromEnv(p, pp, issue) {
@@ -716,11 +729,11 @@ function validateRestMapping(raw, basePath, params, issue) {
716
729
  return null;
717
730
  }
718
731
  }
719
- // A repeatable flag's parsed value is an array; only JSON body placement
732
+ // A repeatable param's parsed value is an array; only JSON body placement
720
733
  // can carry it faithfully (query/header/path would String() it into "a,b").
721
734
  const mappedParam = paramsByName.get(paramName);
722
- if (mappedParam.kind === 'flag' && mappedParam.repeatable === true && inValue !== 'body') {
723
- issue('command_rest_invalid', `repeatable flag must use body placement`, `${paramName}: in "${inValue}"`, 'in: "body" (array values ship as a JSON array)', `Map "${paramName}" to body placement.`, `${path}.params.${paramName}.in`);
735
+ if ((mappedParam.kind === 'flag' || mappedParam.kind === 'positional') && mappedParam.repeatable === true && inValue !== 'body') {
736
+ issue('command_rest_invalid', `repeatable param must use body placement`, `${paramName}: in "${inValue}"`, 'in: "body" (array values ship as a JSON array)', `Map "${paramName}" to body placement.`, `${path}.params.${paramName}.in`);
724
737
  return null;
725
738
  }
726
739
  // Collect body params for coalesced source validation
@@ -283,7 +283,8 @@ export async function parseArgv(params, tokens, options) {
283
283
  result[flagNameToKey(f.name)] = f.default;
284
284
  }
285
285
  }
286
- let positionalValue;
286
+ const positionalValues = [];
287
+ const repeatablePositional = positionalParam?.repeatable === true;
287
288
  let positionalWasSupplied = false; // Track if positional came from argv (not stdin)
288
289
  let forcedPositional = false; // true after bare --
289
290
  let i = 0;
@@ -385,7 +386,13 @@ export async function parseArgv(params, tokens, options) {
385
386
  continue;
386
387
  }
387
388
  // Positional (or token after --)
388
- if (positionalValue !== undefined) {
389
+ if (repeatablePositional && positionalParam !== undefined) {
390
+ positionalValues.push(token);
391
+ positionalWasSupplied = true;
392
+ i++;
393
+ continue;
394
+ }
395
+ if (positionalValues.length > 0) {
389
396
  // A leaf that also declares a `stdin` param treats its stdin value as
390
397
  // write-only/sensitive by convention (`profile env set`'s value, etc.).
391
398
  // An unexpected extra positional here is the single most likely misuse
@@ -404,13 +411,15 @@ export async function parseArgv(params, tokens, options) {
404
411
  if (positionalParam === undefined && stdinParam === undefined) {
405
412
  throw parseArgvError('bad_invocation', `this leaf takes no positional arguments: ${token}`, token, undefined, 'Use --flag for parameters. Run -h for the schema.');
406
413
  }
407
- positionalValue = token;
414
+ positionalValues.push(token);
408
415
  positionalWasSupplied = true;
409
416
  i++;
410
417
  }
411
418
  // Assign positional (already tracked in provided set above)
412
- if (positionalValue !== undefined && positionalParam !== undefined) {
413
- result[flagNameToKey(positionalParam.name)] = positionalValue;
419
+ if (positionalValues.length > 0 && positionalParam !== undefined) {
420
+ result[flagNameToKey(positionalParam.name)] = repeatablePositional
421
+ ? positionalValues
422
+ : positionalValues[0];
414
423
  }
415
424
  // Resolve stdin if declared. A positional token (when there's no dedicated
416
425
  // positional param to claim it) satisfies the stdin param directly, so
@@ -421,7 +430,8 @@ export async function parseArgv(params, tokens, options) {
421
430
  // and name both sources.
422
431
  let stdinWasSupplied = false;
423
432
  if (stdinParam !== undefined) {
424
- if (positionalValue !== undefined && positionalParam === undefined) {
433
+ if (positionalValues.length > 0 && positionalParam === undefined) {
434
+ const positionalValue = positionalValues[0];
425
435
  if (positionalValue === '-') {
426
436
  const raw = await readStdinRaw();
427
437
  // Empty required stdin is NOT thrown here: it is left unset so the
@@ -492,7 +502,7 @@ export async function parseArgv(params, tokens, options) {
492
502
  const envValue = process.env[p.defaultFromEnv];
493
503
  if (envValue === undefined || envValue === '')
494
504
  continue;
495
- result[key] = envValue;
505
+ result[key] = p.kind === 'positional' && p.repeatable === true ? [envValue] : envValue;
496
506
  provided.add(key);
497
507
  }
498
508
  // Validate required params — COMPLETE, never first-failure. Drip-feeding one
@@ -190,6 +190,8 @@ function materializeBodyValue(spec, param, value) {
190
190
  if (param.kind === 'context-file')
191
191
  return value;
192
192
  if ((param.kind === 'positional' || param.kind === 'flag') && param.type === 'path') {
193
+ if (Array.isArray(value))
194
+ return value.map((item) => materializeBodyValue(spec, param, item));
193
195
  const filePath = String(value);
194
196
  const resolved = resolvePath(process.cwd(), filePath);
195
197
  let stat;
@@ -6,7 +6,7 @@ export interface Field {
6
6
  * token caps. Lives here, never in a separate Preconditions section. */
7
7
  constraint: string;
8
8
  }
9
- /** Positional argument — at most one per leaf. */
9
+ /** Positional argument — at most one parameter per leaf; optionally repeatable. */
10
10
  export interface PositionalParam {
11
11
  kind: 'positional';
12
12
  name: string;
@@ -14,6 +14,8 @@ export interface PositionalParam {
14
14
  type?: 'string' | 'path';
15
15
  required: boolean;
16
16
  constraint: string;
17
+ /** When true, every positional token is collected into an array in argv order. */
18
+ repeatable?: boolean;
17
19
  /** Local-file materialization contract, valid only on a `path` param. The
18
20
  * CLI-side value is a path to a local file; the invoker reads that file and
19
21
  * sends its CONTENT as the mapped value — 'text' as UTF-8, 'base64' as the
package/dist/core/help.js CHANGED
@@ -175,7 +175,7 @@ export function renderBranch(h) {
175
175
  /** Build the display label for a param entry (left column). */
176
176
  function paramLabel(p) {
177
177
  if (p.kind === 'positional')
178
- return p.name.toUpperCase();
178
+ return `${p.name.toUpperCase()}${p.repeatable === true ? '...' : ''}`;
179
179
  if (p.kind === 'stdin')
180
180
  return 'stdin';
181
181
  if (p.kind === 'context-file')
@@ -207,7 +207,10 @@ function envDefaultNote(envVar) {
207
207
  function paramDesc(p) {
208
208
  const req = p.required ? 'required' : 'optional';
209
209
  if (p.kind === 'positional') {
210
- return `positional, ${req}.${fileEncodingNote(p.encoding)}${envDefaultNote(p.defaultFromEnv)} ${p.constraint}`;
210
+ const repeatable = p.repeatable === true
211
+ ? ' Repeatable — pass multiple values to accumulate in argv order.'
212
+ : '';
213
+ return `positional, ${req}.${fileEncodingNote(p.encoding)}${envDefaultNote(p.defaultFromEnv)} ${p.constraint}${repeatable}`;
211
214
  }
212
215
  if (p.kind === 'stdin')
213
216
  return `${req}. ${p.constraint}`;
@@ -13,9 +13,12 @@ void rejectedBindingId;
13
13
  function ids(prefix, actions) {
14
14
  return actions.map((action) => `${prefix}.${action}`);
15
15
  }
16
+ function pairedMenuIds(actions) {
17
+ return actions.flatMap((action) => [`crtr.tmux.menu.${action}`, `crtr.tmux.menu-global.${action}`]);
18
+ }
16
19
  const REQUIRED_IDS = [
17
- ...ids('crtr.tmux.menu', ['open', 'promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'view-settings', 'fold-tools', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues']),
18
- ...ids('crtr.tmux.menu.issue', ['idea', 'bug']),
20
+ 'crtr.tmux.menu.open',
21
+ ...pairedMenuIds(['promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'view-settings', 'fold-tools', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues', 'issue.idea', 'issue.bug']),
19
22
  ...ids('crtr.tmux.node', ['next', 'previous']),
20
23
  'crtr.tmux.inbox.toggle',
21
24
  ...ids('crtr.tmux.view', ['next', 'previous']),
@@ -1,5 +1,6 @@
1
1
  import type { BindingDefinition, SafetyRequirement } from './types.js';
2
2
  type TmuxMenuAction = 'open' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'resume' | 'fork' | 'demote' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'context' | 'providers' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
3
+ type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open'> | `issue.${'idea' | 'bug'}`;
3
4
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
4
5
  type InboxAction = 'down' | 'up' | 'open' | 'reply' | 'react' | 'filter' | 'refresh' | 'connect-linkedin' | 'connect-gmail' | 'quit' | 'submit-reply' | 'cancel' | 'reaction-previous' | 'reaction-next' | 'submit-reaction';
5
6
  type LinkedinAction = Exclude<InboxAction, 'filter' | 'connect-linkedin' | 'connect-gmail'>;
@@ -8,8 +9,8 @@ type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' |
8
9
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
9
10
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
11
  type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
11
- export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next' | 'model-ladder.previous' | 'command.inspect' | 'file-review'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view.inspector.${'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
12
- export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.fork">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.background-bash">, BindingDefinition<"crtr.tmux.menu.file-review">, BindingDefinition<"crtr.tmux.menu.pin">, BindingDefinition<"crtr.tmux.menu.show">, BindingDefinition<"crtr.tmux.menu.view-settings">, BindingDefinition<"crtr.tmux.menu.fold-tools">, BindingDefinition<"crtr.tmux.menu.metadata">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.copy-id">, BindingDefinition<"crtr.tmux.menu.reload-viewer">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.inbox.toggle">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.attach.model-ladder.previous">, BindingDefinition<"crtr.attach.command.inspect">, BindingDefinition<"crtr.attach.file-review">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.revive">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view.inspector.down"> | BindingDefinition<"crtr.view.inspector.up"> | BindingDefinition<"crtr.view.inspector.section-next"> | BindingDefinition<"crtr.view.inspector.section-prev"> | BindingDefinition<"crtr.view.inspector.page-down"> | BindingDefinition<"crtr.view.inspector.page-up"> | BindingDefinition<"crtr.view.inspector.toggle-detail"> | BindingDefinition<"crtr.view.inspector.cancel-item"> | BindingDefinition<"crtr.view.inspector.open-log"> | BindingDefinition<"crtr.view.inspector.confirm"> | BindingDefinition<"crtr.view.inspector.dismiss"> | BindingDefinition<"crtr.view.inspector.retarget"> | BindingDefinition<"crtr.view.inspector.refresh"> | BindingDefinition<"crtr.view.inspector.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.9" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.6">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
12
+ export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next' | 'model-ladder.previous' | 'command.inspect' | 'file-review'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view.inspector.${'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
13
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.tmux.view.next"> | BindingDefinition<"crtr.tmux.view.previous"> | BindingDefinition<"crtr.attach.detach"> | BindingDefinition<"crtr.attach.clear-or-detach"> | BindingDefinition<"crtr.attach.graph.toggle"> | BindingDefinition<"crtr.attach.model-ladder.next"> | BindingDefinition<"crtr.attach.model-ladder.previous"> | BindingDefinition<"crtr.attach.command.inspect"> | BindingDefinition<"crtr.attach.file-review"> | BindingDefinition<"crtr.mode.cycle"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.view.host.force-quit"> | BindingDefinition<"crtr.view.host.editor.backspace"> | BindingDefinition<"crtr.view.chat.submit"> | BindingDefinition<"crtr.view.chat.contextual-cancel"> | BindingDefinition<"crtr.view.chat.reconnect"> | BindingDefinition<"crtr.view.chat.quit"> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view.inspector.down"> | BindingDefinition<"crtr.view.inspector.up"> | BindingDefinition<"crtr.view.inspector.section-next"> | BindingDefinition<"crtr.view.inspector.section-prev"> | BindingDefinition<"crtr.view.inspector.page-down"> | BindingDefinition<"crtr.view.inspector.page-up"> | BindingDefinition<"crtr.view.inspector.toggle-detail"> | BindingDefinition<"crtr.view.inspector.cancel-item"> | BindingDefinition<"crtr.view.inspector.open-log"> | BindingDefinition<"crtr.view.inspector.confirm"> | BindingDefinition<"crtr.view.inspector.dismiss"> | BindingDefinition<"crtr.view.inspector.retarget"> | BindingDefinition<"crtr.view.inspector.refresh"> | BindingDefinition<"crtr.view.inspector.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.9" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.6">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
13
14
  export declare const BINDING_IDS: ReadonlySet<string>;
14
15
  export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
15
16
  export {};