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

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 +1039 -799
  21. package/src/App.walk.test.tsx +298 -0
  22. package/src/App.workbench.test.tsx +461 -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 +286 -0
  41. package/src/components/AppShell.zoom.test.tsx +94 -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 +151 -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,432 @@
1
+ import { ScrollArea } from "./components/ScrollArea";
2
+ import React, { useEffect, useState, type Dispatch, type SetStateAction } from "react";
3
+ import type { DiffFileContents, Thread, VerdictKind } from "@cueloop/schema";
4
+ import { returnPaneFor } from "@cueloop/schema";
5
+ import type { Theme } from "./theme";
6
+ import type { QuickAction } from "./config";
7
+ import type { Mode, TreeAsk } from "./intent-dispatch";
8
+ import type { Intent } from "./keymap";
9
+ import type { ReviewController, ToastState } from "./thread-controller";
10
+ import { noteForFile } from "./walk";
11
+ import type { WalkFile } from "./walk";
12
+ import type { CheatsheetSection } from "./key-bindings";
13
+ import type { SettingsCategory, SettingsValues } from "./components/SettingsDialog";
14
+ import type { SettingsNav } from "./use-settings-dialog";
15
+ import { CLIENT_VERSION } from "./version";
16
+ import { ThemeProvider } from "./components/theme-context";
17
+ import type { InboxRow } from "./components/session-tree";
18
+ import { SettingsDialog } from "./components/SettingsDialog";
19
+ import { CompletionOverlay } from "./components/CompletionOverlay";
20
+ import { ThreadTree } from "./components/ThreadTree";
21
+ import { WelcomePlayground } from "./components/WelcomePlayground";
22
+ import { AppShell, type ProjectPanelMode } from "./components/AppShell";
23
+ import { EditorGrid } from "./components/EditorGrid";
24
+ import { ProjectTreeView } from "./components/ProjectTreeView";
25
+ import { ChangesFileTree } from "./components/ChangesColumn";
26
+ import { BareWorkbenchFileView } from "./components/BareWorkbenchFileView";
27
+ import { useChangesWorkbench } from "./use-changes-workbench";
28
+ import { ConfirmDialog } from "./components/ConfirmDialog";
29
+ import { PromptDialog } from "./components/PromptDialog";
30
+ import { WalkWizard } from "./components/WalkWizard";
31
+ import { Toast } from "./components/Toast";
32
+
33
+ export function ErrorScreen({ error, theme }: { error: string; theme: Theme }): React.ReactNode {
34
+ return (
35
+ <ThemeProvider theme={theme}>
36
+ <text fg={theme.red}>cueloop: {error}</text>
37
+ </ThemeProvider>
38
+ );
39
+ }
40
+
41
+ export function ConnectingScreen({ theme }: { theme: Theme }): React.ReactNode {
42
+ return (
43
+ <ThemeProvider theme={theme}>
44
+ <text fg={theme.textDim}>connecting to the daemon…</text>
45
+ </ThemeProvider>
46
+ );
47
+ }
48
+
49
+ export function MenuChrome(props: {
50
+ menuDialog: "keybinds" | "settings" | null;
51
+ theme: Theme;
52
+ keybindsSections: CheatsheetSection[];
53
+ settingsCategories: SettingsCategory[];
54
+ settingsValues: SettingsValues;
55
+ settingsNav: SettingsNav;
56
+ onCategorySelect: (categoryId: string) => void;
57
+ cycleSetting: (rowKey: string) => void;
58
+ }): React.ReactNode {
59
+ const {
60
+ menuDialog,
61
+ theme,
62
+ keybindsSections,
63
+ settingsCategories,
64
+ settingsValues,
65
+ settingsNav,
66
+ onCategorySelect,
67
+ cycleSetting,
68
+ } = props;
69
+
70
+ // the gear opens the settings dialog directly; Keybinds is a leaf in its tree nav
71
+ if (menuDialog !== "settings") return null;
72
+
73
+ return (
74
+ <SettingsDialog
75
+ isOpen
76
+ version={CLIENT_VERSION}
77
+ keybindsSections={keybindsSections}
78
+ categories={settingsCategories}
79
+ values={settingsValues}
80
+ activeCategoryId={settingsNav.categoryId}
81
+ activeRowIndex={settingsNav.rowIndex}
82
+ activeZone={settingsNav.zone}
83
+ onCategorySelect={onCategorySelect}
84
+ onRowActivate={(row) => cycleSetting(row.key)}
85
+ theme={theme}
86
+ />
87
+ );
88
+ }
89
+
90
+ /** The welcome shell's Project pane: the launch repo's changed files in changes mode, its full tree otherwise. */
91
+ function WelcomeProjectPanel({
92
+ mode,
93
+ controller,
94
+ onOpenFile,
95
+ theme,
96
+ }: {
97
+ mode: ProjectPanelMode;
98
+ controller: ReviewController;
99
+ onOpenFile: (path: string) => void;
100
+ theme: Theme;
101
+ }): React.ReactNode {
102
+ const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
103
+
104
+ useEffect(() => {
105
+ let alive = true;
106
+
107
+ void controller.repoChanges().then(
108
+ (files) => {
109
+ if (alive) setChanges(files);
110
+ },
111
+ () => {
112
+ if (alive) setChanges([]);
113
+ },
114
+ );
115
+
116
+ return () => {
117
+ alive = false;
118
+ };
119
+ }, [controller]);
120
+
121
+ if (mode === "changes") {
122
+ return <ChangesFileTree files={changes} onSelectFile={onOpenFile} theme={theme} />;
123
+ }
124
+
125
+ return (
126
+ <ProjectTreeView
127
+ loadFiles={() => controller.repoFiles()}
128
+ onSelectFile={onOpenFile}
129
+ theme={theme}
130
+ />
131
+ );
132
+ }
133
+
134
+ /**
135
+ * The shell with no thread open: the same header and Projects/Threads sidebar as
136
+ * the thread view, and a disposable Welcome tab in the center. There is no
137
+ * separate inbox screen - opening the app lands here, and picking a thread swaps
138
+ * the center for it. Closing the Welcome tab leaves a bare "select a thread" hint.
139
+ */
140
+ export function NoThreadShell(props: {
141
+ rows: InboxRow[];
142
+ inboxCursor: number;
143
+ mode: Mode;
144
+ theme: Theme;
145
+ controller: ReviewController;
146
+ setMode: Dispatch<SetStateAction<Mode>>;
147
+ menuChrome: React.ReactNode;
148
+ onOpenMenu: () => void;
149
+ /** Shared with the thread view, so picking a thread preserves the sidebar. */
150
+ sidebarOpen: boolean;
151
+ onToggleSidebar: () => void;
152
+ pinnedIds: ReadonlySet<string>;
153
+ onPin: (id: string) => void;
154
+ onRename: (id: string, title: string) => void;
155
+ /** Drives the "/" quick actions and skills in the welcome playground's composer. */
156
+ quickActions: QuickAction[];
157
+ /** Reports the welcome composer's open state, so the shell suspends its inbox keys while typing. */
158
+ onWelcomeComposingChange: (composing: boolean) => void;
159
+ }): React.ReactNode {
160
+ const {
161
+ rows,
162
+ inboxCursor,
163
+ mode,
164
+ theme,
165
+ controller,
166
+ setMode,
167
+ menuChrome,
168
+ onOpenMenu,
169
+ sidebarOpen,
170
+ onToggleSidebar,
171
+ pinnedIds,
172
+ onPin,
173
+ onRename,
174
+ quickActions,
175
+ onWelcomeComposingChange,
176
+ } = props;
177
+ const confirming = mode.type === "confirmDelete" ? mode : null;
178
+ // The bare-launch shell is the same four panes as a thread: the Thread pane waits in its empty state
179
+ // and a disposable Welcome tab rides in the Changes editor until a thread or diff is opened.
180
+ const workbench = useChangesWorkbench({ seed: "welcome" });
181
+ const openRepoFile = (path: string): void => workbench.openFile(path, "contents");
182
+
183
+ return (
184
+ <ThemeProvider theme={theme}>
185
+ <AppShell
186
+ sidebarOpen={sidebarOpen}
187
+ onToggleSidebar={onToggleSidebar}
188
+ onOpenMenu={onOpenMenu}
189
+ threadsPanel={
190
+ <ScrollArea>
191
+ <ThreadTree
192
+ rows={rows}
193
+ cursor={inboxCursor}
194
+ pinnedIds={pinnedIds}
195
+ width={30}
196
+ onSelect={(id) => controller.open(id)}
197
+ onRequestDelete={(id, title) =>
198
+ setMode({ type: "confirmDelete", sessionId: id, title })
199
+ }
200
+ onPin={onPin}
201
+ onRename={onRename}
202
+ theme={theme}
203
+ />
204
+ </ScrollArea>
205
+ }
206
+ threadTitle=""
207
+ threadPanel={
208
+ <box
209
+ style={{
210
+ flexGrow: 1,
211
+ flexDirection: "column",
212
+ alignItems: "center",
213
+ justifyContent: "center",
214
+ }}
215
+ >
216
+ <text fg={theme.textDim}>{rows.length === 0 ? "no threads" : "Select a thread"}</text>
217
+ </box>
218
+ }
219
+ changesOpen={workbench.changesOpen}
220
+ projectOpen={workbench.projectOpen}
221
+ onToggleChanges={workbench.toggleChanges}
222
+ onToggleProject={workbench.toggleProject}
223
+ onToggleRight={workbench.toggleRight}
224
+ projectMode={workbench.projectMode}
225
+ zoomHideThread={workbench.zoomed}
226
+ changesPanel={
227
+ <EditorGrid
228
+ tree={workbench.grid}
229
+ focusedGroupId={workbench.activeGroup}
230
+ onFocusGroup={workbench.focusGroup}
231
+ onActivateTab={workbench.activate}
232
+ onCloseTab={workbench.close}
233
+ onSplit={workbench.split}
234
+ onZoom={workbench.toggleZoom}
235
+ zoomed={workbench.zoomed}
236
+ renderTab={(tab) =>
237
+ tab.kind === "welcome" ? (
238
+ <WelcomePlayground
239
+ version={CLIENT_VERSION}
240
+ quickActions={quickActions}
241
+ onComposingChange={onWelcomeComposingChange}
242
+ theme={theme}
243
+ />
244
+ ) : (
245
+ <BareWorkbenchFileView
246
+ path={tab.path ?? ""}
247
+ controller={controller}
248
+ quickActions={quickActions}
249
+ onComposingChange={onWelcomeComposingChange}
250
+ onExit={() => {}}
251
+ theme={theme}
252
+ />
253
+ )
254
+ }
255
+ theme={theme}
256
+ />
257
+ }
258
+ projectPanel={
259
+ <WelcomeProjectPanel
260
+ mode={workbench.projectMode}
261
+ controller={controller}
262
+ onOpenFile={openRepoFile}
263
+ theme={theme}
264
+ />
265
+ }
266
+ theme={theme}
267
+ >
268
+ {menuChrome}
269
+ <ConfirmDialog
270
+ isOpen={confirming !== null}
271
+ title=" Delete plan "
272
+ message={
273
+ confirming ? `Delete "${confirming.title}"? This removes the plan and its review.` : ""
274
+ }
275
+ onConfirm={() => {
276
+ if (confirming) controller.deleteSession(confirming.sessionId);
277
+ setMode({ type: "normal" });
278
+ }}
279
+ onCancel={() => setMode({ type: "normal" })}
280
+ theme={theme}
281
+ />
282
+ {mode.type === "renameThread" ? (
283
+ <PromptDialog
284
+ isOpen
285
+ title=" rename thread "
286
+ label="new title for this thread:"
287
+ value={mode.text}
288
+ placeholder="a short title"
289
+ onInput={(text) => setMode({ ...mode, text })}
290
+ theme={theme}
291
+ />
292
+ ) : null}
293
+ </AppShell>
294
+ </ThemeProvider>
295
+ );
296
+ }
297
+
298
+ export function CompletionScreen(props: {
299
+ theme: Theme;
300
+ session: Thread;
301
+ verdict: VerdictKind;
302
+ completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
303
+ status: string;
304
+ }): React.ReactNode {
305
+ const { theme, session, verdict, completion, status } = props;
306
+
307
+ return (
308
+ <ThemeProvider theme={theme}>
309
+ <CompletionOverlay
310
+ verdict={verdict}
311
+ completion={completion}
312
+ status={status}
313
+ returnsTo={
314
+ returnPaneFor(session.artifact.meta.herdrPane)
315
+ ? (session.artifact.meta.agent ?? "the agent")
316
+ : undefined
317
+ }
318
+ />
319
+ </ThemeProvider>
320
+ );
321
+ }
322
+
323
+ /** The words each tree prompt uses; enter with an empty summary moves back without one. */
324
+ const TREE_PROMPTS: Record<TreeAsk, { title: string; label: string; placeholder: string }> = {
325
+ branch: { title: " Branch ", label: "Name for the new branch:", placeholder: "alt" },
326
+ label: {
327
+ title: " Checkpoint ",
328
+ label: "Name for this checkpoint:",
329
+ placeholder: "before the rewrite",
330
+ },
331
+ navigate: {
332
+ title: " Move back ",
333
+ label: "Summary of what you leave behind (optional):",
334
+ placeholder: "tried a shorter plan",
335
+ },
336
+ };
337
+
338
+ export function TrailingOverlays(props: {
339
+ walking: boolean;
340
+ walk: { index: number } | null;
341
+ walkFileList: WalkFile[];
342
+ viewedPaths: Set<string>;
343
+ session: Thread;
344
+ terminalWidth: number;
345
+ theme: Theme;
346
+ mode: Mode;
347
+ toast: ToastState | null;
348
+ setMode: Dispatch<SetStateAction<Mode>>;
349
+ dispatch: (intent: Intent) => void;
350
+ }): React.ReactNode {
351
+ const {
352
+ walking,
353
+ walk,
354
+ walkFileList,
355
+ viewedPaths,
356
+ session,
357
+ terminalWidth,
358
+ theme,
359
+ mode,
360
+ toast,
361
+ setMode,
362
+ dispatch,
363
+ } = props;
364
+
365
+ return (
366
+ <>
367
+ {walking && walk !== null ? (
368
+ <WalkWizard
369
+ files={walkFileList}
370
+ index={walk.index}
371
+ viewedPaths={viewedPaths}
372
+ note={
373
+ walkFileList[walk.index] !== undefined
374
+ ? noteForFile(session.annotations, walkFileList[walk.index]!.path)
375
+ : undefined
376
+ }
377
+ terminalWidth={terminalWidth}
378
+ onSubmitRequest={() => {
379
+ dispatch({ type: "walkLeave" });
380
+ dispatch({ type: "openSubmit" });
381
+ }}
382
+ onBack={() => dispatch({ type: "walkBack" })}
383
+ />
384
+ ) : null}
385
+ {mode.type === "rename" ? (
386
+ <PromptDialog
387
+ isOpen
388
+ title=" Rename author "
389
+ label="Display name for this collaborator:"
390
+ value={mode.text}
391
+ placeholder="their name"
392
+ onInput={(text) => setMode({ ...mode, text })}
393
+ theme={theme}
394
+ />
395
+ ) : null}
396
+ {mode.type === "renameThread" ? (
397
+ <PromptDialog
398
+ isOpen
399
+ title=" rename thread "
400
+ label="new title for this thread:"
401
+ value={mode.text}
402
+ placeholder="a short title"
403
+ onInput={(text) => setMode({ ...mode, text })}
404
+ theme={theme}
405
+ />
406
+ ) : null}
407
+ {mode.type === "nameSelf" ? (
408
+ <PromptDialog
409
+ isOpen
410
+ title=" Welcome "
411
+ label="Your name (optional) - it attributes the notes you leave:"
412
+ value={mode.text}
413
+ placeholder="your name"
414
+ onInput={(text) => setMode({ ...mode, text })}
415
+ theme={theme}
416
+ />
417
+ ) : null}
418
+ {mode.type === "treePrompt" ? (
419
+ <PromptDialog
420
+ isOpen
421
+ title={TREE_PROMPTS[mode.ask].title}
422
+ label={TREE_PROMPTS[mode.ask].label}
423
+ value={mode.text}
424
+ placeholder={TREE_PROMPTS[mode.ask].placeholder}
425
+ onInput={(text) => setMode({ ...mode, text })}
426
+ theme={theme}
427
+ />
428
+ ) : null}
429
+ {toast ? <Toast title={toast.title} body={toast.body} theme={theme} /> : null}
430
+ </>
431
+ );
432
+ }
@@ -0,0 +1,159 @@
1
+ import type { Dispatch, MutableRefObject, SetStateAction } from "react";
2
+ import type { Thread, VerdictKind } from "@cueloop/schema";
3
+ import type { Mode } from "./intent-dispatch";
4
+ import type { Intent, KeyState } from "./keymap";
5
+ import type { Completion } from "./thread-controller";
6
+ import type { WalkFile } from "./walk";
7
+ import { viewedCount } from "./walk";
8
+ import type { ConfirmCardProps } from "./components/ConfirmCard";
9
+ import type { BreadcrumbItem } from "./components/Breadcrumb";
10
+
11
+ export function computeRoleCapabilities(
12
+ readOnly: boolean,
13
+ role: "owner" | "observer" | "collaborator",
14
+ ) {
15
+ const observer = readOnly || role === "observer";
16
+
17
+ return { observer, isOwner: !observer && role === "owner" };
18
+ }
19
+
20
+ export function deriveReviewFlags(session: Thread | null) {
21
+ return {
22
+ isDiff: session?.artifact.type === "diff",
23
+ isPrototype: session?.artifact.type === "prototype",
24
+ resolved: session?.status === "resolved",
25
+ };
26
+ }
27
+
28
+ export function isWalking(isDiff: boolean, walk: { index: number } | null): boolean {
29
+ return isDiff && walk !== null;
30
+ }
31
+
32
+ /**
33
+ * A prototype renders as a kitty pixel mockup only in the opt-in experimental mode:
34
+ * the flag is on and the artifact carries an HTML entry. Otherwise it is the default
35
+ * markdown design doc, rendered through the thread/markdown path.
36
+ */
37
+ export function isPixelPrototypeMode(
38
+ isPrototype: boolean,
39
+ prototypePixels: boolean,
40
+ prototypePath: string | undefined,
41
+ ): boolean {
42
+ return isPrototype && prototypePixels && Boolean(prototypePath);
43
+ }
44
+
45
+ export function resolveOverlay(
46
+ mode: Mode,
47
+ completionPhase: Completion["phase"],
48
+ walking: boolean,
49
+ ): KeyState["overlay"] {
50
+ if (mode.type === "compose" || mode.type === "railEdit") return "compose";
51
+ if (mode.type === "submit") return "submit";
52
+ if (mode.type === "confirmDelete") return "confirm";
53
+ if (
54
+ mode.type === "rename" ||
55
+ mode.type === "renameThread" ||
56
+ mode.type === "nameSelf" ||
57
+ mode.type === "treePrompt"
58
+ )
59
+ return "prompt";
60
+ if (mode.type === "spanActions") return "spanActions";
61
+ if (completionPhase === "prompt") return "completion-prompt";
62
+ if (completionPhase === "counting") return "completion-counting";
63
+ if (walking) return "walk";
64
+
65
+ return "none";
66
+ }
67
+
68
+ export function isCompletionOverlayPhase(
69
+ completion: Completion,
70
+ ): completion is { phase: "prompt" } | { phase: "counting"; remaining: number } {
71
+ return completion.phase === "prompt" || completion.phase === "counting";
72
+ }
73
+
74
+ export function buildHeaderItems(params: {
75
+ session: Thread;
76
+ resolved: boolean;
77
+ observer: boolean;
78
+ role: "owner" | "observer" | "collaborator";
79
+ }): BreadcrumbItem[] {
80
+ const { session, resolved, observer, role } = params;
81
+
82
+ return [
83
+ { label: "cueloop", tone: "accent" },
84
+ ...(resolved
85
+ ? [
86
+ {
87
+ label: `resolved: ${session.verdict!.kind.replace("_", " ")}`,
88
+ tone: "green" as const,
89
+ },
90
+ ]
91
+ : []),
92
+ ...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
93
+ ...(role === "collaborator"
94
+ ? [{ label: "shared · your notes save as you go", tone: "dim" as const }]
95
+ : []),
96
+ {
97
+ label: `${session.artifact.meta.title ?? session.artifact.meta.planPath ?? session.id} · rev ${session.revisions.length}`,
98
+ tone: "dim",
99
+ },
100
+ { label: `submitted by ${session.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
101
+ ];
102
+ }
103
+
104
+ export function buildRenderFlags(params: {
105
+ session: Thread;
106
+ isOwner: boolean;
107
+ isDiff: boolean;
108
+ isPixelPrototype: boolean;
109
+ resolved: boolean;
110
+ menuDialog: "keybinds" | "settings" | null;
111
+ resolvedIds: Set<string>;
112
+ }) {
113
+ const { session, isOwner, isDiff, isPixelPrototype, resolved, menuDialog, resolvedIds } = params;
114
+
115
+ return {
116
+ showOwnerActions: isOwner && !isDiff && !resolved,
117
+ prototypeCanComment: isOwner && !resolved,
118
+ chromeHidden: menuDialog !== null,
119
+ prototypePath: session.artifact.meta.prototypePath ?? "",
120
+ // a markdown prototype interleaves resolved cards like a plan; only the pixel mockup opts out
121
+ railResolvedIds: isDiff || isPixelPrototype ? null : resolvedIds,
122
+ };
123
+ }
124
+
125
+ interface DraftHandlerDeps {
126
+ liveInput: MutableRefObject<string>;
127
+ setMode: Dispatch<SetStateAction<Mode>>;
128
+ dispatch: (intent: Intent) => void;
129
+ }
130
+
131
+ export function buildSubmitConfirmState(
132
+ deps: DraftHandlerDeps & {
133
+ mode: Mode;
134
+ isDiff: boolean;
135
+ session: Thread;
136
+ walkFileList: WalkFile[];
137
+ viewedPaths: Set<string>;
138
+ },
139
+ ): Omit<ConfirmCardProps, "theme"> | null {
140
+ const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
141
+
142
+ if (mode.type !== "submit") return null;
143
+
144
+ return {
145
+ verdict: mode.verdict,
146
+ summary: mode.summary,
147
+ viewedSummary:
148
+ isDiff && session.viewedPaths !== undefined
149
+ ? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
150
+ : undefined,
151
+ onInput: (summary: string) => {
152
+ liveInput.current = summary;
153
+ setMode({ ...mode, summary });
154
+ },
155
+ onSelectVerdict: (verdict: VerdictKind) => setMode({ ...mode, verdict }),
156
+ onSubmit: () => dispatch({ type: "submitVerdict" }),
157
+ onCancel: () => dispatch({ type: "closeOverlay" }),
158
+ };
159
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Identity } from "@cueloop/schema";
3
+ import { ANONYMOUS_LABEL, resolveDisplayName, shortHandle } from "./attribution";
4
+
5
+ describe(shortHandle, () => {
6
+ test("shortens an ssh fingerprint to a stable handle", () => {
7
+ // Arrange / Act / Assert
8
+ expect(shortHandle("SHA256:1a2b3c4d5e6f")).toBe("1a2b3c4d");
9
+ expect(shortHandle("SHA256:1a2b3c4d5e6f")).toBe(shortHandle("SHA256:1a2b3c4d5e6f"));
10
+ });
11
+ });
12
+
13
+ describe(resolveDisplayName, () => {
14
+ const named: Identity[] = [{ id: "SHA256:abc", provider: "ssh", name: "Al" }];
15
+ const unnamed: Identity[] = [{ id: "SHA256:abc", provider: "ssh" }];
16
+
17
+ test("a local rename override wins over everything", () => {
18
+ // Arrange / Act
19
+ const label = resolveDisplayName("SHA256:abc", named, { "SHA256:abc": "Alex" });
20
+
21
+ // Assert
22
+ expect(label).toBe("Alex");
23
+ });
24
+
25
+ test("falls back to name, then anonymous, then the short handle", () => {
26
+ // Arrange / Act / Assert
27
+ expect(resolveDisplayName("SHA256:abc", named, {})).toBe("Al");
28
+ expect(resolveDisplayName("SHA256:abc", unnamed, {})).toBe(ANONYMOUS_LABEL);
29
+ expect(resolveDisplayName("SHA256:1a2b3c4d5e", [], {})).toBe("1a2b3c4d");
30
+ });
31
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Attribution labels for annotations. An annotation's `author` is an identity
3
+ * id (an SSH fingerprint today). The rail resolves it against the share's
4
+ * participant registry and the planner's local rename overrides; ADR 0006.
5
+ */
6
+
7
+ import type { Identity } from "@cueloop/schema";
8
+
9
+ const FINGERPRINT_PREFIX = "SHA256:";
10
+
11
+ /** Shown for a known participant who chose not to give a name. */
12
+ export const ANONYMOUS_LABEL = "-- anonymous --";
13
+
14
+ /** A short, stable handle from a raw author id when no name is known. */
15
+ export function shortHandle(authorId: string): string {
16
+ const base = authorId.startsWith(FINGERPRINT_PREFIX)
17
+ ? authorId.slice(FINGERPRINT_PREFIX.length)
18
+ : authorId;
19
+
20
+ return base.slice(0, 8) || authorId;
21
+ }
22
+
23
+ /**
24
+ * The rail label for an author id: a local rename wins, then the collaborator's
25
+ * own name, then anonymous for a known-but-unnamed participant, then a short
26
+ * handle for an id we hold no identity for.
27
+ */
28
+ export function resolveDisplayName(
29
+ authorId: string,
30
+ participants: Identity[] | undefined,
31
+ overrides: Record<string, string>,
32
+ ): string {
33
+ const override = overrides[authorId];
34
+
35
+ if (override) return override;
36
+ const identity = participants?.find((participant) => participant.id === authorId);
37
+
38
+ if (identity?.name) return identity.name;
39
+ if (identity) return ANONYMOUS_LABEL;
40
+
41
+ return shortHandle(authorId);
42
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { clipboardCommands } from "./clipboard";
3
+
4
+ describe(clipboardCommands, () => {
5
+ test("uses pbcopy on macOS", () => {
6
+ // Assert
7
+ expect(clipboardCommands("darwin")).toEqual([["pbcopy"]]);
8
+ });
9
+
10
+ test("uses clip.exe on Windows", () => {
11
+ // Assert
12
+ expect(clipboardCommands("win32")).toEqual([["clip.exe"]]);
13
+ });
14
+
15
+ test("prefers wayland, then X11 tools on Linux", () => {
16
+ // Act
17
+ const commands = clipboardCommands("linux");
18
+
19
+ // Assert
20
+ expect(commands[0]).toEqual(["wl-copy"]);
21
+ expect(commands.map((command) => command[0])).toEqual(["wl-copy", "xclip", "xsel"]);
22
+ });
23
+ });