@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,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,222 @@
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", 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 frame = setup.captureCharFrame();
100
+
101
+ expect(frame).toContain("[Approve]");
102
+ expect(frame).toContain(" send ");
103
+ expect(frame).toContain(" cancel ");
104
+ });
105
+
106
+ test("left/right cycles the message selector in the overlay", async () => {
107
+ // Arrange
108
+ const setup = await renderApp();
109
+
110
+ await pressKey(setup, "RETURN", { meta: true });
111
+
112
+ // Assert
113
+ await waitForText(setup, "[Approve]");
114
+
115
+ // Act
116
+ await press(setup, "right");
117
+
118
+ // Assert
119
+ await waitForText(setup, "[Changes]");
120
+
121
+ // Act
122
+ await press(setup, "right");
123
+
124
+ // Assert
125
+ await waitForText(setup, "[Approve]");
126
+
127
+ // Act
128
+ await press(setup, "left");
129
+
130
+ // Assert
131
+ await waitForText(setup, "[Changes]");
132
+ });
133
+
134
+ test("esc closes the overlay", async () => {
135
+ // Arrange
136
+ const setup = await renderApp();
137
+
138
+ await pressKey(setup, "RETURN", { meta: true });
139
+
140
+ // Assert
141
+ await waitForText(setup, "[Approve]");
142
+
143
+ // Act
144
+ await press(setup, "escape");
145
+
146
+ // Assert - a bare ESC settles after the parser's escape-sequence window
147
+ const frame = await waitForTextGone(setup, "[Approve]");
148
+
149
+ expect(frame).not.toContain(" cancel ");
150
+ });
151
+
152
+ test("enter in the overlay resolves the session through the controller", async () => {
153
+ // Arrange
154
+ seedAnnotations(1);
155
+ const setup = await renderApp();
156
+
157
+ // Act
158
+ await pressKey(setup, "RETURN", { meta: true });
159
+
160
+ // Assert - opens on the default message, then cycle to request changes
161
+ await waitForText(setup, "[Approve]");
162
+ await press(setup, "right");
163
+ await waitForText(setup, "[Changes]");
164
+
165
+ // Act
166
+ await setup.mockInput.typeText("Tighten the steps.");
167
+ await pressKey(setup, "RETURN", { meta: true });
168
+
169
+ // Assert - the completion flow after submit is unchanged
170
+ await waitForText(setup, "feedback sent");
171
+ const stored = server.core.sessionGet(session.id);
172
+
173
+ expect(stored.status).toBe("resolved");
174
+ expect(stored.message!.outcome).toBe("changes_requested");
175
+ });
176
+
177
+ test("typing / in the summary opens the skills/actions palette", async () => {
178
+ // Arrange
179
+ seedAnnotations(1);
180
+ const setup = await renderApp();
181
+ await pressKey(setup, "RETURN", { meta: true });
182
+ await waitForText(setup, "[Approve]");
183
+
184
+ // Act
185
+ await setup.mockInput.typeText("/restate");
186
+
187
+ // Assert - the same slash palette the inline composer shows
188
+ await waitForText(setup, "restate-simplified");
189
+ });
190
+
191
+ test("pasting an image into the summary drops in an [Image #n] placeholder", async () => {
192
+ // Arrange
193
+ const setup = await renderApp();
194
+ await pressKey(setup, "RETURN", { meta: true });
195
+ await waitForText(setup, "[Approve]");
196
+
197
+ // Act - a burst of control bytes stands in for the binary an image paste delivers
198
+ await setup.mockInput.typeText("look at this ");
199
+ await setup.mockInput.pasteBracketedText("");
200
+
201
+ // Assert - the raw bytes never reach the draft; a numbered placeholder does
202
+ await waitForText(setup, "[Image #1]");
203
+ expect(setup.captureCharFrame()).toContain("look at this [Image #1]");
204
+ });
205
+
206
+ test("read-only observers cannot open the confirm overlay", async () => {
207
+ // Arrange
208
+ seedAnnotations(1);
209
+ const setup = await renderApp({ readOnly: true });
210
+
211
+ // Act
212
+ await pressKey(setup, "RETURN", { meta: true });
213
+ await settle(setup);
214
+
215
+ // Assert - no overlay, and the session stays pending
216
+ const frame = setup.captureCharFrame();
217
+
218
+ expect(frame).not.toContain("[Approve]");
219
+ expect(frame).not.toContain(" cancel ");
220
+ expect(server.core.sessionGet(session.id).status).toBe("pending");
221
+ });
222
+ });