@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.
- package/package.json +11 -11
- package/src/annotations/components/AnnotationCards.tsx +2 -2
- package/src/annotations/use-annotation-surface.tsx +13 -2
- package/src/app/App.deleted-scroll.test.tsx +1 -0
- package/src/app/App.diff.test.tsx +1 -0
- package/src/app/App.identity.test.tsx +1 -0
- package/src/app/App.regression.test.tsx +5 -0
- package/src/app/App.share-dialog.test.tsx +5 -0
- package/src/app/App.submit-confirm.test.tsx +3 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +6 -0
- package/src/app/App.visual.test.tsx +2 -0
- package/src/app/App.workbench.test.tsx +53 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/components/AppShell.tsx +1 -0
- package/src/app/run.ts +1 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +2 -0
- package/src/diff/components/AnnotatableFileView.tsx +2 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +1 -0
- package/src/diff/diff-intraline.test.ts +1 -0
- package/src/diff/diff-intraline.ts +2 -0
- package/src/diff/split-diff.ts +2 -0
- package/src/integrations/components/ShareDialog.tsx +1 -0
- package/src/markdown/components/ThreadView.tsx +2 -0
- package/src/markdown/mark-runs.ts +1 -0
- package/src/markdown/markdown-highlight.ts +2 -0
- package/src/performance/otlp-trace.test.ts +3 -0
- package/src/performance/otlp-trace.ts +4 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/settings/components/quick-actions-editor.test.tsx +3 -0
- package/src/stories/stories-app.tsx +3 -0
- package/src/stories/story.ts +2 -0
- package/src/terminal/pty.ts +1 -0
- package/src/thread/agent-transcript.ts +4 -0
- package/src/thread/components/AgentConfigControls.test.tsx +2 -0
- package/src/thread/components/AgentThreadPane.test.tsx +54 -5
- package/src/thread/components/AgentThreadPane.tsx +11 -0
- package/src/thread/components/ThreadFooter.test.tsx +2 -0
- package/src/thread/components/thread-tree.test.ts +3 -0
- package/src/thread/components/thread-tree.ts +4 -0
- package/src/thread/thread-controller.curate.test.ts +1 -0
- package/src/thread/thread-controller.live-diff.test.ts +7 -0
- package/src/thread/thread-controller.share.test.ts +1 -0
- package/src/thread/thread-controller.ts +8 -0
- package/src/thread/use-thread-agent.test.tsx +143 -0
- package/src/thread/use-thread-agent.ts +22 -12
- package/src/ui/components/PromptDialog.test.tsx +4 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/primitives/Dialog.test.tsx +1 -0
- package/src/workbench/components/ChangesColumn.tsx +1 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +2 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +1 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +1 -0
- package/src/workbench/components/editor-grid.test.ts +11 -0
- package/src/workbench/components/editor-grid.ts +10 -0
- 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("│");
|
package/src/app/app-screens.tsx
CHANGED
|
@@ -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)
|
|
@@ -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;
|
package/src/diff/split-diff.ts
CHANGED
|
@@ -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
|
|
package/src/stories/story.ts
CHANGED
|
@@ -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);
|
package/src/terminal/pty.ts
CHANGED
|
@@ -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
|
}
|