@cueloop/client 0.1.0-alpha.98 → 0.1.0-alpha.99

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 (60) hide show
  1. package/package.json +11 -11
  2. package/src/annotations/components/AnnotationCards.tsx +2 -2
  3. package/src/annotations/use-annotation-surface.tsx +13 -2
  4. package/src/app/App.deleted-scroll.test.tsx +1 -0
  5. package/src/app/App.diff.test.tsx +1 -0
  6. package/src/app/App.identity.test.tsx +1 -0
  7. package/src/app/App.regression.test.tsx +5 -0
  8. package/src/app/App.share-dialog.test.tsx +5 -0
  9. package/src/app/App.submit-confirm.test.tsx +3 -0
  10. package/src/app/App.test.tsx +1 -0
  11. package/src/app/App.tsx +6 -0
  12. package/src/app/App.visual.test.tsx +2 -0
  13. package/src/app/App.workbench.test.tsx +53 -0
  14. package/src/app/app-screens.tsx +1 -0
  15. package/src/app/components/AppShell.tsx +1 -0
  16. package/src/app/run.ts +1 -0
  17. package/src/diff/components/AnnotatableFileView.test.tsx +2 -0
  18. package/src/diff/components/AnnotatableFileView.tsx +2 -0
  19. package/src/diff/components/DiffContentView.scroll.test.tsx +1 -0
  20. package/src/diff/components/DiffContentView.tsx +1 -0
  21. package/src/diff/diff-intraline.test.ts +1 -0
  22. package/src/diff/diff-intraline.ts +2 -0
  23. package/src/diff/split-diff.ts +2 -0
  24. package/src/integrations/components/ShareDialog.tsx +1 -0
  25. package/src/markdown/components/ThreadView.tsx +2 -0
  26. package/src/markdown/mark-runs.ts +1 -0
  27. package/src/markdown/markdown-highlight.ts +2 -0
  28. package/src/performance/otlp-trace.test.ts +3 -0
  29. package/src/performance/otlp-trace.ts +4 -0
  30. package/src/performance/perf-report.ts +1 -0
  31. package/src/performance/perf-timings.ts +1 -0
  32. package/src/settings/components/quick-actions-editor.test.tsx +3 -0
  33. package/src/stories/stories-app.tsx +3 -0
  34. package/src/stories/story.ts +2 -0
  35. package/src/terminal/pty.ts +1 -0
  36. package/src/thread/agent-transcript.ts +4 -0
  37. package/src/thread/components/AgentConfigControls.test.tsx +2 -0
  38. package/src/thread/components/AgentThreadPane.test.tsx +54 -5
  39. package/src/thread/components/AgentThreadPane.tsx +11 -0
  40. package/src/thread/components/ThreadFooter.test.tsx +2 -0
  41. package/src/thread/components/thread-tree.test.ts +3 -0
  42. package/src/thread/components/thread-tree.ts +4 -0
  43. package/src/thread/thread-controller.curate.test.ts +1 -0
  44. package/src/thread/thread-controller.live-diff.test.ts +7 -0
  45. package/src/thread/thread-controller.share.test.ts +1 -0
  46. package/src/thread/thread-controller.ts +8 -0
  47. package/src/thread/use-thread-agent.test.tsx +143 -0
  48. package/src/thread/use-thread-agent.ts +22 -12
  49. package/src/ui/components/PromptDialog.test.tsx +4 -0
  50. package/src/ui/components/RootOverlay.tsx +1 -0
  51. package/src/ui/components/primitives/Dialog.test.tsx +1 -0
  52. package/src/workbench/components/ChangesColumn.tsx +1 -0
  53. package/src/workbench/components/ChangesFileTree.test.tsx +2 -0
  54. package/src/workbench/components/FileContentsView.tsx +2 -0
  55. package/src/workbench/components/GridTabContent.tsx +1 -0
  56. package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
  57. package/src/workbench/components/ProjectTreeView.tsx +1 -0
  58. package/src/workbench/components/editor-grid.test.ts +11 -0
  59. package/src/workbench/components/editor-grid.ts +10 -0
  60. package/src/workbench/use-changes-workbench.ts +5 -0
@@ -272,6 +272,7 @@ describe("the four-pane workbench", () => {
272
272
  workspace: { repoRoot: repo, branch: "main" },
273
273
  artifact: { type: "diff", content: "", meta: { title: "live changes", workbench: true } },
274
274
  });
275
+
275
276
  writeFileSync(join(repo, "README.md"), "# Edited\n");
276
277
  const setup = await renderReadyApp(
277
278
  <App home={home} sessionId={liveSession.id} layout={reviewLayout()} />,
@@ -336,6 +337,7 @@ describe("the four-pane workbench", () => {
336
337
  const setup = await renderApp();
337
338
 
338
339
  const frame = setup.captureCharFrame();
340
+
339
341
  expect(frame).toContain("cueloop");
340
342
  expect(frame).toContain("Changes");
341
343
  expect(frame).toContain("store.ts");
@@ -353,6 +355,7 @@ describe("the four-pane workbench", () => {
353
355
  await waitForText(setup, "README.md");
354
356
 
355
357
  const frame = setup.captureCharFrame();
358
+
356
359
  // the full tree is showing...
357
360
  expect(frame).toContain("README.md");
358
361
  expect(frame).toContain("src");
@@ -367,10 +370,12 @@ describe("the four-pane workbench", () => {
367
370
  await waitForText(setup, "README.md");
368
371
 
369
372
  const readme = locateText(setup, "README.md");
373
+
370
374
  await setup.mockMouse.click(readme.column, readme.row);
371
375
  await waitForText(setup, "Workbench Fixture");
372
376
 
373
377
  const frame = setup.captureCharFrame();
378
+
374
379
  // the contents tab carries the file's text with no diff markers
375
380
  expect(frame).toContain("A tiny tracked repo.");
376
381
  expect(frame.split("\n")[HEADER_ROW]!).toContain("README.md");
@@ -382,15 +387,18 @@ describe("the four-pane workbench", () => {
382
387
  await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
383
388
  await waitForText(setup, "README.md");
384
389
  const readme = locateText(setup, "README.md");
390
+
385
391
  await setup.mockMouse.click(readme.column, readme.row);
386
392
  await waitForState(setup, () =>
387
393
  setup.captureCharFrame().split("\n")[HEADER_ROW]!.includes("README.md"),
388
394
  );
389
395
 
390
396
  const folder = locateText(setup, "src");
397
+
391
398
  await setup.mockMouse.click(folder.column, folder.row);
392
399
  await waitForText(setup, "util.ts");
393
400
  const util = locateText(setup, "util.ts");
401
+
394
402
  await setup.mockMouse.click(util.column, util.row);
395
403
  await waitForState(setup, () => {
396
404
  const header = setup.captureCharFrame().split("\n")[HEADER_ROW]!;
@@ -419,15 +427,18 @@ describe("the four-pane workbench", () => {
419
427
  await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
420
428
  await waitForText(setup, "README.md");
421
429
  const readme = locateText(setup, "README.md");
430
+
422
431
  await setup.mockMouse.click(readme.column, readme.row);
423
432
  await waitForText(setup, "Workbench Fixture");
424
433
 
425
434
  // the split control appears only on a file tab
426
435
  const split = locateText(setup, "split");
436
+
427
437
  await setup.mockMouse.click(split.column, split.row);
428
438
  await waitForText(setup, "right");
429
439
 
430
440
  const menu = setup.captureCharFrame();
441
+
431
442
  expect(menu).toContain("left");
432
443
  expect(menu).toContain("right");
433
444
  expect(menu).toContain("up");
@@ -450,11 +461,13 @@ describe("the four-pane workbench", () => {
450
461
  const setup = await renderApp();
451
462
 
452
463
  const changes = locateText(setup, "Changes");
464
+
453
465
  // the tab's close box sits just past the label; the ✕ is a hover affordance, the box always clicks
454
466
  await setup.mockMouse.click(changes.column + 8, changes.row);
455
467
  await waitForState(setup, () => !setup.captureCharFrame().includes("store.ts"));
456
468
 
457
469
  const frame = setup.captureCharFrame();
470
+
458
471
  // the Changes editor is gone...
459
472
  expect(frame).not.toContain("store.ts");
460
473
  // ...but the Project sidebar and its toggles remain
@@ -483,11 +496,13 @@ describe("the four-pane workbench", () => {
483
496
  // deep-linked into a session, the Threads sidebar starts collapsed: the brand rides in the
484
497
  // Thread header and the inbox groups are hidden
485
498
  const collapsed = setup.captureCharFrame();
499
+
486
500
  expect(collapsed).toContain("cueloop");
487
501
  expect(collapsed).not.toContain("Threads");
488
502
 
489
503
  // the sidebar toggle sits just left of the brand (one glyph plus its margin)
490
504
  const brand = locateText(setup, "cueloop");
505
+
491
506
  await setup.mockMouse.click(brand.column - 3, brand.row);
492
507
  await waitForText(setup, "Threads");
493
508
 
@@ -496,6 +511,7 @@ describe("the four-pane workbench", () => {
496
511
 
497
512
  // toggling again collapses it back
498
513
  const reopened = locateText(setup, "cueloop");
514
+
499
515
  await setup.mockMouse.click(reopened.column - 3, reopened.row);
500
516
  await waitForState(setup, () => !setup.captureCharFrame().includes("Threads"));
501
517
  expect(setup.captureCharFrame()).not.toContain("Threads");
@@ -505,10 +521,12 @@ describe("the four-pane workbench", () => {
505
521
  const setup = await renderApp();
506
522
 
507
523
  const zoom = locateText(setup, NERD.zoom);
524
+
508
525
  await setup.mockMouse.click(zoom.column, zoom.row);
509
526
  await waitForState(setup, () => !setup.captureCharFrame().includes("Select a thread"));
510
527
 
511
528
  const frame = setup.captureCharFrame();
529
+
512
530
  // the Thread pane's prompt is gone...
513
531
  expect(frame).not.toContain("Select a thread");
514
532
  // ...while the Changes editor and Project sidebar stay
@@ -525,9 +543,11 @@ describe("the four-pane workbench", () => {
525
543
  height: 20,
526
544
  },
527
545
  );
546
+
528
547
  await waitForText(setup, "store.ts");
529
548
 
530
549
  const zoom = locateText(setup, NERD.zoom);
550
+
531
551
  await setup.mockMouse.click(zoom.column, zoom.row);
532
552
  await waitForState(
533
553
  setup,
@@ -554,20 +574,24 @@ describe("the four-pane workbench", () => {
554
574
  // open the collapsed Threads sidebar first (its toggle sits three cells left of the brand); the
555
575
  // brand rides the Thread header, which zoom then hides, so it must be reached before zooming
556
576
  const brand = locateText(setup, "cueloop");
577
+
557
578
  await setup.mockMouse.click(brand.column - 3, brand.row);
558
579
  await waitForText(setup, "Zoomed Plan");
559
580
 
560
581
  // zoom the diff - the Thread pane hides and the footer rides the Changes pane
561
582
  const zoom = locateText(setup, NERD.zoom);
583
+
562
584
  await setup.mockMouse.click(zoom.column, zoom.row);
563
585
  await waitForState(setup, () => !setup.captureCharFrame().includes("Select a thread"));
564
586
 
565
587
  // switch to the plan thread: it has no Changes editor, so a stranded zoom would blank the middle
566
588
  const planRow = locateText(setup, "Zoomed Plan");
589
+
567
590
  await setup.mockMouse.click(planRow.column, planRow.row);
568
591
  await waitForText(setup, "Review this plan.");
569
592
 
570
593
  const frame = setup.captureCharFrame();
594
+
571
595
  // the Thread pane is back with the plan, and the diff's Changes editor is gone - never a blank middle
572
596
  expect(frame).toContain("Review this plan.");
573
597
  expect(frame).not.toContain("store.ts");
@@ -578,6 +602,7 @@ describe("the four-pane workbench", () => {
578
602
 
579
603
  // zoom the Changes editor - the Thread pane hides
580
604
  const zoom = locateText(setup, NERD.zoom);
605
+
581
606
  await setup.mockMouse.click(zoom.column, zoom.row);
582
607
  await waitForState(setup, () => !setup.captureCharFrame().includes("Select a thread"));
583
608
 
@@ -585,6 +610,7 @@ describe("the four-pane workbench", () => {
585
610
  await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
586
611
  await waitForText(setup, "Select a thread");
587
612
  const frame = setup.captureCharFrame();
613
+
588
614
  expect(frame).toContain("Select a thread");
589
615
  // the right region is collapsed and the Changes editor is gone
590
616
  expect(frame).not.toContain("store.ts");
@@ -634,6 +660,7 @@ describe("the bare-launch welcome shell", () => {
634
660
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
635
661
  { width: 180, height: 14 },
636
662
  );
663
+
637
664
  await waitForText(setup, "cueloop");
638
665
 
639
666
  return setup;
@@ -678,11 +705,13 @@ describe("the bare-launch welcome shell", () => {
678
705
  await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
679
706
  await waitForText(setup, "README.md");
680
707
  const readme = locateText(setup, "README.md");
708
+
681
709
  await setup.mockMouse.click(readme.column, readme.row);
682
710
  await waitForText(setup, "edited in the working tree");
683
711
 
684
712
  // comment on a line - the first note find-or-creates the workbench thread
685
713
  const line = "edited in the working tree";
714
+
686
715
  await dragText(setup, line, line, line.length);
687
716
  await typeText(setup, "workbench note");
688
717
  await pressKey(setup, "RETURN", { meta: true });
@@ -694,6 +723,7 @@ describe("the bare-launch welcome shell", () => {
694
723
  const workbench = welcomeServer.core
695
724
  .sessionList()
696
725
  .find((thread) => thread.artifact.meta.workbench === true)!;
726
+
697
727
  expect(workbench.workspace.rootCommit).toBeTruthy();
698
728
  await waitForState(setup, () =>
699
729
  welcomeServer.core
@@ -780,6 +810,7 @@ describe("the bare-launch welcome shell", () => {
780
810
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} layout={reviewLayout()} />,
781
811
  { width: 180, height: 14 },
782
812
  );
813
+
783
814
  await waitForText(setup, "README.md");
784
815
  expect(setup.captureCharFrame()).toContain("README.md");
785
816
  });
@@ -789,6 +820,7 @@ describe("the bare-launch welcome shell", () => {
789
820
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} layout={planLayout()} />,
790
821
  { width: 180, height: 14 },
791
822
  );
823
+
792
824
  await waitForText(setup, "cueloop");
793
825
  // rightSidebar "off": neither the Changes list nor the launch repo's files paint
794
826
  expect(setup.captureCharFrame()).not.toContain("README.md");
@@ -806,6 +838,7 @@ describe("the bare-launch welcome shell", () => {
806
838
  />,
807
839
  { width: 180, height: 14 },
808
840
  );
841
+
809
842
  await waitForText(setup, "Select a thread");
810
843
  expect(setup.captureCharFrame()).toContain("Select a thread");
811
844
  });
@@ -820,6 +853,7 @@ describe("the bare-launch welcome shell", () => {
820
853
  />,
821
854
  { width: 180, height: 14 },
822
855
  );
856
+
823
857
  await waitForText(setup, "README.md");
824
858
 
825
859
  // close the Changes/Project region; the remembered layout follows it to "off"
@@ -842,11 +876,13 @@ describe("the bare-launch welcome shell", () => {
842
876
 
843
877
  await waitForText(setup, "README.md");
844
878
  const readme = locateText(setup, "README.md");
879
+
845
880
  await setup.mockMouse.click(readme.column, readme.row);
846
881
 
847
882
  // a diff shows both the removed old line and the added new line; contents would show only the new
848
883
  await waitForText(setup, "A tiny tracked repo.");
849
884
  const frame = setup.captureCharFrame();
885
+
850
886
  expect(frame).toContain("A tiny tracked repo.");
851
887
  expect(frame).toContain("edited in the working tree");
852
888
  });
@@ -856,8 +892,10 @@ describe("the bare-launch welcome shell", () => {
856
892
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
857
893
  { width: 160, height: 28 },
858
894
  );
895
+
859
896
  await waitForText(setup, "quick brown fox");
860
897
  const line = locateText(setup, "quick brown fox");
898
+
861
899
  await setup.mockMouse.click(line.column, line.row);
862
900
 
863
901
  // esc enters nav; the footer names what this surface can do, not the diff commands
@@ -892,11 +930,14 @@ describe("the bare-launch welcome shell", () => {
892
930
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
893
931
  { width: 160, height: 28 },
894
932
  );
933
+
895
934
  await waitForText(setup, "README.md");
896
935
  const readme = locateText(setup, "README.md");
936
+
897
937
  await setup.mockMouse.click(readme.column, readme.row);
898
938
  await waitForText(setup, "edited in the working tree");
899
939
  const line = locateText(setup, "edited in the working tree");
940
+
900
941
  await setup.mockMouse.click(line.column, line.row);
901
942
 
902
943
  await press(setup, "escape");
@@ -905,6 +946,7 @@ describe("the bare-launch welcome shell", () => {
905
946
  await pressKey(setup, "x");
906
947
  await waitForText(setup, "● x");
907
948
  const frame = setup.captureCharFrame();
949
+
908
950
  expect(frame).toContain("edited in the working tree");
909
951
  expect(frame).not.toContain("change rejected");
910
952
 
@@ -916,13 +958,16 @@ describe("the bare-launch welcome shell", () => {
916
958
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
917
959
  { width: 160, height: 28 },
918
960
  );
961
+
919
962
  await waitForText(setup, "README.md");
920
963
  await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
921
964
  await waitForText(setup, "README.md");
922
965
  const readme = locateText(setup, "README.md");
966
+
923
967
  await setup.mockMouse.click(readme.column, readme.row);
924
968
  await waitForText(setup, "edited in the working tree");
925
969
  const line = locateText(setup, "edited in the working tree");
970
+
926
971
  await setup.mockMouse.click(line.column, line.row);
927
972
 
928
973
  await press(setup, "escape");
@@ -940,6 +985,7 @@ describe("the bare-launch welcome shell", () => {
940
985
  // the Welcome playground measures its width before it paints, so wait for its copy
941
986
  await waitForText(setup, "Getting started");
942
987
  const frame = setup.captureCharFrame();
988
+
943
989
  // the getting-started surface is an editor tab, not the thread pane
944
990
  expect(frame.split("\n")[HEADER_ROW]!).toContain("Welcome");
945
991
  expect(frame).toContain("Getting started");
@@ -952,6 +998,7 @@ describe("the bare-launch welcome shell", () => {
952
998
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
953
999
  { width: 160, height: 24 },
954
1000
  );
1001
+
955
1002
  await waitForText(setup, "Getting started");
956
1003
 
957
1004
  // focus starts on the Threads sidebar: a letter drives the inbox, it never drafts in the editor
@@ -960,6 +1007,7 @@ describe("the bare-launch welcome shell", () => {
960
1007
 
961
1008
  // clicking a line in the Welcome editor focuses that pane; now typing composes there
962
1009
  const line = locateText(setup, "quick brown fox");
1010
+
963
1011
  await setup.mockMouse.click(line.column, line.row);
964
1012
  await typeText(setup, "note");
965
1013
  await waitForText(setup, "● note");
@@ -977,6 +1025,7 @@ describe("the bare-launch welcome shell", () => {
977
1025
  <App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
978
1026
  { width: 160, height: 28 },
979
1027
  );
1028
+
980
1029
  await waitForText(setup, "quick brown fox");
981
1030
 
982
1031
  // open the playground composer, then type letters that are also inbox keys (j, k, d), one at a
@@ -999,6 +1048,7 @@ describe("the bare-launch welcome shell", () => {
999
1048
  const setup = await renderWelcome();
1000
1049
 
1001
1050
  const welcome = locateText(setup, "Welcome");
1051
+
1002
1052
  // the close box sits just past the label
1003
1053
  await setup.mockMouse.click(welcome.column + 8, welcome.row);
1004
1054
  await waitForState(
@@ -1008,6 +1058,7 @@ describe("the bare-launch welcome shell", () => {
1008
1058
 
1009
1059
  // the Threads sidebar and the Thread empty state remain - never a blank shell
1010
1060
  const frame = setup.captureCharFrame();
1061
+
1011
1062
  expect(frame).toContain("cueloop");
1012
1063
  expect(frame).toContain("Select a thread");
1013
1064
  });
@@ -1085,9 +1136,11 @@ describe("the bare-launch welcome shell", () => {
1085
1136
  const lines = setup.captureCharFrame().split("\n");
1086
1137
  // the reopen toggle's divider sits at the far right; find its column on the header row
1087
1138
  const headerDivider = lines[HEADER_ROW]!.lastIndexOf("│");
1139
+
1088
1140
  expect(headerDivider).toBeGreaterThan(0);
1089
1141
  // that column stays clear below the two-row header - no full-height column rule
1090
1142
  const bodyRows = lines.slice(3).filter((line) => line.length > headerDivider);
1143
+
1091
1144
  expect(bodyRows.length).toBeGreaterThan(0);
1092
1145
  for (const line of bodyRows) {
1093
1146
  expect(line[headerDivider]).not.toBe("│");
@@ -216,6 +216,7 @@ export function NoThreadShell(props: {
216
216
  // The bare-launch shell is the same four panes as a thread: the Thread pane waits in its empty state
217
217
  // and a disposable Welcome tab rides in the Changes editor until a thread or diff is opened.
218
218
  const workbench = useChangesWorkbench({ seed: "welcome", layout });
219
+
219
220
  // the vanilla shell remembers the composition the user leaves it in, for the next bare launch
220
221
  useRememberLayout(
221
222
  layout,
@@ -162,6 +162,7 @@ export function AppShell({
162
162
  }: AppShellProps): React.ReactNode {
163
163
  const { registry, context } = extensionInputs(extensionRegistry, extensionContext);
164
164
  const extensionSnapshot = useClientExtensionSnapshot(registry);
165
+
165
166
  useEffect(() => {
166
167
  if (extensionSnapshot.lastError) onExtensionError?.(extensionSnapshot.lastError);
167
168
  }, [extensionSnapshot.lastError, onExtensionError]);
package/src/app/run.ts CHANGED
@@ -53,6 +53,7 @@ export async function runClient(options: RunClientOptions): Promise<number> {
53
53
  renderer.useMouse = false;
54
54
  renderer.useMouse = true;
55
55
  });
56
+
56
57
  return new Promise<number>((resolve) => {
57
58
  let exited = false;
58
59
  const shutdown = async (code: number): Promise<void> => {
@@ -110,6 +110,7 @@ describe("AnnotatableFileView", () => {
110
110
  // Assert - the surface built an anchor quoting the file line and handed it up with the body
111
111
  expect(onAddComment).toHaveBeenCalled();
112
112
  const [anchor, body] = onAddComment.mock.calls[0]!;
113
+
113
114
  expect(anchor.quote).toContain("return a + b");
114
115
  expect(body).toContain("why not reduce?");
115
116
 
@@ -118,6 +119,7 @@ describe("AnnotatableFileView", () => {
118
119
 
119
120
  test("a head-side (deleted-line) note never rebinds onto the current file", async () => {
120
121
  const session = planSession();
122
+
121
123
  // a note left on the removed side of this file: the worktree view must not show it
122
124
  session.annotations = [
123
125
  {
@@ -52,12 +52,14 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
52
52
  const tokens = useComponentTheme(props.theme);
53
53
  const [loaded, setLoaded] = useState<FileLoad | null>(null);
54
54
  const loadRef = useRef(props.loadContents);
55
+
55
56
  useEffect(() => {
56
57
  loadRef.current = props.loadContents;
57
58
  });
58
59
 
59
60
  useEffect(() => {
60
61
  let alive = true;
62
+
61
63
  void loadRef.current(props.path).then(
62
64
  (contents) => {
63
65
  if (alive)
@@ -405,6 +405,7 @@ test("Up keeps the caret on the first screen row while wrapped lines scroll", as
405
405
  />,
406
406
  { width: 60, height: 12 },
407
407
  );
408
+
408
409
  await settle(setup);
409
410
  const found = findById(setup.renderer.root, "diff-scroll");
410
411
 
@@ -813,6 +813,7 @@ export function DiffContentView({
813
813
  </>
814
814
  );
815
815
  }
816
+
816
817
  // stacked: caret bar, old new (each tinted red/green on its side), then the change sign;
817
818
  // the numbers sit on the row's soft band, only the changed code carries the brighter backdrop
818
819
  return lineIndex === 0 ? (
@@ -225,6 +225,7 @@ describe("createIntralineResolver", () => {
225
225
  // Act - the resolver word-diffs only the one block whose row is asked for
226
226
  spy.mockClear();
227
227
  const resolver = createIntralineResolver(rows);
228
+
228
229
  resolver.runsForRow(1); // a row in block 0
229
230
  const oneBlockCalls = spy.mock.calls.length;
230
231
 
@@ -159,8 +159,10 @@ function lineWordSetCache(): (text: string) => ReadonlySet<string> {
159
159
 
160
160
  return (text) => {
161
161
  const cached = cache.get(text);
162
+
162
163
  if (cached !== undefined) return cached;
163
164
  const words = new Set(text.toLowerCase().split(/\s+/).filter(Boolean));
165
+
164
166
  cache.set(text, words);
165
167
 
166
168
  return words;
@@ -100,11 +100,13 @@ export function splitDiffRows(rows: DiffRow[]): SplitRow[] {
100
100
  if (row.kind === "add") {
101
101
  blockFile = row.file;
102
102
  pendingAdditions.push({ row, index });
103
+
103
104
  return;
104
105
  }
105
106
  if (row.kind === "del") {
106
107
  blockFile = row.file;
107
108
  pendingDeletions.push({ row, index });
109
+
108
110
  return;
109
111
  }
110
112
  // any non-change row closes the current change block before it renders
@@ -393,6 +393,7 @@ export function ShareDialog({
393
393
  const moveWizardFocus = (delta: number): void => {
394
394
  const ring = wizardRing();
395
395
  const at = Math.max(0, ring.indexOf(wizardFocus));
396
+
396
397
  setWizardFocus(ring[Math.min(ring.length - 1, Math.max(0, at + delta))] ?? "name");
397
398
  };
398
399
 
@@ -402,6 +402,7 @@ export function ThreadView({
402
402
 
403
403
  const blockNodeFor = (blockIndex: number): React.ReactNode => {
404
404
  const custom = renderBlock?.(blockIndex);
405
+
405
406
  if (custom !== undefined)
406
407
  return (
407
408
  <box key={`custom-${blockIndex}`} ref={virtual.measureRef(blockIndex)}>
@@ -465,6 +466,7 @@ export function ThreadView({
465
466
  const isLastLine = lineIndex === lines.length - 1;
466
467
 
467
468
  const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
469
+
468
470
  lineRows.push(
469
471
  lineRowFor(
470
472
  {
@@ -38,6 +38,7 @@ export function runsFor(text: string, ranges: MarkRange[]): Run[] {
38
38
 
39
39
  if (end <= start) continue;
40
40
  const covering = ranges.filter((range) => range.start <= start && end <= range.end);
41
+
41
42
  // the caret cell shows through a mark, so the head of a selection is visible
42
43
  runs.push({
43
44
  text: text.slice(start, end),
@@ -25,6 +25,7 @@ export interface MarkdownHighlightRange {
25
25
  /** The absolute source offset where each 0-based line begins. */
26
26
  function lineStartOffsets(source: string): number[] {
27
27
  const offsets = [0];
28
+
28
29
  for (let index = 0; index < source.length; index++) {
29
30
  if (source[index] === "\n") offsets.push(index + 1);
30
31
  }
@@ -39,6 +40,7 @@ const INLINE_TOKEN = /(`+)(?:.+?)\1|\[[^\]]+\]\([^)]+\)/g;
39
40
  function scanInlineTokens(text: string, base: number, into: MarkdownHighlightRange[]): void {
40
41
  INLINE_TOKEN.lastIndex = 0;
41
42
  let match: RegExpExecArray | null;
43
+
42
44
  while ((match = INLINE_TOKEN.exec(text)) !== null) {
43
45
  const token = match[0];
44
46
  const start = base + match.index;
@@ -64,6 +64,7 @@ test("phaseMarksToSpans builds a scope root with one child span per phase under
64
64
 
65
65
  expect(spans).toHaveLength(3);
66
66
  const [root, first, second] = spans;
67
+
67
68
  expect(root!.name).toBe("cueloop.startup");
68
69
  expect(root!.parentSpanId).toBeUndefined();
69
70
  expect(first!.name).toBe("cueloop.startup.renderer");
@@ -82,6 +83,7 @@ test("postOtlpTrace sends no request when the endpoint is unset", async () => {
82
83
  return new Response("{}");
83
84
  },
84
85
  });
86
+
85
87
  try {
86
88
  await postOtlpTrace("cueloop-client", [span]);
87
89
  } finally {
@@ -101,6 +103,7 @@ test("postOtlpTrace POSTs OTLP JSON to /v1/traces when the endpoint is set", asy
101
103
  return new Response("{}");
102
104
  },
103
105
  });
106
+
104
107
  // a trailing slash must not double up into "//v1/traces"
105
108
  process.env.OTEL_EXPORTER_OTLP_ENDPOINT = `http://127.0.0.1:${server.port}/`;
106
109
  try {
@@ -28,6 +28,7 @@ export interface TraceSpan {
28
28
 
29
29
  function randomHex(bytes: number): string {
30
30
  const buffer = new Uint8Array(bytes);
31
+
31
32
  crypto.getRandomValues(buffer);
32
33
 
33
34
  return Array.from(buffer, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -97,6 +98,7 @@ export function phaseMarksToSpans(scope: string, marks: readonly PhaseMark[]): T
97
98
  let cursorMs = startMs;
98
99
  const children = marks.map((mark) => {
99
100
  const phaseStartMs = cursorMs;
101
+
100
102
  cursorMs += mark.elapsedMs;
101
103
 
102
104
  return {
@@ -118,10 +120,12 @@ export async function postOtlpTrace(
118
120
  spans: readonly TraceSpan[],
119
121
  ): Promise<void> {
120
122
  const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
123
+
121
124
  if (endpoint === undefined || spans.length === 0) return;
122
125
 
123
126
  // strip a trailing slash so an endpoint like "http://host:4318/" does not become "//v1/traces"
124
127
  const base = endpoint.replace(/\/+$/, "");
128
+
125
129
  await fetch(`${base}/v1/traces`, {
126
130
  method: "POST",
127
131
  headers: { "content-type": "application/json" },
@@ -10,6 +10,7 @@ const PERF_SERVICE_NAME = "cueloop-client";
10
10
 
11
11
  export function reportPerfMarks(scope: string): void {
12
12
  const marks = takePerfMarks();
13
+
13
14
  if (marks.length === 0) return;
14
15
 
15
16
  if (process.env.CUELOOP_PERF === "1") process.stderr.write(formatPerfBlock(scope, marks));
@@ -18,6 +18,7 @@ function perfInstrumentationEnabled(): boolean {
18
18
  export function perfMark(label: string): void {
19
19
  if (!perfInstrumentationEnabled()) return;
20
20
  const now = performance.now();
21
+
21
22
  phaseMarks.push({ label, elapsedMs: now - (lastMarkAt ?? 0) });
22
23
  lastMarkAt = now;
23
24
  }
@@ -99,12 +99,15 @@ describe("editing an expanded action", () => {
99
99
  />,
100
100
  { width: 60, height: 8 },
101
101
  );
102
+
102
103
  await settle(setup);
103
104
 
104
105
  // clicking the title opens the title field; clicking the description opens the metadata field
105
106
  const title = locateText(setup, "Ship it");
107
+
106
108
  await setup.mockMouse.click(title.column, title.row);
107
109
  const description = locateText(setup, "be terse");
110
+
108
111
  await setup.mockMouse.click(description.column, description.row);
109
112
 
110
113
  expect(toggles).toEqual([
@@ -59,6 +59,7 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
59
59
  const toggleFolder = (id: string): void => {
60
60
  setExpandedIds((current) => {
61
61
  const next = new Set(current);
62
+
62
63
  if (next.has(id)) next.delete(id);
63
64
  else next.add(id);
64
65
 
@@ -73,6 +74,7 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
73
74
  const collapseFolder = (id: string): void => {
74
75
  setExpandedIds((current) => {
75
76
  const next = new Set(current);
77
+
76
78
  next.delete(id);
77
79
 
78
80
  return next;
@@ -83,6 +85,7 @@ function StoriesApp({ stories, onExit }: StoriesAppProps): React.ReactNode {
83
85
  const index = rows.findIndex((row) => row.id === selectedId);
84
86
  const nextIndex = Math.max(0, Math.min(rows.length - 1, index + delta));
85
87
  const nextRow = rows[nextIndex];
88
+
86
89
  if (nextRow !== undefined) setSelectedId(nextRow.id);
87
90
  };
88
91
 
@@ -97,6 +97,7 @@ function hoistSingleStoryFolders(nodes: TreeNode[]): TreeNode[] {
97
97
  if (node.children === undefined) return node;
98
98
  const children = hoistSingleStoryFolders(node.children);
99
99
  const onlyChild = children.length === 1 ? children[0] : undefined;
100
+
100
101
  if (
101
102
  onlyChild !== undefined &&
102
103
  onlyChild.children === undefined &&
@@ -126,6 +127,7 @@ export function buildStoryTree(stories: LoadedStory[]): TreeNode[] {
126
127
  for (const segment of [section, ...moduleTitle.split("/")]) {
127
128
  path = path === "" ? segment : `${path}/${segment}`;
128
129
  let folder = foldersByPath.get(path);
130
+
129
131
  if (folder === undefined) {
130
132
  folder = { id: path, label: segment, children: [] };
131
133
  foldersByPath.set(path, folder);
@@ -228,6 +228,7 @@ class Pty implements IPty {
228
228
  this.exitEvent.fire({ exitCode });
229
229
  } else {
230
230
  const delay = Math.min(READ_IDLE_MAX_MS, READ_IDLE_MS * 2 ** Math.min(idlePolls, 5));
231
+
231
232
  idlePolls += 1;
232
233
  await new Promise((resolve) => setTimeout(resolve, delay));
233
234
  }