@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.
- package/package.json +5 -5
- package/src/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +67 -1
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/app-view-model.ts +7 -0
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +4 -0
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +41 -0
- package/src/settings/config.ts +44 -0
- package/src/settings/use-settings-dialog.test.tsx +98 -0
- package/src/settings/use-settings-dialog.tsx +38 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.tsx +1 -0
- package/src/thread/components/AgentThreadPane.test.tsx +31 -0
- package/src/thread/components/AgentThreadPane.tsx +12 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.ts +123 -0
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- 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) });
|