@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
@@ -12,11 +12,11 @@ import type { SettingsCategory, SettingsValues } from "./components/SettingsDial
12
12
  import type { SettingsNav } from "./use-settings-dialog";
13
13
  import { CLIENT_VERSION } from "./version";
14
14
  import { ThemeProvider } from "./components/theme-context";
15
- import { MenuBar } from "./components/MenuBar";
16
- import { KeybindsDialog } from "./components/KeybindsDialog";
15
+ import { NERD } from "./components/primitives/icons";
16
+ import type { InboxRow } from "./components/session-tree";
17
17
  import { SettingsDialog } from "./components/SettingsDialog";
18
18
  import { CompletionOverlay } from "./components/CompletionOverlay";
19
- import { InboxList } from "./components/InboxList";
19
+ import { ThreadsSidebar } from "./components/ThreadsSidebar";
20
20
  import { ConfirmDialog } from "./components/ConfirmDialog";
21
21
  import { PromptDialog } from "./components/PromptDialog";
22
22
  import { WalkWizard } from "./components/WalkWizard";
@@ -39,12 +39,8 @@ export function ConnectingScreen({ theme }: { theme: Theme }): React.ReactNode {
39
39
  }
40
40
 
41
41
  export function MenuChrome(props: {
42
- menuOpen: boolean;
43
42
  menuDialog: "keybinds" | "settings" | null;
44
- status: string;
45
43
  theme: Theme;
46
- setMenuOpen: Dispatch<SetStateAction<boolean>>;
47
- setMenuDialog: Dispatch<SetStateAction<"keybinds" | "settings" | null>>;
48
44
  keybindsSections: CheatsheetSection[];
49
45
  settingsCategories: SettingsCategory[];
50
46
  settingsValues: SettingsValues;
@@ -53,12 +49,8 @@ export function MenuChrome(props: {
53
49
  cycleSetting: (rowKey: string) => void;
54
50
  }): React.ReactNode {
55
51
  const {
56
- menuOpen,
57
52
  menuDialog,
58
- status,
59
53
  theme,
60
- setMenuOpen,
61
- setMenuDialog,
62
54
  keybindsSections,
63
55
  settingsCategories,
64
56
  settingsValues,
@@ -67,53 +59,37 @@ export function MenuChrome(props: {
67
59
  cycleSetting,
68
60
  } = props;
69
61
 
62
+ // the gear opens the settings dialog directly; Keybinds is a leaf in its tree nav
63
+ if (menuDialog !== "settings") return null;
64
+
70
65
  return (
71
- <>
72
- <MenuBar
73
- open={menuOpen}
74
- version={CLIENT_VERSION}
75
- status={status}
76
- onToggle={() => setMenuOpen((isOpen) => !isOpen)}
77
- onSettings={() => {
78
- setMenuOpen(false);
79
- setMenuDialog("settings");
80
- }}
81
- onKeybinds={() => {
82
- setMenuOpen(false);
83
- setMenuDialog("keybinds");
84
- }}
85
- theme={theme}
86
- />
87
- {menuDialog === "keybinds" ? (
88
- <KeybindsDialog sections={keybindsSections} theme={theme} />
89
- ) : null}
90
- {menuDialog === "settings" ? (
91
- <SettingsDialog
92
- isOpen
93
- categories={settingsCategories}
94
- values={settingsValues}
95
- activeCategoryId={settingsNav.categoryId}
96
- activeRowIndex={settingsNav.rowIndex}
97
- activeZone={settingsNav.zone}
98
- onCategorySelect={onCategorySelect}
99
- onRowActivate={(row) => cycleSetting(row.key)}
100
- theme={theme}
101
- />
102
- ) : null}
103
- </>
66
+ <SettingsDialog
67
+ isOpen
68
+ version={CLIENT_VERSION}
69
+ keybindsSections={keybindsSections}
70
+ categories={settingsCategories}
71
+ values={settingsValues}
72
+ activeCategoryId={settingsNav.categoryId}
73
+ activeRowIndex={settingsNav.rowIndex}
74
+ activeZone={settingsNav.zone}
75
+ onCategorySelect={onCategorySelect}
76
+ onRowActivate={(row) => cycleSetting(row.key)}
77
+ theme={theme}
78
+ />
104
79
  );
105
80
  }
106
81
 
107
82
  export function InboxScreen(props: {
108
- inbox: ReviewSession[];
83
+ rows: InboxRow[];
109
84
  inboxCursor: number;
110
85
  mode: Mode;
111
86
  theme: Theme;
112
87
  controller: ReviewController;
113
88
  setMode: Dispatch<SetStateAction<Mode>>;
114
89
  menuChrome: React.ReactNode;
90
+ onOpenMenu: () => void;
115
91
  }): React.ReactNode {
116
- const { inbox, inboxCursor, mode, theme, controller, setMode, menuChrome } = props;
92
+ const { rows, inboxCursor, mode, theme, controller, setMode, menuChrome, onOpenMenu } = props;
117
93
  const confirming = mode.type === "confirmDelete" ? mode : null;
118
94
 
119
95
  return (
@@ -139,6 +115,9 @@ export function InboxScreen(props: {
139
115
  flexDirection: "row",
140
116
  }}
141
117
  >
118
+ <box onMouseUp={onOpenMenu} style={{ paddingRight: 2 }}>
119
+ <text fg={theme.textMuted}>{NERD.settings}</text>
120
+ </box>
142
121
  <text>
143
122
  <span fg={theme.accent}>cueloop</span>
144
123
  <span fg={theme.textDim}> · resume</span>
@@ -146,11 +125,21 @@ export function InboxScreen(props: {
146
125
  </box>
147
126
  <box style={{ flexGrow: 1 }} />
148
127
  </box>
149
- <InboxList
150
- inbox={inbox}
151
- cursor={inboxCursor}
152
- onRequestDelete={(id, title) => setMode({ type: "confirmDelete", sessionId: id, title })}
153
- />
128
+ <box style={{ flexGrow: 1, flexDirection: "row" }}>
129
+ <ThreadsSidebar
130
+ open
131
+ rows={rows}
132
+ cursor={inboxCursor}
133
+ onSelect={(id) => controller.open(id)}
134
+ onRequestDelete={(id, title) =>
135
+ setMode({ type: "confirmDelete", sessionId: id, title })
136
+ }
137
+ theme={theme}
138
+ />
139
+ <box style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }}>
140
+ <text fg={theme.textDim}>Select a thread</text>
141
+ </box>
142
+ </box>
154
143
  {menuChrome}
155
144
  <ConfirmDialog
156
145
  isOpen={confirming !== null}
@@ -112,17 +112,15 @@ export function buildRenderFlags(params: {
112
112
  isDiff: boolean;
113
113
  isPrototype: boolean;
114
114
  resolved: boolean;
115
- menuOpen: boolean;
116
115
  menuDialog: "keybinds" | "settings" | null;
117
116
  resolvedIds: Set<string>;
118
117
  }) {
119
- const { session, isOwner, isDiff, isPrototype, resolved, menuOpen, menuDialog, resolvedIds } =
120
- params;
118
+ const { session, isOwner, isDiff, isPrototype, resolved, menuDialog, resolvedIds } = params;
121
119
 
122
120
  return {
123
121
  showOwnerActions: isOwner && !isDiff && !resolved,
124
122
  prototypeCanComment: isOwner && !resolved,
125
- chromeHidden: menuOpen || menuDialog !== null,
123
+ chromeHidden: menuDialog !== null,
126
124
  prototypePath: session.artifact.meta.prototypePath ?? "",
127
125
  railResolvedIds: isDiff || isPrototype ? null : resolvedIds,
128
126
  };
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { AnnotationCard } from "./AnnotationCard";
5
5
 
6
- export const meta: StoryMeta = { title: "AnnotationCard" };
6
+ export const meta: StoryMeta = { title: "Cards/AnnotationCard" };
7
7
 
8
8
  const draftCallbacks = { onInput: () => {}, onSave: () => {}, onCancel: () => {} };
9
9
 
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { AppShell } from "./AppShell";
5
+
6
+ export const meta: StoryMeta = { title: "Layout/AppShell" };
7
+
8
+ function Sidebar(): React.ReactNode {
9
+ return (
10
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
11
+ <text fg={DARK.textDim}>Projects</text>
12
+ <text fg={DARK.text}> cueloop</text>
13
+ <box style={{ backgroundColor: DARK.elevated }}>
14
+ <text fg={DARK.accent}> Read Cueloop Repository</text>
15
+ </box>
16
+ <text> </text>
17
+ <text fg={DARK.textDim}>Threads</text>
18
+ <text fg={DARK.textMuted}> Welcome to cueloop</text>
19
+ </box>
20
+ );
21
+ }
22
+
23
+ function Main(): React.ReactNode {
24
+ return (
25
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
26
+ <text fg={DARK.text}>read the repo</text>
27
+ <text> </text>
28
+ <text fg={DARK.textMuted}>Repository context loaded. main is clean.</text>
29
+ <text> </text>
30
+ <text fg={DARK.text}>Core architecture</text>
31
+ <text fg={DARK.textMuted}> - packages/schema is the dependency root</text>
32
+ <text fg={DARK.textMuted}> - packages/daemon owns session state</text>
33
+ </box>
34
+ );
35
+ }
36
+
37
+ function Inspector(): React.ReactNode {
38
+ return (
39
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
40
+ <text fg={DARK.text}>cueloop/main</text>
41
+ <text fg={DARK.textDim}> No changes</text>
42
+ </box>
43
+ );
44
+ }
45
+
46
+ function Header(): React.ReactNode {
47
+ return (
48
+ <box style={{ flexDirection: "row", paddingLeft: 1 }}>
49
+ <text fg={DARK.textMuted}>Read Cueloop Repository</text>
50
+ <box style={{ flexGrow: 1 }} />
51
+ <text fg={DARK.textDim}>markdown.test.ts</text>
52
+ </box>
53
+ );
54
+ }
55
+
56
+ function Footer(): React.ReactNode {
57
+ return (
58
+ <box style={{ flexDirection: "row", paddingLeft: 1 }}>
59
+ <text fg={DARK.textDim}>cueloop / main</text>
60
+ <box style={{ flexGrow: 1 }} />
61
+ <text fg={DARK.textDim}>Fable 5</text>
62
+ </box>
63
+ );
64
+ }
65
+
66
+ export const ThreePane: Story = {
67
+ render: () => (
68
+ <AppShell
69
+ sidebar={<Sidebar />}
70
+ main={<Main />}
71
+ mainHeader={<Header />}
72
+ inspector={<Inspector />}
73
+ footer={<Footer />}
74
+ />
75
+ ),
76
+ expectedColors: [DARK.border, DARK.accent],
77
+ size: { width: 120, height: 28 },
78
+ };
79
+
80
+ export const SidebarAndMain: Story = {
81
+ render: () => <AppShell sidebar={<Sidebar />} main={<Main />} />,
82
+ expectedColors: [DARK.border],
83
+ size: { width: 90, height: 20 },
84
+ };
@@ -0,0 +1,127 @@
1
+ // App shell: a grid of pane columns. Each column stacks a header cell over its body; the vertical
2
+ // rules run full height (through the header), and each header cell's bottom rule joins into one
3
+ // horizontal line across the columns. Rules only - no enclosing frames.
4
+
5
+ import React from "react";
6
+ import { DARK, type Theme } from "../theme";
7
+ import type { BorderSides } from "@opentui/core";
8
+
9
+ export interface AppShellProps {
10
+ headerHeight?: number;
11
+ sidebar?: React.ReactNode;
12
+ sidebarHeader?: React.ReactNode;
13
+ sidebarWidth?: number;
14
+ main: React.ReactNode;
15
+ mainHeader?: React.ReactNode;
16
+ inspector?: React.ReactNode;
17
+ inspectorHeader?: React.ReactNode;
18
+ inspectorWidth?: number;
19
+ footer?: React.ReactNode;
20
+ theme?: Theme;
21
+ }
22
+
23
+ interface PaneColumnProps {
24
+ width?: number;
25
+ grow?: boolean;
26
+ side?: BorderSides;
27
+ header: React.ReactNode;
28
+ body: React.ReactNode;
29
+ headerHeight: number;
30
+ theme: Theme;
31
+ }
32
+
33
+ function PaneColumn({
34
+ width,
35
+ grow,
36
+ side,
37
+ header,
38
+ body,
39
+ headerHeight,
40
+ theme,
41
+ }: PaneColumnProps): React.ReactNode {
42
+ return (
43
+ <box
44
+ style={{
45
+ flexDirection: "column",
46
+ ...(grow ? { flexGrow: 1 } : { width }),
47
+ borderStyle: "single",
48
+ border: side ? [side] : [],
49
+ borderColor: theme.border,
50
+ }}
51
+ >
52
+ <box
53
+ style={{
54
+ height: headerHeight,
55
+ borderStyle: "single",
56
+ border: ["bottom"],
57
+ borderColor: theme.border,
58
+ }}
59
+ >
60
+ {header}
61
+ </box>
62
+ <box style={{ flexGrow: 1 }}>{body}</box>
63
+ </box>
64
+ );
65
+ }
66
+
67
+ export function AppShell({
68
+ headerHeight = 3,
69
+ sidebar,
70
+ sidebarHeader,
71
+ sidebarWidth = 28,
72
+ main,
73
+ mainHeader,
74
+ inspector,
75
+ inspectorHeader,
76
+ inspectorWidth = 40,
77
+ footer,
78
+ theme,
79
+ }: AppShellProps): React.ReactNode {
80
+ const tokens = theme ?? DARK;
81
+
82
+ return (
83
+ <box
84
+ style={{
85
+ flexDirection: "column",
86
+ width: "100%",
87
+ height: "100%",
88
+ backgroundColor: tokens.background,
89
+ }}
90
+ >
91
+ <box style={{ flexDirection: "row", flexGrow: 1 }}>
92
+ {sidebar !== undefined ? (
93
+ <PaneColumn
94
+ width={sidebarWidth}
95
+ side="right"
96
+ header={sidebarHeader}
97
+ body={sidebar}
98
+ headerHeight={headerHeight}
99
+ theme={tokens}
100
+ />
101
+ ) : null}
102
+ <PaneColumn
103
+ grow
104
+ header={mainHeader}
105
+ body={main}
106
+ headerHeight={headerHeight}
107
+ theme={tokens}
108
+ />
109
+ {inspector !== undefined ? (
110
+ <PaneColumn
111
+ width={inspectorWidth}
112
+ side="left"
113
+ header={inspectorHeader}
114
+ body={inspector}
115
+ headerHeight={headerHeight}
116
+ theme={tokens}
117
+ />
118
+ ) : null}
119
+ </box>
120
+ {footer !== undefined ? (
121
+ <box style={{ borderStyle: "single", border: ["top"], borderColor: tokens.border }}>
122
+ {footer}
123
+ </box>
124
+ ) : null}
125
+ </box>
126
+ );
127
+ }
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { Breadcrumb } from "./Breadcrumb";
5
5
 
6
- export const meta: StoryMeta = { title: "Breadcrumb" };
6
+ export const meta: StoryMeta = { title: "Chrome/Breadcrumb" };
7
7
 
8
8
  export const SessionHeader: Story = {
9
9
  render: () => (
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+ import type { DiffFileContents } from "@cueloop/schema";
3
+ import { DARK } from "../theme";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { ChangesColumn } from "./ChangesColumn";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/ChangesColumn" };
8
+
9
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
10
+ return <box style={{ width: "100%", height: "100%", flexDirection: "row" }}>{children}</box>;
11
+ }
12
+
13
+ const files: DiffFileContents[] = [
14
+ { path: "packages/client/src/App.tsx", oldContents: "a", newContents: "b", status: "modified" },
15
+ {
16
+ path: "packages/client/src/components/ChangesColumn.tsx",
17
+ oldContents: "",
18
+ newContents: "c",
19
+ status: "added",
20
+ },
21
+ { path: "packages/daemon/src/review.ts", oldContents: "d", newContents: "", status: "deleted" },
22
+ { path: "README.md", oldContents: "e", newContents: "f", status: "modified" },
23
+ ];
24
+
25
+ export const Changed: Story = {
26
+ render: () => (
27
+ <Frame>
28
+ <ChangesColumn
29
+ open
30
+ files={files}
31
+ selectedPath="packages/client/src/App.tsx"
32
+ onSelectFile={() => {}}
33
+ theme={DARK}
34
+ />
35
+ </Frame>
36
+ ),
37
+ expectedColors: [DARK.accent, DARK.border],
38
+ size: { width: 60, height: 12 },
39
+ };
@@ -0,0 +1,147 @@
1
+ // The right-hand Changes column: the diff's changed files as a directory tree.
2
+ // A click on a file scrolls the diff to that file. Renders nothing when collapsed
3
+ // so the diff reclaims the width. Mirrors the left Threads column on the far side.
4
+ // Co-located here: the state hook that opens it by context and the header toggle.
5
+
6
+ import React, { useMemo, useState } from "react";
7
+ import type { DiffFileContents, ReviewSession } from "@cueloop/schema";
8
+ import type { Theme } from "../theme";
9
+ import type { DiffRow } from "../view-diff";
10
+ import { Tree } from "./primitives/Tree";
11
+ import { allFolderIds } from "./primitives/tree-model";
12
+ import { IconButton } from "./primitives/IconButton";
13
+ import { NERD } from "./primitives/icons";
14
+ import { buildFileTree } from "./file-tree";
15
+
16
+ export interface DiffColumnsState {
17
+ changesOpen: boolean;
18
+ toggleChanges: () => void;
19
+ /** The path under the diff cursor, highlighted in the Changes tree. */
20
+ currentFilePath?: string;
21
+ scrollToFile: (path: string) => void;
22
+ }
23
+
24
+ /**
25
+ * Owns the right Changes column for a diff: context opens it when you enter a
26
+ * diff thread, and a file click scrolls the sheet to that file's first row.
27
+ * Resets on session change via the guarded render-time pattern, no effect.
28
+ */
29
+ export function useDiffColumns(params: {
30
+ session: ReviewSession | null;
31
+ rows: DiffRow[];
32
+ cursor: number;
33
+ setCursor: (index: number) => void;
34
+ }): DiffColumnsState {
35
+ const { session, rows, cursor, setCursor } = params;
36
+ const contextDefault = session?.artifact.type === "diff";
37
+ const [changesOpen, setChangesOpen] = useState(contextDefault);
38
+ const [seenSessionId, setSeenSessionId] = useState(session?.id);
39
+
40
+ // entering another thread lets context decide again; within a thread the manual toggle stays
41
+ if (session?.id !== seenSessionId) {
42
+ setSeenSessionId(session?.id);
43
+ setChangesOpen(contextDefault);
44
+ }
45
+
46
+ return {
47
+ changesOpen,
48
+ toggleChanges: () => setChangesOpen((open) => !open),
49
+ currentFilePath: rows[cursor]?.file,
50
+ scrollToFile: (path: string) => {
51
+ const fileRowIndex = rows.findIndex((row) => row.kind === "file" && row.file === path);
52
+
53
+ if (fileRowIndex >= 0) setCursor(fileRowIndex);
54
+ },
55
+ };
56
+ }
57
+
58
+ /** The header toggle for the Changes column; shown only for a diff, where files exist to browse. */
59
+ export function DiffChangesToggle({
60
+ isDiff,
61
+ open,
62
+ onToggle,
63
+ theme,
64
+ }: {
65
+ isDiff: boolean;
66
+ open: boolean;
67
+ onToggle: () => void;
68
+ theme?: Theme;
69
+ }): React.ReactNode {
70
+ if (!isDiff) return null;
71
+
72
+ return (
73
+ <IconButton
74
+ glyph={open ? NERD.sidebarRight : NERD.sidebarRightOff}
75
+ onPress={onToggle}
76
+ marginLeft={2}
77
+ theme={theme}
78
+ />
79
+ );
80
+ }
81
+
82
+ export interface ChangesColumnProps {
83
+ open: boolean;
84
+ files?: readonly DiffFileContents[];
85
+ /** The path whose diff the sheet is scrolled to, highlighted in the tree. */
86
+ selectedPath?: string;
87
+ onSelectFile: (path: string) => void;
88
+ width?: number;
89
+ theme?: Theme;
90
+ }
91
+
92
+ export function ChangesColumn({
93
+ open,
94
+ files = [],
95
+ selectedPath,
96
+ onSelectFile,
97
+ width = 32,
98
+ theme,
99
+ }: ChangesColumnProps): React.ReactNode {
100
+ const nodes = useMemo(() => buildFileTree(files), [files]);
101
+ // every folder opens by default so each changed file is reachable; the user
102
+ // only ever names the folders they fold shut, so a new file set stays open
103
+ const [collapsedIds, setCollapsedIds] = useState<ReadonlySet<string>>(new Set());
104
+ const expandedIds = useMemo(() => {
105
+ const expanded = allFolderIds(nodes);
106
+
107
+ for (const id of collapsedIds) expanded.delete(id);
108
+
109
+ return expanded;
110
+ }, [nodes, collapsedIds]);
111
+
112
+ if (!open) return null;
113
+
114
+ return (
115
+ <box
116
+ style={{
117
+ width,
118
+ flexDirection: "column",
119
+ borderStyle: "single",
120
+ border: ["left"],
121
+ borderColor: theme?.border,
122
+ }}
123
+ >
124
+ <scrollbox style={{ flexGrow: 1 }} focused={false}>
125
+ <Tree
126
+ nodes={nodes}
127
+ expandedIds={expandedIds}
128
+ selectedId={selectedPath}
129
+ flattenEmptyDirectories
130
+ showStatus
131
+ onSelect={onSelectFile}
132
+ onToggle={(id) =>
133
+ setCollapsedIds((current) => {
134
+ const next = new Set(current);
135
+
136
+ if (next.has(id)) next.delete(id);
137
+ else next.add(id);
138
+
139
+ return next;
140
+ })
141
+ }
142
+ theme={theme}
143
+ />
144
+ </scrollbox>
145
+ </box>
146
+ );
147
+ }
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { CodeBlock } from "./CodeBlock";
5
5
 
6
- export const meta: StoryMeta = { title: "CodeBlock" };
6
+ export const meta: StoryMeta = { title: "Primitives/CodeBlock" };
7
7
 
8
8
  const SNIPPET = `export function gate(full: number) {
9
9
  // threshold
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { CompletionOverlay } from "./CompletionOverlay";
5
5
 
6
- export const meta: StoryMeta = { title: "CompletionOverlay" };
6
+ export const meta: StoryMeta = { title: "Overlays/CompletionOverlay" };
7
7
 
8
8
  export const ApprovedPrompt: Story = {
9
9
  render: () => <CompletionOverlay verdict="approve" completion={{ phase: "prompt" }} status="" />,
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { ConfirmCard } from "./ConfirmCard";
5
5
 
6
- export const meta: StoryMeta = { title: "ConfirmCard" };
6
+ export const meta: StoryMeta = { title: "Cards/ConfirmCard" };
7
7
 
8
8
  const callbacks = {
9
9
  onInput: () => {},
@@ -98,11 +98,10 @@ export function ConfirmCard({
98
98
 
99
99
  return (
100
100
  <Card
101
- title=" submit review "
101
+ title=" send message "
102
102
  contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 2 : 0)}
103
103
  borderColor={tokens.text}
104
- backgroundColor="transparent"
105
- marginRight={1}
104
+ backgroundColor={tokens.elevated}
106
105
  theme={theme}
107
106
  >
108
107
  {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
@@ -118,7 +117,7 @@ export function ConfirmCard({
118
117
  <box style={{ height: 1 }} />
119
118
  <Toolbar>
120
119
  <Button variant="solid" marginRight={2} onPress={onSubmit} theme={theme}>
121
- {" Submit "}
120
+ {" send message "}
122
121
  </Button>
123
122
  <Button onPress={onCancel} theme={theme}>
124
123
  {" Cancel "}
@@ -3,7 +3,7 @@ import { DARK } from "../theme";
3
3
  import type { Story, StoryMeta } from "./story";
4
4
  import { ConfirmDialog } from "./ConfirmDialog";
5
5
 
6
- export const meta: StoryMeta = { title: "ConfirmDialog" };
6
+ export const meta: StoryMeta = { title: "Overlays/ConfirmDialog" };
7
7
 
8
8
  const callbacks = { onConfirm: () => {}, onCancel: () => {} };
9
9
 
@@ -4,7 +4,7 @@ import type { Story, StoryMeta } from "./story";
4
4
  import { DiffSheet } from "./DiffSheet";
5
5
  import { FIXTURE_PATCH, fixtureDiffRows } from "./story-fixtures";
6
6
 
7
- export const meta: StoryMeta = { title: "DiffSheet" };
7
+ export const meta: StoryMeta = { title: "Surfaces/DiffSheet" };
8
8
 
9
9
  export const SignedRows: Story = {
10
10
  render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,