@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.readonly.test.tsx +63 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +94 -12
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +3 -2
- package/src/app/app-view-model.ts +11 -1
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/index.ts +1 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +46 -1
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +79 -1
- package/src/settings/config.ts +91 -1
- package/src/settings/use-settings-dialog.test.tsx +172 -0
- package/src/settings/use-settings-dialog.tsx +115 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.test.tsx +41 -0
- package/src/thread/components/AgentConfigControls.tsx +8 -1
- package/src/thread/components/AgentThreadPane.test.tsx +91 -3
- package/src/thread/components/AgentThreadPane.tsx +61 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.share.test.ts +47 -2
- package/src/thread/thread-controller.ts +129 -4
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
collaboratorAnnotations,
|
|
50
50
|
mergeFromShare,
|
|
51
51
|
publishShare,
|
|
52
|
+
snapshotThreadConversation,
|
|
52
53
|
pullShare,
|
|
53
54
|
pushShare,
|
|
54
55
|
revokeShare,
|
|
@@ -435,11 +436,13 @@ class Controller implements ReviewController {
|
|
|
435
436
|
|
|
436
437
|
constructor(private readonly options: ReviewControllerOptions) {
|
|
437
438
|
this.client = options.initialClient ?? null;
|
|
439
|
+
|
|
438
440
|
if (options.initialSession?.id === options.sessionId && options.initialDiff) {
|
|
439
441
|
this.liveDiff = options.initialDiff;
|
|
440
442
|
this.initialDiffAvailable = true;
|
|
441
443
|
this.store.setState({ session: options.initialSession });
|
|
442
444
|
}
|
|
445
|
+
|
|
443
446
|
this.readOnly = options.readOnly ?? false;
|
|
444
447
|
this.clock = options.clock ?? new SystemClock();
|
|
445
448
|
this.shareTransport = options.shareTransport ?? DEFAULT_SHARE_TRANSPORT;
|
|
@@ -473,12 +476,14 @@ class Controller implements ReviewController {
|
|
|
473
476
|
const client = await openClient();
|
|
474
477
|
|
|
475
478
|
if (this.closed) return void client.close();
|
|
479
|
+
|
|
476
480
|
this.client = client;
|
|
477
481
|
client.onEvent((event) => {
|
|
478
482
|
// another controller/observer changed state: re-fetch the active session's content
|
|
479
483
|
const session = this.snapshot.session;
|
|
480
484
|
|
|
481
485
|
if (session && event.sessionId === session.id) void this.refreshSession(session.id);
|
|
486
|
+
|
|
482
487
|
// only the list-changing events touch the Threads sidebar; refreshing on every
|
|
483
488
|
// content edit (session.updated) would amplify an active review into a request storm
|
|
484
489
|
if (event.event !== "session.updated") void this.refreshInbox();
|
|
@@ -508,6 +513,7 @@ class Controller implements ReviewController {
|
|
|
508
513
|
|
|
509
514
|
private clearCountdown(): void {
|
|
510
515
|
if (this.countdown !== undefined) this.clock.clearTimeout(this.countdown);
|
|
516
|
+
|
|
511
517
|
this.countdown = undefined;
|
|
512
518
|
}
|
|
513
519
|
|
|
@@ -577,9 +583,11 @@ class Controller implements ReviewController {
|
|
|
577
583
|
const cached = this.derived.models.get(path);
|
|
578
584
|
|
|
579
585
|
if (cached) return cached;
|
|
586
|
+
|
|
580
587
|
const contents = this.derived.fileContents.get(path);
|
|
581
588
|
|
|
582
589
|
if (!contents) return undefined;
|
|
590
|
+
|
|
583
591
|
const model = parseFileDiff(contents);
|
|
584
592
|
|
|
585
593
|
this.derived.models.set(path, model);
|
|
@@ -594,6 +602,7 @@ class Controller implements ReviewController {
|
|
|
594
602
|
const liveDiff = frozenDiff ? null : this.liveDiff;
|
|
595
603
|
|
|
596
604
|
if (this.derivedFor === session && this.derivedForLiveDiff === liveDiff) return;
|
|
605
|
+
|
|
597
606
|
// reuse the parsed projection across an update that left the inputs unchanged; only the cheap tree can differ
|
|
598
607
|
if (session !== null && this.reusesDerived(session, liveDiff)) {
|
|
599
608
|
this.derivedFor = session;
|
|
@@ -601,11 +610,13 @@ class Controller implements ReviewController {
|
|
|
601
610
|
|
|
602
611
|
return;
|
|
603
612
|
}
|
|
613
|
+
|
|
604
614
|
// a different thread resets fold state; a same-thread re-derivation (content edit) keeps it
|
|
605
615
|
if (this.derivedFor?.id !== session?.id) {
|
|
606
616
|
this.collapsedFiles.clear();
|
|
607
617
|
this.expandedFiles.clear();
|
|
608
618
|
}
|
|
619
|
+
|
|
609
620
|
// returning to a recently-viewed thread whose inputs are unchanged reuses its parsed projection
|
|
610
621
|
const cached = session !== null ? this.derivedCache.get(session.id) : undefined;
|
|
611
622
|
|
|
@@ -624,9 +635,11 @@ class Controller implements ReviewController {
|
|
|
624
635
|
|
|
625
636
|
return;
|
|
626
637
|
}
|
|
638
|
+
|
|
627
639
|
this.derivedFor = session;
|
|
628
640
|
this.derivedForLiveDiff = liveDiff;
|
|
629
641
|
this.derived = this.buildDerived(session, frozenDiff, liveDiff);
|
|
642
|
+
|
|
630
643
|
if (session !== null) this.rememberDerived(session, liveDiff, this.derived);
|
|
631
644
|
}
|
|
632
645
|
|
|
@@ -646,6 +659,7 @@ class Controller implements ReviewController {
|
|
|
646
659
|
const oldest = this.derivedCache.keys().next().value;
|
|
647
660
|
|
|
648
661
|
if (oldest === undefined) break;
|
|
662
|
+
|
|
649
663
|
this.derivedCache.delete(oldest);
|
|
650
664
|
}
|
|
651
665
|
}
|
|
@@ -671,6 +685,7 @@ class Controller implements ReviewController {
|
|
|
671
685
|
|
|
672
686
|
rows(): DiffRow[] {
|
|
673
687
|
this.ensureDerived();
|
|
688
|
+
|
|
674
689
|
if (this.collapsedFiles.size === 0 && this.expandedFiles.size === 0) return this.derived.rows;
|
|
675
690
|
|
|
676
691
|
return applyFold(this.derived.rows, this.collapsedFiles, this.expandedFiles, this.foldFiles());
|
|
@@ -683,6 +698,7 @@ class Controller implements ReviewController {
|
|
|
683
698
|
} else {
|
|
684
699
|
this.collapsedFiles.delete(file);
|
|
685
700
|
}
|
|
701
|
+
|
|
686
702
|
this.update({});
|
|
687
703
|
}
|
|
688
704
|
|
|
@@ -697,6 +713,7 @@ class Controller implements ReviewController {
|
|
|
697
713
|
} else {
|
|
698
714
|
this.expandedFiles.delete(file);
|
|
699
715
|
}
|
|
716
|
+
|
|
700
717
|
this.update({});
|
|
701
718
|
}
|
|
702
719
|
|
|
@@ -770,9 +787,11 @@ class Controller implements ReviewController {
|
|
|
770
787
|
const session = this.snapshot.session;
|
|
771
788
|
|
|
772
789
|
if (readsFrozenDiff(session)) return session!.artifact.files ?? [];
|
|
790
|
+
|
|
773
791
|
const initialFiles = this.consumeInitialDiffFiles(session);
|
|
774
792
|
|
|
775
793
|
if (initialFiles !== null) return initialFiles;
|
|
794
|
+
|
|
776
795
|
// eager: capture the live working-tree diff so the Changes navigator and its file tabs render a real diff
|
|
777
796
|
if (this.client?.repoDiff !== undefined) {
|
|
778
797
|
const diff = await this.client.repoDiff(
|
|
@@ -784,6 +803,7 @@ class Controller implements ReviewController {
|
|
|
784
803
|
// showing changes from the wrong repo; compare the thread id, not the object, so an unrelated
|
|
785
804
|
// re-render that replaced the snapshot for the SAME thread does not drop its diff to "No changes"
|
|
786
805
|
if (this.snapshot.session?.id !== session?.id) return diff.files;
|
|
806
|
+
|
|
787
807
|
const changed =
|
|
788
808
|
this.liveDiff?.patch !== diff.patch ||
|
|
789
809
|
this.liveDiff.files.length !== diff.files.length ||
|
|
@@ -803,8 +823,10 @@ class Controller implements ReviewController {
|
|
|
803
823
|
|
|
804
824
|
return diff.files;
|
|
805
825
|
}
|
|
826
|
+
|
|
806
827
|
// a peer without the live-diff capability still lists changes, just without a diff body
|
|
807
828
|
if (this.client?.repoChanges === undefined) return [];
|
|
829
|
+
|
|
808
830
|
const changes = await this.client.repoChanges(this.sidebarRepoRoot());
|
|
809
831
|
|
|
810
832
|
return changes.map((change) => ({
|
|
@@ -817,6 +839,7 @@ class Controller implements ReviewController {
|
|
|
817
839
|
|
|
818
840
|
private consumeInitialDiffFiles(session: Thread | null): readonly DiffFileContents[] | null {
|
|
819
841
|
if (!this.initialDiffAvailable) return null;
|
|
842
|
+
|
|
820
843
|
this.initialDiffAvailable = false;
|
|
821
844
|
|
|
822
845
|
return session?.id === this.options.sessionId ? (this.liveDiff?.files ?? []) : null;
|
|
@@ -826,6 +849,7 @@ class Controller implements ReviewController {
|
|
|
826
849
|
const session = this.snapshot.session;
|
|
827
850
|
|
|
828
851
|
if (session?.artifact.type !== "diff" || !this.client?.sessionRefreshDiff) return;
|
|
852
|
+
|
|
829
853
|
try {
|
|
830
854
|
await this.client.sessionRefreshDiff(session.id);
|
|
831
855
|
} catch (cause) {
|
|
@@ -839,6 +863,7 @@ class Controller implements ReviewController {
|
|
|
839
863
|
private async refreshSession(id: string): Promise<void> {
|
|
840
864
|
try {
|
|
841
865
|
if (!this.client) return;
|
|
866
|
+
|
|
842
867
|
const session = await this.client.sessionGet(id);
|
|
843
868
|
|
|
844
869
|
// a rapid switch may have moved on during the fetch; never clobber the now-viewed thread
|
|
@@ -898,11 +923,13 @@ class Controller implements ReviewController {
|
|
|
898
923
|
// copy trails the daemon for a live diff, whose session.updated refreshes the open
|
|
899
924
|
// thread but not the list, so a returned-to diff would otherwise show stale content
|
|
900
925
|
if (cached) this.update({ session: cached });
|
|
926
|
+
|
|
901
927
|
void this.refreshSession(id);
|
|
902
928
|
}
|
|
903
929
|
|
|
904
930
|
async createEmptyThread(): Promise<void> {
|
|
905
931
|
if (this.readOnly || !this.client?.sessionCreate) return;
|
|
932
|
+
|
|
906
933
|
const workspace = this.snapshot.session?.workspace ?? {
|
|
907
934
|
repoRoot: this.options.cwd ?? process.cwd(),
|
|
908
935
|
branch: "detached",
|
|
@@ -927,6 +954,7 @@ class Controller implements ReviewController {
|
|
|
927
954
|
|
|
928
955
|
deleteSession(id: string): void {
|
|
929
956
|
if (this.readOnly) return this.setStatus("observer - read-only");
|
|
957
|
+
|
|
930
958
|
// a deleted thread's links must not outlive it; revoke every one first, best-effort, so an
|
|
931
959
|
// unreachable gateway never blocks the local delete (the blob's 30-day TTL is the backstop)
|
|
932
960
|
for (const link of this.linksFor(this.sessionRecord(id)))
|
|
@@ -952,6 +980,7 @@ class Controller implements ReviewController {
|
|
|
952
980
|
|
|
953
981
|
renameSession(id: string, title: string): void {
|
|
954
982
|
if (this.readOnly) return this.setStatus("observer - read-only");
|
|
983
|
+
|
|
955
984
|
void this.client
|
|
956
985
|
?.sessionSetTitle(id, title)
|
|
957
986
|
.catch((cause: unknown) =>
|
|
@@ -963,6 +992,7 @@ class Controller implements ReviewController {
|
|
|
963
992
|
const session = this.snapshot.session;
|
|
964
993
|
|
|
965
994
|
if (!session) return;
|
|
995
|
+
|
|
966
996
|
this.apply(this.client!.sessionSetSelfName(session.id, name));
|
|
967
997
|
}
|
|
968
998
|
|
|
@@ -970,9 +1000,11 @@ class Controller implements ReviewController {
|
|
|
970
1000
|
const session = this.snapshot.session;
|
|
971
1001
|
|
|
972
1002
|
if (!session || session.status === "resolved") return;
|
|
1003
|
+
|
|
973
1004
|
const block = this.display()[displayIndex];
|
|
974
1005
|
|
|
975
1006
|
if (!block) return;
|
|
1007
|
+
|
|
976
1008
|
const working = this.working();
|
|
977
1009
|
|
|
978
1010
|
if (start !== undefined && end !== undefined && block.work) {
|
|
@@ -980,6 +1012,7 @@ class Controller implements ReviewController {
|
|
|
980
1012
|
const endBlock = display[endDisplayIndex];
|
|
981
1013
|
|
|
982
1014
|
if (!endBlock?.work) return;
|
|
1015
|
+
|
|
983
1016
|
const exactSourceRange = this.exactCutSourceRange(
|
|
984
1017
|
session,
|
|
985
1018
|
display,
|
|
@@ -1007,6 +1040,7 @@ class Controller implements ReviewController {
|
|
|
1007
1040
|
const content = applyTextCuts(session.artifact.content, textCuts);
|
|
1008
1041
|
|
|
1009
1042
|
if (content === working) return;
|
|
1043
|
+
|
|
1010
1044
|
const expected: Thread = { ...session, workingCopy: content, textCuts };
|
|
1011
1045
|
|
|
1012
1046
|
if (textCuts.length === 0) {
|
|
@@ -1021,10 +1055,12 @@ class Controller implements ReviewController {
|
|
|
1021
1055
|
|
|
1022
1056
|
return;
|
|
1023
1057
|
}
|
|
1058
|
+
|
|
1024
1059
|
const range = renderedSpanToWork(display, displayIndex, endDisplayIndex, start, end);
|
|
1025
1060
|
const content = cutTextRange(working, block.work, range.start, endBlock.work, range.end);
|
|
1026
1061
|
|
|
1027
1062
|
if (content === working) return;
|
|
1063
|
+
|
|
1028
1064
|
this.setWorkingCopy(content);
|
|
1029
1065
|
} else if (block.type === "del") {
|
|
1030
1066
|
this.restoreDelBlock(block, displayIndex);
|
|
@@ -1035,6 +1071,7 @@ class Controller implements ReviewController {
|
|
|
1035
1071
|
|
|
1036
1072
|
private cutWholeBlock(session: Thread, block: DisplayBlock, working: string): void {
|
|
1037
1073
|
if (!block.work) return;
|
|
1074
|
+
|
|
1038
1075
|
const base = session.artifact.content;
|
|
1039
1076
|
const existingCuts = session.textCuts ?? [];
|
|
1040
1077
|
|
|
@@ -1044,6 +1081,7 @@ class Controller implements ReviewController {
|
|
|
1044
1081
|
const content = applyTextCuts(base, textCuts);
|
|
1045
1082
|
|
|
1046
1083
|
if (content === working) return;
|
|
1084
|
+
|
|
1047
1085
|
this.applyOptimistic(
|
|
1048
1086
|
{ ...session, workingCopy: content, textCuts },
|
|
1049
1087
|
this.client!.sessionSetWorkingCopy(session.id, content, textCuts),
|
|
@@ -1051,11 +1089,13 @@ class Controller implements ReviewController {
|
|
|
1051
1089
|
|
|
1052
1090
|
return;
|
|
1053
1091
|
}
|
|
1092
|
+
|
|
1054
1093
|
const workIndex = parseBlocks(working).findIndex(
|
|
1055
1094
|
(candidate) => candidate.lineStart === block.work!.lineStart,
|
|
1056
1095
|
);
|
|
1057
1096
|
|
|
1058
1097
|
if (workIndex === -1) return;
|
|
1098
|
+
|
|
1059
1099
|
this.applyOptimistic(
|
|
1060
1100
|
{ ...session, workingCopy: cutBlock(working, block.work) },
|
|
1061
1101
|
this.client!.sessionCutBlock(session.id, workIndex),
|
|
@@ -1074,10 +1114,12 @@ class Controller implements ReviewController {
|
|
|
1074
1114
|
const existingCuts = session.textCuts ?? [];
|
|
1075
1115
|
|
|
1076
1116
|
if ((session.workingCopy ?? base) !== applyTextCuts(base, existingCuts)) return null;
|
|
1117
|
+
|
|
1077
1118
|
const startBlock = display[displayIndex];
|
|
1078
1119
|
const endBlock = display[endDisplayIndex];
|
|
1079
1120
|
|
|
1080
1121
|
if (!startBlock?.base || !endBlock?.base) return null;
|
|
1122
|
+
|
|
1081
1123
|
if (existingCuts.length === 0) {
|
|
1082
1124
|
const workRange = renderedSpanToWork(
|
|
1083
1125
|
display,
|
|
@@ -1093,6 +1135,7 @@ class Controller implements ReviewController {
|
|
|
1093
1135
|
? null
|
|
1094
1136
|
: { start: sourceStart, end: sourceEnd };
|
|
1095
1137
|
}
|
|
1138
|
+
|
|
1096
1139
|
const startRange = baseRangeForRendered(
|
|
1097
1140
|
blockRuns(startBlock, true),
|
|
1098
1141
|
renderedStart,
|
|
@@ -1118,6 +1161,7 @@ class Controller implements ReviewController {
|
|
|
1118
1161
|
const session = this.snapshot.session;
|
|
1119
1162
|
|
|
1120
1163
|
if (!session || !block.base) return;
|
|
1164
|
+
|
|
1121
1165
|
const working = this.working();
|
|
1122
1166
|
const line = restoreLine(
|
|
1123
1167
|
nextWorkBlock(this.display(), displayIndex),
|
|
@@ -1129,12 +1173,14 @@ class Controller implements ReviewController {
|
|
|
1129
1173
|
);
|
|
1130
1174
|
|
|
1131
1175
|
if (baseIndex === -1) return;
|
|
1176
|
+
|
|
1132
1177
|
const expected: Thread = {
|
|
1133
1178
|
...session,
|
|
1134
1179
|
workingCopy: restoreBlock(base, working, block.base, line),
|
|
1135
1180
|
};
|
|
1136
1181
|
|
|
1137
1182
|
if (expected.workingCopy === undefined) delete expected.workingCopy;
|
|
1183
|
+
|
|
1138
1184
|
this.applyOptimistic(expected, this.client!.sessionRestoreBlock(session.id, baseIndex, line));
|
|
1139
1185
|
}
|
|
1140
1186
|
|
|
@@ -1144,11 +1190,13 @@ class Controller implements ReviewController {
|
|
|
1144
1190
|
const session = this.snapshot.session;
|
|
1145
1191
|
|
|
1146
1192
|
if (!session || session.status === "resolved") return null;
|
|
1193
|
+
|
|
1147
1194
|
if (!session.artifact.files?.length) {
|
|
1148
1195
|
this.setStatus("hunk curation needs full file contents (PR diffs cannot be curated)");
|
|
1149
1196
|
|
|
1150
1197
|
return null;
|
|
1151
1198
|
}
|
|
1199
|
+
|
|
1152
1200
|
const row = this.rows()[rowIndex];
|
|
1153
1201
|
|
|
1154
1202
|
if (!row || row.kind === "file" || row.kind === "hunk") {
|
|
@@ -1156,6 +1204,7 @@ class Controller implements ReviewController {
|
|
|
1156
1204
|
|
|
1157
1205
|
return null;
|
|
1158
1206
|
}
|
|
1207
|
+
|
|
1159
1208
|
const model = this.fileModel(row.file);
|
|
1160
1209
|
|
|
1161
1210
|
if (!model) return null;
|
|
@@ -1167,9 +1216,11 @@ class Controller implements ReviewController {
|
|
|
1167
1216
|
const located = this.curationRow(rowIndex);
|
|
1168
1217
|
|
|
1169
1218
|
if (!located) return;
|
|
1219
|
+
|
|
1170
1220
|
const target = hunkRejectionForRow(located.row.file, located.model, located.row);
|
|
1171
1221
|
|
|
1172
1222
|
if (!target) return this.setStatus("no hunk under the cursor");
|
|
1223
|
+
|
|
1173
1224
|
const wholeHunk = (rejection: HunkRejection): boolean => rejectsWholeHunk(rejection, target);
|
|
1174
1225
|
|
|
1175
1226
|
if (this.rejections.some(wholeHunk)) {
|
|
@@ -1189,14 +1240,18 @@ class Controller implements ReviewController {
|
|
|
1189
1240
|
const located = this.curationRow(rowIndex);
|
|
1190
1241
|
|
|
1191
1242
|
if (!located) return;
|
|
1243
|
+
|
|
1192
1244
|
if (located.row.kind !== "add" && located.row.kind !== "del")
|
|
1193
1245
|
return this.setStatus("move to a changed line to reject a change");
|
|
1246
|
+
|
|
1194
1247
|
const target = changeRejectionForRow(located.row.file, located.model, located.row);
|
|
1195
1248
|
|
|
1196
1249
|
if (!target) return this.setStatus("no change under the cursor");
|
|
1250
|
+
|
|
1197
1251
|
const wholeCovers = this.rejections.some((rejection) => rejectsWholeHunk(rejection, target));
|
|
1198
1252
|
|
|
1199
1253
|
if (wholeCovers) return this.setStatus("the whole hunk is rejected - restore it first");
|
|
1254
|
+
|
|
1200
1255
|
if (this.rejections.some((rejection) => sameRejection(rejection, target))) {
|
|
1201
1256
|
this.curate(this.rejections.filter((rejection) => !sameRejection(rejection, target)));
|
|
1202
1257
|
} else {
|
|
@@ -1209,15 +1264,19 @@ class Controller implements ReviewController {
|
|
|
1209
1264
|
const session = this.snapshot.session;
|
|
1210
1265
|
|
|
1211
1266
|
if (!session) return;
|
|
1267
|
+
|
|
1212
1268
|
const expected: Thread = { ...session, curation: rejections };
|
|
1213
1269
|
|
|
1214
1270
|
if (!rejections.length) delete expected.curation;
|
|
1271
|
+
|
|
1215
1272
|
this.applyOptimistic(expected, this.client!.sessionCurate(session.id, rejections));
|
|
1216
1273
|
}
|
|
1217
1274
|
|
|
1218
1275
|
rejectedRows(): Set<number> {
|
|
1219
1276
|
this.ensureDerived();
|
|
1277
|
+
|
|
1220
1278
|
if (!this.rejections.length) return EMPTY_REJECTED_ROWS;
|
|
1279
|
+
|
|
1221
1280
|
const rejected = new Set<number>();
|
|
1222
1281
|
|
|
1223
1282
|
this.derived.rows.forEach((row, index) => {
|
|
@@ -1244,10 +1303,12 @@ class Controller implements ReviewController {
|
|
|
1244
1303
|
const kept = this.rejections.filter((rejection) => curationItemId(rejection) !== id);
|
|
1245
1304
|
|
|
1246
1305
|
if (kept.length === this.rejections.length) return;
|
|
1306
|
+
|
|
1247
1307
|
this.curate(kept);
|
|
1248
1308
|
|
|
1249
1309
|
return;
|
|
1250
1310
|
}
|
|
1311
|
+
|
|
1251
1312
|
const display = this.display();
|
|
1252
1313
|
const displayIndex = display.findIndex(
|
|
1253
1314
|
(block) => block.type === "del" && block.base && planCutId(block.base) === id,
|
|
@@ -1259,6 +1320,7 @@ class Controller implements ReviewController {
|
|
|
1259
1320
|
/** The curated-out diff rejections as removal cards (newest decisions last). */
|
|
1260
1321
|
private diffCurationItems(): CurationItem[] {
|
|
1261
1322
|
if (!this.rejections.length) return EMPTY_CURATION_ITEMS;
|
|
1323
|
+
|
|
1262
1324
|
const items: CurationItem[] = [];
|
|
1263
1325
|
|
|
1264
1326
|
for (const rejection of this.rejections) {
|
|
@@ -1284,6 +1346,7 @@ class Controller implements ReviewController {
|
|
|
1284
1346
|
|
|
1285
1347
|
this.derived.display.forEach((block, displayIndex) => {
|
|
1286
1348
|
if (block.type !== "del" || !block.base) return;
|
|
1349
|
+
|
|
1287
1350
|
items.push({
|
|
1288
1351
|
id: planCutId(block.base),
|
|
1289
1352
|
source: "plan",
|
|
@@ -1300,10 +1363,12 @@ class Controller implements ReviewController {
|
|
|
1300
1363
|
const model = this.fileModel(rejection.path);
|
|
1301
1364
|
|
|
1302
1365
|
if (!model) return [];
|
|
1366
|
+
|
|
1303
1367
|
const rows: DiffRow[] = [];
|
|
1304
1368
|
|
|
1305
1369
|
for (const row of this.derived.rows) {
|
|
1306
1370
|
if (row.file !== rejection.path || (row.kind !== "add" && row.kind !== "del")) continue;
|
|
1371
|
+
|
|
1307
1372
|
if (rejection.changeIndex === undefined) {
|
|
1308
1373
|
const target = hunkRejectionForRow(rejection.path, model, row);
|
|
1309
1374
|
|
|
@@ -1322,6 +1387,7 @@ class Controller implements ReviewController {
|
|
|
1322
1387
|
const session = this.snapshot.session;
|
|
1323
1388
|
|
|
1324
1389
|
if (!session || session.status === "resolved") return;
|
|
1390
|
+
|
|
1325
1391
|
try {
|
|
1326
1392
|
const result = editInEditor(this.working(), "plan.md", { editor: this.editor });
|
|
1327
1393
|
|
|
@@ -1336,7 +1402,9 @@ class Controller implements ReviewController {
|
|
|
1336
1402
|
const session = this.snapshot.session;
|
|
1337
1403
|
|
|
1338
1404
|
if (!session || session.status === "resolved") return;
|
|
1405
|
+
|
|
1339
1406
|
if (content === this.working()) return;
|
|
1407
|
+
|
|
1340
1408
|
this.setWorkingCopy(content);
|
|
1341
1409
|
this.reconcileAnnotations(session, content);
|
|
1342
1410
|
}
|
|
@@ -1368,6 +1436,7 @@ class Controller implements ReviewController {
|
|
|
1368
1436
|
const session = this.snapshot.session;
|
|
1369
1437
|
|
|
1370
1438
|
if (!session) return undefined;
|
|
1439
|
+
|
|
1371
1440
|
let anchor;
|
|
1372
1441
|
let resolvedTarget = target;
|
|
1373
1442
|
// a file target (a Changes-panel diff note) and a diff artifact both anchor into the diff rows
|
|
@@ -1414,6 +1483,7 @@ class Controller implements ReviewController {
|
|
|
1414
1483
|
workIndexOf(endDisplayIndex),
|
|
1415
1484
|
);
|
|
1416
1485
|
}
|
|
1486
|
+
|
|
1417
1487
|
// absent target means the reviewed artifact, so keep artifact notes free of the field
|
|
1418
1488
|
const wire =
|
|
1419
1489
|
resolvedTarget.kind === "artifact"
|
|
@@ -1431,6 +1501,7 @@ class Controller implements ReviewController {
|
|
|
1431
1501
|
const session = this.snapshot.session;
|
|
1432
1502
|
|
|
1433
1503
|
if (!session) return undefined;
|
|
1504
|
+
|
|
1434
1505
|
const wire =
|
|
1435
1506
|
target.kind === "artifact"
|
|
1436
1507
|
? { id: newAnnotationId(), kind: "comment", anchor, body }
|
|
@@ -1446,7 +1517,9 @@ class Controller implements ReviewController {
|
|
|
1446
1517
|
/** Find-or-create the per-repo workbench thread and adopt it as active; true once a session exists. */
|
|
1447
1518
|
private async ensureWorkbenchSession(): Promise<boolean> {
|
|
1448
1519
|
if (this.snapshot.session) return true;
|
|
1520
|
+
|
|
1449
1521
|
if (this.client?.sessionWorkbench === undefined) return false;
|
|
1522
|
+
|
|
1450
1523
|
let workbench;
|
|
1451
1524
|
|
|
1452
1525
|
try {
|
|
@@ -1466,6 +1539,7 @@ class Controller implements ReviewController {
|
|
|
1466
1539
|
|
|
1467
1540
|
async commentOnWorkbench(anchor: Anchor, target: AnnotationTarget, body: string): Promise<void> {
|
|
1468
1541
|
if (!(await this.ensureWorkbenchSession())) return;
|
|
1542
|
+
|
|
1469
1543
|
this.addComment(anchor, target, body);
|
|
1470
1544
|
}
|
|
1471
1545
|
|
|
@@ -1478,6 +1552,7 @@ class Controller implements ReviewController {
|
|
|
1478
1552
|
body: string,
|
|
1479
1553
|
): Promise<void> {
|
|
1480
1554
|
if (!(await this.ensureWorkbenchSession())) return;
|
|
1555
|
+
|
|
1481
1556
|
// annotate re-derives the file path and rev from the diff row it lands on
|
|
1482
1557
|
this.annotate("comment", displayIndex, start, end, body, endDisplayIndex, {
|
|
1483
1558
|
kind: "file",
|
|
@@ -1491,6 +1566,7 @@ class Controller implements ReviewController {
|
|
|
1491
1566
|
const root = session?.annotations.find((annotation) => annotation.id === rootAnnotationId);
|
|
1492
1567
|
|
|
1493
1568
|
if (!session || !root) return undefined;
|
|
1569
|
+
|
|
1494
1570
|
// a reply to a reply still hangs off the discussion's root comment, and shares its target
|
|
1495
1571
|
// so it renders and resolves on the same surface
|
|
1496
1572
|
const base = {
|
|
@@ -1513,6 +1589,7 @@ class Controller implements ReviewController {
|
|
|
1513
1589
|
const session = this.snapshot.session;
|
|
1514
1590
|
|
|
1515
1591
|
if (!session) return undefined;
|
|
1592
|
+
|
|
1516
1593
|
const anchor = { quote, prefix: "", suffix: "", selector };
|
|
1517
1594
|
const wire = { id: newAnnotationId(), kind: "comment", anchor, body };
|
|
1518
1595
|
const persisted = this.client!.sessionComment(session.id, wire);
|
|
@@ -1527,9 +1604,11 @@ class Controller implements ReviewController {
|
|
|
1527
1604
|
const session = this.snapshot.session;
|
|
1528
1605
|
|
|
1529
1606
|
if (!session) return;
|
|
1607
|
+
|
|
1530
1608
|
const existing = session.annotations.find((annotation) => annotation.id === id);
|
|
1531
1609
|
|
|
1532
1610
|
if (!existing) return;
|
|
1611
|
+
|
|
1533
1612
|
// the daemon's annotate primitive upserts by id: same id, anchor, and
|
|
1534
1613
|
// reply link, new body
|
|
1535
1614
|
const wire: Omit<Annotation, "createdAt"> = {
|
|
@@ -1540,6 +1619,7 @@ class Controller implements ReviewController {
|
|
|
1540
1619
|
};
|
|
1541
1620
|
|
|
1542
1621
|
if (existing.replyTo !== undefined) wire.replyTo = existing.replyTo;
|
|
1622
|
+
|
|
1543
1623
|
const persisted = this.client!.sessionComment(session.id, wire);
|
|
1544
1624
|
|
|
1545
1625
|
this.applyOptimistic(withAnnotationUpserted(session, wire), persisted);
|
|
@@ -1559,6 +1639,7 @@ class Controller implements ReviewController {
|
|
|
1559
1639
|
);
|
|
1560
1640
|
|
|
1561
1641
|
if (links.length === 0) return;
|
|
1642
|
+
|
|
1562
1643
|
void persisted
|
|
1563
1644
|
.then(() => Promise.all(links.map((link) => this.shareTransport.push(link.id, [annotation]))))
|
|
1564
1645
|
.catch(() => {});
|
|
@@ -1568,6 +1649,7 @@ class Controller implements ReviewController {
|
|
|
1568
1649
|
const session = this.snapshot.session;
|
|
1569
1650
|
|
|
1570
1651
|
if (!session) return;
|
|
1652
|
+
|
|
1571
1653
|
this.apply(this.client!.sessionRemoveAnnotation(session.id, id));
|
|
1572
1654
|
}
|
|
1573
1655
|
|
|
@@ -1575,6 +1657,7 @@ class Controller implements ReviewController {
|
|
|
1575
1657
|
const session = this.snapshot.session;
|
|
1576
1658
|
|
|
1577
1659
|
if (!session) return;
|
|
1660
|
+
|
|
1578
1661
|
this.apply(this.client!.sessionSetWorkingCopy(session.id, content));
|
|
1579
1662
|
}
|
|
1580
1663
|
|
|
@@ -1591,9 +1674,11 @@ class Controller implements ReviewController {
|
|
|
1591
1674
|
const session = this.snapshot.session;
|
|
1592
1675
|
|
|
1593
1676
|
if (!session || session.artifact.type !== "diff") return;
|
|
1677
|
+
|
|
1594
1678
|
const files = this.files();
|
|
1595
1679
|
|
|
1596
1680
|
if (files.length === 0) return this.setStatus("nothing to walk - the diff is empty");
|
|
1681
|
+
|
|
1597
1682
|
// resume at the first unviewed file; a finished walk reopens on the end card
|
|
1598
1683
|
this.update({ walk: { index: firstUnviewedIndex(files, this.viewedSet()) } });
|
|
1599
1684
|
}
|
|
@@ -1603,16 +1688,19 @@ class Controller implements ReviewController {
|
|
|
1603
1688
|
const session = this.snapshot.session;
|
|
1604
1689
|
|
|
1605
1690
|
if (!walk || !session) return;
|
|
1691
|
+
|
|
1606
1692
|
const files = this.files();
|
|
1607
1693
|
const current = files[walk.index];
|
|
1608
1694
|
|
|
1609
1695
|
if (!current) return; // already on the end card
|
|
1696
|
+
|
|
1610
1697
|
// advancing IS the viewed mark: the step is complete once you move past
|
|
1611
1698
|
// it; the daemon primitive merges, so only the new path travels
|
|
1612
1699
|
if (!this.viewedSet().has(current.path)) {
|
|
1613
1700
|
this.locallyViewed.add(current.path);
|
|
1614
1701
|
this.apply(this.client!.sessionSetViewed(session.id, [current.path]));
|
|
1615
1702
|
}
|
|
1703
|
+
|
|
1616
1704
|
this.update({ walk: { index: walk.index + 1 } });
|
|
1617
1705
|
}
|
|
1618
1706
|
|
|
@@ -1620,6 +1708,7 @@ class Controller implements ReviewController {
|
|
|
1620
1708
|
const walk = this.snapshot.walk;
|
|
1621
1709
|
|
|
1622
1710
|
if (!walk) return;
|
|
1711
|
+
|
|
1623
1712
|
this.update({ walk: { index: Math.max(0, walk.index - 1) } });
|
|
1624
1713
|
}
|
|
1625
1714
|
|
|
@@ -1631,6 +1720,7 @@ class Controller implements ReviewController {
|
|
|
1631
1720
|
const session = this.snapshot.session;
|
|
1632
1721
|
|
|
1633
1722
|
if (!session) return;
|
|
1723
|
+
|
|
1634
1724
|
this.walkLeave();
|
|
1635
1725
|
const actionBodies = Object.fromEntries(
|
|
1636
1726
|
slashItemsFrom(this.quickActions).map((item) => [item.name, item.body]),
|
|
@@ -1641,14 +1731,17 @@ class Controller implements ReviewController {
|
|
|
1641
1731
|
// The completion overlay heading already states the message, so the
|
|
1642
1732
|
// status line stays empty here - only export/error messages fill it.
|
|
1643
1733
|
this.update({ session: resolved, status: "" });
|
|
1734
|
+
|
|
1644
1735
|
if (message === "comment") {
|
|
1645
1736
|
this.setStatus("comment sent - thread stays open");
|
|
1646
1737
|
|
|
1647
1738
|
return;
|
|
1648
1739
|
}
|
|
1740
|
+
|
|
1649
1741
|
// notes-vault export: guarded by each exporter's policy (default manual = no-op)
|
|
1650
1742
|
for (const exporter of this.exporters) {
|
|
1651
1743
|
if (!exporter.runsOn(message)) continue;
|
|
1744
|
+
|
|
1652
1745
|
void exporter.run(resolved).then((exportResult) => {
|
|
1653
1746
|
this.setStatus(
|
|
1654
1747
|
exportResult.success && exportResult.path
|
|
@@ -1672,12 +1765,13 @@ class Controller implements ReviewController {
|
|
|
1672
1765
|
}
|
|
1673
1766
|
|
|
1674
1767
|
/** A workbench thread renders live locally; freeze its diff so a remote reviewer gets a stable snapshot. */
|
|
1675
|
-
private freezeForShare(session: Thread): Promise<Thread> {
|
|
1768
|
+
private async freezeForShare(session: Thread): Promise<Thread> {
|
|
1676
1769
|
const repoDiff = this.client?.repoDiff;
|
|
1770
|
+
const shared = repoDiff
|
|
1771
|
+
? await snapshotWorkbench(session, (root, vcs) => repoDiff.call(this.client, root, vcs))
|
|
1772
|
+
: session;
|
|
1677
1773
|
|
|
1678
|
-
return
|
|
1679
|
-
? snapshotWorkbench(session, (root, vcs) => repoDiff.call(this.client, root, vcs))
|
|
1680
|
-
: Promise.resolve(session);
|
|
1774
|
+
return this.client ? snapshotThreadConversation(shared, this.client) : shared;
|
|
1681
1775
|
}
|
|
1682
1776
|
|
|
1683
1777
|
share(): void {
|
|
@@ -1690,9 +1784,11 @@ class Controller implements ReviewController {
|
|
|
1690
1784
|
|
|
1691
1785
|
createShareLink(input: NewShareLink): void {
|
|
1692
1786
|
if (this.readOnly) return this.setStatus("observer - read-only");
|
|
1787
|
+
|
|
1693
1788
|
const session = this.snapshot.session;
|
|
1694
1789
|
|
|
1695
1790
|
if (!session || !this.client) return;
|
|
1791
|
+
|
|
1696
1792
|
const client = this.client;
|
|
1697
1793
|
const shareBranch = session.history?.branch ?? MAIN_BRANCH;
|
|
1698
1794
|
|
|
@@ -1714,6 +1810,7 @@ class Controller implements ReviewController {
|
|
|
1714
1810
|
const id = this.shareTransport.parseShareId(line);
|
|
1715
1811
|
|
|
1716
1812
|
if (!id) throw new Error("gateway returned no share id");
|
|
1813
|
+
|
|
1717
1814
|
const link: ShareLink = {
|
|
1718
1815
|
id,
|
|
1719
1816
|
name: input.name?.trim() || undefined,
|
|
@@ -1723,6 +1820,7 @@ class Controller implements ReviewController {
|
|
|
1723
1820
|
};
|
|
1724
1821
|
|
|
1725
1822
|
if (input.agentEnabled !== undefined) link.agentEnabled = input.agentEnabled;
|
|
1823
|
+
|
|
1726
1824
|
await client.sessionSetShares(session.id, [...this.linksFor(this.snapshot.session), link]);
|
|
1727
1825
|
this.setStatus("");
|
|
1728
1826
|
this.showToast(line, copied ? "link copied" : "link created");
|
|
@@ -1734,21 +1832,26 @@ class Controller implements ReviewController {
|
|
|
1734
1832
|
|
|
1735
1833
|
updateShareLink(id: string, input: NewShareLink): void {
|
|
1736
1834
|
if (this.readOnly) return this.setStatus("observer - read-only");
|
|
1835
|
+
|
|
1737
1836
|
const session = this.snapshot.session;
|
|
1738
1837
|
|
|
1739
1838
|
if (!session || !this.client) return;
|
|
1839
|
+
|
|
1740
1840
|
const links = this.linksFor(session);
|
|
1741
1841
|
|
|
1742
1842
|
if (!links.some((link) => link.id === id)) return;
|
|
1843
|
+
|
|
1743
1844
|
const allowlist = input.requireAuth ? input.allowlist : [];
|
|
1744
1845
|
|
|
1745
1846
|
const policy: SharePolicy = { requireAuth: input.requireAuth, allowlist };
|
|
1746
1847
|
|
|
1747
1848
|
if (input.agentEnabled !== undefined) policy.agentEnabled = input.agentEnabled;
|
|
1849
|
+
|
|
1748
1850
|
// Publish the same policy stored on the local link.
|
|
1749
1851
|
void this.shareTransport.push(id, [], policy).catch(() => {});
|
|
1750
1852
|
const next = links.map((link) => {
|
|
1751
1853
|
if (link.id !== id) return link;
|
|
1854
|
+
|
|
1752
1855
|
const updated = {
|
|
1753
1856
|
...link,
|
|
1754
1857
|
name: input.name?.trim() || undefined,
|
|
@@ -1769,9 +1872,11 @@ class Controller implements ReviewController {
|
|
|
1769
1872
|
|
|
1770
1873
|
deleteShareLink(id: string): void {
|
|
1771
1874
|
if (this.readOnly) return this.setStatus("observer - read-only");
|
|
1875
|
+
|
|
1772
1876
|
const session = this.snapshot.session;
|
|
1773
1877
|
|
|
1774
1878
|
if (!session || !this.client) return;
|
|
1879
|
+
|
|
1775
1880
|
const remaining = this.linksFor(session).filter((link) => link.id !== id);
|
|
1776
1881
|
|
|
1777
1882
|
void this.shareTransport.revoke(id).catch(() => {});
|
|
@@ -1792,10 +1897,12 @@ class Controller implements ReviewController {
|
|
|
1792
1897
|
/** Stop sharing the open thread: revoke every link so none of them resolve. */
|
|
1793
1898
|
unshare(): void {
|
|
1794
1899
|
if (this.readOnly) return this.setStatus("observer - read-only");
|
|
1900
|
+
|
|
1795
1901
|
const session = this.snapshot.session;
|
|
1796
1902
|
const links = this.linksFor(session);
|
|
1797
1903
|
|
|
1798
1904
|
if (!session || links.length === 0) return this.setStatus("this thread is not shared");
|
|
1905
|
+
|
|
1799
1906
|
this.stopShareSync();
|
|
1800
1907
|
void Promise.all(links.map((link) => this.shareTransport.revoke(link.id).catch(() => {})));
|
|
1801
1908
|
void this.client?.sessionSetShares(session.id, []).catch(() => {});
|
|
@@ -1805,10 +1912,13 @@ class Controller implements ReviewController {
|
|
|
1805
1912
|
const session = this.snapshot.session;
|
|
1806
1913
|
|
|
1807
1914
|
if (!session?.history || !this.client) return;
|
|
1915
|
+
|
|
1808
1916
|
const target = entryTarget(session.history, entryId);
|
|
1809
1917
|
|
|
1810
1918
|
if (target === null) return this.setStatus("that entry is not on any branch");
|
|
1919
|
+
|
|
1811
1920
|
if (target.kind === "here") return this.setStatus("already at the tip");
|
|
1921
|
+
|
|
1812
1922
|
if (target.kind === "switch") {
|
|
1813
1923
|
this.moveTree(session, switchBranch(session.history, target.branch), () =>
|
|
1814
1924
|
this.client!.sessionSwitch(session.id, target.branch),
|
|
@@ -1816,6 +1926,7 @@ class Controller implements ReviewController {
|
|
|
1816
1926
|
|
|
1817
1927
|
return;
|
|
1818
1928
|
}
|
|
1929
|
+
|
|
1819
1930
|
// an entry another branch reaches: the daemon stands on that branch first, in the same request
|
|
1820
1931
|
const onBranch = switchBranch(session.history, target.branch);
|
|
1821
1932
|
const options = summary === undefined || summary === "" ? {} : { summary };
|
|
@@ -1832,9 +1943,12 @@ class Controller implements ReviewController {
|
|
|
1832
1943
|
const branchName = name.trim();
|
|
1833
1944
|
|
|
1834
1945
|
if (!session?.history || !this.client) return;
|
|
1946
|
+
|
|
1835
1947
|
if (!branchName) return this.setStatus("a branch needs a name");
|
|
1948
|
+
|
|
1836
1949
|
if (session.history.tips[branchName] !== undefined)
|
|
1837
1950
|
return this.setStatus(`branch ${branchName} exists`);
|
|
1951
|
+
|
|
1838
1952
|
this.moveTree(session, createBranch(session.history, branchName), () =>
|
|
1839
1953
|
this.client!.sessionBranch(session.id, branchName),
|
|
1840
1954
|
);
|
|
@@ -1845,7 +1959,9 @@ class Controller implements ReviewController {
|
|
|
1845
1959
|
const name = label.trim();
|
|
1846
1960
|
|
|
1847
1961
|
if (!session?.history || !this.client) return;
|
|
1962
|
+
|
|
1848
1963
|
if (!name) return this.setStatus("a checkpoint needs a name");
|
|
1964
|
+
|
|
1849
1965
|
this.moveTree(session, labelTip(session.history, name), () =>
|
|
1850
1966
|
this.client!.sessionLabel(session.id, name),
|
|
1851
1967
|
);
|
|
@@ -1855,6 +1971,7 @@ class Controller implements ReviewController {
|
|
|
1855
1971
|
const session = this.snapshot.session;
|
|
1856
1972
|
|
|
1857
1973
|
if (!session || !this.client) return;
|
|
1974
|
+
|
|
1858
1975
|
this.client
|
|
1859
1976
|
.sessionFork(session.id)
|
|
1860
1977
|
.then((fork) => {
|
|
@@ -1869,6 +1986,7 @@ class Controller implements ReviewController {
|
|
|
1869
1986
|
const session = this.snapshot.session;
|
|
1870
1987
|
|
|
1871
1988
|
if (!session || !this.client) return;
|
|
1989
|
+
|
|
1872
1990
|
const client = this.client;
|
|
1873
1991
|
|
|
1874
1992
|
this.setStatus("forking and sharing…");
|
|
@@ -1888,6 +2006,7 @@ class Controller implements ReviewController {
|
|
|
1888
2006
|
await client.sessionSetShares(fork.id, [
|
|
1889
2007
|
{ id, requireAuth: false, allowlist: [], shareBranch },
|
|
1890
2008
|
]);
|
|
2009
|
+
|
|
1891
2010
|
this.setStatus("");
|
|
1892
2011
|
this.showToast(line, copied ? "fork shared - link copied" : "fork shared");
|
|
1893
2012
|
})
|
|
@@ -1962,6 +2081,7 @@ class Controller implements ReviewController {
|
|
|
1962
2081
|
const shareId = this.linksFor(this.snapshot.session)[0]?.id;
|
|
1963
2082
|
|
|
1964
2083
|
if (this.shareRun !== run || this.closed || !shareId) return;
|
|
2084
|
+
|
|
1965
2085
|
void this.pullShared();
|
|
1966
2086
|
this.shareStop = this.shareTransport.watch(shareId, {
|
|
1967
2087
|
onSession: (remote) => {
|
|
@@ -1970,6 +2090,7 @@ class Controller implements ReviewController {
|
|
|
1970
2090
|
},
|
|
1971
2091
|
onClose: () => {
|
|
1972
2092
|
if (this.shareRun !== run || this.closed) return;
|
|
2093
|
+
|
|
1973
2094
|
this.shareStop = null;
|
|
1974
2095
|
this.shareReconnect = this.clock.setTimeout(connect, delay);
|
|
1975
2096
|
delay = Math.min(delay * 2, SHARE_RECONNECT_MAX_MS);
|
|
@@ -1986,7 +2107,9 @@ class Controller implements ReviewController {
|
|
|
1986
2107
|
|
|
1987
2108
|
private stopShareSync(): void {
|
|
1988
2109
|
this.shareRun = null;
|
|
2110
|
+
|
|
1989
2111
|
if (this.shareReconnect !== undefined) this.clock.clearTimeout(this.shareReconnect);
|
|
2112
|
+
|
|
1990
2113
|
this.shareReconnect = undefined;
|
|
1991
2114
|
this.shareStop?.();
|
|
1992
2115
|
this.shareStop = null;
|
|
@@ -1995,6 +2118,7 @@ class Controller implements ReviewController {
|
|
|
1995
2118
|
// ── completion hand-back ────────────────────
|
|
1996
2119
|
private startCounting(remaining: number): void {
|
|
1997
2120
|
if (remaining <= 0) return this.finishReview();
|
|
2121
|
+
|
|
1998
2122
|
this.update({ completion: { phase: "counting", remaining } });
|
|
1999
2123
|
this.countdown = this.clock.setTimeout(() => this.startCounting(remaining - 1), 1000);
|
|
2000
2124
|
}
|
|
@@ -2005,6 +2129,7 @@ class Controller implements ReviewController {
|
|
|
2005
2129
|
const pane = returnPaneFor(this.snapshot.session?.artifact.meta.herdrPane);
|
|
2006
2130
|
|
|
2007
2131
|
if (herdr && pane) focusHerdrPane(herdr.binPath, pane);
|
|
2132
|
+
|
|
2008
2133
|
this.options.onExit?.(0);
|
|
2009
2134
|
}
|
|
2010
2135
|
|