@cueloop/client 0.1.0-alpha.64 → 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 (109) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +8 -73
  4. package/src/App.export.test.tsx +2 -2
  5. package/src/App.plan-review.test.tsx +60 -412
  6. package/src/App.prototype.test.tsx +1 -8
  7. package/src/App.readonly.test.tsx +85 -62
  8. package/src/App.rendering.test.tsx +5 -6
  9. package/src/App.submit-confirm.test.tsx +32 -70
  10. package/src/App.test.tsx +136 -66
  11. package/src/App.toast.test.tsx +17 -8
  12. package/src/App.tsx +243 -284
  13. package/src/App.walk.test.tsx +1 -1
  14. package/src/app-screens.tsx +67 -52
  15. package/src/app-view-model.ts +6 -100
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +2 -2
  18. package/src/components/AppShell.stories.tsx +84 -0
  19. package/src/components/AppShell.tsx +127 -0
  20. package/src/components/Breadcrumb.stories.tsx +1 -1
  21. package/src/components/ChangesColumn.stories.tsx +39 -0
  22. package/src/components/ChangesColumn.tsx +147 -0
  23. package/src/components/CodeBlock.stories.tsx +1 -1
  24. package/src/components/CompletionOverlay.stories.tsx +1 -1
  25. package/src/components/ConfirmCard.stories.tsx +1 -1
  26. package/src/components/ConfirmCard.tsx +3 -4
  27. package/src/components/ConfirmDialog.stories.tsx +1 -1
  28. package/src/components/DiffSheet.stories.tsx +1 -1
  29. package/src/components/InboxList.stories.tsx +23 -8
  30. package/src/components/InboxList.tsx +45 -19
  31. package/src/components/KeybindsDialog.stories.tsx +1 -1
  32. package/src/components/KeybindsDialog.tsx +61 -17
  33. package/src/components/MarkerPopover.stories.tsx +1 -1
  34. package/src/components/MenuBar.stories.tsx +3 -12
  35. package/src/components/MenuBar.tsx +25 -56
  36. package/src/components/PromptDialog.stories.tsx +1 -1
  37. package/src/components/PrototypeSheet.stories.tsx +1 -1
  38. package/src/components/ReviewPanel.stories.tsx +15 -1
  39. package/src/components/ReviewRail.stories.tsx +15 -1
  40. package/src/components/ReviewRail.tsx +13 -6
  41. package/src/components/SettingsDialog.stories.tsx +7 -1
  42. package/src/components/SettingsDialog.tsx +112 -66
  43. package/src/components/SettingsRows.stories.tsx +1 -1
  44. package/src/components/ShellHeader.stories.tsx +50 -0
  45. package/src/components/ShellHeader.tsx +137 -0
  46. package/src/components/ThreadFooter.stories.tsx +30 -0
  47. package/src/components/ThreadFooter.tsx +49 -0
  48. package/src/components/ThreadView.selection.test.tsx +812 -0
  49. package/src/components/ThreadView.slash.test.tsx +113 -0
  50. package/src/components/ThreadView.stories.tsx +26 -0
  51. package/src/components/ThreadView.tsx +1708 -0
  52. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  53. package/src/components/ThreadsSidebar.tsx +57 -0
  54. package/src/components/Toast.stories.tsx +1 -1
  55. package/src/components/TreePane.stories.tsx +106 -0
  56. package/src/components/TreePane.tsx +156 -0
  57. package/src/components/WalkWizard.stories.tsx +1 -1
  58. package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
  59. package/src/components/agent-launcher.stories.tsx +1 -1
  60. package/src/components/file-tree.test.ts +45 -0
  61. package/src/components/file-tree.ts +57 -0
  62. package/src/components/pages/Workspace.stories.tsx +181 -0
  63. package/src/components/primitives/Button.stories.tsx +1 -1
  64. package/src/components/primitives/Card.stories.tsx +1 -1
  65. package/src/components/primitives/Dialog.stories.tsx +1 -1
  66. package/src/components/primitives/IconButton.stories.tsx +36 -0
  67. package/src/components/primitives/IconButton.tsx +43 -0
  68. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  69. package/src/components/primitives/Tabs.stories.tsx +1 -1
  70. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  71. package/src/components/primitives/Tree.stories.tsx +124 -0
  72. package/src/components/primitives/Tree.tsx +87 -0
  73. package/src/components/primitives/icons.ts +38 -0
  74. package/src/components/primitives/tree-model.test.ts +99 -0
  75. package/src/components/primitives/tree-model.ts +137 -0
  76. package/src/components/quick-actions-editor.stories.tsx +1 -1
  77. package/src/components/session-tree.test.ts +114 -0
  78. package/src/components/session-tree.ts +94 -0
  79. package/src/components/stories-app.tsx +142 -82
  80. package/src/components/story-tree.test.ts +100 -0
  81. package/src/components/story.ts +55 -0
  82. package/src/components/theme-context.stories.tsx +1 -1
  83. package/src/diff-hunk-curate.ts +4 -104
  84. package/src/index.ts +1 -0
  85. package/src/intent-dispatch.test.ts +136 -3
  86. package/src/intent-dispatch.ts +92 -24
  87. package/src/key-bindings.test.ts +1 -1
  88. package/src/keymap.test.ts +4 -4
  89. package/src/keymap.ts +14 -7
  90. package/src/session-controller.curate.test.ts +89 -2
  91. package/src/session-controller.share.test.ts +195 -55
  92. package/src/session-controller.tree.test.ts +228 -0
  93. package/src/session-controller.ts +331 -72
  94. package/src/share.ts +114 -16
  95. package/src/test-support.test.ts +193 -1
  96. package/src/test-support.ts +64 -0
  97. package/src/theme.ts +4 -4
  98. package/src/thread-chords.test.ts +154 -0
  99. package/src/thread-chords.ts +144 -0
  100. package/src/thread-selection.test.ts +210 -0
  101. package/src/thread-selection.ts +243 -0
  102. package/src/tree-view.test.ts +128 -0
  103. package/src/tree-view.ts +148 -0
  104. package/src/use-settings-dialog.tsx +9 -0
  105. package/src/view-plan.ts +29 -12
  106. package/src/App.review-panel.test.tsx +0 -143
  107. package/src/components/PlanSheet.stories.tsx +0 -113
  108. package/src/components/PlanSheet.tsx +0 -419
  109. package/src/components/plan-sheet-run-style.ts +0 -88
@@ -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
@@ -2,7 +2,7 @@ import React, { type Dispatch, type SetStateAction } from "react";
2
2
  import type { ReviewSession, VerdictKind } from "@cueloop/schema";
3
3
  import { returnPaneFor } from "@cueloop/schema";
4
4
  import type { Theme } from "./theme";
5
- import type { Mode } from "./intent-dispatch";
5
+ import type { Mode, TreeAsk } from "./intent-dispatch";
6
6
  import type { Intent } from "./keymap";
7
7
  import type { ReviewController, ToastState } from "./session-controller";
8
8
  import { noteForFile } from "./walk";
@@ -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}
@@ -195,6 +184,21 @@ export function CompletionScreen(props: {
195
184
  );
196
185
  }
197
186
 
187
+ /** The words each tree prompt uses; enter with an empty summary moves back without one. */
188
+ const TREE_PROMPTS: Record<TreeAsk, { title: string; label: string; placeholder: string }> = {
189
+ branch: { title: " Branch ", label: "Name for the new branch:", placeholder: "alt" },
190
+ label: {
191
+ title: " Checkpoint ",
192
+ label: "Name for this checkpoint:",
193
+ placeholder: "before the rewrite",
194
+ },
195
+ navigate: {
196
+ title: " Move back ",
197
+ label: "Summary of what you leave behind (optional):",
198
+ placeholder: "tried a shorter plan",
199
+ },
200
+ };
201
+
198
202
  export function TrailingOverlays(props: {
199
203
  walking: boolean;
200
204
  walk: { index: number } | null;
@@ -264,6 +268,17 @@ export function TrailingOverlays(props: {
264
268
  theme={theme}
265
269
  />
266
270
  ) : null}
271
+ {mode.type === "treePrompt" ? (
272
+ <PromptDialog
273
+ isOpen
274
+ title={TREE_PROMPTS[mode.ask].title}
275
+ label={TREE_PROMPTS[mode.ask].label}
276
+ value={mode.text}
277
+ placeholder={TREE_PROMPTS[mode.ask].placeholder}
278
+ onInput={(text) => setMode({ ...mode, text })}
279
+ theme={theme}
280
+ />
281
+ ) : null}
267
282
  {toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
268
283
  </>
269
284
  );
@@ -1,14 +1,11 @@
1
1
  import type { Dispatch, MutableRefObject, SetStateAction } from "react";
2
2
  import type { ReviewSession, VerdictKind } from "@cueloop/schema";
3
- import { activeSpanState, reviewerAnnotations, type Mode } from "./intent-dispatch";
4
- import { displayText, type DisplayBlock } from "./view-plan";
3
+ import { reviewerAnnotations, type Mode } from "./intent-dispatch";
5
4
  import type { DiffRow } from "./view-diff";
6
5
  import type { Intent, KeyState } from "./keymap";
7
- import type { Completion, ReviewController } from "./session-controller";
8
- import type { QuickAction } from "./config";
6
+ import type { Completion } from "./session-controller";
9
7
  import type { WalkFile } from "./walk";
10
8
  import { viewedCount } from "./walk";
11
- import type { PlanComposeState, PlanPopoverState } from "./components/PlanSheet";
12
9
  import type { DiffComposeState } from "./components/DiffSheet";
13
10
  import type { ConfirmCardProps } from "./components/ConfirmCard";
14
11
  import type { RailCardEdit } from "./components/ReviewRail";
@@ -44,7 +41,8 @@ export function resolveOverlay(
44
41
  if (mode.type === "compose" || mode.type === "railEdit") return "compose";
45
42
  if (mode.type === "submit") return "submit";
46
43
  if (mode.type === "confirmDelete") return "confirm";
47
- if (mode.type === "rename" || mode.type === "nameSelf") return "prompt";
44
+ if (mode.type === "rename" || mode.type === "nameSelf" || mode.type === "treePrompt")
45
+ return "prompt";
48
46
  if (mode.type === "spanActions") return "spanActions";
49
47
  if (completionPhase === "prompt") return "completion-prompt";
50
48
  if (completionPhase === "counting") return "completion-counting";
@@ -114,65 +112,26 @@ export function buildRenderFlags(params: {
114
112
  isDiff: boolean;
115
113
  isPrototype: boolean;
116
114
  resolved: boolean;
117
- menuOpen: boolean;
118
115
  menuDialog: "keybinds" | "settings" | null;
119
116
  resolvedIds: Set<string>;
120
117
  }) {
121
- const { session, isOwner, isDiff, isPrototype, resolved, menuOpen, menuDialog, resolvedIds } =
122
- params;
118
+ const { session, isOwner, isDiff, isPrototype, resolved, menuDialog, resolvedIds } = params;
123
119
 
124
120
  return {
125
121
  showOwnerActions: isOwner && !isDiff && !resolved,
126
122
  prototypeCanComment: isOwner && !resolved,
127
- chromeHidden: menuOpen || menuDialog !== null,
123
+ chromeHidden: menuDialog !== null,
128
124
  prototypePath: session.artifact.meta.prototypePath ?? "",
129
125
  railResolvedIds: isDiff || isPrototype ? null : resolvedIds,
130
126
  };
131
127
  }
132
128
 
133
- export function buildActiveSpan(
134
- mode: Mode,
135
- isDiff: boolean,
136
- ): { displayIndex: number; start: number; end: number } | null {
137
- const markedSpan = activeSpanState(mode);
138
-
139
- if (markedSpan)
140
- return { displayIndex: markedSpan.displayIndex, start: markedSpan.start, end: markedSpan.end };
141
- if (mode.type === "compose" && !isDiff)
142
- return { displayIndex: mode.displayIndex, start: mode.start, end: mode.end };
143
-
144
- return null;
145
- }
146
-
147
129
  interface DraftHandlerDeps {
148
130
  liveInput: MutableRefObject<string>;
149
131
  setMode: Dispatch<SetStateAction<Mode>>;
150
132
  dispatch: (intent: Intent) => void;
151
133
  }
152
134
 
153
- export function buildComposeState(
154
- deps: DraftHandlerDeps & { mode: Mode; isDiff: boolean; display: DisplayBlock[] },
155
- ): PlanComposeState | null {
156
- const { mode, isDiff, display, liveInput, setMode, dispatch } = deps;
157
-
158
- if (mode.type !== "compose" || isDiff) return null;
159
-
160
- return {
161
- kind: mode.kind,
162
- displayIndex: mode.displayIndex,
163
- quote: displayText(display[mode.displayIndex]!).slice(mode.start, mode.end),
164
- draft: {
165
- text: mode.text,
166
- onInput: (text: string) => {
167
- liveInput.current = text;
168
- setMode({ ...mode, text });
169
- },
170
- onSave: () => dispatch({ type: "saveCompose" }),
171
- onCancel: () => dispatch({ type: "closeOverlay" }),
172
- },
173
- };
174
- }
175
-
176
135
  export function buildDiffComposeState(
177
136
  deps: DraftHandlerDeps & { mode: Mode; isDiff: boolean; rows: DiffRow[] },
178
137
  ): DiffComposeState | null {
@@ -196,59 +155,6 @@ export function buildDiffComposeState(
196
155
  };
197
156
  }
198
157
 
199
- export function buildPopoverState(deps: {
200
- mode: Mode;
201
- isDiff: boolean;
202
- quickActions: QuickAction[];
203
- isOwner: boolean;
204
- observer: boolean;
205
- resolved: boolean;
206
- controller: ReviewController;
207
- dispatch: (intent: Intent) => void;
208
- }): PlanPopoverState | null {
209
- const { mode, isDiff, quickActions, isOwner, observer, resolved, controller, dispatch } = deps;
210
- const markedSpan = activeSpanState(mode);
211
-
212
- if (!markedSpan || isDiff) return null;
213
- const spanMutationBlock = (): string | null =>
214
- observer ? "observer - read-only" : resolved ? "review submitted - read-only" : null;
215
-
216
- return {
217
- displayIndex: markedSpan.displayIndex,
218
- view: mode.type === "spanActions" ? ("actions" as const) : ("toolbar" as const),
219
- actions: quickActions,
220
- actionIndex: mode.type === "spanActions" ? mode.index : 0,
221
- canCut: isOwner,
222
- onComment: () => {
223
- const blocked = spanMutationBlock();
224
-
225
- if (blocked) return controller.setStatus(blocked);
226
- dispatch({ type: "openCompose", kind: "comment", from: "span" });
227
- },
228
- onCut: () => {
229
- const blocked = spanMutationBlock();
230
-
231
- if (blocked) return controller.setStatus(blocked);
232
- if (!isOwner) return;
233
- dispatch({ type: "spanCut" });
234
- },
235
- onOpenActions: () => {
236
- const blocked = spanMutationBlock();
237
-
238
- if (blocked) return controller.setStatus(blocked);
239
- dispatch({ type: "openSpanActions" });
240
- },
241
- onClose: () => dispatch({ type: "closeOverlay" }),
242
- onPickAction: (index: number) => {
243
- const blocked = spanMutationBlock();
244
-
245
- if (blocked) return controller.setStatus(blocked);
246
- dispatch({ type: "pickSpanAction", index });
247
- },
248
- onBack: () => dispatch({ type: "closeSpanActions" }),
249
- };
250
- }
251
-
252
158
  export function buildSubmitConfirmState(
253
159
  deps: DraftHandlerDeps & {
254
160
  mode: Mode;
@@ -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
 
@@ -181,11 +181,11 @@ export function AnnotationCard({
181
181
  );
182
182
 
183
183
  if (draft) {
184
- const verb = "comment on";
184
+ const primitive = "comment on";
185
185
 
186
186
  return (
187
187
  <Card
188
- title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
188
+ title={` ${primitive} "${truncateToSingleLine(quote, 40)}" `}
189
189
  contentRows={editorRowCount + 1}
190
190
  borderColor={kindColor}
191
191
  backgroundColor="transparent"
@@ -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
+ };