@cueloop/client 0.1.0-alpha.76 → 0.1.0-alpha.78

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 (82) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +3 -3
  3. package/src/App.diff.test.tsx +14 -2
  4. package/src/App.export.test.tsx +1 -2
  5. package/src/App.identity.test.tsx +78 -0
  6. package/src/App.plan-review.test.tsx +16 -0
  7. package/src/App.share-choice.test.tsx +105 -0
  8. package/src/App.submit-confirm.test.tsx +34 -5
  9. package/src/App.test.tsx +2 -2
  10. package/src/App.toast.test.tsx +3 -1
  11. package/src/App.tsx +180 -5
  12. package/src/App.walk.test.tsx +2 -2
  13. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  14. package/src/ansi-screen.test.ts +28 -0
  15. package/src/ansi-screen.ts +72 -0
  16. package/src/app-screens.tsx +20 -3
  17. package/src/app-view-model.ts +4 -0
  18. package/src/components/AnnotatableFileView.tsx +10 -2
  19. package/src/components/AnnotationCards.tsx +40 -6
  20. package/src/components/AnsiScreen.stories.tsx +12 -0
  21. package/src/components/AnsiScreen.tsx +28 -0
  22. package/src/components/CompletionOverlay.stories.tsx +15 -1
  23. package/src/components/CompletionOverlay.tsx +22 -5
  24. package/src/components/ConfirmCard.stories.tsx +2 -0
  25. package/src/components/ConfirmCard.tsx +45 -18
  26. package/src/components/ConfirmDialog.tsx +11 -12
  27. package/src/components/DiffContentView.tsx +9 -5
  28. package/src/components/FileContentsView.tsx +1 -1
  29. package/src/components/GridTabContent.tsx +2 -0
  30. package/src/components/ManageAccessDialog.stories.tsx +36 -0
  31. package/src/components/ManageAccessDialog.test.tsx +94 -0
  32. package/src/components/ManageAccessDialog.tsx +122 -0
  33. package/src/components/PromptDialog.test.tsx +2 -2
  34. package/src/components/PromptDialog.tsx +9 -10
  35. package/src/components/ShareChoiceDialog.stories.tsx +34 -0
  36. package/src/components/ShareChoiceDialog.test.tsx +103 -0
  37. package/src/components/ShareChoiceDialog.tsx +80 -0
  38. package/src/components/SlashComposer.stories.tsx +37 -0
  39. package/src/components/SlashComposer.tsx +87 -0
  40. package/src/components/ThreadTree.tsx +11 -6
  41. package/src/components/ThreadView.selection.test.tsx +88 -1
  42. package/src/components/ThreadView.slash.test.tsx +0 -1
  43. package/src/components/ThreadView.tsx +71 -8
  44. package/src/components/Toast.tsx +0 -5
  45. package/src/components/WalkWizard.stories.tsx +2 -2
  46. package/src/components/WalkWizard.tsx +10 -12
  47. package/src/components/__snapshots__/stories.test.tsx.snap +277 -61
  48. package/src/components/primitives/Card.tsx +3 -0
  49. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  50. package/src/components/primitives/DialogActions.tsx +39 -0
  51. package/src/components/session-tree.ts +7 -6
  52. package/src/components/stories-app.tsx +13 -4
  53. package/src/components/stories.test.tsx +8 -1
  54. package/src/components/story-tree.test.ts +32 -24
  55. package/src/components/story.ts +32 -15
  56. package/src/config-identity.test.ts +78 -0
  57. package/src/config.ts +44 -1
  58. package/src/diff-intraline.test.ts +75 -2
  59. package/src/diff-intraline.ts +95 -26
  60. package/src/github-identity.test.ts +58 -0
  61. package/src/github-identity.ts +41 -0
  62. package/src/intent-dispatch.test.ts +17 -2
  63. package/src/intent-dispatch.ts +7 -1
  64. package/src/keymap.test.ts +2 -2
  65. package/src/keymap.ts +1 -1
  66. package/src/pasted-image.test.ts +67 -0
  67. package/src/pasted-image.ts +60 -0
  68. package/src/run.ts +31 -19
  69. package/src/serve.ts +1 -1
  70. package/src/share.ts +11 -3
  71. package/src/slash-palette.test.ts +17 -0
  72. package/src/slash-palette.ts +11 -0
  73. package/src/thread-controller.curate.test.ts +1 -0
  74. package/src/thread-controller.derivation.test.ts +62 -0
  75. package/src/thread-controller.live-diff.test.ts +1 -0
  76. package/src/thread-controller.optimistic.test.ts +69 -0
  77. package/src/thread-controller.share.test.ts +36 -2
  78. package/src/thread-controller.switch-cache.test.ts +146 -0
  79. package/src/thread-controller.tree.test.ts +1 -0
  80. package/src/thread-controller.ts +171 -24
  81. package/src/use-annotation-surface.tsx +53 -29
  82. package/src/use-settings-dialog.tsx +24 -0
package/src/App.tsx CHANGED
@@ -15,11 +15,15 @@ import {
15
15
  loadConfig,
16
16
  persistAuthorName,
17
17
  persistDiffView,
18
+ persistIdentity,
18
19
  persistPins,
19
20
  type AutoClose,
20
21
  type DiffViewMode,
22
+ type IdentityConfig,
21
23
  type QuickAction,
22
24
  } from "./config";
25
+ import { resolveDisplayName } from "./attribution";
26
+ import { resolveGithubIdentity } from "./github-identity";
23
27
  import { loadSkills, PaletteNamesContext, SlashSkillsContext } from "./skills";
24
28
  import { mergeSlashItems, slashItemsFrom, type SlashItem } from "./slash-palette";
25
29
  import {
@@ -38,6 +42,7 @@ import { KeyBindings, type CheatsheetSection } from "./key-bindings";
38
42
  import { useReadySignal } from "./ready-signal";
39
43
  import { ThemeProvider } from "./components/theme-context";
40
44
  import { Button } from "./components/primitives/Button";
45
+ import { ShareChoiceDialog, SHARE_CHOICES } from "./components/ShareChoiceDialog";
41
46
  import { Toolbar } from "./components/primitives/Toolbar";
42
47
  import { groupInbox, projectName, threadTitle } from "./components/session-tree";
43
48
  import { ThreadTree } from "./components/ThreadTree";
@@ -52,6 +57,7 @@ import { useRememberLayout } from "./use-remember-layout";
52
57
  import type { LaunchLayout } from "./launch-layout";
53
58
  import { ThreadFooter } from "./components/ThreadFooter";
54
59
  import { ConfirmCard } from "./components/ConfirmCard";
60
+ import { ManageAccessDialog } from "./components/ManageAccessDialog";
55
61
  import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
56
62
  import {
57
63
  diffChordEntries,
@@ -67,7 +73,7 @@ import {
67
73
  import { type DiffFoldControls } from "./components/DiffContentView";
68
74
  import { commentCountsByFile } from "./view-diff";
69
75
  import { annotationTarget } from "@cueloop/schema";
70
- import type { Artifact, DiffFileContents, Thread } from "@cueloop/schema";
76
+ import type { Annotation, Artifact, DiffFileContents, Identity, Thread } from "@cueloop/schema";
71
77
  import { PrototypePixels } from "./prototype-pixels";
72
78
  import type { PrototypeElement } from "./prototype-browser";
73
79
  import {
@@ -139,6 +145,11 @@ function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
139
145
  return menuDialog !== null;
140
146
  }
141
147
 
148
+ /** The private-share allowlist for a session, or an empty list for a public one. */
149
+ function allowedGithubLogins(session: Thread | null): string[] {
150
+ return session?.access?.githubLogins ?? [];
151
+ }
152
+
142
153
  /** The drop-up chrome or a floating popover menu (thread actions, editor split) holds the keyboard. */
143
154
  function keyboardHeldByMenu(
144
155
  menuDialog: "keybinds" | "settings" | null,
@@ -184,11 +195,89 @@ function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["ov
184
195
  return menuOwnsKeyboard || overlay !== "none";
185
196
  }
186
197
 
198
+ /** The highlighted share option, or the public default when the choice is closed. */
199
+ function shareChoiceIndex(mode: Mode): number {
200
+ return mode.type === "shareChoice" ? mode.index : 0;
201
+ }
202
+
203
+ interface ShareChoiceActions {
204
+ setMode: (mode: Mode) => void;
205
+ isOwner: boolean;
206
+ publish: () => void;
207
+ openManageAccess: () => void;
208
+ }
209
+
210
+ /** Move the share-choice highlight by one row, clamped to the list. */
211
+ function movedShareChoice(index: number, delta: number): Mode {
212
+ const next = Math.min(SHARE_CHOICES.length - 1, Math.max(0, index + delta));
213
+
214
+ return { type: "shareChoice", index: next };
215
+ }
216
+
217
+ /** Commit the highlighted share option: public publishes a link, private opens manage-access. */
218
+ function commitShareChoice(index: number, actions: ShareChoiceActions): void {
219
+ const picked = SHARE_CHOICES[index]?.choice;
220
+
221
+ actions.setMode({ type: "normal" });
222
+
223
+ if (!actions.isOwner) return;
224
+
225
+ if (picked === "private") actions.openManageAccess();
226
+ else actions.publish();
227
+ }
228
+
229
+ /** Route one key to the open share choice: escape cancels, arrows move, enter commits. */
230
+ function handleShareChoiceKey(
231
+ index: number,
232
+ key: { name: string },
233
+ actions: ShareChoiceActions,
234
+ ): void {
235
+ if (key.name === "escape") return actions.setMode({ type: "normal" });
236
+
237
+ if (key.name === "down" || key.name === "j") return actions.setMode(movedShareChoice(index, 1));
238
+
239
+ if (key.name === "up" || key.name === "k") return actions.setMode(movedShareChoice(index, -1));
240
+
241
+ if (key.name === "return" || key.name === "enter") commitShareChoice(index, actions);
242
+ }
243
+
244
+ /**
245
+ * An app-level dialog owns the keyboard while open: the manage-access dialog takes its own keys,
246
+ * and the share choice moves the highlight, commits on enter, and cancels on escape. Returns true
247
+ * when a dialog consumed the key so the caller stops the grammar.
248
+ */
249
+ function appDialogOwnsKey(
250
+ accessDialogOpen: boolean,
251
+ mode: Mode,
252
+ key: { name: string },
253
+ actions: ShareChoiceActions,
254
+ ): boolean {
255
+ if (accessDialogOpen) return true;
256
+
257
+ if (mode.type !== "shareChoice") return false;
258
+
259
+ handleShareChoiceKey(mode.index, key, actions);
260
+
261
+ return true;
262
+ }
263
+
187
264
  /** The footer submit fires only for the owner of an unresolved review, never an observer. */
188
265
  function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
189
266
  return isOwner && !resolved && !observer;
190
267
  }
191
268
 
269
+ /** The author display name for a comment's hover tooltip: the reviewer's own name for their notes, else the resolved collaborator name. */
270
+ function authorLabelResolver(
271
+ ownName: string | undefined,
272
+ participants: Identity[] | undefined,
273
+ overrides: Record<string, string>,
274
+ ): (annotation: Annotation) => string | undefined {
275
+ return (annotation) =>
276
+ annotation.author === undefined
277
+ ? (ownName ?? "you")
278
+ : resolveDisplayName(annotation.author, participants, overrides);
279
+ }
280
+
192
281
  /** The Project pane body: the resolved repo's changed files in changes mode, its full tree otherwise. */
193
282
  function ProjectPanelBody(props: {
194
283
  mode: ProjectPanelMode;
@@ -505,6 +594,7 @@ export function App({
505
594
  const [mode, setMode] = useState<Mode>({ type: "normal" });
506
595
  // the top-left settings gear drop-down and the centered dialog it opens
507
596
  const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
597
+ const [accessDialogOpen, setAccessDialogOpen] = useState(false);
508
598
  const [autoClose, setAutoClose] = useState<AutoClose>("off");
509
599
  // unified or side-by-side diff; split only lays out when the Changes pane is zoomed
510
600
  const [diffView, setDiffView] = useState<DiffViewMode>("split");
@@ -526,6 +616,7 @@ export function App({
526
616
  const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
527
617
  const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
528
618
  const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
619
+ const [identity, setIdentity] = useState<IdentityConfig>({ provider: "typed" });
529
620
  const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
530
621
  const [skills, setSkills] = useState<SlashItem[]>([]);
531
622
  const paletteNames = useMemo(
@@ -545,6 +636,7 @@ export function App({
545
636
  setThemeName(config.ui.theme);
546
637
  setThemeOverrides(config.themeOverrides);
547
638
  setAuthorNames(config.authors);
639
+ setIdentity(config.identity);
548
640
  setQuickActions(config.actions);
549
641
  setSkills(loadSkills(config.skillsPath));
550
642
  setAutoClose(config.ui.autoClose);
@@ -578,6 +670,29 @@ export function App({
578
670
  if (!known) setMode({ type: "nameSelf", text: "" });
579
671
  }, [role, selfAuthor, session]);
580
672
 
673
+ // Bumped on every identity change so a slow GitHub sync that lands after a newer rename is discarded.
674
+ const identityGenerationRef = useRef(0);
675
+ const applyIdentity = (next: IdentityConfig): void => {
676
+ identityGenerationRef.current += 1;
677
+ setIdentity(next);
678
+ try {
679
+ persistIdentity(next);
680
+ } catch {
681
+ controller.setStatus("could not save identity to config");
682
+ }
683
+ };
684
+ const syncGithubIdentity = (): void => {
685
+ const generation = (identityGenerationRef.current += 1);
686
+ void resolveGithubIdentity().then((github) => {
687
+ if (identityGenerationRef.current !== generation) return;
688
+ if (!github) return controller.setStatus("GitHub not connected - run gh auth login");
689
+ const name = github.name?.trim() || github.login;
690
+
691
+ applyIdentity({ name, provider: "github" });
692
+ controller.setStatus(`identity synced from GitHub - ${name}`);
693
+ });
694
+ };
695
+
581
696
  // ── settings dialog: config-backed model, navigation, persistence ──
582
697
  const {
583
698
  settingsNav,
@@ -600,6 +715,13 @@ export function App({
600
715
  quickActions,
601
716
  setQuickActions,
602
717
  setMenuDialog,
718
+ identityName: identity.name,
719
+ identityProvider: identity.provider,
720
+ onSyncGithubIdentity: syncGithubIdentity,
721
+ onRenameDisplayName: () => {
722
+ setMenuDialog(null);
723
+ setMode({ type: "renameSelf", text: identity.name ?? "" });
724
+ },
603
725
  });
604
726
 
605
727
  // ── derived view model ──────────────────────
@@ -718,6 +840,9 @@ export function App({
718
840
  setAuthorNames((prev) => ({ ...prev, [id]: name }));
719
841
  },
720
842
  renameThread: (id: string, title: string) => controller.renameSession(id, title),
843
+ setLocalIdentityName: (name: string) => {
844
+ applyIdentity(name ? { name, provider: "typed" } : { provider: "typed" });
845
+ },
721
846
  liveInput,
722
847
  setCursor,
723
848
  setInboxCursor,
@@ -746,8 +871,8 @@ export function App({
746
871
 
747
872
  const menuControl = useMenuControlState();
748
873
  const menuOwnsKeyboard = keyboardHeldByMenu(menuDialog, menuControl.openMenuId);
749
- // an overlay (submit, walk, prompt, confirm) or the menu takes the keyboard
750
- // from the thread view; the view suspends its own grammar meanwhile
874
+ // an overlay (submit, walk, prompt, confirm, share choice) or the menu takes the
875
+ // keyboard from the thread view; the view suspends its own grammar meanwhile
751
876
  const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
752
877
 
753
878
  const leaderCombos = leaderCombosFor(keysRef.current.leader);
@@ -787,6 +912,16 @@ export function App({
787
912
 
788
913
  useKeyboard((key) => {
789
914
  if (quitKeyHandled(key, onExit)) return;
915
+ // an app-level dialog (manage-access, share choice) owns the keyboard while open
916
+ if (
917
+ appDialogOwnsKey(accessDialogOpen, mode, key, {
918
+ setMode,
919
+ isOwner,
920
+ publish: () => controller.share(),
921
+ openManageAccess: () => setAccessDialogOpen(true),
922
+ })
923
+ )
924
+ return;
790
925
  if (menuModalHandled(menuControl, key)) return;
791
926
  if (
792
927
  appLeaderHandled({ focusedPane, key, leaderCombos, pending: leaderPending, runLeaderCommand })
@@ -919,6 +1054,11 @@ export function App({
919
1054
  );
920
1055
 
921
1056
  const activeSession = session;
1057
+ const resolveAuthorLabel = authorLabelResolver(
1058
+ identity.name,
1059
+ activeSession.participants,
1060
+ authorNames,
1061
+ );
922
1062
 
923
1063
  if (isCompletionOverlayPhase(completion) && activeSession.verdict)
924
1064
  return (
@@ -928,6 +1068,9 @@ export function App({
928
1068
  verdict={activeSession.verdict.kind}
929
1069
  completion={completion}
930
1070
  status={status}
1071
+ onClose={() => dispatch({ type: "finishReview" })}
1072
+ onBackToPlan={() => dispatch({ type: "dismissCompletion" })}
1073
+ onAlways={() => dispatch({ type: "optInAutoClose" })}
931
1074
  />
932
1075
  );
933
1076
 
@@ -946,6 +1089,7 @@ export function App({
946
1089
  session: activeSession,
947
1090
  walkFileList,
948
1091
  viewedPaths,
1092
+ quickActions,
949
1093
  liveInput,
950
1094
  setMode,
951
1095
  dispatch,
@@ -968,12 +1112,19 @@ export function App({
968
1112
  runEditorHandOff();
969
1113
  };
970
1114
 
971
- // clicking the header Share button: publish the plan, copy the ssh line
1115
+ // the share choice's public option: publish the plan, copy the ssh line
972
1116
  const onShareRequest = (): void => {
973
1117
  if (!isOwner) return controller.setStatus("only the plan owner can share");
974
1118
  controller.share();
975
1119
  };
976
1120
 
1121
+ // the share choice's private option opens the manage-access allowlist surface
1122
+ const onPrivateShareRequest = (): void => {
1123
+ if (!isOwner) return controller.setStatus("only the plan owner can share");
1124
+ setAccessDialogOpen(true);
1125
+ };
1126
+ const allowedLogins = allowedGithubLogins(session);
1127
+
977
1128
  // clicking the rail Submit button: same read-only answer as the submit key
978
1129
  const onSubmitRequest = (): void => {
979
1130
  if (observer) return controller.setStatus("observer - read-only");
@@ -1027,7 +1178,7 @@ export function App({
1027
1178
  <Button onPress={onEditRequest} theme={theme}>
1028
1179
  {" edit "}
1029
1180
  </Button>
1030
- <Button onPress={onShareRequest} theme={theme}>
1181
+ <Button onPress={() => dispatch({ type: "share" })} theme={theme}>
1031
1182
  {" share "}
1032
1183
  </Button>
1033
1184
  </Toolbar>
@@ -1092,6 +1243,7 @@ export function App({
1092
1243
  void controller.reply(rootAnnotationId, body)
1093
1244
  }
1094
1245
  onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
1246
+ resolveAuthorLabel={resolveAuthorLabel}
1095
1247
  onExit={() => onExit?.(0)}
1096
1248
  />
1097
1249
  )}
@@ -1160,6 +1312,7 @@ export function App({
1160
1312
  onReply: (rootAnnotationId, body) =>
1161
1313
  void controller.reply(rootAnnotationId, body),
1162
1314
  onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
1315
+ resolveAuthorLabel,
1163
1316
  leaderCombos,
1164
1317
  onLeaderCommand: runLeaderCommand,
1165
1318
  onExit: () => onExit?.(0),
@@ -1231,6 +1384,28 @@ export function App({
1231
1384
  </box>
1232
1385
  </box>
1233
1386
  ) : null}
1387
+ <ManageAccessDialog
1388
+ isOpen={accessDialogOpen}
1389
+ logins={allowedLogins}
1390
+ onAdd={(login) => controller.setShareAccess([...allowedLogins, login])}
1391
+ onRemove={(login) =>
1392
+ controller.setShareAccess(allowedLogins.filter((entry) => entry !== login))
1393
+ }
1394
+ onCreateLink={() => {
1395
+ setAccessDialogOpen(false);
1396
+ controller.share();
1397
+ }}
1398
+ onClose={() => setAccessDialogOpen(false)}
1399
+ theme={theme}
1400
+ />
1401
+ <ShareChoiceDialog
1402
+ isOpen={mode.type === "shareChoice"}
1403
+ selectedIndex={shareChoiceIndex(mode)}
1404
+ onPublicShare={onShareRequest}
1405
+ onPrivateShare={onPrivateShareRequest}
1406
+ onClose={() => setMode({ type: "normal" })}
1407
+ theme={theme}
1408
+ />
1234
1409
  {menuChrome}
1235
1410
  </AppShell>
1236
1411
  </MenuControlProvider>
@@ -162,7 +162,7 @@ describe("the guided walk", () => {
162
162
  expect(setup.captureCharFrame()).toContain("3/3 files viewed");
163
163
 
164
164
  // Act
165
- await press(setup, "enter");
165
+ await pressKey(setup, "RETURN", { meta: true });
166
166
 
167
167
  // Assert
168
168
  await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
@@ -253,7 +253,7 @@ describe("the guided walk", () => {
253
253
  await pressKey(setup, "RETURN", { meta: true });
254
254
  // nothing pending despite the note, so the confirm card defaults to approve
255
255
  await waitForText(setup, "[Approve]");
256
- await press(setup, "enter");
256
+ await pressKey(setup, "RETURN", { meta: true });
257
257
 
258
258
  // Assert
259
259
  await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
@@ -1,4 +1,4 @@
1
- // Bun Snapshot v1, https://goo.gl/fbAQLP
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
2
 
3
3
  exports[`view-plan regression harness (M0) blockRuns output is stable across the representative blocks 1`] = `
4
4
  [
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ansiScreenToLines } from "./ansi-screen";
3
+
4
+ describe("ansiScreenToLines", () => {
5
+ test("places text at the positioned cursor", () => {
6
+ const lines = ansiScreenToLines("\x1b[2;3Hhi", 10, 4);
7
+
8
+ expect(lines[0]).toBe("");
9
+ expect(lines[1]).toBe(" hi");
10
+ expect(lines[2]).toBe("");
11
+ });
12
+
13
+ test("clears the screen before repositioning", () => {
14
+ const lines = ansiScreenToLines("\x1b[1;1Hold\x1b[2J\x1b[1;1Hnew", 10, 2);
15
+
16
+ expect(lines[0]).toBe("new");
17
+ });
18
+
19
+ test("ignores colour, cursor-visibility, and clipboard sequences", () => {
20
+ const lines = ansiScreenToLines("\x1b[?25l\x1b[1;1H\x1b[32mgo\x1b]52;c;AA\x07", 10, 2);
21
+
22
+ expect(lines[0]).toBe("go");
23
+ });
24
+
25
+ test("clamps writes outside the grid instead of throwing", () => {
26
+ expect(() => ansiScreenToLines("\x1b[99;99Hx", 5, 3)).not.toThrow();
27
+ });
28
+ });
@@ -0,0 +1,72 @@
1
+ /** Interpret an absolute-positioned ANSI byte stream (cursor moves plus printed text) into a grid of rows. */
2
+
3
+ const CSI = "\x1b[";
4
+
5
+ export function ansiScreenToLines(ansi: string, cols: number, rows: number): string[] {
6
+ const grid: string[][] = Array.from({ length: rows }, () =>
7
+ Array.from({ length: cols }, () => " "),
8
+ );
9
+ let row = 0;
10
+ let column = 0;
11
+ let index = 0;
12
+
13
+ const put = (character: string): void => {
14
+ if (row >= 0 && row < rows && column >= 0 && column < cols) grid[row]![column] = character;
15
+ column += 1;
16
+ };
17
+
18
+ while (index < ansi.length) {
19
+ const character = ansi[index]!;
20
+
21
+ if (character === "\x1b") {
22
+ index = consumeEscape(ansi, index, (nextRow, nextColumn) => {
23
+ if (nextRow === "clear") {
24
+ for (const line of grid) line.fill(" ");
25
+ row = 0;
26
+ column = 0;
27
+ } else {
28
+ row = nextRow;
29
+ column = nextColumn;
30
+ }
31
+ });
32
+ continue;
33
+ }
34
+ if (character === "\n") {
35
+ row += 1;
36
+ column = 0;
37
+ } else if (character === "\r") {
38
+ column = 0;
39
+ } else {
40
+ put(character);
41
+ }
42
+ index += 1;
43
+ }
44
+
45
+ return grid.map((line) => line.join("").replace(/\s+$/, ""));
46
+ }
47
+
48
+ type EscapeEffect = (row: number | "clear", column: number) => void;
49
+
50
+ function consumeEscape(ansi: string, start: number, apply: EscapeEffect): number {
51
+ if (ansi[start + 1] === "]") {
52
+ const bell = ansi.indexOf("\x07", start);
53
+
54
+ return bell === -1 ? ansi.length : bell + 1;
55
+ }
56
+ if (ansi.slice(start, start + CSI.length) !== CSI) return start + 1;
57
+ let index = start + CSI.length;
58
+
59
+ while (index < ansi.length && !/[A-Za-z]/.test(ansi[index]!)) index += 1;
60
+ const final = ansi[index];
61
+ const parameters = ansi.slice(start + CSI.length, index);
62
+
63
+ if (final === "H" || final === "f") {
64
+ const [rowParameter = "1", columnParameter = "1"] = parameters.split(";");
65
+
66
+ apply(Math.max(0, Number(rowParameter) - 1), Math.max(0, Number(columnParameter) - 1));
67
+ } else if (final === "J" && parameters === "2") {
68
+ apply("clear", 0);
69
+ }
70
+
71
+ return index + 1;
72
+ }
@@ -388,7 +388,6 @@ export function NoThreadShell(props: {
388
388
  <PromptDialog
389
389
  isOpen
390
390
  title=" rename thread "
391
- label="new title for this thread:"
392
391
  value={mode.text}
393
392
  placeholder="a short title"
394
393
  onInput={(text) => setMode({ ...mode, text })}
@@ -412,8 +411,11 @@ export function CompletionScreen(props: {
412
411
  verdict: VerdictKind;
413
412
  completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
414
413
  status: string;
414
+ onClose: () => void;
415
+ onBackToPlan: () => void;
416
+ onAlways: () => void;
415
417
  }): React.ReactNode {
416
- const { theme, session, verdict, completion, status } = props;
418
+ const { theme, session, verdict, completion, status, onClose, onBackToPlan, onAlways } = props;
417
419
 
418
420
  return (
419
421
  <ThemeProvider theme={theme}>
@@ -421,6 +423,9 @@ export function CompletionScreen(props: {
421
423
  verdict={verdict}
422
424
  completion={completion}
423
425
  status={status}
426
+ onClose={onClose}
427
+ onBackToPlan={onBackToPlan}
428
+ onAlways={onAlways}
424
429
  returnsTo={
425
430
  returnPaneFor(session.artifact.meta.herdrPane)
426
431
  ? (session.artifact.meta.agent ?? "the agent")
@@ -510,7 +515,6 @@ export function TrailingOverlays(props: {
510
515
  <PromptDialog
511
516
  isOpen
512
517
  title=" rename thread "
513
- label="new title for this thread:"
514
518
  value={mode.text}
515
519
  placeholder="a short title"
516
520
  onInput={(text) => setMode({ ...mode, text })}
@@ -532,6 +536,19 @@ export function TrailingOverlays(props: {
532
536
  theme={theme}
533
537
  />
534
538
  ) : null}
539
+ {mode.type === "renameSelf" ? (
540
+ <PromptDialog
541
+ isOpen
542
+ title=" Display name "
543
+ label="Your display name - it attributes the notes you leave:"
544
+ value={mode.text}
545
+ placeholder="your name"
546
+ onInput={(text) => setMode({ ...mode, text })}
547
+ onSave={() => dispatch({ type: "confirmDialog" })}
548
+ onCancel={() => setMode({ type: "normal" })}
549
+ theme={theme}
550
+ />
551
+ ) : null}
535
552
  {mode.type === "treePrompt" ? (
536
553
  <PromptDialog
537
554
  isOpen
@@ -4,6 +4,7 @@ import type { Mode } from "./intent-dispatch";
4
4
  import type { Intent, KeyState } from "./keymap";
5
5
  import type { Completion } from "./thread-controller";
6
6
  import type { WalkFile } from "./walk";
7
+ import type { QuickAction } from "./config";
7
8
  import { viewedCount } from "./walk";
8
9
  import type { ConfirmCardProps } from "./components/ConfirmCard";
9
10
  import type { BreadcrumbItem } from "./components/Breadcrumb";
@@ -57,6 +58,7 @@ export function resolveOverlay(
57
58
  mode.type === "treePrompt"
58
59
  )
59
60
  return "prompt";
61
+ if (mode.type === "shareChoice") return "shareChoice";
60
62
  if (mode.type === "spanActions") return "spanActions";
61
63
  if (completionPhase === "prompt") return "completion-prompt";
62
64
  if (completionPhase === "counting") return "completion-counting";
@@ -135,6 +137,7 @@ export function buildSubmitConfirmState(
135
137
  session: Thread;
136
138
  walkFileList: WalkFile[];
137
139
  viewedPaths: Set<string>;
140
+ quickActions: QuickAction[];
138
141
  },
139
142
  ): Omit<ConfirmCardProps, "theme"> | null {
140
143
  const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
@@ -144,6 +147,7 @@ export function buildSubmitConfirmState(
144
147
  return {
145
148
  verdict: mode.verdict,
146
149
  summary: mode.summary,
150
+ quickActions: deps.quickActions,
147
151
  viewedSummary:
148
152
  isDiff && session.viewedPaths !== undefined
149
153
  ? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
@@ -1,10 +1,16 @@
1
1
  // A project file shown as its own annotatable surface: the file's lines render through the diff
2
2
  // sheet as context rows (one line-number gutter, no +/- sign), so selecting text leaves a comment
3
- // anchored to the file exactly as a plan or diff comment is. Used where a review session exists to
3
+ // anchored to the file exactly as a plan or diff comment is. Used where a thread exists to
4
4
  // hold the notes; the bare-launch welcome shell uses the read-only FileContentsView instead.
5
5
 
6
6
  import React, { useEffect, useRef, useState } from "react";
7
- import { makeAnchor, annotationTarget, type Anchor, type Thread } from "@cueloop/schema";
7
+ import {
8
+ makeAnchor,
9
+ annotationTarget,
10
+ type Anchor,
11
+ type Annotation,
12
+ type Thread,
13
+ } from "@cueloop/schema";
8
14
  import type { Theme } from "../theme";
9
15
  import type { QuickAction } from "../config";
10
16
  import { useComponentTheme } from "./theme-context";
@@ -28,6 +34,7 @@ export interface AnnotatableFileViewProps {
28
34
  onAddComment: (anchor: Anchor, body: string) => void;
29
35
  onReply: (rootAnnotationId: string, body: string) => void;
30
36
  onUpdateAnnotation: (id: string, body: string) => void;
37
+ resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
31
38
  onExit: () => void;
32
39
  theme?: Theme;
33
40
  }
@@ -119,6 +126,7 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
119
126
  }
120
127
  onReply={props.onReply}
121
128
  onUpdateAnnotation={props.onUpdateAnnotation}
129
+ resolveAuthorLabel={props.resolveAuthorLabel}
122
130
  onExit={props.onExit}
123
131
  theme={props.theme}
124
132
  />