@cueloop/client 0.1.0-alpha.97 → 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.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +31 -8
- package/src/annotations/use-annotation-surface.tsx +257 -31
- 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.thread-agent.test.ts +12 -0
- package/src/app/App.tsx +223 -97
- package/src/app/App.visual.test.tsx +2 -0
- package/src/app/App.workbench.test.tsx +53 -0
- package/src/app/app-screens.tsx +3 -0
- package/src/app/components/AppShell.tsx +1 -0
- package/src/app/components/AppShell.zoom.test.tsx +4 -3
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/run.ts +1 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +2 -0
- package/src/diff/components/AnnotatableFileView.tsx +8 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +10 -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 +62 -16
- 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/settings/config.test.ts +15 -0
- package/src/settings/config.ts +14 -4
- package/src/stories/__snapshots__/stories.test.tsx.snap +74 -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-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.test.tsx +2 -0
- package/src/thread/components/ThreadFooter.tsx +3 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +25 -5
- 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 +42 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- 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 +7 -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
|
@@ -160,6 +160,7 @@ export function NoThreadShell(props: {
|
|
|
160
160
|
inboxCursor: number;
|
|
161
161
|
/** Open a sidebar thread and move the cursor onto it, so the row shows its selected backdrop at once. */
|
|
162
162
|
onOpenThread: (id: string) => void;
|
|
163
|
+
onCreateThread?: () => void;
|
|
163
164
|
mode: Mode;
|
|
164
165
|
theme: Theme;
|
|
165
166
|
controller: ReviewController;
|
|
@@ -215,6 +216,7 @@ export function NoThreadShell(props: {
|
|
|
215
216
|
// The bare-launch shell is the same four panes as a thread: the Thread pane waits in its empty state
|
|
216
217
|
// and a disposable Welcome tab rides in the Changes editor until a thread or diff is opened.
|
|
217
218
|
const workbench = useChangesWorkbench({ seed: "welcome", layout });
|
|
219
|
+
|
|
218
220
|
// the vanilla shell remembers the composition the user leaves it in, for the next bare launch
|
|
219
221
|
useRememberLayout(
|
|
220
222
|
layout,
|
|
@@ -291,6 +293,7 @@ export function NoThreadShell(props: {
|
|
|
291
293
|
pinnedIds={pinnedIds}
|
|
292
294
|
width={30}
|
|
293
295
|
onSelect={onOpenThread}
|
|
296
|
+
onCreateThread={props.onCreateThread}
|
|
294
297
|
onRequestDelete={(id, title) =>
|
|
295
298
|
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
296
299
|
}
|
|
@@ -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]);
|
|
@@ -13,7 +13,7 @@ import { AppShell } from "./AppShell";
|
|
|
13
13
|
import { EditorGrid } from "../../workbench/components/EditorGrid";
|
|
14
14
|
import { changesTab, makeGroup, splitGroup } from "../../workbench/components/editor-grid";
|
|
15
15
|
import { NERD } from "../../ui/components/primitives/icons";
|
|
16
|
-
import { allowEventLoopUpdates } from "../../testing/test-support";
|
|
16
|
+
import { allowEventLoopUpdates, settle } from "../../testing/test-support";
|
|
17
17
|
|
|
18
18
|
const grid = makeGroup([changesTab()]);
|
|
19
19
|
|
|
@@ -59,8 +59,9 @@ async function zoomIconColumn(
|
|
|
59
59
|
{ width, height: 8 },
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Split widths commit through React after the frame measurement. Renderer idle alone
|
|
63
|
+
// can still expose the initial fractional split, before its whole-cell widths land.
|
|
64
|
+
await settle(setup);
|
|
64
65
|
const column = setup.captureCharFrame().split("\n")[0]!.indexOf(NERD.zoom);
|
|
65
66
|
|
|
66
67
|
setup.renderer.destroy();
|
|
@@ -62,6 +62,7 @@ const EMPTY_SNAPSHOT: ControllerSnapshot = {
|
|
|
62
62
|
function baseController(): ReviewController {
|
|
63
63
|
return {
|
|
64
64
|
readOnly: false,
|
|
65
|
+
createEmptyThread: mock(async () => {}),
|
|
65
66
|
subscribe: mock(() => () => {}),
|
|
66
67
|
getSnapshot: mock(() => EMPTY_SNAPSHOT),
|
|
67
68
|
connect: mock(),
|
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
|
{
|
|
@@ -34,6 +34,9 @@ export interface AnnotatableFileViewProps {
|
|
|
34
34
|
onAddComment: (anchor: Anchor, body: string) => void;
|
|
35
35
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
36
36
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
37
|
+
onInvoke?: () => void;
|
|
38
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
39
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
37
40
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
38
41
|
onExit: () => void;
|
|
39
42
|
theme?: Theme;
|
|
@@ -49,12 +52,14 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
|
|
|
49
52
|
const tokens = useComponentTheme(props.theme);
|
|
50
53
|
const [loaded, setLoaded] = useState<FileLoad | null>(null);
|
|
51
54
|
const loadRef = useRef(props.loadContents);
|
|
55
|
+
|
|
52
56
|
useEffect(() => {
|
|
53
57
|
loadRef.current = props.loadContents;
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
useEffect(() => {
|
|
57
61
|
let alive = true;
|
|
62
|
+
|
|
58
63
|
void loadRef.current(props.path).then(
|
|
59
64
|
(contents) => {
|
|
60
65
|
if (alive)
|
|
@@ -126,6 +131,9 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
|
|
|
126
131
|
}
|
|
127
132
|
onReply={props.onReply}
|
|
128
133
|
onUpdateAnnotation={props.onUpdateAnnotation}
|
|
134
|
+
onInvoke={props.onInvoke}
|
|
135
|
+
isAnnotationReadOnly={props.isAnnotationReadOnly}
|
|
136
|
+
annotationAction={props.annotationAction}
|
|
129
137
|
resolveAuthorLabel={props.resolveAuthorLabel}
|
|
130
138
|
onExit={props.onExit}
|
|
131
139
|
theme={props.theme}
|
|
@@ -98,6 +98,9 @@ export interface DiffContentViewProps {
|
|
|
98
98
|
onAnnotate: (span: TextSpan, body: string) => void;
|
|
99
99
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
100
100
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
101
|
+
onInvoke?: () => void;
|
|
102
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
103
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
101
104
|
/** The author's display name for a comment's hover tooltip. */
|
|
102
105
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
103
106
|
onNavCommand?: (key: KeyEvent, selection: TextSpan | null) => boolean;
|
|
@@ -542,6 +545,9 @@ export function DiffContentView({
|
|
|
542
545
|
onAnnotate,
|
|
543
546
|
onReply,
|
|
544
547
|
onUpdateAnnotation,
|
|
548
|
+
onInvoke,
|
|
549
|
+
isAnnotationReadOnly,
|
|
550
|
+
annotationAction,
|
|
545
551
|
resolveAuthorLabel,
|
|
546
552
|
onNavCommand,
|
|
547
553
|
onExit,
|
|
@@ -610,6 +616,9 @@ export function DiffContentView({
|
|
|
610
616
|
onAnnotate,
|
|
611
617
|
onReply,
|
|
612
618
|
onUpdateAnnotation,
|
|
619
|
+
onInvoke,
|
|
620
|
+
isAnnotationReadOnly,
|
|
621
|
+
annotationAction,
|
|
613
622
|
dragViewport: () => scrollBoxDragViewport(scrollRef.current),
|
|
614
623
|
resolveAuthorLabel: (annotation) => {
|
|
615
624
|
const label = resolveAuthorLabel?.(annotation);
|
|
@@ -804,6 +813,7 @@ export function DiffContentView({
|
|
|
804
813
|
</>
|
|
805
814
|
);
|
|
806
815
|
}
|
|
816
|
+
|
|
807
817
|
// stacked: caret bar, old new (each tinted red/green on its side), then the change sign;
|
|
808
818
|
// the numbers sit on the row's soft band, only the changed code carries the brighter backdrop
|
|
809
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
|
|