@north-light/crouter 0.3.159 → 0.3.161

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 (74) 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/workflow-codification.md +82 -0
  4. package/dist/clients/attach/__tests__/bash-call-width.test.d.ts +1 -0
  5. package/dist/clients/attach/__tests__/bash-call-width.test.js +38 -0
  6. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +20 -0
  7. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +3 -1
  8. package/dist/clients/attach/__tests__/markdown-headings.test.js +8 -0
  9. package/dist/clients/attach/chrome/bash-jobs.d.ts +5 -3
  10. package/dist/clients/attach/chrome/bash-jobs.js +15 -8
  11. package/dist/clients/attach/chrome/canvas-panels.js +10 -3
  12. package/dist/clients/attach/chrome/roster.d.ts +7 -2
  13. package/dist/clients/attach/chrome/roster.js +87 -3
  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/glyph-codepoints.d.ts +3 -0
  23. package/dist/clients/attach/render/glyph-codepoints.js +42 -0
  24. package/dist/clients/attach/render/markdown-headings.js +15 -4
  25. package/dist/clients/attach/render/tool-calls.d.ts +32 -0
  26. package/dist/clients/attach/render/tool-calls.js +251 -0
  27. package/dist/clients/attach/session/input-wiring.d.ts +3 -0
  28. package/dist/clients/attach/session/input-wiring.js +3 -0
  29. package/dist/clients/attach/slash/dispatch.d.ts +4 -0
  30. package/dist/clients/attach/slash/dispatch.js +8 -0
  31. package/dist/clients/attach/viewer.js +699 -695
  32. package/dist/commands/cron.js +2 -2
  33. package/dist/commands/node.js +3 -3
  34. package/dist/commands/search.js +1 -1
  35. package/dist/commands/sys/config.js +10 -2
  36. package/dist/commands/sys/doctor.js +22 -1
  37. package/dist/commands/sys/setup-core.d.ts +5 -1
  38. package/dist/commands/sys/setup-core.js +16 -2
  39. package/dist/commands/sys/setup-wizard.d.ts +11 -0
  40. package/dist/commands/sys/setup-wizard.js +111 -6
  41. package/dist/core/__tests__/tmux-surface.test.js +5 -2
  42. package/dist/core/canvas/canvas.js +1 -1
  43. package/dist/core/canvas/labels.d.ts +6 -3
  44. package/dist/core/canvas/labels.js +9 -4
  45. package/dist/core/canvas/nav-render.d.ts +6 -0
  46. package/dist/core/canvas/nav-render.js +8 -0
  47. package/dist/core/canvas/types.d.ts +6 -0
  48. package/dist/core/config.js +3 -2
  49. package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
  50. package/dist/core/keybindings/catalog.d.ts +2 -2
  51. package/dist/core/keybindings/catalog.js +2 -0
  52. package/dist/core/preview-registry.d.ts +5 -0
  53. package/dist/core/preview-registry.js +10 -0
  54. package/dist/core/runtime/canvas-extensions.d.ts +4 -0
  55. package/dist/core/runtime/canvas-extensions.js +4 -0
  56. package/dist/core/runtime/naming-persist.d.ts +4 -3
  57. package/dist/core/runtime/naming-persist.js +10 -9
  58. package/dist/core/runtime/naming.d.ts +28 -8
  59. package/dist/core/runtime/naming.js +175 -32
  60. package/dist/core/runtime/nerd-font.d.ts +28 -0
  61. package/dist/core/runtime/nerd-font.js +127 -0
  62. package/dist/core/runtime/tmux.js +16 -0
  63. package/dist/daemon/api/map.js +2 -0
  64. package/dist/pi-extensions/naming-tool.d.ts +24 -0
  65. package/dist/pi-extensions/naming-tool.js +67 -0
  66. package/dist/types.d.ts +5 -0
  67. package/dist/types.js +1 -0
  68. package/dist/web-client/assets/index-B76ZKfT_.js +79 -0
  69. package/dist/web-client/assets/{index-CpEl9LTS.css → index-CqLKj8Xu.css} +1 -1
  70. package/dist/web-client/index.html +2 -2
  71. package/dist/web-client/sw.js +1 -1
  72. package/package.json +8 -8
  73. package/runtime.lock.json +161 -161
  74. package/dist/web-client/assets/index-CsuwzlcQ.js +0 -79
@@ -0,0 +1,251 @@
1
+ // render/tool-calls.ts — attach-viewer call headers for the plain agent tools.
2
+ //
3
+ // pi's builtin renderers name a tool with a bold word (`read <path>`,
4
+ // `$ <the entire command line>`). In the crtr viewer a tool call is a card the
5
+ // human scans, not a shell echo (inline-ui-placement rule 12): each tool gets
6
+ // its own Nerd Font glyph, and `bash` gets a compact, syntax-colored ONE-LINE
7
+ // summary instead of dumping a multi-line command into the transcript. The
8
+ // full command is still one ctrl+o away (rule 10: terse by default, reveal on
9
+ // demand).
10
+ //
11
+ // Only the CALL is re-skinned here; every result renderer stays pi's builtin,
12
+ // so output, truncation, timing and error chrome are untouched.
13
+ import { createBashToolDefinition, createReadToolDefinition, createWriteToolDefinition } from '@earendil-works/pi-coding-agent';
14
+ import { Text, getCapabilities, hyperlink, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
15
+ import { homedir } from 'node:os';
16
+ import { isAbsolute, resolve } from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
18
+ import { TOOL_ICON } from '../../../core/preview-registry.js';
19
+ function str(value) {
20
+ return typeof value === 'string' ? value : '';
21
+ }
22
+ /** Key in a tool's shared render-state bag (`context.state`) carrying the
23
+ * viewer's fold. `MinimizableToolComponent` sets it; the fold-aware renderCalls
24
+ * below read it and emit ONLY their one-line header while it is true, which is
25
+ * what makes a folded card exactly `<icon> <tool> <path>` with no body — the
26
+ * renderer owns its folded form, no post-hoc slicing of rendered rows. */
27
+ export const FOLDED_STATE_KEY = 'crtrFolded';
28
+ export function isFoldedContext(context) {
29
+ return context.state?.[FOLDED_STATE_KEY] === true;
30
+ }
31
+ /** The one-line folded form of a file tool's call: `<icon> <label> <path>`. */
32
+ export function foldedFileCallLine(tool, rawPath, theme, cwd) {
33
+ return `${header(TOOL_ICON[tool], tool, theme)} ${pathDisplay(rawPath, theme, cwd)}`;
34
+ }
35
+ /** `~`-shorten an absolute path the way pi's renderToolPath does (its
36
+ * render-utils module is not exported from the package root). */
37
+ function shortenPath(path) {
38
+ const home = homedir();
39
+ return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
40
+ }
41
+ /** Accent-colored, OSC-8-linked path — the same affordance pi gives file tools. */
42
+ function pathDisplay(rawPath, theme, cwd) {
43
+ if (!rawPath)
44
+ return theme.fg('toolOutput', '...');
45
+ const styled = theme.fg('accent', shortenPath(rawPath));
46
+ if (!getCapabilities().hyperlinks)
47
+ return styled;
48
+ const absolute = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
49
+ return hyperlink(styled, pathToFileURL(absolute).href);
50
+ }
51
+ /** `<icon> <label>` — the glyph in accent, the tool name in pi's tool title style. */
52
+ function header(icon, label, theme) {
53
+ return `${theme.fg('accent', icon)} ${theme.fg('toolTitle', theme.bold(label))}`;
54
+ }
55
+ /** Prefix a pi-built `Text` in place (its `text` field is TS-private but a plain
56
+ * runtime property). Used where the header line is baked into the same string as
57
+ * a body we want to keep verbatim — write's syntax-highlighted content preview,
58
+ * edit's header inside its own card. Silently no-ops if pi's Text ever stops
59
+ * looking like this, so the worst case is a missing glyph, never a crash. */
60
+ export function prefixToolText(component, prefix) {
61
+ const text = component;
62
+ if (typeof text.text !== 'string' || typeof text.setText !== 'function')
63
+ return;
64
+ if (text.text.startsWith(prefix))
65
+ return;
66
+ text.setText(`${prefix}${text.text}`);
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // read
70
+ // ---------------------------------------------------------------------------
71
+ /** Attach-viewer `read`: an icon + the path (plus any `:offset-limit` range).
72
+ * The call is a single line by construction, so a folded read shows exactly
73
+ * what it read and nothing else. */
74
+ export function createCrtrReadToolDefinition(cwd) {
75
+ const builtin = createReadToolDefinition(cwd);
76
+ return {
77
+ name: 'read',
78
+ label: 'read',
79
+ description: 'Render read calls with a crtr icon header in the attach viewer.',
80
+ parameters: {},
81
+ renderCall(args, theme, context) {
82
+ const a = args;
83
+ const raw = str(a?.file_path ?? a?.path);
84
+ let range = '';
85
+ if (a?.offset !== undefined || a?.limit !== undefined) {
86
+ const start = typeof a?.offset === 'number' ? a.offset : 1;
87
+ const end = typeof a?.limit === 'number' ? start + a.limit - 1 : undefined;
88
+ range = theme.fg('warning', `:${start}${end !== undefined ? `-${end}` : ''}`);
89
+ }
90
+ const line = `${header(TOOL_ICON.read, 'read', theme)} ${pathDisplay(raw, theme, context.cwd ?? cwd)}${range}`;
91
+ const component = context.lastComponent ?? new Text('', 0, 0);
92
+ component.setText(line);
93
+ return component;
94
+ },
95
+ renderResult(result, options, theme, context) {
96
+ return builtin.renderResult(result, options, theme, context);
97
+ },
98
+ };
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // write
102
+ // ---------------------------------------------------------------------------
103
+ /** Attach-viewer `write`: pi's builtin call renderer (it owns the
104
+ * syntax-highlighted content preview and its expand hint) with the icon
105
+ * prefixed onto its header line. Folded, only that header survives. */
106
+ export function createCrtrWriteToolDefinition(cwd) {
107
+ const builtin = createWriteToolDefinition(cwd);
108
+ return {
109
+ name: 'write',
110
+ label: 'write',
111
+ description: 'Render write calls with a crtr icon header in the attach viewer.',
112
+ parameters: {},
113
+ renderCall(args, theme, context) {
114
+ if (isFoldedContext(context)) {
115
+ const a = args;
116
+ const line = foldedFileCallLine('write', str(a?.file_path ?? a?.path), theme, context.cwd ?? cwd);
117
+ // Reuse the previous component when it is Text-like (the builtin's is a
118
+ // Text subclass), so fold toggles don't churn component identity.
119
+ const last = context.lastComponent;
120
+ if (last && typeof last.setText === 'function') {
121
+ last.setText(line);
122
+ return last;
123
+ }
124
+ return new Text(line, 0, 0);
125
+ }
126
+ const component = builtin.renderCall(args, theme, context);
127
+ prefixToolText(component, `${theme.fg('accent', TOOL_ICON.write)} `);
128
+ return component;
129
+ },
130
+ renderResult(result, options, theme, context) {
131
+ return builtin.renderResult(result, options, theme, context);
132
+ },
133
+ };
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // bash (non-crtr commands; `crtr ...` invocations get the command card in
137
+ // render/crtr-output.ts)
138
+ // ---------------------------------------------------------------------------
139
+ const OPERATOR_RE = /^(&&|\|\||\||;|>>|>|<|2>&1|&)$/;
140
+ /** Split a command into shell-ish tokens, keeping quoted spans whole. */
141
+ function tokenize(line) {
142
+ return line.match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+/g) ?? [];
143
+ }
144
+ /** Color a single command line so it reads at a glance: program name first,
145
+ * operators marking the pipeline joints, flags and quoted strings toned to
146
+ * their own hues, everything else plain tool output. */
147
+ function styleCommandLine(line, theme) {
148
+ const tokens = tokenize(line);
149
+ let atSegmentStart = true;
150
+ const styled = tokens.map((token) => {
151
+ if (OPERATOR_RE.test(token)) {
152
+ atSegmentStart = true;
153
+ return theme.fg('syntaxOperator', token);
154
+ }
155
+ if (atSegmentStart && !token.includes('=')) {
156
+ atSegmentStart = false;
157
+ return theme.fg('syntaxFunction', theme.bold(token));
158
+ }
159
+ atSegmentStart = false;
160
+ if (token.startsWith('-'))
161
+ return theme.fg('muted', token);
162
+ if (/^["']/.test(token))
163
+ return theme.fg('syntaxString', token);
164
+ return theme.fg('toolOutput', token);
165
+ });
166
+ return styled.join(' ');
167
+ }
168
+ /** The bash call header. Collapsed it is exactly ONE line — icon, the command's
169
+ * first line styled, truncated to the pane with a muted `+N lines` count when
170
+ * there is more. Expanded (ctrl+o) it prints every line of the command, wrapped
171
+ * (never clipped — expanded is the raw truth) into the pane with continuations
172
+ * hanging under the icon. A line wider than the pane would desync pi-tui's line
173
+ * accounting against the terminal's own wrapping and garble the frame. */
174
+ class BashCallComponent {
175
+ command = '';
176
+ expanded = false;
177
+ timeout;
178
+ theme;
179
+ /** Nothing is cached across paints (render recomputes from the fields), so
180
+ * pi's cache-drop hook has nothing to do here. */
181
+ invalidate() { }
182
+ update(theme, command, expanded, timeout) {
183
+ this.theme = theme;
184
+ this.command = command;
185
+ this.expanded = expanded;
186
+ this.timeout = timeout;
187
+ }
188
+ render(width) {
189
+ const theme = this.theme;
190
+ if (!theme)
191
+ return [];
192
+ const icon = theme.fg('accent', TOOL_ICON.bash);
193
+ const lines = this.command.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
194
+ const suffix = this.timeout ? theme.fg('muted', ` (timeout ${this.timeout}s)`) : '';
195
+ if (lines.length === 0)
196
+ return [`${icon} ${theme.fg('toolOutput', '...')}`];
197
+ if (this.expanded) {
198
+ const prefixWidth = visibleWidth(`${icon} `);
199
+ const indent = ' '.repeat(prefixWidth);
200
+ const inner = Math.max(1, width - prefixWidth);
201
+ const body = [];
202
+ for (let i = 0; i < lines.length; i++) {
203
+ let styled = styleCommandLine(lines[i], theme);
204
+ if (i === lines.length - 1)
205
+ styled += suffix;
206
+ const wrapped = wrapTextWithAnsi(styled, inner);
207
+ for (let j = 0; j < wrapped.length; j++) {
208
+ body.push(`${i === 0 && j === 0 ? `${icon} ` : indent}${wrapped[j]}`);
209
+ }
210
+ }
211
+ // Final backstop: at a pane narrower than the prefix itself the wrap
212
+ // budget bottoms out at 1 and a wrapped line can still overhang.
213
+ return body.map((line) => truncateToWidth(line, width, ''));
214
+ }
215
+ const more = lines.length > 1 ? theme.fg('muted', ` +${lines.length - 1} lines`) : '';
216
+ const fixed = visibleWidth(`${icon} `) + visibleWidth(more) + visibleWidth(suffix);
217
+ const budget = Math.max(8, width - fixed);
218
+ const first = lines[0];
219
+ const shown = first.length > budget ? `${first.slice(0, Math.max(1, budget - 1))}…` : first;
220
+ return [truncateToWidth(`${icon} ${styleCommandLine(shown, theme)}${more}${suffix}`, width)];
221
+ }
222
+ }
223
+ /** Attach-viewer `bash` for anything that is not a `crtr` invocation. */
224
+ export function createCrtrPlainBashToolDefinition(cwd) {
225
+ const builtin = createBashToolDefinition(cwd);
226
+ return {
227
+ name: 'bash',
228
+ label: 'bash',
229
+ description: 'Render plain bash calls as a compact styled command line in the attach viewer.',
230
+ parameters: {},
231
+ renderCall(args, theme, context) {
232
+ const a = args;
233
+ // pi's builtin bash result renderer reads the elapsed clock off this shared
234
+ // state, which its own renderCall would have started — keep stamping it so a
235
+ // running command still shows its duration.
236
+ const state = context.state;
237
+ if (state && context.executionStarted && state.startedAt === undefined) {
238
+ state.startedAt = Date.now();
239
+ state.endedAt = undefined;
240
+ }
241
+ const component = context.lastComponent instanceof BashCallComponent ? context.lastComponent : new BashCallComponent();
242
+ component.update(theme, str(a?.command),
243
+ // Folded wins over ctrl+o expansion: a folded bash is always its one-line form.
244
+ Boolean(context.expanded) && !isFoldedContext(context), typeof a?.timeout === 'number' ? a.timeout : undefined);
245
+ return component;
246
+ },
247
+ renderResult(result, options, theme, context) {
248
+ return builtin.renderResult(result, options, theme, context);
249
+ },
250
+ };
251
+ }
@@ -15,6 +15,9 @@ export interface InputWiringHooks {
15
15
  lastAssistantText: () => string | undefined;
16
16
  /** Ctrl+O — flip global tool-output expansion; returns the new state. */
17
17
  toggleToolsExpanded: () => boolean;
18
+ /** `/fold-tools` — flip the fold-finished-tool-calls view setting; returns the
19
+ * new state. */
20
+ toggleFoldTools: () => boolean;
18
21
  /** Ctrl+T — flip thinking-block visibility; returns true when now HIDDEN. */
19
22
  toggleThinking: () => boolean;
20
23
  /** `/graph` and the graph keybinding. */
@@ -80,6 +80,9 @@ export function createInputController(ctx, km, hooks) {
80
80
  // state owned by ChatView, surfaced as a footer notice mirroring pi's
81
81
  // status line.
82
82
  onToggleToolsExpand: () => setNotice(hooks.toggleToolsExpanded() ? 'Tool output: expanded' : 'Tool output: collapsed'),
83
+ onFoldTools: () => setNotice(hooks.toggleFoldTools()
84
+ ? 'Finished tool calls: folded to one line'
85
+ : 'Finished tool calls: shown in full'),
83
86
  onToggleThinking: () => setNotice(hooks.toggleThinking() ? 'Thinking blocks: hidden' : 'Thinking blocks: visible'),
84
87
  // The correlated read-op channel the native pickers (/model, /resume,
85
88
  // /fork, /tree, /settings, /scoped-models) fetch their payloads over.
@@ -71,6 +71,10 @@ export interface SlashContext {
71
71
  onColor?: (color: string | null) => void;
72
72
  /** Open the focused node's progressively disclosed metadata dossier. */
73
73
  onNodeMetadata?: () => void;
74
+ /** `/fold-tools` — flip the "fold finished tool calls" view setting. Pure
75
+ * viewer-local render state (ChatView owns it), so it is safe for an observer
76
+ * and for a remote attach. Absent → the command notifies it is unwired. */
77
+ onFoldTools?: () => void;
74
78
  }
75
79
  /** Canvas slash-commands — implemented NATIVE in the viewer, the ONLY live host:
76
80
  * the broker binds mode:'rpc', so broker pi-extensions never run their
@@ -42,6 +42,7 @@ export const CANVAS_SLASH_COMMANDS = [
42
42
  { name: 'mcp', description: 'MCP servers — status from the adapter’s config/cache; Enter (or /mcp <server>) connects, reconnects, or re-auths' },
43
43
  { name: 'rename', description: 'Rename this node everywhere — node row, viewer window, session chip, tmux pane: /rename <name>' },
44
44
  { name: 'color', description: 'Color this node — tmux pane outline + name chip: /color <color> (red, blue, #rrggbb, …) or /color none' },
45
+ { name: 'fold-tools', description: 'View setting — fold every FINISHED tool call to its single call line (Alt+C → w → f)' },
45
46
  ];
46
47
  const CANVAS_NAMES = new Set(CANVAS_SLASH_COMMANDS.map((c) => c.name));
47
48
  /** Native canvas commands that shell a LOCAL `crtr` mutation or read against
@@ -394,6 +395,13 @@ function dispatchCanvasCommand(name, arg, ctx) {
394
395
  return true;
395
396
  case 'color':
396
397
  return colorNode(arg, ctx);
398
+ case 'fold-tools':
399
+ // Viewer-local render state only — no frame, no CLI, no node mutation.
400
+ if (ctx.onFoldTools)
401
+ ctx.onFoldTools();
402
+ else
403
+ ctx.notify('The fold-tools view setting is not wired into this viewer');
404
+ return true;
397
405
  default:
398
406
  ctx.notify(`/${name} is not available in attach`);
399
407
  return true;