@cueloop/client 0.1.0-alpha.31 → 0.1.0-alpha.33

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 (61) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +4 -3
  3. package/src/App.diff.test.tsx +5 -2
  4. package/src/App.export.test.tsx +5 -7
  5. package/src/App.plan-review.test.tsx +14 -17
  6. package/src/App.readonly.test.tsx +9 -7
  7. package/src/App.rendering.test.tsx +6 -2
  8. package/src/App.review-panel.test.tsx +19 -14
  9. package/src/App.submit-confirm.test.tsx +6 -7
  10. package/src/App.test.tsx +8 -5
  11. package/src/App.toast.test.tsx +68 -0
  12. package/src/App.tsx +154 -50
  13. package/src/App.walk.test.tsx +7 -4
  14. package/src/components/AnnotationCard.stories.tsx +1 -1
  15. package/src/components/AnnotationCard.tsx +18 -13
  16. package/src/components/CompletionOverlay.tsx +1 -1
  17. package/src/components/ConfirmCard.tsx +1 -0
  18. package/src/components/ConfirmDialog.stories.tsx +1 -1
  19. package/src/components/ConfirmDialog.tsx +1 -1
  20. package/src/components/DiffSheet.stories.tsx +21 -3
  21. package/src/components/DiffSheet.tsx +45 -19
  22. package/src/components/InboxList.stories.tsx +1 -1
  23. package/src/components/InboxList.tsx +3 -3
  24. package/src/components/KeybindsDialog.stories.tsx +31 -0
  25. package/src/components/KeybindsDialog.tsx +42 -0
  26. package/src/components/MenuBar.stories.tsx +33 -0
  27. package/src/components/MenuBar.tsx +58 -0
  28. package/src/components/PlanSheet.stories.tsx +2 -2
  29. package/src/components/PlanSheet.tsx +26 -75
  30. package/src/components/PromptDialog.stories.tsx +2 -2
  31. package/src/components/PromptDialog.tsx +1 -1
  32. package/src/components/ReviewPanel.stories.tsx +5 -17
  33. package/src/components/ReviewPanel.tsx +10 -29
  34. package/src/components/ReviewRail.stories.tsx +39 -0
  35. package/src/components/ReviewRail.tsx +62 -57
  36. package/src/components/SettingsDialog.stories.tsx +1 -1
  37. package/src/components/SettingsDialog.tsx +4 -3
  38. package/src/components/SettingsRows.stories.tsx +1 -1
  39. package/src/components/SettingsRows.tsx +1 -1
  40. package/src/components/Toast.stories.tsx +12 -0
  41. package/src/components/Toast.tsx +29 -0
  42. package/src/components/WalkWizard.stories.tsx +1 -1
  43. package/src/components/WalkWizard.tsx +1 -1
  44. package/src/components/__snapshots__/stories.test.tsx.snap +598 -566
  45. package/src/components/primitives/Button.stories.tsx +2 -2
  46. package/src/components/primitives/Dialog.stories.tsx +1 -1
  47. package/src/components/primitives/Dialog.tsx +7 -5
  48. package/src/components/primitives/Tabs.tsx +19 -38
  49. package/src/components/stories-app.tsx +1 -1
  50. package/src/components/stories.test.tsx +2 -0
  51. package/src/key-bindings.test.ts +4 -4
  52. package/src/key-bindings.ts +41 -2
  53. package/src/serve.test.ts +2 -2
  54. package/src/session-controller.share.test.ts +13 -0
  55. package/src/session-controller.ts +19 -1
  56. package/src/test-support.test.ts +55 -0
  57. package/src/test-support.ts +113 -23
  58. package/src/theme.ts +16 -16
  59. package/src/version.ts +5 -0
  60. package/src/components/ComposeBar.stories.tsx +0 -18
  61. package/src/components/ComposeBar.tsx +0 -54
package/src/App.tsx CHANGED
@@ -16,21 +16,27 @@ import { type VerdictKind } from "@cueloop/schema";
16
16
  import { displayText, marksByDisplay, type Mark } from "./view-plan";
17
17
  import { noteForFile, viewedCount } from "./walk";
18
18
  import { DARK, dimmedTheme } from "./theme";
19
- import { DEFAULT_KEYS, loadConfig, persistAuthorName } from "./config";
19
+ import { DEFAULT_KEYS, loadConfig, persistAuthorName, persistAutoClose, type AutoClose } from "./config";
20
20
  import { returnPaneFor } from "@cueloop/schema";
21
21
  import { createReviewController } from "./session-controller";
22
22
  import type { SessionClient } from "@cueloop/daemon/client";
23
23
  import { createIntentDispatch, reviewerAnnotations, type Mode } from "./intent-dispatch";
24
24
  import { reduceKey, type KeyState } from "./keymap";
25
- import { KeyBindings, type HintMode } from "./key-bindings";
25
+ import { KeyBindings } from "./key-bindings";
26
26
  import { ThemeProvider } from "./components/theme-context";
27
- import { StatusBar } from "./components/primitives/StatusBar";
27
+ import { Button } from "./components/primitives/Button";
28
+ import { Toolbar } from "./components/primitives/Toolbar";
29
+ import { MenuBar } from "./components/MenuBar";
30
+ import { KeybindsDialog } from "./components/KeybindsDialog";
31
+ import { SettingsDialog, type SettingsCategory } from "./components/SettingsDialog";
32
+ import { CLIENT_VERSION } from "./version";
28
33
  import { Breadcrumb, type BreadcrumbItem } from "./components/Breadcrumb";
29
34
  import { PlanSheet, type PlanSheetHandle } from "./components/PlanSheet";
30
35
  import { DiffSheet } from "./components/DiffSheet";
31
36
  import { annotationBlocking, type ReviewRailHandle } from "./components/ReviewRail";
32
37
  import { ReviewPanel } from "./components/ReviewPanel";
33
38
  import {
39
+ REVIEW_COMPACT_WIDTH,
34
40
  REVIEW_DEFAULT_WIDTH,
35
41
  resolveReviewWidth,
36
42
  toggleReviewPanelMode,
@@ -41,14 +47,11 @@ import { CompletionOverlay } from "./components/CompletionOverlay";
41
47
  import { InboxList } from "./components/InboxList";
42
48
  import { ConfirmDialog } from "./components/ConfirmDialog";
43
49
  import { PromptDialog } from "./components/PromptDialog";
44
- import { ComposeBar } from "./components/ComposeBar";
45
50
  import { WalkWizard } from "./components/WalkWizard";
51
+ import { Toast } from "./components/Toast";
46
52
 
47
- /**
48
- * The breadcrumb header and the status bar each occupy one terminal row; the
49
- * review layout (plan column, divider, rail) gets the rows that remain.
50
- */
51
- const CHROME_ROWS = 2;
53
+ /** A toast clears itself after this idle; esc dismisses it sooner. */
54
+ const TOAST_DISMISS_MS = 4000;
52
55
 
53
56
  export interface AppProps {
54
57
  home?: string;
@@ -94,7 +97,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
94
97
  controller.connect();
95
98
  return () => controller.close();
96
99
  }, [controller]);
97
- const { session, inbox, status, error, completion, editOrphanCount, walk } = useSyncExternalStore(
100
+ const { session, inbox, status, toast, error, completion, editOrphanCount, walk } = useSyncExternalStore(
98
101
  controller.subscribe,
99
102
  controller.getSnapshot,
100
103
  controller.getSnapshot,
@@ -106,12 +109,21 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
106
109
  return controller.startSharePoll();
107
110
  }, [isOwner, session?.id, session?.shareId, controller]);
108
111
  const renderer = useRenderer();
109
- const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
112
+ const { width: terminalWidth } = useTerminalDimensions();
110
113
 
111
114
  // ── view state ──────────────────────────────
112
115
  const [cursor, setCursor] = useState(0);
113
116
  const [inboxCursor, setInboxCursor] = useState(0);
114
117
  const [mode, setMode] = useState<Mode>({ type: "normal" });
118
+ // the bottom-left menu drop-up and the centered dialog it opens
119
+ const [menuOpen, setMenuOpen] = useState(false);
120
+ const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
121
+ const [autoClose, setAutoClose] = useState<AutoClose>("off");
122
+ const [settingsNav, setSettingsNav] = useState<{ categoryId: string; rowIndex: number; zone: "nav" | "body" }>({
123
+ categoryId: "general",
124
+ rowIndex: 0,
125
+ zone: "body",
126
+ });
115
127
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
116
128
  const [railTab, setRailTab] = useState<"review" | "agent">("review");
117
129
  // review panel layout: mode + expanded width are client view state, loaded
@@ -143,6 +155,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
143
155
  setReviewWidth(config.ui.reviewWidth);
144
156
  reviewWidthRef.current = config.ui.reviewWidth;
145
157
  setAuthorNames(config.authors);
158
+ setAutoClose(config.ui.autoClose);
146
159
  controller.applyConfig(config);
147
160
  }, [session?.workspace.repoRoot, controller, keyBindings]);
148
161
  useEffect(
@@ -152,6 +165,12 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
152
165
  [],
153
166
  );
154
167
 
168
+ useEffect(() => {
169
+ if (!toast) return;
170
+ const timer = setTimeout(() => controller.dismissToast(), TOAST_DISMISS_MS);
171
+ return () => clearTimeout(timer);
172
+ }, [toast, controller]);
173
+
155
174
  // First open of a share: ask the collaborator for a display name once, unless
156
175
  // a past visit already recorded one. esc skips and their notes read anonymous.
157
176
  const promptedSelfRef = useRef(false);
@@ -300,7 +319,57 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
300
319
  ? "walk"
301
320
  : "none";
302
321
 
322
+ // ── settings dialog: config-backed model, navigation, persistence ──
323
+ const settingsCategories: SettingsCategory[] = [
324
+ {
325
+ id: "general",
326
+ name: "General",
327
+ description: "submission behaviour",
328
+ rows: [{ key: "autoClose", label: "Auto-close on submit", kind: "cycle", options: ["off", "3s", "10s"] }],
329
+ },
330
+ {
331
+ id: "display",
332
+ name: "Display",
333
+ description: "the review panel",
334
+ rows: [{ key: "reviewPanel", label: "Review panel", kind: "cycle", options: ["expanded", "compact", "hidden"] }],
335
+ },
336
+ ];
337
+ const settingsValues = { autoClose: autoClose === "off" ? "off" : `${autoClose}s`, reviewPanel: reviewMode };
338
+ const cycleSetting = (rowKey: string): void => {
339
+ if (rowKey === "autoClose") {
340
+ const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
341
+ setAutoClose(next);
342
+ persistAutoClose(next);
343
+ } else if (rowKey === "reviewPanel") {
344
+ const order: ReviewPanelMode[] = ["expanded", "compact", "hidden"];
345
+ const next = order[(order.indexOf(reviewMode) + 1) % order.length]!;
346
+ setReviewMode(next);
347
+ controller.saveReviewPanel({ mode: next });
348
+ }
349
+ };
350
+ const handleSettingsKey = (name: string): void => {
351
+ if (name === "escape") return void setMenuDialog(null);
352
+ const categoryIndex = settingsCategories.findIndex((category) => category.id === settingsNav.categoryId);
353
+ const category = settingsCategories[categoryIndex]!;
354
+ if (settingsNav.zone === "nav") {
355
+ if (name === "j" || name === "down") setSettingsNav({ categoryId: settingsCategories[Math.min(settingsCategories.length - 1, categoryIndex + 1)]!.id, rowIndex: 0, zone: "nav" });
356
+ else if (name === "k" || name === "up") setSettingsNav({ categoryId: settingsCategories[Math.max(0, categoryIndex - 1)]!.id, rowIndex: 0, zone: "nav" });
357
+ else if (name === "l" || name === "tab" || name === "return") setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: 0 }));
358
+ return;
359
+ }
360
+ if (name === "j" || name === "down") setSettingsNav((state) => ({ ...state, rowIndex: Math.min(category.rows.length - 1, state.rowIndex + 1) }));
361
+ else if (name === "k" || name === "up") setSettingsNav((state) => ({ ...state, rowIndex: Math.max(0, state.rowIndex - 1) }));
362
+ else if (name === "h" || name === "tab") setSettingsNav((state) => ({ ...state, zone: "nav" }));
363
+ else if (name === "return" || name === "space") cycleSetting(category.rows[settingsNav.rowIndex]!.key);
364
+ };
365
+
303
366
  useKeyboard((key) => {
367
+ if (menuDialog === "settings") return void handleSettingsKey(key.name);
368
+ if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
369
+ if (menuOpen) return void (key.name === "escape" && setMenuOpen(false));
370
+ // the toast is non-modal: escape only dismisses it when nothing else owns
371
+ // escape, so an open overlay (compose, submit, prompt, walk) still cancels
372
+ if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span") return controller.dismissToast();
304
373
  const state: KeyState = {
305
374
  keys: keysRef.current,
306
375
  readOnly: observer,
@@ -398,6 +467,24 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
398
467
  }
399
468
  : null;
400
469
 
470
+ const diffComposeState =
471
+ mode.type === "compose" && isDiff
472
+ ? {
473
+ kind: mode.kind,
474
+ rowIndex: mode.displayIndex,
475
+ quote: rows[mode.displayIndex]?.text.replace(/\n$/, "") ?? "",
476
+ draft: {
477
+ text: mode.text,
478
+ onInput: (text: string) => {
479
+ liveInput.current = text;
480
+ setMode({ ...mode, text });
481
+ },
482
+ onSave: () => dispatch({ type: "saveCompose" }),
483
+ onCancel: () => dispatch({ type: "closeOverlay" }),
484
+ },
485
+ }
486
+ : null;
487
+
401
488
  const activeSpan =
402
489
  mode.type === "span"
403
490
  ? { displayIndex: mode.span.displayIndex, start: mode.span.start, end: mode.span.end }
@@ -484,36 +571,28 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
484
571
  }
485
572
  : null;
486
573
 
574
+ // width the review panel occupies (divider + rail), so the header can reserve
575
+ // it and keep Edit/Share pinned to the plan sheet's right edge, not the rail's
576
+ const railFootprint =
577
+ reviewMode === "hidden"
578
+ ? 0
579
+ : 1 + (reviewMode === "compact" ? REVIEW_COMPACT_WIDTH : resolveReviewWidth(reviewWidth, terminalWidth));
580
+
581
+ // status badges sit right after the product word so they survive a header
582
+ // that is too narrow for the whole trail (the rail can eat the width)
487
583
  const headerItems: BreadcrumbItem[] = [
488
584
  { label: "cueloop", tone: "accent" },
489
- { label: `${activeSession.artifact.meta.title ?? activeSession.artifact.meta.planPath ?? activeSession.id} · rev ${activeSession.revisions.length}`, tone: "dim" },
490
585
  ...(resolved ? [{ label: `resolved: ${activeSession.verdict!.kind.replace("_", " ")}`, tone: "green" as const }] : []),
491
586
  ...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
492
587
  ...(role === "collaborator" ? [{ label: "shared · your notes save as you go", tone: "dim" as const }] : []),
493
- ...(status ? [{ label: status, tone: "accent" as const }] : []),
588
+ { label: `${activeSession.artifact.meta.title ?? activeSession.artifact.meta.planPath ?? activeSession.id} · rev ${activeSession.revisions.length}`, tone: "dim" },
589
+ { label: `submitted by ${activeSession.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
494
590
  ];
495
591
 
496
- keyBindings.setContext({ overlay, spanMode: mode.type === "span" });
497
- const hintMode: HintMode = observer
498
- ? "read-only"
499
- : mode.type === "submit"
500
- ? "submit"
501
- : mode.type === "span"
502
- ? "span"
503
- : mode.type === "compose" || mode.type === "railEdit"
504
- ? "compose"
505
- : walking
506
- ? "walk"
507
- : focusedAnnotationId !== undefined
508
- ? "card"
509
- : role === "collaborator"
510
- ? "collaborator"
511
- : "normal";
512
-
513
592
  return (
514
593
  <ThemeProvider theme={theme}>
515
594
  <box
516
- style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg }}
595
+ style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.background }}
517
596
  onMouseDrag={(event: MouseEvent) => {
518
597
  if (!dividerDragging || reviewMode !== "expanded") return;
519
598
  const next = widthFromMouseColumn(event.x, terminalWidth);
@@ -526,7 +605,19 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
526
605
  controller.saveReviewPanel({ width: reviewWidthRef.current });
527
606
  }}
528
607
  >
529
- <Breadcrumb items={headerItems} />
608
+ <box style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}>
609
+ <box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
610
+ <Breadcrumb items={headerItems} />
611
+ <box style={{ flexGrow: 1 }} />
612
+ {isOwner && !isDiff && !resolved ? (
613
+ <Toolbar>
614
+ <Button onPress={onEditRequest} theme={theme}>{" Edit "}</Button>
615
+ <Button onPress={onShareRequest} theme={theme}>{" Share "}</Button>
616
+ </Toolbar>
617
+ ) : null}
618
+ </box>
619
+ <box style={{ width: railFootprint }} />
620
+ </box>
530
621
  <box style={{ flexGrow: 1, flexDirection: "row" }}>
531
622
  {isDiff ? (
532
623
  // the sheet dims to reading-quiet colors while the wizard has focus
@@ -535,6 +626,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
535
626
  cursor={cursor}
536
627
  annotations={activeSession.annotations}
537
628
  focusedAnnotationId={focusedAnnotationId}
629
+ compose={diffComposeState}
538
630
  theme={walking ? dimmedTheme(theme) : undefined}
539
631
  />
540
632
  ) : (
@@ -548,16 +640,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
548
640
  compose={composeState}
549
641
  editOrphanCount={editOrphanCount}
550
642
  onLineActivate={onLineActivate}
551
- onEditRequest={onEditRequest}
552
- onShareRequest={onShareRequest}
553
- canEdit={isOwner}
554
643
  />
555
644
  )}
556
645
  <ReviewPanel
557
646
  mode={reviewMode}
558
647
  width={resolveReviewWidth(reviewWidth, terminalWidth)}
559
- height={terminalHeight - CHROME_ROWS}
560
- dragging={dividerDragging}
561
648
  onDividerGrab={onDividerGrab}
562
649
  onToggle={onToggleReviewPanel}
563
650
  railRef={railRef}
@@ -577,19 +664,21 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
577
664
  }}
578
665
  />
579
666
  </box>
580
- {mode.type === "compose" && isDiff ? (
581
- <ComposeBar
582
- kind={mode.kind}
583
- quote={rows[mode.displayIndex]?.text ?? ""}
584
- text={mode.text}
585
- onInput={(text) => {
586
- liveInput.current = text;
587
- setMode({ ...mode, text });
588
- }}
589
- />
590
- ) : (
591
- <StatusBar>{keyBindings.statusHint(hintMode)}</StatusBar>
592
- )}
667
+ <MenuBar
668
+ open={menuOpen}
669
+ version={CLIENT_VERSION}
670
+ status={status}
671
+ onToggle={() => setMenuOpen((isOpen) => !isOpen)}
672
+ onSettings={() => {
673
+ setMenuOpen(false);
674
+ setMenuDialog("settings");
675
+ }}
676
+ onKeybinds={() => {
677
+ setMenuOpen(false);
678
+ setMenuDialog("keybinds");
679
+ }}
680
+ theme={theme}
681
+ />
593
682
  {walking && walk !== null ? (
594
683
  <WalkWizard
595
684
  files={walkFileList}
@@ -630,6 +719,21 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
630
719
  theme={theme}
631
720
  />
632
721
  ) : null}
722
+ {toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
723
+ {menuDialog === "keybinds" ? <KeybindsDialog sections={keyBindings.cheatsheet()} theme={theme} /> : null}
724
+ {menuDialog === "settings" ? (
725
+ <SettingsDialog
726
+ isOpen
727
+ categories={settingsCategories}
728
+ values={settingsValues}
729
+ activeCategoryId={settingsNav.categoryId}
730
+ activeRowIndex={settingsNav.rowIndex}
731
+ activeZone={settingsNav.zone}
732
+ onCategorySelect={(id) => setSettingsNav({ categoryId: id, rowIndex: 0, zone: "body" })}
733
+ onRowActivate={(row) => cycleSetting(row.key)}
734
+ theme={theme}
735
+ />
736
+ ) : null}
633
737
  </box>
634
738
  </ThemeProvider>
635
739
  );
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
12
  import { DARK } from "./theme";
13
- import { press, waitForState, waitForText, waitForTextGone } from "./test-support";
13
+ import { isolateUserConfig, press, waitForState, waitForText, waitForTextGone } from "./test-support";
14
14
 
15
15
  const PATCH = `diff --git a/src/a.ts b/src/a.ts
16
16
  index 111..222 100644
@@ -38,11 +38,13 @@ index 555..666 100644
38
38
  `;
39
39
 
40
40
  let home: string;
41
+ let restoreUserConfig: () => void;
41
42
  let server: DaemonServer;
42
43
  let session: ReviewSession;
43
44
 
44
45
  beforeEach(() => {
45
46
  home = mkdtempSync(join(tmpdir(), "cueloop-walk-"));
47
+ restoreUserConfig = isolateUserConfig(home);
46
48
  server = new DaemonServer({ home, idleExitMs: 0 });
47
49
  server.start();
48
50
  session = server.core.sessionCreate({
@@ -51,6 +53,7 @@ beforeEach(() => {
51
53
  });
52
54
  });
53
55
  afterEach(() => {
56
+ restoreUserConfig();
54
57
  server.stop();
55
58
  rmSync(home, { recursive: true, force: true });
56
59
  });
@@ -187,7 +190,7 @@ describe("the guided walk", () => {
187
190
  // two full App boots with daemon round-trips need more than the default
188
191
  // budget, and the whole-suite publish lane has timed even 15s out; give the
189
192
  // double-boot chain generous headroom so runner load cannot flake it.
190
- }, 30_000);
193
+ }, 60_000);
191
194
 
192
195
  test("the agent-note block renders only for files carrying a note", async () => {
193
196
  // Arrange
@@ -236,7 +239,7 @@ describe("the guided walk", () => {
236
239
  // Assert
237
240
  // before the walk the sheet's added line wears the insertion color
238
241
  await waitForText(setup, "+new line");
239
- expect(foregroundsOf(setup, "+new line")).toContain(DARK.insFg);
242
+ expect(foregroundsOf(setup, "+new line")).toContain(DARK.insertedForeground);
240
243
 
241
244
  // Act
242
245
  await press(setup, "w");
@@ -246,7 +249,7 @@ describe("the guided walk", () => {
246
249
  // dimmed: the sheet line drops to the dim token...
247
250
  expect(foregroundsOf(setup, "+new line")).toEqual([DARK.textDim]);
248
251
  // ...while the wizard preview keeps the insertion color
249
- expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insFg);
252
+ expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insertedForeground);
250
253
  });
251
254
 
252
255
  test("walking a resolved review answers read-only", async () => {
@@ -11,7 +11,7 @@ export const CommentDraft: Story = {
11
11
  render: () => (
12
12
  <AnnotationCard kind="comment" quote="persists sessions" draft={{ text: "Which daemon?", ...draftCallbacks }} />
13
13
  ),
14
- expectedColors: [DARK.accent, DARK.elevated],
14
+ expectedColors: [DARK.accent],
15
15
  };
16
16
 
17
17
  export const SuggestionDraft: Story = {
@@ -32,8 +32,10 @@ export interface AnnotationSaved {
32
32
  isSelected: boolean;
33
33
  isOrphan: boolean;
34
34
  isBlocking: boolean;
35
- /** Resolved author display name; own notes carry none. Set = name-in-border. */
35
+ /** A collaborator's resolved display name; own notes carry none. Set = name-in-border, blue. */
36
36
  authorLabel?: string;
37
+ /** The viewer's own note when collaborators are also present (e.g. "me"). Set = tag-in-border, accent. */
38
+ selfLabel?: string;
37
39
  /** Non-null while the card body is being rewritten in place. */
38
40
  editing: AnnotationDraft | null;
39
41
  onPress: () => void;
@@ -122,15 +124,15 @@ function DraftEditor({
122
124
  }}
123
125
  style={{
124
126
  height: rows,
125
- backgroundColor: tokens.elevated,
126
- focusedBackgroundColor: tokens.elevated,
127
+ backgroundColor: "transparent",
128
+ focusedBackgroundColor: "transparent",
127
129
  textColor: tokens.text,
128
130
  focusedTextColor: tokens.text,
129
131
  }}
130
132
  />
131
133
  <Toolbar>
132
134
  <Button variant="solid" marginRight={2} onPress={draft.onSave} theme={theme}>
133
- {" Save ⏎ "}
135
+ {" Save "}
134
136
  </Button>
135
137
  <Button onPress={draft.onCancel} theme={theme}>
136
138
  {" Cancel esc "}
@@ -160,6 +162,7 @@ export function AnnotationCard({ id, kind, quote, draft, saved, theme }: Annotat
160
162
  title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
161
163
  contentRows={editorRowCount + 1}
162
164
  borderColor={kindColor}
165
+ backgroundColor="transparent"
163
166
  marginLeft={2}
164
167
  marginRight={2}
165
168
  theme={theme}
@@ -169,24 +172,26 @@ export function AnnotationCard({ id, kind, quote, draft, saved, theme }: Annotat
169
172
  );
170
173
  }
171
174
  const card = saved!;
172
- // A collaborator's note (author set) renders as a named, bordered card; own notes stay borderless.
173
- const authored = card.authorLabel !== undefined && card.authorLabel !== "";
174
- const indent = authored ? "" : " ";
175
+ const collaboratorName = card.authorLabel !== undefined && card.authorLabel !== "" ? card.authorLabel : undefined;
176
+ const ownTag = card.selfLabel !== undefined && card.selfLabel !== "" ? card.selfLabel : undefined;
177
+ const borderLabel = collaboratorName ?? ownTag;
178
+ const borderColor = ownTag ? tokens.accent : tokens.blue;
179
+ const indent = borderLabel ? "" : " ";
175
180
  const marker = card.isSelected ? "▸ " : indent;
176
181
  // Fixed content rows (a bordered box must declare a height or it collapses): header + quote + body, or the editor.
177
182
  const contentRows = card.editing ? editorRowCount + 3 : 3;
178
183
  // The border + its side padding eat this much content width, so text truncates shorter.
179
- const borderInset = authored ? 4 : 0;
184
+ const borderInset = borderLabel ? 4 : 0;
180
185
  return (
181
186
  <box
182
187
  id={id}
183
- title={authored ? ` ${card.authorLabel} ` : undefined}
188
+ title={borderLabel ? ` ${borderLabel} ` : undefined}
184
189
  style={{
185
190
  flexDirection: "column",
186
- marginBottom: 1,
187
- backgroundColor: card.isSelected ? tokens.elevated : undefined,
188
- ...(authored
189
- ? { height: cardHeight(contentRows), border: true, borderStyle: FRAME_BORDER_STYLE, borderColor: tokens.blue, paddingLeft: 1, paddingRight: 1 }
191
+ marginBottom: 0,
192
+ backgroundColor: card.isSelected && !card.editing ? tokens.elevated : undefined,
193
+ ...(borderLabel
194
+ ? { height: cardHeight(contentRows), border: true, borderStyle: FRAME_BORDER_STYLE, borderColor, paddingLeft: 1, paddingRight: 1 }
190
195
  : {}),
191
196
  }}
192
197
  onMouseUp={card.onPress}
@@ -27,7 +27,7 @@ export function CompletionOverlay({ verdict, completion, status, returnsTo, them
27
27
  style={{
28
28
  width: "100%",
29
29
  height: "100%",
30
- backgroundColor: tokens.bg,
30
+ backgroundColor: tokens.background,
31
31
  flexDirection: "column",
32
32
  justifyContent: "center",
33
33
  alignItems: "center",
@@ -94,6 +94,7 @@ export function ConfirmCard({
94
94
  title=" submit review "
95
95
  contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 1 : 0)}
96
96
  borderColor={tokens.accent}
97
+ backgroundColor="transparent"
97
98
  marginRight={1}
98
99
  theme={theme}
99
100
  >
@@ -16,6 +16,6 @@ export const DeletePlan: Story = {
16
16
  {...callbacks}
17
17
  />
18
18
  ),
19
- expectedColors: [DARK.backdrop, DARK.elevated, DARK.accent],
19
+ expectedColors: [DARK.accent],
20
20
  size: { width: 64, height: 16 },
21
21
  };
@@ -44,7 +44,7 @@ export function ConfirmDialog({
44
44
  <box style={{ flexGrow: 1 }} />
45
45
  <Toolbar>
46
46
  <Button variant="solid" onPress={onConfirm} theme={theme}>
47
- {` ${confirmLabel} ⏎ `}
47
+ {` ${confirmLabel} `}
48
48
  </Button>
49
49
  <Button onPress={onCancel} theme={theme}>
50
50
  {` ${cancelLabel} esc `}
@@ -8,13 +8,31 @@ export const meta: StoryMeta = { title: "DiffSheet" };
8
8
 
9
9
  export const SignedRows: Story = {
10
10
  render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
11
- expectedColors: [DARK.insFg, DARK.delFg],
11
+ expectedColors: [DARK.insertedForeground, DARK.deletedForeground],
12
12
  size: { width: 90, height: 16 },
13
13
  };
14
14
 
15
15
  export const CursorOnAddedLine: Story = {
16
16
  render: () => <DiffSheet rows={fixtureDiffRows()} cursor={4} annotations={[]} />,
17
- expectedColors: [DARK.cursorBg],
17
+ expectedColors: [DARK.cursorBackground],
18
+ size: { width: 90, height: 16 },
19
+ };
20
+
21
+ export const ComposingOnLine: Story = {
22
+ render: () => (
23
+ <DiffSheet
24
+ rows={fixtureDiffRows()}
25
+ cursor={4}
26
+ annotations={[]}
27
+ compose={{
28
+ kind: "comment",
29
+ rowIndex: 4,
30
+ quote: " private items = new Map();",
31
+ draft: { text: "Map needs an eviction story.", onInput: () => {}, onSave: () => {}, onCancel: () => {} },
32
+ }}
33
+ />
34
+ ),
35
+ expectedColors: [DARK.accent],
18
36
  size: { width: 90, height: 16 },
19
37
  };
20
38
 
@@ -39,6 +57,6 @@ export const AnnotatedLine: Story = {
39
57
  ]}
40
58
  />
41
59
  ),
42
- expectedColors: [DARK.markCommentBg],
60
+ expectedColors: [DARK.markCommentBackground],
43
61
  size: { width: 90, height: 16 },
44
62
  };