@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81

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 (307) 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 +40 -32
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +77 -51
  10. package/src/App.diff.test.tsx +176 -45
  11. package/src/App.export.test.tsx +58 -41
  12. package/src/App.focus.test.ts +89 -0
  13. package/src/App.identity.test.tsx +78 -0
  14. package/src/App.plan-render.test.tsx +209 -0
  15. package/src/App.plan-review.test.tsx +428 -0
  16. package/src/App.prototype-markdown.test.tsx +92 -0
  17. package/src/App.prototype.test.tsx +176 -0
  18. package/src/App.readonly.test.tsx +129 -67
  19. package/src/App.regression.test.tsx +94 -0
  20. package/src/App.rendering.test.tsx +61 -35
  21. package/src/App.share-dialog.test.tsx +172 -0
  22. package/src/App.submit-confirm.test.tsx +222 -0
  23. package/src/App.test.tsx +333 -131
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1338 -608
  26. package/src/App.visual.test.tsx +57 -0
  27. package/src/App.walk.test.tsx +299 -0
  28. package/src/App.workbench.test.tsx +701 -0
  29. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  30. package/src/annotation-palette.ts +62 -0
  31. package/src/ansi-screen.test.ts +28 -0
  32. package/src/ansi-screen.ts +72 -0
  33. package/src/app-key-state.ts +61 -0
  34. package/src/app-screens.tsx +574 -0
  35. package/src/app-view-model.ts +162 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/clipboard.test.ts +23 -0
  39. package/src/clipboard.ts +33 -0
  40. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  41. package/src/components/AnnotatableFileView.test.tsx +102 -0
  42. package/src/components/AnnotatableFileView.tsx +134 -0
  43. package/src/components/AnnotationCards.stories.tsx +86 -0
  44. package/src/components/AnnotationCards.tsx +341 -0
  45. package/src/components/AnsiScreen.stories.tsx +12 -0
  46. package/src/components/AnsiScreen.tsx +28 -0
  47. package/src/components/AppHeader.stories.tsx +50 -0
  48. package/src/components/AppHeader.tsx +83 -0
  49. package/src/components/AppShell.brand.test.tsx +58 -0
  50. package/src/components/AppShell.stories.tsx +89 -0
  51. package/src/components/AppShell.tsx +329 -0
  52. package/src/components/AppShell.zoom.test.tsx +135 -0
  53. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  54. package/src/components/BareWorkbenchFileView.tsx +64 -0
  55. package/src/components/Breadcrumb.stories.tsx +19 -0
  56. package/src/components/Breadcrumb.tsx +45 -0
  57. package/src/components/ChangesColumn.stories.tsx +44 -0
  58. package/src/components/ChangesColumn.tsx +199 -0
  59. package/src/components/ChangesFileTree.test.tsx +61 -0
  60. package/src/components/CodeBlock.stories.tsx +42 -0
  61. package/src/components/CodeBlock.tsx +84 -0
  62. package/src/components/CompletionOverlay.stories.tsx +37 -0
  63. package/src/components/CompletionOverlay.tsx +77 -0
  64. package/src/components/ConfirmCard.stories.tsx +37 -0
  65. package/src/components/ConfirmCard.tsx +153 -0
  66. package/src/components/ConfirmDialog.stories.tsx +21 -0
  67. package/src/components/ConfirmDialog.tsx +72 -0
  68. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  69. package/src/components/DiffContentView.split.test.tsx +140 -0
  70. package/src/components/DiffContentView.stories.tsx +80 -0
  71. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  72. package/src/components/DiffContentView.tsx +876 -0
  73. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  74. package/src/components/DiscussionMarkerRail.tsx +186 -0
  75. package/src/components/EditorGrid.stories.tsx +59 -0
  76. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  77. package/src/components/EditorGrid.tsx +426 -0
  78. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  79. package/src/components/FileContentsView.stories.tsx +16 -0
  80. package/src/components/FileContentsView.test.tsx +29 -0
  81. package/src/components/FileContentsView.tsx +98 -0
  82. package/src/components/GridTabContent.stories.tsx +59 -0
  83. package/src/components/GridTabContent.tsx +182 -0
  84. package/src/components/HandleChipsInput.stories.tsx +24 -0
  85. package/src/components/HandleChipsInput.test.tsx +35 -0
  86. package/src/components/HandleChipsInput.tsx +145 -0
  87. package/src/components/KeybindsDialog.stories.tsx +31 -0
  88. package/src/components/KeybindsDialog.tsx +103 -0
  89. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  90. package/src/components/MarkdownGridBlock.tsx +72 -0
  91. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  92. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  93. package/src/components/MarkdownThreadEditor.tsx +162 -0
  94. package/src/components/MenuBar.stories.tsx +28 -0
  95. package/src/components/MenuBar.tsx +44 -0
  96. package/src/components/NavModeHint.stories.tsx +40 -0
  97. package/src/components/NavModeHint.tsx +31 -0
  98. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  99. package/src/components/OverlayScrollbar.test.tsx +139 -0
  100. package/src/components/OverlayScrollbar.tsx +129 -0
  101. package/src/components/PaneGrid.stories.tsx +84 -0
  102. package/src/components/PaneGrid.tsx +128 -0
  103. package/src/components/PanelColumn.stories.tsx +25 -0
  104. package/src/components/PanelColumn.tsx +98 -0
  105. package/src/components/ProjectTreeView.stories.tsx +25 -0
  106. package/src/components/ProjectTreeView.test.tsx +59 -0
  107. package/src/components/ProjectTreeView.tsx +111 -0
  108. package/src/components/PromptDialog.stories.tsx +36 -0
  109. package/src/components/PromptDialog.test.tsx +61 -0
  110. package/src/components/PromptDialog.tsx +99 -0
  111. package/src/components/PrototypeContentView.stories.tsx +19 -0
  112. package/src/components/PrototypeContentView.tsx +446 -0
  113. package/src/components/RootOverlay.stories.tsx +45 -0
  114. package/src/components/RootOverlay.tsx +48 -0
  115. package/src/components/ScrollArea.stories.tsx +20 -0
  116. package/src/components/ScrollArea.tsx +44 -0
  117. package/src/components/SettingsDialog.stories.tsx +117 -0
  118. package/src/components/SettingsDialog.tsx +186 -0
  119. package/src/components/SettingsRows.stories.tsx +18 -0
  120. package/src/components/SettingsRows.tsx +112 -0
  121. package/src/components/ShareDialog.stories.tsx +121 -0
  122. package/src/components/ShareDialog.tsx +552 -0
  123. package/src/components/ShellHeader.stories.tsx +50 -0
  124. package/src/components/ShellHeader.tsx +137 -0
  125. package/src/components/SlashComposer.stories.tsx +37 -0
  126. package/src/components/SlashComposer.tsx +87 -0
  127. package/src/components/ThreadFooter.stories.tsx +30 -0
  128. package/src/components/ThreadFooter.test.tsx +47 -0
  129. package/src/components/ThreadFooter.tsx +76 -0
  130. package/src/components/ThreadTree.stories.tsx +46 -0
  131. package/src/components/ThreadTree.tsx +255 -0
  132. package/src/components/ThreadView.selection.test.tsx +899 -0
  133. package/src/components/ThreadView.slash.test.tsx +146 -0
  134. package/src/components/ThreadView.stories.tsx +26 -0
  135. package/src/components/ThreadView.tsx +472 -0
  136. package/src/components/Toast.stories.tsx +19 -0
  137. package/src/components/Toast.tsx +52 -0
  138. package/src/components/Tooltip.stories.tsx +26 -0
  139. package/src/components/Tooltip.tsx +72 -0
  140. package/src/components/TreePane.stories.tsx +106 -0
  141. package/src/components/TreePane.tsx +157 -0
  142. package/src/components/WalkWizard.stories.tsx +100 -0
  143. package/src/components/WalkWizard.tsx +150 -0
  144. package/src/components/WelcomePlayground.stories.tsx +12 -0
  145. package/src/components/WelcomePlayground.test.tsx +47 -0
  146. package/src/components/WelcomePlayground.tsx +142 -0
  147. package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
  148. package/src/components/diff-content-view-layout.test.ts +198 -0
  149. package/src/components/diff-content-view-layout.ts +84 -0
  150. package/src/components/editor-grid.test.ts +86 -0
  151. package/src/components/editor-grid.ts +138 -0
  152. package/src/components/file-tree.test.ts +60 -0
  153. package/src/components/file-tree.ts +112 -0
  154. package/src/components/menu-control.stories.tsx +35 -0
  155. package/src/components/menu-control.tsx +43 -0
  156. package/src/components/pages/Workspace.stories.tsx +181 -0
  157. package/src/components/primitives/Button.stories.tsx +32 -0
  158. package/src/components/primitives/Button.tsx +56 -0
  159. package/src/components/primitives/Card.stories.tsx +25 -0
  160. package/src/components/primitives/Card.tsx +63 -0
  161. package/src/components/primitives/Dialog.stories.tsx +24 -0
  162. package/src/components/primitives/Dialog.test.tsx +37 -0
  163. package/src/components/primitives/Dialog.tsx +71 -0
  164. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  165. package/src/components/primitives/DialogActions.tsx +39 -0
  166. package/src/components/primitives/IconButton.stories.tsx +36 -0
  167. package/src/components/primitives/IconButton.tsx +62 -0
  168. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  169. package/src/components/primitives/StatusBar.tsx +23 -0
  170. package/src/components/primitives/Switch.stories.tsx +17 -0
  171. package/src/components/primitives/Switch.tsx +32 -0
  172. package/src/components/primitives/Tabs.stories.tsx +29 -0
  173. package/src/components/primitives/Tabs.tsx +102 -0
  174. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  175. package/src/components/primitives/Toolbar.tsx +14 -0
  176. package/src/components/primitives/Tree.stories.tsx +124 -0
  177. package/src/components/primitives/Tree.tsx +100 -0
  178. package/src/components/primitives/frame.test.ts +12 -0
  179. package/src/components/primitives/frame.ts +17 -0
  180. package/src/components/primitives/icons.ts +61 -0
  181. package/src/components/primitives/tree-model.test.ts +99 -0
  182. package/src/components/primitives/tree-model.ts +137 -0
  183. package/src/components/quick-actions-editor.stories.tsx +58 -0
  184. package/src/components/quick-actions-editor.test.tsx +117 -0
  185. package/src/components/quick-actions-editor.tsx +226 -0
  186. package/src/components/share-dialog-store.ts +58 -0
  187. package/src/components/stories-app.tsx +193 -0
  188. package/src/components/stories.test.tsx +74 -0
  189. package/src/components/story-fixtures.ts +102 -0
  190. package/src/components/story-tree.test.ts +108 -0
  191. package/src/components/story.ts +159 -0
  192. package/src/components/syntax-highlight.ts +133 -0
  193. package/src/components/terminal-pane.test.tsx +40 -0
  194. package/src/components/terminal-pane.ts +199 -0
  195. package/src/components/theme-context.stories.tsx +46 -0
  196. package/src/components/theme-context.tsx +37 -0
  197. package/src/components/thread-tree.test.ts +134 -0
  198. package/src/components/thread-tree.ts +109 -0
  199. package/src/components/truncate-text.ts +7 -0
  200. package/src/components/truncate-title.test.ts +31 -0
  201. package/src/components/truncate-title.ts +15 -0
  202. package/src/config-identity.test.ts +78 -0
  203. package/src/config.test.ts +517 -33
  204. package/src/config.ts +467 -57
  205. package/src/diff-fold.test.ts +78 -0
  206. package/src/diff-fold.ts +126 -0
  207. package/src/diff-hunk-curate.test.ts +315 -0
  208. package/src/diff-hunk-curate.ts +153 -0
  209. package/src/diff-intraline.test.ts +238 -0
  210. package/src/diff-intraline.ts +236 -0
  211. package/src/diff-syntax.test.ts +112 -0
  212. package/src/diff-syntax.ts +179 -0
  213. package/src/discussions.ts +79 -0
  214. package/src/editor.test.ts +149 -0
  215. package/src/editor.ts +139 -9
  216. package/src/ghostty-terminal.test.ts +85 -0
  217. package/src/ghostty-terminal.ts +199 -0
  218. package/src/github-identity.test.ts +58 -0
  219. package/src/github-identity.ts +41 -0
  220. package/src/herdr.test.ts +9 -34
  221. package/src/herdr.ts +25 -21
  222. package/src/index.ts +13 -0
  223. package/src/integrations.test.ts +41 -0
  224. package/src/integrations.ts +32 -0
  225. package/src/intent-dispatch.test.ts +723 -0
  226. package/src/intent-dispatch.ts +692 -0
  227. package/src/key-bindings.test.ts +156 -0
  228. package/src/key-bindings.ts +459 -0
  229. package/src/keymap.test.ts +461 -113
  230. package/src/keymap.ts +321 -79
  231. package/src/kitty-image.test.ts +66 -0
  232. package/src/kitty-image.ts +138 -0
  233. package/src/launch-layout.test.ts +52 -0
  234. package/src/launch-layout.ts +42 -0
  235. package/src/mark-runs.ts +121 -0
  236. package/src/markdown-editor-syntax-style.ts +50 -0
  237. package/src/markdown-grid.test.ts +101 -0
  238. package/src/markdown-grid.ts +203 -0
  239. package/src/markdown-highlight.test.ts +61 -0
  240. package/src/markdown-highlight.ts +122 -0
  241. package/src/pasted-image.test.ts +67 -0
  242. package/src/pasted-image.ts +60 -0
  243. package/src/perf/otlp-trace.test.ts +116 -0
  244. package/src/perf/otlp-trace.ts +130 -0
  245. package/src/perf/perf-report.ts +17 -0
  246. package/src/perf/perf-timings.test.ts +18 -0
  247. package/src/perf/perf-timings.ts +38 -0
  248. package/src/prototype-browser.test.ts +64 -0
  249. package/src/prototype-browser.ts +329 -0
  250. package/src/prototype-pixels.tsx +35 -0
  251. package/src/pty.test.ts +81 -0
  252. package/src/pty.ts +245 -0
  253. package/src/ready-signal.test.ts +85 -0
  254. package/src/ready-signal.ts +52 -0
  255. package/src/run.ts +78 -13
  256. package/src/serve.test.ts +24 -12
  257. package/src/serve.ts +52 -10
  258. package/src/share.ts +237 -0
  259. package/src/skills.test.ts +74 -0
  260. package/src/skills.ts +75 -0
  261. package/src/slash-palette.test.ts +103 -0
  262. package/src/slash-palette.ts +129 -0
  263. package/src/split-diff.test.ts +110 -0
  264. package/src/split-diff.ts +149 -0
  265. package/src/test-support.test.ts +264 -0
  266. package/src/test-support.ts +369 -0
  267. package/src/theme-presets.test.ts +51 -0
  268. package/src/theme-presets.ts +214 -0
  269. package/src/theme.ts +84 -19
  270. package/src/thread-chords.test.ts +79 -0
  271. package/src/thread-chords.ts +163 -0
  272. package/src/thread-controller.curate.test.ts +450 -0
  273. package/src/thread-controller.derivation.test.ts +62 -0
  274. package/src/thread-controller.live-diff.test.ts +322 -0
  275. package/src/thread-controller.optimistic.test.ts +69 -0
  276. package/src/thread-controller.share.test.ts +561 -0
  277. package/src/thread-controller.switch-cache.test.ts +176 -0
  278. package/src/thread-controller.tree.test.ts +261 -0
  279. package/src/thread-controller.ts +1893 -0
  280. package/src/thread-selection.test.ts +210 -0
  281. package/src/thread-selection.ts +243 -0
  282. package/src/tree-view.test.ts +128 -0
  283. package/src/tree-view.ts +148 -0
  284. package/src/use-annotation-surface.tsx +915 -0
  285. package/src/use-changes-workbench.ts +190 -0
  286. package/src/use-frame-measure.ts +41 -0
  287. package/src/use-remember-layout.ts +31 -0
  288. package/src/use-settings-dialog.tsx +320 -0
  289. package/src/use-terminal-virtualizer.ts +233 -0
  290. package/src/use-thread-body-editing.tsx +58 -0
  291. package/src/version.ts +5 -0
  292. package/src/view-diff.test.ts +137 -9
  293. package/src/view-diff.ts +269 -23
  294. package/src/view-plan.regression.test.ts +60 -0
  295. package/src/view-plan.test.ts +456 -0
  296. package/src/view-plan.ts +719 -0
  297. package/src/visual/contrast.test.ts +35 -0
  298. package/src/visual/contrast.ts +33 -0
  299. package/src/walk.test.ts +121 -0
  300. package/src/walk.ts +74 -0
  301. package/src/workbench-snapshot.test.ts +81 -0
  302. package/src/workbench-snapshot.ts +29 -0
  303. package/src/session-controller.ts +0 -345
  304. package/src/syntax.test.ts +0 -41
  305. package/src/syntax.ts +0 -88
  306. package/src/view.test.ts +0 -157
  307. package/src/view.ts +0 -312
@@ -0,0 +1,162 @@
1
+ import type { Dispatch, MutableRefObject, SetStateAction } from "react";
2
+ import type { Thread, MessageOutcome } 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 type { QuickAction } from "./config";
8
+ import { viewedCount } from "./walk";
9
+ import type { ConfirmCardProps } from "./components/ConfirmCard";
10
+ import type { BreadcrumbItem } from "./components/Breadcrumb";
11
+
12
+ export function computeRoleCapabilities(
13
+ readOnly: boolean,
14
+ role: "owner" | "observer" | "collaborator",
15
+ ) {
16
+ const observer = readOnly || role === "observer";
17
+
18
+ return { observer, isOwner: !observer && role === "owner" };
19
+ }
20
+
21
+ export function deriveReviewFlags(session: Thread | null) {
22
+ return {
23
+ isDiff: session?.artifact.type === "diff",
24
+ isPrototype: session?.artifact.type === "prototype",
25
+ resolved: session?.status === "resolved",
26
+ };
27
+ }
28
+
29
+ export function isWalking(isDiff: boolean, walk: { index: number } | null): boolean {
30
+ return isDiff && walk !== null;
31
+ }
32
+
33
+ /**
34
+ * A prototype renders as a kitty pixel mockup only in the opt-in experimental mode:
35
+ * the flag is on and the artifact carries an HTML entry. Otherwise it is the default
36
+ * markdown design doc, rendered through the thread/markdown path.
37
+ */
38
+ export function isPixelPrototypeMode(
39
+ isPrototype: boolean,
40
+ prototypePixels: boolean,
41
+ prototypePath: string | undefined,
42
+ ): boolean {
43
+ return isPrototype && prototypePixels && Boolean(prototypePath);
44
+ }
45
+
46
+ export function resolveOverlay(
47
+ mode: Mode,
48
+ completionPhase: Completion["phase"],
49
+ walking: boolean,
50
+ ): KeyState["overlay"] {
51
+ if (mode.type === "compose" || mode.type === "railEdit") return "compose";
52
+ if (mode.type === "submit") return "submit";
53
+ if (mode.type === "confirmDelete") return "confirm";
54
+ if (
55
+ mode.type === "rename" ||
56
+ mode.type === "renameThread" ||
57
+ mode.type === "nameSelf" ||
58
+ mode.type === "treePrompt"
59
+ )
60
+ return "prompt";
61
+ if (mode.type === "spanActions") return "spanActions";
62
+ if (completionPhase === "prompt") return "completion-prompt";
63
+ if (completionPhase === "counting") return "completion-counting";
64
+ if (walking) return "walk";
65
+
66
+ return "none";
67
+ }
68
+
69
+ export function isCompletionOverlayPhase(
70
+ completion: Completion,
71
+ ): completion is { phase: "prompt" } | { phase: "counting"; remaining: number } {
72
+ return completion.phase === "prompt" || completion.phase === "counting";
73
+ }
74
+
75
+ export function buildHeaderItems(params: {
76
+ session: Thread;
77
+ resolved: boolean;
78
+ observer: boolean;
79
+ role: "owner" | "observer" | "collaborator";
80
+ }): BreadcrumbItem[] {
81
+ const { session, resolved, observer, role } = params;
82
+
83
+ return [
84
+ { label: "cueloop", tone: "accent" },
85
+ ...(resolved
86
+ ? [
87
+ {
88
+ label: `resolved: ${session.message!.outcome.replace("_", " ")}`,
89
+ tone: "green" as const,
90
+ },
91
+ ]
92
+ : []),
93
+ ...(observer ? [{ label: "observer", tone: "dim" as const }] : []),
94
+ ...(role === "collaborator"
95
+ ? [{ label: "shared · your notes save as you go", tone: "dim" as const }]
96
+ : []),
97
+ {
98
+ label: `${session.artifact.meta.title ?? session.artifact.meta.planPath ?? session.id} · rev ${session.revisions.length}`,
99
+ tone: "dim",
100
+ },
101
+ { label: `submitted by ${session.artifact.meta.agent ?? "unknown"}`, tone: "dim" },
102
+ ];
103
+ }
104
+
105
+ export function buildRenderFlags(params: {
106
+ session: Thread;
107
+ isOwner: boolean;
108
+ isDiff: boolean;
109
+ isPixelPrototype: boolean;
110
+ resolved: boolean;
111
+ menuDialog: "keybinds" | "settings" | null;
112
+ resolvedIds: Set<string>;
113
+ }) {
114
+ const { session, isOwner, isDiff, isPixelPrototype, resolved, menuDialog, resolvedIds } = params;
115
+
116
+ return {
117
+ showOwnerActions: isOwner && !isDiff && !resolved,
118
+ prototypeCanComment: isOwner && !resolved,
119
+ chromeHidden: menuDialog !== null,
120
+ prototypePath: session.artifact.meta.prototypePath ?? "",
121
+ // a markdown prototype interleaves resolved cards like a plan; only the pixel mockup opts out
122
+ railResolvedIds: isDiff || isPixelPrototype ? null : resolvedIds,
123
+ };
124
+ }
125
+
126
+ interface DraftHandlerDeps {
127
+ liveInput: MutableRefObject<string>;
128
+ setMode: Dispatch<SetStateAction<Mode>>;
129
+ dispatch: (intent: Intent) => void;
130
+ }
131
+
132
+ export function buildSubmitConfirmState(
133
+ deps: DraftHandlerDeps & {
134
+ mode: Mode;
135
+ isDiff: boolean;
136
+ session: Thread;
137
+ walkFileList: WalkFile[];
138
+ viewedPaths: Set<string>;
139
+ quickActions: QuickAction[];
140
+ },
141
+ ): Omit<ConfirmCardProps, "theme"> | null {
142
+ const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
143
+
144
+ if (mode.type !== "submit") return null;
145
+
146
+ return {
147
+ message: mode.message,
148
+ summary: mode.summary,
149
+ quickActions: deps.quickActions,
150
+ viewedSummary:
151
+ isDiff && session.viewedPaths !== undefined
152
+ ? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
153
+ : undefined,
154
+ onInput: (summary: string) => {
155
+ liveInput.current = summary;
156
+ setMode({ ...mode, summary });
157
+ },
158
+ onSelectMessage: (message: MessageOutcome) => setMode({ ...mode, message }),
159
+ onSubmit: () => dispatch({ type: "submitMessage" }),
160
+ onCancel: () => dispatch({ type: "closeOverlay" }),
161
+ };
162
+ }
@@ -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
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Best-effort system clipboard. Each platform has its own tool; we try them in
3
+ * order and report whether one worked, so the caller can fall back to printing
4
+ * the line (the over-SSH case, where no clipboard tool exists). Mirrors how
5
+ * herdr confirms a copy.
6
+ */
7
+
8
+ /** The clipboard-write commands to try, most-preferred first, for this platform. */
9
+ export function clipboardCommands(platform: NodeJS.Platform = process.platform): string[][] {
10
+ if (platform === "darwin") return [["pbcopy"]];
11
+ if (platform === "win32") return [["clip.exe"]];
12
+
13
+ return [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
14
+ }
15
+
16
+ /** Copy `text` to the clipboard; false when no tool is available or all fail. */
17
+ export async function copyToClipboard(text: string): Promise<boolean> {
18
+ for (const command of clipboardCommands()) {
19
+ try {
20
+ const proc = Bun.spawn(command, {
21
+ stdin: Buffer.from(text),
22
+ stdout: "ignore",
23
+ stderr: "ignore",
24
+ });
25
+
26
+ if ((await proc.exited) === 0) return true;
27
+ } catch {
28
+ // tool not installed - try the next one
29
+ }
30
+ }
31
+
32
+ return false;
33
+ }
@@ -0,0 +1,42 @@
1
+ import React from "react";
2
+ import { SCHEMA_VERSION, type Thread } from "@cueloop/schema";
3
+ import { DARK } from "../theme";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { AnnotatableFileView } from "./AnnotatableFileView";
6
+
7
+ export const meta: StoryMeta = { title: "Surfaces/AnnotatableFileView" };
8
+
9
+ const SAMPLE = `export function add(a: number, b: number): number {\n return a + b;\n}\n`;
10
+
11
+ const SESSION: Thread = {
12
+ schemaVersion: SCHEMA_VERSION,
13
+ id: "ses_story",
14
+ workspace: { repoRoot: "/repo", branch: "main" },
15
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
16
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00Z" }],
17
+ annotations: [],
18
+ message: null,
19
+ status: "pending",
20
+ createdAt: "2026-01-01T00:00:00Z",
21
+ };
22
+
23
+ const noop = (): void => {};
24
+
25
+ export const Loaded: Story = {
26
+ render: () => (
27
+ <AnnotatableFileView
28
+ path="src/add.ts"
29
+ loadContents={() => Promise.resolve(SAMPLE)}
30
+ session={SESSION}
31
+ quickActions={[]}
32
+ observer={false}
33
+ onAddComment={noop}
34
+ onReply={noop}
35
+ onUpdateAnnotation={noop}
36
+ onExit={noop}
37
+ theme={DARK}
38
+ />
39
+ ),
40
+ expectedColors: [DARK.textDim],
41
+ size: { width: 60, height: 12 },
42
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A project file is its own annotation surface: selecting a line and typing leaves a comment whose
3
+ * anchor quotes that file line, handed up as an Anchor the app persists with a file target.
4
+ */
5
+
6
+ import { describe, expect, mock, test } from "bun:test";
7
+ import { testRender } from "@opentui/react/test-utils";
8
+ import React from "react";
9
+ import { SCHEMA_VERSION, type Anchor, type Thread } from "@cueloop/schema";
10
+ import { AnnotatableFileView } from "./AnnotatableFileView";
11
+ import { DARK } from "../theme";
12
+ import { dragText, typeText, pressKey, waitForText } from "../test-support";
13
+
14
+ const SAMPLE = "export function add(a: number, b: number) {\n return a + b;\n}\n";
15
+
16
+ function planSession(): Thread {
17
+ return {
18
+ schemaVersion: SCHEMA_VERSION,
19
+ id: "ses_plan",
20
+ workspace: { repoRoot: "/repo", branch: "main" },
21
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
22
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00Z" }],
23
+ annotations: [],
24
+ message: null,
25
+ status: "pending",
26
+ createdAt: "2026-01-01T00:00:00Z",
27
+ };
28
+ }
29
+
30
+ const noop = (): void => {};
31
+
32
+ describe("AnnotatableFileView", () => {
33
+ test("commenting a file line hands up an anchor that quotes the line", async () => {
34
+ const onAddComment = mock((_anchor: Anchor, _body: string) => {});
35
+ const setup = await testRender(
36
+ <AnnotatableFileView
37
+ path="src/add.ts"
38
+ loadContents={() => Promise.resolve(SAMPLE)}
39
+ session={planSession()}
40
+ quickActions={[]}
41
+ observer={false}
42
+ onAddComment={onAddComment}
43
+ onReply={noop}
44
+ onUpdateAnnotation={noop}
45
+ onExit={noop}
46
+ theme={DARK}
47
+ />,
48
+ { width: 64, height: 12 },
49
+ );
50
+
51
+ // Act - select the middle line, type a comment, send it
52
+ await waitForText(setup, "return a + b");
53
+ await dragText(setup, "return a + b", "return a + b", "return a + b".length);
54
+ await typeText(setup, "why not reduce?");
55
+ await pressKey(setup, "RETURN", { meta: true });
56
+
57
+ // Assert - the surface built an anchor quoting the file line and handed it up with the body
58
+ expect(onAddComment).toHaveBeenCalled();
59
+ const [anchor, body] = onAddComment.mock.calls[0]!;
60
+ expect(anchor.quote).toContain("return a + b");
61
+ expect(body).toContain("why not reduce?");
62
+
63
+ setup.renderer.destroy();
64
+ });
65
+
66
+ test("a head-side (deleted-line) note never rebinds onto the current file", async () => {
67
+ const session = planSession();
68
+ // a note left on the removed side of this file: the worktree view must not show it
69
+ session.annotations = [
70
+ {
71
+ id: "h1",
72
+ kind: "comment",
73
+ anchor: { quote: "return a + b", prefix: "", suffix: "" },
74
+ target: { kind: "file", path: "src/add.ts", rev: "head" },
75
+ body: "deleted-side note",
76
+ createdAt: "2026-01-01T00:00:00Z",
77
+ },
78
+ ];
79
+ const setup = await testRender(
80
+ <AnnotatableFileView
81
+ path="src/add.ts"
82
+ loadContents={() => Promise.resolve(SAMPLE)}
83
+ session={session}
84
+ quickActions={[]}
85
+ observer={false}
86
+ onAddComment={noop}
87
+ onReply={noop}
88
+ onUpdateAnnotation={noop}
89
+ onExit={noop}
90
+ theme={DARK}
91
+ />,
92
+ { width: 64, height: 12 },
93
+ );
94
+
95
+ await waitForText(setup, "return a + b");
96
+
97
+ // Assert - the current file shows, but the head-side note is not attached to it
98
+ expect(setup.captureCharFrame()).not.toContain("deleted-side note");
99
+
100
+ setup.renderer.destroy();
101
+ });
102
+ });
@@ -0,0 +1,134 @@
1
+ // A project file shown as its own annotatable surface: the file's lines render through the diff
2
+ // sheet as context rows (one line-number gutter, no +/- sign), so selecting text leaves a comment
3
+ // anchored to the file exactly as a plan or diff comment is. Used where a thread exists to
4
+ // hold the notes; the bare-launch welcome shell uses the read-only FileContentsView instead.
5
+
6
+ import React, { useEffect, useRef, useState } from "react";
7
+ import {
8
+ makeAnchor,
9
+ annotationTarget,
10
+ type Anchor,
11
+ type Annotation,
12
+ type Thread,
13
+ } from "@cueloop/schema";
14
+ import type { Theme } from "../theme";
15
+ import type { QuickAction } from "../config";
16
+ import { useComponentTheme } from "./theme-context";
17
+ import { DiffContentView } from "./DiffContentView";
18
+ import { diffRowBlocks, fileContentsRows, marksByRows } from "../view-diff";
19
+
20
+ export interface AnnotatableFileViewProps {
21
+ path: string;
22
+ loadContents: (path: string) => Promise<string | null>;
23
+ session: Thread;
24
+ quickActions: QuickAction[];
25
+ observer: boolean;
26
+ resolved?: boolean;
27
+ suspended?: boolean;
28
+ focusedAnnotationId?: string;
29
+ onFocusAnnotation?: (annotationId: string | undefined) => void;
30
+ onComposingChange?: (composing: boolean) => void;
31
+ onCursorChange?: (rowIndex: number) => void;
32
+ onObserverBlocked?: (reason: "observer" | "resolved") => void;
33
+ /** Persist a comment on this file; the view built the anchor against the file's own lines. */
34
+ onAddComment: (anchor: Anchor, body: string) => void;
35
+ onReply: (rootAnnotationId: string, body: string) => void;
36
+ onUpdateAnnotation: (id: string, body: string) => void;
37
+ resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
38
+ onExit: () => void;
39
+ theme?: Theme;
40
+ }
41
+
42
+ /** A finished read for a specific path; `lines` is null when the file could not be read. */
43
+ interface FileLoad {
44
+ path: string;
45
+ lines: string[] | null;
46
+ }
47
+
48
+ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.ReactNode {
49
+ const tokens = useComponentTheme(props.theme);
50
+ const [loaded, setLoaded] = useState<FileLoad | null>(null);
51
+ const loadRef = useRef(props.loadContents);
52
+ useEffect(() => {
53
+ loadRef.current = props.loadContents;
54
+ });
55
+
56
+ useEffect(() => {
57
+ let alive = true;
58
+ void loadRef.current(props.path).then(
59
+ (contents) => {
60
+ if (alive)
61
+ setLoaded({ path: props.path, lines: contents === null ? null : contents.split("\n") });
62
+ },
63
+ () => {
64
+ if (alive) setLoaded({ path: props.path, lines: null });
65
+ },
66
+ );
67
+
68
+ return () => {
69
+ alive = false;
70
+ };
71
+ }, [props.path]);
72
+
73
+ if (loaded === null || loaded.path !== props.path) {
74
+ return (
75
+ <box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
76
+ <text fg={tokens.textDim}>loading...</text>
77
+ </box>
78
+ );
79
+ }
80
+ if (loaded.lines === null) {
81
+ return (
82
+ <box
83
+ style={{ flexGrow: 1, alignItems: "center", justifyContent: "center", paddingBottom: 2 }}
84
+ >
85
+ <text fg={tokens.textDim}>File deleted</text>
86
+ </box>
87
+ );
88
+ }
89
+
90
+ const rows = fileContentsRows(props.path, loaded.lines.join("\n"));
91
+ // the file's worktree notes: these rows are the current file, so a head-side (deleted-line) note
92
+ // must stay orphaned here rather than rebind to similar surviving text
93
+ const fileNotes = props.session.annotations.filter((annotation) => {
94
+ const target = annotationTarget(annotation);
95
+
96
+ return target.kind === "file" && target.path === props.path && target.rev === "worktree";
97
+ });
98
+
99
+ return (
100
+ <DiffContentView
101
+ rows={rows}
102
+ session={props.session}
103
+ marks={marksByRows(fileNotes, rows, props.focusedAnnotationId)}
104
+ quickActions={props.quickActions}
105
+ observer={props.observer}
106
+ commentsEnabled
107
+ resolved={props.resolved}
108
+ suspended={props.suspended}
109
+ fileView
110
+ onComposingChange={props.onComposingChange}
111
+ onObserverBlocked={props.onObserverBlocked}
112
+ onCursorChange={props.onCursorChange}
113
+ focusedAnnotationId={props.focusedAnnotationId}
114
+ onFocusAnnotation={props.onFocusAnnotation}
115
+ onAnnotate={(span, body) =>
116
+ props.onAddComment(
117
+ makeAnchor(
118
+ diffRowBlocks(rows),
119
+ span.start.blockIndex,
120
+ span.start.char,
121
+ span.end.char,
122
+ span.end.blockIndex,
123
+ ),
124
+ body,
125
+ )
126
+ }
127
+ onReply={props.onReply}
128
+ onUpdateAnnotation={props.onUpdateAnnotation}
129
+ resolveAuthorLabel={props.resolveAuthorLabel}
130
+ onExit={props.onExit}
131
+ theme={props.theme}
132
+ />
133
+ );
134
+ }
@@ -0,0 +1,86 @@
1
+ import React from "react";
2
+ import type { Annotation } from "@cueloop/schema";
3
+ import { DARK } from "../theme";
4
+ import { annotationPaletteFor } from "../annotation-palette";
5
+ import { slashItemsFrom } from "../slash-palette";
6
+ import { DEFAULT_QUICK_ACTIONS } from "../config";
7
+ import type { Story, StoryMeta } from "./story";
8
+ import { CommentRow, ComposerPalette, DiscussionCard } from "./AnnotationCards";
9
+
10
+ export const meta: StoryMeta = { title: "Chrome/AnnotationCards" };
11
+
12
+ const palette = annotationPaletteFor(DARK);
13
+
14
+ function annotation(id: string, body: string, author?: string): Annotation {
15
+ const comment: Annotation = {
16
+ id,
17
+ kind: "comment",
18
+ anchor: { quote: "the daemon", prefix: "", suffix: "" },
19
+ body,
20
+ createdAt: "2026-09-07T10:00:00.000Z",
21
+ };
22
+
23
+ if (author !== undefined) comment.author = author;
24
+
25
+ return comment;
26
+ }
27
+
28
+ // A two-voice discussion: the owner's comment then a collaborator's reply, each
29
+ // segment wearing its own accent edge.
30
+ export const TwoVoices: Story = {
31
+ render: () => (
32
+ <DiscussionCard
33
+ tokens={DARK}
34
+ segments={[
35
+ {
36
+ color: palette.cardEdge,
37
+ node: <CommentRow annotation={annotation("a", "Which daemon?")} tokens={DARK} />,
38
+ },
39
+ {
40
+ color: DARK.text,
41
+ node: (
42
+ <CommentRow
43
+ annotation={annotation("b", "The one the CLI spawns.", "fp:collab")}
44
+ tokens={DARK}
45
+ />
46
+ ),
47
+ },
48
+ ]}
49
+ />
50
+ ),
51
+ expectedColors: [palette.cardEdge, DARK.text],
52
+ size: { width: 60, height: 8 },
53
+ };
54
+
55
+ export const FocusedCard: Story = {
56
+ render: () => (
57
+ <DiscussionCard
58
+ focused
59
+ tokens={DARK}
60
+ segments={[
61
+ {
62
+ color: palette.cardEdge,
63
+ node: (
64
+ <CommentRow annotation={annotation("a", "Needs an eviction story.")} tokens={DARK} />
65
+ ),
66
+ },
67
+ ]}
68
+ />
69
+ ),
70
+ expectedColors: [DARK.text],
71
+ size: { width: 60, height: 6 },
72
+ };
73
+
74
+ // The palette a leading "/" opens under the composer, with the second item selected.
75
+ export const SlashPaletteOpen: Story = {
76
+ render: () => (
77
+ <ComposerPalette
78
+ slashActive
79
+ slashItems={slashItemsFrom(DEFAULT_QUICK_ACTIONS)}
80
+ slashIndex={1}
81
+ tokens={DARK}
82
+ />
83
+ ),
84
+ expectedColors: [DARK.accent, DARK.textDim],
85
+ size: { width: 90, height: 8 },
86
+ };