@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
package/src/app/ready-signal.ts
CHANGED
|
@@ -23,6 +23,7 @@ function announceReady(): void {
|
|
|
23
23
|
const path = process.env[READY_FILE_ENV];
|
|
24
24
|
|
|
25
25
|
if (!path) return;
|
|
26
|
+
|
|
26
27
|
writeFileSync(path, "ready\n");
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -37,6 +38,7 @@ export function useReadySignal(ready: boolean, onReady?: () => void): void {
|
|
|
37
38
|
|
|
38
39
|
useEffect(() => {
|
|
39
40
|
if (!ready || fired.current) return;
|
|
41
|
+
|
|
40
42
|
const emit = (): void => {
|
|
41
43
|
fired.current = true;
|
|
42
44
|
announceReady();
|
package/src/app/run.ts
CHANGED
|
@@ -57,6 +57,7 @@ export async function runClient(options: RunClientOptions): Promise<number> {
|
|
|
57
57
|
let exited = false;
|
|
58
58
|
const shutdown = async (code: number): Promise<void> => {
|
|
59
59
|
if (exited) return;
|
|
60
|
+
|
|
60
61
|
exited = true;
|
|
61
62
|
renderer.destroy();
|
|
62
63
|
// close the warm prototype Chromium before exit so it never orphans; the
|
|
@@ -101,6 +102,7 @@ export async function runClient(options: RunClientOptions): Promise<number> {
|
|
|
101
102
|
.waitForThemeMode(THEME_QUERY_TIMEOUT_MS)
|
|
102
103
|
.then((mode) => {
|
|
103
104
|
perfMark("themeQuery");
|
|
105
|
+
|
|
104
106
|
// the query can answer after a fast quit; never render onto a destroyed renderer
|
|
105
107
|
if (!exited && mode && mode !== "dark") renderApp(mode);
|
|
106
108
|
})
|
package/src/app/serve.test.ts
CHANGED
|
@@ -49,9 +49,11 @@ function sshCapture(
|
|
|
49
49
|
let done = false;
|
|
50
50
|
const finish = (err?: Error) => {
|
|
51
51
|
if (done) return;
|
|
52
|
+
|
|
52
53
|
done = true;
|
|
53
54
|
clearTimeout(timer);
|
|
54
55
|
conn.end();
|
|
56
|
+
|
|
55
57
|
if (err) reject(err);
|
|
56
58
|
else resolve(out);
|
|
57
59
|
};
|
|
@@ -61,8 +63,10 @@ function sshCapture(
|
|
|
61
63
|
.on("ready", () => {
|
|
62
64
|
conn.shell({ term: "xterm-256color", cols: 100, rows: 30 }, (err, stream) => {
|
|
63
65
|
if (err) return finish(err);
|
|
66
|
+
|
|
64
67
|
stream.on("data", (chunk: Buffer) => {
|
|
65
68
|
out += chunk.toString("utf8");
|
|
69
|
+
|
|
66
70
|
if (until(out)) finish();
|
|
67
71
|
});
|
|
68
72
|
stream.on("close", () => finish(new Error(`stream closed early; captured:\n${out}`)));
|
|
@@ -17,6 +17,7 @@ const noop = (): void => {};
|
|
|
17
17
|
/** Find a renderable by id anywhere in the tree (getRenderable is not recursive). */
|
|
18
18
|
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
19
19
|
if (node.id === id) return node;
|
|
20
|
+
|
|
20
21
|
for (const child of node.getChildren()) {
|
|
21
22
|
const found = findById(child, id);
|
|
22
23
|
|
|
@@ -81,6 +82,7 @@ test("walking the caret down past wrapped discussion cards keeps it on screen an
|
|
|
81
82
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
82
83
|
|
|
83
84
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
85
|
+
|
|
84
86
|
const scrollbox = found;
|
|
85
87
|
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
86
88
|
|
|
@@ -156,6 +158,7 @@ test("wheel input accelerates a continuous gesture and settles back to precise s
|
|
|
156
158
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
157
159
|
|
|
158
160
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
161
|
+
|
|
159
162
|
const scrollbox = found;
|
|
160
163
|
const acceleration = scrollbox.scrollAcceleration;
|
|
161
164
|
const first = acceleration.tick(1000);
|
|
@@ -196,6 +199,7 @@ test("wheel scrolling up keeps the caret on the bottom visible code row", async
|
|
|
196
199
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
197
200
|
|
|
198
201
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
202
|
+
|
|
199
203
|
const scrollbox = found;
|
|
200
204
|
|
|
201
205
|
for (const key of Array.from({ length: 30 }, () => "down" as const)) {
|
|
@@ -270,6 +274,7 @@ test("rapid wheel scrolling through a large wrapped diff does not loop React upd
|
|
|
270
274
|
for (const [index, direction] of Array.from({ length: 240 }, () => "down" as const).entries()) {
|
|
271
275
|
// eslint-disable-next-line no-await-in-loop
|
|
272
276
|
await setup.mockMouse.scroll(20, 5, direction);
|
|
277
|
+
|
|
273
278
|
if (index % 12 === 0) {
|
|
274
279
|
// eslint-disable-next-line no-await-in-loop
|
|
275
280
|
await settle(setup);
|
|
@@ -315,6 +320,7 @@ test("walking long wrapped code lines advances the viewport one visual row per k
|
|
|
315
320
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
316
321
|
|
|
317
322
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
323
|
+
|
|
318
324
|
const scrollbox = found;
|
|
319
325
|
const steps: number[] = [];
|
|
320
326
|
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
@@ -410,6 +416,7 @@ test("Up keeps the caret on the first screen row while wrapped lines scroll", as
|
|
|
410
416
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
411
417
|
|
|
412
418
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
419
|
+
|
|
413
420
|
const scrollbox = found;
|
|
414
421
|
|
|
415
422
|
for (let index = 0; index < 40; index++) await press(setup, "down");
|
|
@@ -423,10 +430,12 @@ test("Up keeps the caret on the first screen row while wrapped lines scroll", as
|
|
|
423
430
|
const top = scrollbox.scrollTop;
|
|
424
431
|
|
|
425
432
|
expect(previousTop - top).toBeLessThanOrEqual(1);
|
|
433
|
+
|
|
426
434
|
if (top < previousTop) {
|
|
427
435
|
expect(marker).toBe(0);
|
|
428
436
|
scrolled = true;
|
|
429
437
|
}
|
|
438
|
+
|
|
430
439
|
previousTop = top;
|
|
431
440
|
}
|
|
432
441
|
expect(scrolled).toBe(true);
|
|
@@ -72,6 +72,7 @@ test("renders tree-sitter colors, with the changed word in the diff color on top
|
|
|
72
72
|
for (const line of setup.captureSpans().lines) {
|
|
73
73
|
for (const span of line.spans) {
|
|
74
74
|
if (span.text.includes("250")) changedColors.add(hex(span.fg));
|
|
75
|
+
|
|
75
76
|
if (span.text.includes("100")) changedColors.add(hex(span.fg));
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -56,6 +56,7 @@ function lineNumberWidth(rows: DiffRow[]): number {
|
|
|
56
56
|
|
|
57
57
|
for (const row of rows) {
|
|
58
58
|
if (row.oldLine !== undefined) width = Math.max(width, String(row.oldLine).length);
|
|
59
|
+
|
|
59
60
|
if (row.newLine !== undefined) width = Math.max(width, String(row.newLine).length);
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -339,10 +340,12 @@ function splitLayout(splitRows: SplitRow[], rows: DiffRow[], textWidth: number):
|
|
|
339
340
|
|
|
340
341
|
return { index, height: row ? headerHeight(row) : 1 };
|
|
341
342
|
}
|
|
343
|
+
|
|
342
344
|
const sideHeight = (line: SplitLine | undefined): number =>
|
|
343
345
|
line ? codeRowHeight(line.row, textWidth) : 0;
|
|
344
346
|
|
|
345
347
|
if (pair.left) itemOfRow[pair.left.rowIndex] = index;
|
|
348
|
+
|
|
346
349
|
if (pair.right) itemOfRow[pair.right.rowIndex] = index;
|
|
347
350
|
|
|
348
351
|
return { index, height: Math.max(1, sideHeight(pair.left), sideHeight(pair.right)) };
|
|
@@ -401,6 +404,7 @@ function codeLineSpans(
|
|
|
401
404
|
) {
|
|
402
405
|
previous.text += character;
|
|
403
406
|
} else spans.push({ text: character, fg, bg, attributes });
|
|
407
|
+
|
|
404
408
|
column++;
|
|
405
409
|
}
|
|
406
410
|
}
|
|
@@ -465,12 +469,14 @@ function emphasisBackgroundColumns(
|
|
|
465
469
|
const columns: Array<string | undefined> = Array.from({ length: text.length }, () => undefined);
|
|
466
470
|
|
|
467
471
|
if (!intraline || !emphasisBackground) return columns;
|
|
472
|
+
|
|
468
473
|
let offset = 0;
|
|
469
474
|
|
|
470
475
|
for (const run of intraline) {
|
|
471
476
|
if (run.changed)
|
|
472
477
|
for (let index = 0; index < run.text.length && offset + index < text.length; index++)
|
|
473
478
|
columns[offset + index] = emphasisBackground;
|
|
479
|
+
|
|
474
480
|
offset += run.text.length;
|
|
475
481
|
}
|
|
476
482
|
|
|
@@ -655,6 +661,7 @@ export function DiffContentView({
|
|
|
655
661
|
const scrollbox = scrollRef.current;
|
|
656
662
|
|
|
657
663
|
if (!scrollbox) return false;
|
|
664
|
+
|
|
658
665
|
let distance: number;
|
|
659
666
|
|
|
660
667
|
if (targetY !== undefined) {
|
|
@@ -664,10 +671,12 @@ export function DiffContentView({
|
|
|
664
671
|
distance = direction === 1 ? targetY - bottom : top - targetY;
|
|
665
672
|
} else {
|
|
666
673
|
if (from === to) return false;
|
|
674
|
+
|
|
667
675
|
const item = layout.itemOfRow[to];
|
|
668
676
|
const target = item === undefined ? undefined : virtual.startOfIndex(item);
|
|
669
677
|
|
|
670
678
|
if (target === undefined) return false;
|
|
679
|
+
|
|
671
680
|
const top = scrollbox.scrollTop;
|
|
672
681
|
const bottom = top + scrollbox.viewport.height - 1;
|
|
673
682
|
|
|
@@ -675,8 +684,10 @@ export function DiffContentView({
|
|
|
675
684
|
}
|
|
676
685
|
|
|
677
686
|
if (distance <= 0) return false;
|
|
687
|
+
|
|
678
688
|
// Let the caret commit before scrolling the next visual row into view.
|
|
679
689
|
if (distance === 1) return false;
|
|
690
|
+
|
|
680
691
|
scrollbox.scrollBy(direction);
|
|
681
692
|
|
|
682
693
|
return distance > 1;
|
|
@@ -689,15 +700,18 @@ export function DiffContentView({
|
|
|
689
700
|
|
|
690
701
|
useEffect(() => {
|
|
691
702
|
if (revealItem === undefined || (!surface.compose && !surface.focusedDiscussion)) return;
|
|
703
|
+
|
|
692
704
|
virtual.scrollToIndex(revealItem, surface.compose ? "end" : "auto");
|
|
693
705
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
694
706
|
}, [surface.revealBlockIndex, surface.compose, surface.focusedDiscussion]);
|
|
695
707
|
|
|
696
708
|
useLayoutEffect(() => {
|
|
697
709
|
if (revealItem === undefined || surface.compose || surface.focusedDiscussion) return;
|
|
710
|
+
|
|
698
711
|
const scrollbox = scrollRef.current;
|
|
699
712
|
|
|
700
713
|
if (!scrollbox) return;
|
|
714
|
+
|
|
701
715
|
const headY = surface.headVisualY();
|
|
702
716
|
|
|
703
717
|
if (headY !== undefined) {
|
|
@@ -709,9 +723,11 @@ export function DiffContentView({
|
|
|
709
723
|
|
|
710
724
|
return;
|
|
711
725
|
}
|
|
726
|
+
|
|
712
727
|
const start = virtual.startOfIndex(revealItem);
|
|
713
728
|
|
|
714
729
|
if (start === undefined) return;
|
|
730
|
+
|
|
715
731
|
const top = scrollbox.scrollTop;
|
|
716
732
|
const bottom = top + scrollbox.viewport.height - 1;
|
|
717
733
|
|
|
@@ -779,6 +795,7 @@ export function DiffContentView({
|
|
|
779
795
|
</>
|
|
780
796
|
);
|
|
781
797
|
}
|
|
798
|
+
|
|
782
799
|
if (split) {
|
|
783
800
|
return (
|
|
784
801
|
<>
|
|
@@ -862,6 +879,7 @@ export function DiffContentView({
|
|
|
862
879
|
</text>
|
|
863
880
|
</box>,
|
|
864
881
|
);
|
|
882
|
+
|
|
865
883
|
// cards render on the side that owns the row, so a shared context row's card is not duplicated
|
|
866
884
|
if (paintMarks) cards.push(...surface.cardsAfterLine(rowIndex, line, isLastLine));
|
|
867
885
|
}
|
|
@@ -895,6 +913,7 @@ export function DiffContentView({
|
|
|
895
913
|
const row = rows[rowIndex]!;
|
|
896
914
|
|
|
897
915
|
if (!isCodeRow(row)) return headerNode(row, rowIndex);
|
|
916
|
+
|
|
898
917
|
const { lines, cards } = codeRowLines(row, rowIndex, textWidth, `row-${rowIndex}`);
|
|
899
918
|
|
|
900
919
|
return (
|
|
@@ -919,6 +938,7 @@ export function DiffContentView({
|
|
|
919
938
|
|
|
920
939
|
return { node: filler };
|
|
921
940
|
}
|
|
941
|
+
|
|
922
942
|
// the change side (right: additions and context) owns the caret and marks; the left carries them
|
|
923
943
|
// only on its deletions, so a context row shared by both columns is not marked twice
|
|
924
944
|
const paintMarks = side === "right" || line.kind === "del";
|
|
@@ -951,6 +971,7 @@ export function DiffContentView({
|
|
|
951
971
|
rowIndex,
|
|
952
972
|
);
|
|
953
973
|
}
|
|
974
|
+
|
|
954
975
|
const left = splitSide(pair.left, "left", textWidth, `pair-${pairIndex}-left`);
|
|
955
976
|
const right = splitSide(pair.right, "right", textWidth, `pair-${pairIndex}-right`);
|
|
956
977
|
// both sides answer to the pair's id, so a reveal of either base row lands here
|
|
@@ -993,6 +1014,7 @@ export function DiffContentView({
|
|
|
993
1014
|
if (firstItem.start > 0) {
|
|
994
1015
|
materialized.push(<box key="spacer-above" style={{ height: firstItem.start }} />);
|
|
995
1016
|
}
|
|
1017
|
+
|
|
996
1018
|
for (const item of virtual.items) {
|
|
997
1019
|
// the wrapper is what gets measured, so a row's cards count toward its height
|
|
998
1020
|
materialized.push(
|
|
@@ -1001,6 +1023,7 @@ export function DiffContentView({
|
|
|
1001
1023
|
</box>,
|
|
1002
1024
|
);
|
|
1003
1025
|
}
|
|
1026
|
+
|
|
1004
1027
|
if (below > 0) materialized.push(<box key="spacer-below" style={{ height: below }} />);
|
|
1005
1028
|
}
|
|
1006
1029
|
|
|
@@ -1020,7 +1043,9 @@ export function DiffContentView({
|
|
|
1020
1043
|
const scrollbox = scrollRef.current;
|
|
1021
1044
|
|
|
1022
1045
|
if (!scrollbox || !event.scroll || event.modifiers.shift) return;
|
|
1046
|
+
|
|
1023
1047
|
if (event.scroll.direction !== "up" && event.scroll.direction !== "down") return;
|
|
1048
|
+
|
|
1024
1049
|
const before = scrollbox.scrollTop;
|
|
1025
1050
|
const followCaret = surface.prepareViewportScroll();
|
|
1026
1051
|
|
|
@@ -52,11 +52,13 @@ function changedColumns(length: number, intralineRuns: IntralineRun[] | undefine
|
|
|
52
52
|
const changed = Array.from({ length }, () => false);
|
|
53
53
|
|
|
54
54
|
if (!intralineRuns) return changed;
|
|
55
|
+
|
|
55
56
|
let offset = 0;
|
|
56
57
|
|
|
57
58
|
for (const run of intralineRuns) {
|
|
58
59
|
if (run.changed)
|
|
59
60
|
for (let index = 0; index < run.text.length; index++) changed[offset + index] = true;
|
|
61
|
+
|
|
60
62
|
offset += run.text.length;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -75,6 +77,7 @@ function syntaxColorColumns(
|
|
|
75
77
|
const color = colorForSyntaxGroup(span.group, tokens);
|
|
76
78
|
|
|
77
79
|
if (!color) continue;
|
|
80
|
+
|
|
78
81
|
for (let column = span.start; column < span.end && column < length; column++) {
|
|
79
82
|
colors[column] = color;
|
|
80
83
|
}
|
package/src/diff/diff-fold.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function weaveFullFileRows(
|
|
|
55
55
|
fillTo(row.newLine);
|
|
56
56
|
woven.push(row);
|
|
57
57
|
newCursor = row.newLine + 1;
|
|
58
|
+
|
|
58
59
|
if (row.kind === "ctx") oldCursor = (row.oldLine ?? oldCursor) + 1;
|
|
59
60
|
} else {
|
|
60
61
|
// a deletion consumes an old line only; it keeps its place before the additions
|
|
@@ -90,6 +91,7 @@ export function applyFold(
|
|
|
90
91
|
files: readonly FileContents[] | undefined,
|
|
91
92
|
): DiffRow[] {
|
|
92
93
|
if (collapsed.size === 0 && expanded.size === 0) return base;
|
|
94
|
+
|
|
93
95
|
const contentsByPath = new Map((files ?? []).map((file) => [file.path, file]));
|
|
94
96
|
const out: DiffRow[] = [];
|
|
95
97
|
let index = 0;
|
|
@@ -102,6 +104,7 @@ export function applyFold(
|
|
|
102
104
|
index += 1;
|
|
103
105
|
continue;
|
|
104
106
|
}
|
|
107
|
+
|
|
105
108
|
out.push(row);
|
|
106
109
|
const file = row.file;
|
|
107
110
|
let end = index + 1;
|
|
@@ -119,6 +122,7 @@ export function applyFold(
|
|
|
119
122
|
} else {
|
|
120
123
|
out.push(...body);
|
|
121
124
|
}
|
|
125
|
+
|
|
122
126
|
index = end;
|
|
123
127
|
}
|
|
124
128
|
|
|
@@ -65,6 +65,7 @@ function changeSideLine(
|
|
|
65
65
|
): { side: DiffSide; lineNumber: number } | null {
|
|
66
66
|
if (row.kind === "add" && row.newLine !== undefined)
|
|
67
67
|
return { side: "addition", lineNumber: row.newLine };
|
|
68
|
+
|
|
68
69
|
if (row.kind === "del" && row.oldLine !== undefined)
|
|
69
70
|
return { side: "deletion", lineNumber: row.oldLine };
|
|
70
71
|
|
|
@@ -104,6 +105,7 @@ export function changeRejectionForRow(
|
|
|
104
105
|
const change = changeSideLine(row);
|
|
105
106
|
|
|
106
107
|
if (!change) return null;
|
|
108
|
+
|
|
107
109
|
const located = locateLine(model, change.side, change.lineNumber);
|
|
108
110
|
|
|
109
111
|
if (!located || located.changeIndex === undefined) return null;
|
|
@@ -139,6 +141,7 @@ export function isRowRejected(
|
|
|
139
141
|
const change = changeSideLine(row);
|
|
140
142
|
|
|
141
143
|
if (!change) return false;
|
|
144
|
+
|
|
142
145
|
const located = locateLine(model, change.side, change.lineNumber);
|
|
143
146
|
|
|
144
147
|
if (!located || located.changeIndex === undefined) return false;
|
|
@@ -53,6 +53,7 @@ function intralineBlocks(rows: DiffRow[]): IntralineBlock[] {
|
|
|
53
53
|
index++;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
+
|
|
56
57
|
const deletionStart = index;
|
|
57
58
|
|
|
58
59
|
while (index < rows.length && rows[index]!.kind === "del") index++;
|
|
@@ -127,6 +128,7 @@ export function createIntralineResolver(rows: DiffRow[]): IntralineResolver {
|
|
|
127
128
|
const blockIndex = blockContaining(blocks, rowIndex);
|
|
128
129
|
|
|
129
130
|
if (blockIndex === undefined) return undefined;
|
|
131
|
+
|
|
130
132
|
if (!computed.has(blockIndex)) {
|
|
131
133
|
computed.add(blockIndex);
|
|
132
134
|
for (const [row, runs] of blockRunEntries(rows, blocks[blockIndex]!))
|
|
@@ -161,6 +163,7 @@ function lineWordSetCache(): (text: string) => ReadonlySet<string> {
|
|
|
161
163
|
const cached = cache.get(text);
|
|
162
164
|
|
|
163
165
|
if (cached !== undefined) return cached;
|
|
166
|
+
|
|
164
167
|
const words = new Set(text.toLowerCase().split(/\s+/).filter(Boolean));
|
|
165
168
|
|
|
166
169
|
cache.set(text, words);
|
|
@@ -180,6 +183,7 @@ function linesSimilar(
|
|
|
180
183
|
const newWords = wordsOf(newText);
|
|
181
184
|
|
|
182
185
|
if (oldWords.size === 0 && newWords.size === 0) return true;
|
|
186
|
+
|
|
183
187
|
let intersection = 0;
|
|
184
188
|
|
|
185
189
|
for (const word of oldWords) if (newWords.has(word)) intersection++;
|
|
@@ -198,6 +202,7 @@ function linesSimilar(
|
|
|
198
202
|
*/
|
|
199
203
|
function alignedPairs(deletionTexts: string[], additionTexts: string[]): Array<[number, number]> {
|
|
200
204
|
if (deletionTexts.length === 1 && additionTexts.length === 1) return [[0, 0]];
|
|
205
|
+
|
|
201
206
|
const pairs: Array<[number, number]> = [];
|
|
202
207
|
let deletionOffset = 0;
|
|
203
208
|
let additionOffset = 0;
|
|
@@ -227,6 +232,7 @@ function sideRuns(changes: WordChange[], side: "added" | "removed"): IntralineRu
|
|
|
227
232
|
|
|
228
233
|
for (const change of changes) {
|
|
229
234
|
if (change.kind !== "common" && change.kind !== side) continue;
|
|
235
|
+
|
|
230
236
|
const changed = change.kind === side;
|
|
231
237
|
const last = runs[runs.length - 1];
|
|
232
238
|
|
package/src/diff/diff-syntax.ts
CHANGED
|
@@ -47,9 +47,11 @@ export function highlightJobs(rows: DiffRow[]): HighlightJob[] {
|
|
|
47
47
|
if (filetype) {
|
|
48
48
|
if (oldLines.length && (hasChange || newLines.length === 0))
|
|
49
49
|
jobs.push({ filetype, source: oldLines.join("\n"), rowIndexByLine: oldRowIndexByLine });
|
|
50
|
+
|
|
50
51
|
if (newLines.length)
|
|
51
52
|
jobs.push({ filetype, source: newLines.join("\n"), rowIndexByLine: newRowIndexByLine });
|
|
52
53
|
}
|
|
54
|
+
|
|
53
55
|
oldLines = [];
|
|
54
56
|
newLines = [];
|
|
55
57
|
oldRowIndexByLine = [];
|
|
@@ -70,16 +72,19 @@ export function highlightJobs(rows: DiffRow[]): HighlightJob[] {
|
|
|
70
72
|
flush();
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
75
|
+
|
|
73
76
|
const text = stripTrailingNewline(row.text);
|
|
74
77
|
|
|
75
78
|
if (row.kind === "ctx" || row.kind === "del") {
|
|
76
79
|
oldLines.push(text);
|
|
77
80
|
oldRowIndexByLine.push(rowIndex);
|
|
78
81
|
}
|
|
82
|
+
|
|
79
83
|
if (row.kind === "ctx" || row.kind === "add") {
|
|
80
84
|
newLines.push(text);
|
|
81
85
|
newRowIndexByLine.push(rowIndex);
|
|
82
86
|
}
|
|
87
|
+
|
|
83
88
|
if (row.kind === "add" || row.kind === "del") hasChange = true;
|
|
84
89
|
}
|
|
85
90
|
flush();
|
|
@@ -145,6 +150,7 @@ export function spansByLine(source: string, highlights: SimpleHighlight[]): Synt
|
|
|
145
150
|
const group = chars[column];
|
|
146
151
|
|
|
147
152
|
if (group === undefined) continue;
|
|
153
|
+
|
|
148
154
|
const previous = spans[spans.length - 1];
|
|
149
155
|
|
|
150
156
|
if (previous && previous.group === group && previous.end === column)
|
|
@@ -166,6 +172,7 @@ export async function highlightDiffRows(rows: DiffRow[]): Promise<Map<number, Sy
|
|
|
166
172
|
const jobs = highlightJobs(rows);
|
|
167
173
|
|
|
168
174
|
if (!jobs.length) return spansByRow;
|
|
175
|
+
|
|
169
176
|
const client = getTreeSitterClient();
|
|
170
177
|
|
|
171
178
|
await client.initialize();
|
|
@@ -173,6 +180,7 @@ export async function highlightDiffRows(rows: DiffRow[]): Promise<Map<number, Sy
|
|
|
173
180
|
const result = await client.highlightOnce(job.source, job.filetype);
|
|
174
181
|
|
|
175
182
|
if (!result.highlights) continue;
|
|
183
|
+
|
|
176
184
|
const lineSpans = spansByLine(job.source, result.highlights);
|
|
177
185
|
|
|
178
186
|
job.rowIndexByLine.forEach((rowIndex, line) => {
|
package/src/diff/split-diff.ts
CHANGED
|
@@ -84,6 +84,7 @@ export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
|
|
|
84
84
|
|
|
85
85
|
const flushChangeBlock = (): void => {
|
|
86
86
|
if (pendingDeletions.length === 0 && pendingAdditions.length === 0) return;
|
|
87
|
+
|
|
87
88
|
split.push(...zipChangeBlock(pendingDeletions, pendingAdditions, blockFile));
|
|
88
89
|
pendingDeletions = [];
|
|
89
90
|
pendingAdditions = [];
|
|
@@ -96,14 +97,17 @@ export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
|
|
|
96
97
|
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
100
|
+
|
|
99
101
|
if (row.kind === "del") {
|
|
100
102
|
blockFile = row.file;
|
|
101
103
|
pendingDeletions.push({ row, index });
|
|
102
104
|
|
|
103
105
|
return;
|
|
104
106
|
}
|
|
107
|
+
|
|
105
108
|
// any non-change row closes the current change block before it renders
|
|
106
109
|
flushChangeBlock();
|
|
110
|
+
|
|
107
111
|
if (row.kind === "file" || row.kind === "hunk") {
|
|
108
112
|
split.push({ kind: row.kind, file: row.file, text: row.text, rowIndex: index });
|
|
109
113
|
} else
|
|
@@ -135,7 +139,9 @@ export function splitRowOffsets(splitRows: SplitRow[]): Map<number, number> {
|
|
|
135
139
|
contentY += 1;
|
|
136
140
|
} else {
|
|
137
141
|
if (row.left) offsets.set(row.left.rowIndex, contentY);
|
|
142
|
+
|
|
138
143
|
if (row.right) offsets.set(row.right.rowIndex, contentY);
|
|
144
|
+
|
|
139
145
|
contentY += 1;
|
|
140
146
|
}
|
|
141
147
|
}
|
package/src/diff/view-diff.ts
CHANGED
|
@@ -92,10 +92,12 @@ export function fileChangeCounts(
|
|
|
92
92
|
|
|
93
93
|
for (const row of rows) {
|
|
94
94
|
if (row.kind !== "add" && row.kind !== "del") continue;
|
|
95
|
+
|
|
95
96
|
const entry = counts.get(row.file) ?? { additions: 0, deletions: 0 };
|
|
96
97
|
|
|
97
98
|
if (row.kind === "add") entry.additions += 1;
|
|
98
99
|
else entry.deletions += 1;
|
|
100
|
+
|
|
99
101
|
counts.set(row.file, entry);
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -143,9 +145,11 @@ export function marksByRows(
|
|
|
143
145
|
for (const annotation of annotations) {
|
|
144
146
|
// an addressed annotation keeps its record but paints no highlight
|
|
145
147
|
if (isAddressed(annotation)) continue;
|
|
148
|
+
|
|
146
149
|
const resolved = resolveAnchor(annotation.anchor, blocks);
|
|
147
150
|
|
|
148
151
|
if (!resolved) continue;
|
|
152
|
+
|
|
149
153
|
const span: TextSpan = {
|
|
150
154
|
start: { blockIndex: resolved.blockIndex, char: resolved.start },
|
|
151
155
|
end: { blockIndex: resolved.endBlockIndex, char: resolved.end },
|
|
@@ -156,6 +160,7 @@ export function marksByRows(
|
|
|
156
160
|
const range = spanRangeInBlock(span, rowIndex, blocks[rowIndex]!.text.length);
|
|
157
161
|
|
|
158
162
|
if (!range) continue;
|
|
163
|
+
|
|
159
164
|
const marks = marksByIndex.get(rowIndex) ?? [];
|
|
160
165
|
|
|
161
166
|
marks.push({
|
|
@@ -205,6 +210,7 @@ function addResolvedMark(
|
|
|
205
210
|
blocks[rowIndex]!.text.length === 0;
|
|
206
211
|
|
|
207
212
|
if (!range && !marksEmptyLine) continue;
|
|
213
|
+
|
|
208
214
|
const marks = marksByIndex.get(rowIndex) ?? [];
|
|
209
215
|
|
|
210
216
|
marks.push({
|
|
@@ -228,12 +234,14 @@ function addReviewFindingMark(
|
|
|
228
234
|
const finding = annotation.reviewComment;
|
|
229
235
|
|
|
230
236
|
if (!finding || isAddressed(annotation)) return false;
|
|
237
|
+
|
|
231
238
|
const end = resolveReviewAnchorRow(rows, annotation.anchor, finding.path, finding.side);
|
|
232
239
|
const start = finding.startAnchor
|
|
233
240
|
? resolveReviewAnchorRow(rows, finding.startAnchor, finding.path, finding.side)
|
|
234
241
|
: end;
|
|
235
242
|
|
|
236
243
|
if (!start || !end || start.rowIndex > end.rowIndex) return false;
|
|
244
|
+
|
|
237
245
|
addResolvedMark(marksByIndex, annotation, blocks, start.rowIndex, end.rowIndex, focusedId);
|
|
238
246
|
|
|
239
247
|
return true;
|
|
@@ -261,6 +269,7 @@ export function fileRowRange(rows: DiffRow[], path: string): { start: number; en
|
|
|
261
269
|
const start = rows.findIndex((row) => row.file === path);
|
|
262
270
|
|
|
263
271
|
if (start === -1) return null;
|
|
272
|
+
|
|
264
273
|
let end = start;
|
|
265
274
|
|
|
266
275
|
while (end < rows.length && rows[end]!.file === path) end++;
|
|
@@ -284,6 +293,7 @@ export function fileTargetMarks(
|
|
|
284
293
|
const target = annotationTarget(annotation);
|
|
285
294
|
|
|
286
295
|
if (target.kind !== "file") continue;
|
|
296
|
+
|
|
287
297
|
const forPath = byFile.get(target.path) ?? [];
|
|
288
298
|
|
|
289
299
|
forPath.push(annotation);
|
|
@@ -295,6 +305,7 @@ export function fileTargetMarks(
|
|
|
295
305
|
const range = fileRowRange(rows, path);
|
|
296
306
|
|
|
297
307
|
if (!range) continue;
|
|
308
|
+
|
|
298
309
|
const base = range.start;
|
|
299
310
|
const fileRows = rows.slice(range.start, range.end);
|
|
300
311
|
const reviewRootIds = new Set(
|
|
@@ -319,10 +330,12 @@ export function fileTargetMarks(
|
|
|
319
330
|
const finding = annotation.reviewComment;
|
|
320
331
|
|
|
321
332
|
if (!finding || isAddressed(annotation)) continue;
|
|
333
|
+
|
|
322
334
|
if (addReviewFindingMark(fileMarks, annotation, fileRows, blocks, focusedId)) {
|
|
323
335
|
resolvedIds.add(annotation.id);
|
|
324
336
|
continue;
|
|
325
337
|
}
|
|
338
|
+
|
|
326
339
|
const relativeRow = fileRows.findIndex(
|
|
327
340
|
(row) =>
|
|
328
341
|
(finding.side === "RIGHT" ? row.newLine : row.oldLine) === finding.line &&
|
|
@@ -349,11 +362,13 @@ export function fileTargetMarks(
|
|
|
349
362
|
for (const annotation of fileAnnotations) {
|
|
350
363
|
if (!annotation.replyTo || resolvedIds.has(annotation.id) || isAddressed(annotation))
|
|
351
364
|
continue;
|
|
365
|
+
|
|
352
366
|
const parent = [...fileMarks.entries()].find(([, marks]) =>
|
|
353
367
|
marks.some((mark) => mark.annotationId === annotation.replyTo),
|
|
354
368
|
);
|
|
355
369
|
|
|
356
370
|
if (!parent) continue;
|
|
371
|
+
|
|
357
372
|
const [fallbackRow, parentMarks] = parent;
|
|
358
373
|
const parentMark = parentMarks.find((mark) => mark.annotationId === annotation.replyTo)!;
|
|
359
374
|
const replyMark: Mark = {
|
|
@@ -448,6 +463,7 @@ export function commentCountsByFile(session: Thread, rows: DiffRow[]): Map<strin
|
|
|
448
463
|
const file = rows[discussion.blockIndex]?.file;
|
|
449
464
|
|
|
450
465
|
if (file === undefined) continue;
|
|
466
|
+
|
|
451
467
|
counts.set(file, (counts.get(file) ?? 0) + discussion.annotations.length);
|
|
452
468
|
}
|
|
453
469
|
|
|
@@ -496,6 +512,7 @@ export function resolveReviewAnchorRow(
|
|
|
496
512
|
(side === "RIGHT" ? row.newLine === undefined : row.oldLine === undefined)
|
|
497
513
|
)
|
|
498
514
|
return [];
|
|
515
|
+
|
|
499
516
|
const context = diffRowAnchorContext(rows, rowIndex);
|
|
500
517
|
|
|
501
518
|
return [
|
|
@@ -508,6 +525,7 @@ export function resolveReviewAnchorRow(
|
|
|
508
525
|
});
|
|
509
526
|
|
|
510
527
|
if (candidates.length === 0) return undefined;
|
|
528
|
+
|
|
511
529
|
const bestScore = Math.max(...candidates.map((candidate) => candidate.score));
|
|
512
530
|
const best = candidates.filter((candidate) => candidate.score === bestScore);
|
|
513
531
|
|
package/src/diff/walk.ts
CHANGED
|
@@ -32,9 +32,12 @@ export function walkFiles(rows: DiffRow[]): WalkFile[] {
|
|
|
32
32
|
byPath.set(row.file, file);
|
|
33
33
|
files.push(file);
|
|
34
34
|
}
|
|
35
|
+
|
|
35
36
|
if (row.kind !== "add" && row.kind !== "del") continue;
|
|
37
|
+
|
|
36
38
|
if (row.kind === "add") file.added += 1;
|
|
37
39
|
else file.removed += 1;
|
|
40
|
+
|
|
38
41
|
if (file.preview.length < WALK_PREVIEW_ROWS) {
|
|
39
42
|
file.preview.push({
|
|
40
43
|
sign: row.kind === "add" ? "+" : "-",
|
|
@@ -33,9 +33,11 @@ export function resolveDisplayName(
|
|
|
33
33
|
const override = overrides[authorId];
|
|
34
34
|
|
|
35
35
|
if (override) return override;
|
|
36
|
+
|
|
36
37
|
const identity = participants?.find((participant) => participant.id === authorId);
|
|
37
38
|
|
|
38
39
|
if (identity?.name) return identity.name;
|
|
40
|
+
|
|
39
41
|
if (identity) return ANONYMOUS_LABEL;
|
|
40
42
|
|
|
41
43
|
return shortHandle(authorId);
|