@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,18 +1,28 @@
1
- /**
2
- * Tier-2 wiring test for the notes-vault export: with [integrations.obsidian]
3
- * configured to export on resolve, submitting a review writes the note into
4
- * the vault and surfaces the path in the status line.
5
- */
1
+ /** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
6
2
 
7
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
9
13
  import { tmpdir } from "node:os";
10
14
  import { join } from "node:path";
11
15
  import React from "react";
12
- import { testRender } from "@opentui/react/test-utils";
13
16
  import { DaemonServer } from "@cueloop/daemon";
14
- import type { ReviewSession } from "@cueloop/schema";
17
+ import type { Thread } from "@cueloop/schema";
15
18
  import { App } from "./App";
19
+ import {
20
+ isolateUserConfig,
21
+ waitForState,
22
+ waitForText,
23
+ pressKey,
24
+ renderReadyApp,
25
+ } from "./test-support";
16
26
 
17
27
  const PLAN = `# Migration Plan
18
28
 
@@ -22,66 +32,73 @@ Move the store atomically.
22
32
  let home: string;
23
33
  let vault: string;
24
34
  let server: DaemonServer;
25
- let session: ReviewSession;
26
- let savedConfigEnv: string | undefined;
35
+ let session: Thread;
36
+ let restoreUserConfig: () => void;
27
37
 
28
38
  beforeEach(() => {
29
39
  home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
30
40
  vault = join(home, "vault");
31
41
  mkdirSync(vault);
32
42
  const configPath = join(home, "config.toml");
33
- writeFileSync(configPath, `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`);
34
- savedConfigEnv = process.env.CUELOOP_CONFIG;
35
- process.env.CUELOOP_CONFIG = configPath;
43
+
44
+ writeFileSync(
45
+ configPath,
46
+ `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "message"\n`,
47
+ );
48
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
36
49
  server = new DaemonServer({ home, idleExitMs: 0 });
37
50
  server.start();
38
51
  session = server.core.sessionCreate({
39
52
  workspace: { repoRoot: "/repo", branch: "main" },
40
- artifact: { type: "plan", content: PLAN, meta: { title: "Migration Plan", planPath: "plan.md" } },
53
+ artifact: {
54
+ type: "plan",
55
+ content: PLAN,
56
+ meta: { title: "Migration Plan", planPath: "plan.md" },
57
+ },
41
58
  });
42
59
  });
43
60
  afterEach(() => {
44
- if (savedConfigEnv === undefined) delete process.env.CUELOOP_CONFIG;
45
- else process.env.CUELOOP_CONFIG = savedConfigEnv;
61
+ restoreUserConfig();
46
62
  server.stop();
47
63
  rmSync(home, { recursive: true, force: true });
48
64
  });
49
65
 
50
66
  describe("obsidian export on resolve", () => {
51
67
  test("submitting a review writes the plan into the vault and shows the path", async () => {
52
- const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 32 });
53
- for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
54
- await Bun.sleep(25);
55
- await setup.renderOnce();
56
- }
57
- // open the submit overlay, keep the default verdict (approve), submit
58
- setup.mockInput.pressKey("RETURN");
59
- await Bun.sleep(15);
60
- await setup.renderOnce();
61
- expect(setup.captureCharFrame()).toContain("verdict");
62
- setup.mockInput.pressKey("RETURN");
63
- // wall-clock deadlines: the export is an async round-trip after resolve,
64
- // and an iteration count times out on a loaded runner
68
+ // Arrange
69
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
70
+ width: 120,
71
+ height: 32,
72
+ });
73
+
74
+ await waitForText(setup, "cueloop");
75
+
76
+ // Act
77
+ await pressKey(setup, "RETURN", { meta: true }); // open the submit overlay, keep the default message (approve)
78
+
79
+ // Assert
80
+ await waitForText(setup, "[approve]");
81
+
82
+ // Act
83
+ await pressKey(setup, "RETURN", { meta: true }); // submit
84
+
85
+ // Assert
86
+ // the export is an async round-trip after resolve; the status line lands
87
+ // on a later render than the file write
65
88
  const dir = join(vault, "cueloop");
66
- const fileDeadline = Date.now() + 30_000;
67
- while (Date.now() < fileDeadline && !existsSync(dir)) {
68
- await Bun.sleep(25);
69
- await setup.renderOnce();
70
- }
71
- // the status line lands on a later render than the file write
72
- const statusDeadline = Date.now() + 30_000;
73
- while (Date.now() < statusDeadline && !setup.captureCharFrame().includes("exported to")) {
74
- await Bun.sleep(25);
75
- await setup.renderOnce();
76
- }
89
+
90
+ await waitForState(setup, () => existsSync(dir));
91
+ await waitForText(setup, "exported to");
77
92
 
78
93
  const notes = readdirSync(dir);
94
+
79
95
  expect(notes.length).toBe(1);
80
96
  expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
81
97
  const written = readFileSync(join(dir, notes[0]!), "utf8");
98
+
82
99
  expect(written).toContain("source: cueloop");
83
100
  expect(written).toContain(`session: ${session.id}`);
84
- expect(written).toContain("verdict: approve");
101
+ expect(written).toContain("message: approved");
85
102
  expect(written).toContain("Move the store atomically.");
86
103
  expect(setup.captureCharFrame()).toContain("exported to");
87
104
  });
@@ -0,0 +1,89 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { nextFocusPane, reconciledFocus, threadsNavHandled, visiblePanes } from "./App";
3
+
4
+ describe("threads keyboard nav", () => {
5
+ const base = () => {
6
+ let cursor = 1;
7
+ let opened = 0;
8
+
9
+ return {
10
+ setInboxCursor: (update: (cursor: number) => number) => (cursor = update(cursor)),
11
+ openSession: () => (opened += 1),
12
+ read: () => ({
13
+ get cursor() {
14
+ return cursor;
15
+ },
16
+ get opened() {
17
+ return opened;
18
+ },
19
+ }),
20
+ };
21
+ };
22
+
23
+ test("moves the cursor and opens only when the Threads pane is quiet and focused", () => {
24
+ const spy = base();
25
+ const call = (name: string) =>
26
+ threadsNavHandled({
27
+ focusedPane: "threads",
28
+ quiet: true,
29
+ key: { name },
30
+ count: 3,
31
+ setInboxCursor: spy.setInboxCursor,
32
+ openSession: spy.openSession,
33
+ });
34
+
35
+ expect(call("down")).toBe(true);
36
+ expect(spy.read().cursor).toBe(2);
37
+ expect(call("down")).toBe(true); // clamps at the last row
38
+ expect(spy.read().cursor).toBe(2);
39
+ expect(call("k")).toBe(true);
40
+ expect(spy.read().cursor).toBe(1);
41
+ expect(call("return")).toBe(true);
42
+ expect(spy.read().opened).toBe(1);
43
+ });
44
+
45
+ test("defers when another pane is focused, keys are owned elsewhere, or the inbox is empty", () => {
46
+ const spy = base();
47
+ const params = {
48
+ quiet: true,
49
+ key: { name: "down" },
50
+ count: 3,
51
+ setInboxCursor: spy.setInboxCursor,
52
+ openSession: spy.openSession,
53
+ };
54
+
55
+ expect(threadsNavHandled({ ...params, focusedPane: "changes" })).toBe(false);
56
+ expect(threadsNavHandled({ ...params, focusedPane: "threads", quiet: false })).toBe(false);
57
+ expect(threadsNavHandled({ ...params, focusedPane: "threads", count: 0 })).toBe(false);
58
+ expect(spy.read().cursor).toBe(1);
59
+ });
60
+ });
61
+
62
+ describe("pane focus cycle", () => {
63
+ test("visiblePanes lists only the open panes, in reading order", () => {
64
+ expect(visiblePanes(true, true, true, true)).toEqual([
65
+ "threads",
66
+ "thread",
67
+ "changes",
68
+ "project",
69
+ ]);
70
+ expect(visiblePanes(true, false, true, true)).toEqual(["threads", "changes", "project"]);
71
+ expect(visiblePanes(false, false, true, false)).toEqual(["changes"]);
72
+ });
73
+
74
+ test("nextFocusPane wraps forward and back over the visible panes", () => {
75
+ const panes = visiblePanes(true, false, true, true);
76
+
77
+ expect(nextFocusPane("threads", panes, false)).toBe("changes");
78
+ expect(nextFocusPane("project", panes, false)).toBe("threads");
79
+ expect(nextFocusPane("threads", panes, true)).toBe("project");
80
+ expect(nextFocusPane("changes", [], false)).toBe("changes");
81
+ });
82
+
83
+ test("reconciledFocus falls back only when the focused pane is not navigable", () => {
84
+ expect(reconciledFocus(["threads", "changes"], "threads")).toBeNull();
85
+ expect(reconciledFocus(["threads", "changes"], "thread")).toBe("threads");
86
+ expect(reconciledFocus(["changes", "project"], "thread")).toBe("changes");
87
+ expect(reconciledFocus([], "thread")).toBeNull();
88
+ });
89
+ });
@@ -0,0 +1,78 @@
1
+ /** Reviewer identity surfaces: the Account settings tab and the author-name tooltip on a comment dot. */
2
+
3
+ import { afterEach, beforeEach, 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 { DaemonServer } from "@cueloop/daemon";
9
+ import { App } from "./App";
10
+ import { isolateUserConfig, locateText, renderReadyApp, waitForText } from "./test-support";
11
+
12
+ const PLAN = "# Migration Plan\n\nThe daemon persists sessions to disk atomically.\n";
13
+
14
+ let home: string;
15
+ let restoreUserConfig: () => void;
16
+ let server: DaemonServer;
17
+ let sessionId: string;
18
+
19
+ beforeEach(() => {
20
+ home = mkdtempSync(join(tmpdir(), "cueloop-identity-"));
21
+ restoreUserConfig = isolateUserConfig(home);
22
+ server = new DaemonServer({ home, idleExitMs: 0 });
23
+ server.start();
24
+ const session = server.core.sessionCreate({
25
+ workspace: { repoRoot: "/repo", branch: "main" },
26
+ artifact: {
27
+ type: "plan",
28
+ content: PLAN,
29
+ meta: { title: "Migration Plan", planPath: "plan.md" },
30
+ },
31
+ });
32
+ sessionId = session.id;
33
+ });
34
+ afterEach(() => {
35
+ restoreUserConfig();
36
+ server.stop();
37
+ rmSync(home, { recursive: true, force: true });
38
+ });
39
+
40
+ async function renderApp() {
41
+ return renderReadyApp(<App home={home} sessionId={sessionId} />, { width: 120, height: 32 });
42
+ }
43
+
44
+ test("the Account settings tab shows the display name row and the GitHub sync action", async () => {
45
+ const setup = await renderApp();
46
+
47
+ await setup.mockMouse.click(1, 0);
48
+ await waitForText(setup, "settings");
49
+ const menu = setup.captureCharFrame().split("\n");
50
+ const settingsRow = menu.findIndex((line) => line.includes("settings"));
51
+
52
+ await setup.mockMouse.click(menu[settingsRow]!.indexOf("settings") + 1, settingsRow);
53
+ await waitForText(setup, "Account");
54
+ const dialog = setup.captureCharFrame().split("\n");
55
+ const accountRow = dialog.findIndex((line) => line.includes("Account"));
56
+
57
+ await setup.mockMouse.click(dialog[accountRow]!.indexOf("Account") + 1, accountRow);
58
+ await waitForText(setup, "Display name");
59
+ expect(setup.captureCharFrame()).toContain("Sync from GitHub");
60
+ });
61
+
62
+ test("hovering an own comment's dot surfaces the author tooltip", async () => {
63
+ server.core.sessionAnnotate(sessionId, {
64
+ id: "a_own",
65
+ kind: "comment",
66
+ anchor: { quote: "atomically", prefix: "sessions to disk ", suffix: "." },
67
+ body: "My note.",
68
+ });
69
+ const setup = await renderApp();
70
+
71
+ await waitForText(setup, "● My note.");
72
+ // the tooltip is not painted until the pointer is over the dot
73
+ expect(setup.captureCharFrame()).not.toContain("you");
74
+ const dot = locateText(setup, "● My note.");
75
+
76
+ await setup.mockMouse.moveTo(dot.column, dot.row);
77
+ await waitForText(setup, "you");
78
+ });
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The read-only plan render: h1/h2 sit over
3
+ * a rule, links show their label in the link color with the URL concealed,
4
+ * inline code grays with its backticks gone, GFM tables render as an aligned grid
5
+ * and leading YAML frontmatter as a bordered key/value table. Because rendered
6
+ * text now differs from the source, a comment dragged over a concealed-markup
7
+ * passage must still anchor to the source and repaint on the visible label.
8
+ * Char-frame + styled-span assertions over the real App and an in-process daemon.
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import React from "react";
16
+ import { DaemonServer } from "@cueloop/daemon";
17
+ import type { Thread } from "@cueloop/schema";
18
+ import { App } from "./App";
19
+ import { DARK } from "./theme";
20
+ import {
21
+ dragText,
22
+ isolateUserConfig,
23
+ pressKey,
24
+ renderReadyApp,
25
+ settle,
26
+ typeText as type,
27
+ waitForState,
28
+ waitForText,
29
+ } from "./test-support";
30
+
31
+ const PLAN = `---
32
+ title: Migration Plan
33
+ owner: platform team
34
+ ---
35
+
36
+ # Migration Plan
37
+
38
+ ## Rollout
39
+
40
+ Ship it behind a \`flag\` and read [OpenTUI](https://opentui) first.
41
+
42
+ | Name | Size |
43
+ | :--- | ---: |
44
+ | daemon | 12 |
45
+ | client | 7 |
46
+ `;
47
+
48
+ let home: string;
49
+ let server: DaemonServer;
50
+ let session: Thread;
51
+ let restoreUserConfig: () => void;
52
+
53
+ beforeEach(() => {
54
+ home = mkdtempSync(join(tmpdir(), "cueloop-plan-render-"));
55
+ restoreUserConfig = isolateUserConfig(home);
56
+ server = new DaemonServer({ home, idleExitMs: 0 });
57
+ server.start();
58
+ session = server.core.sessionCreate({
59
+ workspace: { repoRoot: "/repo", branch: "main" },
60
+ artifact: {
61
+ type: "plan",
62
+ content: PLAN,
63
+ meta: { title: "Migration Plan", planPath: "plan.md" },
64
+ },
65
+ });
66
+ });
67
+ afterEach(() => {
68
+ restoreUserConfig();
69
+ server.stop();
70
+ rmSync(home, { recursive: true, force: true });
71
+ });
72
+
73
+ type Setup = Awaited<ReturnType<typeof renderPlan>>;
74
+
75
+ async function renderPlan() {
76
+ const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
77
+ width: 90,
78
+ height: 40,
79
+ });
80
+
81
+ await waitForText(setup, "cueloop");
82
+ await settle(setup);
83
+
84
+ return setup;
85
+ }
86
+
87
+ /** Hex foregrounds of every styled span containing the needle. */
88
+ function foregroundsOf(setup: Setup, needle: string): string[] {
89
+ const foregrounds: string[] = [];
90
+
91
+ for (const line of setup.captureSpans().lines) {
92
+ for (const span of line.spans) {
93
+ if (span.fg === undefined || !span.text.includes(needle)) continue;
94
+ const [red, green, blue] = span.fg.toInts();
95
+
96
+ foregrounds.push(
97
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
98
+ );
99
+ }
100
+ }
101
+
102
+ return foregrounds;
103
+ }
104
+
105
+ /** Hex backgrounds of every styled span containing the needle. */
106
+ function backgroundsOf(setup: Setup, needle: string): string[] {
107
+ const backgrounds: string[] = [];
108
+
109
+ for (const line of setup.captureSpans().lines) {
110
+ for (const span of line.spans) {
111
+ if (!span.text.includes(needle)) continue;
112
+ const [red, green, blue] = span.bg.toInts();
113
+
114
+ backgrounds.push(
115
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
116
+ );
117
+ }
118
+ }
119
+
120
+ return backgrounds;
121
+ }
122
+
123
+ const THREAD_MARK = "#463852";
124
+
125
+ describe("read-only markdown render", () => {
126
+ test("h1 and h2 sit over a rule", async () => {
127
+ const setup = await renderPlan();
128
+ const rows = setup.captureCharFrame().split("\n");
129
+ // "Migration Plan" also rides the header bar; a heading is any body row directly over a rule
130
+ const ruleBelow = (heading: string): boolean =>
131
+ rows.some((row, index) => row.includes(heading) && (rows[index + 1] ?? "").includes("──"));
132
+
133
+ expect(ruleBelow("Migration Plan")).toBe(true);
134
+ expect(ruleBelow("Rollout")).toBe(true);
135
+ });
136
+
137
+ test("a link shows its label in the link color with the URL concealed", async () => {
138
+ const setup = await renderPlan();
139
+ const frame = setup.captureCharFrame();
140
+
141
+ expect(frame).toContain("OpenTUI");
142
+ expect(frame).not.toContain("https://opentui");
143
+ expect(frame).not.toContain("[OpenTUI]");
144
+ expect(foregroundsOf(setup, "OpenTUI")).toContain(DARK.blue);
145
+ });
146
+
147
+ test("inline code grays with its backticks concealed", async () => {
148
+ const setup = await renderPlan();
149
+ const frame = setup.captureCharFrame();
150
+
151
+ expect(frame).toContain("flag");
152
+ expect(frame).not.toContain("`flag`");
153
+ expect(foregroundsOf(setup, "flag")).toContain(DARK.textDim);
154
+ });
155
+
156
+ test("a table renders as an aligned grid without pipes", async () => {
157
+ const setup = await renderPlan();
158
+ const rows = setup.captureCharFrame().split("\n");
159
+ const header = rows.find((row) => row.includes("Name") && row.includes("Size"))!;
160
+
161
+ expect(header).not.toContain("|");
162
+ // the numeric column is right-aligned, so 12 and 7 line up on their last digit
163
+ const daemon = rows.find((row) => row.includes("daemon"))!;
164
+ const client = rows.find((row) => row.includes("client"))!;
165
+
166
+ expect(daemon.indexOf("12") + 1).toBe(client.indexOf("7"));
167
+ });
168
+
169
+ test("frontmatter renders as a bordered key/value grid", async () => {
170
+ const setup = await renderPlan();
171
+ const frame = setup.captureCharFrame();
172
+
173
+ expect(frame).toContain("┌");
174
+ expect(frame).toContain("└");
175
+ expect(frame).toContain("title");
176
+ expect(frame).toContain("platform team");
177
+ });
178
+ });
179
+
180
+ describe("annotating a grid block", () => {
181
+ test("a comment dragged over a table cell anchors on the grid and marks it", async () => {
182
+ const setup = await renderPlan();
183
+
184
+ // the table renders as a grid, yet it still takes a comment like any other block
185
+ await dragText(setup, "daemon", "daemon", "daemon".length);
186
+ await type(setup, "which daemon?");
187
+ await pressKey(setup, "RETURN", { meta: true });
188
+
189
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
190
+ // the grid carries the mark backdrop, so an annotation resolving into it stays visible
191
+ await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
192
+ }, 60_000);
193
+ });
194
+
195
+ describe("anchoring over concealed markup", () => {
196
+ test("a comment dragged over a rendered link label anchors to the source and repaints", async () => {
197
+ const setup = await renderPlan();
198
+
199
+ // drag the visible label "OpenTUI" (its brackets and URL are concealed)
200
+ await dragText(setup, "OpenTUI", "OpenTUI", "OpenTUI".length);
201
+ await type(setup, "which build?");
202
+ await pressKey(setup, "RETURN", { meta: true });
203
+
204
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
205
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("OpenTUI");
206
+ // the saved comment keeps the visible label marked
207
+ await waitForState(setup, () => backgroundsOf(setup, "OpenTUI").includes(THREAD_MARK));
208
+ }, 60_000);
209
+ });