@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,290 @@
1
+ // The one app shell: a single header row over four full-height panes - Threads, Thread, Changes,
2
+ // Project - divided by straight rules; each header cell's thin bottom rule shares the divider gray so
3
+ // the whole header underline reads as one line joining the side rules.
4
+ // The right region (Changes + Project) toggles as a unit: the Project pane is the right sidebar and is
5
+ // always present when the region is on, Changes rides on top of it, and a thin rail holds the sidebar
6
+ // toggle when the region is closed. Each pane owns its own header controls; the thread header never does.
7
+
8
+ import React from "react";
9
+ import { useTerminalDimensions } from "@opentui/react";
10
+ import { DARK, type Theme } from "../theme";
11
+ import { PanelColumn } from "./PanelColumn";
12
+ import { IconButton } from "./primitives/IconButton";
13
+ import { NERD, HEADER_UNDERLINE_CHARS } from "./primitives/icons";
14
+ import { TooltipProvider } from "./Tooltip";
15
+ import { RootOverlayProvider } from "./RootOverlay";
16
+
17
+ export type ProjectPanelMode = "changes" | "tree";
18
+
19
+ export interface AppShellProps {
20
+ sidebarOpen: boolean;
21
+ onToggleSidebar: () => void;
22
+ onOpenMenu: () => void;
23
+ threadsPanel: React.ReactNode;
24
+ /** The Thread header title; mirrors the selected sidebar thread, blank on a bare launch. */
25
+ threadTitle: string;
26
+ /** Owner actions at the right edge of the Thread header (Edit/Share) - the only thread-header controls. */
27
+ threadActions?: React.ReactNode;
28
+ threadPanel: React.ReactNode;
29
+ changesOpen: boolean;
30
+ projectOpen: boolean;
31
+ /** Toggle the Changes editor (also switches the Project tree to changed-files mode). */
32
+ onToggleChanges: () => void;
33
+ /** Toggle the Project tree to the full project view. */
34
+ onToggleProject: () => void;
35
+ /** Open or close the whole right region (the right sidebar). */
36
+ onToggleRight: () => void;
37
+ projectMode: ProjectPanelMode;
38
+ /** The Changes editor grid; rendered only when changesOpen. */
39
+ changesPanel?: React.ReactNode;
40
+ /** The thread footer, shown beneath the Changes grid while the Thread pane is zoomed away. */
41
+ changesFooter?: React.ReactNode;
42
+ projectPanel: React.ReactNode;
43
+ /** Hide the Thread pane so Changes fills the middle (zoom); the sidebars stay. */
44
+ zoomHideThread?: boolean;
45
+ footer?: React.ReactNode;
46
+ children?: React.ReactNode;
47
+ theme?: Theme;
48
+ threadsWidth?: number;
49
+ projectWidth?: number;
50
+ }
51
+
52
+ /** The right region's reserved width: the open project pane, nothing when the region is closed, else the collapsed rail. */
53
+ function rightRegionColumns(
54
+ projectOpen: boolean,
55
+ projectWidth: number,
56
+ rightRegionClosed: boolean,
57
+ collapsedRailWidth: number,
58
+ ): number {
59
+ if (projectOpen) return projectWidth;
60
+
61
+ return rightRegionClosed ? 0 : collapsedRailWidth;
62
+ }
63
+
64
+ /** The Thread header's right cluster: the owner actions, plus the reopen control when the region is closed. */
65
+ function threadHeaderRight(
66
+ threadActions: React.ReactNode,
67
+ reopenControl: React.ReactNode,
68
+ showReopen: boolean,
69
+ ): React.ReactNode {
70
+ if (!showReopen) return threadActions;
71
+
72
+ return (
73
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
74
+ {threadActions ? <box style={{ paddingRight: 2 }}>{threadActions}</box> : null}
75
+ {reopenControl}
76
+ </box>
77
+ );
78
+ }
79
+
80
+ export function AppShell({
81
+ sidebarOpen,
82
+ onToggleSidebar,
83
+ onOpenMenu,
84
+ threadsPanel,
85
+ threadTitle,
86
+ threadActions,
87
+ threadPanel,
88
+ changesOpen,
89
+ projectOpen,
90
+ onToggleChanges,
91
+ onToggleProject,
92
+ onToggleRight,
93
+ projectMode,
94
+ changesPanel,
95
+ changesFooter,
96
+ projectPanel,
97
+ zoomHideThread,
98
+ footer,
99
+ children,
100
+ theme,
101
+ threadsWidth = 30,
102
+ projectWidth = 32,
103
+ }: AppShellProps): React.ReactNode {
104
+ const tokens = theme ?? DARK;
105
+ const { width: terminalWidth } = useTerminalDimensions();
106
+ // with both panels closed the right region collapses to nothing: the reopen toggle
107
+ // rides the thread header, and the middle reclaims the width the rail would have taken
108
+ const rightRegionClosed = !projectOpen && !changesOpen;
109
+ // Thread and Changes would otherwise split the middle as two flexBasis-0 items, and Yoga adds the
110
+ // Changes left border on top of its share: at even widths the halves come out fractional, the
111
+ // Changes side rounds up, and the row overflows a cell under the Project border. Sizing the Thread
112
+ // pane to a whole number leaves Changes the lone flex item, which lays out exactly.
113
+ const collapsedRailWidth = 4;
114
+ const rightRegionWidth = rightRegionColumns(
115
+ projectOpen,
116
+ projectWidth,
117
+ rightRegionClosed,
118
+ collapsedRailWidth,
119
+ );
120
+ const middleWidth = terminalWidth - (sidebarOpen ? threadsWidth : 0) - rightRegionWidth;
121
+ const threadPaneWidth =
122
+ changesOpen && !zoomHideThread ? Math.max(20, Math.floor(middleWidth / 2)) : undefined;
123
+
124
+ // gear + sidebar toggle + product mark: global chrome, in the Threads header when open, else at the
125
+ // left of the Thread header when the Threads pane is collapsed
126
+ const brandChrome = (
127
+ <box style={{ flexDirection: "row", flexShrink: 0 }}>
128
+ <box onMouseUp={onOpenMenu} style={{ paddingRight: 2 }}>
129
+ <text fg={tokens.textMuted}>{NERD.settings}</text>
130
+ </box>
131
+ <IconButton
132
+ glyph={sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff}
133
+ onPress={onToggleSidebar}
134
+ tip="Toggle Threads"
135
+ marginRight={2}
136
+ theme={tokens}
137
+ />
138
+ <text fg={tokens.accent}>cueloop</text>
139
+ </box>
140
+ );
141
+
142
+ const projectToggles = (
143
+ <box style={{ flexDirection: "row", flexShrink: 0, alignItems: "center" }}>
144
+ <IconButton
145
+ glyph="changes"
146
+ active={projectMode === "changes"}
147
+ onPress={onToggleChanges}
148
+ marginRight={2}
149
+ theme={tokens}
150
+ />
151
+ <IconButton
152
+ glyph="project"
153
+ active={projectMode === "tree"}
154
+ onPress={onToggleProject}
155
+ marginRight={2}
156
+ theme={tokens}
157
+ />
158
+ <IconButton
159
+ glyph={NERD.sidebarRight}
160
+ onPress={onToggleRight}
161
+ tip="Toggle Right Sidebar"
162
+ theme={tokens}
163
+ />
164
+ </box>
165
+ );
166
+
167
+ // the reopen control - a divider tick then the toggle - shared by the collapsed
168
+ // rail and, when the rail is suppressed, the thread header
169
+ const reopenRightControl = (
170
+ <>
171
+ <text fg={tokens.border}>{"│"}</text>
172
+ <IconButton
173
+ glyph={NERD.sidebarRightOff}
174
+ onPress={onToggleRight}
175
+ tip="Toggle Right Sidebar"
176
+ marginLeft={1}
177
+ theme={tokens}
178
+ />
179
+ </>
180
+ );
181
+
182
+ return (
183
+ <box
184
+ style={{
185
+ flexDirection: "column",
186
+ width: "100%",
187
+ height: "100%",
188
+ backgroundColor: tokens.background,
189
+ }}
190
+ >
191
+ <RootOverlayProvider>
192
+ <TooltipProvider theme={tokens}>
193
+ <box style={{ flexGrow: 1, flexDirection: "row" }}>
194
+ {sidebarOpen ? (
195
+ <PanelColumn width={threadsWidth} border="right" header={brandChrome} theme={tokens}>
196
+ {threadsPanel}
197
+ </PanelColumn>
198
+ ) : null}
199
+ {!zoomHideThread ? (
200
+ <PanelColumn
201
+ width={threadPaneWidth}
202
+ header={
203
+ <box style={{ flexDirection: "row" }}>
204
+ {!sidebarOpen ? <box style={{ paddingRight: 2 }}>{brandChrome}</box> : null}
205
+ {threadTitle ? <text fg={tokens.textDim}>{threadTitle}</text> : null}
206
+ </box>
207
+ }
208
+ // with the region closed, the reopen toggle rides the thread header instead of an empty column
209
+ headerRight={threadHeaderRight(
210
+ threadActions,
211
+ reopenRightControl,
212
+ rightRegionClosed,
213
+ )}
214
+ theme={tokens}
215
+ >
216
+ {threadPanel}
217
+ </PanelColumn>
218
+ ) : null}
219
+ {changesOpen ? (
220
+ <box
221
+ style={{
222
+ flexDirection: "column",
223
+ flexGrow: 1,
224
+ flexBasis: 0,
225
+ minWidth: 0,
226
+ borderStyle: "single",
227
+ // zoom drops the Thread pane, whose sidebar rule already divides here, so drop this
228
+ // one then to avoid a double border
229
+ border: zoomHideThread ? [] : ["left"],
230
+ borderColor: tokens.border,
231
+ }}
232
+ >
233
+ <box style={{ flexGrow: 1, minHeight: 0 }}>{changesPanel}</box>
234
+ {zoomHideThread ? changesFooter : null}
235
+ </box>
236
+ ) : null}
237
+ {projectOpen ? (
238
+ <PanelColumn
239
+ width={projectWidth}
240
+ border="left"
241
+ header={null}
242
+ headerRight={projectToggles}
243
+ theme={tokens}
244
+ >
245
+ {projectPanel}
246
+ </PanelColumn>
247
+ ) : rightRegionClosed ? null : (
248
+ // Changes open but Project closed: a divider tick above the continuous header underline holds the reopen toggle;
249
+ // the rule lives in the header only, never running the pane's full height. The extra column
250
+ // of right padding keeps the reopen icon off the terminal's last column, which squeezes it
251
+ <box style={{ flexDirection: "column", width: 4 }}>
252
+ <box
253
+ customBorderChars={HEADER_UNDERLINE_CHARS}
254
+ style={{
255
+ flexDirection: "row",
256
+ alignItems: "center",
257
+ height: 2,
258
+ paddingRight: 1,
259
+ backgroundColor: tokens.panel,
260
+ borderStyle: "single",
261
+ border: ["bottom"],
262
+ borderColor: tokens.border,
263
+ }}
264
+ >
265
+ {reopenRightControl}
266
+ </box>
267
+ </box>
268
+ )}
269
+ </box>
270
+ {footer !== undefined ? (
271
+ <box
272
+ style={{
273
+ flexDirection: "row",
274
+ height: 1,
275
+ paddingLeft: 1,
276
+ paddingRight: 1,
277
+ borderStyle: "single",
278
+ border: ["top"],
279
+ borderColor: tokens.border,
280
+ }}
281
+ >
282
+ {footer}
283
+ </box>
284
+ ) : null}
285
+ {children}
286
+ </TooltipProvider>
287
+ </RootOverlayProvider>
288
+ </box>
289
+ );
290
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Regression: the Changes header controls keep their column when zoom hides the Thread
3
+ * pane. Thread and Changes used to split the middle as two flexBasis-0 items, and Yoga
4
+ * added the Changes left border on top of its share, so at even terminal widths the
5
+ * Changes pane overflowed a cell under the Project border and the zoom icon appeared to
6
+ * jump left on zoom. The shell now sizes the Thread pane to a whole number.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { testRender } from "@opentui/react/test-utils";
11
+ import React from "react";
12
+ import { AppShell } from "./AppShell";
13
+ import { EditorGrid } from "./EditorGrid";
14
+ import { changesTab, makeGroup } from "./editor-grid";
15
+ import { NERD } from "./primitives/icons";
16
+ import { allowEventLoopUpdates } from "../test-support";
17
+
18
+ const grid = makeGroup([changesTab()]);
19
+
20
+ async function zoomIconColumn(
21
+ width: number,
22
+ zoomed: boolean,
23
+ sidebarOpen: boolean,
24
+ ): Promise<number> {
25
+ const setup = await testRender(
26
+ <AppShell
27
+ sidebarOpen={sidebarOpen}
28
+ onToggleSidebar={() => {}}
29
+ onOpenMenu={() => {}}
30
+ threadsPanel={<text>threads</text>}
31
+ threadTitle="thread"
32
+ threadPanel={<text>body</text>}
33
+ changesOpen
34
+ projectOpen
35
+ onToggleChanges={() => {}}
36
+ onToggleProject={() => {}}
37
+ onToggleRight={() => {}}
38
+ projectMode="changes"
39
+ zoomHideThread={zoomed}
40
+ projectPanel={<text>project</text>}
41
+ changesPanel={
42
+ <EditorGrid
43
+ tree={grid}
44
+ focusedGroupId={grid.id}
45
+ onFocusGroup={() => {}}
46
+ onActivateTab={() => {}}
47
+ onCloseTab={() => {}}
48
+ onSplit={() => {}}
49
+ onZoom={() => {}}
50
+ zoomed={zoomed}
51
+ renderTab={() => <text>diff</text>}
52
+ />
53
+ }
54
+ />,
55
+ { width, height: 8 },
56
+ );
57
+
58
+ allowEventLoopUpdates();
59
+ await setup.waitForVisualIdle();
60
+ const column = setup.captureCharFrame().split("\n")[0]!.indexOf(NERD.zoom);
61
+
62
+ setup.renderer.destroy();
63
+
64
+ return column;
65
+ }
66
+
67
+ describe("zoom icon in the shell", () => {
68
+ // even widths are where the fractional split used to overflow; odd widths never did
69
+ for (const width of [160, 161]) {
70
+ for (const sidebarOpen of [false, true]) {
71
+ test(`stays in its column when zoom toggles (width ${width}, sidebar ${sidebarOpen ? "open" : "closed"})`, async () => {
72
+ // Act
73
+ const unzoomed = await zoomIconColumn(width, false, sidebarOpen);
74
+ const zoomed = await zoomIconColumn(width, true, sidebarOpen);
75
+
76
+ // Assert
77
+ expect(unzoomed).toBeGreaterThan(0);
78
+ expect(zoomed).toBe(unzoomed);
79
+ });
80
+ }
81
+ }
82
+
83
+ test("the footer rides the Changes pane only while the Thread pane is zoomed away", async () => {
84
+ async function footerVisible(zoomed: boolean): Promise<boolean> {
85
+ const setup = await testRender(
86
+ <AppShell
87
+ sidebarOpen={false}
88
+ onToggleSidebar={() => {}}
89
+ onOpenMenu={() => {}}
90
+ threadsPanel={<text>threads</text>}
91
+ threadTitle="thread"
92
+ threadPanel={<text>body</text>}
93
+ changesOpen
94
+ projectOpen
95
+ onToggleChanges={() => {}}
96
+ onToggleProject={() => {}}
97
+ onToggleRight={() => {}}
98
+ projectMode="changes"
99
+ zoomHideThread={zoomed}
100
+ projectPanel={<text>project</text>}
101
+ changesFooter={<text>send message</text>}
102
+ changesPanel={<text>diff</text>}
103
+ />,
104
+ { width: 160, height: 8 },
105
+ );
106
+
107
+ allowEventLoopUpdates();
108
+ await setup.waitForVisualIdle();
109
+ const shown = setup.captureCharFrame().includes("send message");
110
+
111
+ setup.renderer.destroy();
112
+
113
+ return shown;
114
+ }
115
+
116
+ // Act
117
+ const [zoomed, unzoomed] = await Promise.all([footerVisible(true), footerVisible(false)]);
118
+
119
+ // Assert - the Thread pane carries the footer normally, so it must not double up in Changes
120
+ expect(zoomed).toBe(true);
121
+ expect(unzoomed).toBe(false);
122
+ });
123
+
124
+ test("the icon clears the Project border by its padding in both states", async () => {
125
+ // Act
126
+ const setupColumns = await Promise.all([
127
+ zoomIconColumn(160, false, false),
128
+ zoomIconColumn(160, true, false),
129
+ ]);
130
+
131
+ // Assert - width 160, project pane 32: the border sits at 128 and the glyph one padding cell
132
+ // before it, the same inset the right-sidebar icon keeps from its own edge
133
+ for (const column of setupColumns) expect(128 - column).toBe(2);
134
+ });
135
+ });
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { BareWorkbenchFileView, type WorkbenchCommenter } from "./BareWorkbenchFileView";
5
+
6
+ export const meta: StoryMeta = { title: "Surfaces/BareWorkbenchFileView" };
7
+
8
+ const SAMPLE = `export function add(a: number, b: number): number {\n return a + b;\n}\n`;
9
+
10
+ const controller: WorkbenchCommenter = {
11
+ repoReadFile: () => Promise.resolve(SAMPLE),
12
+ commentOnWorkbench: () => Promise.resolve(),
13
+ };
14
+
15
+ export const Loaded: Story = {
16
+ render: () => (
17
+ <BareWorkbenchFileView
18
+ path="src/add.ts"
19
+ controller={controller}
20
+ quickActions={[]}
21
+ onExit={() => {}}
22
+ theme={DARK}
23
+ />
24
+ ),
25
+ expectedColors: [DARK.textDim],
26
+ size: { width: 60, height: 12 },
27
+ };
@@ -0,0 +1,64 @@
1
+ // A project file in the bare launch shell: annotatable like any file, but no thread exists yet. The
2
+ // first comment find-or-creates the per-repo workbench thread (controller.commentOnWorkbench), which
3
+ // opens it - so this ephemeral view lives only until that first note promotes it to a real thread.
4
+
5
+ import React from "react";
6
+ import { SCHEMA_VERSION, type Anchor, type AnnotationTarget, type Thread } from "@cueloop/schema";
7
+ import type { Theme } from "../theme";
8
+ import type { QuickAction } from "../config";
9
+ import { AnnotatableFileView } from "./AnnotatableFileView";
10
+
11
+ /** The controller slice this view needs: read a file, and route a comment to the workbench thread. */
12
+ export interface WorkbenchCommenter {
13
+ repoReadFile(path: string): Promise<string | null>;
14
+ commentOnWorkbench(anchor: Anchor, target: AnnotationTarget, body: string): Promise<void>;
15
+ }
16
+
17
+ /** An empty, unsaved thread: it holds no notes, just enough shape for the annotation surface. */
18
+ function draftThread(): Thread {
19
+ return {
20
+ schemaVersion: SCHEMA_VERSION,
21
+ id: "workbench-draft",
22
+ workspace: { repoRoot: "", branch: "main" },
23
+ artifact: { type: "diff", content: "", meta: {} },
24
+ revisions: [],
25
+ annotations: [],
26
+ verdict: null,
27
+ status: "pending",
28
+ createdAt: "1970-01-01T00:00:00.000Z",
29
+ };
30
+ }
31
+
32
+ export interface BareWorkbenchFileViewProps {
33
+ path: string;
34
+ controller: WorkbenchCommenter;
35
+ quickActions: QuickAction[];
36
+ /** Reports whether the composer is open, so the shell suspends its inbox keys while typing. */
37
+ onComposingChange?: (composing: boolean) => void;
38
+ onExit: () => void;
39
+ theme?: Theme;
40
+ }
41
+
42
+ export function BareWorkbenchFileView(props: BareWorkbenchFileViewProps): React.ReactNode {
43
+ return (
44
+ <AnnotatableFileView
45
+ path={props.path}
46
+ loadContents={(path) => props.controller.repoReadFile(path)}
47
+ session={draftThread()}
48
+ quickActions={props.quickActions}
49
+ observer={false}
50
+ onComposingChange={props.onComposingChange}
51
+ onAddComment={(anchor: Anchor, body: string) =>
52
+ void props.controller.commentOnWorkbench(
53
+ anchor,
54
+ { kind: "file", path: props.path, rev: "worktree" },
55
+ body,
56
+ )
57
+ }
58
+ onReply={() => {}}
59
+ onUpdateAnnotation={() => {}}
60
+ onExit={props.onExit}
61
+ theme={props.theme}
62
+ />
63
+ );
64
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { Breadcrumb } from "./Breadcrumb";
5
+
6
+ export const meta: StoryMeta = { title: "Chrome/Breadcrumb" };
7
+
8
+ export const SessionHeader: Story = {
9
+ render: () => (
10
+ <Breadcrumb
11
+ items={[
12
+ { label: "cueloop", tone: "accent" },
13
+ { label: "Migration Plan · rev 2", tone: "dim" },
14
+ { label: "resolved: approve", tone: "green" },
15
+ ]}
16
+ />
17
+ ),
18
+ expectedColors: [DARK.accent, DARK.green],
19
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One-row breadcrumb trail: toned segments joined by " · " on the panel
3
+ * surface. The app header is a breadcrumb (product · artifact · revision)
4
+ * followed by state badges; each item picks a semantic tone.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { Theme } from "../theme";
9
+ import { useComponentTheme } from "./theme-context";
10
+
11
+ export type BreadcrumbTone = "accent" | "dim" | "text" | "green";
12
+
13
+ export interface BreadcrumbItem {
14
+ label: string;
15
+ tone?: BreadcrumbTone;
16
+ }
17
+
18
+ export interface BreadcrumbProps {
19
+ items: BreadcrumbItem[];
20
+ theme?: Theme;
21
+ }
22
+
23
+ export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
24
+ const tokens = useComponentTheme(theme);
25
+ const toneColor = (tone?: BreadcrumbTone): string =>
26
+ tone === "accent"
27
+ ? tokens.accent
28
+ : tone === "green"
29
+ ? tokens.green
30
+ : tone === "text"
31
+ ? tokens.text
32
+ : tokens.textDim;
33
+
34
+ return (
35
+ <box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
36
+ <text fg={tokens.text}>
37
+ {items.map((item, index) => (
38
+ <span key={index} fg={toneColor(item.tone)}>
39
+ {index === 0 ? item.label : ` · ${item.label}`}
40
+ </span>
41
+ ))}
42
+ </text>
43
+ </box>
44
+ );
45
+ }
@@ -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
+ };