@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.66

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 (78) hide show
  1. package/package.json +5 -5
  2. package/src/App.diff.test.tsx +8 -73
  3. package/src/App.plan-review.test.tsx +4 -185
  4. package/src/App.prototype.test.tsx +1 -8
  5. package/src/App.readonly.test.tsx +6 -28
  6. package/src/App.submit-confirm.test.tsx +19 -72
  7. package/src/App.test.tsx +70 -24
  8. package/src/App.tsx +177 -254
  9. package/src/App.walk.test.tsx +1 -1
  10. package/src/app-screens.tsx +40 -51
  11. package/src/app-view-model.ts +2 -4
  12. package/src/components/AnnotationCard.stories.tsx +1 -1
  13. package/src/components/AppShell.stories.tsx +84 -0
  14. package/src/components/AppShell.tsx +127 -0
  15. package/src/components/Breadcrumb.stories.tsx +1 -1
  16. package/src/components/ChangesColumn.stories.tsx +39 -0
  17. package/src/components/ChangesColumn.tsx +147 -0
  18. package/src/components/CodeBlock.stories.tsx +1 -1
  19. package/src/components/CompletionOverlay.stories.tsx +1 -1
  20. package/src/components/ConfirmCard.stories.tsx +1 -1
  21. package/src/components/ConfirmCard.tsx +3 -4
  22. package/src/components/ConfirmDialog.stories.tsx +1 -1
  23. package/src/components/DiffSheet.stories.tsx +1 -1
  24. package/src/components/InboxList.stories.tsx +23 -8
  25. package/src/components/InboxList.tsx +45 -19
  26. package/src/components/KeybindsDialog.stories.tsx +1 -1
  27. package/src/components/MarkerPopover.stories.tsx +1 -1
  28. package/src/components/MenuBar.stories.tsx +3 -12
  29. package/src/components/MenuBar.tsx +25 -56
  30. package/src/components/PromptDialog.stories.tsx +1 -1
  31. package/src/components/PrototypeSheet.stories.tsx +1 -1
  32. package/src/components/ReviewPanel.stories.tsx +1 -1
  33. package/src/components/ReviewRail.stories.tsx +1 -1
  34. package/src/components/SettingsDialog.stories.tsx +7 -1
  35. package/src/components/SettingsDialog.tsx +112 -66
  36. package/src/components/SettingsRows.stories.tsx +1 -1
  37. package/src/components/ShellHeader.stories.tsx +50 -0
  38. package/src/components/ShellHeader.tsx +137 -0
  39. package/src/components/ThreadFooter.stories.tsx +30 -0
  40. package/src/components/ThreadFooter.tsx +49 -0
  41. package/src/components/ThreadView.slash.test.tsx +113 -0
  42. package/src/components/ThreadView.stories.tsx +1 -1
  43. package/src/components/ThreadView.tsx +99 -11
  44. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  45. package/src/components/ThreadsSidebar.tsx +57 -0
  46. package/src/components/Toast.stories.tsx +1 -1
  47. package/src/components/TreePane.stories.tsx +1 -1
  48. package/src/components/WalkWizard.stories.tsx +1 -1
  49. package/src/components/__snapshots__/stories.test.tsx.snap +348 -138
  50. package/src/components/agent-launcher.stories.tsx +1 -1
  51. package/src/components/file-tree.test.ts +45 -0
  52. package/src/components/file-tree.ts +57 -0
  53. package/src/components/pages/Workspace.stories.tsx +181 -0
  54. package/src/components/primitives/Button.stories.tsx +1 -1
  55. package/src/components/primitives/Card.stories.tsx +1 -1
  56. package/src/components/primitives/Dialog.stories.tsx +1 -1
  57. package/src/components/primitives/IconButton.stories.tsx +36 -0
  58. package/src/components/primitives/IconButton.tsx +43 -0
  59. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  60. package/src/components/primitives/Tabs.stories.tsx +1 -1
  61. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  62. package/src/components/primitives/Tree.stories.tsx +124 -0
  63. package/src/components/primitives/Tree.tsx +87 -0
  64. package/src/components/primitives/icons.ts +38 -0
  65. package/src/components/primitives/tree-model.test.ts +99 -0
  66. package/src/components/primitives/tree-model.ts +137 -0
  67. package/src/components/quick-actions-editor.stories.tsx +1 -1
  68. package/src/components/session-tree.test.ts +114 -0
  69. package/src/components/session-tree.ts +94 -0
  70. package/src/components/stories-app.tsx +142 -82
  71. package/src/components/story-tree.test.ts +100 -0
  72. package/src/components/story.ts +55 -0
  73. package/src/components/theme-context.stories.tsx +1 -1
  74. package/src/session-controller.ts +9 -4
  75. package/src/theme.ts +4 -4
  76. package/src/use-settings-dialog.tsx +9 -0
  77. package/src/App.review-panel.test.tsx +0 -140
  78. package/src/App.tree.test.tsx +0 -224
package/src/App.tsx CHANGED
@@ -18,7 +18,7 @@ import React, {
18
18
  useSyncExternalStore,
19
19
  } from "react";
20
20
  import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
21
- import type { Clock, MouseEvent } from "@opentui/core";
21
+ import type { Clock } from "@opentui/core";
22
22
  import { marksByDisplay, type Mark } from "./view-plan";
23
23
  import { dimmedTheme } from "./theme";
24
24
  import {
@@ -39,13 +39,19 @@ import {
39
39
  import type { Theme } from "./theme";
40
40
  import { createReviewController, type ShareTransport } from "./session-controller";
41
41
  import type { SessionClient } from "@cueloop/daemon/client";
42
- import { launchHarnessInSplit } from "@cueloop/daemon/herdr-split";
43
42
  import { createIntentDispatch, type Mode } from "./intent-dispatch";
44
43
  import { reduceKey, type KeyState } from "./keymap";
45
44
  import { KeyBindings, type CheatsheetSection } from "./key-bindings";
46
45
  import { ThemeProvider } from "./components/theme-context";
47
46
  import { Button } from "./components/primitives/Button";
48
47
  import { Toolbar } from "./components/primitives/Toolbar";
48
+ import { NERD } from "./components/primitives/icons";
49
+ import { groupInbox, projectName } from "./components/session-tree";
50
+ import { ThreadsSidebar } from "./components/ThreadsSidebar";
51
+ import { ChangesColumn, DiffChangesToggle, useDiffColumns } from "./components/ChangesColumn";
52
+ import { IconButton } from "./components/primitives/IconButton";
53
+ import { ThreadFooter } from "./components/ThreadFooter";
54
+ import { ConfirmCard } from "./components/ConfirmCard";
49
55
  import { Breadcrumb } from "./components/Breadcrumb";
50
56
  import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
51
57
  import {
@@ -58,23 +64,12 @@ import { DiffSheet } from "./components/DiffSheet";
58
64
  import { PrototypeSheet } from "./components/PrototypeSheet";
59
65
  import type { PrototypeElement } from "./prototype-browser";
60
66
  import { type RailTab, type ReviewRailHandle } from "./components/ReviewRail";
61
- import type { AgentTerminalHandle } from "./components/agent-launcher";
62
- import { ReviewPanel } from "./components/ReviewPanel";
67
+ import { REVIEW_DEFAULT_WIDTH, type ReviewPanelMode } from "./review-panel";
63
68
  import {
64
- REVIEW_DEFAULT_WIDTH,
65
- resolveReviewWidth,
66
- toggleReviewPanelMode,
67
- widthFromMouseColumn,
68
- type ReviewPanelMode,
69
- } from "./review-panel";
70
- import {
71
- buildCardEditState,
72
69
  buildDiffComposeState,
73
70
  buildHeaderItems,
74
71
  buildRenderFlags,
75
72
  buildSubmitConfirmState,
76
- computePendingCount,
77
- computeRailFootprint,
78
73
  computeRoleCapabilities,
79
74
  deriveReviewFlags,
80
75
  isCompletionOverlayPhase,
@@ -129,17 +124,23 @@ export interface AppProps {
129
124
  }
130
125
 
131
126
  /** True while the drop-up or one of its dialogs is open and owns the keyboard. */
132
- function menuChromeOpen(menuOpen: boolean, menuDialog: "keybinds" | "settings" | null): boolean {
133
- return menuOpen || menuDialog !== null;
127
+ function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
128
+ return menuDialog !== null;
129
+ }
130
+
131
+ /** True while a menu or an overlay owns the keyboard instead of the thread view. */
132
+ function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["overlay"]): boolean {
133
+ return menuOwnsKeyboard || overlay !== "none";
134
134
  }
135
135
 
136
- /** True while a menu, an overlay, or the agent terminal owns the keyboard instead of the thread view. */
137
- function keyboardOwnedElsewhere(
138
- menuOwnsKeyboard: boolean,
139
- overlay: KeyState["overlay"],
140
- agentTerminal: AgentTerminalHandle | null,
141
- ): boolean {
142
- return menuOwnsKeyboard || overlay !== "none" || agentTerminal !== null;
136
+ /** The footer submit fires only for the owner of an unresolved review, never an observer. */
137
+ function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
138
+ return isOwner && !resolved && !observer;
139
+ }
140
+
141
+ /** The left panel toggle glyph: the mirrored sidebar icon, filled when the column is open. */
142
+ function sidebarToggleGlyph(open: boolean): string {
143
+ return open ? NERD.sidebarLeft : NERD.sidebarLeftOff;
143
144
  }
144
145
 
145
146
  /** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
@@ -215,9 +216,13 @@ export function App({
215
216
  // ── view state ──────────────────────────────
216
217
  const [cursor, setCursor] = useState(0);
217
218
  const [inboxCursor, setInboxCursor] = useState(0);
219
+ // Projects and Threads grouping; ordered is the flat sequence the inbox cursor walks
220
+ const grouped = useMemo(() => groupInbox(inbox ?? []), [inbox]);
221
+ // the left Projects and Threads column; collapsed by default per the context
222
+ // matrix (the thread owns the width), toggled open to jump between threads
223
+ const [sidebarOpen, setSidebarOpen] = useState(false);
218
224
  const [mode, setMode] = useState<Mode>({ type: "normal" });
219
- // the bottom-left menu drop-up and the centered dialog it opens
220
- const [menuOpen, setMenuOpen] = useState(false);
225
+ // the top-left settings gear drop-down and the centered dialog it opens
221
226
  const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
222
227
  const [autoClose, setAutoClose] = useState<AutoClose>("off");
223
228
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
@@ -225,14 +230,11 @@ export function App({
225
230
  const [railTab, setRailTab] = useState<RailTab>("review");
226
231
  // the tree row the reviewer stands on in the rail's Tree tab
227
232
  const [selectedEntryId, setSelectedEntryId] = useState<string | undefined>(undefined);
228
- // A running in-tab agent terminal (embedded harness); while set, keys route to it.
229
- const [agentTerminal, setAgentTerminal] = useState<AgentTerminalHandle | null>(null);
230
233
  // review panel layout: mode + expanded width are client view state, loaded
231
234
  // from and persisted to the user config so they survive a restart. The ref
232
235
  // mirrors the width so the drag-end persist reads the latest value.
233
236
  const [reviewMode, setReviewMode] = useState<ReviewPanelMode>("expanded");
234
237
  const [reviewWidth, setReviewWidth] = useState(REVIEW_DEFAULT_WIDTH);
235
- const [dividerDragging, setDividerDragging] = useState(false);
236
238
  const reviewWidthRef = useRef(REVIEW_DEFAULT_WIDTH);
237
239
  // ~2s focus pulse on the document highlight when a rail card is activated
238
240
  const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
@@ -320,8 +322,8 @@ export function App({
320
322
  // ── derived view model ──────────────────────
321
323
  const display = controller.display();
322
324
  const rows = controller.rows();
325
+ const diffColumns = useDiffColumns({ session, rows, cursor, setCursor });
323
326
  const rejectedRows = controller.rejectedRows();
324
- const curationItems = controller.curationItems();
325
327
  const marks = useMemo(
326
328
  () =>
327
329
  session
@@ -347,79 +349,17 @@ export function App({
347
349
  // sort position per annotation so the rail interleaves annotation and removal
348
350
  // cards in one line-ordered stack: a diff row carries its blockIndex; a plan
349
351
  // annotation resolves to the display index it marked
350
- const annotationPositions = useMemo(() => {
351
- const positions = new Map<string, number>();
352
-
353
- if (!session) return positions;
354
- if (isDiff) {
355
- for (const annotation of session.annotations) {
356
- const blockIndex =
357
- "blockIndex" in annotation.anchor ? annotation.anchor.blockIndex : undefined;
358
-
359
- if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
360
- }
361
- } else {
362
- for (const [displayIndex, blockMarks] of marks) {
363
- for (const mark of blockMarks)
364
- if (mark.annotationId) positions.set(mark.annotationId, displayIndex);
365
- }
366
- }
367
-
368
- return positions;
369
- }, [session, isDiff, marks]);
370
-
371
352
  // ── the guided walk's view model ────────────
372
353
  const walkFileList = controller.files();
373
354
  const walking = isWalking(isDiff, walk);
374
355
  const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
375
356
 
376
357
  // ── selection symmetry: one selected id, both sides ──
377
- const pulse = (id: string): void => {
378
- setPulsedAnnotationId(id);
379
- if (pulseTimer.current) clearTimeout(pulseTimer.current);
380
- pulseTimer.current = setTimeout(() => setPulsedAnnotationId(null), 2000);
381
- };
382
-
383
- /** Card activation scrolls the document to the anchor and pulses it. */
384
- const revealAnchor = (annotationId: string): void => {
385
- for (const [displayIndex, blockMarks] of marks) {
386
- if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
387
- setCursor(displayIndex);
388
-
389
- return;
390
- }
391
- };
392
-
393
358
  const selectCardFromDocument = (annotationId: string): void => {
394
359
  setFocusedAnnotationId(annotationId);
395
360
  railRef.current?.revealCard(annotationId);
396
361
  };
397
362
 
398
- const selectCardFromRail = (annotationId: string): void => {
399
- setFocusedAnnotationId(annotationId);
400
- pulse(annotationId);
401
- revealAnchor(annotationId);
402
- };
403
-
404
- // selecting a removal reveals its source line, so the reader sees what the
405
- // pending undo would bring back: the diff sheet follows the cursor; the plan
406
- // sheet also needs an explicit scroll to the cut block
407
- const selectCurationFromRail = (curationId: string): void => {
408
- setSelectedCurationId(curationId);
409
- const item = curationItems.find((candidate) => candidate.id === curationId);
410
-
411
- if (!item) return;
412
- setCursor(item.revealIndex);
413
- };
414
-
415
- // the selected removal card's undo button: same restore path as the u key,
416
- // including the observer read-only guard the keyboard path gets via reduceKey
417
- const undoCurationFromRail = (curationId: string): void => {
418
- if (observer) return controller.setStatus("observer - read-only");
419
- controller.restoreCuration(curationId);
420
- setSelectedCurationId(undefined);
421
- };
422
-
423
363
  const openCardEdit = (annotationId: string): void => {
424
364
  if (observer) return controller.setStatus("observer - read-only");
425
365
  const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
@@ -459,7 +399,7 @@ export function App({
459
399
  display,
460
400
  rows,
461
401
  cursor,
462
- inbox,
402
+ inbox: inbox === null ? null : grouped.ordered,
463
403
  inboxCursor,
464
404
  mode,
465
405
  session,
@@ -495,10 +435,10 @@ export function App({
495
435
 
496
436
  const overlay = resolveOverlay(mode, completion.phase, walking);
497
437
 
498
- const menuOwnsKeyboard = menuChromeOpen(menuOpen, menuDialog);
438
+ const menuOwnsKeyboard = menuChromeOpen(menuDialog);
499
439
  // an overlay (submit, walk, prompt, confirm) or the menu takes the keyboard
500
440
  // from the thread view; the view suspends its own grammar meanwhile
501
- const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay, agentTerminal);
441
+ const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
502
442
 
503
443
  useKeyboard((key) => {
504
444
  // The thread view owns the document grammar while active (its own
@@ -520,16 +460,8 @@ export function App({
520
460
  // The prototype compose textarea owns the keyboard while open: let it receive
521
461
  // the typed note instead of the global keymap acting on each letter.
522
462
  if (prototypeComposing) return;
523
- // A running in-tab agent terminal owns the keyboard: forward every key to it,
524
- // with ctrl+] as the detach chord back to the review.
525
- if (agentTerminal) {
526
- if (key.ctrl && key.name === "]") return void agentTerminal.detach();
527
-
528
- return void agentTerminal.write(key.sequence);
529
- }
530
463
  if (menuDialog === "settings") return void handleSettingsKey(key.name);
531
464
  if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
532
- if (menuOpen) return void (key.name === "escape" && setMenuOpen(false));
533
465
  // the toast is non-modal: escape only dismisses it when nothing else owns
534
466
  // escape, so an open overlay (compose, submit, prompt, walk) still cancels
535
467
  if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
@@ -552,8 +484,14 @@ export function App({
552
484
  display,
553
485
  });
554
486
 
555
- keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
556
- const action = keyBindings.resolveAction({ name: key.name, shift: !!key.shift });
487
+ keyBindings.setContext({
488
+ overlay: state.overlay,
489
+ spanMode: state.spanMode,
490
+ });
491
+ const action = keyBindings.resolveAction({
492
+ name: key.name,
493
+ shift: !!key.shift,
494
+ });
557
495
 
558
496
  for (const intent of reduceKey(
559
497
  state,
@@ -567,12 +505,8 @@ export function App({
567
505
  // reused by the inbox and by plan/diff review so the two never drift ──
568
506
  const menuChrome = (
569
507
  <MenuChrome
570
- menuOpen={menuOpen}
571
508
  menuDialog={menuDialog}
572
- status={status}
573
509
  theme={theme}
574
- setMenuOpen={setMenuOpen}
575
- setMenuDialog={setMenuDialog}
576
510
  keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
577
511
  settingsCategories={settingsCategories}
578
512
  settingsValues={settingsValues}
@@ -587,13 +521,14 @@ export function App({
587
521
  if (!session)
588
522
  return inbox ? (
589
523
  <InboxScreen
590
- inbox={inbox}
524
+ rows={grouped.rows}
591
525
  inboxCursor={inboxCursor}
592
526
  mode={mode}
593
527
  theme={theme}
594
528
  controller={controller}
595
529
  setMode={setMode}
596
530
  menuChrome={menuChrome}
531
+ onOpenMenu={() => setMenuDialog("settings")}
597
532
  />
598
533
  ) : (
599
534
  <ConnectingScreen theme={theme} />
@@ -630,21 +565,21 @@ export function App({
630
565
  setMode,
631
566
  dispatch,
632
567
  });
633
- const cardEditState = buildCardEditState({ mode, liveInput, setMode, dispatch });
634
- const pendingCount = computePendingCount(activeSession);
635
- const railFootprint = computeRailFootprint(reviewMode, reviewWidth, terminalWidth);
636
- const headerItems = buildHeaderItems({ session: activeSession, resolved, observer, role });
637
- const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath, railResolvedIds } =
638
- buildRenderFlags({
639
- session: activeSession,
640
- isOwner,
641
- isDiff,
642
- isPrototype,
643
- resolved,
644
- menuOpen,
645
- menuDialog,
646
- resolvedIds,
647
- });
568
+ const headerItems = buildHeaderItems({
569
+ session: activeSession,
570
+ resolved,
571
+ observer,
572
+ role,
573
+ });
574
+ const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath } = buildRenderFlags({
575
+ session: activeSession,
576
+ isOwner,
577
+ isDiff,
578
+ isPrototype,
579
+ resolved,
580
+ menuDialog,
581
+ resolvedIds,
582
+ });
648
583
 
649
584
  const onEditRequest = (): void => {
650
585
  // A share viewer/observer has no Edit affordance (the button is hidden), so
@@ -668,19 +603,6 @@ export function App({
668
603
  dispatch({ type: "openSubmit" });
669
604
  };
670
605
 
671
- // the clickable chevron toggles expanded <-> compact; the keybinding (b)
672
- // cycles all three including hidden
673
- const onToggleReviewPanel = (): void => {
674
- const next = toggleReviewPanelMode(reviewMode);
675
-
676
- setReviewMode(next);
677
- controller.saveReviewPanel({ mode: next });
678
- };
679
- // grabbing the divider only arms a drag when there is a width to drag
680
- const onDividerGrab = (): void => {
681
- if (reviewMode === "expanded") setDividerDragging(true);
682
- };
683
-
684
606
  return (
685
607
  <ThemeProvider theme={theme}>
686
608
  <box
@@ -690,26 +612,25 @@ export function App({
690
612
  height: "100%",
691
613
  backgroundColor: theme.background,
692
614
  }}
693
- onMouseDrag={(event: MouseEvent) => {
694
- if (!dividerDragging || reviewMode !== "expanded") return;
695
- const next = widthFromMouseColumn(event.x, terminalWidth);
696
-
697
- // many raw mouse-moves land in the same column: only re-render when the
698
- // width actually changes, so the drag does not reconcile per pixel
699
- if (next === reviewWidthRef.current) return;
700
- reviewWidthRef.current = next;
701
- setReviewWidth(next);
702
- }}
703
- onMouseUp={() => {
704
- if (!dividerDragging) return;
705
- setDividerDragging(false);
706
- controller.saveReviewPanel({ width: reviewWidthRef.current });
707
- }}
708
615
  >
709
616
  <box
710
- style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}
617
+ style={{
618
+ flexDirection: "row",
619
+ height: 2,
620
+ paddingTop: 1,
621
+ backgroundColor: theme.panel,
622
+ }}
711
623
  >
712
624
  <box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
625
+ <box onMouseUp={() => setMenuDialog("settings")} style={{ paddingRight: 2 }}>
626
+ <text fg={theme.textMuted}>{NERD.settings}</text>
627
+ </box>
628
+ <IconButton
629
+ glyph={sidebarToggleGlyph(sidebarOpen)}
630
+ onPress={() => setSidebarOpen((open) => !open)}
631
+ marginRight={2}
632
+ theme={theme}
633
+ />
713
634
  <Breadcrumb items={headerItems} />
714
635
  <box style={{ flexGrow: 1 }} />
715
636
  {showOwnerActions ? (
@@ -722,111 +643,98 @@ export function App({
722
643
  </Button>
723
644
  </Toolbar>
724
645
  ) : null}
646
+ <DiffChangesToggle
647
+ isDiff={isDiff}
648
+ open={diffColumns.changesOpen}
649
+ onToggle={diffColumns.toggleChanges}
650
+ theme={theme}
651
+ />
725
652
  </box>
726
- <box style={{ width: railFootprint }} />
727
653
  </box>
728
654
  <box style={{ flexGrow: 1, flexDirection: "row" }}>
729
- {isPrototype ? (
730
- <PrototypeSheet
731
- prototypePath={prototypePath}
732
- quickActions={quickActions}
733
- canComment={prototypeCanComment}
734
- onCommentElement={onCommentPrototype}
735
- onComposingChange={setPrototypeComposing}
736
- hidden={chromeHidden}
737
- />
738
- ) : isDiff ? (
739
- // the sheet dims to reading-quiet colors while the wizard has focus
740
- <DiffSheet
741
- rows={rows}
742
- cursor={cursor}
743
- annotations={activeSession.annotations}
744
- focusedAnnotationId={focusedAnnotationId}
745
- rejectedRows={rejectedRows}
746
- compose={diffComposeState}
747
- theme={walking ? dimmedTheme(theme) : undefined}
748
- />
749
- ) : (
750
- <ThreadView
751
- session={activeSession}
752
- suspended={threadViewSuspended}
753
- editOrphanCount={editOrphanCount}
754
- onComposingChange={setThreadComposing}
755
- resolved={resolved}
756
- onObserverBlocked={(reason) =>
757
- controller.setStatus(
758
- reason === "observer" ? "observer - read-only" : "review submitted - read-only",
759
- )
760
- }
761
- onCursorChange={setCursor}
762
- focusedAnnotationId={focusedAnnotationId}
763
- onFocusAnnotation={setFocusedAnnotationId}
764
- display={display}
765
- marks={marks}
766
- quickActions={quickActions}
767
- observer={observer}
768
- onAnnotate={(span, body) =>
769
- void controller.annotate(
770
- "comment",
771
- span.start.blockIndex,
772
- span.start.char,
773
- span.end.char,
774
- body,
775
- span.end.blockIndex,
776
- )
777
- }
778
- onReply={(rootAnnotationId, body) => void controller.reply(rootAnnotationId, body)}
779
- onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
780
- onExit={() => onExit?.(0)}
655
+ <ThreadsSidebar
656
+ open={sidebarOpen}
657
+ rows={grouped.rows}
658
+ cursor={inboxCursor}
659
+ activeId={session.id}
660
+ onSelect={(id) => controller.open(id)}
661
+ theme={theme}
662
+ />
663
+ <box style={{ flexGrow: 1, flexDirection: "column" }}>
664
+ <box style={{ flexGrow: 1, flexDirection: "row" }}>
665
+ {isPrototype ? (
666
+ <PrototypeSheet
667
+ prototypePath={prototypePath}
668
+ quickActions={quickActions}
669
+ canComment={prototypeCanComment}
670
+ onCommentElement={onCommentPrototype}
671
+ onComposingChange={setPrototypeComposing}
672
+ hidden={chromeHidden}
673
+ />
674
+ ) : isDiff ? (
675
+ // the sheet dims to reading-quiet colors while the wizard has focus
676
+ <DiffSheet
677
+ rows={rows}
678
+ cursor={cursor}
679
+ annotations={activeSession.annotations}
680
+ focusedAnnotationId={focusedAnnotationId}
681
+ rejectedRows={rejectedRows}
682
+ compose={diffComposeState}
683
+ theme={walking ? dimmedTheme(theme) : undefined}
684
+ />
685
+ ) : (
686
+ <ThreadView
687
+ session={activeSession}
688
+ suspended={threadViewSuspended}
689
+ editOrphanCount={editOrphanCount}
690
+ onComposingChange={setThreadComposing}
691
+ resolved={resolved}
692
+ onObserverBlocked={(reason) =>
693
+ controller.setStatus(
694
+ reason === "observer"
695
+ ? "observer - read-only"
696
+ : "review submitted - read-only",
697
+ )
698
+ }
699
+ onCursorChange={setCursor}
700
+ focusedAnnotationId={focusedAnnotationId}
701
+ onFocusAnnotation={setFocusedAnnotationId}
702
+ display={display}
703
+ marks={marks}
704
+ quickActions={quickActions}
705
+ observer={observer}
706
+ onAnnotate={(span, body) =>
707
+ void controller.annotate(
708
+ "comment",
709
+ span.start.blockIndex,
710
+ span.start.char,
711
+ span.end.char,
712
+ body,
713
+ span.end.blockIndex,
714
+ )
715
+ }
716
+ onReply={(rootAnnotationId, body) =>
717
+ void controller.reply(rootAnnotationId, body)
718
+ }
719
+ onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
720
+ onExit={() => onExit?.(0)}
721
+ />
722
+ )}
723
+ </box>
724
+ <ThreadFooter
725
+ repo={projectName(activeSession.workspace)}
726
+ branch={activeSession.workspace.branch}
727
+ onSubmit={onSubmitRequest}
728
+ canSubmit={canSubmitReview(isOwner, resolved, observer)}
729
+ theme={theme}
781
730
  />
782
- )}
783
- <ReviewPanel
784
- mode={reviewMode}
785
- width={resolveReviewWidth(reviewWidth, terminalWidth)}
786
- onDividerGrab={onDividerGrab}
787
- onToggle={onToggleReviewPanel}
788
- railRef={railRef}
789
- rail={{
790
- session: activeSession,
791
- authorNames,
792
- selectedId: focusedAnnotationId,
793
- resolvedIds: railResolvedIds,
794
- curationItems,
795
- selectedCurationId,
796
- annotationPositions,
797
- railTab,
798
- pendingCount,
799
- cardEdit: cardEditState,
800
- submitConfirm: submitConfirmState,
801
- onTabChange: setRailTab,
802
- onSelectCard: selectCardFromRail,
803
- onActivateCard: openCardEdit,
804
- onSelectCuration: selectCurationFromRail,
805
- onUndoCuration: undoCurationFromRail,
806
- onSubmitRequest,
807
- onLaunchHarness: (command, seedText) =>
808
- launchHarnessInSplit({
809
- command,
810
- cwd: activeSession.artifact.meta.cwd ?? process.cwd(),
811
- seedText,
812
- }),
813
- onAgentTerminal: setAgentTerminal,
814
- tree: {
815
- rows: controller.treeRows(),
816
- selectedEntryId,
817
- canMove: isOwner && !resolved,
818
- canFork: isOwner,
819
- onSelect: setSelectedEntryId,
820
- onGo: (entryId) => {
821
- setSelectedEntryId(entryId);
822
- dispatch({ type: "treeGo" });
823
- },
824
- onBranch: () => dispatch({ type: "treeBranch" }),
825
- onLabel: () => dispatch({ type: "treeLabel" }),
826
- onFork: () => dispatch({ type: "treeFork" }),
827
- onForkAndShare: () => dispatch({ type: "treeForkShare" }),
828
- },
829
- }}
731
+ </box>
732
+ <ChangesColumn
733
+ open={diffColumns.changesOpen}
734
+ files={activeSession.artifact.files}
735
+ selectedPath={diffColumns.currentFilePath}
736
+ onSelectFile={diffColumns.scrollToFile}
737
+ theme={theme}
830
738
  />
831
739
  </box>
832
740
  <TrailingOverlays
@@ -842,6 +750,21 @@ export function App({
842
750
  setMode={setMode}
843
751
  dispatch={dispatch}
844
752
  />
753
+ {submitConfirmState !== null ? (
754
+ <box
755
+ style={{
756
+ position: "absolute",
757
+ left: 0,
758
+ top: 0,
759
+ width: "100%",
760
+ height: "100%",
761
+ justifyContent: "center",
762
+ alignItems: "center",
763
+ }}
764
+ >
765
+ <ConfirmCard {...submitConfirmState} theme={theme} />
766
+ </box>
767
+ ) : null}
845
768
  {menuChrome}
846
769
  </box>
847
770
  </ThemeProvider>
@@ -157,7 +157,7 @@ describe("the guided walk", () => {
157
157
  await press(setup, "enter");
158
158
 
159
159
  // Assert
160
- await waitForText(setup, "submit review");
160
+ await waitForText(setup, "send message");
161
161
  expect(setup.captureCharFrame()).toContain("3/3 files viewed");
162
162
 
163
163
  // Act