@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67

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 (160) hide show
  1. package/package.json +6 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +79 -112
  4. package/src/App.plan-review.test.tsx +23 -205
  5. package/src/App.prototype.test.tsx +1 -8
  6. package/src/App.readonly.test.tsx +9 -27
  7. package/src/App.submit-confirm.test.tsx +19 -72
  8. package/src/App.test.tsx +136 -31
  9. package/src/App.tsx +491 -322
  10. package/src/App.walk.test.tsx +15 -16
  11. package/src/App.workbench.test.tsx +390 -0
  12. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  13. package/src/annotation-palette.ts +61 -0
  14. package/src/app-screens.tsx +197 -77
  15. package/src/app-view-model.ts +9 -69
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +1 -1
  18. package/src/components/AnnotationCards.stories.tsx +87 -0
  19. package/src/components/AnnotationCards.tsx +303 -0
  20. package/src/components/AppHeader.stories.tsx +50 -0
  21. package/src/components/AppHeader.tsx +83 -0
  22. package/src/components/AppShell.stories.tsx +89 -0
  23. package/src/components/AppShell.tsx +239 -0
  24. package/src/components/AppShell.zoom.test.tsx +94 -0
  25. package/src/components/Breadcrumb.stories.tsx +1 -1
  26. package/src/components/ChangesColumn.stories.tsx +39 -0
  27. package/src/components/ChangesColumn.tsx +170 -0
  28. package/src/components/CodeBlock.stories.tsx +1 -1
  29. package/src/components/CompletionOverlay.stories.tsx +1 -1
  30. package/src/components/ConfirmCard.stories.tsx +1 -1
  31. package/src/components/ConfirmCard.tsx +3 -4
  32. package/src/components/ConfirmDialog.stories.tsx +1 -1
  33. package/src/components/DiffSheet.scroll.test.tsx +38 -55
  34. package/src/components/DiffSheet.split.test.tsx +140 -0
  35. package/src/components/DiffSheet.stories.tsx +56 -54
  36. package/src/components/DiffSheet.syntax.test.tsx +24 -7
  37. package/src/components/DiffSheet.tsx +607 -276
  38. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  39. package/src/components/DiscussionMarkerRail.tsx +181 -0
  40. package/src/components/EditorGrid.stories.tsx +59 -0
  41. package/src/components/EditorGrid.tabs.test.tsx +105 -0
  42. package/src/components/EditorGrid.tsx +338 -0
  43. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  44. package/src/components/FileContentsView.stories.tsx +16 -0
  45. package/src/components/FileContentsView.test.tsx +29 -0
  46. package/src/components/FileContentsView.tsx +78 -0
  47. package/src/components/InboxList.stories.tsx +23 -8
  48. package/src/components/InboxList.tsx +188 -33
  49. package/src/components/KeybindsDialog.stories.tsx +1 -1
  50. package/src/components/MarkerPopover.stories.tsx +1 -1
  51. package/src/components/MenuBar.stories.tsx +3 -12
  52. package/src/components/MenuBar.tsx +25 -56
  53. package/src/components/PaneGrid.stories.tsx +84 -0
  54. package/src/components/PaneGrid.tsx +128 -0
  55. package/src/components/PanelColumn.stories.tsx +25 -0
  56. package/src/components/PanelColumn.tsx +95 -0
  57. package/src/components/ProjectTreeView.stories.tsx +25 -0
  58. package/src/components/ProjectTreeView.test.tsx +32 -0
  59. package/src/components/ProjectTreeView.tsx +82 -0
  60. package/src/components/PromptDialog.stories.tsx +1 -1
  61. package/src/components/PromptDialog.tsx +8 -1
  62. package/src/components/PrototypeSheet.stories.tsx +1 -1
  63. package/src/components/RootOverlay.stories.tsx +44 -0
  64. package/src/components/RootOverlay.tsx +35 -0
  65. package/src/components/SettingsDialog.stories.tsx +7 -1
  66. package/src/components/SettingsDialog.tsx +114 -67
  67. package/src/components/SettingsRows.stories.tsx +1 -1
  68. package/src/components/ShellHeader.stories.tsx +50 -0
  69. package/src/components/ShellHeader.tsx +137 -0
  70. package/src/components/SurfaceScrollbar.stories.tsx +34 -0
  71. package/src/components/SurfaceScrollbar.test.tsx +111 -0
  72. package/src/components/SurfaceScrollbar.tsx +93 -0
  73. package/src/components/ThreadFooter.stories.tsx +30 -0
  74. package/src/components/ThreadFooter.test.tsx +31 -0
  75. package/src/components/ThreadFooter.tsx +54 -0
  76. package/src/components/ThreadView.slash.test.tsx +113 -0
  77. package/src/components/ThreadView.stories.tsx +1 -1
  78. package/src/components/ThreadView.tsx +62 -1352
  79. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  80. package/src/components/ThreadsSidebar.tsx +70 -0
  81. package/src/components/Toast.stories.tsx +1 -1
  82. package/src/components/Tooltip.stories.tsx +26 -0
  83. package/src/components/Tooltip.tsx +72 -0
  84. package/src/components/TreePane.stories.tsx +1 -1
  85. package/src/components/WalkWizard.stories.tsx +1 -1
  86. package/src/components/WelcomeSurface.stories.tsx +12 -0
  87. package/src/components/WelcomeSurface.tsx +52 -0
  88. package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
  89. package/src/components/agent-launcher.stories.tsx +1 -1
  90. package/src/components/diff-sheet-layout.test.ts +125 -55
  91. package/src/components/diff-sheet-layout.ts +3 -103
  92. package/src/components/editor-grid.test.ts +86 -0
  93. package/src/components/editor-grid.ts +138 -0
  94. package/src/components/fade-title.test.ts +49 -0
  95. package/src/components/fade-title.ts +61 -0
  96. package/src/components/file-tree.test.ts +60 -0
  97. package/src/components/file-tree.ts +112 -0
  98. package/src/components/pages/Workspace.stories.tsx +181 -0
  99. package/src/components/primitives/Button.stories.tsx +1 -1
  100. package/src/components/primitives/Card.stories.tsx +1 -1
  101. package/src/components/primitives/Dialog.stories.tsx +1 -1
  102. package/src/components/primitives/IconButton.stories.tsx +36 -0
  103. package/src/components/primitives/IconButton.tsx +62 -0
  104. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  105. package/src/components/primitives/Tabs.stories.tsx +1 -1
  106. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  107. package/src/components/primitives/Tree.stories.tsx +124 -0
  108. package/src/components/primitives/Tree.tsx +96 -0
  109. package/src/components/primitives/icons.ts +61 -0
  110. package/src/components/primitives/tree-model.test.ts +99 -0
  111. package/src/components/primitives/tree-model.ts +137 -0
  112. package/src/components/quick-actions-editor.stories.tsx +1 -1
  113. package/src/components/session-tree.test.ts +134 -0
  114. package/src/components/session-tree.ts +108 -0
  115. package/src/components/stories-app.tsx +142 -82
  116. package/src/components/story-tree.test.ts +100 -0
  117. package/src/components/story.ts +55 -0
  118. package/src/components/theme-context.stories.tsx +1 -1
  119. package/src/config.test.ts +20 -49
  120. package/src/config.ts +26 -28
  121. package/src/diff-fold.test.ts +78 -0
  122. package/src/diff-fold.ts +126 -0
  123. package/src/diff-syntax.test.ts +16 -0
  124. package/src/diff-syntax.ts +6 -3
  125. package/src/discussions.ts +79 -0
  126. package/src/intent-dispatch.test.ts +31 -11
  127. package/src/intent-dispatch.ts +93 -48
  128. package/src/key-bindings.test.ts +1 -1
  129. package/src/keymap.test.ts +3 -25
  130. package/src/keymap.ts +17 -18
  131. package/src/mark-runs.ts +120 -0
  132. package/src/pty.ts +10 -2
  133. package/src/run.ts +17 -7
  134. package/src/serve.test.ts +4 -8
  135. package/src/session-controller.curate.test.ts +75 -0
  136. package/src/session-controller.share.test.ts +1 -0
  137. package/src/session-controller.tree.test.ts +26 -0
  138. package/src/session-controller.ts +160 -31
  139. package/src/slash-palette.ts +83 -0
  140. package/src/split-diff.test.ts +110 -0
  141. package/src/split-diff.ts +149 -0
  142. package/src/test-support.test.ts +3 -3
  143. package/src/test-support.ts +39 -8
  144. package/src/theme.ts +4 -4
  145. package/src/thread-chords.test.ts +25 -9
  146. package/src/thread-chords.ts +33 -8
  147. package/src/use-annotation-surface.tsx +837 -0
  148. package/src/use-changes-workbench.ts +151 -0
  149. package/src/use-frame-measure.ts +38 -0
  150. package/src/use-settings-dialog.tsx +23 -24
  151. package/src/use-terminal-virtualizer.ts +233 -0
  152. package/src/view-diff.ts +114 -0
  153. package/src/App.review-panel.test.tsx +0 -140
  154. package/src/App.tree.test.tsx +0 -224
  155. package/src/components/ReviewPanel.stories.tsx +0 -100
  156. package/src/components/ReviewPanel.tsx +0 -103
  157. package/src/components/ReviewRail.stories.tsx +0 -232
  158. package/src/components/ReviewRail.tsx +0 -327
  159. package/src/review-panel.test.ts +0 -113
  160. package/src/review-panel.ts +0 -83
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.65",
3
+ "version": "0.1.0-alpha.67",
4
4
  "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -26,15 +26,16 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@cueloop/daemon": "0.1.0-alpha.65",
30
- "@cueloop/extension-api": "0.1.0-alpha.65",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.65",
32
- "@cueloop/schema": "0.1.0-alpha.65",
29
+ "@cueloop/daemon": "0.1.0-alpha.67",
30
+ "@cueloop/extension-api": "0.1.0-alpha.67",
31
+ "@cueloop/integration-obsidian": "0.1.0-alpha.67",
32
+ "@cueloop/schema": "0.1.0-alpha.67",
33
33
  "@opentui/core": "^0.5.8",
34
34
  "@opentui/keymap": "0.5.8",
35
35
  "@opentui/react": "^0.5.8",
36
36
  "@opentui/ssh": "0.5.8",
37
37
  "@pierre/diffs": "^1.3.6",
38
+ "@tanstack/virtual-core": "^3.17.9",
38
39
  "diff": "^9",
39
40
  "puppeteer-core": "^25.8.0",
40
41
  "react": "^19.2.0",
@@ -118,8 +118,8 @@ describe("completion overlay", () => {
118
118
  // window before it is delivered - the frame wait absorbs it
119
119
  await press(setup, "escape");
120
120
 
121
- // Assert
122
- const frame = await waitForText(setup, "resolved: approve");
121
+ // Assert - the countdown is cancelled and we are back in the (resolved) thread view
122
+ const frame = await waitForText(setup, "Ship the thing");
123
123
 
124
124
  expect(frame).not.toContain("closing in");
125
125
  });
@@ -1,4 +1,4 @@
1
- /** Diff-review flow in the virtual terminal (slice 3). */
1
+ /** Diff-review flow in the virtual terminal: the diff sheet annotates like the thread view. */
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -9,7 +9,17 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { isolateUserConfig, press, waitForText } from "./test-support";
12
+ import { NERD } from "./components/primitives/icons";
13
+ import {
14
+ clickText,
15
+ dragText,
16
+ isolateUserConfig,
17
+ press,
18
+ pressKey,
19
+ typeText,
20
+ waitForState,
21
+ waitForText,
22
+ } from "./test-support";
13
23
 
14
24
  const PATCH = `diff --git a/src/store.ts b/src/store.ts
15
25
  index 111..222 100644
@@ -43,13 +53,13 @@ afterEach(() => {
43
53
  rmSync(home, { recursive: true, force: true });
44
54
  });
45
55
 
46
- async function renderApp() {
47
- const setup = await testRender(<App home={home} sessionId={session.id} />, {
56
+ async function renderApp(sessionId = session.id) {
57
+ const setup = await testRender(<App home={home} sessionId={sessionId} />, {
48
58
  width: 120,
49
59
  height: 30,
50
60
  });
51
61
 
52
- await waitForText(setup, "cueloop");
62
+ await waitForText(setup, "new Map()");
53
63
 
54
64
  return setup;
55
65
  }
@@ -62,46 +72,74 @@ describe("diff review", () => {
62
72
  // Assert
63
73
  const frame = setup.captureCharFrame();
64
74
 
65
- expect(frame).toContain("■ src/store.ts");
75
+ expect(frame).toContain("src/store.ts");
66
76
  expect(frame).toContain("@@ -1,4 +1,4 @@");
67
- expect(frame).toContain("- private items = [];");
68
- expect(frame).toContain("+ private items = new Map();");
77
+ expect(frame).toMatch(/- {2,}private items = \[\];/);
78
+ expect(frame).toMatch(/\+ {2,}private items = new Map\(\);/);
69
79
  });
70
80
 
71
- test("line comment: quote-anchored, lands in feedback with the code line", async () => {
81
+ test("a drag marks the code, typing comments on it, and the comment lands in feedback", async () => {
72
82
  // Arrange
73
83
  const setup = await renderApp();
74
84
 
75
- // Act
76
- // rows: file(0), hunk(1), ctx(2), del(3), add(4)
77
- for (let i = 0; i < 4; i++) await press(setup, "j");
78
- await press(setup, "c");
85
+ // Act - mark "new Map()" on the added line and just start typing, as in the thread view
86
+ await dragText(setup, "new Map()", "new Map()", "new Map()".length);
87
+ await typeText(setup, "Map needs an eviction story.");
88
+ await waitForText(setup, "● Map needs an eviction story.");
89
+ await pressKey(setup, "RETURN", { meta: true });
79
90
 
80
- // Assert
81
- await waitForText(setup, 'comment on "');
91
+ // Assert - the discussion card hangs under the line; the anchor is the marked words
92
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
93
+ await waitForText(setup, "● Map needs an eviction story.");
94
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("new Map()");
82
95
 
83
- // Act
84
- await setup.mockInput.typeText("Map needs an eviction story.");
96
+ // Act - submit with the session chord (cmd+enter, no composer open), confirm request_changes
97
+ await pressKey(setup, "RETURN", { meta: true });
85
98
  await press(setup, "enter");
86
99
 
87
100
  // Assert
88
- // inline annotation card rendered under the line
89
- await waitForText(setup, "◆ Map needs an eviction story.");
90
- const stored = server.core.sessionGet(session.id);
101
+ await waitForText(setup, "feedback sent");
102
+ const resolved = server.core.sessionGet(session.id);
103
+
104
+ expect(resolved.verdict!.feedback).toContain("new Map()");
105
+ expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
106
+ });
91
107
 
92
- expect(stored.annotations.length).toBe(1);
93
- expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
108
+ test("a comment reopens with its mark painted on the code", async () => {
109
+ // Arrange - a stored comment anchored to the added line's words
110
+ server.core.sessionAnnotate(session.id, {
111
+ id: "a_existing",
112
+ kind: "comment",
113
+ anchor: { quote: "new Map()", prefix: " private items = ", suffix: ";" },
114
+ body: "Existing note.",
115
+ });
94
116
 
95
117
  // Act
96
- await press(setup, "enter"); // submit
97
- await press(setup, "enter"); // confirm request_changes
118
+ const setup = await renderApp();
98
119
 
99
120
  // Assert
100
- await waitForText(setup, "feedback sent");
101
- const resolved = server.core.sessionGet(session.id);
121
+ await waitForText(setup, "● Existing note.");
122
+ });
102
123
 
103
- expect(resolved.verdict!.feedback).toContain("new Map()");
104
- expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
124
+ test("option+c folds the caret's file to its band; the chevron restores its body", async () => {
125
+ // Arrange - the caret opens on the file's first code line
126
+ const setup = await renderApp();
127
+
128
+ // Act
129
+ await pressKey(setup, "c", { meta: true });
130
+
131
+ // Assert - the body is gone but the band (with its counts) remains
132
+ await waitForState(setup, () => !setup.captureCharFrame().includes("new Map()"));
133
+ const collapsed = setup.captureCharFrame();
134
+
135
+ expect(collapsed).toContain("src/store.ts");
136
+ expect(collapsed).toContain("+1");
137
+
138
+ // Act - the band's chevron unfolds it again
139
+ await clickText(setup, NERD.chevronRight);
140
+
141
+ // Assert
142
+ await waitForText(setup, "new Map()");
105
143
  });
106
144
 
107
145
  test("curation needs full file contents; a legacy diff answers", async () => {
@@ -109,7 +147,7 @@ describe("diff review", () => {
109
147
  const setup = await renderApp();
110
148
 
111
149
  // Act
112
- await press(setup, "x");
150
+ await pressKey(setup, "x", { meta: true });
113
151
 
114
152
  // Assert
115
153
  await waitForText(setup, "hunk curation needs full file contents");
@@ -133,17 +171,11 @@ describe("diff review", () => {
133
171
  ],
134
172
  },
135
173
  });
136
- const setup = await testRender(<App home={home} sessionId={withFiles.id} />, {
137
- width: 120,
138
- height: 30,
139
- });
140
-
141
- await waitForText(setup, "cueloop");
174
+ const setup = await renderApp(withFiles.id);
142
175
 
143
- // Act - move to the added line and reject its change
144
- // rows: file(0), hunk(1), ctx(2), del(3), add(4)
145
- for (let i = 0; i < 4; i++) await press(setup, "j");
146
- await press(setup, "x");
176
+ // Act - place the caret on the added line and reject its change
177
+ await clickText(setup, "new Map()");
178
+ await pressKey(setup, "x", { meta: true });
147
179
 
148
180
  // Assert - the single change is gone, so the curated working copy is empty
149
181
  await waitForText(setup, "change rejected");
@@ -152,8 +184,9 @@ describe("diff review", () => {
152
184
  expect(stored.workingCopy).toBe("");
153
185
  });
154
186
 
155
- test("a rejected change lists in the rail and undo (u) restores it", async () => {
156
- // Arrange - a diff session that carries full file contents
187
+ test("a diff opens with the Changes column listing every changed file", async () => {
188
+ // Arrange - two changed files; config.ts is not in the patch text, so it can
189
+ // only appear here by way of the Changes column reading artifact.files
157
190
  const withFiles = server.core.sessionCreate({
158
191
  workspace: { repoRoot: "/repo", branch: "main" },
159
192
  artifact: {
@@ -161,12 +194,8 @@ describe("diff review", () => {
161
194
  content: PATCH,
162
195
  meta: { title: "working tree" },
163
196
  files: [
164
- {
165
- path: "src/store.ts",
166
- oldContents: "export class Store {\n private items = [];\n}\n",
167
- newContents: "export class Store {\n private items = new Map();\n}\n",
168
- status: "modified",
169
- },
197
+ { path: "src/store.ts", oldContents: "a", newContents: "b", status: "modified" },
198
+ { path: "src/config.ts", oldContents: "", newContents: "c", status: "added" },
170
199
  ],
171
200
  },
172
201
  });
@@ -177,70 +206,8 @@ describe("diff review", () => {
177
206
 
178
207
  await waitForText(setup, "cueloop");
179
208
 
180
- // Act - reject the change under the added line
181
- // rows: file(0), hunk(1), ctx(2), del(3), add(4)
182
- for (let i = 0; i < 4; i++) await press(setup, "j");
183
- await press(setup, "x");
184
-
185
- // Assert - the rail shows a removal card titled REJECT · me for the change
186
- await waitForText(setup, "REJECT · me");
187
- expect(server.core.sessionGet(withFiles.id).workingCopy).toBe("");
188
-
189
- // Act - undo restores it (no selection, so the last item)
190
- await press(setup, "u");
191
-
192
- // Assert - the change returns to the curated diff (working copy cleared)
193
- await waitForText(setup, "removal restored");
194
- expect(server.core.sessionGet(withFiles.id).workingCopy).toBeUndefined();
195
- });
196
-
197
- test("a selected removal card shows an undo button that restores it on click", async () => {
198
- // Arrange - a diff session that carries full file contents
199
- const withFiles = server.core.sessionCreate({
200
- workspace: { repoRoot: "/repo", branch: "main" },
201
- artifact: {
202
- type: "diff",
203
- content: PATCH,
204
- meta: { title: "working tree" },
205
- files: [
206
- {
207
- path: "src/store.ts",
208
- oldContents: "export class Store {\n private items = [];\n}\n",
209
- newContents: "export class Store {\n private items = new Map();\n}\n",
210
- status: "modified",
211
- },
212
- ],
213
- },
214
- });
215
- const setup = await testRender(<App home={home} sessionId={withFiles.id} />, {
216
- width: 120,
217
- height: 30,
218
- });
219
-
220
- await waitForText(setup, "cueloop");
221
-
222
- // Act - reject the change; the removal card lands unselected (no undo button)
223
- for (let i = 0; i < 4; i++) await press(setup, "j");
224
- await press(setup, "x");
225
- await waitForText(setup, "REJECT · me");
226
- expect(setup.captureCharFrame()).not.toContain("undo");
227
-
228
- // Act - click the card to select it; the undo button appears
229
- const clickText = async (needle: string): Promise<void> => {
230
- const lines = setup.captureCharFrame().split("\n");
231
- const row = lines.findIndex((line) => line.includes(needle));
232
-
233
- await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
234
- };
235
-
236
- await clickText("REJECT · me");
237
- await waitForText(setup, "undo");
238
-
239
- // Act - click the undo button
240
- await clickText("undo");
241
-
242
- // Assert - the change returns to the curated diff (working copy cleared)
243
- await waitForText(setup, "removal restored");
244
- expect(server.core.sessionGet(withFiles.id).workingCopy).toBeUndefined();
209
+ // Assert - the context default opens the column for a diff, so both files show
210
+ await waitForText(setup, "config.ts");
211
+ expect(setup.captureCharFrame()).toContain("store.ts");
245
212
  });
246
213
  });
@@ -10,7 +10,6 @@ import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
12
  import { DEFAULT_QUICK_ACTIONS, quickActionBody } from "./config";
13
- import { DARK } from "./theme";
14
13
  import {
15
14
  clickText,
16
15
  dragText,
@@ -100,14 +99,13 @@ describe("share button", () => {
100
99
  const setup = await renderApp();
101
100
 
102
101
  // Assert
103
- // both word-buttons ride the same header row, so Share sits next to Edit
102
+ // both word-buttons ride the header's thread segment, so share sits next to edit
104
103
  const headerLine = setup
105
104
  .captureCharFrame()
106
105
  .split("\n")
107
- .find((line) => line.includes("submitted by"));
106
+ .find((line) => line.includes("edit"));
108
107
 
109
- expect(headerLine).toContain("Edit");
110
- expect(headerLine).toContain("Share");
108
+ expect(headerLine).toContain("share");
111
109
  });
112
110
 
113
111
  test("a read-only viewer (a plan shared over ssh) never sees the Share button", async () => {
@@ -120,7 +118,7 @@ describe("share button", () => {
120
118
  await waitForText(viewer, "cueloop");
121
119
 
122
120
  // Assert
123
- expect(viewer.captureCharFrame()).not.toContain("Share");
121
+ expect(viewer.captureCharFrame()).not.toContain(" share ");
124
122
  });
125
123
 
126
124
  test("a resolved plan hides Edit and Share (no re-sharing a finished review)", async () => {
@@ -130,16 +128,13 @@ describe("share button", () => {
130
128
  // Act
131
129
  const setup = await renderApp();
132
130
 
133
- await waitForText(setup, "submitted by");
131
+ await waitForText(setup, "Migration Plan");
134
132
 
135
133
  // Assert - the owner toolbar is gone once the review is resolved
136
- const headerLine = setup
137
- .captureCharFrame()
138
- .split("\n")
139
- .find((line) => line.includes("submitted by"));
134
+ const frame = setup.captureCharFrame();
140
135
 
141
- expect(headerLine).not.toContain("Edit");
142
- expect(headerLine).not.toContain("Share");
136
+ expect(frame).not.toContain(" edit ");
137
+ expect(frame).not.toContain(" share ");
143
138
  });
144
139
  });
145
140
 
@@ -149,7 +144,7 @@ describe("edit affordance", () => {
149
144
  const owner = await renderApp();
150
145
 
151
146
  // Assert
152
- expect(owner.captureCharFrame()).toContain("Edit");
147
+ expect(owner.captureCharFrame()).toContain(" edit ");
153
148
  });
154
149
 
155
150
  test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
@@ -162,32 +157,7 @@ describe("edit affordance", () => {
162
157
  await waitForText(viewer, "cueloop");
163
158
 
164
159
  // Assert
165
- expect(viewer.captureCharFrame()).not.toContain("Edit");
166
- });
167
- });
168
-
169
- describe("attribution", () => {
170
- test("a collaborator's note shows their name in the card border", async () => {
171
- // Arrange - a pulled collaborator note carries an author fingerprint
172
- const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
173
- const blocks = parseBlocks(PLAN);
174
- const contextBlockIndex = blocks.findIndex((block) =>
175
- block.text.startsWith("The daemon persists"),
176
- );
177
-
178
- server.core.sessionAnnotate(session.id, {
179
- id: "collab-1",
180
- kind: "comment",
181
- anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
182
- body: "who owns retries?",
183
- author: "SHA256:1a2b3c4d5e6f",
184
- });
185
-
186
- // Act
187
- const setup = await renderApp();
188
-
189
- // Assert - the short author handle rides the card border
190
- await waitForText(setup, "1a2b3c4d");
160
+ expect(viewer.captureCharFrame()).not.toContain(" edit ");
191
161
  });
192
162
  });
193
163
 
@@ -299,171 +269,20 @@ describe("compose newline convention", () => {
299
269
  });
300
270
  });
301
271
 
302
- describe("the document selects, the rail edits", () => {
303
- test("option+e edits the focused card in place; enter saves through the controller", async () => {
304
- // Arrange
305
- const setup = await renderApp();
306
-
307
- await clickText(setup, "daemon");
308
- await type(setup, "Needs a citation.");
309
- await pressKey(setup, "RETURN", { meta: true });
310
- await waitForText(setup, "COMMENT · me");
311
-
312
- // Act: focus the card, then turn its body into an input in place
313
- await pressKey(setup, "n", { meta: true });
314
- await pressKey(setup, "e", { meta: true });
315
- await type(setup, " And a link.");
316
- await press(setup, "enter");
317
-
318
- // Assert
319
- await waitForState(
320
- setup,
321
- () =>
322
- server.core.sessionGet(session.id).annotations[0]!.body === "Needs a citation. And a link.",
323
- );
324
- });
325
-
326
- test("activating a collaborator's card opens rename, not a body edit", async () => {
327
- // Arrange - a pulled collaborator note carries an author fingerprint
328
- const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
329
- const blocks = parseBlocks(PLAN);
330
- const contextBlockIndex = blocks.findIndex((block) =>
331
- block.text.startsWith("The daemon persists"),
332
- );
333
-
334
- server.core.sessionAnnotate(session.id, {
335
- id: "collab-1",
336
- kind: "comment",
337
- anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
338
- body: "who owns retries?",
339
- author: "SHA256:1a2b3c4d5e6f",
340
- });
341
- const setup = await renderApp();
342
-
343
- await waitForText(setup, "1a2b3c4d");
344
-
345
- // Act - focus the collaborator card, then activate it
346
- await pressKey(setup, "n", { meta: true });
347
- await pressKey(setup, "e", { meta: true });
348
-
349
- // Assert - the rename prompt opens; the body never becomes an editor
350
- await waitForText(setup, "Rename author");
351
- });
352
-
353
- test("option+backspace deletes the focused card and un-paints the document mark", async () => {
354
- // Arrange
355
- const setup = await renderApp();
356
-
357
- await dragText(setup, "daemon", "daemon persists", "daemon".length);
358
- await type(setup, "Delete me.");
359
- await pressKey(setup, "RETURN", { meta: true });
360
- await waitForText(setup, "COMMENT · me");
361
- expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
362
- await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
363
-
364
- // Act
365
- await pressKey(setup, "n", { meta: true });
366
- await pressKey(setup, "BACKSPACE", { meta: true });
367
-
368
- // Assert
369
- await waitForTextGone(setup, "COMMENT · me");
370
- expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
371
- expect(backgroundsOf(setup, "daemon")).not.toContain(THREAD_MARK);
372
- });
373
- });
374
-
375
- describe("plan cut removals", () => {
376
- test("option+x cuts the caret's block into a rail removal card and option+u restores it", async () => {
377
- // Arrange
378
- const setup = await renderApp();
379
-
380
- await clickText(setup, "daemon");
381
-
382
- // Act - no card is focused, so the cut takes the block under the caret
383
- await pressKey(setup, "x", { meta: true });
384
-
385
- // Assert - the rail shows a removal card titled CUT · me for the cut block
386
- await waitForText(setup, "CUT · me");
387
- await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy !== undefined);
388
-
389
- // Act - undo restores it (no selection, so the last removal)
390
- await pressKey(setup, "u", { meta: true });
391
-
392
- // Assert - the block returns and the working copy is pristine again
393
- await waitForText(setup, "removal restored");
394
- await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
395
- });
396
- });
397
-
398
- describe("addressed annotations leave the open list", () => {
399
- test("a revision that addresses one card hides it, un-paints it, and shows the count", async () => {
400
- // Arrange: two annotations through the daemon, then a revision addressing one
401
- const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
402
- const blocks = parseBlocks(PLAN);
403
- const contextBlockIndex = blocks.findIndex((block) =>
404
- block.text.startsWith("The daemon persists"),
405
- );
406
-
407
- server.core.sessionAnnotate(session.id, {
408
- id: "a_settled",
409
- kind: "comment",
410
- anchor: makeAnchor(blocks, contextBlockIndex, 0, 10), // "The daemon"
411
- body: "settled note",
412
- });
413
- server.core.sessionAnnotate(session.id, {
414
- id: "a_open",
415
- kind: "comment",
416
- anchor: makeAnchor(blocks, contextBlockIndex, 11, 19), // "persists"
417
- body: "still open note",
418
- });
419
- server.core.sessionSubmitRevision(session.id, PLAN, ["a_settled"]);
420
-
421
- // Act
422
- const setup = await renderApp();
423
-
424
- await waitForText(setup, "still open note");
425
-
426
- // Assert: the open card renders, the addressed one is gone behind the count
427
- const frame = setup.captureCharFrame();
428
-
429
- expect(frame).toContain("✓ 1 addressed by revision");
430
- expect(frame).not.toContain("settled note");
431
- expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
432
- expect(backgroundsOf(setup, "The daemon")).not.toContain(DARK.markCommentBackground); // no highlight paint
433
- });
434
- });
435
-
436
272
  describe("sheet header", () => {
437
- test("shows the submitting agent, the revision, and the Edit and Share word-buttons", async () => {
438
- // Arrange
439
- const setup = await renderApp();
440
-
441
- // Assert
442
- const frame = setup.captureCharFrame();
443
-
444
- expect(frame).toContain("submitted by agent/worker-3");
445
- expect(frame).toContain("rev 1");
446
- expect(frame).toContain("Edit");
447
- expect(frame).toContain("Share");
448
- });
449
-
450
- test("the Agent tab shows the text-only harness launcher buttons", async () => {
273
+ test("mirrors the thread name and carries the Edit and Share word-buttons", async () => {
451
274
  // Arrange
452
275
  const setup = await renderApp();
453
- const lines = setup.captureCharFrame().split("\n");
454
- const tabRow = lines.findIndex((line) => line.includes("Review") && line.includes("Agent"));
455
- const tabColumn = lines[tabRow]!.indexOf("Agent");
456
-
457
- // Act
458
- await setup.mockMouse.click(tabColumn + 1, tabRow);
459
- await setup.renderOnce();
460
276
 
461
- // Assert - the three launcher buttons, in order
462
- await waitForText(setup, "Claude Code");
277
+ // Assert - the minimal header shows the thread title (mirror) plus the owner
278
+ // actions; the revision and submitter metadata are no longer in the header
463
279
  const frame = setup.captureCharFrame();
464
280
 
465
- expect(frame).toContain("Pi");
466
- expect(frame).toContain("OpenAI Codex");
281
+ expect(frame).toContain("Migration Plan");
282
+ expect(frame).not.toContain("submitted by");
283
+ expect(frame).not.toContain("rev 1");
284
+ expect(frame).toContain(" edit ");
285
+ expect(frame).toContain(" share ");
467
286
  });
468
287
  });
469
288
 
@@ -472,8 +291,9 @@ describe("quick-actions settings editor", () => {
472
291
  // Arrange
473
292
  const setup = await renderApp();
474
293
 
475
- // Act - open Settings, enter Actions, expand the first action, type into its input
476
- await clickText(setup, "menu");
294
+ // Act - open Settings from the top-left gear, enter Actions, expand the first action, type
295
+ await setup.mockMouse.click(1, 0);
296
+ await waitForText(setup, "Keybinds");
477
297
  await clickText(setup, "Settings");
478
298
  await clickText(setup, "Actions");
479
299
  await clickText(setup, "Zoom out, research in depth");
@@ -502,15 +322,13 @@ describe("edit-exit reconciliation", () => {
502
322
  await clickText(setup, "daemon");
503
323
  await type(setup, "Anchor me to the doomed passage.");
504
324
  await pressKey(setup, "RETURN", { meta: true });
505
- await waitForText(setup, "COMMENT · me");
325
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
506
326
 
507
327
  // Act
508
328
  await pressKey(setup, "e", { ctrl: true });
509
329
 
510
- // Assert
330
+ // Assert - the thread view banner reports the orphaned annotation
511
331
  await waitForText(setup, "1 annotation no longer match - the passage was removed.");
512
- // the rail card flips to ORPHANED once the working-copy refresh lands
513
- await waitForText(setup, "· ORPHANED");
514
332
  // the annotation is NOT deleted: the feedback serializer handles orphans
515
333
  expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
516
334
  } finally {
@@ -105,7 +105,7 @@ async function clickText(setup: Setup, needle: string): Promise<void> {
105
105
  }
106
106
 
107
107
  describe("prototype review", () => {
108
- test("click an element, comment on it, and it lands in the rail", async () => {
108
+ test("click an element, comment on it, and it saves to the daemon", async () => {
109
109
  // Arrange - wait until the fake browser produced its first screenshot, so
110
110
  // the sheet is interactive before the click
111
111
  const setup = await renderApp();
@@ -135,13 +135,6 @@ describe("prototype review", () => {
135
135
  expect(stored.kind).toBe("comment");
136
136
  expect(stored.body).toBe("tighten the padding");
137
137
  expect(stored.anchor.selector).toBe("main > div.card");
138
-
139
- // and it renders in the rail
140
- await settle(setup);
141
- const frame = setup.captureCharFrame();
142
-
143
- expect(frame).toContain("tighten the padding");
144
- expect(frame).toContain("COMMENT");
145
138
  });
146
139
 
147
140
  test("escape cancels the compose, matching the plan composer", async () => {