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

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,341 @@
1
+ /**
2
+ * The inline comment pieces every annotated surface renders: the composer (a
3
+ * textarea in a card row), the "/" actions-and-skills palette below it, the
4
+ * edge-segmented discussion card, and one comment row. The plan thread view
5
+ * and the diff sheet both draw their comments with these, so a discussion looks
6
+ * and behaves the same on prose and on code.
7
+ */
8
+
9
+ import React, { useContext, useLayoutEffect, useRef, useState } from "react";
10
+ import { usePaste } from "@opentui/react";
11
+ import type {
12
+ BoxRenderable,
13
+ KeyBinding,
14
+ MouseEvent as TerminalMouseEvent,
15
+ TextareaRenderable,
16
+ } from "@opentui/core";
17
+ import type { Annotation } from "@cueloop/schema";
18
+ import type { Theme } from "../theme";
19
+ import { useTooltip } from "./Tooltip";
20
+ import { lighten } from "../annotation-palette";
21
+ import { useFrameMeasure } from "../use-frame-measure";
22
+ import { imagePlaceholder, looksLikeBinaryPaste } from "../pasted-image";
23
+ import { skillReferenceRanges, type SlashItem } from "../slash-palette";
24
+ import { PaletteNamesContext } from "../skills";
25
+ import { referenceStyleFor } from "./syntax-highlight";
26
+
27
+ /* -------------------------------------------------------------- composer */
28
+
29
+ // cmd+enter sends (super under the kitty protocol, meta where cmd arrives
30
+ // ESC-prefixed, ctrl as the fallback where the terminal itself consumes
31
+ // cmd+enter); plain enter breaks the line
32
+ const COMPOSE_KEY_BINDINGS: KeyBinding[] = [
33
+ { name: "return", super: true, action: "submit" },
34
+ { name: "return", meta: true, action: "submit" },
35
+ { name: "return", ctrl: true, action: "submit" },
36
+ { name: "return", action: "newline" },
37
+ { name: "return", shift: true, action: "newline" },
38
+ ];
39
+
40
+ export function composeRowCount(text: string, contentWidth: number): number {
41
+ const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
42
+ let visualRowCount = 0;
43
+
44
+ for (const line of text.split("\n")) {
45
+ visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
46
+ }
47
+
48
+ // no cap: the card is the discussion, and folding is its containment
49
+ return Math.max(1, visualRowCount);
50
+ }
51
+
52
+ export function Composer({
53
+ seed,
54
+ glyph,
55
+ tokens,
56
+ onSave,
57
+ onReady,
58
+ onInput,
59
+ placeholder,
60
+ }: {
61
+ seed: string;
62
+ glyph: string;
63
+ tokens: Theme;
64
+ onSave: (body: string) => void;
65
+ onReady: () => void;
66
+ onInput: (text: string, caret: number) => void;
67
+ placeholder?: string;
68
+ }): React.ReactNode {
69
+ const editorRef = useRef<TextareaRenderable | null>(null);
70
+ const pastedImageCount = useRef(0);
71
+ const [rows, setRows] = useState(1);
72
+ // action and skill names whose "/name" references paint in the accent color
73
+ const referenceNames = useContext(PaletteNamesContext);
74
+
75
+ usePaste((event) => {
76
+ const editor = editorRef.current;
77
+
78
+ if (!editor || !editor.focused || !looksLikeBinaryPaste(event.bytes)) return;
79
+ event.preventDefault();
80
+ pastedImageCount.current += 1;
81
+ editor.editBuffer.insertText(imagePlaceholder(pastedImageCount.current));
82
+ });
83
+
84
+ const paintReferences = (editor: TextareaRenderable): void => {
85
+ const { styleId } = referenceStyleFor(tokens);
86
+
87
+ editor.editBuffer.clearAllHighlights();
88
+ for (const range of skillReferenceRanges(editor.plainText, referenceNames)) {
89
+ editor.editBuffer.addHighlightByCharRange({ ...range, styleId });
90
+ }
91
+ };
92
+
93
+ // synchronously at commit, before the next tty key is read: signalling readiness in a
94
+ // post-paint effect lets a fast burst race the mount, splitting input between the seed
95
+ // buffer and this textarea out of order (#365)
96
+ useLayoutEffect(() => {
97
+ const editor = editorRef.current;
98
+
99
+ if (!editor) return;
100
+ editor.cursorOffset = seed.length;
101
+ editor.editBuffer.setSyntaxStyle(referenceStyleFor(tokens).style);
102
+ paintReferences(editor);
103
+ setRows(composeRowCount(editor.plainText, editor.width));
104
+ onReady();
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, []);
107
+
108
+ return (
109
+ <box style={{ flexDirection: "row" }}>
110
+ <text selectable={false} fg={tokens.text} style={{ flexShrink: 0 }}>{`${glyph} `}</text>
111
+ <textarea
112
+ ref={editorRef}
113
+ focused
114
+ initialValue={seed}
115
+ placeholder={placeholder}
116
+ placeholderColor={tokens.textDim}
117
+ cursorStyle={{ style: "block", blinking: true }}
118
+ keyBindings={COMPOSE_KEY_BINDINGS}
119
+ onSubmit={() => onSave(editorRef.current?.plainText ?? "")}
120
+ onContentChange={() => {
121
+ const editor = editorRef.current;
122
+
123
+ if (!editor) return;
124
+ paintReferences(editor);
125
+ setRows(composeRowCount(editor.plainText, editor.width));
126
+ onInput(editor.plainText, editor.cursorOffset);
127
+ }}
128
+ style={{
129
+ height: rows,
130
+ flexGrow: 1,
131
+ backgroundColor: "transparent",
132
+ focusedBackgroundColor: "transparent",
133
+ textColor: tokens.text,
134
+ focusedTextColor: tokens.text,
135
+ }}
136
+ />
137
+ </box>
138
+ );
139
+ }
140
+
141
+ /* ---------------------------------------------------------- slash palette */
142
+
143
+ function SlashList({
144
+ items,
145
+ selected,
146
+ tokens,
147
+ }: {
148
+ items: SlashItem[];
149
+ selected: number;
150
+ tokens: Theme;
151
+ }): React.ReactNode {
152
+ const WINDOW = 5;
153
+ const start = Math.max(0, Math.min(selected - 2, items.length - WINDOW));
154
+ const visible = items.slice(start, start + WINDOW);
155
+
156
+ if (items.length === 0) return <text fg={tokens.textDim}>no matching actions</text>;
157
+
158
+ const nameColumn = Math.min(36, Math.max(16, ...visible.map((item) => item.name.length + 5)));
159
+
160
+ return (
161
+ <box style={{ flexDirection: "column" }}>
162
+ {visible.map((item, offset) => {
163
+ const index = start + offset;
164
+ const isSelected = index === selected;
165
+ const name = `${isSelected ? "→ " : " "}/${item.name}`.padEnd(nameColumn);
166
+
167
+ return (
168
+ <text key={item.name} style={{ flexShrink: 0, wrapMode: "none" }}>
169
+ <span fg={isSelected ? tokens.accent : tokens.textMuted}>{name}</span>
170
+ <span fg={tokens.textDim}>{item.description}</span>
171
+ </text>
172
+ );
173
+ })}
174
+ {items.length > WINDOW ? (
175
+ <text fg={tokens.textDim}>{`(${selected + 1}/${items.length})`}</text>
176
+ ) : null}
177
+ </box>
178
+ );
179
+ }
180
+
181
+ /** Below the composer: the actions/skills list while the caret is on a "/word", else nothing. */
182
+ export function ComposerPalette({
183
+ slashActive,
184
+ slashItems,
185
+ slashIndex,
186
+ tokens,
187
+ }: {
188
+ slashActive: boolean;
189
+ slashItems: SlashItem[];
190
+ slashIndex: number;
191
+ tokens: Theme;
192
+ }): React.ReactNode {
193
+ if (!slashActive) return null;
194
+
195
+ return (
196
+ <box style={{ flexDirection: "column", marginLeft: 3 }}>
197
+ <SlashList
198
+ items={slashItems}
199
+ selected={Math.min(slashIndex, Math.max(0, slashItems.length - 1))}
200
+ tokens={tokens}
201
+ />
202
+ </box>
203
+ );
204
+ }
205
+
206
+ /* ------------------------------------------------------------ discussion card */
207
+
208
+ /** One card row group with its own accent-edge color, one per voice. */
209
+ export interface EdgeSegment {
210
+ color: string;
211
+ node: React.ReactNode;
212
+ }
213
+
214
+ /**
215
+ * The comment-card frame. The accent edge is drawn by hand so that (a) its
216
+ * corner cells are half-height glyphs ("╷"/"╵") terminating flush with the
217
+ * hairline rows, and (b) each voice's rows carry their own edge color with
218
+ * zero gap. Focus lightens every segment; the hairlines never change.
219
+ */
220
+ export function DiscussionCard({
221
+ segments,
222
+ tokens,
223
+ focused = false,
224
+ onFocus,
225
+ }: {
226
+ segments: EdgeSegment[];
227
+ tokens: Theme;
228
+ focused?: boolean;
229
+ onFocus?: () => void;
230
+ }): React.ReactNode {
231
+ const segmentRefs = useRef<Array<BoxRenderable | null>>([]);
232
+ const noHeights: number[] = [];
233
+ const heights = useFrameMeasure(
234
+ () => segmentRefs.current.map((renderable) => renderable?.height ?? 0),
235
+ (left, right) => left.join() === right.join(),
236
+ noHeights,
237
+ );
238
+
239
+ const edgeColor = (color: string): string => (focused ? lighten(color) : color);
240
+ const glyphs = focused
241
+ ? { top: "╻", middle: "┃", bottom: "╹" }
242
+ : { top: "╷", middle: "│", bottom: "╵" };
243
+ const edgeRows: Array<{ glyph: string; color: string }> = [
244
+ {
245
+ glyph: glyphs.top,
246
+ color: edgeColor(segments[0]?.color ?? tokens.border),
247
+ },
248
+ ];
249
+
250
+ for (let index = 0; index < segments.length; index++) {
251
+ const segmentRows = Math.max(1, heights[index] ?? 1);
252
+
253
+ for (let row = 0; row < segmentRows; row++) {
254
+ edgeRows.push({
255
+ glyph: glyphs.middle,
256
+ color: edgeColor(segments[index]!.color),
257
+ });
258
+ }
259
+ }
260
+ edgeRows.push({
261
+ glyph: glyphs.bottom,
262
+ color: edgeColor(segments[segments.length - 1]?.color ?? tokens.border),
263
+ });
264
+
265
+ return (
266
+ <box style={{ flexDirection: "row", marginTop: 0, marginLeft: 2 }}>
267
+ <text selectable={false} style={{ flexShrink: 0, width: 1 }}>
268
+ {edgeRows.map((row, index) => (
269
+ <span key={index} fg={row.color}>
270
+ {index < edgeRows.length - 1 ? `${row.glyph}\n` : row.glyph}
271
+ </span>
272
+ ))}
273
+ </text>
274
+ <box
275
+ onMouseDown={onFocus}
276
+ style={{
277
+ flexGrow: 1,
278
+ flexDirection: "column",
279
+ border: ["top", "right", "bottom"],
280
+ borderStyle: "single",
281
+ borderColor: tokens.border,
282
+ backgroundColor: "transparent",
283
+ paddingLeft: 1,
284
+ paddingRight: 1,
285
+ }}
286
+ >
287
+ {segments.map((segment, index) => (
288
+ <box
289
+ key={index}
290
+ ref={(renderable: BoxRenderable | null) => {
291
+ segmentRefs.current[index] = renderable;
292
+ }}
293
+ style={{ flexDirection: "column" }}
294
+ >
295
+ {segment.node}
296
+ </box>
297
+ ))}
298
+ </box>
299
+ </box>
300
+ );
301
+ }
302
+
303
+ export function CommentRow({
304
+ annotation,
305
+ tokens,
306
+ authorLabel,
307
+ }: {
308
+ annotation: Annotation;
309
+ tokens: Theme;
310
+ /** The author's resolved display name, shown as a tooltip when the dot is hovered. */
311
+ authorLabel?: string;
312
+ }): React.ReactNode {
313
+ // own comments (no author) wear the filled dot, collaborators the outline
314
+ const own = annotation.author === undefined;
315
+ const glyph = own ? "●" : "○";
316
+ const glyphColor = own ? tokens.text : tokens.textMuted;
317
+ const { showTooltip, hideTooltip } = useTooltip();
318
+ const dotHover = authorLabel
319
+ ? {
320
+ onMouseOver: (event: TerminalMouseEvent) => showTooltip(authorLabel, event.x, event.y),
321
+ onMouseOut: hideTooltip,
322
+ }
323
+ : {};
324
+
325
+ return (
326
+ <box style={{ flexDirection: "column" }}>
327
+ {annotation.body.split("\n").map((line, lineIndex) => (
328
+ <box key={lineIndex} style={{ flexDirection: "row" }}>
329
+ <box style={{ flexShrink: 0 }} {...(lineIndex === 0 ? dotHover : {})}>
330
+ <text selectable={false} fg={glyphColor}>
331
+ {lineIndex === 0 ? `${glyph} ` : " "}
332
+ </text>
333
+ </box>
334
+ <text fg={tokens.text} style={{ wrapMode: "word", flexGrow: 1, flexShrink: 1 }}>
335
+ {line}
336
+ </text>
337
+ </box>
338
+ ))}
339
+ </box>
340
+ );
341
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import type { Story, StoryMeta } from "./story";
3
+ import { AnsiScreen } from "./AnsiScreen";
4
+
5
+ export const meta: StoryMeta = { title: "Primitives/AnsiScreen" };
6
+
7
+ const POSITIONED = "\x1b[2;4HHello from\x1b[4;6Hraw, positioned ANSI";
8
+
9
+ export const Positioned: Story = {
10
+ render: () => <AnsiScreen ansi={POSITIONED} cols={40} rows={6} />,
11
+ size: { width: 44, height: 8 },
12
+ };
@@ -0,0 +1,28 @@
1
+ /** Paints a raw ANSI screen (cursor-positioned text) as a terminal would, for previewing SSH surfaces. */
2
+
3
+ import React from "react";
4
+ import type { Theme } from "../theme";
5
+ import { ansiScreenToLines } from "../ansi-screen";
6
+ import { useComponentTheme } from "./theme-context";
7
+
8
+ export interface AnsiScreenProps {
9
+ ansi: string;
10
+ cols: number;
11
+ rows: number;
12
+ theme?: Theme;
13
+ }
14
+
15
+ export function AnsiScreen({ ansi, cols, rows, theme }: AnsiScreenProps): React.ReactNode {
16
+ const tokens = useComponentTheme(theme);
17
+ const lines = ansiScreenToLines(ansi, cols, rows);
18
+
19
+ return (
20
+ <box style={{ flexDirection: "column" }}>
21
+ {lines.map((line, index) => (
22
+ <text key={index} fg={tokens.text} selectable={false} style={{ wrapMode: "none" }}>
23
+ {line === "" ? " " : line}
24
+ </text>
25
+ ))}
26
+ </box>
27
+ );
28
+ }
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { AppHeader } from "./AppHeader";
5
+ import { Button } from "./primitives/Button";
6
+ import { Toolbar } from "./primitives/Toolbar";
7
+
8
+ export const meta: StoryMeta = { title: "Chrome/AppHeader" };
9
+
10
+ export const NoThread: Story = {
11
+ render: () => (
12
+ <AppHeader
13
+ onOpenMenu={() => {}}
14
+ sidebarOpen
15
+ onToggleSidebar={() => {}}
16
+ title=""
17
+ changesOpen={false}
18
+ onToggleChanges={() => {}}
19
+ theme={DARK}
20
+ />
21
+ ),
22
+ expectedColors: [DARK.accent],
23
+ size: { width: 80, height: 3 },
24
+ };
25
+
26
+ export const ThreadWithActions: Story = {
27
+ render: () => (
28
+ <AppHeader
29
+ onOpenMenu={() => {}}
30
+ sidebarOpen
31
+ onToggleSidebar={() => {}}
32
+ title="Review the accent change"
33
+ editShare={
34
+ <Toolbar>
35
+ <Button onPress={() => {}} theme={DARK}>
36
+ {" Edit "}
37
+ </Button>
38
+ <Button onPress={() => {}} theme={DARK}>
39
+ {" Share "}
40
+ </Button>
41
+ </Toolbar>
42
+ }
43
+ changesOpen
44
+ onToggleChanges={() => {}}
45
+ theme={DARK}
46
+ />
47
+ ),
48
+ expectedColors: [DARK.accent],
49
+ size: { width: 80, height: 3 },
50
+ };
@@ -0,0 +1,83 @@
1
+ // The one workbench header, split into column-aligned segments so it reads the
2
+ // same for every primitive: gear + left toggle over the Threads sidebar, the
3
+ // current thread's title (mirroring the sidebar selection) plus the owner's
4
+ // Edit/Share over the thread column, and a Changes label + right toggle over the
5
+ // right column. Collapsing a column keeps its toggle reachable at the edge.
6
+
7
+ import React from "react";
8
+ import type { Theme } from "../theme";
9
+ import { IconButton } from "./primitives/IconButton";
10
+ import { NERD } from "./primitives/icons";
11
+
12
+ export interface AppHeaderProps {
13
+ onOpenMenu: () => void;
14
+ sidebarOpen: boolean;
15
+ sidebarWidth?: number;
16
+ onToggleSidebar: () => void;
17
+ /** The center segment title; mirrors the selected sidebar thread, blank when none. */
18
+ title: string;
19
+ /** Owner actions rendered at the right edge of the thread segment (Edit/Share). */
20
+ editShare?: React.ReactNode;
21
+ changesOpen: boolean;
22
+ changesWidth?: number;
23
+ onToggleChanges: () => void;
24
+ theme?: Theme;
25
+ }
26
+
27
+ export function AppHeader({
28
+ onOpenMenu,
29
+ sidebarOpen,
30
+ sidebarWidth = 30,
31
+ onToggleSidebar,
32
+ title,
33
+ editShare,
34
+ changesOpen,
35
+ changesWidth = 32,
36
+ onToggleChanges,
37
+ theme,
38
+ }: AppHeaderProps): React.ReactNode {
39
+ return (
40
+ <box style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme?.panel }}>
41
+ {/* over the Threads sidebar: settings gear + the left toggle, product mark */}
42
+ <box
43
+ style={{
44
+ flexDirection: "row",
45
+ width: sidebarOpen ? sidebarWidth : undefined,
46
+ }}
47
+ >
48
+ <box onMouseUp={onOpenMenu} style={{ paddingRight: 2 }}>
49
+ <text fg={theme?.textMuted}>{NERD.settings}</text>
50
+ </box>
51
+ <IconButton
52
+ glyph={sidebarOpen ? NERD.sidebarLeft : NERD.sidebarLeftOff}
53
+ onPress={onToggleSidebar}
54
+ marginRight={2}
55
+ theme={theme}
56
+ />
57
+ <text fg={theme?.accent}>cueloop</text>
58
+ </box>
59
+ {/* over the thread column: the title, then the owner's Edit/Share at its right edge */}
60
+ <box style={{ flexDirection: "row", flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}>
61
+ <text fg={theme?.textDim}>{title}</text>
62
+ <box style={{ flexGrow: 1 }} />
63
+ {editShare}
64
+ </box>
65
+ {/* over the right column: the Changes label when open, the right toggle always at the edge */}
66
+ <box
67
+ style={{
68
+ flexDirection: "row",
69
+ paddingRight: 1,
70
+ width: changesOpen ? changesWidth : undefined,
71
+ }}
72
+ >
73
+ {changesOpen ? <text fg={theme?.textDim}>Changes</text> : null}
74
+ <box style={{ flexGrow: 1 }} />
75
+ <IconButton
76
+ glyph={changesOpen ? NERD.sidebarRight : NERD.sidebarRightOff}
77
+ onPress={onToggleChanges}
78
+ theme={theme}
79
+ />
80
+ </box>
81
+ </box>
82
+ );
83
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The collapsed-sidebar brand row keeps the gear, the Threads toggle, and the
3
+ * "cueloop" mark on one line. A long thread title used to shrink the brand box
4
+ * until "cueloop" and the toggle wrapped onto the header's underline row; the
5
+ * title truncates instead, so the brand never shrinks.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { testRender } from "@opentui/react/test-utils";
10
+ import React from "react";
11
+ import { AppShell } from "./AppShell";
12
+ import { NERD } from "./primitives/icons";
13
+ import { settle } from "../test-support";
14
+
15
+ const LONG_TITLE = "working tree @ GRO-1981-fe-billing-promo-cookie-migration";
16
+
17
+ async function brandFrame(): Promise<string[]> {
18
+ const setup = await testRender(
19
+ <AppShell
20
+ sidebarOpen={false}
21
+ onToggleSidebar={() => {}}
22
+ onOpenMenu={() => {}}
23
+ threadsPanel={<text>threads</text>}
24
+ threadTitle={LONG_TITLE}
25
+ threadPanel={<text>Select a thread</text>}
26
+ changesOpen
27
+ projectOpen
28
+ onToggleChanges={() => {}}
29
+ onToggleProject={() => {}}
30
+ onToggleRight={() => {}}
31
+ projectMode="changes"
32
+ projectPanel={<text>project</text>}
33
+ changesPanel={<text>diff</text>}
34
+ />,
35
+ { width: 160, height: 12 },
36
+ );
37
+
38
+ await settle(setup);
39
+ await settle(setup);
40
+ const lines = setup.captureCharFrame().split("\n");
41
+
42
+ setup.renderer.destroy();
43
+
44
+ return lines;
45
+ }
46
+
47
+ describe("collapsed-sidebar brand row", () => {
48
+ test("the Threads toggle stays on the cueloop row and the title tails off in an ellipsis", async () => {
49
+ const lines = await brandFrame();
50
+ const brandRow = lines.findIndex((line) => line.includes("cueloop"));
51
+ const toggleRow = lines.findIndex((line) => line.includes(NERD.sidebarLeftOff));
52
+
53
+ expect(brandRow).toBeGreaterThanOrEqual(0);
54
+ expect(toggleRow).toBe(brandRow);
55
+ expect(lines[brandRow]).toContain("…");
56
+ expect(lines[brandRow]).not.toContain(LONG_TITLE);
57
+ });
58
+ });
@@ -0,0 +1,89 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { AppShell } from "./AppShell";
5
+
6
+ export const meta: StoryMeta = { title: "Layout/AppShell" };
7
+
8
+ function Threads(): React.ReactNode {
9
+ return (
10
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
11
+ <text fg={DARK.textDim}>Pinned</text>
12
+ <text fg={DARK.accent}> Read Cueloop Repository</text>
13
+ <text> </text>
14
+ <text fg={DARK.textDim}>Threads</text>
15
+ <text fg={DARK.textMuted}> A standalone thought</text>
16
+ </box>
17
+ );
18
+ }
19
+
20
+ function Thread(): React.ReactNode {
21
+ return (
22
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
23
+ <text fg={DARK.text}>the conversation renders here</text>
24
+ </box>
25
+ );
26
+ }
27
+
28
+ function Changes(): React.ReactNode {
29
+ return (
30
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
31
+ <text fg={DARK.textMuted}>the editor grid renders here</text>
32
+ </box>
33
+ );
34
+ }
35
+
36
+ function Project(): React.ReactNode {
37
+ return (
38
+ <box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
39
+ <text fg={DARK.textDim}>No changes</text>
40
+ </box>
41
+ );
42
+ }
43
+
44
+ const noop = (): void => {};
45
+
46
+ export const FourPane: Story = {
47
+ render: () => (
48
+ <AppShell
49
+ sidebarOpen
50
+ onToggleSidebar={noop}
51
+ onOpenMenu={noop}
52
+ threadsPanel={<Threads />}
53
+ threadTitle="Read Cueloop Repository"
54
+ threadPanel={<Thread />}
55
+ changesOpen
56
+ projectOpen
57
+ onToggleChanges={noop}
58
+ onToggleProject={noop}
59
+ onToggleRight={noop}
60
+ projectMode="changes"
61
+ changesPanel={<Changes />}
62
+ projectPanel={<Project />}
63
+ />
64
+ ),
65
+ expectedColors: [DARK.border, DARK.accent],
66
+ size: { width: 130, height: 28 },
67
+ };
68
+
69
+ export const CollapsedRightRail: Story = {
70
+ render: () => (
71
+ <AppShell
72
+ sidebarOpen
73
+ onToggleSidebar={noop}
74
+ onOpenMenu={noop}
75
+ threadsPanel={<Threads />}
76
+ threadTitle="Read Cueloop Repository"
77
+ threadPanel={<Thread />}
78
+ changesOpen={false}
79
+ projectOpen={false}
80
+ onToggleChanges={noop}
81
+ onToggleProject={noop}
82
+ onToggleRight={noop}
83
+ projectMode="tree"
84
+ projectPanel={<Project />}
85
+ />
86
+ ),
87
+ expectedColors: [DARK.border],
88
+ size: { width: 100, height: 22 },
89
+ };