@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.103

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 (122) hide show
  1. package/package.json +5 -5
  2. package/src/annotations/annotation-palette.ts +1 -0
  3. package/src/annotations/components/AnnotationCards.tsx +4 -0
  4. package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
  5. package/src/annotations/discussions.ts +1 -0
  6. package/src/annotations/thread-selection.ts +6 -0
  7. package/src/annotations/use-annotation-surface.tsx +97 -0
  8. package/src/app/App.deleted-scroll.test.tsx +3 -0
  9. package/src/app/App.harness-settings.test.tsx +77 -0
  10. package/src/app/App.plan-render.test.tsx +2 -0
  11. package/src/app/App.plan-review.test.tsx +1 -0
  12. package/src/app/App.test.tsx +1 -0
  13. package/src/app/App.tsx +67 -1
  14. package/src/app/App.visual.test.tsx +1 -0
  15. package/src/app/App.walk.test.tsx +1 -0
  16. package/src/app/App.workbench.test.tsx +1 -0
  17. package/src/app/app-screens.tsx +1 -0
  18. package/src/app/app-view-model.ts +7 -0
  19. package/src/app/components/WorkspacePanels.tsx +1 -0
  20. package/src/app/intent-dispatch.test.ts +1 -0
  21. package/src/app/intent-dispatch.ts +26 -0
  22. package/src/app/ready-signal.ts +2 -0
  23. package/src/app/run.ts +2 -0
  24. package/src/app/serve.test.ts +4 -0
  25. package/src/diff/components/AnnotatableFileView.tsx +1 -0
  26. package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
  27. package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
  28. package/src/diff/components/DiffContentView.tsx +25 -0
  29. package/src/diff/components/diff-content-view-layout.ts +3 -0
  30. package/src/diff/diff-fold.ts +4 -0
  31. package/src/diff/diff-hunk-curate.ts +3 -0
  32. package/src/diff/diff-intraline.ts +6 -0
  33. package/src/diff/diff-syntax.ts +8 -0
  34. package/src/diff/split-diff.ts +6 -0
  35. package/src/diff/view-diff.ts +18 -0
  36. package/src/diff/walk.ts +3 -0
  37. package/src/integrations/attribution.ts +2 -0
  38. package/src/integrations/client-extension-registry.ts +4 -0
  39. package/src/integrations/clipboard.ts +1 -0
  40. package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
  41. package/src/integrations/components/ShareDialog.tsx +28 -0
  42. package/src/integrations/editor.ts +3 -0
  43. package/src/integrations/github-identity.test.ts +1 -0
  44. package/src/integrations/github-identity.ts +1 -0
  45. package/src/integrations/herdr.ts +2 -0
  46. package/src/integrations/pasted-image.ts +2 -0
  47. package/src/integrations/share.ts +4 -0
  48. package/src/keyboard/components/HandleChipsInput.tsx +4 -0
  49. package/src/keyboard/components/SlashComposer.tsx +3 -0
  50. package/src/keyboard/key-bindings.ts +3 -0
  51. package/src/keyboard/keymap.ts +51 -0
  52. package/src/keyboard/skills.ts +2 -0
  53. package/src/keyboard/slash-palette.ts +8 -0
  54. package/src/keyboard/thread-chords.ts +5 -0
  55. package/src/keyboard/use-shared-keyboard.ts +1 -0
  56. package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
  57. package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
  58. package/src/markdown/components/ThreadView.tsx +13 -0
  59. package/src/markdown/mark-runs.ts +4 -0
  60. package/src/markdown/markdown-editor-syntax-style.ts +1 -0
  61. package/src/markdown/markdown-grid.ts +6 -0
  62. package/src/markdown/markdown-highlight.ts +1 -0
  63. package/src/markdown/thread-code-syntax.ts +3 -0
  64. package/src/markdown/view-plan.ts +52 -0
  65. package/src/performance/perf-report.ts +1 -0
  66. package/src/performance/perf-timings.ts +1 -0
  67. package/src/prototype/components/PrototypeContentView.tsx +17 -0
  68. package/src/prototype/prototype-browser.ts +11 -0
  69. package/src/settings/components/SettingsDialog.tsx +3 -0
  70. package/src/settings/config.test.ts +41 -0
  71. package/src/settings/config.ts +44 -0
  72. package/src/settings/use-settings-dialog.test.tsx +98 -0
  73. package/src/settings/use-settings-dialog.tsx +38 -1
  74. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  75. package/src/settings/use-thread-harness-setting.ts +86 -0
  76. package/src/stories/stories.test.tsx +4 -0
  77. package/src/stories/story.ts +6 -0
  78. package/src/terminal/ansi-screen.ts +5 -0
  79. package/src/terminal/components/terminal-pane.ts +12 -0
  80. package/src/terminal/ghostty-terminal.ts +10 -0
  81. package/src/terminal/kitty-image.ts +3 -0
  82. package/src/terminal/pty.ts +9 -0
  83. package/src/testing/test-support.test.ts +2 -0
  84. package/src/testing/test-support.ts +16 -0
  85. package/src/thread/agent-continuation.test.ts +2 -0
  86. package/src/thread/agent-transcript.ts +7 -0
  87. package/src/thread/components/AgentConfigControls.tsx +1 -0
  88. package/src/thread/components/AgentThreadPane.test.tsx +31 -0
  89. package/src/thread/components/AgentThreadPane.tsx +12 -0
  90. package/src/thread/components/ThreadTree.tsx +1 -0
  91. package/src/thread/components/thread-tree.test.ts +1 -0
  92. package/src/thread/thread-controller.curate.test.ts +2 -0
  93. package/src/thread/thread-controller.ts +123 -0
  94. package/src/thread/tree-view.ts +4 -0
  95. package/src/thread/use-thread-agent.test.tsx +1 -0
  96. package/src/thread/use-thread-agent.ts +5 -0
  97. package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
  98. package/src/ui/components/OverlayScrollbar.tsx +5 -0
  99. package/src/ui/components/PromptDialog.tsx +2 -0
  100. package/src/ui/components/RootOverlay.tsx +1 -0
  101. package/src/ui/components/ScrollArea.tsx +2 -0
  102. package/src/ui/components/primitives/Button.tsx +1 -0
  103. package/src/ui/components/primitives/Tabs.tsx +1 -0
  104. package/src/ui/components/primitives/Tree.tsx +4 -0
  105. package/src/ui/components/primitives/tree-model.ts +7 -0
  106. package/src/ui/components/syntax-highlight.ts +5 -0
  107. package/src/ui/components/truncate-title.ts +1 -0
  108. package/src/ui/use-frame-measure.ts +3 -0
  109. package/src/ui/use-terminal-virtualizer.ts +6 -0
  110. package/src/workbench/components/ChangesColumn.tsx +7 -0
  111. package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
  112. package/src/workbench/components/EditorGrid.tsx +8 -0
  113. package/src/workbench/components/FileContentsView.tsx +2 -0
  114. package/src/workbench/components/GridTabContent.tsx +2 -0
  115. package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
  116. package/src/workbench/components/ProjectTreeView.tsx +8 -0
  117. package/src/workbench/components/TreePane.tsx +2 -0
  118. package/src/workbench/components/editor-grid.ts +3 -0
  119. package/src/workbench/components/file-tree.ts +2 -0
  120. package/src/workbench/use-changes-workbench.ts +5 -0
  121. package/src/workbench/use-remember-layout.ts +2 -0
  122. package/src/workbench/use-repo-changes.ts +2 -0
@@ -98,6 +98,7 @@ function highlightedByRow(): Map<number, string> {
98
98
 
99
99
  if (backdrop || (caretCell && underlined)) marked += span.text;
100
100
  }
101
+
101
102
  if (marked.length > 0) rows.set(rowIndex, marked);
102
103
  });
103
104
 
@@ -121,6 +122,7 @@ function caretCells(): Array<{ row: number; column: number; text: string }> {
121
122
  if (red === CARET_RGB[0] && green === CARET_RGB[1] && blue === CARET_RGB[2]) {
122
123
  cells.push({ row, column, text: span.text });
123
124
  }
125
+
124
126
  column += span.width;
125
127
  }
126
128
  });
@@ -158,6 +160,7 @@ async function framesUntilHighlight(expected: string, limit = 4): Promise<number
158
160
  // one scheduler turn (the input parser and React commit), then one frame
159
161
  await new Promise((resolve) => setTimeout(resolve, 0));
160
162
  await setup.renderOnce();
163
+
161
164
  if (highlightedText() === expected) return frame;
162
165
  }
163
166
 
@@ -103,7 +103,9 @@ function blockStyle(block: DisplayBlock, tokens: Theme): BlockStyle {
103
103
  /** The read-only color of an inline markdown role: links accent, code and removals dim, insertions green. */
104
104
  function roleForeground(role: RunRole, baseFg: string, tokens: Theme): string {
105
105
  if (role === "link") return tokens.blue;
106
+
106
107
  if (role === "ins") return tokens.green;
108
+
107
109
  if (role === "code" || role === "del") return tokens.textDim;
108
110
 
109
111
  return baseFg;
@@ -112,7 +114,9 @@ function roleForeground(role: RunRole, baseFg: string, tokens: Theme): string {
112
114
  /** The text attributes an inline role adds over a block's base attributes. */
113
115
  function roleAttributes(role: RunRole, baseAttributes: number): number {
114
116
  if (role === "strong") return baseAttributes | BOLD;
117
+
115
118
  if (role === "em") return baseAttributes | ITALIC;
119
+
116
120
  if (role === "strike" || role === "del") return baseAttributes | CUT;
117
121
 
118
122
  return baseAttributes;
@@ -288,6 +292,7 @@ export function ThreadView({
288
292
  const block = display[index];
289
293
 
290
294
  if (!block) return 1;
295
+
291
296
  const usable = viewWidth > 0 ? Math.max(1, viewWidth - 6) : 40;
292
297
  const lines = Math.max(1, Math.ceil(displayText(block).length / usable));
293
298
 
@@ -378,6 +383,7 @@ export function ThreadView({
378
383
  // measurement never adds a row and shifts the blocks below it out from under a pending click
379
384
  const headingRule = (block: DisplayBlock): React.ReactNode => {
380
385
  if (block.kind !== "h1" && block.kind !== "h2") return null;
386
+
381
387
  const width = Math.max(1, (viewWidth > 6 ? viewWidth : 46) - 6);
382
388
 
383
389
  return (
@@ -398,6 +404,7 @@ export function ThreadView({
398
404
  {custom}
399
405
  </box>
400
406
  );
407
+
401
408
  const block = display[blockIndex]!;
402
409
  const { baseFg, baseAttributes, marker } = blockStyle(block, tokens);
403
410
  // list items of one list stay tight; every other block sits a blank row below its neighbour
@@ -472,8 +479,11 @@ export function ThreadView({
472
479
  cards.length > 0,
473
480
  ),
474
481
  );
482
+
475
483
  if (isLastLine) lineRows.push(headingRule(block));
484
+
476
485
  lineRows.push(...cards);
486
+
477
487
  if (cards.length > 0 && !isLastLine) {
478
488
  lineRows.push(<box key={`gap-${lineIndex}`} style={{ height: 1 }} />);
479
489
  }
@@ -508,14 +518,17 @@ export function ThreadView({
508
518
  for (let index = 0; index < display.length; index++) {
509
519
  nodes.push(blockNodeFor(index));
510
520
  mountedRows += estimateBlockRows(index);
521
+
511
522
  if (mountedRows > terminalHeight + OVERSCAN_BLOCKS) break;
512
523
  }
513
524
 
514
525
  return nodes;
515
526
  }
527
+
516
528
  if (firstItem.start > 0) {
517
529
  nodes.push(<box key="spacer-above" style={{ height: firstItem.start }} />);
518
530
  }
531
+
519
532
  for (const item of virtual.items) nodes.push(blockNodeFor(item.index));
520
533
  const below = virtual.totalSize - lastItem.end;
521
534
 
@@ -36,6 +36,7 @@ export function runsFor(text: string, ranges: MarkRange[]): Run[] {
36
36
  const end = edges[index + 1]!;
37
37
 
38
38
  if (end <= start) continue;
39
+
39
40
  const covering = ranges.filter((range) => range.start <= start && end <= range.end);
40
41
 
41
42
  // the caret cell shows through a mark, so the head of a selection is visible
@@ -84,6 +85,7 @@ export function wrapLines(text: string, width: number): VisualLine[] {
84
85
  lines.push({ start: lineStart, end: lineEnd });
85
86
  lineStart = wordStart;
86
87
  }
88
+
87
89
  // a token wider than the line (a long identifier, a URL) breaks at the cell edge, so
88
90
  // every visual line stays one terminal row and nothing is clipped away
89
91
  while (wordEnd - lineStart > width) {
@@ -94,6 +96,7 @@ export function wrapLines(text: string, width: number): VisualLine[] {
94
96
  }
95
97
  lines.push({ start: lineStart, end: Math.max(lineEnd, lineStart) });
96
98
  }
99
+
97
100
  segmentStart = segmentEnd + 1;
98
101
  }
99
102
 
@@ -114,6 +117,7 @@ export function lineMarkRanges(ranges: MarkRange[], line: VisualLine): MarkRange
114
117
  /** Printable input that should reach a composer: one key, or a whole pasted run in one event. */
115
118
  export function printableSequence(key: KeyEvent): string | null {
116
119
  if (!key.sequence || key.ctrl || key.meta) return null;
120
+
117
121
  // reject any run carrying a control byte, so escape sequences and \r\t\b never seed a draft
118
122
  for (const character of key.sequence) if (character < " ") return null;
119
123
 
@@ -32,6 +32,7 @@ export function markdownEditorStyle(theme: Theme): MarkdownEditorStyle {
32
32
  const cached = markdownEditorStyleCache.get(theme);
33
33
 
34
34
  if (cached) return cached;
35
+
35
36
  const groupStyles = markdownGroupStyles(theme);
36
37
  const style = SyntaxStyle.fromStyles(groupStyles);
37
38
  const styleIds = new Map<MarkdownHighlightGroup, number>();
@@ -52,7 +52,9 @@ function splitTableRow(line: string): string[] {
52
52
  }
53
53
  }
54
54
  parts.push(cell);
55
+
55
56
  if (parts.length > 1 && parts[0] === "") parts.shift();
57
+
56
58
  if (parts.length > 1 && parts[parts.length - 1] === "") parts.pop();
57
59
 
58
60
  return parts.map((part) => part.trim());
@@ -78,6 +80,7 @@ function pad(text: string, width: number, align: ColumnAlign): string {
78
80
  const slack = Math.max(0, width - text.length);
79
81
 
80
82
  if (align === "right") return " ".repeat(slack) + text;
83
+
81
84
  if (align === "center") {
82
85
  const left = Math.floor(slack / 2);
83
86
 
@@ -132,6 +135,7 @@ export function parseFrontmatterRows(text: string): FrontmatterRow[] {
132
135
 
133
136
  for (const line of text.split("\n")) {
134
137
  if (line.trim() === "") continue;
138
+
135
139
  const match = /^([^:\s][^:]*):\s?(.*)$/.exec(line);
136
140
 
137
141
  if (match) {
@@ -163,6 +167,7 @@ function wrapCell(value: string, width: number): string[] {
163
167
  */
164
168
  export function layoutFrontmatterGrid(rows: FrontmatterRow[], maxWidth: number): GridLine[] {
165
169
  if (rows.length === 0) return [];
170
+
166
171
  const keyWidth = Math.max(3, ...rows.map((row) => row.key.length));
167
172
  // size the value column to its content, capped at the width left after the key column and borders,
168
173
  // so a short frontmatter table stays compact and a long value wraps instead of overflowing
@@ -183,6 +188,7 @@ export function layoutFrontmatterGrid(rows: FrontmatterRow[], maxWidth: number):
183
188
 
184
189
  rows.forEach((row, rowIndex) => {
185
190
  if (rowIndex > 0) lines.push(rule("├", "┼", "┤"));
191
+
186
192
  const valueLines = wrapCell(row.value, valueWidth);
187
193
 
188
194
  valueLines.forEach((valueLine, valueIndex) => {
@@ -50,6 +50,7 @@ function scanInlineTokens(text: string, base: number, into: MarkdownHighlightRan
50
50
  into.push({ start, end, group: "code" });
51
51
  continue;
52
52
  }
53
+
53
54
  // a link colors its brackets and href; the [] label stays plain unless it is an @-scope, matching how a
54
55
  // markdown source view paints links
55
56
  const closeBracket = token.indexOf("]");
@@ -14,6 +14,7 @@ export async function highlightThreadCodeBlocks(
14
14
  ): Promise<Map<number, SyntaxSpan[]>> {
15
15
  const jobs = display.flatMap((block, blockIndex) => {
16
16
  if (block.kind !== "code") return [];
17
+
17
18
  const filetype = filetypeFor(block.work?.lang ?? block.base?.lang);
18
19
 
19
20
  return filetype ? [{ blockIndex, filetype, source: renderedText(block) }] : [];
@@ -21,6 +22,7 @@ export async function highlightThreadCodeBlocks(
21
22
  const byBlock = new Map<number, SyntaxSpan[]>();
22
23
 
23
24
  if (jobs.length === 0) return byBlock;
25
+
24
26
  const client = getTreeSitterClient();
25
27
 
26
28
  await client.initialize();
@@ -28,6 +30,7 @@ export async function highlightThreadCodeBlocks(
28
30
  const result = await client.highlightOnce(job.source, job.filetype);
29
31
 
30
32
  if (!result.highlights) continue;
33
+
31
34
  const spans: SyntaxSpan[] = [];
32
35
  let offset = 0;
33
36
  const lines = job.source.split("\n");
@@ -61,6 +61,7 @@ function singleRangeTextChanges(
61
61
  newText: string,
62
62
  ): ReturnType<typeof wordLevelChanges> | null {
63
63
  if (oldText === newText || oldText.length === newText.length) return null;
64
+
64
65
  let prefixLength = 0;
65
66
 
66
67
  while (
@@ -77,7 +78,9 @@ function singleRangeTextChanges(
77
78
  oldText[oldText.length - 1 - suffixLength] === newText[newText.length - 1 - suffixLength]
78
79
  )
79
80
  suffixLength++;
81
+
80
82
  if (prefixLength + suffixLength !== shorterLength) return null;
83
+
81
84
  const commonPrefix = oldText.slice(0, prefixLength);
82
85
  const commonSuffix = oldText.slice(oldText.length - suffixLength);
83
86
  const changed =
@@ -101,8 +104,11 @@ function singleRangeTextChanges(
101
104
  /** Word-overlap similarity gate: an unrelated cut + insert must not merge. */
102
105
  function similar(baseBlock: Block, workBlock: Block, expectedText?: string): boolean {
103
106
  if (baseBlock.kind !== workBlock.kind) return false;
107
+
104
108
  if (expectedText === workBlock.text) return true;
109
+
105
110
  if (singleRangeTextChanges(baseBlock.text, workBlock.text)) return true;
111
+
106
112
  const words = (text: string) => new Set(text.toLowerCase().split(/\s+/).filter(Boolean));
107
113
  const baseWords = words(baseBlock.text);
108
114
  const workWords = words(workBlock.text);
@@ -132,6 +138,7 @@ export function buildDisplay(
132
138
  const segments = sourceSegments[blockIndex];
133
139
 
134
140
  if (!segments?.length) return;
141
+
135
142
  const sourceStart = segments[0]!.sourceStart;
136
143
  const last = segments.at(-1)!;
137
144
  const sourceEnd = last.sourceStart + last.textEnd - last.textStart;
@@ -143,6 +150,7 @@ export function buildDisplay(
143
150
  const cut = exactCuts[index]!;
144
151
 
145
152
  if (cut.start >= sourceEnd) break;
153
+
146
154
  const start = blockTextBoundary(segments, block.text.length, cut.start);
147
155
  const end = blockTextBoundary(segments, block.text.length, cut.end);
148
156
 
@@ -153,6 +161,7 @@ export function buildDisplay(
153
161
  };
154
162
 
155
163
  if (ranges.length > 0) data.textCutRanges = ranges;
164
+
156
165
  cutData.set(block, data);
157
166
  });
158
167
  const blockCutData = (block: Block) => cutData.get(block) ?? {};
@@ -160,6 +169,7 @@ export function buildDisplay(
160
169
  const data = blockCutData(block);
161
170
 
162
171
  if (!data.textCutRanges) return undefined;
172
+
163
173
  let text = block.text;
164
174
 
165
175
  for (let index = data.textCutRanges.length - 1; index >= 0; index--) {
@@ -178,6 +188,7 @@ export function buildDisplay(
178
188
  if (!data.textCutRanges) {
179
189
  return { type: "same", kind: base.kind, base, work: base, ...data };
180
190
  }
191
+
181
192
  const text = expectedBlockText(base)!;
182
193
 
183
194
  return {
@@ -207,6 +218,7 @@ export function buildDisplay(
207
218
 
208
219
  return display;
209
220
  }
221
+
210
222
  const workBlocks = parseBlocks(workingContent);
211
223
  const signature = (block: Block) => block.kind + "\0" + block.text;
212
224
  const ops = lcsDiff(
@@ -225,6 +237,7 @@ export function buildDisplay(
225
237
  opIndex++;
226
238
  continue;
227
239
  }
240
+
228
241
  const deletedBlocks: Block[] = [];
229
242
  const addedBlocks: Block[] = [];
230
243
 
@@ -233,6 +246,7 @@ export function buildDisplay(
233
246
 
234
247
  if (changeOp.kind === "del") deletedBlocks.push(changeOp.oldValue!);
235
248
  else addedBlocks.push(changeOp.newValue!);
249
+
236
250
  opIndex++;
237
251
  }
238
252
  let deletedIndex = 0;
@@ -309,6 +323,7 @@ function blockTextBoundary(
309
323
  const sourceEnd = segment.sourceStart + segment.textEnd - segment.textStart;
310
324
 
311
325
  if (sourceOffset < segment.sourceStart) return segment.textStart;
326
+
312
327
  if (sourceOffset <= sourceEnd) return segment.textStart + sourceOffset - segment.sourceStart;
313
328
  }
314
329
 
@@ -377,13 +392,16 @@ export function marksByDisplay(
377
392
  for (const annotation of annotations) {
378
393
  // an addressed annotation keeps its record but paints no highlight
379
394
  if (isAddressed(annotation)) continue;
395
+
380
396
  const resolved = resolveAnchor(annotation.anchor, workBlocks);
381
397
 
382
398
  if (!resolved) continue;
399
+
383
400
  const startEntry = workEntries[resolved.blockIndex];
384
401
  const endEntry = workEntries[resolved.endBlockIndex];
385
402
 
386
403
  if (!startEntry || !endEntry) continue;
404
+
387
405
  // resolveAnchor works in the block's work text; the surface paints and hit-tests in rendered
388
406
  // text (markers concealed), so map the span onto the visible characters it covers
389
407
  const renderedStart = renderedOffsetAtOrAfter(
@@ -396,6 +414,7 @@ export function marksByDisplay(
396
414
  );
397
415
 
398
416
  if (renderedStart === null || renderedEndInclusive === null) continue;
417
+
399
418
  const span: TextSpan = {
400
419
  start: { blockIndex: startEntry.displayIndex, char: renderedStart },
401
420
  end: { blockIndex: endEntry.displayIndex, char: renderedEndInclusive + 1 },
@@ -407,6 +426,7 @@ export function marksByDisplay(
407
426
  const range = spanRangeInBlock(span, entry.displayIndex, renderedText(entry.block).length);
408
427
 
409
428
  if (!range) continue;
429
+
410
430
  const marks = marksByIndex.get(entry.displayIndex) ?? [];
411
431
 
412
432
  marks.push({
@@ -425,6 +445,7 @@ export function marksByDisplay(
425
445
 
426
446
  function exactCutRuns(block: DisplayBlock, literal: boolean): StyleRun[] | null {
427
447
  if (!block.base || !block.work || !block.textCutRanges) return null;
448
+
428
449
  const localCuts = block.textCutRanges;
429
450
  const expected = localCuts.reduceRight(
430
451
  (text, cut) => text.slice(0, cut.start) + text.slice(cut.end),
@@ -432,6 +453,7 @@ function exactCutRuns(block: DisplayBlock, literal: boolean): StyleRun[] | null
432
453
  );
433
454
 
434
455
  if (expected !== block.work.text) return null;
456
+
435
457
  const sourceRuns = literal
436
458
  ? [{ text: block.base.text, role: "plain" as const, start: 0 }]
437
459
  : inlineStyleRuns(block.base.text, 0);
@@ -441,6 +463,7 @@ function exactCutRuns(block: DisplayBlock, literal: boolean): StyleRun[] | null
441
463
 
442
464
  for (const sourceRun of sourceRuns) {
443
465
  if (sourceRun.start === null) continue;
466
+
444
467
  const runStart = sourceRun.start;
445
468
  const runEnd = runStart + sourceRun.text.length;
446
469
  let position = runStart;
@@ -461,6 +484,7 @@ function exactCutRuns(block: DisplayBlock, literal: boolean): StyleRun[] | null
461
484
  });
462
485
  break;
463
486
  }
487
+
464
488
  if (position < cut.start) {
465
489
  const commonEnd = Math.min(runEnd, cut.start);
466
490
 
@@ -473,6 +497,7 @@ function exactCutRuns(block: DisplayBlock, literal: boolean): StyleRun[] | null
473
497
  position = commonEnd;
474
498
  continue;
475
499
  }
500
+
476
501
  const removedEnd = Math.min(runEnd, cut.end);
477
502
 
478
503
  runs.push({
@@ -482,6 +507,7 @@ function exactCutRuns(block: DisplayBlock, literal: boolean): StyleRun[] | null
482
507
  baseStart: position,
483
508
  });
484
509
  position = removedEnd;
510
+
485
511
  if (position === cut.end) {
486
512
  deletedBefore += cut.end - cut.start;
487
513
  cutIndex++;
@@ -518,6 +544,7 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
518
544
  runs.push({ text: change.text, role: "del", start: null });
519
545
  continue;
520
546
  }
547
+
521
548
  // A code/rule block stays literal; prose segments inline-tokenize so their
522
549
  // markers conceal while their work offsets stay intact. Added text keeps
523
550
  // the green diff role (it wins over emphasis); context text shows emphasis.
@@ -533,6 +560,7 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
533
560
 
534
561
  return runs;
535
562
  }
563
+
536
564
  const text = displayText(block);
537
565
 
538
566
  // Prose carries inline markup; code fences and rules are literal. Concealed
@@ -550,6 +578,7 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
550
578
  * document could otherwise splice into the terminal escape stream. */
551
579
  export function safeLinkHref(href: string | undefined): string | undefined {
552
580
  if (href === undefined) return undefined;
581
+
553
582
  if (!/^(https?:|mailto:)/i.test(href)) return undefined;
554
583
 
555
584
  return /^[\x21-\x7e]+$/.test(href) ? href : undefined;
@@ -565,6 +594,7 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
565
594
 
566
595
  for (const run of inlineRuns(text)) {
567
596
  if (run.role === "marker" || run.start === null) continue;
597
+
568
598
  const styleRun: StyleRun = {
569
599
  text: run.text,
570
600
  role: run.role === "text" ? "plain" : run.role,
@@ -572,6 +602,7 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
572
602
  };
573
603
 
574
604
  if (run.href !== undefined) styleRun.href = run.href;
605
+
575
606
  runs.push(styleRun);
576
607
  }
577
608
 
@@ -595,6 +626,7 @@ export function renderedStyleRuns(block: DisplayBlock): RenderedRun[] {
595
626
  const positioned: RenderedRun = { text: run.text, role: run.role, start: rendered };
596
627
 
597
628
  if (run.href !== undefined) positioned.href = run.href;
629
+
598
630
  runs.push(positioned);
599
631
  rendered += run.text.length;
600
632
  }
@@ -619,6 +651,7 @@ function addSyntaxBoundaries(
619
651
  ): void {
620
652
  for (const span of syntaxSpans) {
621
653
  if (span.start > line.start && span.start < line.end) cuts.add(span.start);
654
+
622
655
  if (span.end > line.start && span.end < line.end) cuts.add(span.end);
623
656
  }
624
657
  }
@@ -647,6 +680,7 @@ export function styledRunsFor(
647
680
 
648
681
  for (const run of roleRuns) {
649
682
  if (run.start > line.start && run.start < line.end) cuts.add(run.start);
683
+
650
684
  const runEnd = run.start + run.text.length;
651
685
 
652
686
  if (runEnd > line.start && runEnd < line.end) cuts.add(runEnd);
@@ -669,6 +703,7 @@ export function styledRunsFor(
669
703
  const end = edges[index + 1]!;
670
704
 
671
705
  if (end <= start) continue;
706
+
672
707
  const roleRun = roleRuns.find(
673
708
  (candidate) => candidate.start <= start && start < candidate.start + candidate.text.length,
674
709
  );
@@ -681,9 +716,11 @@ export function styledRunsFor(
681
716
  };
682
717
 
683
718
  if (roleRun?.href !== undefined) styled.href = roleRun.href;
719
+
684
720
  const syntaxGroup = syntaxGroupForRun(syntaxSpans, start, end);
685
721
 
686
722
  if (syntaxGroup) styled.syntaxGroup = syntaxGroup;
723
+
687
724
  runs.push(styled);
688
725
  }
689
726
 
@@ -693,6 +730,7 @@ export function styledRunsFor(
693
730
  /** Split runs at mark boundaries; marks only bind to runs with offsets. */
694
731
  export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
695
732
  if (!marks.length) return runs;
733
+
696
734
  const splitRuns: StyleRun[] = [];
697
735
 
698
736
  for (const run of runs) {
@@ -700,6 +738,7 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
700
738
  splitRuns.push(run);
701
739
  continue;
702
740
  }
741
+
703
742
  const bounds = new Set<number>([0, run.text.length]);
704
743
 
705
744
  for (const mark of marks) {
@@ -719,6 +758,7 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
719
758
  const sliceEnd = boundaries[boundaryIndex + 1]!;
720
759
 
721
760
  if (sliceStart >= sliceEnd) continue;
761
+
722
762
  const absoluteStart = runStart + sliceStart;
723
763
  const mark = marks.find(
724
764
  (candidate) => candidate.start <= absoluteStart && runStart + sliceEnd <= candidate.end,
@@ -751,6 +791,7 @@ export function renderedOffsetFor(runs: StyleRun[], workOffset: number): number
751
791
  if (run.start !== null && workOffset >= run.start && workOffset < run.start + run.text.length) {
752
792
  return rendered + (workOffset - run.start);
753
793
  }
794
+
754
795
  rendered += run.text.length;
755
796
  }
756
797
 
@@ -770,6 +811,7 @@ export function renderedOffsetAtOrAfter(runs: StyleRun[], workOffset: number): n
770
811
  if (run.start !== null && run.start + run.text.length > workOffset) {
771
812
  return rendered + Math.max(0, workOffset - run.start);
772
813
  }
814
+
773
815
  rendered += run.text.length;
774
816
  }
775
817
 
@@ -788,6 +830,7 @@ export function renderedOffsetAtOrBefore(runs: StyleRun[], workOffset: number):
788
830
  if (run.start !== null && run.start <= workOffset) {
789
831
  best = rendered + Math.min(workOffset - run.start, run.text.length - 1);
790
832
  }
833
+
791
834
  rendered += run.text.length;
792
835
  }
793
836
 
@@ -812,11 +855,14 @@ export function workRangeForRendered(
812
855
  const runRenderedEnd = rendered + run.text.length;
813
856
 
814
857
  rendered = runRenderedEnd;
858
+
815
859
  if (run.start === null) continue;
860
+
816
861
  const overlapStart = Math.max(renderedStart, runRenderedStart);
817
862
  const overlapEnd = Math.min(renderedEnd, runRenderedEnd);
818
863
 
819
864
  if (overlapEnd <= overlapStart) continue;
865
+
820
866
  const workStart = run.start + (overlapStart - runRenderedStart);
821
867
  const workEnd = run.start + (overlapEnd - runRenderedStart);
822
868
 
@@ -844,11 +890,14 @@ export function baseRangeForRendered(
844
890
  const runRenderedEnd = rendered + run.text.length;
845
891
 
846
892
  rendered = runRenderedEnd;
893
+
847
894
  if (run.baseStart === undefined) continue;
895
+
848
896
  const overlapStart = Math.max(renderedStart, runRenderedStart);
849
897
  const overlapEnd = Math.min(renderedEnd, runRenderedEnd);
850
898
 
851
899
  if (overlapEnd <= overlapStart) continue;
900
+
852
901
  const baseStart = run.baseStart + overlapStart - runRenderedStart;
853
902
  const baseEnd = run.baseStart + overlapEnd - runRenderedStart;
854
903
 
@@ -883,6 +932,7 @@ export function renderedSpanToWork(
883
932
 
884
933
  return range ?? { start: renderedStart, end: renderedEnd };
885
934
  }
935
+
886
936
  const startRuns = blockRuns(startBlock, true);
887
937
  const startRange = workRangeForRendered(
888
938
  startRuns,
@@ -952,6 +1002,7 @@ export function spanFromRange(
952
1002
  const wordIndex = words.findIndex((word) => word[1] > start);
953
1003
 
954
1004
  if (wordIndex === -1) return null;
1005
+
955
1006
  let wordEnd = wordIndex;
956
1007
 
957
1008
  for (let index = wordIndex; index < words.length && words[index]![0] < end; index++) {
@@ -989,6 +1040,7 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
989
1040
  nextSpan.wordEnd = width;
990
1041
  nextSpan.wordIndex = 0;
991
1042
  } else return span;
1043
+
992
1044
  nextSpan.start = words[nextSpan.wordIndex]![0];
993
1045
  nextSpan.end = words[nextSpan.wordEnd]![1];
994
1046
 
@@ -14,5 +14,6 @@ export function reportPerfMarks(scope: string): void {
14
14
  if (marks.length === 0) return;
15
15
 
16
16
  if (process.env.CUELOOP_PERF === "1") process.stderr.write(formatPerfBlock(scope, marks));
17
+
17
18
  void postOtlpTrace(PERF_SERVICE_NAME, phaseMarksToSpans(scope, marks));
18
19
  }
@@ -17,6 +17,7 @@ function perfInstrumentationEnabled(): boolean {
17
17
  /** Record the elapsed ms since the previous mark (or process start) under `label`. */
18
18
  export function perfMark(label: string): void {
19
19
  if (!perfInstrumentationEnabled()) return;
20
+
20
21
  const now = performance.now();
21
22
 
22
23
  phaseMarks.push({ label, elapsedMs: now - (lastMarkAt ?? 0) });