@north-light/crouter 0.3.195 → 0.3.196

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.
@@ -6,7 +6,7 @@ export declare class ViewerAssistantMessageComponent extends AssistantMessageCom
6
6
  /** The stripped copy last handed to the base class, for identity comparison. */
7
7
  private stripped;
8
8
  private hidden;
9
- constructor(message: AssistantMessageLike | undefined, hideThinkingBlock?: boolean, markdownTheme?: ConstructorParameters<typeof AssistantMessageComponent>[2], hiddenThinkingLabel?: string);
9
+ constructor(message: AssistantMessageLike | undefined, hideThinkingBlock?: boolean, markdownTheme?: ConstructorParameters<typeof AssistantMessageComponent>[2], hiddenThinkingLabel?: string, markdownTransformers?: ConstructorParameters<typeof AssistantMessageComponent>[5]);
10
10
  updateContent(message: AssistantMessageLike): void;
11
11
  setHideThinkingBlock(hide: boolean): void;
12
12
  }
@@ -23,9 +23,10 @@ export class ViewerAssistantMessageComponent extends AssistantMessageComponent {
23
23
  /** The stripped copy last handed to the base class, for identity comparison. */
24
24
  stripped;
25
25
  hidden;
26
- constructor(message, hideThinkingBlock = false, markdownTheme, hiddenThinkingLabel) {
26
+ constructor(message, hideThinkingBlock = false, markdownTheme, hiddenThinkingLabel, markdownTransformers) {
27
27
  // Defer content to our own updateContent so stripping applies to it too.
28
- super(undefined, hideThinkingBlock, markdownTheme, hiddenThinkingLabel);
28
+ // `undefined` outputPad keeps the base class's own default.
29
+ super(undefined, hideThinkingBlock, markdownTheme, hiddenThinkingLabel, undefined, markdownTransformers);
29
30
  this.hidden = hideThinkingBlock;
30
31
  if (message)
31
32
  this.updateContent(message);
@@ -111,13 +111,16 @@ export declare class ChatView {
111
111
  private readonly recapPalette;
112
112
  /** Bold accent for the expanded crtr-context label. */
113
113
  private readonly labelStyle;
114
- /** Raw broker messages retained beside their viewer components so a local
115
- * Ctrl+O disclosure toggle can re-project settled Markdown without touching
116
- * persisted/copyable source. Includes condensed-history components. */
117
- private readonly assistantSources;
118
114
  /** Undefined honors each `<details open>` attribute until Ctrl+O establishes
119
115
  * one viewer-wide disclosure state. */
120
116
  private detailsExpandedOverride;
117
+ /** Viewer-local Markdown projection, handed to pi's message components so
118
+ * THEY apply it at render time with the exact content width. The viewer keeps
119
+ * no projected copy: the components hold canonical source, and each
120
+ * transformer reads live viewer state (disclosure, palette) on every call.
121
+ * Diagrams and the HTML allowlist apply only to assistant-authored
122
+ * Markdown. */
123
+ private readonly markdownTransformers;
121
124
  /** The assistant message component currently being streamed (between
122
125
  * message_start and message_end for an assistant turn). */
123
126
  private streamingComponent;
@@ -268,19 +271,6 @@ export declare class ChatView {
268
271
  /** Append conversation content above the independent live status area — or into
269
272
  * the nested container while condensed history is under construction. */
270
273
  private append;
271
- /** Project one raw assistant message through every attach-local Markdown
272
- * transform. This is the sole pipeline used by snapshots, streaming, settled
273
- * messages, and Ctrl+O re-renders. */
274
- private renderAssistantMessage;
275
- /** Retain canonical source and update one stable transcript component with its
276
- * viewer-local projection. */
277
- private updateAssistantComponent;
278
- /** Re-project every assistant component in place after a disclosure toggle. */
279
- private rerenderAssistantMessages;
280
- /** Approximate render width for inline mermaid diagrams. Only affects
281
- * termrender's trailing padding (stripped in the transform), so the terminal
282
- * column count is close enough without threading the live pane width here. */
283
- private renderWidth;
284
274
  /** Number of rendered conversation components in the container currently being
285
275
  * appended to (live history or the nested condensed region). */
286
276
  private chatChildCount;
@@ -28,9 +28,9 @@ import { CRTR_OUTPUT_CUSTOM_TYPE, CrtrOutputMessageComponent, createCrtrBashTool
28
28
  import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE } from '../../../core/runtime/session-cycles.js';
29
29
  import { generatedContextExpandedBody, generatedContextPresentation, isRuntimeRestartContinuation, } from '../../../shared/generated-context.js';
30
30
  import { assistantVisibleText, isTrueUserMessage, } from '../../../shared/tool-groups.js';
31
- import { transformMessageDiagrams } from './diagram.js';
32
- import { transformMessageHtml } from './html-markdown.js';
33
- import { styleAttachMarkdownSource, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-source.js';
31
+ import { transformDiagramFences } from './diagram.js';
32
+ import { transformHtmlMarkdown } from './html-markdown.js';
33
+ import { styleAttachAgentMarkdown, styleAttachMarkdownSource, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-source.js';
34
34
  import { createCrtrEditToolDefinition } from './edit-diff.js';
35
35
  import { GroupActivityRecorder } from './group-activity.js';
36
36
  import { GroupRecapComponent } from './group-recap.js';
@@ -305,13 +305,29 @@ export class ChatView {
305
305
  recapPalette;
306
306
  /** Bold accent for the expanded crtr-context label. */
307
307
  labelStyle = (s) => `\x1b[1m${s}\x1b[22m`;
308
- /** Raw broker messages retained beside their viewer components so a local
309
- * Ctrl+O disclosure toggle can re-project settled Markdown without touching
310
- * persisted/copyable source. Includes condensed-history components. */
311
- assistantSources = new Map();
312
308
  /** Undefined honors each `<details open>` attribute until Ctrl+O establishes
313
309
  * one viewer-wide disclosure state. */
314
310
  detailsExpandedOverride;
311
+ /** Viewer-local Markdown projection, handed to pi's message components so
312
+ * THEY apply it at render time with the exact content width. The viewer keeps
313
+ * no projected copy: the components hold canonical source, and each
314
+ * transformer reads live viewer state (disclosure, palette) on every call.
315
+ * Diagrams and the HTML allowlist apply only to assistant-authored
316
+ * Markdown. */
317
+ markdownTransformers = [
318
+ (markdown, { messageType, availableWidth }) => (messageType === 'assistant' ? transformDiagramFences(markdown, availableWidth) : markdown),
319
+ (markdown, { messageType }) => (messageType === 'assistant'
320
+ ? transformHtmlMarkdown(markdown, {
321
+ detailsExpanded: this.detailsExpandedOverride,
322
+ styles: {
323
+ kbd: (text) => this.recapPalette.active(this.recapPalette.bold(`‹${text}›`)),
324
+ mark: (text) => `\x1b[7m${this.recapPalette.bold(this.recapPalette.accent(text))}\x1b[27m`,
325
+ hint: this.dimStyle,
326
+ },
327
+ })
328
+ : markdown),
329
+ (markdown, { messageType }) => (messageType === 'user' ? styleAttachMarkdownSource(markdown) : styleAttachAgentMarkdown(markdown)),
330
+ ];
315
331
  /** The assistant message component currently being streamed (between
316
332
  * message_start and message_end for an assistant turn). */
317
333
  streamingComponent;
@@ -499,11 +515,11 @@ export class ChatView {
499
515
  this.closeOpenGroup();
500
516
  let separator;
501
517
  if (isStreamingTail) {
502
- this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
518
+ this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel, this.markdownTransformers);
503
519
  this.appendAssistantPart(this.streamingComponent, !hasProse);
504
520
  this.registerCopyBlock(this.streamingComponent, 'assistant', '');
505
521
  this.streamingWasGroupMember = !hasProse;
506
- this.updateAssistantComponent(this.streamingComponent, message);
522
+ this.streamingComponent.updateContent(message);
507
523
  this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(message));
508
524
  separator = this.streamingToolSeparator = this.appendAssistantSeparator(!hasProse);
509
525
  }
@@ -638,7 +654,6 @@ export class ChatView {
638
654
  if (this.streamingComponent) {
639
655
  this.historyContainer.removeChild(this.streamingComponent);
640
656
  this.unregisterCopyBlock(this.streamingComponent);
641
- this.assistantSources.delete(this.streamingComponent);
642
657
  this.streamingComponent = undefined;
643
658
  }
644
659
  this.streamingToolSeparator = undefined;
@@ -673,7 +688,10 @@ export class ChatView {
673
688
  result = { kind: 'output', expanded: this.toolOutputExpanded };
674
689
  }
675
690
  this.detailsExpandedOverride = this.toolOutputExpanded;
676
- this.rerenderAssistantMessages();
691
+ // The disclosure state lives in the transformer closure, so the transcript
692
+ // only has to drop its projections: pi's message components rebuild their
693
+ // Markdown on invalidate() and re-run the transformer on the next render.
694
+ this.historyContainer.invalidate();
677
695
  this.applyToolDisplay();
678
696
  return result;
679
697
  }
@@ -772,11 +790,11 @@ export class ChatView {
772
790
  const hasProse = assistantVisibleText(event.message).trim() !== '';
773
791
  if (hasProse)
774
792
  this.closeOpenGroup();
775
- this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
793
+ this.streamingComponent = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel, this.markdownTransformers);
776
794
  this.streamingWasGroupMember = !hasProse;
777
795
  this.appendAssistantPart(this.streamingComponent, !hasProse);
778
796
  this.registerCopyBlock(this.streamingComponent, 'assistant', '');
779
- this.updateAssistantComponent(this.streamingComponent, event.message);
797
+ this.streamingComponent.updateContent(event.message);
780
798
  this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(event.message));
781
799
  this.streamingToolSeparator = this.appendAssistantSeparator(!hasProse);
782
800
  this.setAssistantSeparatorContent(this.streamingToolSeparator, event.message);
@@ -786,10 +804,10 @@ export class ChatView {
786
804
  case 'message_update': {
787
805
  if (this.streamingComponent && event.message.role === 'assistant') {
788
806
  this.promoteStreamingAssistantToBoundary(event.message);
789
- // Upgrade complete rich-Markdown constructs mid-stream. Incomplete
790
- // Mermaid fences and HTML pairs remain literal until their closing
791
- // marker arrives, then this same settled-message pipeline projects them.
792
- this.updateAssistantComponent(this.streamingComponent, event.message);
807
+ // Incomplete Mermaid fences and HTML pairs stay literal until their
808
+ // closing marker arrives; the transformers upgrade them on the render
809
+ // that follows, with no separate mid-stream path.
810
+ this.streamingComponent.updateContent(event.message);
793
811
  this.registerCopyBlock(this.streamingComponent, 'assistant', assistantVisibleText(event.message));
794
812
  this.historyContainer.markDirty(this.streamingComponent);
795
813
  const toolCalls = this.assistantToolCalls(event.message);
@@ -842,8 +860,7 @@ export class ChatView {
842
860
  // updateContent) so the rendered message shows the annotation.
843
861
  event.message.errorMessage = errorMessage;
844
862
  }
845
- // Project every supported rich-Markdown construct in the settled reply.
846
- this.updateAssistantComponent(this.streamingComponent, event.message);
863
+ this.streamingComponent.updateContent(event.message);
847
864
  if (stopReason === 'aborted' || stopReason === 'error') {
848
865
  const text = errorMessage ?? 'Error';
849
866
  for (const component of this.pendingTools.values()) {
@@ -1111,18 +1128,20 @@ export class ChatView {
1111
1128
  this.appendUserPart(new Spacer(1), !isTrueUserMessage(message));
1112
1129
  const skillBlock = parseSkillBlock(textContent);
1113
1130
  if (skillBlock) {
1131
+ // SkillInvocationMessageComponent has no transformer seam, so its body
1132
+ // is still styled before construction.
1114
1133
  const component = new SkillInvocationMessageComponent({ ...skillBlock, content: styleAttachMarkdownSource(skillBlock.content) }, attachMarkdownTheme());
1115
1134
  component.setExpanded(this.toolOutputExpanded);
1116
1135
  this.appendUserPart(component, !isTrueUserMessage(message));
1117
1136
  this.registerCopyBlock(component, 'user', textContent);
1118
1137
  if (skillBlock.userMessage) {
1119
- const userMessage = new UserMessageComponent(styleAttachMarkdownSource(skillBlock.userMessage), attachMarkdownTheme());
1138
+ const userMessage = new UserMessageComponent(skillBlock.userMessage, attachMarkdownTheme(), undefined, this.markdownTransformers);
1120
1139
  this.appendUserPart(userMessage, !isTrueUserMessage(message));
1121
1140
  this.registerCopyBlock(userMessage, 'user', skillBlock.userMessage);
1122
1141
  }
1123
1142
  }
1124
1143
  else {
1125
- const component = new UserMessageComponent(styleAttachMarkdownSource(textContent), attachMarkdownTheme());
1144
+ const component = new UserMessageComponent(textContent, attachMarkdownTheme(), undefined, this.markdownTransformers);
1126
1145
  this.appendUserPart(component, !isTrueUserMessage(message));
1127
1146
  this.registerCopyBlock(component, 'user', textContent);
1128
1147
  }
@@ -1145,8 +1164,8 @@ export class ChatView {
1145
1164
  // -------------------------------------------------------------------------
1146
1165
  /** Render an assistant reply plus its initially-hidden boundary separator. */
1147
1166
  appendAssistantMessage(message, groupMember = false) {
1148
- const component = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel);
1149
- this.updateAssistantComponent(component, message);
1167
+ const component = new ViewerAssistantMessageComponent(undefined, this.hideThinking, attachMarkdownTheme(), this.hiddenThinkingLabel, this.markdownTransformers);
1168
+ component.updateContent(message);
1150
1169
  this.appendAssistantPart(component, groupMember);
1151
1170
  this.registerCopyBlock(component, 'assistant', assistantVisibleText(message));
1152
1171
  const separator = this.appendAssistantSeparator(groupMember);
@@ -1380,39 +1399,6 @@ export class ChatView {
1380
1399
  append(child) {
1381
1400
  this.appendTarget.addChild(child);
1382
1401
  }
1383
- /** Project one raw assistant message through every attach-local Markdown
1384
- * transform. This is the sole pipeline used by snapshots, streaming, settled
1385
- * messages, and Ctrl+O re-renders. */
1386
- renderAssistantMessage(message) {
1387
- const withDiagrams = transformMessageDiagrams(message, this.renderWidth());
1388
- const withHtml = transformMessageHtml(withDiagrams, {
1389
- detailsExpanded: this.detailsExpandedOverride,
1390
- styles: {
1391
- kbd: (text) => this.recapPalette.active(this.recapPalette.bold(`‹${text}›`)),
1392
- mark: (text) => `\x1b[7m${this.recapPalette.bold(this.recapPalette.accent(text))}\x1b[27m`,
1393
- hint: this.dimStyle,
1394
- },
1395
- });
1396
- return styleAttachMessageMarkdown(withHtml);
1397
- }
1398
- /** Retain canonical source and update one stable transcript component with its
1399
- * viewer-local projection. */
1400
- updateAssistantComponent(component, message) {
1401
- this.assistantSources.set(component, message);
1402
- component.updateContent(this.renderAssistantMessage(message));
1403
- }
1404
- /** Re-project every assistant component in place after a disclosure toggle. */
1405
- rerenderAssistantMessages() {
1406
- for (const [component, source] of this.assistantSources) {
1407
- component.updateContent(this.renderAssistantMessage(source));
1408
- }
1409
- }
1410
- /** Approximate render width for inline mermaid diagrams. Only affects
1411
- * termrender's trailing padding (stripped in the transform), so the terminal
1412
- * column count is close enough without threading the live pane width here. */
1413
- renderWidth() {
1414
- return Math.max(40, process.stdout.columns ?? 80);
1415
- }
1416
1402
  /** Number of rendered conversation components in the container currently being
1417
1403
  * appended to (live history or the nested condensed region). */
1418
1404
  chatChildCount() {
@@ -1426,7 +1412,6 @@ export class ChatView {
1426
1412
  this.streamingToolSeparator = undefined;
1427
1413
  this.streamingWasGroupMember = false;
1428
1414
  this.foldedToolSeparatorContents.clear();
1429
- this.assistantSources.clear();
1430
1415
  this.groups.length = 0;
1431
1416
  this.openGroup = undefined;
1432
1417
  this.groupSummaries.clear();
@@ -2,13 +2,3 @@
2
2
  export declare function textHasDiagram(text: string): boolean;
3
3
  /** Replace every supported complete fence in `text` with its rendered diagram. */
4
4
  export declare function transformDiagramFences(text: string, width: number): string;
5
- /** True when any text part of an assistant message carries a supported fence. */
6
- export declare function messageHasDiagram(message: {
7
- content?: unknown;
8
- }): boolean;
9
- /** Return a shallow clone of an assistant message with every supported fence
10
- * rendered inline. Non-text parts (thinking, toolCall) pass through untouched,
11
- * and a message without a supported fence keeps the same reference. */
12
- export declare function transformMessageDiagrams<T extends {
13
- content?: unknown;
14
- }>(message: T, width: number): T;
@@ -101,35 +101,3 @@ export function transformDiagramFences(text, width) {
101
101
  return source === null ? whole : renderDiagram(whole, source, width);
102
102
  });
103
103
  }
104
- /** True when any text part of an assistant message carries a supported fence. */
105
- export function messageHasDiagram(message) {
106
- const content = message.content;
107
- if (typeof content === 'string')
108
- return textHasDiagram(content);
109
- if (!Array.isArray(content))
110
- return false;
111
- return content.some((p) => p?.type === 'text' && textHasDiagram(p.text ?? ''));
112
- }
113
- /** Return a shallow clone of an assistant message with every supported fence
114
- * rendered inline. Non-text parts (thinking, toolCall) pass through untouched,
115
- * and a message without a supported fence keeps the same reference. */
116
- export function transformMessageDiagrams(message, width) {
117
- const content = message.content;
118
- if (typeof content === 'string') {
119
- if (!textHasDiagram(content))
120
- return message;
121
- return { ...message, content: transformDiagramFences(content, width) };
122
- }
123
- if (!Array.isArray(content) || !messageHasDiagram(message))
124
- return message;
125
- return {
126
- ...message,
127
- content: content.map((part) => {
128
- const p = part;
129
- if (p?.type === 'text' && typeof p.text === 'string' && textHasDiagram(p.text)) {
130
- return { ...p, text: transformDiagramFences(p.text, width) };
131
- }
132
- return part;
133
- }),
134
- };
135
- }
@@ -11,7 +11,3 @@ export interface HtmlMarkdownOptions {
11
11
  }
12
12
  /** Transform the strict supported HTML subset in one Markdown string. */
13
13
  export declare function transformHtmlMarkdown(source: string, options: HtmlMarkdownOptions): string;
14
- /** Copy an assistant message for terminal rendering, enhancing only text parts. */
15
- export declare function transformMessageHtml<T extends {
16
- content?: unknown;
17
- }>(message: T, options: HtmlMarkdownOptions): T;
@@ -204,25 +204,3 @@ export function transformHtmlMarkdown(source, options) {
204
204
  out += transformInlineHtml(masked.slice(cursor), options.styles);
205
205
  return protectedMarkdown.restore(out);
206
206
  }
207
- /** Copy an assistant message for terminal rendering, enhancing only text parts. */
208
- export function transformMessageHtml(message, options) {
209
- const content = message.content;
210
- if (typeof content === 'string') {
211
- const transformed = transformHtmlMarkdown(content, options);
212
- return transformed === content ? message : { ...message, content: transformed };
213
- }
214
- if (!Array.isArray(content))
215
- return message;
216
- let changed = false;
217
- const next = content.map((part) => {
218
- const candidate = part;
219
- if (candidate?.type !== 'text' || typeof candidate.text !== 'string')
220
- return part;
221
- const transformed = transformHtmlMarkdown(candidate.text, options);
222
- if (transformed === candidate.text)
223
- return part;
224
- changed = true;
225
- return { ...candidate, text: transformed };
226
- });
227
- return changed ? { ...message, content: next } : message;
228
- }
@@ -2,6 +2,11 @@
2
2
  * tag lines. Fenced examples remain literal Markdown, never decorated
3
3
  * terminal output. */
4
4
  export declare function styleAttachMarkdownSource(markdown: string): string;
5
+ /** Every source-text transform the viewer applies to AGENT-authored Markdown,
6
+ * in one place: heading and XML syntax, then glyph expansion. A human's own
7
+ * message keeps `styleAttachMarkdownSource` alone — the `U+XXXX` escape hatch
8
+ * exists for agents that cannot type a Nerd Font character. */
9
+ export declare function styleAttachAgentMarkdown(markdown: string): string;
5
10
  /** Copy a pi message for terminal rendering and decorate only Markdown-bearing
6
11
  * text/thinking blocks. The broker's source event is never mutated. */
7
12
  export declare function styleAttachMessageMarkdown<T>(message: T): T;
@@ -127,9 +127,11 @@ export function styleAttachMarkdownSource(markdown) {
127
127
  }
128
128
  return out.join('\n');
129
129
  }
130
- /** Every source-text transform the viewer applies to a Markdown-bearing block,
131
- * in one place: heading and XML syntax, then glyph expansion. */
132
- function renderableText(markdown) {
130
+ /** Every source-text transform the viewer applies to AGENT-authored Markdown,
131
+ * in one place: heading and XML syntax, then glyph expansion. A human's own
132
+ * message keeps `styleAttachMarkdownSource` alone — the `U+XXXX` escape hatch
133
+ * exists for agents that cannot type a Nerd Font character. */
134
+ export function styleAttachAgentMarkdown(markdown) {
133
135
  return expandGlyphCodepoints(styleAttachMarkdownSource(markdown));
134
136
  }
135
137
  /** Copy a pi message for terminal rendering and decorate only Markdown-bearing
@@ -139,7 +141,7 @@ export function styleAttachMessageMarkdown(message) {
139
141
  return message;
140
142
  const candidate = message;
141
143
  if (typeof candidate.content === 'string') {
142
- return { ...candidate, content: renderableText(candidate.content) };
144
+ return { ...candidate, content: styleAttachAgentMarkdown(candidate.content) };
143
145
  }
144
146
  if (!Array.isArray(candidate.content))
145
147
  return message;
@@ -150,10 +152,10 @@ export function styleAttachMessageMarkdown(message) {
150
152
  return part;
151
153
  const block = part;
152
154
  if (block.type === 'text' && typeof block.text === 'string') {
153
- return { ...block, text: renderableText(block.text) };
155
+ return { ...block, text: styleAttachAgentMarkdown(block.text) };
154
156
  }
155
157
  if (block.type === 'thinking' && typeof block.thinking === 'string') {
156
- return { ...block, thinking: renderableText(block.thinking) };
158
+ return { ...block, thinking: styleAttachAgentMarkdown(block.thinking) };
157
159
  }
158
160
  return part;
159
161
  }),
@@ -161,5 +163,5 @@ export function styleAttachMessageMarkdown(message) {
161
163
  }
162
164
  /** Copy an expanded pi summary without changing its persisted source. */
163
165
  export function styleAttachSummaryMarkdown(message) {
164
- return { ...message, summary: renderableText(message.summary) };
166
+ return { ...message, summary: styleAttachAgentMarkdown(message.summary) };
165
167
  }