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

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