@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71

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 (248) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +39 -32
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +76 -50
  10. package/src/App.diff.test.tsx +156 -42
  11. package/src/App.export.test.tsx +58 -40
  12. package/src/App.plan-review.test.tsx +363 -0
  13. package/src/App.prototype-markdown.test.tsx +92 -0
  14. package/src/App.prototype.test.tsx +176 -0
  15. package/src/App.readonly.test.tsx +128 -67
  16. package/src/App.rendering.test.tsx +61 -35
  17. package/src/App.submit-confirm.test.tsx +191 -0
  18. package/src/App.test.tsx +310 -114
  19. package/src/App.toast.test.tsx +89 -0
  20. package/src/App.tsx +1043 -798
  21. package/src/App.walk.test.tsx +298 -0
  22. package/src/App.workbench.test.tsx +495 -0
  23. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  24. package/src/annotation-palette.ts +61 -0
  25. package/src/app-key-state.ts +61 -0
  26. package/src/app-screens.tsx +432 -0
  27. package/src/app-view-model.ts +159 -0
  28. package/src/attribution.test.ts +31 -0
  29. package/src/attribution.ts +42 -0
  30. package/src/clipboard.test.ts +23 -0
  31. package/src/clipboard.ts +33 -0
  32. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  33. package/src/components/AnnotatableFileView.test.tsx +102 -0
  34. package/src/components/AnnotatableFileView.tsx +126 -0
  35. package/src/components/AnnotationCards.stories.tsx +87 -0
  36. package/src/components/AnnotationCards.tsx +303 -0
  37. package/src/components/AppHeader.stories.tsx +50 -0
  38. package/src/components/AppHeader.tsx +83 -0
  39. package/src/components/AppShell.stories.tsx +89 -0
  40. package/src/components/AppShell.tsx +290 -0
  41. package/src/components/AppShell.zoom.test.tsx +135 -0
  42. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  43. package/src/components/BareWorkbenchFileView.tsx +64 -0
  44. package/src/components/Breadcrumb.stories.tsx +19 -0
  45. package/src/components/Breadcrumb.tsx +45 -0
  46. package/src/components/ChangesColumn.stories.tsx +39 -0
  47. package/src/components/ChangesColumn.tsx +171 -0
  48. package/src/components/CodeBlock.stories.tsx +42 -0
  49. package/src/components/CodeBlock.tsx +84 -0
  50. package/src/components/CompletionOverlay.stories.tsx +23 -0
  51. package/src/components/CompletionOverlay.tsx +60 -0
  52. package/src/components/ConfirmCard.stories.tsx +35 -0
  53. package/src/components/ConfirmCard.tsx +128 -0
  54. package/src/components/ConfirmDialog.stories.tsx +21 -0
  55. package/src/components/ConfirmDialog.tsx +66 -0
  56. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  57. package/src/components/DiffContentView.split.test.tsx +140 -0
  58. package/src/components/DiffContentView.stories.tsx +80 -0
  59. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  60. package/src/components/DiffContentView.tsx +859 -0
  61. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  62. package/src/components/DiscussionMarkerRail.tsx +181 -0
  63. package/src/components/EditorGrid.stories.tsx +59 -0
  64. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  65. package/src/components/EditorGrid.tsx +368 -0
  66. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  67. package/src/components/FileContentsView.stories.tsx +16 -0
  68. package/src/components/FileContentsView.test.tsx +29 -0
  69. package/src/components/FileContentsView.tsx +98 -0
  70. package/src/components/KeybindsDialog.stories.tsx +31 -0
  71. package/src/components/KeybindsDialog.tsx +103 -0
  72. package/src/components/MenuBar.stories.tsx +28 -0
  73. package/src/components/MenuBar.tsx +44 -0
  74. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  75. package/src/components/OverlayScrollbar.test.tsx +139 -0
  76. package/src/components/OverlayScrollbar.tsx +129 -0
  77. package/src/components/PaneGrid.stories.tsx +84 -0
  78. package/src/components/PaneGrid.tsx +128 -0
  79. package/src/components/PanelColumn.stories.tsx +25 -0
  80. package/src/components/PanelColumn.tsx +95 -0
  81. package/src/components/ProjectTreeView.stories.tsx +25 -0
  82. package/src/components/ProjectTreeView.test.tsx +32 -0
  83. package/src/components/ProjectTreeView.tsx +83 -0
  84. package/src/components/PromptDialog.stories.tsx +36 -0
  85. package/src/components/PromptDialog.tsx +87 -0
  86. package/src/components/PrototypeContentView.stories.tsx +19 -0
  87. package/src/components/PrototypeContentView.tsx +446 -0
  88. package/src/components/RootOverlay.stories.tsx +44 -0
  89. package/src/components/RootOverlay.tsx +35 -0
  90. package/src/components/ScrollArea.stories.tsx +20 -0
  91. package/src/components/ScrollArea.tsx +44 -0
  92. package/src/components/SettingsDialog.stories.tsx +117 -0
  93. package/src/components/SettingsDialog.tsx +195 -0
  94. package/src/components/SettingsRows.stories.tsx +18 -0
  95. package/src/components/SettingsRows.tsx +112 -0
  96. package/src/components/ShellHeader.stories.tsx +50 -0
  97. package/src/components/ShellHeader.tsx +137 -0
  98. package/src/components/ThreadFooter.stories.tsx +30 -0
  99. package/src/components/ThreadFooter.test.tsx +31 -0
  100. package/src/components/ThreadFooter.tsx +54 -0
  101. package/src/components/ThreadTree.stories.tsx +46 -0
  102. package/src/components/ThreadTree.tsx +219 -0
  103. package/src/components/ThreadView.selection.test.tsx +812 -0
  104. package/src/components/ThreadView.slash.test.tsx +158 -0
  105. package/src/components/ThreadView.stories.tsx +26 -0
  106. package/src/components/ThreadView.tsx +330 -0
  107. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  108. package/src/components/ThreadsSidebar.tsx +71 -0
  109. package/src/components/Toast.stories.tsx +12 -0
  110. package/src/components/Toast.tsx +45 -0
  111. package/src/components/Tooltip.stories.tsx +26 -0
  112. package/src/components/Tooltip.tsx +72 -0
  113. package/src/components/TreePane.stories.tsx +106 -0
  114. package/src/components/TreePane.tsx +157 -0
  115. package/src/components/WalkWizard.stories.tsx +100 -0
  116. package/src/components/WalkWizard.tsx +152 -0
  117. package/src/components/WelcomePlayground.stories.tsx +12 -0
  118. package/src/components/WelcomePlayground.test.tsx +47 -0
  119. package/src/components/WelcomePlayground.tsx +140 -0
  120. package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
  121. package/src/components/agent-launcher.stories.tsx +26 -0
  122. package/src/components/agent-launcher.test.tsx +50 -0
  123. package/src/components/agent-launcher.tsx +149 -0
  124. package/src/components/diff-content-view-layout.test.ts +198 -0
  125. package/src/components/diff-content-view-layout.ts +84 -0
  126. package/src/components/editor-grid.test.ts +86 -0
  127. package/src/components/editor-grid.ts +138 -0
  128. package/src/components/fade-title.test.ts +49 -0
  129. package/src/components/fade-title.ts +61 -0
  130. package/src/components/file-tree.test.ts +60 -0
  131. package/src/components/file-tree.ts +112 -0
  132. package/src/components/pages/Workspace.stories.tsx +181 -0
  133. package/src/components/primitives/Button.stories.tsx +32 -0
  134. package/src/components/primitives/Button.tsx +52 -0
  135. package/src/components/primitives/Card.stories.tsx +25 -0
  136. package/src/components/primitives/Card.tsx +60 -0
  137. package/src/components/primitives/Dialog.stories.tsx +24 -0
  138. package/src/components/primitives/Dialog.tsx +66 -0
  139. package/src/components/primitives/IconButton.stories.tsx +36 -0
  140. package/src/components/primitives/IconButton.tsx +62 -0
  141. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  142. package/src/components/primitives/StatusBar.tsx +23 -0
  143. package/src/components/primitives/Tabs.stories.tsx +29 -0
  144. package/src/components/primitives/Tabs.tsx +102 -0
  145. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  146. package/src/components/primitives/Toolbar.tsx +14 -0
  147. package/src/components/primitives/Tree.stories.tsx +124 -0
  148. package/src/components/primitives/Tree.tsx +96 -0
  149. package/src/components/primitives/frame.test.ts +12 -0
  150. package/src/components/primitives/frame.ts +14 -0
  151. package/src/components/primitives/icons.ts +61 -0
  152. package/src/components/primitives/tree-model.test.ts +99 -0
  153. package/src/components/primitives/tree-model.ts +137 -0
  154. package/src/components/quick-actions-editor.stories.tsx +54 -0
  155. package/src/components/quick-actions-editor.test.tsx +49 -0
  156. package/src/components/quick-actions-editor.tsx +150 -0
  157. package/src/components/session-tree.test.ts +134 -0
  158. package/src/components/session-tree.ts +108 -0
  159. package/src/components/stories-app.tsx +184 -0
  160. package/src/components/stories.test.tsx +66 -0
  161. package/src/components/story-fixtures.ts +102 -0
  162. package/src/components/story-tree.test.ts +100 -0
  163. package/src/components/story.ts +142 -0
  164. package/src/components/syntax-highlight.ts +110 -0
  165. package/src/components/terminal-pane.test.tsx +40 -0
  166. package/src/components/terminal-pane.ts +199 -0
  167. package/src/components/theme-context.stories.tsx +46 -0
  168. package/src/components/theme-context.tsx +37 -0
  169. package/src/components/truncate-text.ts +7 -0
  170. package/src/config.test.ts +461 -31
  171. package/src/config.ts +389 -57
  172. package/src/diff-fold.test.ts +78 -0
  173. package/src/diff-fold.ts +126 -0
  174. package/src/diff-hunk-curate.test.ts +315 -0
  175. package/src/diff-hunk-curate.ts +153 -0
  176. package/src/diff-intraline.test.ts +165 -0
  177. package/src/diff-intraline.ts +146 -0
  178. package/src/diff-syntax.test.ts +112 -0
  179. package/src/diff-syntax.ts +179 -0
  180. package/src/discussions.ts +79 -0
  181. package/src/editor.test.ts +149 -0
  182. package/src/editor.ts +139 -9
  183. package/src/ghostty-terminal.test.ts +85 -0
  184. package/src/ghostty-terminal.ts +199 -0
  185. package/src/herdr.test.ts +9 -34
  186. package/src/herdr.ts +25 -21
  187. package/src/index.ts +11 -0
  188. package/src/integrations.test.ts +41 -0
  189. package/src/integrations.ts +32 -0
  190. package/src/intent-dispatch.test.ts +698 -0
  191. package/src/intent-dispatch.ts +700 -0
  192. package/src/key-bindings.test.ts +156 -0
  193. package/src/key-bindings.ts +465 -0
  194. package/src/keymap.test.ts +611 -0
  195. package/src/keymap.ts +410 -0
  196. package/src/kitty-image.test.ts +66 -0
  197. package/src/kitty-image.ts +138 -0
  198. package/src/mark-runs.ts +120 -0
  199. package/src/prototype-browser.test.ts +56 -0
  200. package/src/prototype-browser.ts +307 -0
  201. package/src/prototype-pixels.tsx +35 -0
  202. package/src/pty.test.ts +81 -0
  203. package/src/pty.ts +245 -0
  204. package/src/ready-signal.test.ts +85 -0
  205. package/src/ready-signal.ts +52 -0
  206. package/src/run.ts +30 -9
  207. package/src/serve.test.ts +24 -12
  208. package/src/serve.ts +20 -9
  209. package/src/share.ts +215 -0
  210. package/src/skills.test.ts +74 -0
  211. package/src/skills.ts +68 -0
  212. package/src/slash-palette.test.ts +54 -0
  213. package/src/slash-palette.ts +104 -0
  214. package/src/split-diff.test.ts +110 -0
  215. package/src/split-diff.ts +149 -0
  216. package/src/test-support.test.ts +264 -0
  217. package/src/test-support.ts +355 -0
  218. package/src/theme-presets.test.ts +51 -0
  219. package/src/theme-presets.ts +214 -0
  220. package/src/theme.ts +84 -19
  221. package/src/thread-chords.test.ts +170 -0
  222. package/src/thread-chords.ts +169 -0
  223. package/src/thread-controller.curate.test.ts +438 -0
  224. package/src/thread-controller.live-diff.test.ts +199 -0
  225. package/src/thread-controller.share.test.ts +472 -0
  226. package/src/thread-controller.tree.test.ts +255 -0
  227. package/src/thread-controller.ts +1509 -0
  228. package/src/thread-selection.test.ts +210 -0
  229. package/src/thread-selection.ts +243 -0
  230. package/src/tree-view.test.ts +128 -0
  231. package/src/tree-view.ts +148 -0
  232. package/src/use-annotation-surface.tsx +851 -0
  233. package/src/use-changes-workbench.ts +153 -0
  234. package/src/use-frame-measure.ts +38 -0
  235. package/src/use-settings-dialog.tsx +274 -0
  236. package/src/use-terminal-virtualizer.ts +233 -0
  237. package/src/version.ts +5 -0
  238. package/src/view-diff.test.ts +66 -9
  239. package/src/view-diff.ts +231 -23
  240. package/src/view-plan.regression.test.ts +60 -0
  241. package/src/view-plan.test.ts +406 -0
  242. package/src/view-plan.ts +570 -0
  243. package/src/walk.test.ts +121 -0
  244. package/src/walk.ts +74 -0
  245. package/src/syntax.test.ts +0 -41
  246. package/src/syntax.ts +0 -88
  247. package/src/view.test.ts +0 -157
  248. package/src/view.ts +0 -312
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The settings overlay: a centered Dialog whose left nav is a tree - a Settings
3
+ * group over its categories, plus Keybinds as a sibling leaf - and whose right
4
+ * body shows the active category's typed rows or the keybinds cheatsheet. The
5
+ * component is controlled: the caller owns the open flag, the active
6
+ * category/row/zone, and value changes, so the app's keyboard grammar (and the
7
+ * stories catalog) drive it without a second key handler inside.
8
+ */
9
+
10
+ import { ScrollArea } from "./ScrollArea";
11
+ import React from "react";
12
+ import { useTerminalDimensions } from "@opentui/react";
13
+ import type { Theme } from "../theme";
14
+ import type { CheatsheetSection } from "../key-bindings";
15
+ import { useComponentTheme } from "./theme-context";
16
+ import { Dialog } from "./primitives/Dialog";
17
+ import { Tree } from "./primitives/Tree";
18
+ import type { TreeNode } from "./primitives/tree-model";
19
+ import { CycleRow, TextRow, ToggleRow, type SettingsRowDescriptor } from "./SettingsRows";
20
+
21
+ export interface SettingsCategory {
22
+ id: string;
23
+ name: string;
24
+ description: string;
25
+ rows: SettingsRowDescriptor[];
26
+ /** A bespoke body (e.g. the quick-actions editor) rendered instead of typed rows. */
27
+ customBody?: React.ReactNode;
28
+ }
29
+
30
+ export type SettingsValues = Record<string, string | boolean>;
31
+
32
+ /** The category id whose right pane shows the keybinds cheatsheet rather than rows. */
33
+ const KEYBINDS_CATEGORY_ID = "keybinds";
34
+
35
+ export interface SettingsDialogProps {
36
+ isOpen: boolean;
37
+ /** The client version, shown in the dialog footer (its home now the menu bar is gone). */
38
+ version: string;
39
+ categories: SettingsCategory[];
40
+ values: SettingsValues;
41
+ /** The keybinds cheatsheet, shown when the Keybinds leaf is active. */
42
+ keybindsSections: CheatsheetSection[];
43
+ activeCategoryId: string;
44
+ activeRowIndex: number;
45
+ activeZone: "nav" | "body";
46
+ onCategorySelect: (categoryId: string) => void;
47
+ onRowActivate: (row: SettingsRowDescriptor) => void;
48
+ theme?: Theme;
49
+ }
50
+
51
+ /** The left nav tree: a Settings group over its categories, then Keybinds as a leaf. */
52
+ function navTree(categories: SettingsCategory[]): TreeNode[] {
53
+ const children = categories
54
+ .filter((category) => category.id !== KEYBINDS_CATEGORY_ID)
55
+ .map((category) => ({ id: category.id, label: category.name }));
56
+ const nodes: TreeNode[] = [{ id: "settings", label: "Settings", children }];
57
+ const keybinds = categories.find((category) => category.id === KEYBINDS_CATEGORY_ID);
58
+
59
+ if (keybinds !== undefined) nodes.push({ id: keybinds.id, label: keybinds.name });
60
+
61
+ return nodes;
62
+ }
63
+
64
+ function KeybindsBody({
65
+ sections,
66
+ tokens,
67
+ }: {
68
+ sections: CheatsheetSection[];
69
+ tokens: Theme;
70
+ }): React.ReactNode {
71
+ return (
72
+ <ScrollArea>
73
+ {sections.map((section) => (
74
+ <box key={section.title} style={{ flexDirection: "column" }}>
75
+ <text fg={tokens.accent}>{section.title}</text>
76
+ {section.entries.map((entry, index) => (
77
+ <text key={`${section.title}-${index}`}>
78
+ <span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
79
+ <span fg={tokens.textMuted}>{entry.label}</span>
80
+ </text>
81
+ ))}
82
+ <text> </text>
83
+ </box>
84
+ ))}
85
+ </ScrollArea>
86
+ );
87
+ }
88
+
89
+ export function SettingsDialog({
90
+ isOpen,
91
+ version,
92
+ categories,
93
+ values,
94
+ keybindsSections,
95
+ activeCategoryId,
96
+ activeRowIndex,
97
+ activeZone,
98
+ onCategorySelect,
99
+ onRowActivate,
100
+ theme,
101
+ }: SettingsDialogProps): React.ReactNode {
102
+ const tokens = useComponentTheme(theme);
103
+ const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
104
+
105
+ if (!isOpen) return null;
106
+ const category =
107
+ categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
108
+
109
+ if (!category) return null;
110
+ const onKeybinds = category.id === KEYBINDS_CATEGORY_ID;
111
+
112
+ return (
113
+ <Dialog
114
+ isOpen
115
+ title=" Settings "
116
+ width={Math.min(76, terminalWidth - 6)}
117
+ height={Math.min(22, terminalHeight - 4)}
118
+ background={tokens.elevated}
119
+ theme={theme}
120
+ >
121
+ <box style={{ flexDirection: "row", flexGrow: 1 }}>
122
+ <box style={{ flexDirection: "column", width: 20, paddingLeft: 1, paddingRight: 1 }}>
123
+ <Tree
124
+ nodes={navTree(categories)}
125
+ expandedIds={new Set(["settings"])}
126
+ selectedId={category.id}
127
+ hideIcons
128
+ onSelect={onCategorySelect}
129
+ onToggle={onCategorySelect}
130
+ theme={theme}
131
+ />
132
+ </box>
133
+ <box style={{ borderStyle: "single", border: ["left"], borderColor: tokens.border }} />
134
+ <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2, paddingRight: 1 }}>
135
+ <text fg={tokens.text}>{category.name}</text>
136
+ <text fg={tokens.textDim}>{category.description}</text>
137
+ <box style={{ height: 1 }} />
138
+ {onKeybinds ? <KeybindsBody sections={keybindsSections} tokens={tokens} /> : null}
139
+ {onKeybinds ? null : category.customBody}
140
+ {onKeybinds
141
+ ? null
142
+ : category.rows.map((row, rowIndex) => {
143
+ const isActive = activeZone === "body" && rowIndex === activeRowIndex;
144
+ const value = values[row.key];
145
+
146
+ if (row.kind === "toggle") {
147
+ return (
148
+ <ToggleRow
149
+ key={row.key}
150
+ label={row.label}
151
+ value={value === true}
152
+ isActive={isActive}
153
+ onPress={() => onRowActivate(row)}
154
+ theme={theme}
155
+ />
156
+ );
157
+ }
158
+ if (row.kind === "cycle") {
159
+ return (
160
+ <CycleRow
161
+ key={row.key}
162
+ label={row.label}
163
+ value={String(value ?? "")}
164
+ isActive={isActive}
165
+ onPress={() => onRowActivate(row)}
166
+ theme={theme}
167
+ />
168
+ );
169
+ }
170
+
171
+ return (
172
+ <TextRow
173
+ key={row.key}
174
+ label={row.label}
175
+ value={String(value ?? "")}
176
+ isActive={isActive}
177
+ onPress={() => onRowActivate(row)}
178
+ theme={theme}
179
+ />
180
+ );
181
+ })}
182
+ </box>
183
+ </box>
184
+ <box style={{ flexDirection: "row", height: 1, paddingLeft: 1, paddingRight: 1 }}>
185
+ <text fg={tokens.textDim}>
186
+ {activeZone === "nav"
187
+ ? "j/k nav · l/tab open · esc close"
188
+ : "j/k row · l/space change · h back · esc close"}
189
+ </text>
190
+ <box style={{ flexGrow: 1 }} />
191
+ <text fg={tokens.textDim}>{`cueloop v${version}`}</text>
192
+ </box>
193
+ </Dialog>
194
+ );
195
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { CycleRow, TextRow, ToggleRow } from "./SettingsRows";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/SettingsRows" };
7
+
8
+ export const AllKinds: Story = {
9
+ render: () => (
10
+ <box style={{ flexDirection: "column", width: 50 }}>
11
+ <ToggleRow label="Line numbers" value={true} isActive={false} onPress={() => {}} />
12
+ <ToggleRow label="Hide whitespace" value={false} isActive={true} onPress={() => {}} />
13
+ <CycleRow label="Palette" value="dark" isActive={false} onPress={() => {}} />
14
+ <TextRow label="Vault path" value="~/vaults/notes" isActive={false} onPress={() => {}} />
15
+ </box>
16
+ ),
17
+ expectedColors: [DARK.green, DARK.accent, DARK.border],
18
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Typed settings rows for the settings dialog: toggle (on/off), cycle
3
+ * (enumerated options), and text (freeform value). Every row is one line -
4
+ * label column left, value right - and presses go through `onPress` so mouse
5
+ * and keyboard activation share one path.
6
+ */
7
+
8
+ import React from "react";
9
+ import type { Theme } from "../theme";
10
+ import { useComponentTheme } from "./theme-context";
11
+
12
+ export interface SettingsRowDescriptor {
13
+ key: string;
14
+ label: string;
15
+ kind: "toggle" | "cycle" | "text";
16
+ options?: string[];
17
+ hint?: string;
18
+ }
19
+
20
+ interface RowShellProps {
21
+ label: string;
22
+ isActive: boolean;
23
+ onPress: () => void;
24
+ theme?: Theme;
25
+ children: React.ReactNode;
26
+ }
27
+
28
+ function RowShell({ label, isActive, onPress, theme, children }: RowShellProps): React.ReactNode {
29
+ const tokens = useComponentTheme(theme);
30
+
31
+ return (
32
+ <box
33
+ style={{
34
+ flexDirection: "row",
35
+ height: 1,
36
+ backgroundColor: isActive ? tokens.border : undefined,
37
+ }}
38
+ onMouseUp={onPress}
39
+ >
40
+ <box style={{ width: 26, paddingLeft: 1 }}>
41
+ <text fg={isActive ? tokens.text : tokens.textMuted}>{label}</text>
42
+ </box>
43
+ {children}
44
+ </box>
45
+ );
46
+ }
47
+
48
+ export interface ToggleRowProps {
49
+ label: string;
50
+ value: boolean;
51
+ isActive: boolean;
52
+ onPress: () => void;
53
+ theme?: Theme;
54
+ }
55
+
56
+ export function ToggleRow({
57
+ label,
58
+ value,
59
+ isActive,
60
+ onPress,
61
+ theme,
62
+ }: ToggleRowProps): React.ReactNode {
63
+ const tokens = useComponentTheme(theme);
64
+
65
+ return (
66
+ <RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
67
+ <text fg={value ? tokens.green : tokens.accent}>{value ? "on" : "off"}</text>
68
+ </RowShell>
69
+ );
70
+ }
71
+
72
+ export interface CycleRowProps {
73
+ label: string;
74
+ value: string;
75
+ isActive: boolean;
76
+ onPress: () => void;
77
+ theme?: Theme;
78
+ }
79
+
80
+ export function CycleRow({
81
+ label,
82
+ value,
83
+ isActive,
84
+ onPress,
85
+ theme,
86
+ }: CycleRowProps): React.ReactNode {
87
+ const tokens = useComponentTheme(theme);
88
+
89
+ return (
90
+ <RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
91
+ <text fg={tokens.accent}>{value}</text>
92
+ </RowShell>
93
+ );
94
+ }
95
+
96
+ export interface TextRowProps {
97
+ label: string;
98
+ value: string;
99
+ isActive: boolean;
100
+ onPress: () => void;
101
+ theme?: Theme;
102
+ }
103
+
104
+ export function TextRow({ label, value, isActive, onPress, theme }: TextRowProps): React.ReactNode {
105
+ const tokens = useComponentTheme(theme);
106
+
107
+ return (
108
+ <RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
109
+ <text fg={tokens.accent}>{value}</text>
110
+ </RowShell>
111
+ );
112
+ }
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ShellHeader } from "./ShellHeader";
5
+ import { NERD } from "./primitives/icons";
6
+
7
+ export const meta: StoryMeta = { title: "Layout/ShellHeader" };
8
+
9
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
10
+ return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
11
+ }
12
+
13
+ export const ReviewSurface: Story = {
14
+ render: () => (
15
+ <Frame>
16
+ <ShellHeader
17
+ leftIcons={[{ glyph: NERD.settings }]}
18
+ leftLabel="Read Cueloop Repository"
19
+ tabs={[{ label: "Changes", active: true }]}
20
+ rightIcons={[
21
+ { glyph: NERD.expand },
22
+ { glyph: NERD.diff, active: true },
23
+ { glyph: NERD.listTree },
24
+ ]}
25
+ onToggleSidebar={() => {}}
26
+ onToggleInspector={() => {}}
27
+ />
28
+ </Frame>
29
+ ),
30
+ expectedColors: [DARK.accent, DARK.textMuted],
31
+ size: { width: 120, height: 3 },
32
+ };
33
+
34
+ export const InspectorCollapsed: Story = {
35
+ render: () => (
36
+ <Frame>
37
+ <ShellHeader
38
+ leftIcons={[{ glyph: NERD.settings }]}
39
+ leftLabel="Read Cueloop Repository"
40
+ rightIcons={[{ glyph: NERD.search }, { glyph: NERD.expand }]}
41
+ onToggleSidebar={() => {}}
42
+ sidebarOpen={false}
43
+ onToggleInspector={() => {}}
44
+ inspectorOpen={false}
45
+ />
46
+ </Frame>
47
+ ),
48
+ expectedColors: [DARK.textMuted],
49
+ size: { width: 120, height: 3 },
50
+ };
@@ -0,0 +1,137 @@
1
+ // The app shell's top bar: left panel toggle + icons + label, an optional title, right icons + panel toggle.
2
+
3
+ import React from "react";
4
+ import { DARK, type Theme } from "../theme";
5
+ import { NERD } from "./primitives/icons";
6
+
7
+ export interface ShellTab {
8
+ label: string;
9
+ active?: boolean;
10
+ }
11
+
12
+ // A header icon; active paints it in the brand accent to mark the current view or mode.
13
+ export interface ShellIcon {
14
+ glyph: string;
15
+ active?: boolean;
16
+ onPress?: () => void;
17
+ }
18
+
19
+ export interface ShellHeaderProps {
20
+ leftIcons?: readonly ShellIcon[];
21
+ leftLabel?: string;
22
+ titleIcon?: string;
23
+ title?: string;
24
+ tabs?: readonly ShellTab[];
25
+ rightIcons?: readonly ShellIcon[];
26
+ sidebarOpen?: boolean;
27
+ onToggleSidebar?: () => void;
28
+ inspectorOpen?: boolean;
29
+ onToggleInspector?: () => void;
30
+ theme?: Theme;
31
+ }
32
+
33
+ function IconView({
34
+ icon,
35
+ tokens,
36
+ marginRight,
37
+ }: {
38
+ icon: ShellIcon;
39
+ tokens: Theme;
40
+ marginRight: number;
41
+ }): React.ReactNode {
42
+ const color = icon.active ? tokens.accent : tokens.textMuted;
43
+
44
+ // alignSelf center keeps the clickable box on the same row as sibling icons
45
+ return (
46
+ <box onMouseUp={icon.onPress} style={{ alignSelf: "center", marginRight }}>
47
+ <text fg={color}>{icon.glyph}</text>
48
+ </box>
49
+ );
50
+ }
51
+
52
+ export function ShellHeader({
53
+ leftIcons = [],
54
+ leftLabel,
55
+ titleIcon,
56
+ title,
57
+ tabs = [],
58
+ rightIcons = [],
59
+ sidebarOpen = true,
60
+ onToggleSidebar,
61
+ inspectorOpen = true,
62
+ onToggleInspector,
63
+ theme,
64
+ }: ShellHeaderProps): React.ReactNode {
65
+ const tokens = theme ?? DARK;
66
+
67
+ return (
68
+ <box
69
+ style={{
70
+ flexDirection: "row",
71
+ height: "100%",
72
+ alignItems: "center",
73
+ paddingLeft: 2,
74
+ paddingRight: 2,
75
+ }}
76
+ >
77
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
78
+ {onToggleSidebar !== undefined ? (
79
+ <IconView
80
+ icon={{
81
+ glyph: sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff,
82
+ onPress: onToggleSidebar,
83
+ }}
84
+ tokens={tokens}
85
+ marginRight={3}
86
+ />
87
+ ) : null}
88
+ {leftIcons.map((icon, index) => (
89
+ <IconView
90
+ key={`left-${index}-${icon.glyph}`}
91
+ icon={icon}
92
+ tokens={tokens}
93
+ marginRight={3}
94
+ />
95
+ ))}
96
+ {leftLabel !== undefined ? (
97
+ <text fg={tokens.textMuted}>
98
+ {leftLabel}
99
+ {" "}
100
+ </text>
101
+ ) : null}
102
+ {titleIcon !== undefined ? <text fg={tokens.textDim}>{titleIcon} </text> : null}
103
+ {title !== undefined ? <text fg={tokens.text}>{title}</text> : null}
104
+ </box>
105
+ <box style={{ flexGrow: 1 }} />
106
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
107
+ {tabs.map((tab, index) => (
108
+ <text
109
+ key={`tab-${index}-${tab.label}`}
110
+ fg={tab.active ? tokens.accent : tokens.textMuted}
111
+ >
112
+ {tab.label}
113
+ {" "}
114
+ </text>
115
+ ))}
116
+ {rightIcons.map((icon, index) => (
117
+ <IconView
118
+ key={`right-${index}-${icon.glyph}`}
119
+ icon={icon}
120
+ tokens={tokens}
121
+ marginRight={3}
122
+ />
123
+ ))}
124
+ {onToggleInspector !== undefined ? (
125
+ <IconView
126
+ icon={{
127
+ glyph: inspectorOpen ? NERD.sidebarRight : NERD.sidebarRightOff,
128
+ onPress: onToggleInspector,
129
+ }}
130
+ tokens={tokens}
131
+ marginRight={0}
132
+ />
133
+ ) : null}
134
+ </box>
135
+ </box>
136
+ );
137
+ }
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ThreadFooter } from "./ThreadFooter";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/ThreadFooter" };
7
+
8
+ function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
9
+ return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
10
+ }
11
+
12
+ export const Default: Story = {
13
+ render: () => (
14
+ <Frame>
15
+ <ThreadFooter repo="cueloop" branch="main" onSubmit={() => {}} />
16
+ </Frame>
17
+ ),
18
+ expectedColors: [DARK.accent, DARK.textMuted],
19
+ size: { width: 80, height: 2 },
20
+ };
21
+
22
+ export const ReadOnly: Story = {
23
+ render: () => (
24
+ <Frame>
25
+ <ThreadFooter repo="cueloop" branch="design/tui" canSubmit={false} onSubmit={() => {}} />
26
+ </Frame>
27
+ ),
28
+ expectedColors: [DARK.textDim],
29
+ size: { width: 80, height: 2 },
30
+ };
@@ -0,0 +1,31 @@
1
+ /** The thread footer keeps its repo/branch context on one line: a long branch truncates to fit rather
2
+ * than wrapping and pushing the send control onto a second row. */
3
+
4
+ import { test, expect } from "bun:test";
5
+ import React from "react";
6
+ import { testRender } from "@opentui/react/test-utils";
7
+ import { ThreadFooter } from "./ThreadFooter";
8
+ import { DARK } from "../theme";
9
+
10
+ test("a long branch truncates on one line and keeps the send control in place", async () => {
11
+ const setup = await testRender(
12
+ <box style={{ width: 60, height: 3, backgroundColor: DARK.background }}>
13
+ <box style={{ flexGrow: 1 }} />
14
+ <ThreadFooter
15
+ repo="cueloop"
16
+ branch="design/tui-tree-and-edgy-really-long-name"
17
+ onSubmit={() => {}}
18
+ theme={DARK}
19
+ />
20
+ </box>,
21
+ { width: 60, height: 3 },
22
+ );
23
+ await setup.renderOnce();
24
+
25
+ const lines = setup.captureCharFrame().split("\n");
26
+ const footerRow = lines.find((line) => line.includes("cueloop"))!;
27
+ // the repo/branch context and the send control share the single footer row
28
+ expect(footerRow).toContain("send message");
29
+ // the branch is clipped, not wrapped: it never reaches its tail on a second row
30
+ expect(setup.captureCharFrame()).not.toContain("long-name");
31
+ });
@@ -0,0 +1,54 @@
1
+ // The thread's bottom band: a header-like bar under the thread column, or the
2
+ // Changes column when a zoom drops the thread column. It carries the repo/branch
3
+ // context on the left and a written, clickable "Send message" control on the right.
4
+
5
+ import React from "react";
6
+ import { DARK, type Theme } from "../theme";
7
+ import { NERD } from "./primitives/icons";
8
+
9
+ export interface ThreadFooterProps {
10
+ repo: string;
11
+ branch: string;
12
+ onSubmit?: () => void;
13
+ /** When false the send control dims and does not fire (e.g. an observer). */
14
+ canSubmit?: boolean;
15
+ theme?: Theme;
16
+ }
17
+
18
+ export function ThreadFooter({
19
+ repo,
20
+ branch,
21
+ onSubmit,
22
+ canSubmit = true,
23
+ theme,
24
+ }: ThreadFooterProps): React.ReactNode {
25
+ const tokens = theme ?? DARK;
26
+
27
+ return (
28
+ <box
29
+ style={{
30
+ flexDirection: "row",
31
+ height: 2,
32
+ alignItems: "center",
33
+ paddingLeft: 1,
34
+ paddingRight: 1,
35
+ backgroundColor: tokens.panel,
36
+ borderStyle: "single",
37
+ border: ["top"],
38
+ borderColor: tokens.border,
39
+ }}
40
+ >
41
+ <box style={{ flexShrink: 1, minWidth: 0, marginRight: 2 }}>
42
+ <text truncate>
43
+ <span fg={tokens.blue}>{`${NERD.folderClosed} `}</span>
44
+ <span fg={tokens.textMuted}>{repo}</span>
45
+ <span fg={tokens.textDim}>{` / ${branch}`}</span>
46
+ </text>
47
+ </box>
48
+ <box style={{ flexGrow: 1 }} />
49
+ <box onMouseUp={canSubmit ? onSubmit : undefined} style={{ flexShrink: 0 }}>
50
+ <text fg={canSubmit ? tokens.accent : tokens.textDim}>send message</text>
51
+ </box>
52
+ </box>
53
+ );
54
+ }
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { ThreadTree } from "./ThreadTree";
5
+ import { groupInbox } from "./session-tree";
6
+ import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
7
+
8
+ export const meta: StoryMeta = { title: "Surfaces/ThreadTree" };
9
+
10
+ /** ThreadTree is the body under App's header/menu chrome; a frame gives it height. */
11
+ function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
12
+ return <box style={{ flexDirection: "column", width: "100%", height: "100%" }}>{children}</box>;
13
+ }
14
+
15
+ export const TwoPending: Story = {
16
+ render: () => {
17
+ const plan = fixturePlanSession();
18
+ // one repo-bound thread (a project) and one standalone thread
19
+ const projectPlan = {
20
+ ...plan,
21
+ workspace: {
22
+ ...plan.workspace,
23
+ rootCommit: "root-1",
24
+ remote: "git@github.com:acme/widget.git",
25
+ },
26
+ };
27
+ const { rows } = groupInbox([projectPlan, fixtureDiffSession()]);
28
+
29
+ return (
30
+ <BodyFrame>
31
+ <ThreadTree rows={rows} cursor={0} />
32
+ </BodyFrame>
33
+ );
34
+ },
35
+ expectedColors: [DARK.accent],
36
+ size: { width: 80, height: 12 },
37
+ };
38
+
39
+ export const Empty: Story = {
40
+ render: () => (
41
+ <BodyFrame>
42
+ <ThreadTree rows={[]} cursor={0} />
43
+ </BodyFrame>
44
+ ),
45
+ size: { width: 80, height: 8 },
46
+ };