@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.104
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.readonly.test.tsx +63 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +94 -12
- 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 +3 -2
- package/src/app/app-view-model.ts +11 -1
- 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/index.ts +1 -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 +46 -1
- 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 +79 -1
- package/src/settings/config.ts +91 -1
- package/src/settings/use-settings-dialog.test.tsx +172 -0
- package/src/settings/use-settings-dialog.tsx +115 -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.test.tsx +41 -0
- package/src/thread/components/AgentConfigControls.tsx +8 -1
- package/src/thread/components/AgentThreadPane.test.tsx +91 -3
- package/src/thread/components/AgentThreadPane.tsx +61 -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.share.test.ts +47 -2
- package/src/thread/thread-controller.ts +129 -4
- 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
|
@@ -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) });
|
|
@@ -126,6 +126,7 @@ function terminalColorScheme(textColor: string): "dark" | "light" {
|
|
|
126
126
|
const hex = textColor.replace("#", "");
|
|
127
127
|
|
|
128
128
|
if (hex.length < 6) return "dark";
|
|
129
|
+
|
|
129
130
|
const r = parseInt(hex.slice(0, 2), 16);
|
|
130
131
|
const g = parseInt(hex.slice(2, 4), 16);
|
|
131
132
|
const b = parseInt(hex.slice(4, 6), 16);
|
|
@@ -181,12 +182,14 @@ function PrototypeContentViewImpl({
|
|
|
181
182
|
// harness), but the launch trigger still runs.
|
|
182
183
|
useEffect(() => {
|
|
183
184
|
if (!renderer) return;
|
|
185
|
+
|
|
184
186
|
const rendererOutput = v.safeParse(v.object({ writeOut: v.function() }), renderer);
|
|
185
187
|
const rawWrite = rendererOutput.success ? rendererOutput.output.writeOut : null;
|
|
186
188
|
const write = rawWrite ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
|
|
187
189
|
const medium = resolveTransmitMedium();
|
|
188
190
|
const paint = (): void => {
|
|
189
191
|
if (!write) return;
|
|
192
|
+
|
|
190
193
|
// an app overlay is covering the sheet: pull the image so it does not show
|
|
191
194
|
// through the overlay's cells, and re-transmit once the overlay closes
|
|
192
195
|
if (hiddenRef.current) {
|
|
@@ -197,10 +200,12 @@ function PrototypeContentViewImpl({
|
|
|
197
200
|
|
|
198
201
|
return;
|
|
199
202
|
}
|
|
203
|
+
|
|
200
204
|
const region = regionOf(regionRef.current);
|
|
201
205
|
const png = pngRef.current;
|
|
202
206
|
|
|
203
207
|
if (!region || !png) return;
|
|
208
|
+
|
|
204
209
|
if (transmittedRef.current !== png) {
|
|
205
210
|
transmitKittyImage(write, png, region, PROTOTYPE_IMAGE_ID, medium);
|
|
206
211
|
transmittedRef.current = png;
|
|
@@ -219,6 +224,7 @@ function PrototypeContentViewImpl({
|
|
|
219
224
|
|
|
220
225
|
return () => {
|
|
221
226
|
renderer.off("frame", onFrame);
|
|
227
|
+
|
|
222
228
|
if (write) deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
223
229
|
};
|
|
224
230
|
}, [renderer]);
|
|
@@ -241,11 +247,13 @@ function PrototypeContentViewImpl({
|
|
|
241
247
|
const caps = renderer?.capabilities;
|
|
242
248
|
|
|
243
249
|
if (launchedRef.current || !region) return;
|
|
250
|
+
|
|
244
251
|
if (caps && caps.kitty_graphics === false) {
|
|
245
252
|
setStatus("unsupported");
|
|
246
253
|
|
|
247
254
|
return;
|
|
248
255
|
}
|
|
256
|
+
|
|
249
257
|
launchedRef.current = true;
|
|
250
258
|
const capture = captureConfig(region, renderer!);
|
|
251
259
|
|
|
@@ -266,6 +274,7 @@ function PrototypeContentViewImpl({
|
|
|
266
274
|
|
|
267
275
|
return;
|
|
268
276
|
}
|
|
277
|
+
|
|
269
278
|
browserRef.current = browser;
|
|
270
279
|
setPng(await browser.screenshot());
|
|
271
280
|
setStatus("ready");
|
|
@@ -304,6 +313,7 @@ function PrototypeContentViewImpl({
|
|
|
304
313
|
const region = regionOf(regionRef.current);
|
|
305
314
|
|
|
306
315
|
if (status !== "ready" || !browser || !region) return;
|
|
316
|
+
|
|
307
317
|
const geometry = {
|
|
308
318
|
x: region.column,
|
|
309
319
|
y: region.row,
|
|
@@ -313,10 +323,12 @@ function PrototypeContentViewImpl({
|
|
|
313
323
|
const cssPoint = imageCellToCss(event, geometry, viewportRef.current);
|
|
314
324
|
|
|
315
325
|
if (!cssPoint) return;
|
|
326
|
+
|
|
316
327
|
void (async () => {
|
|
317
328
|
const element = await browser.elementAt(cssPoint.x, cssPoint.y);
|
|
318
329
|
|
|
319
330
|
if (!element) return;
|
|
331
|
+
|
|
320
332
|
const cell = cssBoxToCell(element.box, geometry, viewportRef.current);
|
|
321
333
|
const topRaw = cell.row - geometry.y;
|
|
322
334
|
|
|
@@ -326,6 +338,7 @@ function PrototypeContentViewImpl({
|
|
|
326
338
|
top: Math.max(0, topRaw < OVERLAY_ROWS ? topRaw + 1 : topRaw - OVERLAY_ROWS),
|
|
327
339
|
regionColumns: geometry.width,
|
|
328
340
|
});
|
|
341
|
+
|
|
329
342
|
// selecting an element opens the compose card directly, the same edit
|
|
330
343
|
// primitive plan and diff use - there is no intermediate marker toolbar
|
|
331
344
|
if (canComment) {
|
|
@@ -344,6 +357,7 @@ function PrototypeContentViewImpl({
|
|
|
344
357
|
const browser = browserRef.current;
|
|
345
358
|
|
|
346
359
|
if (status !== "ready" || !browser) return;
|
|
360
|
+
|
|
347
361
|
const delta = event.scroll?.direction === "up" ? -SCROLL_STEP : SCROLL_STEP;
|
|
348
362
|
|
|
349
363
|
// a selection would drift once the page scrolls under it, so drop it
|
|
@@ -356,7 +370,9 @@ function PrototypeContentViewImpl({
|
|
|
356
370
|
|
|
357
371
|
const commit = (body: string): void => {
|
|
358
372
|
if (!canComment) return;
|
|
373
|
+
|
|
359
374
|
if (selected && body.trim()) onCommentElement(selected, body.trim());
|
|
375
|
+
|
|
360
376
|
clearSelection();
|
|
361
377
|
};
|
|
362
378
|
|
|
@@ -435,6 +451,7 @@ export const PrototypeContentView = React.memo(PrototypeContentViewImpl);
|
|
|
435
451
|
|
|
436
452
|
function statusLine(status: SheetStatus, errorMessage: string): string {
|
|
437
453
|
if (status === "loading") return "rendering prototype…";
|
|
454
|
+
|
|
438
455
|
if (status === "unsupported")
|
|
439
456
|
return "prototype preview needs a graphics terminal (kitty or ghostty)";
|
|
440
457
|
|
|
@@ -114,6 +114,7 @@ async function warmBrowser(executablePath: string | undefined): Promise<Puppetee
|
|
|
114
114
|
const cached = sharedBrowser ? await sharedBrowser.catch(() => null) : null;
|
|
115
115
|
|
|
116
116
|
if (cached && cached.connected) return cached;
|
|
117
|
+
|
|
117
118
|
const puppeteer = (await import("puppeteer-core")).default;
|
|
118
119
|
const launched = puppeteer.launch({
|
|
119
120
|
executablePath: executablePath ?? chromeExecutable(),
|
|
@@ -124,6 +125,7 @@ async function warmBrowser(executablePath: string | undefined): Promise<Puppetee
|
|
|
124
125
|
sharedBrowser = launched;
|
|
125
126
|
const browser = await launched.catch((error) => {
|
|
126
127
|
if (sharedBrowser === launched) sharedBrowser = null;
|
|
128
|
+
|
|
127
129
|
throw error;
|
|
128
130
|
});
|
|
129
131
|
|
|
@@ -143,7 +145,9 @@ export async function closePrototypeBrowser(): Promise<void> {
|
|
|
143
145
|
const pending = sharedBrowser;
|
|
144
146
|
|
|
145
147
|
sharedBrowser = null;
|
|
148
|
+
|
|
146
149
|
if (!pending) return;
|
|
150
|
+
|
|
147
151
|
const browser = await pending.catch(() => null);
|
|
148
152
|
|
|
149
153
|
await browser?.close().catch(() => undefined);
|
|
@@ -233,8 +237,10 @@ function elementAtScript(x: number, y: number) {
|
|
|
233
237
|
while (current && current !== document.body) {
|
|
234
238
|
if (isNamed(current)) {
|
|
235
239
|
if (current.childElementCount > 1) return current;
|
|
240
|
+
|
|
236
241
|
namedFallback = namedFallback ?? current;
|
|
237
242
|
}
|
|
243
|
+
|
|
238
244
|
current = current.parentElement;
|
|
239
245
|
}
|
|
240
246
|
|
|
@@ -251,6 +257,7 @@ function elementAtScript(x: number, y: number) {
|
|
|
251
257
|
parts.unshift(part + "#" + CSS.escape(current.id));
|
|
252
258
|
break;
|
|
253
259
|
}
|
|
260
|
+
|
|
254
261
|
const parent: Element | null = current.parentElement;
|
|
255
262
|
|
|
256
263
|
if (parent) {
|
|
@@ -259,6 +266,7 @@ function elementAtScript(x: number, y: number) {
|
|
|
259
266
|
|
|
260
267
|
if (twins.length > 1) part += ":nth-of-type(" + (twins.indexOf(current) + 1) + ")";
|
|
261
268
|
}
|
|
269
|
+
|
|
262
270
|
parts.unshift(part);
|
|
263
271
|
current = current.parentElement;
|
|
264
272
|
}
|
|
@@ -282,10 +290,12 @@ function elementAtScript(x: number, y: number) {
|
|
|
282
290
|
const hit = document.elementFromPoint(x, y);
|
|
283
291
|
|
|
284
292
|
if (!(hit instanceof Element)) return null;
|
|
293
|
+
|
|
285
294
|
const node = focusableTarget(hit) ?? componentRoot(hit);
|
|
286
295
|
const selector = selectorFor(node);
|
|
287
296
|
|
|
288
297
|
if (!selector) return null;
|
|
298
|
+
|
|
289
299
|
const text = (node.textContent || "").replace(/\s+/g, " ").trim();
|
|
290
300
|
const tag = node.tagName.toLowerCase();
|
|
291
301
|
const quote = text
|
|
@@ -313,6 +323,7 @@ function chromeExecutable(): string {
|
|
|
313
323
|
if (process.platform === "darwin") {
|
|
314
324
|
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
315
325
|
}
|
|
326
|
+
|
|
316
327
|
if (process.platform === "win32") {
|
|
317
328
|
return "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
|
|
318
329
|
}
|
|
@@ -92,10 +92,12 @@ export function SettingsDialog({
|
|
|
92
92
|
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
93
93
|
|
|
94
94
|
if (!isOpen) return null;
|
|
95
|
+
|
|
95
96
|
const category =
|
|
96
97
|
categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
|
|
97
98
|
|
|
98
99
|
if (!category) return null;
|
|
100
|
+
|
|
99
101
|
const onKeybinds = category.id === KEYBINDS_CATEGORY_ID;
|
|
100
102
|
|
|
101
103
|
return (
|
|
@@ -143,6 +145,7 @@ export function SettingsDialog({
|
|
|
143
145
|
/>
|
|
144
146
|
);
|
|
145
147
|
}
|
|
148
|
+
|
|
146
149
|
if (row.kind === "cycle") {
|
|
147
150
|
return (
|
|
148
151
|
<CycleRow
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
persistLayout,
|
|
13
13
|
persistPins,
|
|
14
14
|
persistReviewSkill,
|
|
15
|
+
persistThreadModelDefault,
|
|
15
16
|
persistReviewWorkspace,
|
|
16
17
|
persistTheme,
|
|
17
18
|
quickActionBody,
|
|
@@ -674,3 +675,80 @@ test("Thread agents default off and require a boolean experimental TOML flag", (
|
|
|
674
675
|
rmSync(dir, { recursive: true, force: true });
|
|
675
676
|
}
|
|
676
677
|
});
|
|
678
|
+
|
|
679
|
+
test("Thread harness preference persists without changing experimental or other settings", async () => {
|
|
680
|
+
const { persistThreadHarness } = await import("./config");
|
|
681
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-thread-config-"));
|
|
682
|
+
const path = join(dir, "config.toml");
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
writeFileSync(path, '[experimental]\nthread_agent = true\n[ui]\ntheme = "cueloop"\n');
|
|
686
|
+
persistThreadHarness("fx", path);
|
|
687
|
+
const config = loadConfig({ userConfigPath: path });
|
|
688
|
+
|
|
689
|
+
expect(config.thread.harness).toBe("fx");
|
|
690
|
+
expect(config.experimental.threadAgent).toBe(true);
|
|
691
|
+
expect(config.ui.theme).toBe("cueloop");
|
|
692
|
+
persistThreadHarness("pi", path);
|
|
693
|
+
expect(loadConfig({ userConfigPath: path }).thread.harness).toBe("pi");
|
|
694
|
+
} finally {
|
|
695
|
+
rmSync(dir, { recursive: true, force: true });
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("a malformed Thread section does not discard other preferences", () => {
|
|
700
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-invalid-thread-config-"));
|
|
701
|
+
const path = join(dir, "config.toml");
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
for (const section of ['thread = "fx"\n', '[thread]\nharness = "typo"\n']) {
|
|
705
|
+
writeFileSync(
|
|
706
|
+
path,
|
|
707
|
+
`${section}[experimental]\nthread_agent = true\n[ui]\ntheme = "cueloop"\n`,
|
|
708
|
+
);
|
|
709
|
+
const config = loadConfig({ userConfigPath: path });
|
|
710
|
+
|
|
711
|
+
expect(config.thread.harness).toBe("pi");
|
|
712
|
+
expect(config.experimental.threadAgent).toBe(true);
|
|
713
|
+
expect(config.ui.theme).toBe("cueloop");
|
|
714
|
+
}
|
|
715
|
+
} finally {
|
|
716
|
+
rmSync(dir, { recursive: true, force: true });
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("Thread model defaults layer independently and persistence stays in the selected table", () => {
|
|
721
|
+
const directory = mkdtempSync(join(tmpdir(), "cueloop-model-defaults-"));
|
|
722
|
+
const user = join(directory, "user.toml");
|
|
723
|
+
const repository = join(directory, "repo");
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
const defaults = loadConfig({ userConfigPath: user }).thread;
|
|
727
|
+
|
|
728
|
+
expect(defaults.pi).toEqual({ model: "openai-codex/gpt-6.1-sol", effort: "medium" });
|
|
729
|
+
expect(defaults.fx).toEqual({ model: "grok/grok-4.7", effort: "medium" });
|
|
730
|
+
writeFileSync(
|
|
731
|
+
user,
|
|
732
|
+
'[thread.pi]\nmodel = "openai-codex/custom"\neffort = "high"\n\n[thread.fx]\nmodel = "grok/custom"\neffort = "low"\n',
|
|
733
|
+
);
|
|
734
|
+
persistThreadModelDefault("pi", "effort", "medium", user);
|
|
735
|
+
persistThreadModelDefault("fx", "model", "grok/grok-4.7", user);
|
|
736
|
+
|
|
737
|
+
const config = loadConfig({ userConfigPath: user });
|
|
738
|
+
|
|
739
|
+
expect(config.thread.pi).toEqual({ model: "openai-codex/custom", effort: "medium" });
|
|
740
|
+
expect(config.thread.fx).toEqual({ model: "grok/grok-4.7", effort: "low" });
|
|
741
|
+
mkdirSync(join(repository, ".cueloop"), { recursive: true });
|
|
742
|
+
writeFileSync(
|
|
743
|
+
join(repository, ".cueloop/config.toml"),
|
|
744
|
+
'[thread.pi]\neffort = "high"\n[thread.fx]\nmodel = " "\neffort = "invalid"\n',
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const layered = loadConfig({ userConfigPath: user, repoRoot: repository });
|
|
748
|
+
|
|
749
|
+
expect(layered.thread.pi).toEqual({ model: "openai-codex/custom", effort: "high" });
|
|
750
|
+
expect(layered.thread.fx).toEqual(config.thread.fx);
|
|
751
|
+
} finally {
|
|
752
|
+
rmSync(directory, { recursive: true, force: true });
|
|
753
|
+
}
|
|
754
|
+
});
|