@north-light/crouter 0.3.158 → 0.3.160

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 (85) hide show
  1. package/dist/api/dto/nodes.d.ts +3 -0
  2. package/dist/builtin-memory/internal/INDEX.md +1 -0
  3. package/dist/builtin-memory/internal/storage-tiers.md +2 -0
  4. package/dist/builtin-memory/internal/workflow-codification.md +82 -0
  5. package/dist/clients/attach/__tests__/bash-call-width.test.d.ts +1 -0
  6. package/dist/clients/attach/__tests__/bash-call-width.test.js +38 -0
  7. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +20 -0
  8. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +3 -1
  9. package/dist/clients/attach/chrome/bash-jobs.d.ts +5 -8
  10. package/dist/clients/attach/chrome/bash-jobs.js +20 -32
  11. package/dist/clients/attach/chrome/canvas-panels.js +10 -3
  12. package/dist/clients/attach/chrome/roster.d.ts +10 -4
  13. package/dist/clients/attach/chrome/roster.js +96 -36
  14. package/dist/clients/attach/input/controller.d.ts +3 -0
  15. package/dist/clients/attach/input/controller.js +1 -0
  16. package/dist/clients/attach/render/assistant-message.d.ts +13 -0
  17. package/dist/clients/attach/render/assistant-message.js +55 -0
  18. package/dist/clients/attach/render/chat-view.d.ts +29 -0
  19. package/dist/clients/attach/render/chat-view.js +233 -12
  20. package/dist/clients/attach/render/context-message.js +1 -1
  21. package/dist/clients/attach/render/edit-diff.js +20 -1
  22. package/dist/clients/attach/render/tool-calls.d.ts +32 -0
  23. package/dist/clients/attach/render/tool-calls.js +251 -0
  24. package/dist/clients/attach/session/input-wiring.d.ts +3 -0
  25. package/dist/clients/attach/session/input-wiring.js +3 -0
  26. package/dist/clients/attach/slash/dispatch.d.ts +4 -0
  27. package/dist/clients/attach/slash/dispatch.js +8 -0
  28. package/dist/clients/attach/viewer.js +568 -566
  29. package/dist/commands/cron.js +2 -2
  30. package/dist/commands/node.js +141 -3
  31. package/dist/commands/search.js +1 -1
  32. package/dist/commands/surface-inspect.js +4 -3
  33. package/dist/commands/sys/config.js +10 -2
  34. package/dist/commands/sys/doctor.js +22 -1
  35. package/dist/commands/sys/setup-core.d.ts +5 -1
  36. package/dist/commands/sys/setup-core.js +16 -2
  37. package/dist/commands/sys/setup-wizard.d.ts +11 -0
  38. package/dist/commands/sys/setup-wizard.js +111 -6
  39. package/dist/core/__tests__/canvas-inbox-watcher.test.js +5 -3
  40. package/dist/core/__tests__/tmux-surface.test.js +5 -2
  41. package/dist/core/bash-jobs.d.ts +11 -0
  42. package/dist/core/bash-jobs.js +41 -1
  43. package/dist/core/canvas/canvas.js +1 -1
  44. package/dist/core/canvas/labels.d.ts +1 -0
  45. package/dist/core/canvas/labels.js +3 -1
  46. package/dist/core/canvas/nav-render.d.ts +6 -0
  47. package/dist/core/canvas/nav-render.js +8 -0
  48. package/dist/core/canvas/types.d.ts +6 -0
  49. package/dist/core/config.js +3 -2
  50. package/dist/core/feed/inbox.d.ts +3 -3
  51. package/dist/core/feed/inbox.js +10 -6
  52. package/dist/core/inspector/core.d.ts +17 -2
  53. package/dist/core/inspector/core.js +172 -23
  54. package/dist/core/inspector/model.d.ts +30 -1
  55. package/dist/core/inspector/model.js +39 -0
  56. package/dist/core/inspector/text.js +14 -1
  57. package/dist/core/inspector/tui.js +71 -3
  58. package/dist/core/keybindings/__tests__/resolve.test.js +2 -2
  59. package/dist/core/keybindings/catalog.d.ts +3 -3
  60. package/dist/core/keybindings/catalog.js +5 -2
  61. package/dist/core/preview-registry.d.ts +5 -0
  62. package/dist/core/preview-registry.js +10 -0
  63. package/dist/core/runtime/canvas-extensions.d.ts +4 -0
  64. package/dist/core/runtime/canvas-extensions.js +4 -0
  65. package/dist/core/runtime/naming-persist.d.ts +4 -3
  66. package/dist/core/runtime/naming-persist.js +10 -9
  67. package/dist/core/runtime/naming.d.ts +28 -8
  68. package/dist/core/runtime/naming.js +168 -32
  69. package/dist/core/runtime/nerd-font.d.ts +28 -0
  70. package/dist/core/runtime/nerd-font.js +127 -0
  71. package/dist/core/runtime/tmux.js +16 -0
  72. package/dist/core/tui/host.js +21 -6
  73. package/dist/daemon/api/map.js +2 -0
  74. package/dist/pi-extensions/canvas-bash-valve.js +5 -0
  75. package/dist/pi-extensions/naming-tool.d.ts +24 -0
  76. package/dist/pi-extensions/naming-tool.js +67 -0
  77. package/dist/types.d.ts +5 -0
  78. package/dist/types.js +1 -0
  79. package/dist/web-client/assets/index-B76ZKfT_.js +79 -0
  80. package/dist/web-client/assets/{index-CpEl9LTS.css → index-CqLKj8Xu.css} +1 -1
  81. package/dist/web-client/index.html +2 -2
  82. package/dist/web-client/sw.js +1 -1
  83. package/package.json +1 -1
  84. package/runtime.lock.json +2 -2
  85. package/dist/web-client/assets/index-CsuwzlcQ.js +0 -79
@@ -13,6 +13,8 @@ export const ICON = {
13
13
  artifacts: cp(0xf1da), // history/archive — what it left behind
14
14
  paths: cp(0xf0f6), // file-text — on-disk locations
15
15
  cron: cp(0xf017), // clock — one armed cron
16
+ job: cp(0xf013), // cog — one live background bash job
17
+ log: cp(0xf120), // terminal — the job's log stream
16
18
  section: cp(0xf192), // dot-circle — the active tab marker
17
19
  raw: cp(0xf121), // code — the raw record
18
20
  empty: cp(0xf1b3), // cubes — nothing armed
@@ -42,6 +44,43 @@ export function scheduledItems(crons) {
42
44
  return a.cron.cron_id.localeCompare(b.cron.cron_id);
43
45
  });
44
46
  }
47
+ /** Elapsed duration in the shape every bash-handoff surface uses: `12s`,
48
+ * `3m 40s`, `1h 04m`. Duplicated from core/bash-jobs.ts on purpose — a view
49
+ * module stays a pure read and never pulls the node:fs job scanner in. */
50
+ export function elapsed(ms) {
51
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
52
+ if (totalSeconds < 60)
53
+ return `${totalSeconds}s`;
54
+ const totalMinutes = Math.floor(totalSeconds / 60);
55
+ if (totalMinutes < 60)
56
+ return `${totalMinutes}m ${String(totalSeconds % 60).padStart(2, '0')}s`;
57
+ return `${Math.floor(totalMinutes / 60)}h ${String(totalMinutes % 60).padStart(2, '0')}m`;
58
+ }
59
+ /** How a running job's age reads at a glance: an ordered ramp, not decoration.
60
+ * Fresh work is green, a job past 15m is amber, past an hour is red — the same
61
+ * good/warn/bad progression the node status glyphs use. */
62
+ export function jobTone(elapsedMs) {
63
+ if (elapsedMs >= 60 * 60_000)
64
+ return 'bad';
65
+ if (elapsedMs >= 15 * 60_000)
66
+ return 'warn';
67
+ return 'good';
68
+ }
69
+ /** Milliseconds a job has been running, live from `started_at`. */
70
+ export function jobElapsedMs(job, now = Date.now()) {
71
+ const at = Date.parse(job.started_at);
72
+ return Number.isFinite(at) ? now - at : job.elapsed_ms;
73
+ }
74
+ /** Live elapsed time for a job row — recomputed from `started_at` so the number
75
+ * keeps ticking between the view's 5s refreshes; `elapsed_ms` is the fallback. */
76
+ export function jobElapsed(job, now = Date.now()) {
77
+ return elapsed(jobElapsedMs(job, now));
78
+ }
79
+ /** First line of a command, whitespace-collapsed and capped for a one-row cell. */
80
+ export function commandCell(command, cap = 60) {
81
+ const oneLine = (command.split('\n')[0] ?? '').replace(/\s+/g, ' ').trim();
82
+ return oneLine.length > cap ? `${oneLine.slice(0, cap - 1)}…` : oneLine;
83
+ }
45
84
  export function relativeTime(iso, now = Date.now()) {
46
85
  if (iso === null)
47
86
  return '—';
@@ -1,4 +1,4 @@
1
- import { overviewGroups, relativeTime, scheduledItems, value } from './model.js';
1
+ import { jobElapsed, overviewGroups, relativeTime, scheduledItems, value } from './model.js';
2
2
  /** The piped path: same information and grouping as the TUI, minus the glyphs
3
3
  * and hue a pipe cannot carry. */
4
4
  export const inspectorText = {
@@ -27,6 +27,19 @@ export const inspectorText = {
27
27
  const cron = item.cron;
28
28
  lines.push('', ` ${cron.name} · ${item.cadence} · ${relativeTime(item.nextAt)}`, ` id ${cron.cron_id}`, ` next ${item.nextAt}`, ` state ${value(cron.state)}`, ` output ${cron.on_output}${cron.sink ? ` → ${cron.sink}` : ''}`, ` expires ${value(cron.expires_at)}`);
29
29
  }
30
+ lines.push('', 'BACKGROUND BASH JOBS');
31
+ if (state.jobs.error) {
32
+ lines.push(` ${state.jobs.error.display.explanation}`, ` ${state.jobs.error.display.nextStep}`);
33
+ return lines.join('\n');
34
+ }
35
+ const jobs = state.jobs.data ?? [];
36
+ if (jobs.length === 0)
37
+ lines.push(' nothing running');
38
+ for (const job of jobs) {
39
+ lines.push('', ` ${job.command.split('\n')[0]} · ${jobElapsed(job)}`, ` id ${job.job_id}`, ` started ${job.started_at}`, ` log ${job.log_path}`);
40
+ for (const line of job.log_tail.slice(-5))
41
+ lines.push(` | ${line}`);
42
+ }
30
43
  return lines.join('\n');
31
44
  },
32
45
  };
@@ -1,4 +1,4 @@
1
- import { ICON, overviewGroups, relativeTime, scheduledItems, shortPath, value, } from './model.js';
1
+ import { ICON, commandCell, jobElapsed, jobElapsedMs, jobTone, overviewGroups, relativeTime, scheduledItems, shortPath, value, } from './model.js';
2
2
  // ── Visual grammar ───────────────────────────────────────────────────────────
3
3
  //
4
4
  // Three primitives carry the whole screen: a TAB STRIP (where am I / where can
@@ -21,6 +21,7 @@ const HEAD_STYLE = { fg: '36', bold: true };
21
21
  const SECTIONS = [
22
22
  { id: 'overview', label: 'overview' },
23
23
  { id: 'schedule', label: 'schedule' },
24
+ { id: 'jobs', label: 'jobs' },
24
25
  { id: 'raw', label: 'raw' },
25
26
  ];
26
27
  /** `label` padded into the dim label column, then the value. */
@@ -50,7 +51,9 @@ function tabStrip(state, width) {
50
51
  const active = section.id === state.section;
51
52
  const badge = section.id === 'schedule'
52
53
  ? ` ${scheduledItems(state.schedule.data ?? []).length}`
53
- : '';
54
+ : section.id === 'jobs'
55
+ ? ` ${(state.jobs.data ?? []).length}`
56
+ : '';
54
57
  spans.push({
55
58
  text: active ? `${ICON.section} ${section.label}${badge}` : `${section.label}${badge}`,
56
59
  style: active ? { fg: '36', bold: true } : { dim: true },
@@ -147,6 +150,52 @@ function cronItem(item, expanded) {
147
150
  lines,
148
151
  };
149
152
  }
153
+ /** One live background job as a list item. Collapsed it shows the single thing
154
+ * a watcher wants — the newest log line — under the command and its running
155
+ * time; expanding adds the job's identity and a live tail of its log, so the
156
+ * Inspector answers "what is it doing right now" without a second surface. */
157
+ function jobItem(job, expanded, width) {
158
+ const tone = TONE[jobTone(jobElapsedMs(job))];
159
+ const last = job.log_tail[job.log_tail.length - 1];
160
+ const lines = [];
161
+ if (!expanded) {
162
+ lines.push([{ text: `${INDENT}${INDENT}${last === undefined ? 'no output yet' : logCell(last, width - 6)}`, style: { dim: true } }]);
163
+ }
164
+ else {
165
+ for (const [label, text] of [['job', job.job_id], ['started', job.started_at], ['log', shortPath(job.log_path)], ['watch', `tail -f ${shortPath(job.log_path)}`]]) {
166
+ lines.push([{ text: `${INDENT}${INDENT}${label.padEnd(LABEL_COL)}`, style: LABEL_STYLE }, { text, style: label === 'watch' ? { fg: '36' } : undefined }]);
167
+ }
168
+ lines.push([]);
169
+ lines.push(groupHeader(ICON.log, `last ${job.log_tail.length} lines`, width - 2));
170
+ if (job.log_tail.length === 0)
171
+ lines.push([{ text: `${INDENT}${INDENT}no output yet`, style: { dim: true } }]);
172
+ for (const line of job.log_tail)
173
+ lines.push([{ text: `${INDENT}${INDENT}${logCell(line, width - 6)}`, style: { dim: true } }]);
174
+ }
175
+ lines.push([]); // one blank row between jobs — each item reads as its own block
176
+ return {
177
+ spans: [
178
+ { text: ` ${ICON.job} `, style: tone },
179
+ { text: commandCell(job.command, Math.max(20, width - 26)), style: { bold: true } },
180
+ ],
181
+ right: [{ text: `${jobElapsed(job)} `, style: tone }],
182
+ lines,
183
+ };
184
+ }
185
+ /** A log line, tabs expanded and clipped to the card — never wrapped, so one
186
+ * runaway line cannot push the rest of the tail off the screen. */
187
+ function logCell(line, width) {
188
+ const flat = line.replace(/\t/g, ' ').replace(/[\u0000-\u0008\u000b-\u001f]/g, '');
189
+ const cap = Math.max(10, width);
190
+ return flat.length > cap ? `${flat.slice(0, cap - 1)}…` : flat;
191
+ }
192
+ function jobsEmptyRows() {
193
+ return [
194
+ [{ text: ` ${ICON.empty} nothing running`, style: { dim: true } }],
195
+ [],
196
+ [{ text: `${INDENT}This node has no bash commands running in the background.`, style: { dim: true } }],
197
+ ];
198
+ }
150
199
  function scheduleEmptyRows() {
151
200
  return [
152
201
  [{ text: ` ${ICON.empty} nothing armed`, style: { dim: true } }],
@@ -185,6 +234,23 @@ export const inspectorTui = {
185
234
  paint(state, rawRows(state), draw, content);
186
235
  return;
187
236
  }
237
+ if (state.section === 'jobs') {
238
+ if (state.jobs.error) {
239
+ const d = state.jobs.error.display;
240
+ paint(state, errorRows(d.headline, d.explanation, d.nextStep), draw, content);
241
+ return;
242
+ }
243
+ const jobs = state.jobs.data ?? [];
244
+ if (jobs.length === 0) {
245
+ paint(state, jobsEmptyRows(), draw, content);
246
+ return;
247
+ }
248
+ draw.spans(content.row, content.col, tabStrip(state, content.width), content.width);
249
+ const cursor = Math.max(0, jobs.findIndex((job) => job.job_id === state.selectedJobId));
250
+ const rows = jobs.map((job, index) => jobItem(job, index === cursor && state.expanded, content.width));
251
+ draw.list({ ...content, row: content.row + 2, height: Math.max(0, content.height - 2) }, rows, cursor, state.scroll.jobs);
252
+ return;
253
+ }
188
254
  if (state.schedule.error) {
189
255
  const d = state.schedule.error.display;
190
256
  paint(state, errorRows(d.headline, d.explanation, d.nextStep), draw, content);
@@ -209,8 +275,10 @@ export const inspectorTui = {
209
275
  { bindingId: 'crtr.view.inspector.section-prev', intent: 'sectionPrev' },
210
276
  { bindingId: 'crtr.view.inspector.page-down', intent: 'pageDown' },
211
277
  { bindingId: 'crtr.view.inspector.page-up', intent: 'pageUp' },
212
- { bindingId: 'crtr.view.inspector.toggle-detail', intent: 'toggleDetail', when: (state) => state.section === 'schedule' && state.selectedCronId !== null, hint: { label: 'details' } },
278
+ { bindingId: 'crtr.view.inspector.toggle-detail', intent: 'toggleDetail', when: (state) => (state.section === 'schedule' && state.selectedCronId !== null) || (state.section === 'jobs' && state.selectedJobId !== null), hint: { label: 'details' } },
213
279
  { bindingId: 'crtr.view.inspector.cancel-item', intent: 'cancelItem', when: (state) => state.section === 'schedule' && state.selectedCronId !== null, hint: { label: 'cancel' } },
280
+ { bindingId: 'crtr.view.inspector.cancel-item', intent: 'cancelItem', when: (state) => state.section === 'jobs' && state.selectedJobId !== null, hint: { label: 'stop job' } },
281
+ { bindingId: 'crtr.view.inspector.open-log', intent: 'openLog', when: (state) => state.section === 'jobs' && state.selectedJobId !== null, hint: { label: 'open log' } },
214
282
  { bindingId: 'crtr.view.inspector.confirm', intent: 'confirm', when: (state) => state.pendingCancel !== null, hint: { label: 'confirm' } },
215
283
  { bindingId: 'crtr.view.inspector.dismiss', intent: 'dismiss', hint: { label: 'dismiss' } },
216
284
  { bindingId: 'crtr.view.inspector.retarget', intent: 'retarget', when: (state) => state.section === 'schedule' && state.selectedCronId !== null, hint: { label: 'retarget' } },
@@ -14,7 +14,7 @@ function ids(prefix, actions) {
14
14
  return actions.map((action) => `${prefix}.${action}`);
15
15
  }
16
16
  const REQUIRED_IDS = [
17
- ...ids('crtr.tmux.menu', ['open', 'promote', 'resume', 'fork', 'demote', 'detach', 'close-subtree', 'background-bash', 'file-review', 'pin', 'show', 'metadata', 'context', 'providers', 'graph', 'focus-manager', 'copy-id', 'reload-viewer', 'issues']),
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
18
  ...ids('crtr.tmux.menu.issue', ['idea', 'bug']),
19
19
  ...ids('crtr.tmux.node', ['next', 'previous']),
20
20
  'crtr.tmux.inbox.toggle',
@@ -31,7 +31,7 @@ const REQUIRED_IDS = [
31
31
  ...ids('crtr.view.linkedin', ['down', 'up', 'open', 'reply', 'react', 'refresh', 'quit', 'submit-reply', 'cancel', 'reaction-previous', 'reaction-next', 'submit-reaction']),
32
32
  ...ids('crtr.view.prompt-review', ['down', 'up', 'compare-previous', 'compare-next', 'toggle-mode', 'expand', 'export', 'refresh', 'quit']),
33
33
  ...ids('crtr.view.workspace-sidebar', ['down', 'up', 'open', 'refresh', 'quit']),
34
- ...ids('crtr.view.inspector', ['down', 'up', 'section-next', 'section-prev', 'page-down', 'page-up', 'toggle-detail', 'cancel-item', 'confirm', 'dismiss', 'retarget', 'refresh', 'quit']),
34
+ ...ids('crtr.view.inspector', ['down', 'up', 'section-next', 'section-prev', 'page-down', 'page-up', 'toggle-detail', 'cancel-item', 'open-log', 'confirm', 'dismiss', 'retarget', 'refresh', 'quit']),
35
35
  ...ids('crtr.view-picker', ['down', 'up', 'run', 'quit']),
36
36
  ...ids('crtr.browse', ['quit', 'down', 'up', 'first', 'last', 'expand-or-child', 'collapse-or-parent', 'toggle-fold', 'tab-next', 'tab-previous', 'tab.1', 'tab.2', 'tab.3', 'tab.4', 'tab.5', 'close-subtree', 'confirm-close', 'sort', 'scope', 'profile', 'residents', 'preview', 'search', 'revive-now', 'focus', 'focus-background']),
37
37
  ...ids('crtr.browse.search', ['cancel', 'commit', 'delete', 'quit']),
@@ -1,5 +1,5 @@
1
1
  import type { BindingDefinition, SafetyRequirement } from './types.js';
2
- type TmuxMenuAction = 'open' | 'show' | 'metadata' | 'promote' | 'resume' | 'fork' | 'demote' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'context' | 'providers' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
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
3
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
4
4
  type InboxAction = 'down' | 'up' | 'open' | 'reply' | 'react' | 'filter' | 'refresh' | 'connect-linkedin' | 'connect-gmail' | 'quit' | 'submit-reply' | 'cancel' | 'reaction-previous' | 'reaction-next' | 'submit-reaction';
5
5
  type LinkedinAction = Exclude<InboxAction, 'filter' | 'connect-linkedin' | 'connect-gmail'>;
@@ -8,8 +8,8 @@ type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' |
8
8
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
9
9
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
10
  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' | '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.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.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">];
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">];
13
13
  export declare const BINDING_IDS: ReadonlySet<string>;
14
14
  export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
15
15
  export {};
@@ -37,6 +37,8 @@ export const BINDING_CATALOG = Object.freeze([
37
37
  b('crtr.tmux.menu.file-review', 'Global tmux', 'Review a file', 'Open a line-anchored review of a file named in the transcript.', ['v'], 'tmux-menu-selector', ['tmux.menu']),
38
38
  b('crtr.tmux.menu.pin', 'Global tmux', 'Pin conversation', 'Pin or unpin this conversation in the canvas navigator.', ['shift+f'], 'tmux-menu-selector', ['tmux.menu']),
39
39
  b('crtr.tmux.menu.show', 'Global tmux', 'Show node', 'Open the focused node’s inspection menu.', ['s'], 'tmux-menu-selector', ['tmux.menu']),
40
+ b('crtr.tmux.menu.view-settings', 'Global tmux', 'View settings', 'Open the viewer display-settings menu.', ['w'], 'tmux-menu-selector', ['tmux.menu']),
41
+ b('crtr.tmux.menu.fold-tools', 'Global tmux', 'Fold finished tool calls', 'Fold every finished tool call in the transcript to its single call line.', ['f'], 'tmux-menu-selector', ['tmux.menu.view-settings']),
40
42
  b('crtr.tmux.menu.metadata', 'Global tmux', 'Show metadata', 'Show the focused node’s metadata dossier.', ['m'], 'tmux-menu-selector', ['tmux.menu.show']),
41
43
  b('crtr.tmux.menu.context', 'Global tmux', 'Browse context', 'Browse the focused node context.', ['c'], 'tmux-menu-selector', ['tmux.menu.show']),
42
44
  b('crtr.tmux.menu.providers', 'Global tmux', 'Open settings', 'Open the crouter settings panel.', ['p'], 'tmux-menu-selector', ['tmux.menu']),
@@ -118,8 +120,9 @@ export const BINDING_CATALOG = Object.freeze([
118
120
  b('crtr.view.inspector.section-prev', 'Views', 'Previous Inspector section', 'Move to the previous Inspector section.', ['shift+tab'], 'terminal', ['view.inspector']),
119
121
  b('crtr.view.inspector.page-down', 'Views', 'Page down Inspector', 'Scroll the Inspector detail body down.', ['ctrl+d', 'pagedown'], 'terminal', ['view.inspector']),
120
122
  b('crtr.view.inspector.page-up', 'Views', 'Page up Inspector', 'Scroll the Inspector detail body up.', ['ctrl+u', 'pageup'], 'terminal', ['view.inspector']),
121
- b('crtr.view.inspector.toggle-detail', 'Views', 'Toggle cron detail', 'Expand or collapse the selected cron.', ['enter'], 'terminal', ['view.inspector']),
122
- b('crtr.view.inspector.cancel-item', 'Views', 'Cancel cron', 'Arm cancellation for the selected cron.', ['x'], 'terminal', ['view.inspector']),
123
+ b('crtr.view.inspector.toggle-detail', 'Views', 'Toggle item detail', 'Expand or collapse the selected cron or background job.', ['enter'], 'terminal', ['view.inspector']),
124
+ b('crtr.view.inspector.cancel-item', 'Views', 'Cancel selected item', 'Arm cancellation for the selected cron, or stopping for the selected background job.', ['x'], 'terminal', ['view.inspector']),
125
+ b('crtr.view.inspector.open-log', 'Views', 'Open job log', 'Follow the selected background job’s log in a new tmux window.', ['o'], 'terminal', ['view.inspector']),
123
126
  b('crtr.view.inspector.confirm', 'Views', 'Confirm Inspector action', 'Confirm the armed Inspector action.', ['y'], 'terminal', ['view.inspector']),
124
127
  b('crtr.view.inspector.dismiss', 'Views', 'Dismiss Inspector action', 'Dismiss an Inspector confirmation or banner.', ['escape'], 'terminal', ['view.inspector'], 'dismiss-modal'),
125
128
  b('crtr.view.inspector.retarget', 'Views', 'Retarget Inspector', 'Inspect the selected cron anchor or creator node.', ['t'], 'terminal', ['view.inspector']),
@@ -9,6 +9,11 @@ export declare function suppressOutputForLeaf(helpName: string): boolean;
9
9
  * abc`) don't prevent the match. */
10
10
  export declare function suppressOutputForPath(tokens: readonly string[]): boolean;
11
11
  export declare const HELP_ICON: string;
12
+ /** Glyphs for the plain agent tools the attach viewer re-skins (render/tool-calls.ts).
13
+ * Same registry as the crtr command icons so every Nerd Font codepoint the viewer
14
+ * paints lives in one file: terminal for `bash`, file-text for `read`, floppy for
15
+ * `write` (content committed to disk), pencil-square for `edit`. */
16
+ export declare const TOOL_ICON: Readonly<Record<'bash' | 'read' | 'write' | 'edit', string>>;
12
17
  /** Resolve the Nerd Font glyph for a command's argv tokens: the longest
13
18
  * matching LEAF_ICON_CP prefix, else the tokens[0] family glyph, else the
14
19
  * generic ◆ fallback for an unmapped family. */
@@ -53,6 +53,16 @@ export function suppressOutputForPath(tokens) {
53
53
  * regardless of which command's help was requested. */
54
54
  const HELP_ICON_CP = 0xf059;
55
55
  export const HELP_ICON = String.fromCodePoint(HELP_ICON_CP);
56
+ /** Glyphs for the plain agent tools the attach viewer re-skins (render/tool-calls.ts).
57
+ * Same registry as the crtr command icons so every Nerd Font codepoint the viewer
58
+ * paints lives in one file: terminal for `bash`, file-text for `read`, floppy for
59
+ * `write` (content committed to disk), pencil-square for `edit`. */
60
+ export const TOOL_ICON = {
61
+ bash: String.fromCodePoint(0xf120),
62
+ read: String.fromCodePoint(0xf15c),
63
+ write: String.fromCodePoint(0xf0c7),
64
+ edit: String.fromCodePoint(0xf044),
65
+ };
56
66
  /** One glyph per root subtree (`crtr <family> ...`) — the fallback for any
57
67
  * leaf not listed in LEAF_ICON_CP below. Codepoints are Nerd Font
58
68
  * private-use-area icons chosen per family concept (see command-icons.md). */
@@ -7,6 +7,10 @@ export declare const CANVAS_CONTEXT_INTRO_PATH: string;
7
7
  export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
8
8
  export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
9
9
  export declare const CANVAS_BASH_VALVE_PATH: string;
10
+ /** NOT a canvas extension — loaded only by the headless namer subprocess
11
+ * (`runtime/naming.ts`) to give it a constrained-schema `name_session` tool.
12
+ * Deliberately absent from CANVAS_EXTENSIONS: a node must never carry it. */
13
+ export declare const NAMING_TOOL_PATH: string;
10
14
  /** The canvas extensions every node loads, in order. The inbox watcher commits
11
15
  * its cursor at agent_settled before the stophook checks it for idle release. */
12
16
  export declare const CANVAS_EXTENSIONS: string[];
@@ -19,6 +19,10 @@ export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro'
19
19
  export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate');
20
20
  export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured-output');
21
21
  export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
22
+ /** NOT a canvas extension — loaded only by the headless namer subprocess
23
+ * (`runtime/naming.ts`) to give it a constrained-schema `name_session` tool.
24
+ * Deliberately absent from CANVAS_EXTENSIONS: a node must never carry it. */
25
+ export const NAMING_TOOL_PATH = resolveExtension('naming-tool');
22
26
  /** The canvas extensions every node loads, in order. The inbox watcher commits
23
27
  * its cursor at agent_settled before the stophook checks it for idle release. */
24
28
  export const CANVAS_EXTENSIONS = [
@@ -1,8 +1,9 @@
1
1
  import type { NodeMeta } from '../canvas/index.js';
2
2
  /** Asynchronously generate a name for `prompt` and persist it to the node's
3
- * meta as `description` — only if the node has none yet (so a later message
4
- * never clobbers it). Non-blocking: safe to call from inside a live pi event
5
- * loop. Best-effort; swallows all errors.
3
+ * meta as `description` + `icon` — only if the node has none yet (so a later
4
+ * message never clobbers it). Non-blocking: safe to call from inside a live pi
5
+ * event loop. Best-effort; swallows all errors. The slug fallback carries no
6
+ * icon: a locally derived name has no judgment behind it to pick a glyph from.
6
7
  *
7
8
  * `onNamed` (optional) fires with the freshly-persisted meta the moment the
8
9
  * name lands — the canvas-goal-capture naming hook passes a callback that calls
@@ -8,9 +8,10 @@
8
8
  import { headlessName, sanitizeSessionName, slugFromPrompt } from './naming.js';
9
9
  import { getNode, updateNode } from '../canvas/index.js';
10
10
  /** Asynchronously generate a name for `prompt` and persist it to the node's
11
- * meta as `description` — only if the node has none yet (so a later message
12
- * never clobbers it). Non-blocking: safe to call from inside a live pi event
13
- * loop. Best-effort; swallows all errors.
11
+ * meta as `description` + `icon` — only if the node has none yet (so a later
12
+ * message never clobbers it). Non-blocking: safe to call from inside a live pi
13
+ * event loop. Best-effort; swallows all errors. The slug fallback carries no
14
+ * icon: a locally derived name has no judgment behind it to pick a glyph from.
14
15
  *
15
16
  * `onNamed` (optional) fires with the freshly-persisted meta the moment the
16
17
  * name lands — the canvas-goal-capture naming hook passes a callback that calls
@@ -20,22 +21,22 @@ export function generateAndPersistName(nodeId, prompt, onNamed) {
20
21
  const body = (prompt ?? '').trim();
21
22
  if (body === '')
22
23
  return;
23
- const persist = (name) => {
24
+ const persist = (generated) => {
24
25
  try {
25
26
  const meta = getNode(nodeId);
26
27
  if (meta === null)
27
28
  return;
28
29
  if ((meta.description ?? '').trim() !== '')
29
30
  return; // already named
30
- const clean = sanitizeSessionName(name);
31
- const updated = updateNode(nodeId, { description: clean !== '' ? clean : slugFromPrompt(body) });
31
+ const clean = sanitizeSessionName(generated.name);
32
+ const description = clean !== '' ? clean : slugFromPrompt(body);
33
+ const icon = generated.icon.trim();
34
+ const updated = updateNode(nodeId, { description, ...(icon !== '' ? { icon } : {}) });
32
35
  onNamed?.(updated);
33
36
  }
34
37
  catch {
35
38
  // best-effort
36
39
  }
37
40
  };
38
- void headlessName(body).then((name) => {
39
- persist(name !== '' ? name : slugFromPrompt(body));
40
- });
41
+ void headlessName(body).then(persist);
41
42
  }
@@ -5,18 +5,38 @@ export declare function sanitizeSessionName(raw: string): string;
5
5
  /** Local fallback: derive a name straight from the prompt (no pi call). Drops
6
6
  * stop-words, takes the first few content words. */
7
7
  export declare function slugFromPrompt(prompt: string): string;
8
- /** Ask pi headlessly for a kebab-case name for `body`, async. Resolves to the
9
- * sanitized name, or '' on any failure (non-zero exit, timeout, empty/garbled
10
- * output) so the caller can fall back to a local slug. Owns the subprocess
11
- * mechanics — crucially it hands pi an immediate stdin EOF: `pi -p` reads
12
- * stdin, and execFile's default stdin is an OPEN pipe that never closes, so
13
- * without this pi blocks waiting for EOF and the call exits non-zero (the
14
- * regression that silently lost every LLM name to the slug fallback).
8
+ /** A generated session name: the kebab-case handle plus the Nerd Font glyph the
9
+ * namer chose for it. `icon` is '' when the model returned nothing usable —
10
+ * every surface renders the name alone in that case. */
11
+ export interface SessionName {
12
+ name: string;
13
+ icon: string;
14
+ }
15
+ /** Normalize a model-supplied icon to a single renderable glyph, or '' when
16
+ * nothing usable came back.
17
+ *
18
+ * The model is asked for a `U+XXXX` codepoint rather than the literal
19
+ * character: Nerd Font glyphs sit in the private-use areas, which models
20
+ * reproduce unreliably as literal text (they tend to substitute an emoji), but
21
+ * name accurately as codepoints. A literal glyph is still accepted — including
22
+ * an emoji, which renders fine even if it is not the house style — so a model
23
+ * that ignores the format still contributes. Rejects a spelled-out
24
+ * `nf-fa-bug`, a bare ASCII letter, and anything multi-glyph, all of which
25
+ * would render as noise or tofu in a label. */
26
+ export declare function sanitizeIcon(raw: string): string;
27
+ /** Ask pi headlessly for a structured {name, icon} for `body`, async. Resolves
28
+ * to a sanitized SessionName, or `{name:'',icon:''}` on any failure (non-zero
29
+ * exit, timeout, no tool call, garbled emission) so the caller can fall back to
30
+ * a local slug. Owns the subprocess mechanics — crucially it hands pi an
31
+ * immediate stdin EOF: `pi -p` reads stdin, and execFile's default stdin is an
32
+ * OPEN pipe that never closes, so without this pi blocks waiting for EOF and
33
+ * the call exits non-zero (the regression that silently lost every LLM name to
34
+ * the slug fallback).
15
35
  *
16
36
  * Exported (not just for tests): the canvas-coupled persistence wrapper
17
37
  * generateAndPersistName lives in naming-persist.ts to keep this module
18
38
  * canvas-free, and imports this pure helper. */
19
- export declare function headlessName(body: string): Promise<string>;
39
+ export declare function headlessName(body: string): Promise<SessionName>;
20
40
  /** A filesystem-safe slug for the `.todos/<slug>.md` fallback path: lowercase,
21
41
  * [a-z0-9] kept, everything else → single hyphen, clamped to 8 words. */
22
42
  export declare function titleSlug(title: string): string;