@north-light/crouter 0.3.294 → 0.3.295

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.
@@ -251,42 +251,72 @@ function surfaceEntryText(entry, theme) {
251
251
  constraints.push(`gate ${predicateValueText(entry.gate)}`);
252
252
  return `${theme.fg('text', constraints.join(' '))} ${theme.fg('dim', 'at')} ${theme.fg(rungTone(entry.at), entry.at)}`;
253
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 = [];
254
+ const ROUTING_INDENT = ' ';
255
+ /** The routing block under the folded call's header line: one line per field,
256
+ * in the order the substrate resolves them. A fact the invocation does not
257
+ * state is left out rather than shown as empty. */
258
+ function memoryRoutingFields(routing, theme) {
259
+ const label = (text) => theme.fg('dim', text);
260
+ const fields = [];
264
261
  if (routing.scope !== undefined)
265
- placement.push(labelledFact('scope', theme.fg('text', routing.scope), theme));
262
+ fields.push({ label: label('scope'), values: [theme.fg('text', routing.scope)] });
266
263
  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)}`);
264
+ fields.push({ label: label('gate'), values: [theme.fg('text', routing.gate)] });
265
+ fields.push({
266
+ label: label('surface'),
267
+ values: routing.surfaces.length === 0
268
+ ? [theme.fg('muted', 'none')]
269
+ : routing.surfaces.map((entry) => surfaceEntryText(entry, theme)),
270
+ });
274
271
  if (routing.whenAndWhyToRead !== undefined) {
275
- lines.push(`${indent}${labelledFact('when-and-why-to-read', theme.fg('text', routing.whenAndWhyToRead), theme)}`);
272
+ fields.push({ label: label('when-and-why-to-read'), values: [theme.fg('text', routing.whenAndWhyToRead)] });
273
+ }
274
+ return fields;
275
+ }
276
+ /** One field's rows: the label, then its values in a column of their own. Each
277
+ * value starts at that column — so a second surface entry reads as a sibling of
278
+ * the first — and a value too long for the pane wraps under it. A field that
279
+ * states SEVERAL values wraps one notch deeper still, which is what keeps a
280
+ * wrapped surface entry from reading as the next entry. */
281
+ function routingFieldLines(field, width) {
282
+ const head = `${ROUTING_INDENT}${field.label} `;
283
+ const column = visibleWidth(head);
284
+ const valueIndent = ' '.repeat(column);
285
+ const wrapIndent = field.values.length > 1 ? `${valueIndent} ` : valueIndent;
286
+ const lines = [];
287
+ for (const value of field.values) {
288
+ const wrapped = wrapTextWithAnsi(value, Math.max(1, width - visibleWidth(wrapIndent)));
289
+ wrapped.forEach((line, index) => {
290
+ const prefix = index > 0 ? wrapIndent : lines.length === 0 ? head : valueIndent;
291
+ lines.push(`${prefix}${line}`);
292
+ });
276
293
  }
277
294
  return lines;
278
295
  }
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. */
296
+ /** A folded memory call: its header line, then its routing fields. Values WRAP
297
+ * rather than clip — the row states the document's whole reach, so a value cut
298
+ * off at the pane edge is routing the human cannot read. Wrapping keeps the
299
+ * folded call painting exactly the rows it reports, because the viewer
300
+ * measures a block by rendering it at the width it paints at and caches that
301
+ * measurement per width (MeasuredContainer). What must hold at every width is
302
+ * that no row overhangs the pane, or the terminal wraps a row nobody counted. */
282
303
  class FoldedCrtrCallComponent {
283
- lines;
284
- constructor(lines) {
285
- this.lines = lines;
304
+ header;
305
+ fields;
306
+ constructor(header, fields) {
307
+ this.header = header;
308
+ this.fields = fields;
286
309
  }
287
310
  invalidate() { }
288
311
  render(width) {
289
- return this.lines.map((line) => truncateToWidth(line, width, '…'));
312
+ const lines = wrapTextWithAnsi(this.header, Math.max(1, width));
313
+ for (const field of this.fields)
314
+ lines.push(...routingFieldLines(field, width));
315
+ // Final backstop: a pane too narrow to leave the value column any room to
316
+ // wrap into (24 cells for when-and-why-to-read, 12 for a wrapped surface
317
+ // entry) is the only way a row can overhang. It erases the value it clips
318
+ // rather than trimming it — at that width the label alone fills the pane.
319
+ return lines.map((line) => truncateToWidth(line, width, ''));
290
320
  }
291
321
  }
292
322
  /** Syntax-color flags and their values while leaving the command path and
@@ -860,7 +890,7 @@ export function createCrtrBashToolDefinition(args, fold = new FoldedToolCallCont
860
890
  if (routing !== undefined) {
861
891
  const named = routing.name === '' ? '' : ` ${theme.fg('toolTitle', theme.bold(routing.name))}`;
862
892
  const header = `${styledPrefix} ${theme.fg('toolTitle', theme.bold(routing.leaf))}${named}`;
863
- return fold.capture(new FoldedCrtrCallComponent([header, ...memoryRoutingLines(routing, theme)]));
893
+ return fold.capture(new FoldedCrtrCallComponent(header, memoryRoutingFields(routing, theme)));
864
894
  }
865
895
  const styledPath = path
866
896
  ? ` ${fold.folded ? styleFoldedCrtrTokens(pathTokens, theme) : theme.fg('toolTitle', theme.bold(path))}`