@north-light/crouter 0.3.291 → 0.3.292

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.
@@ -11,11 +11,11 @@ const theme = {
11
11
  };
12
12
  /** pi calls tool renderers with a shared state bag; a normal (unfolded) call has an empty one. */
13
13
  function renderCall(command) {
14
- const definition = createCrtrBashToolDefinition({ command });
14
+ const definition = createCrtrBashToolDefinition({ command })?.definition;
15
15
  return definition.renderCall({ command }, theme, { state: {} }).render(80).join('\n');
16
16
  }
17
17
  function renderResult(command, text, expanded) {
18
- const definition = createCrtrBashToolDefinition({ command });
18
+ const definition = createCrtrBashToolDefinition({ command })?.definition;
19
19
  return definition.renderResult({ content: [{ type: 'text', text }] }, { expanded }, theme, {}).render(70);
20
20
  }
21
21
  test('detects crtr in bash command position, not heredoc body prose', () => {
@@ -50,7 +50,7 @@ test('scopes -h/--help detection and the displayed path to the crtr invocation\'
50
50
  });
51
51
  test('crtr results do not invalidate their owning tool component', () => {
52
52
  let ownerInvalidations = 0;
53
- const def = createCrtrBashToolDefinition({ command: 'crtr canvas dashboard' });
53
+ const def = createCrtrBashToolDefinition({ command: 'crtr canvas dashboard' })?.definition;
54
54
  const component = def.renderResult({ content: [{ type: 'text', text: 'dashboard' }] }, { expanded: false }, theme, { invalidate: () => { ownerInvalidations++; } });
55
55
  component.invalidate?.();
56
56
  assert.equal(ownerInvalidations, 0);
@@ -201,8 +201,8 @@ class MinimizableToolComponent extends ToolExecutionComponent {
201
201
  expandedState = false;
202
202
  foldController;
203
203
  resultObserver;
204
- /** A foldable definition owns its folded call component; an edit definition
205
- * may also observe results before the imported shell renders them. */
204
+ /** A foldable definition owns its folded call component; it may also observe
205
+ * results before the imported shell renders them. */
206
206
  setToolView(foldController, resultObserver) {
207
207
  this.foldController = foldController;
208
208
  this.resultObserver = resultObserver;
@@ -1387,7 +1387,14 @@ export class ChatView {
1387
1387
  let resultObserver;
1388
1388
  let toolDefinition;
1389
1389
  if (name === 'bash') {
1390
- toolDefinition = createCrtrBashToolDefinition(args, fold) ?? createCrtrPlainBashToolDefinition(this.cwd, fold, this.backgroundBashHint);
1390
+ const crtr = createCrtrBashToolDefinition(args, fold);
1391
+ if (crtr === undefined) {
1392
+ toolDefinition = createCrtrPlainBashToolDefinition(this.cwd, fold, this.backgroundBashHint);
1393
+ }
1394
+ else {
1395
+ toolDefinition = crtr.definition;
1396
+ resultObserver = crtr.observeResult;
1397
+ }
1391
1398
  }
1392
1399
  else if (name === 'edit') {
1393
1400
  const edit = createCrtrEditToolDefinition(this.cwd, fold);
@@ -1,5 +1,5 @@
1
1
  import { Box } from '@earendil-works/pi-tui';
2
- import { FoldedToolCallController } from './tool-calls.js';
2
+ import { FoldedToolCallController, type ToolResultObserver } from './tool-calls.js';
3
3
  type Theme = {
4
4
  fg: (name: string, text: string) => string;
5
5
  bg: (name: string, text: string) => string;
@@ -45,5 +45,9 @@ export declare class CrtrOutputMessageComponent extends Box {
45
45
  constructor(content: string, expanded: boolean, theme: Theme);
46
46
  setExpanded(expanded: boolean): void;
47
47
  }
48
- export declare function createCrtrBashToolDefinition(args: unknown, fold?: FoldedToolCallController): unknown | undefined;
48
+ export type CrtrBashToolView = {
49
+ definition: unknown;
50
+ observeResult: ToolResultObserver;
51
+ };
52
+ export declare function createCrtrBashToolDefinition(args: unknown, fold?: FoldedToolCallController): CrtrBashToolView | undefined;
49
53
  export {};
@@ -1,6 +1,8 @@
1
1
  import { truncateToVisualLines } from '@earendil-works/pi-coding-agent';
2
2
  import { Box, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
3
- import { HELP_ICON, iconForPath, suppressOutputForPath, summarizePath, } from '../../../core/preview-registry.js';
3
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
4
+ import { crtrPreviewRecords, HELP_ICON, iconForPath, suppressOutputForPath, summarizePath, } from '../../../core/preview-registry.js';
5
+ import { parseSurfaces } from '../../../core/substrate/schema.js';
4
6
  import { FoldedToolCallController } from './tool-calls.js';
5
7
  // Hard cap on a collapsed crtr card's total rendered lines (rule 12,
6
8
  // inline-ui-placement): help synthesis or a normal truncated result never
@@ -152,6 +154,141 @@ function commandFromArgs(args) {
152
154
  const CRTR_FLAG_TOKEN = /^-{1,2}[A-Za-z0-9]/;
153
155
  const CRTR_ATTACHED_FLAG_VALUE = /^(--?[A-Za-z0-9][^=]*)(=)(.*)$/;
154
156
  const CRTR_NUMBER_VALUE = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
157
+ // Memory routing
158
+ //
159
+ // `memory write` and `memory edit` decide who sees a document, when, and at what
160
+ // context cost. Folded, their argv is a wall of frontmatter flags that wraps over
161
+ // several rows, so the row states the routing itself instead: the scope the
162
+ // document landed in, its gate, its surface entries with the rung each delivers
163
+ // at, and its `when-and-why-to-read` line.
164
+ //
165
+ // The frontmatter flags are in the invocation's own argv — the same text the
166
+ // write copies verbatim into the document. `scope` is not a flag: the selector
167
+ // rules resolve it, and the leaf reports it in its RESULT. The result is
168
+ // reachable here because this definition observes every settled tool result
169
+ // (`observeResult`), which runs before a fold hides the result record.
170
+ const MEMORY_ROUTING_LEAVES = new Set(['memory write', 'memory edit']);
171
+ /** Every value the invocation passed for one flag, in either spelling
172
+ * (`--flag value`, `--flag=value`). A flag left without a value takes the next
173
+ * flag token as its own, so the separated spelling only claims a non-flag. */
174
+ function crtrFlagValues(tokens, flag) {
175
+ const attached = `--${flag}=`;
176
+ const values = [];
177
+ tokens.forEach((token, index) => {
178
+ if (token === `--${flag}`) {
179
+ const value = tokens[index + 1];
180
+ if (value !== undefined && !CRTR_FLAG_TOKEN.test(value))
181
+ values.push(value);
182
+ return;
183
+ }
184
+ if (token.startsWith(attached))
185
+ values.push(token.slice(attached.length));
186
+ });
187
+ return values;
188
+ }
189
+ function crtrFlagValue(tokens, flag) {
190
+ return crtrFlagValues(tokens, flag)[0];
191
+ }
192
+ function parsedYaml(raw) {
193
+ try {
194
+ return parseYaml(raw);
195
+ }
196
+ catch {
197
+ return undefined;
198
+ }
199
+ }
200
+ /** A predicate printed the way it is authored on the command line: one-line
201
+ * YAML flow. Anything that is not a field→matcher map is shown as typed. */
202
+ function predicateText(raw) {
203
+ const parsed = parsedYaml(raw);
204
+ return parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)
205
+ ? raw.trim()
206
+ : stringifyYaml(parsed, { collectionStyle: 'flow', flowCollectionPadding: false, lineWidth: 0 }).trim();
207
+ }
208
+ function predicateValueText(value) {
209
+ return stringifyYaml(value, { collectionStyle: 'flow', flowCollectionPadding: false, lineWidth: 0 }).trim();
210
+ }
211
+ function stringField(record, field) {
212
+ const value = record?.[field];
213
+ return typeof value === 'string' && value !== '' ? value : undefined;
214
+ }
215
+ /** The routing a `memory write`/`memory edit` invocation states, or undefined
216
+ * for every other leaf. The result record is authoritative where it speaks:
217
+ * it carries the scope the selector rules chose and the resolved canonical
218
+ * name, which composes the store's namespace onto the name that was passed. */
219
+ function memoryRouting(tokens, records) {
220
+ const leaf = tokens.slice(0, 2).join(' ');
221
+ if (!MEMORY_ROUTING_LEAVES.has(leaf))
222
+ return undefined;
223
+ const positional = tokens[2] !== undefined && !CRTR_FLAG_TOKEN.test(tokens[2]) ? tokens[2] : undefined;
224
+ // One bash call may run several crtr invocations; the row shows the first, so
225
+ // it reads the first record this leaf produced.
226
+ const result = records.find((record) => record.path === leaf)?.result;
227
+ const gate = crtrFlagValue(tokens, 'gate');
228
+ return {
229
+ leaf,
230
+ name: stringField(result, 'name') ?? positional ?? '',
231
+ scope: stringField(result, 'scope') ?? crtrFlagValue(tokens, 'scope'),
232
+ gate: gate === undefined ? undefined : predicateText(gate),
233
+ surfaces: parseSurfaces(crtrFlagValues(tokens, 'surface').map(parsedYaml)),
234
+ whenAndWhyToRead: crtrFlagValue(tokens, 'when-and-why-to-read'),
235
+ };
236
+ }
237
+ /** How much of the document the entry delivers, toned by how much context it
238
+ * costs every agent it reaches. */
239
+ function rungTone(rung) {
240
+ return rung === 'content' ? 'warning' : rung === 'preview' ? 'accent' : 'muted';
241
+ }
242
+ /** One `surfaces` entry in its authored key order: the event, what it matches,
243
+ * the predicates that narrow it, and the rung it delivers at. */
244
+ function surfaceEntryText(entry, theme) {
245
+ const constraints = [entry.on];
246
+ if (entry.match !== undefined)
247
+ constraints.push(entry.match.join(' '));
248
+ if (entry.matchFrontmatter !== undefined)
249
+ constraints.push(`match-frontmatter ${predicateValueText(entry.matchFrontmatter)}`);
250
+ if (entry.gate !== undefined)
251
+ constraints.push(`gate ${predicateValueText(entry.gate)}`);
252
+ return `${theme.fg('text', constraints.join(' '))} ${theme.fg('dim', 'at')} ${theme.fg(rungTone(entry.at), entry.at)}`;
253
+ }
254
+ function labelledFact(label, value, theme) {
255
+ return `${theme.fg('dim', label)} ${value}`;
256
+ }
257
+ /** The routing block under the folded call's header line: scope and gate
258
+ * together, then the surface entries, then the routing line. A fact the
259
+ * invocation does not state is left out rather than shown as empty. */
260
+ function memoryRoutingLines(routing, theme) {
261
+ const indent = ' ';
262
+ const lines = [];
263
+ const placement = [];
264
+ if (routing.scope !== undefined)
265
+ placement.push(labelledFact('scope', theme.fg('text', routing.scope), theme));
266
+ if (routing.gate !== undefined)
267
+ placement.push(labelledFact('gate', theme.fg('text', routing.gate), theme));
268
+ if (placement.length > 0)
269
+ lines.push(`${indent}${placement.join(theme.fg('dim', ' · '))}`);
270
+ const surfaces = routing.surfaces.length === 0
271
+ ? theme.fg('muted', 'none')
272
+ : routing.surfaces.map((entry) => surfaceEntryText(entry, theme)).join(theme.fg('dim', ', '));
273
+ lines.push(`${indent}${labelledFact('surface', surfaces, theme)}`);
274
+ if (routing.whenAndWhyToRead !== undefined) {
275
+ lines.push(`${indent}${labelledFact('when-and-why-to-read', theme.fg('text', routing.whenAndWhyToRead), theme)}`);
276
+ }
277
+ return lines;
278
+ }
279
+ /** A folded call that states more than one line. Every line is clipped to the
280
+ * pane, never wrapped: the viewer's frame length depends on a folded call
281
+ * painting exactly the rows it reports. */
282
+ class FoldedCrtrCallComponent {
283
+ lines;
284
+ constructor(lines) {
285
+ this.lines = lines;
286
+ }
287
+ invalidate() { }
288
+ render(width) {
289
+ return this.lines.map((line) => truncateToWidth(line, width, '…'));
290
+ }
291
+ }
155
292
  /** Syntax-color flags and their values while leaving the command path and
156
293
  * positional arguments in the folded row's existing bold title style. */
157
294
  function styleFoldedCrtrTokens(tokens, theme) {
@@ -182,17 +319,6 @@ function resultText(result) {
182
319
  .join('\n')
183
320
  .trimEnd();
184
321
  }
185
- /** The canvas preview extension attaches the CLI's exact result record here.
186
- * This deliberately validates only the transport envelope; command-owned
187
- * summaries validate the fields they use in preview-registry.ts. */
188
- function previewFromDetails(details) {
189
- if (details === null || typeof details !== 'object')
190
- return undefined;
191
- const preview = details['crtrPreview'];
192
- if (preview === null || typeof preview !== 'object' || typeof preview['path'] !== 'string')
193
- return undefined;
194
- return preview;
195
- }
196
322
  function styledLabelValue(indent, marker, label, separator, value, theme) {
197
323
  const styledMarker = marker === '' ? '' : theme.fg('accent', marker);
198
324
  return `${indent}${styledMarker}${theme.fg('dim', label)}${theme.fg('dim', separator)}${theme.fg('text', value)}`;
@@ -700,7 +826,15 @@ export function createCrtrBashToolDefinition(args, fold = new FoldedToolCallCont
700
826
  const command = commandFromArgs(args);
701
827
  if (!isCrtrBashCommand(command))
702
828
  return undefined;
703
- return {
829
+ // The invocations' own result records, kept from the settled tool result so a
830
+ // folded row can still state a fact the argv does not carry.
831
+ let records = [];
832
+ const observeResult = (result, isPartial) => {
833
+ if (isPartial)
834
+ return;
835
+ records = crtrPreviewRecords(result);
836
+ };
837
+ const definition = {
704
838
  name: 'bash',
705
839
  label: 'bash',
706
840
  description: 'Render crtr bash output with crtr-specific chrome in the attach viewer.',
@@ -719,13 +853,23 @@ export function createCrtrBashToolDefinition(args, fold = new FoldedToolCallCont
719
853
  const icon = isHelp ? HELP_ICON : iconForPath(pathTokens);
720
854
  const prefix = isHelp ? `${icon} crtr help` : `${icon} crtr`;
721
855
  const styledPrefix = theme.fg('accent', theme.bold(prefix));
856
+ // A memory write/edit folds to its routing, not to its flag dump: the
857
+ // fold is what the human scans, and these flags are the document's whole
858
+ // reach.
859
+ const routing = fold.folded && !isHelp ? memoryRouting(pathTokens, records) : undefined;
860
+ if (routing !== undefined) {
861
+ const named = routing.name === '' ? '' : ` ${theme.fg('toolTitle', theme.bold(routing.name))}`;
862
+ const header = `${styledPrefix} ${theme.fg('toolTitle', theme.bold(routing.leaf))}${named}`;
863
+ return fold.capture(new FoldedCrtrCallComponent([header, ...memoryRoutingLines(routing, theme)]));
864
+ }
722
865
  const styledPath = path
723
866
  ? ` ${fold.folded ? styleFoldedCrtrTokens(pathTokens, theme) : theme.fg('toolTitle', theme.bold(path))}`
724
867
  : '';
725
868
  return fold.capture(new Text(`${styledPrefix}${styledPath}`, 0, 0));
726
869
  },
727
870
  renderResult(result, options, theme, context) {
728
- return new CrtrResultComponent(resultText(result), options, context, theme, command, DEFAULT_PREVIEW_STATE, previewFromDetails(result.details));
871
+ return new CrtrResultComponent(resultText(result), options, context, theme, command, DEFAULT_PREVIEW_STATE, crtrPreviewRecords(result).at(-1));
729
872
  },
730
873
  };
874
+ return { definition, observeResult };
731
875
  }
@@ -1,18 +1,7 @@
1
- import { FoldedToolCallController, type FileLineChanges } from './tool-calls.js';
2
- type ToolResultLike = {
3
- content?: Array<{
4
- type?: unknown;
5
- text?: unknown;
6
- }>;
7
- details?: {
8
- diff?: unknown;
9
- };
10
- isError?: boolean;
11
- };
12
- export type EditResultObserver = (result: ToolResultLike, isPartial: boolean) => void;
1
+ import { FoldedToolCallController, type FileLineChanges, type ToolResultObserver } from './tool-calls.js';
13
2
  export type CrtrEditToolView = {
14
3
  definition: unknown;
15
- observeResult: EditResultObserver;
4
+ observeResult: ToolResultObserver;
16
5
  };
17
6
  /** Count only changed rows in pi's display diff; context and ellipsis rows do
18
7
  * not contribute to the folded file summary. */
@@ -21,4 +10,3 @@ export declare function countDisplayDiffLines(diff: string): FileLineChanges;
21
10
  * card has two explicit phases: streamed argument diff, then executed result
22
11
  * diff. No asynchronous preview can write after settlement. */
23
12
  export declare function createCrtrEditToolDefinition(cwd: string, fold?: FoldedToolCallController): CrtrEditToolView;
24
- export {};
@@ -266,7 +266,10 @@ export function createCrtrEditToolDefinition(cwd, fold = new FoldedToolCallContr
266
266
  return;
267
267
  state.settled = true;
268
268
  state.isError = result.isError === true;
269
- const resultDiff = result.details?.diff;
269
+ const details = result.details;
270
+ const resultDiff = details !== null && typeof details === 'object'
271
+ ? details.diff
272
+ : undefined;
270
273
  state.diff = !state.isError && typeof resultDiff === 'string' ? resultDiff : undefined;
271
274
  };
272
275
  const definition = {
@@ -3,6 +3,7 @@
3
3
  // calls into the exact evidence already present in the session transcript.
4
4
  import { homedir } from 'node:os';
5
5
  import { join, resolve } from 'node:path';
6
+ import { crtrPreviewRecords } from '../../../core/preview-registry.js';
6
7
  import { countDisplayDiffLines } from './edit-diff.js';
7
8
  /** Distinct event lines one recap will show before it starts counting. */
8
9
  const MAX_RECAP_EVENTS = 3;
@@ -27,27 +28,6 @@ function lineCount(content) {
27
28
  return 0;
28
29
  return (content.match(/\n/g)?.length ?? 0) + (content.endsWith('\n') ? 0 : 1);
29
30
  }
30
- /** One crtr invocation's own structured result, mirrored onto the bash tool
31
- * result by the canvas preview extension — present on live tool events and on
32
- * rebuilt snapshot messages alike, which is what makes it valid evidence for a
33
- * recap that reduces historical transcript.
34
- *
35
- * `crtrPreviews` carries every invocation in the bash call; `crtrPreview` (the
36
- * last one alone) is all a transcript persisted before the channel became a
37
- * list can offer, so both are read. */
38
- function crtrPreviews(result) {
39
- const details = object(object(result)?.details);
40
- if (details === undefined)
41
- return [];
42
- const all = details['crtrPreviews'];
43
- const records = Array.isArray(all) ? all : [details['crtrPreview']];
44
- return records.flatMap((entry) => {
45
- const record = object(entry);
46
- if (record === undefined || typeof record['path'] !== 'string')
47
- return [];
48
- return [{ path: record['path'], result: object(record['result']), error: object(record['error']) }];
49
- });
50
- }
51
31
  function text(value) {
52
32
  return typeof value === 'string' && value !== '' ? value : undefined;
53
33
  }
@@ -250,7 +230,7 @@ export class GroupActivityRecorder {
250
230
  }
251
231
  extract(entry, result) {
252
232
  if (entry.name === 'bash') {
253
- for (const invocation of crtrPreviews(result))
233
+ for (const invocation of crtrPreviewRecords(result))
254
234
  this.noteCrtrInvocation(invocation);
255
235
  return;
256
236
  }
@@ -6,6 +6,21 @@ type Theme = {
6
6
  /** Per-tool fold state owned by the attach viewer. Tool definitions capture the
7
7
  * call component they render while folded, so the viewer never reaches into
8
8
  * `ToolExecutionComponent`'s private renderer fields to recover it. */
9
+ /** A settled tool result on its way to the transcript. `MinimizableToolComponent`
10
+ * hands it to a definition's observer BEFORE the imported shell renders it, so a
11
+ * definition whose folded call row states a fact from the result (an edit's
12
+ * changed-line counts, a memory write's scope) still has that fact once the fold
13
+ * renders instead of the result record — on the live stream and on a rebuilt
14
+ * transcript alike. */
15
+ export type ObservedToolResult = {
16
+ content?: Array<{
17
+ type?: unknown;
18
+ text?: unknown;
19
+ }>;
20
+ details?: unknown;
21
+ isError?: boolean;
22
+ };
23
+ export type ToolResultObserver = (result: ObservedToolResult, isPartial: boolean) => void;
9
24
  export declare class FoldedToolCallController {
10
25
  folded: boolean;
11
26
  private component;
@@ -36,9 +36,6 @@ function backgroundHintLines(theme, startedAt, hint, width) {
36
36
  return [];
37
37
  return [truncateToWidth(` ${theme.fg('muted', `⧗ ${text}`)}`, width, '…')];
38
38
  }
39
- /** Per-tool fold state owned by the attach viewer. Tool definitions capture the
40
- * call component they render while folded, so the viewer never reaches into
41
- * `ToolExecutionComponent`'s private renderer fields to recover it. */
42
39
  export class FoldedToolCallController {
43
40
  folded = false;
44
41
  component;