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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +5 -5
  2. package/src/annotations/annotation-palette.ts +1 -0
  3. package/src/annotations/components/AnnotationCards.tsx +4 -0
  4. package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
  5. package/src/annotations/discussions.ts +1 -0
  6. package/src/annotations/thread-selection.ts +6 -0
  7. package/src/annotations/use-annotation-surface.tsx +97 -0
  8. package/src/app/App.deleted-scroll.test.tsx +3 -0
  9. package/src/app/App.harness-settings.test.tsx +77 -0
  10. package/src/app/App.plan-render.test.tsx +2 -0
  11. package/src/app/App.plan-review.test.tsx +1 -0
  12. package/src/app/App.test.tsx +1 -0
  13. package/src/app/App.tsx +67 -1
  14. package/src/app/App.visual.test.tsx +1 -0
  15. package/src/app/App.walk.test.tsx +1 -0
  16. package/src/app/App.workbench.test.tsx +1 -0
  17. package/src/app/app-screens.tsx +1 -0
  18. package/src/app/app-view-model.ts +7 -0
  19. package/src/app/components/WorkspacePanels.tsx +1 -0
  20. package/src/app/intent-dispatch.test.ts +1 -0
  21. package/src/app/intent-dispatch.ts +26 -0
  22. package/src/app/ready-signal.ts +2 -0
  23. package/src/app/run.ts +2 -0
  24. package/src/app/serve.test.ts +4 -0
  25. package/src/diff/components/AnnotatableFileView.tsx +1 -0
  26. package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
  27. package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
  28. package/src/diff/components/DiffContentView.tsx +25 -0
  29. package/src/diff/components/diff-content-view-layout.ts +3 -0
  30. package/src/diff/diff-fold.ts +4 -0
  31. package/src/diff/diff-hunk-curate.ts +3 -0
  32. package/src/diff/diff-intraline.ts +6 -0
  33. package/src/diff/diff-syntax.ts +8 -0
  34. package/src/diff/split-diff.ts +6 -0
  35. package/src/diff/view-diff.ts +18 -0
  36. package/src/diff/walk.ts +3 -0
  37. package/src/integrations/attribution.ts +2 -0
  38. package/src/integrations/client-extension-registry.ts +4 -0
  39. package/src/integrations/clipboard.ts +1 -0
  40. package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
  41. package/src/integrations/components/ShareDialog.tsx +28 -0
  42. package/src/integrations/editor.ts +3 -0
  43. package/src/integrations/github-identity.test.ts +1 -0
  44. package/src/integrations/github-identity.ts +1 -0
  45. package/src/integrations/herdr.ts +2 -0
  46. package/src/integrations/pasted-image.ts +2 -0
  47. package/src/integrations/share.ts +4 -0
  48. package/src/keyboard/components/HandleChipsInput.tsx +4 -0
  49. package/src/keyboard/components/SlashComposer.tsx +3 -0
  50. package/src/keyboard/key-bindings.ts +3 -0
  51. package/src/keyboard/keymap.ts +51 -0
  52. package/src/keyboard/skills.ts +2 -0
  53. package/src/keyboard/slash-palette.ts +8 -0
  54. package/src/keyboard/thread-chords.ts +5 -0
  55. package/src/keyboard/use-shared-keyboard.ts +1 -0
  56. package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
  57. package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
  58. package/src/markdown/components/ThreadView.tsx +13 -0
  59. package/src/markdown/mark-runs.ts +4 -0
  60. package/src/markdown/markdown-editor-syntax-style.ts +1 -0
  61. package/src/markdown/markdown-grid.ts +6 -0
  62. package/src/markdown/markdown-highlight.ts +1 -0
  63. package/src/markdown/thread-code-syntax.ts +3 -0
  64. package/src/markdown/view-plan.ts +52 -0
  65. package/src/performance/perf-report.ts +1 -0
  66. package/src/performance/perf-timings.ts +1 -0
  67. package/src/prototype/components/PrototypeContentView.tsx +17 -0
  68. package/src/prototype/prototype-browser.ts +11 -0
  69. package/src/settings/components/SettingsDialog.tsx +3 -0
  70. package/src/settings/config.test.ts +41 -0
  71. package/src/settings/config.ts +44 -0
  72. package/src/settings/use-settings-dialog.test.tsx +98 -0
  73. package/src/settings/use-settings-dialog.tsx +38 -1
  74. package/src/settings/use-thread-harness-setting.test.tsx +87 -0
  75. package/src/settings/use-thread-harness-setting.ts +86 -0
  76. package/src/stories/stories.test.tsx +4 -0
  77. package/src/stories/story.ts +6 -0
  78. package/src/terminal/ansi-screen.ts +5 -0
  79. package/src/terminal/components/terminal-pane.ts +12 -0
  80. package/src/terminal/ghostty-terminal.ts +10 -0
  81. package/src/terminal/kitty-image.ts +3 -0
  82. package/src/terminal/pty.ts +9 -0
  83. package/src/testing/test-support.test.ts +2 -0
  84. package/src/testing/test-support.ts +16 -0
  85. package/src/thread/agent-continuation.test.ts +2 -0
  86. package/src/thread/agent-transcript.ts +7 -0
  87. package/src/thread/components/AgentConfigControls.tsx +1 -0
  88. package/src/thread/components/AgentThreadPane.test.tsx +31 -0
  89. package/src/thread/components/AgentThreadPane.tsx +12 -0
  90. package/src/thread/components/ThreadTree.tsx +1 -0
  91. package/src/thread/components/thread-tree.test.ts +1 -0
  92. package/src/thread/thread-controller.curate.test.ts +2 -0
  93. package/src/thread/thread-controller.ts +123 -0
  94. package/src/thread/tree-view.ts +4 -0
  95. package/src/thread/use-thread-agent.test.tsx +1 -0
  96. package/src/thread/use-thread-agent.ts +5 -0
  97. package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
  98. package/src/ui/components/OverlayScrollbar.tsx +5 -0
  99. package/src/ui/components/PromptDialog.tsx +2 -0
  100. package/src/ui/components/RootOverlay.tsx +1 -0
  101. package/src/ui/components/ScrollArea.tsx +2 -0
  102. package/src/ui/components/primitives/Button.tsx +1 -0
  103. package/src/ui/components/primitives/Tabs.tsx +1 -0
  104. package/src/ui/components/primitives/Tree.tsx +4 -0
  105. package/src/ui/components/primitives/tree-model.ts +7 -0
  106. package/src/ui/components/syntax-highlight.ts +5 -0
  107. package/src/ui/components/truncate-title.ts +1 -0
  108. package/src/ui/use-frame-measure.ts +3 -0
  109. package/src/ui/use-terminal-virtualizer.ts +6 -0
  110. package/src/workbench/components/ChangesColumn.tsx +7 -0
  111. package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
  112. package/src/workbench/components/EditorGrid.tsx +8 -0
  113. package/src/workbench/components/FileContentsView.tsx +2 -0
  114. package/src/workbench/components/GridTabContent.tsx +2 -0
  115. package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
  116. package/src/workbench/components/ProjectTreeView.tsx +8 -0
  117. package/src/workbench/components/TreePane.tsx +2 -0
  118. package/src/workbench/components/editor-grid.ts +3 -0
  119. package/src/workbench/components/file-tree.ts +2 -0
  120. package/src/workbench/use-changes-workbench.ts +5 -0
  121. package/src/workbench/use-remember-layout.ts +2 -0
  122. package/src/workbench/use-repo-changes.ts +2 -0
@@ -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
  })
@@ -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}`)));
@@ -81,6 +81,7 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
81
81
  </box>
82
82
  );
83
83
  }
84
+
84
85
  if (loaded.lines === null) {
85
86
  return (
86
87
  <box
@@ -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
  }
@@ -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
 
@@ -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) => {
@@ -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
  }
@@ -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);