@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90

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 (316) 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 +42 -34
  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 +259 -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 +330 -161
  16. package/src/App.prototype-markdown.test.tsx +128 -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 +264 -0
  23. package/src/App.test.tsx +402 -126
  24. package/src/App.toast.test.tsx +98 -0
  25. package/src/App.tsx +1402 -1060
  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 +774 -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 +577 -0
  35. package/src/app-view-model.ts +168 -0
  36. package/src/attribution.test.ts +31 -0
  37. package/src/attribution.ts +42 -0
  38. package/src/client-extension-registry.test.tsx +61 -0
  39. package/src/client-extension-registry.ts +159 -0
  40. package/src/clipboard.test.ts +23 -0
  41. package/src/clipboard.ts +33 -0
  42. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  43. package/src/components/AnnotatableFileView.test.tsx +102 -0
  44. package/src/components/AnnotatableFileView.tsx +134 -0
  45. package/src/components/AnnotationCards.stories.tsx +86 -0
  46. package/src/components/AnnotationCards.tsx +341 -0
  47. package/src/components/AnsiScreen.stories.tsx +12 -0
  48. package/src/components/AnsiScreen.tsx +28 -0
  49. package/src/components/AppHeader.stories.tsx +50 -0
  50. package/src/components/AppHeader.tsx +83 -0
  51. package/src/components/AppShell.brand.test.tsx +58 -0
  52. package/src/components/AppShell.extensions.test.tsx +191 -0
  53. package/src/components/AppShell.stories.tsx +89 -0
  54. package/src/components/AppShell.tsx +362 -0
  55. package/src/components/AppShell.zoom.test.tsx +135 -0
  56. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  57. package/src/components/BareWorkbenchFileView.tsx +64 -0
  58. package/src/components/Breadcrumb.stories.tsx +19 -0
  59. package/src/components/Breadcrumb.tsx +45 -0
  60. package/src/components/ChangesColumn.stories.tsx +44 -0
  61. package/src/components/ChangesColumn.tsx +199 -0
  62. package/src/components/ChangesFileTree.test.tsx +61 -0
  63. package/src/components/CodeBlock.stories.tsx +42 -0
  64. package/src/components/CodeBlock.tsx +84 -0
  65. package/src/components/CompletionOverlay.stories.tsx +37 -0
  66. package/src/components/CompletionOverlay.tsx +77 -0
  67. package/src/components/ConfirmCard.stories.tsx +37 -0
  68. package/src/components/ConfirmCard.tsx +150 -0
  69. package/src/components/ConfirmDialog.stories.tsx +21 -0
  70. package/src/components/ConfirmDialog.tsx +72 -0
  71. package/src/components/DiffContentView.scroll.test.tsx +149 -0
  72. package/src/components/DiffContentView.split.test.tsx +140 -0
  73. package/src/components/DiffContentView.stories.tsx +80 -0
  74. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  75. package/src/components/DiffContentView.tsx +906 -0
  76. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  77. package/src/components/DiscussionMarkerRail.tsx +186 -0
  78. package/src/components/EditorGrid.stories.tsx +59 -0
  79. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  80. package/src/components/EditorGrid.tsx +426 -0
  81. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  82. package/src/components/ExtensionZone.stories.tsx +28 -0
  83. package/src/components/ExtensionZone.tsx +145 -0
  84. package/src/components/FileContentsView.stories.tsx +16 -0
  85. package/src/components/FileContentsView.test.tsx +29 -0
  86. package/src/components/FileContentsView.tsx +98 -0
  87. package/src/components/GridTabContent.stories.tsx +59 -0
  88. package/src/components/GridTabContent.tsx +182 -0
  89. package/src/components/HandleChipsInput.stories.tsx +24 -0
  90. package/src/components/HandleChipsInput.test.tsx +35 -0
  91. package/src/components/HandleChipsInput.tsx +145 -0
  92. package/src/components/KeybindsDialog.stories.tsx +31 -0
  93. package/src/components/KeybindsDialog.tsx +103 -0
  94. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  95. package/src/components/MarkdownGridBlock.tsx +72 -0
  96. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  97. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  98. package/src/components/MarkdownThreadEditor.tsx +162 -0
  99. package/src/components/MenuBar.stories.tsx +28 -0
  100. package/src/components/MenuBar.tsx +44 -0
  101. package/src/components/NavModeHint.stories.tsx +40 -0
  102. package/src/components/NavModeHint.tsx +31 -0
  103. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  104. package/src/components/OverlayScrollbar.test.tsx +139 -0
  105. package/src/components/OverlayScrollbar.tsx +129 -0
  106. package/src/components/PaneGrid.stories.tsx +84 -0
  107. package/src/components/PaneGrid.tsx +128 -0
  108. package/src/components/PanelColumn.stories.tsx +25 -0
  109. package/src/components/PanelColumn.tsx +98 -0
  110. package/src/components/ProjectTreeView.stories.tsx +25 -0
  111. package/src/components/ProjectTreeView.test.tsx +75 -0
  112. package/src/components/ProjectTreeView.tsx +125 -0
  113. package/src/components/PromptDialog.stories.tsx +36 -0
  114. package/src/components/PromptDialog.test.tsx +61 -0
  115. package/src/components/PromptDialog.tsx +99 -0
  116. package/src/components/PrototypeContentView.stories.tsx +19 -0
  117. package/src/components/PrototypeContentView.tsx +446 -0
  118. package/src/components/RootOverlay.stories.tsx +45 -0
  119. package/src/components/RootOverlay.tsx +48 -0
  120. package/src/components/ScrollArea.stories.tsx +20 -0
  121. package/src/components/ScrollArea.tsx +44 -0
  122. package/src/components/SettingsDialog.stories.tsx +117 -0
  123. package/src/components/SettingsDialog.tsx +186 -0
  124. package/src/components/SettingsRows.stories.tsx +18 -0
  125. package/src/components/SettingsRows.tsx +112 -0
  126. package/src/components/ShareDialog.stories.tsx +121 -0
  127. package/src/components/ShareDialog.tsx +552 -0
  128. package/src/components/ShellHeader.stories.tsx +50 -0
  129. package/src/components/ShellHeader.tsx +137 -0
  130. package/src/components/SlashComposer.stories.tsx +37 -0
  131. package/src/components/SlashComposer.tsx +87 -0
  132. package/src/components/ThreadFooter.stories.tsx +30 -0
  133. package/src/components/ThreadFooter.test.tsx +47 -0
  134. package/src/components/ThreadFooter.tsx +76 -0
  135. package/src/components/ThreadTree.stories.tsx +46 -0
  136. package/src/components/ThreadTree.tsx +255 -0
  137. package/src/components/ThreadView.selection.test.tsx +951 -0
  138. package/src/components/ThreadView.slash.test.tsx +146 -0
  139. package/src/components/ThreadView.stories.tsx +26 -0
  140. package/src/components/ThreadView.tsx +525 -0
  141. package/src/components/Toast.stories.tsx +19 -0
  142. package/src/components/Toast.tsx +52 -0
  143. package/src/components/Tooltip.stories.tsx +26 -0
  144. package/src/components/Tooltip.tsx +72 -0
  145. package/src/components/TreePane.stories.tsx +106 -0
  146. package/src/components/TreePane.tsx +157 -0
  147. package/src/components/WalkWizard.stories.tsx +100 -0
  148. package/src/components/WalkWizard.tsx +150 -0
  149. package/src/components/WelcomePlayground.stories.tsx +12 -0
  150. package/src/components/WelcomePlayground.test.tsx +49 -0
  151. package/src/components/WelcomePlayground.tsx +139 -0
  152. package/src/components/WorkspacePanels.stories.tsx +36 -0
  153. package/src/components/WorkspacePanels.tsx +116 -0
  154. package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
  155. package/src/components/diff-content-view-layout.test.ts +198 -0
  156. package/src/components/diff-content-view-layout.ts +84 -0
  157. package/src/components/editor-grid.test.ts +86 -0
  158. package/src/components/editor-grid.ts +138 -0
  159. package/src/components/file-tree.test.ts +60 -0
  160. package/src/components/file-tree.ts +112 -0
  161. package/src/components/menu-control.stories.tsx +35 -0
  162. package/src/components/menu-control.tsx +43 -0
  163. package/src/components/pages/Workspace.stories.tsx +181 -0
  164. package/src/components/primitives/Button.stories.tsx +32 -0
  165. package/src/components/primitives/Button.tsx +59 -0
  166. package/src/components/primitives/Card.stories.tsx +25 -0
  167. package/src/components/primitives/Card.tsx +63 -0
  168. package/src/components/primitives/Dialog.stories.tsx +24 -0
  169. package/src/components/primitives/Dialog.test.tsx +37 -0
  170. package/src/components/primitives/Dialog.tsx +71 -0
  171. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  172. package/src/components/primitives/DialogActions.tsx +39 -0
  173. package/src/components/primitives/IconButton.stories.tsx +36 -0
  174. package/src/components/primitives/IconButton.tsx +62 -0
  175. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  176. package/src/components/primitives/StatusBar.tsx +23 -0
  177. package/src/components/primitives/Switch.stories.tsx +17 -0
  178. package/src/components/primitives/Switch.tsx +32 -0
  179. package/src/components/primitives/Tabs.stories.tsx +29 -0
  180. package/src/components/primitives/Tabs.tsx +102 -0
  181. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  182. package/src/components/primitives/Toolbar.tsx +14 -0
  183. package/src/components/primitives/Tree.stories.tsx +124 -0
  184. package/src/components/primitives/Tree.tsx +100 -0
  185. package/src/components/primitives/frame.test.ts +12 -0
  186. package/src/components/primitives/frame.ts +17 -0
  187. package/src/components/primitives/icons.ts +61 -0
  188. package/src/components/primitives/tree-model.test.ts +99 -0
  189. package/src/components/primitives/tree-model.ts +137 -0
  190. package/src/components/quick-actions-editor.stories.tsx +58 -0
  191. package/src/components/quick-actions-editor.test.tsx +117 -0
  192. package/src/components/quick-actions-editor.tsx +226 -0
  193. package/src/components/share-dialog-store.ts +58 -0
  194. package/src/components/stories-app.tsx +193 -0
  195. package/src/components/stories.test.tsx +74 -0
  196. package/src/components/story-fixtures.ts +102 -0
  197. package/src/components/story-tree.test.ts +108 -0
  198. package/src/components/story.ts +159 -0
  199. package/src/components/syntax-highlight.ts +133 -0
  200. package/src/components/terminal-pane.test.tsx +40 -0
  201. package/src/components/terminal-pane.ts +199 -0
  202. package/src/components/theme-context.stories.tsx +46 -0
  203. package/src/components/theme-context.tsx +37 -0
  204. package/src/components/thread-tree.test.ts +134 -0
  205. package/src/components/thread-tree.ts +109 -0
  206. package/src/components/truncate-text.ts +7 -0
  207. package/src/components/truncate-title.test.ts +31 -0
  208. package/src/components/truncate-title.ts +15 -0
  209. package/src/config-identity.test.ts +78 -0
  210. package/src/config.test.ts +579 -34
  211. package/src/config.ts +533 -57
  212. package/src/diff-fold.test.ts +78 -0
  213. package/src/diff-fold.ts +126 -0
  214. package/src/diff-hunk-curate.test.ts +315 -0
  215. package/src/diff-hunk-curate.ts +153 -0
  216. package/src/diff-intraline.test.ts +238 -0
  217. package/src/diff-intraline.ts +236 -0
  218. package/src/diff-syntax.test.ts +112 -0
  219. package/src/diff-syntax.ts +179 -0
  220. package/src/discussions.ts +79 -0
  221. package/src/editor.test.ts +149 -0
  222. package/src/editor.ts +139 -9
  223. package/src/ghostty-terminal.test.ts +85 -0
  224. package/src/ghostty-terminal.ts +199 -0
  225. package/src/github-identity.test.ts +58 -0
  226. package/src/github-identity.ts +41 -0
  227. package/src/herdr.test.ts +9 -34
  228. package/src/herdr.ts +25 -21
  229. package/src/index.ts +28 -0
  230. package/src/integrations.test.ts +41 -0
  231. package/src/integrations.ts +32 -0
  232. package/src/intent-dispatch.test.ts +724 -0
  233. package/src/intent-dispatch.ts +690 -0
  234. package/src/key-bindings.test.ts +156 -0
  235. package/src/key-bindings.ts +459 -0
  236. package/src/keymap.test.ts +456 -115
  237. package/src/keymap.ts +315 -81
  238. package/src/kitty-image.test.ts +66 -0
  239. package/src/kitty-image.ts +138 -0
  240. package/src/launch-layout.test.ts +67 -0
  241. package/src/launch-layout.ts +47 -0
  242. package/src/mark-runs.ts +121 -0
  243. package/src/markdown-editor-syntax-style.ts +50 -0
  244. package/src/markdown-grid.test.ts +101 -0
  245. package/src/markdown-grid.ts +203 -0
  246. package/src/markdown-highlight.test.ts +61 -0
  247. package/src/markdown-highlight.ts +122 -0
  248. package/src/pasted-image.test.ts +67 -0
  249. package/src/pasted-image.ts +60 -0
  250. package/src/perf/otlp-trace.test.ts +116 -0
  251. package/src/perf/otlp-trace.ts +130 -0
  252. package/src/perf/perf-report.ts +17 -0
  253. package/src/perf/perf-timings.test.ts +18 -0
  254. package/src/perf/perf-timings.ts +38 -0
  255. package/src/prototype-browser.test.ts +81 -0
  256. package/src/prototype-browser.ts +329 -0
  257. package/src/prototype-pixels.tsx +35 -0
  258. package/src/pty.test.ts +81 -0
  259. package/src/pty.ts +245 -0
  260. package/src/ready-signal.test.ts +85 -0
  261. package/src/ready-signal.ts +52 -0
  262. package/src/run.ts +85 -12
  263. package/src/serve.test.ts +24 -12
  264. package/src/serve.ts +52 -10
  265. package/src/share.ts +237 -0
  266. package/src/skills.test.ts +74 -0
  267. package/src/skills.ts +75 -0
  268. package/src/slash-palette.test.ts +103 -0
  269. package/src/slash-palette.ts +129 -0
  270. package/src/split-diff.test.ts +110 -0
  271. package/src/split-diff.ts +149 -0
  272. package/src/test-support.test.ts +264 -0
  273. package/src/test-support.ts +369 -0
  274. package/src/theme-presets.test.ts +51 -0
  275. package/src/theme-presets.ts +219 -0
  276. package/src/theme.ts +88 -19
  277. package/src/thread-chords.test.ts +79 -0
  278. package/src/thread-chords.ts +163 -0
  279. package/src/thread-code-syntax.ts +45 -0
  280. package/src/thread-controller.curate.test.ts +476 -0
  281. package/src/thread-controller.derivation.test.ts +62 -0
  282. package/src/thread-controller.live-diff.test.ts +322 -0
  283. package/src/thread-controller.optimistic.test.ts +69 -0
  284. package/src/thread-controller.share.test.ts +561 -0
  285. package/src/thread-controller.switch-cache.test.ts +176 -0
  286. package/src/thread-controller.tree.test.ts +261 -0
  287. package/src/thread-controller.ts +2050 -0
  288. package/src/thread-selection.test.ts +210 -0
  289. package/src/thread-selection.ts +243 -0
  290. package/src/tree-view.test.ts +128 -0
  291. package/src/tree-view.ts +153 -0
  292. package/src/use-annotation-surface.tsx +975 -0
  293. package/src/use-changes-workbench.ts +190 -0
  294. package/src/use-frame-measure.ts +41 -0
  295. package/src/use-remember-layout.ts +31 -0
  296. package/src/use-repo-changes.ts +87 -0
  297. package/src/use-settings-dialog.tsx +364 -0
  298. package/src/use-terminal-virtualizer.ts +259 -0
  299. package/src/use-thread-body-editing.tsx +58 -0
  300. package/src/version.ts +5 -0
  301. package/src/view-diff.test.ts +279 -9
  302. package/src/view-diff.ts +460 -24
  303. package/src/view-plan.regression.test.ts +60 -0
  304. package/src/view-plan.test.ts +545 -0
  305. package/src/view-plan.ts +1019 -0
  306. package/src/visual/contrast.test.ts +35 -0
  307. package/src/visual/contrast.ts +33 -0
  308. package/src/walk.test.ts +121 -0
  309. package/src/walk.ts +74 -0
  310. package/src/workbench-snapshot.test.ts +99 -0
  311. package/src/workbench-snapshot.ts +59 -0
  312. package/src/session-controller.ts +0 -398
  313. package/src/syntax.test.ts +0 -41
  314. package/src/syntax.ts +0 -88
  315. package/src/view.test.ts +0 -157
  316. package/src/view.ts +0 -364
@@ -1,14 +1,14 @@
1
- /** Plan rendering (#67): code blocks verbatim + contained, spacing never collapses. */
1
+ /** Plan rendering: code blocks verbatim + contained, spacing never collapses. */
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
  import { mkdtempSync, rmSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import React from "react";
8
- import { testRender } from "@opentui/react/test-utils";
9
8
  import { DaemonServer } from "@cueloop/daemon";
10
- import type { ReviewSession } from "@cueloop/schema";
9
+ import type { Thread } from "@cueloop/schema";
11
10
  import { App } from "./App";
11
+ import { isolateUserConfig, waitForText, renderReadyApp } from "./test-support";
12
12
 
13
13
  const PLAN = `# Render Plan
14
14
 
@@ -29,11 +29,13 @@ Deploys block below the threshold.
29
29
  `;
30
30
 
31
31
  let home: string;
32
+ let restoreUserConfig: () => void;
32
33
  let server: DaemonServer;
33
- let session: ReviewSession;
34
+ let session: Thread;
34
35
 
35
36
  beforeEach(() => {
36
37
  home = mkdtempSync(join(tmpdir(), "cueloop-render-"));
38
+ restoreUserConfig = isolateUserConfig(home);
37
39
  server = new DaemonServer({ home, idleExitMs: 0 });
38
40
  server.start();
39
41
  session = server.core.sessionCreate({
@@ -42,67 +44,91 @@ beforeEach(() => {
42
44
  });
43
45
  });
44
46
  afterEach(() => {
47
+ restoreUserConfig();
45
48
  server.stop();
46
49
  rmSync(home, { recursive: true, force: true });
47
50
  });
48
51
 
49
52
  async function frame(): Promise<string> {
50
- const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 36 });
51
- const deadline = Date.now() + 10_000;
52
- while (Date.now() < deadline && !setup.captureCharFrame().includes("Render Plan")) {
53
- await Bun.sleep(25);
54
- await setup.renderOnce();
55
- }
56
- // give the async highlighter a beat, then take the final frame
57
- await Bun.sleep(150);
58
- await setup.renderOnce();
53
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
54
+ width: 120,
55
+ height: 36,
56
+ });
57
+
58
+ await waitForText(setup, "Render Plan");
59
+ // async highlights settle within the visual-idle wait
60
+ await setup.waitForVisualIdle();
61
+
59
62
  return setup.captureCharFrame();
60
63
  }
61
64
 
62
65
  describe("code blocks", () => {
63
66
  test("lines render verbatim: indentation preserved, no word-wrap", async () => {
64
- const f = await frame();
65
- expect(f).toContain(" const blocked = full < 0.6;");
66
- expect(f).toContain(' return <text>{blocked ? "blocked" : "clear"}</text>;');
67
+ // Arrange
68
+ const rendered = await frame();
69
+
70
+ // Assert
71
+ expect(rendered).toContain(" const blocked = full < 0.6;");
72
+ expect(rendered).toContain(' return <text>{blocked ? "blocked" : "clear"}</text>;');
67
73
  });
68
74
 
69
75
  test("the block carries its language tag", async () => {
70
- const f = await frame();
71
- expect(f).toContain("tsx");
76
+ // Arrange
77
+ const rendered = await frame();
78
+
79
+ // Assert
80
+ expect(rendered).toContain("tsx");
72
81
  });
73
82
  });
74
83
 
75
84
  describe("block spacing", () => {
85
+ // the review rail paints box edges on the right, so read the document
86
+ // column (everything left of the first rail glyph) before asserting spacing
87
+ const planColumn = (line: string): string => line.split(/[│╭╰]/)[0]!.trimEnd();
88
+
76
89
  test("a code block never glues to the list item above it", async () => {
77
- const lines = (await frame()).split("\n").map((l) => l.trimEnd());
78
- const li = lines.findIndex((l) => l.includes("wire the duck over MQTT"));
79
- const codeHeader = lines.findIndex((l) => l.trim().startsWith("tsx"));
80
- expect(li).toBeGreaterThan(-1);
81
- expect(codeHeader).toBeGreaterThan(li);
82
- expect(lines.slice(li + 1, codeHeader).some((l) => l === "")).toBe(true);
90
+ // Arrange
91
+ const lines = (await frame()).split("\n").map(planColumn);
92
+ const listItemLineIndex = lines.findIndex((line) => line.includes("wire the duck over MQTT"));
93
+ const codeHeader = lines.findIndex((line) => line.trim().startsWith("tsx"));
94
+
95
+ // Assert
96
+ expect(listItemLineIndex).toBeGreaterThan(-1);
97
+ expect(codeHeader).toBeGreaterThan(listItemLineIndex);
98
+ expect(lines.slice(listItemLineIndex + 1, codeHeader).some((line) => line === "")).toBe(true);
83
99
  });
84
100
 
85
101
  test("a heading never sits directly on the previous block", async () => {
86
- const lines = (await frame()).split("\n").map((l) => l.trimEnd());
87
- const gate = lines.findIndex((l) => l.trim() === "Gate");
102
+ // Arrange
103
+ const lines = (await frame()).split("\n").map(planColumn);
104
+ const gate = lines.findIndex((line) => line.trim() === "Gate");
105
+
106
+ // Assert
88
107
  expect(gate).toBeGreaterThan(0);
89
108
  expect(lines[gate - 1]).toBe("");
90
109
  });
91
110
 
92
111
  test("consecutive list items stay tight", async () => {
112
+ // Arrange
93
113
  const listPlan = "# T\n\n- one\n- two\n";
94
114
  const s2 = server.core.sessionCreate({
95
115
  workspace: { repoRoot: "/repo", branch: "main" },
96
116
  artifact: { type: "plan", content: listPlan, meta: {} },
97
117
  });
98
- const setup = await testRender(<App home={home} sessionId={s2.id} />, { width: 120, height: 30 });
99
- const deadline = Date.now() + 10_000;
100
- while (Date.now() < deadline && !setup.captureCharFrame().includes("one")) {
101
- await Bun.sleep(25);
102
- await setup.renderOnce();
103
- }
104
- const lines = setup.captureCharFrame().split("\n").map((l) => l.trimEnd());
105
- const one = lines.findIndex((l) => l.includes("- one"));
106
- expect(lines[one + 1]).toContain("- two");
118
+ const setup = await renderReadyApp(<App home={home} sessionId={s2.id} />, {
119
+ width: 120,
120
+ height: 30,
121
+ });
122
+
123
+ await waitForText(setup, "one");
124
+
125
+ // Assert
126
+ const lines = setup
127
+ .captureCharFrame()
128
+ .split("\n")
129
+ .map((line) => line.trimEnd());
130
+ const one = lines.findIndex((line) => line.includes("· one"));
131
+
132
+ expect(lines[one + 1]).toContain("· two");
107
133
  });
108
134
  });
@@ -0,0 +1,172 @@
1
+ /** The share shortcut opens the two-column share dialog reachable from any view. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import type { Thread } from "@cueloop/schema";
11
+ import { App } from "./App";
12
+ import type { ShareTransport } from "./thread-controller";
13
+ import {
14
+ isolateUserConfig,
15
+ press,
16
+ pressKey,
17
+ settle,
18
+ waitForText,
19
+ waitForTextGone,
20
+ } from "./test-support";
21
+
22
+ const shareTransport: ShareTransport = {
23
+ publish: mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true })),
24
+ pull: mock(async () => {
25
+ throw new Error("Unexpected share pull");
26
+ }),
27
+ push: mock(async () => {}),
28
+ watch: () => () => {},
29
+ revoke: async () => {},
30
+ parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
31
+ formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
32
+ collaboratorAnnotations: () => [],
33
+ mergeFromShare: () => ({ annotations: [] }),
34
+ };
35
+
36
+ const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
37
+
38
+ let home: string;
39
+ let restoreUserConfig: () => void;
40
+ let server: DaemonServer;
41
+ let session: Thread;
42
+
43
+ beforeEach(() => {
44
+ home = mkdtempSync(join(tmpdir(), "cueloop-share-choice-"));
45
+ restoreUserConfig = isolateUserConfig(home);
46
+ server = new DaemonServer({ home, idleExitMs: 0 });
47
+ server.start();
48
+ session = server.core.sessionCreate({
49
+ workspace: { repoRoot: "/repo", branch: "main" },
50
+ artifact: {
51
+ type: "plan",
52
+ content: PLAN,
53
+ meta: { title: "Migration Plan", planPath: "plan.md" },
54
+ },
55
+ });
56
+ });
57
+ afterEach(() => {
58
+ restoreUserConfig();
59
+ server.stop();
60
+ rmSync(home, { recursive: true, force: true });
61
+ });
62
+
63
+ describe("share dialog", () => {
64
+ test("the shortcut opens the two-column share dialog", async () => {
65
+ // Arrange
66
+ const setup = await testRender(
67
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
68
+ { width: 120, height: 32 },
69
+ );
70
+ await waitForText(setup, "cueloop");
71
+
72
+ // Act
73
+ await pressKey(setup, "s", { ctrl: true });
74
+
75
+ // Assert - the nav categories and the empty-state new-link affordance
76
+ await waitForText(setup, "share externally");
77
+ await waitForText(setup, "export");
78
+ await waitForText(setup, "+ new link");
79
+ });
80
+
81
+ test("new link opens the wizard, and require auth reveals the allowlist", async () => {
82
+ // Arrange
83
+ const setup = await testRender(
84
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
85
+ { width: 120, height: 32 },
86
+ );
87
+ await waitForText(setup, "cueloop");
88
+
89
+ // Act: open the dialog, step into the body, activate "+ new link"
90
+ await pressKey(setup, "s", { ctrl: true });
91
+ await waitForText(setup, "+ new link");
92
+ await press(setup, "enter");
93
+ await press(setup, "enter");
94
+
95
+ // Assert - the wizard's fields
96
+ await waitForText(setup, "link name");
97
+ await waitForText(setup, "require auth");
98
+
99
+ // Act: focus the auth row and switch it on (enter toggles the focused row)
100
+ await press(setup, "down");
101
+ await press(setup, "enter");
102
+
103
+ // Assert - the handles input appears with its grayed placeholder
104
+ await waitForText(setup, "handle");
105
+
106
+ // Act: backspace on the empty allowlist collapses require auth back off
107
+ await press(setup, "backspace");
108
+
109
+ // Assert - the handles section is gone
110
+ await waitForTextGone(setup, "handle");
111
+ });
112
+
113
+ test("a private link with no handles cannot be created", async () => {
114
+ // Arrange
115
+ const setup = await testRender(
116
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
117
+ { width: 120, height: 32 },
118
+ );
119
+ await waitForText(setup, "cueloop");
120
+
121
+ // Act: open the wizard, turn auth on but add no handles, then try to create
122
+ await pressKey(setup, "s", { ctrl: true });
123
+ await waitForText(setup, "+ new link");
124
+ await press(setup, "enter"); // into the body
125
+ await press(setup, "enter"); // open the wizard
126
+ await waitForText(setup, "require auth");
127
+ await press(setup, "down"); // focus the auth row
128
+ await press(setup, "enter"); // toggle auth on (focus moves to the empty allowlist)
129
+ await press(setup, "down"); // move to the create action
130
+ await press(setup, "enter"); // create is blocked while the allowlist is empty
131
+ await settle(setup);
132
+
133
+ // Assert - the wizard is still open (nothing was created and the dialog did not fall back to the list)
134
+ expect(setup.captureCharFrame()).toContain("require auth");
135
+ });
136
+
137
+ test("right enters the links body and left returns to the nav", async () => {
138
+ // Arrange
139
+ const setup = await testRender(
140
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
141
+ { width: 120, height: 32 },
142
+ );
143
+ await waitForText(setup, "cueloop");
144
+
145
+ // Act + Assert: right arrow crosses into the body (its hint appears)
146
+ await pressKey(setup, "s", { ctrl: true });
147
+ await waitForText(setup, "+ new link");
148
+ await press(setup, "right");
149
+ await waitForText(setup, "h back");
150
+
151
+ // Act + Assert: left arrow returns to the nav (its hint returns)
152
+ await press(setup, "left");
153
+ await waitForText(setup, "l/enter open");
154
+ });
155
+
156
+ test("escape closes the dialog", async () => {
157
+ // Arrange
158
+ const setup = await testRender(
159
+ <App home={home} sessionId={session.id} shareTransport={shareTransport} />,
160
+ { width: 120, height: 32 },
161
+ );
162
+ await waitForText(setup, "cueloop");
163
+
164
+ // Act
165
+ await pressKey(setup, "s", { ctrl: true });
166
+ await waitForText(setup, "share externally");
167
+ await press(setup, "escape");
168
+
169
+ // Assert
170
+ await waitForTextGone(setup, "share externally");
171
+ });
172
+ });
@@ -0,0 +1,264 @@
1
+ /** The send-message confirm: cmd+enter opens a centered overlay with a message selector, summary input, and word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import React from "react";
8
+ import { testRender } from "@opentui/react/test-utils";
9
+ import { DaemonServer } from "@cueloop/daemon";
10
+ import { makeAnchor, parseBlocks, type Thread } from "@cueloop/schema";
11
+ import { App } from "./App";
12
+ import {
13
+ isolateUserConfig,
14
+ press,
15
+ pressKey,
16
+ settle,
17
+ waitForText,
18
+ waitForTextGone,
19
+ } from "./test-support";
20
+
21
+ const PLAN = `# Migration Plan
22
+
23
+ ## Context
24
+
25
+ The daemon persists sessions to disk atomically.
26
+
27
+ ## Steps
28
+
29
+ - move the store
30
+ - add recovery
31
+ `;
32
+
33
+ let home: string;
34
+ let restoreUserConfig: () => void;
35
+ let server: DaemonServer;
36
+ let session: Thread;
37
+
38
+ beforeEach(() => {
39
+ home = mkdtempSync(join(tmpdir(), "cueloop-confirm-"));
40
+ restoreUserConfig = isolateUserConfig(home);
41
+ server = new DaemonServer({ home, idleExitMs: 0 });
42
+ server.start();
43
+ session = server.core.sessionCreate({
44
+ workspace: { repoRoot: "/repo", branch: "main" },
45
+ artifact: {
46
+ type: "plan",
47
+ content: PLAN,
48
+ meta: { title: "Migration Plan", planPath: "plan.md" },
49
+ },
50
+ });
51
+ });
52
+ afterEach(() => {
53
+ restoreUserConfig();
54
+ server.stop();
55
+ rmSync(home, { recursive: true, force: true });
56
+ });
57
+
58
+ async function renderApp(options: { readOnly?: boolean } = {}) {
59
+ const setup = await testRender(
60
+ <App home={home} sessionId={session.id} readOnly={options.readOnly ?? false} />,
61
+ {
62
+ width: 120,
63
+ height: 32,
64
+ },
65
+ );
66
+
67
+ await waitForText(setup, "cueloop");
68
+
69
+ return setup;
70
+ }
71
+
72
+ /** Seed annotations directly through the daemon core (all on one block). */
73
+ function seedAnnotations(count: number): void {
74
+ const blocks = parseBlocks(PLAN);
75
+
76
+ for (let index = 1; index <= count; index++) {
77
+ server.core.sessionAnnotate(session.id, {
78
+ id: `a_confirm_${index}`,
79
+ kind: "comment",
80
+ anchor: makeAnchor(blocks, 2, 0, 10),
81
+ body: `note ${String(index).padStart(2, "0")}`,
82
+ });
83
+ }
84
+ }
85
+
86
+ describe("send message confirm", () => {
87
+ test("cmd+enter opens the send-message confirm overlay with stable message choices", async () => {
88
+ // Arrange
89
+ const setup = await renderApp();
90
+
91
+ // Assert - the footer carries the send control before the overlay opens
92
+ expect(setup.captureCharFrame()).toContain("Send message (0)");
93
+
94
+ // Act
95
+ await pressKey(setup, "RETURN", { meta: true });
96
+
97
+ // Assert - the overlay: message selector and word-buttons
98
+ await waitForText(setup, "[approve]"); // nothing pending: approve default
99
+ const initialFrame = setup.captureCharFrame();
100
+ const initialSelector = initialFrame
101
+ .split("\n")
102
+ .find((row) => row.includes("[comment] [approve] [changes]"));
103
+
104
+ expect(initialSelector).toBeDefined();
105
+ expect(initialFrame).toContain(" send ");
106
+ expect(initialFrame).toContain(" cancel ");
107
+
108
+ // Act - move the active choice across the selector
109
+ await press(setup, "right");
110
+ const changesSelector = setup
111
+ .captureCharFrame()
112
+ .split("\n")
113
+ .find((row) => row.includes("[comment] [approve] [changes]"));
114
+ await press(setup, "right");
115
+ const commentSelector = setup
116
+ .captureCharFrame()
117
+ .split("\n")
118
+ .find((row) => row.includes("[comment] [approve] [changes]"));
119
+
120
+ // Assert - active styling does not move or resize any choice
121
+ expect(changesSelector).toBe(initialSelector);
122
+ expect(commentSelector).toBe(initialSelector);
123
+ });
124
+
125
+ test("left/right cycles the message selector in the overlay", async () => {
126
+ // Arrange
127
+ const setup = await renderApp();
128
+
129
+ await pressKey(setup, "RETURN", { meta: true });
130
+
131
+ // Assert
132
+ await waitForText(setup, "[approve]");
133
+
134
+ // Act
135
+ await press(setup, "right");
136
+
137
+ // Assert
138
+ await waitForText(setup, "[changes]");
139
+
140
+ // Act
141
+ await press(setup, "right");
142
+
143
+ // Assert
144
+ await waitForText(setup, "[comment]");
145
+
146
+ // Act
147
+ await press(setup, "left");
148
+
149
+ // Assert
150
+ await waitForText(setup, "[changes]");
151
+ });
152
+
153
+ test("esc closes the overlay", async () => {
154
+ // Arrange
155
+ const setup = await renderApp();
156
+
157
+ await pressKey(setup, "RETURN", { meta: true });
158
+
159
+ // Assert
160
+ await waitForText(setup, "[approve]");
161
+
162
+ // Act
163
+ await press(setup, "escape");
164
+
165
+ // Assert - a bare ESC settles after the parser's escape-sequence window
166
+ const frame = await waitForTextGone(setup, "[approve]");
167
+
168
+ expect(frame).not.toContain(" cancel ");
169
+ });
170
+
171
+ test("enter in the overlay resolves the session through the controller", async () => {
172
+ // Arrange
173
+ seedAnnotations(1);
174
+ const setup = await renderApp();
175
+
176
+ // Act
177
+ await pressKey(setup, "RETURN", { meta: true });
178
+
179
+ // Assert - opens on the default message, then cycle to request changes
180
+ await waitForText(setup, "[approve]");
181
+ await press(setup, "right");
182
+ await waitForText(setup, "[changes]");
183
+
184
+ // Act
185
+ await setup.mockInput.typeText("Tighten the steps.");
186
+ await pressKey(setup, "RETURN", { meta: true });
187
+
188
+ // Assert - the completion flow after submit is unchanged
189
+ await waitForText(setup, "feedback sent");
190
+ const stored = server.core.sessionGet(session.id);
191
+
192
+ expect(stored.status).toBe("resolved");
193
+ expect(stored.message!.outcome).toBe("changes_requested");
194
+ });
195
+
196
+ test("Comment sends feedback while leaving the Thread open", async () => {
197
+ seedAnnotations(1);
198
+ const setup = await renderApp();
199
+
200
+ await pressKey(setup, "RETURN", { meta: true });
201
+ await waitForText(setup, "[approve]");
202
+ await press(setup, "left");
203
+ await waitForText(setup, "[comment]");
204
+
205
+ await setup.mockInput.typeText("For now.");
206
+ await pressKey(setup, "RETURN", { meta: true });
207
+
208
+ await waitForText(setup, "comment sent - thread stays open");
209
+ const stored = server.core.sessionGet(session.id);
210
+
211
+ expect(stored.status).toBe("pending");
212
+ expect(stored.message!.outcome).toBe("comment");
213
+ expect(stored.message!.annotations?.map((annotation) => annotation.id)).toEqual([
214
+ "a_confirm_1",
215
+ ]);
216
+ expect(setup.captureCharFrame()).not.toContain("feedback sent");
217
+ });
218
+
219
+ test("typing / in the summary opens the skills/actions palette", async () => {
220
+ // Arrange
221
+ seedAnnotations(1);
222
+ const setup = await renderApp();
223
+ await pressKey(setup, "RETURN", { meta: true });
224
+ await waitForText(setup, "[approve]");
225
+
226
+ // Act
227
+ await setup.mockInput.typeText("/restate");
228
+
229
+ // Assert - the same slash palette the inline composer shows
230
+ await waitForText(setup, "restate-simplified");
231
+ });
232
+
233
+ test("pasting an image into the summary drops in an [Image #n] placeholder", async () => {
234
+ // Arrange
235
+ const setup = await renderApp();
236
+ await pressKey(setup, "RETURN", { meta: true });
237
+ await waitForText(setup, "[approve]");
238
+
239
+ // Act - a burst of control bytes stands in for the binary an image paste delivers
240
+ await setup.mockInput.typeText("look at this ");
241
+ await setup.mockInput.pasteBracketedText("");
242
+
243
+ // Assert - the raw bytes never reach the draft; a numbered placeholder does
244
+ await waitForText(setup, "[Image #1]");
245
+ expect(setup.captureCharFrame()).toContain("look at this [Image #1]");
246
+ });
247
+
248
+ test("read-only observers cannot open the confirm overlay", async () => {
249
+ // Arrange
250
+ seedAnnotations(1);
251
+ const setup = await renderApp({ readOnly: true });
252
+
253
+ // Act
254
+ await pressKey(setup, "RETURN", { meta: true });
255
+ await settle(setup);
256
+
257
+ // Assert - no overlay, and the session stays pending
258
+ const frame = setup.captureCharFrame();
259
+
260
+ expect(frame).not.toContain("[approve]");
261
+ expect(frame).not.toContain(" cancel ");
262
+ expect(server.core.sessionGet(session.id).status).toBe("pending");
263
+ });
264
+ });