@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60

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 (98) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +7 -0
  3. package/src/App.diff.test.tsx +11 -0
  4. package/src/App.export.test.tsx +5 -0
  5. package/src/App.plan-review.test.tsx +42 -0
  6. package/src/App.prototype.test.tsx +11 -0
  7. package/src/App.readonly.test.tsx +10 -0
  8. package/src/App.rendering.test.tsx +4 -0
  9. package/src/App.review-panel.test.tsx +7 -0
  10. package/src/App.submit-confirm.test.tsx +11 -0
  11. package/src/App.test.tsx +13 -0
  12. package/src/App.toast.test.tsx +2 -0
  13. package/src/App.tsx +27 -0
  14. package/src/App.walk.test.tsx +10 -0
  15. package/src/app-key-state.ts +1 -0
  16. package/src/app-screens.tsx +4 -0
  17. package/src/app-view-model.ts +21 -0
  18. package/src/attribution.ts +4 -0
  19. package/src/clipboard.ts +3 -0
  20. package/src/components/AnnotationCard.tsx +9 -0
  21. package/src/components/Breadcrumb.tsx +1 -0
  22. package/src/components/CodeBlock.tsx +1 -0
  23. package/src/components/CompletionOverlay.tsx +1 -0
  24. package/src/components/ConfirmCard.tsx +2 -0
  25. package/src/components/ConfirmDialog.tsx +2 -0
  26. package/src/components/DiffSheet.scroll.test.tsx +12 -0
  27. package/src/components/DiffSheet.syntax.test.tsx +3 -0
  28. package/src/components/DiffSheet.tsx +19 -0
  29. package/src/components/InboxList.tsx +2 -0
  30. package/src/components/KeybindsDialog.tsx +1 -0
  31. package/src/components/MarkerPopover.tsx +1 -0
  32. package/src/components/MenuBar.tsx +1 -0
  33. package/src/components/PlanSheet.tsx +26 -0
  34. package/src/components/PromptDialog.tsx +2 -0
  35. package/src/components/PrototypeSheet.tsx +27 -0
  36. package/src/components/ReviewPanel.tsx +2 -0
  37. package/src/components/ReviewRail.tsx +1 -0
  38. package/src/components/SettingsDialog.tsx +6 -0
  39. package/src/components/SettingsRows.tsx +4 -0
  40. package/src/components/Toast.tsx +1 -0
  41. package/src/components/agent-launcher.test.tsx +1 -0
  42. package/src/components/agent-launcher.tsx +2 -0
  43. package/src/components/diff-sheet-layout.test.ts +1 -0
  44. package/src/components/diff-sheet-layout.ts +17 -0
  45. package/src/components/plan-sheet-run-style.ts +4 -0
  46. package/src/components/primitives/Button.tsx +1 -0
  47. package/src/components/primitives/Card.tsx +1 -0
  48. package/src/components/primitives/Dialog.tsx +2 -0
  49. package/src/components/primitives/StatusBar.tsx +1 -0
  50. package/src/components/primitives/Tabs.tsx +4 -0
  51. package/src/components/quick-actions-editor.tsx +2 -0
  52. package/src/components/stories-app.tsx +1 -0
  53. package/src/components/stories.test.tsx +4 -0
  54. package/src/components/story.ts +6 -0
  55. package/src/components/syntax-highlight.ts +10 -0
  56. package/src/components/terminal-pane.ts +12 -0
  57. package/src/components/theme-context.tsx +1 -0
  58. package/src/components/truncate-text.ts +1 -0
  59. package/src/config.test.ts +18 -0
  60. package/src/config.ts +27 -0
  61. package/src/diff-hunk-curate.ts +20 -0
  62. package/src/diff-intraline.test.ts +1 -0
  63. package/src/diff-intraline.ts +14 -0
  64. package/src/diff-syntax.ts +22 -0
  65. package/src/editor.test.ts +6 -0
  66. package/src/editor.ts +12 -0
  67. package/src/ghostty-terminal.test.ts +2 -0
  68. package/src/ghostty-terminal.ts +8 -0
  69. package/src/herdr.test.ts +2 -0
  70. package/src/herdr.ts +3 -0
  71. package/src/integrations.ts +2 -0
  72. package/src/intent-dispatch.test.ts +6 -0
  73. package/src/intent-dispatch.ts +37 -0
  74. package/src/key-bindings.test.ts +4 -0
  75. package/src/key-bindings.ts +22 -0
  76. package/src/keymap.test.ts +13 -0
  77. package/src/keymap.ts +43 -0
  78. package/src/kitty-image.ts +9 -0
  79. package/src/prototype-browser.test.ts +1 -0
  80. package/src/prototype-browser.ts +22 -0
  81. package/src/pty.test.ts +3 -0
  82. package/src/pty.ts +15 -0
  83. package/src/run.ts +1 -0
  84. package/src/serve.test.ts +1 -0
  85. package/src/serve.ts +3 -0
  86. package/src/session-controller.curate.test.ts +7 -0
  87. package/src/session-controller.share.test.ts +11 -0
  88. package/src/session-controller.ts +79 -0
  89. package/src/share.ts +7 -0
  90. package/src/test-support.test.ts +5 -0
  91. package/src/test-support.ts +10 -0
  92. package/src/theme-presets.ts +1 -0
  93. package/src/use-settings-dialog.tsx +12 -0
  94. package/src/view-diff.test.ts +3 -0
  95. package/src/view-diff.ts +5 -0
  96. package/src/view-plan.test.ts +7 -0
  97. package/src/view-plan.ts +59 -0
  98. package/src/walk.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.59",
3
+ "version": "0.1.0-alpha.60",
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,10 +26,10 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@cueloop/daemon": "0.1.0-alpha.59",
30
- "@cueloop/extension-api": "0.1.0-alpha.59",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.59",
32
- "@cueloop/schema": "0.1.0-alpha.59",
29
+ "@cueloop/daemon": "0.1.0-alpha.60",
30
+ "@cueloop/extension-api": "0.1.0-alpha.60",
31
+ "@cueloop/integration-obsidian": "0.1.0-alpha.60",
32
+ "@cueloop/schema": "0.1.0-alpha.60",
33
33
  "@opentui/core": "^0.5.8",
34
34
  "@opentui/keymap": "0.5.8",
35
35
  "@opentui/react": "^0.5.8",
@@ -47,7 +47,9 @@ async function renderApp(onExit?: (code: number) => void) {
47
47
  height: 30,
48
48
  },
49
49
  );
50
+
50
51
  await waitForText(setup, "cueloop");
52
+
51
53
  return setup;
52
54
  }
53
55
 
@@ -70,6 +72,7 @@ describe("completion overlay", () => {
70
72
 
71
73
  // Assert
72
74
  const frame = await waitForText(setup, "closing in 5s");
75
+
73
76
  expect(frame).toContain("review approved");
74
77
  expect(frame).toContain("close [return]");
75
78
  expect(frame).toContain("return to plan [esc]");
@@ -84,6 +87,7 @@ describe("completion overlay", () => {
84
87
  test("a remembers the countdown as the persisted default", async () => {
85
88
  // Arrange
86
89
  const setup = await renderApp();
90
+
87
91
  await submitApprove(setup);
88
92
  await waitForText(setup, "closing in 5s");
89
93
 
@@ -105,6 +109,7 @@ describe("completion overlay", () => {
105
109
  test("esc dismisses to the resolved read-only view", async () => {
106
110
  // Arrange
107
111
  const setup = await renderApp();
112
+
108
113
  await submitApprove(setup);
109
114
  await waitForText(setup, "closing in 5s");
110
115
 
@@ -115,6 +120,7 @@ describe("completion overlay", () => {
115
120
 
116
121
  // Assert
117
122
  const frame = await waitForText(setup, "resolved: approve");
123
+
118
124
  expect(frame).not.toContain("closing in");
119
125
  });
120
126
 
@@ -123,6 +129,7 @@ describe("completion overlay", () => {
123
129
  writeFileSync(configPath, "[ui]\nauto_close = 1\n");
124
130
  let exited = -1;
125
131
  const setup = await renderApp((code) => (exited = code));
132
+
126
133
  await submitApprove(setup);
127
134
  await waitForText(setup, "closing in 1s");
128
135
  expect(exited).toBe(-1);
@@ -48,7 +48,9 @@ async function renderApp() {
48
48
  width: 120,
49
49
  height: 30,
50
50
  });
51
+
51
52
  await waitForText(setup, "cueloop");
53
+
52
54
  return setup;
53
55
  }
54
56
 
@@ -59,6 +61,7 @@ describe("diff review", () => {
59
61
 
60
62
  // Assert
61
63
  const frame = setup.captureCharFrame();
64
+
62
65
  expect(frame).toContain("■ src/store.ts");
63
66
  expect(frame).toContain("@@ -1,4 +1,4 @@");
64
67
  expect(frame).toContain("- private items = [];");
@@ -85,6 +88,7 @@ describe("diff review", () => {
85
88
  // inline annotation card rendered under the line
86
89
  await waitForText(setup, "◆ Map needs an eviction story.");
87
90
  const stored = server.core.sessionGet(session.id);
91
+
88
92
  expect(stored.annotations.length).toBe(1);
89
93
  expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
90
94
 
@@ -95,6 +99,7 @@ describe("diff review", () => {
95
99
  // Assert
96
100
  await waitForText(setup, "feedback sent");
97
101
  const resolved = server.core.sessionGet(session.id);
102
+
98
103
  expect(resolved.verdict!.feedback).toContain("new Map()");
99
104
  expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
100
105
  });
@@ -132,6 +137,7 @@ describe("diff review", () => {
132
137
  width: 120,
133
138
  height: 30,
134
139
  });
140
+
135
141
  await waitForText(setup, "cueloop");
136
142
 
137
143
  // Act - move to the added line and reject its change
@@ -142,6 +148,7 @@ describe("diff review", () => {
142
148
  // Assert - the single change is gone, so the curated working copy is empty
143
149
  await waitForText(setup, "change rejected");
144
150
  const stored = server.core.sessionGet(withFiles.id);
151
+
145
152
  expect(stored.workingCopy).toBe("");
146
153
  });
147
154
 
@@ -167,6 +174,7 @@ describe("diff review", () => {
167
174
  width: 120,
168
175
  height: 30,
169
176
  });
177
+
170
178
  await waitForText(setup, "cueloop");
171
179
 
172
180
  // Act - reject the change under the added line
@@ -208,6 +216,7 @@ describe("diff review", () => {
208
216
  width: 120,
209
217
  height: 30,
210
218
  });
219
+
211
220
  await waitForText(setup, "cueloop");
212
221
 
213
222
  // Act - reject the change; the removal card lands unselected (no undo button)
@@ -220,8 +229,10 @@ describe("diff review", () => {
220
229
  const clickText = async (needle: string): Promise<void> => {
221
230
  const lines = setup.captureCharFrame().split("\n");
222
231
  const row = lines.findIndex((line) => line.includes(needle));
232
+
223
233
  await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
224
234
  };
235
+
225
236
  await clickText("REJECT · me");
226
237
  await waitForText(setup, "undo");
227
238
 
@@ -35,6 +35,7 @@ beforeEach(() => {
35
35
  vault = join(home, "vault");
36
36
  mkdirSync(vault);
37
37
  const configPath = join(home, "config.toml");
38
+
38
39
  writeFileSync(
39
40
  configPath,
40
41
  `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`,
@@ -64,6 +65,7 @@ describe("obsidian export on resolve", () => {
64
65
  width: 120,
65
66
  height: 32,
66
67
  });
68
+
67
69
  await waitForText(setup, "cueloop");
68
70
 
69
71
  // Act
@@ -79,13 +81,16 @@ describe("obsidian export on resolve", () => {
79
81
  // the export is an async round-trip after resolve; the status line lands
80
82
  // on a later render than the file write
81
83
  const dir = join(vault, "cueloop");
84
+
82
85
  await waitForState(setup, () => existsSync(dir));
83
86
  await waitForText(setup, "exported to");
84
87
 
85
88
  const notes = readdirSync(dir);
89
+
86
90
  expect(notes.length).toBe(1);
87
91
  expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
88
92
  const written = readFileSync(join(dir, notes[0]!), "utf8");
93
+
89
94
  expect(written).toContain("source: cueloop");
90
95
  expect(written).toContain(`session: ${session.id}`);
91
96
  expect(written).toContain("verdict: approve");
@@ -63,7 +63,9 @@ async function renderApp() {
63
63
  width: 120,
64
64
  height: 32,
65
65
  });
66
+
66
67
  await waitForText(setup, "cueloop");
68
+
67
69
  return setup;
68
70
  }
69
71
 
@@ -75,29 +77,35 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
75
77
  */
76
78
  function backgroundsOf(setup: Setup, needle: string): string[] {
77
79
  const backgrounds: string[] = [];
80
+
78
81
  for (const line of setup.captureSpans().lines) {
79
82
  for (const span of line.spans) {
80
83
  if (!span.text.includes(needle)) continue;
81
84
  const [red, green, blue] = span.bg.toInts();
85
+
82
86
  backgrounds.push(
83
87
  "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
84
88
  );
85
89
  }
86
90
  }
91
+
87
92
  return backgrounds;
88
93
  }
89
94
 
90
95
  function foregroundsOf(setup: Setup, needle: string): string[] {
91
96
  const foregrounds: string[] = [];
97
+
92
98
  for (const line of setup.captureSpans().lines) {
93
99
  for (const span of line.spans) {
94
100
  if (!span.text.includes(needle)) continue;
95
101
  const [red, green, blue] = span.fg.toInts();
102
+
96
103
  foregrounds.push(
97
104
  "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
98
105
  );
99
106
  }
100
107
  }
108
+
101
109
  return foregrounds;
102
110
  }
103
111
 
@@ -111,6 +119,7 @@ async function toContextParagraph(setup: Setup): Promise<void> {
111
119
  async function clickText(setup: Setup, needle: string): Promise<void> {
112
120
  const lines = setup.captureCharFrame().split("\n");
113
121
  const row = lines.findIndex((line) => line.includes(needle));
122
+
114
123
  await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
115
124
  await setup.renderOnce();
116
125
  }
@@ -126,6 +135,7 @@ describe("share button", () => {
126
135
  .captureCharFrame()
127
136
  .split("\n")
128
137
  .find((line) => line.includes("submitted by"));
138
+
129
139
  expect(headerLine).toContain("Edit");
130
140
  expect(headerLine).toContain("Share");
131
141
  });
@@ -136,6 +146,7 @@ describe("share button", () => {
136
146
  width: 120,
137
147
  height: 32,
138
148
  });
149
+
139
150
  await waitForText(viewer, "cueloop");
140
151
 
141
152
  // Assert
@@ -148,6 +159,7 @@ describe("share button", () => {
148
159
 
149
160
  // Act
150
161
  const setup = await renderApp();
162
+
151
163
  await waitForText(setup, "submitted by");
152
164
 
153
165
  // Assert - the owner toolbar is gone once the review is resolved
@@ -155,6 +167,7 @@ describe("share button", () => {
155
167
  .captureCharFrame()
156
168
  .split("\n")
157
169
  .find((line) => line.includes("submitted by"));
170
+
158
171
  expect(headerLine).not.toContain("Edit");
159
172
  expect(headerLine).not.toContain("Share");
160
173
  });
@@ -175,6 +188,7 @@ describe("edit affordance", () => {
175
188
  width: 120,
176
189
  height: 32,
177
190
  });
191
+
178
192
  await waitForText(viewer, "cueloop");
179
193
 
180
194
  // Assert
@@ -190,6 +204,7 @@ describe("attribution", () => {
190
204
  const contextBlockIndex = blocks.findIndex((block) =>
191
205
  block.text.startsWith("The daemon persists"),
192
206
  );
207
+
193
208
  server.core.sessionAnnotate(session.id, {
194
209
  id: "collab-1",
195
210
  kind: "comment",
@@ -210,6 +225,7 @@ describe("selection feeds the annotation quote", () => {
210
225
  test("v anchors, l extends, c composes with the char-precise quote", async () => {
211
226
  // Arrange
212
227
  const setup = await renderApp();
228
+
213
229
  await toContextParagraph(setup);
214
230
 
215
231
  // Act
@@ -227,6 +243,7 @@ describe("selection feeds the annotation quote", () => {
227
243
  // Assert
228
244
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
229
245
  const stored = server.core.sessionGet(session.id);
246
+
230
247
  expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
231
248
  });
232
249
 
@@ -236,6 +253,7 @@ describe("selection feeds the annotation quote", () => {
236
253
  const lines = setup.captureCharFrame().split("\n");
237
254
  const row = lines.findIndex((line) => line.includes("persists sessions"));
238
255
  const startColumn = lines[row]!.indexOf("persists");
256
+
239
257
  expect(row).toBeGreaterThan(-1);
240
258
 
241
259
  // Act
@@ -254,6 +272,7 @@ describe("selection feeds the annotation quote", () => {
254
272
  // Assert
255
273
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
256
274
  const stored = server.core.sessionGet(session.id);
275
+
257
276
  expect(stored.annotations[0]!.anchor.quote).toBe("persists sessions");
258
277
  });
259
278
  });
@@ -262,6 +281,7 @@ describe("inline compose keeps the anchor painted", () => {
262
281
  test("the selection stays painted while composing; cancel un-paints; save converts to the comment highlight", async () => {
263
282
  // Arrange
264
283
  const setup = await renderApp();
284
+
265
285
  await toContextParagraph(setup);
266
286
 
267
287
  // Act
@@ -296,6 +316,7 @@ describe("inline compose keeps the anchor painted", () => {
296
316
  await settle(setup);
297
317
  // the whole cursor block is the anchor here; check the highlight landed
298
318
  const stored = server.core.sessionGet(session.id);
319
+
299
320
  expect(stored.annotations.length).toBe(1);
300
321
  expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(
301
322
  DARK.markCommentBackground,
@@ -308,6 +329,7 @@ describe("inline compose keeps the anchor painted", () => {
308
329
  test("clicking the Cancel word-button closes the compose box", async () => {
309
330
  // Arrange
310
331
  const setup = await renderApp();
332
+
311
333
  await toContextParagraph(setup);
312
334
  await press(setup, "v");
313
335
  await press(setup, "l");
@@ -324,6 +346,7 @@ describe("inline compose keeps the anchor painted", () => {
324
346
  test("clicking Cancel in a saved card's edit composer closes it", async () => {
325
347
  // Arrange
326
348
  const setup = await renderApp();
349
+
327
350
  await toContextParagraph(setup);
328
351
  await press(setup, "v");
329
352
  await press(setup, "l");
@@ -355,7 +378,9 @@ describe("compose newline convention", () => {
355
378
  height: 32,
356
379
  otherModifiersMode: true,
357
380
  });
381
+
358
382
  await waitForText(setup, "cueloop");
383
+
359
384
  return setup;
360
385
  }
361
386
 
@@ -370,6 +395,7 @@ describe("compose newline convention", () => {
370
395
  test("option+return inserts a newline; a bare return submits the multiline body", async () => {
371
396
  // Arrange
372
397
  const setup = await renderModifierAwareApp();
398
+
373
399
  await toContextParagraph(setup);
374
400
  await press(setup, "v");
375
401
  await press(setup, "l");
@@ -396,6 +422,7 @@ describe("compose newline convention", () => {
396
422
  test("shift+return still inserts a newline (existing muscle memory)", async () => {
397
423
  // Arrange
398
424
  const setup = await renderModifierAwareApp();
425
+
399
426
  await toContextParagraph(setup);
400
427
  await press(setup, "c");
401
428
 
@@ -418,6 +445,7 @@ describe("compose newline convention", () => {
418
445
  test("escape cancels the composer without saving", async () => {
419
446
  // Arrange
420
447
  const setup = await renderModifierAwareApp();
448
+
421
449
  await toContextParagraph(setup);
422
450
  await press(setup, "c");
423
451
  await waitForText(setup, 'comment on "');
@@ -436,6 +464,7 @@ describe("the document selects, the rail edits", () => {
436
464
  test("e edits the selected card in place; enter saves through the controller", async () => {
437
465
  // Arrange
438
466
  const setup = await renderApp();
467
+
439
468
  await toContextParagraph(setup);
440
469
  await press(setup, "c");
441
470
  await type(setup, "Needs a citation.");
@@ -463,6 +492,7 @@ describe("the document selects, the rail edits", () => {
463
492
  const contextBlockIndex = blocks.findIndex((block) =>
464
493
  block.text.startsWith("The daemon persists"),
465
494
  );
495
+
466
496
  server.core.sessionAnnotate(session.id, {
467
497
  id: "collab-1",
468
498
  kind: "comment",
@@ -471,6 +501,7 @@ describe("the document selects, the rail edits", () => {
471
501
  author: "SHA256:1a2b3c4d5e6f",
472
502
  });
473
503
  const setup = await renderApp();
504
+
474
505
  await waitForText(setup, "1a2b3c4d");
475
506
 
476
507
  // Act - focus the collaborator card, then activate it
@@ -484,6 +515,7 @@ describe("the document selects, the rail edits", () => {
484
515
  test("x deletes the selected card and un-paints the document highlight", async () => {
485
516
  // Arrange
486
517
  const setup = await renderApp();
518
+
487
519
  await toContextParagraph(setup);
488
520
  await press(setup, "c");
489
521
  await type(setup, "Delete me.");
@@ -506,6 +538,7 @@ describe("plan cut removals", () => {
506
538
  test("a cut block becomes a rail removal card and u restores it", async () => {
507
539
  // Arrange
508
540
  const setup = await renderApp();
541
+
509
542
  await toContextParagraph(setup);
510
543
 
511
544
  // Act - no card is selected, so x cuts the block under the cursor
@@ -532,6 +565,7 @@ describe("addressed annotations leave the open list", () => {
532
565
  const contextBlockIndex = blocks.findIndex((block) =>
533
566
  block.text.startsWith("The daemon persists"),
534
567
  );
568
+
535
569
  server.core.sessionAnnotate(session.id, {
536
570
  id: "a_settled",
537
571
  kind: "comment",
@@ -548,10 +582,12 @@ describe("addressed annotations leave the open list", () => {
548
582
 
549
583
  // Act
550
584
  const setup = await renderApp();
585
+
551
586
  await waitForText(setup, "still open note");
552
587
 
553
588
  // Assert: the open card renders, the addressed one is gone behind the count
554
589
  const frame = setup.captureCharFrame();
590
+
555
591
  expect(frame).toContain("✓ 1 addressed by revision");
556
592
  expect(frame).not.toContain("settled note");
557
593
  expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
@@ -566,6 +602,7 @@ describe("sheet header", () => {
566
602
 
567
603
  // Assert
568
604
  const frame = setup.captureCharFrame();
605
+
569
606
  expect(frame).toContain("submitted by agent/worker-3");
570
607
  expect(frame).toContain("rev 1");
571
608
  expect(frame).toContain("Edit");
@@ -586,6 +623,7 @@ describe("sheet header", () => {
586
623
  // Assert - the three launcher buttons, in order
587
624
  await waitForText(setup, "Claude Code");
588
625
  const frame = setup.captureCharFrame();
626
+
589
627
  expect(frame).toContain("Pi");
590
628
  expect(frame).toContain("OpenAI Codex");
591
629
  });
@@ -615,12 +653,14 @@ describe("quick-actions settings editor", () => {
615
653
  describe("edit-exit reconciliation", () => {
616
654
  test("an edit that removes an anchored passage orphans the annotation and shows the banner", async () => {
617
655
  const script = join(home, "fake-editor.sh");
656
+
618
657
  await Bun.write(script, `#!/bin/sh\nsed -i '' '/^The daemon persists/d' "$1"\n`);
619
658
  Bun.spawnSync(["chmod", "+x", script]);
620
659
  process.env.CUELOOP_EDITOR = script;
621
660
  try {
622
661
  // Arrange
623
662
  const setup = await renderApp();
663
+
624
664
  await toContextParagraph(setup);
625
665
  await press(setup, "c");
626
666
  await type(setup, "Anchor me to the doomed passage.");
@@ -650,6 +690,7 @@ describe("marker-actions popover", () => {
650
690
  test("v shows the toolbar, a opens quick-actions, enter inserts the preset comment", async () => {
651
691
  // Arrange
652
692
  const setup = await renderApp();
693
+
653
694
  await toContextParagraph(setup);
654
695
 
655
696
  // Act - marking a span reveals the inline toolbar
@@ -666,6 +707,7 @@ describe("marker-actions popover", () => {
666
707
  // Assert - a comment annotation was created with the second default's body
667
708
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
668
709
  const stored = server.core.sessionGet(session.id);
710
+
669
711
  expect(stored.annotations[0]!.kind).toBe("comment");
670
712
  expect(stored.annotations[0]!.body).toBe(quickActionBody(DEFAULT_QUICK_ACTIONS[1]!));
671
713
  });
@@ -41,11 +41,13 @@ const fakeRenderer: PrototypeRenderer = {
41
41
  screenshot: async () => {
42
42
  rendered = true;
43
43
  screenshotCount += 1;
44
+
44
45
  return new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
45
46
  },
46
47
  elementAt: async () => FAKE_ELEMENT,
47
48
  scrollBy: async (deltaY) => {
48
49
  scrollDeltas.push(deltaY);
50
+
49
51
  return true;
50
52
  },
51
53
  close: async () => undefined,
@@ -86,7 +88,9 @@ async function renderApp() {
86
88
  width: 120,
87
89
  height: 32,
88
90
  });
91
+
89
92
  await waitForText(setup, "cueloop");
93
+
90
94
  return setup;
91
95
  }
92
96
 
@@ -95,6 +99,7 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
95
99
  async function clickText(setup: Setup, needle: string): Promise<void> {
96
100
  const lines = setup.captureCharFrame().split("\n");
97
101
  const row = lines.findIndex((line) => line.includes(needle));
102
+
98
103
  await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
99
104
  await setup.renderOnce();
100
105
  }
@@ -104,6 +109,7 @@ describe("prototype review", () => {
104
109
  // Arrange - wait until the fake browser produced its first screenshot, so
105
110
  // the sheet is interactive before the click
106
111
  const setup = await renderApp();
112
+
107
113
  await waitForState(setup, () => rendered);
108
114
  await settle(setup);
109
115
  // selecting an element must not re-screenshot the page (the popover-click
@@ -125,6 +131,7 @@ describe("prototype review", () => {
125
131
  // Assert - the annotation is stored against the element's selector
126
132
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
127
133
  const stored = server.core.sessionGet(session.id).annotations[0]!;
134
+
128
135
  expect(stored.kind).toBe("comment");
129
136
  expect(stored.body).toBe("tighten the padding");
130
137
  expect(stored.anchor.selector).toBe("main > div.card");
@@ -132,6 +139,7 @@ describe("prototype review", () => {
132
139
  // and it renders in the rail
133
140
  await settle(setup);
134
141
  const frame = setup.captureCharFrame();
142
+
135
143
  expect(frame).toContain("tighten the padding");
136
144
  expect(frame).toContain("COMMENT");
137
145
  });
@@ -139,6 +147,7 @@ describe("prototype review", () => {
139
147
  test("escape cancels the compose, matching the plan composer", async () => {
140
148
  // Arrange
141
149
  const setup = await renderApp();
150
+
142
151
  await waitForState(setup, () => rendered);
143
152
  await settle(setup);
144
153
  await setup.mockMouse.click(6, 6);
@@ -158,6 +167,7 @@ describe("prototype review", () => {
158
167
  test("wheel over the preview scrolls the page down, then up, and clears a selection", async () => {
159
168
  // Arrange
160
169
  const setup = await renderApp();
170
+
161
171
  await waitForState(setup, () => rendered);
162
172
  await settle(setup);
163
173
 
@@ -167,6 +177,7 @@ describe("prototype review", () => {
167
177
  await waitForText(setup, "comment");
168
178
  // scrolling moves the page, so it DOES re-screenshot (unlike a click)
169
179
  const screenshotsBeforeScroll = screenshotCount;
180
+
170
181
  await setup.mockMouse.scroll(6, 6, "down");
171
182
  await waitForState(setup, () => scrollDeltas.length === 1);
172
183
  await waitForTextGone(setup, "comment");
@@ -54,13 +54,16 @@ async function renderObserver() {
54
54
  width: 120,
55
55
  height: 32,
56
56
  });
57
+
57
58
  await waitForText(setup, "cueloop");
59
+
58
60
  return setup;
59
61
  }
60
62
 
61
63
  /** Session state that any mutating verb would change. */
62
64
  function snapshot() {
63
65
  const stored = server.core.sessionGet(session.id);
66
+
64
67
  return {
65
68
  annotations: stored.annotations.length,
66
69
  workingCopy: stored.workingCopy,
@@ -75,6 +78,7 @@ describe("observer rendering", () => {
75
78
 
76
79
  // Assert
77
80
  const frame = setup.captureCharFrame();
81
+
78
82
  expect(frame).toContain("· observer");
79
83
  expect(frame).toContain("menu");
80
84
  });
@@ -90,10 +94,12 @@ describe("observer verbs are blocked", () => {
90
94
  test(`${verb} (${key}) answers observer - read-only and mutates nothing`, async () => {
91
95
  const setup = await renderObserver();
92
96
  const before = snapshot();
97
+
93
98
  await press(setup, "j");
94
99
  await press(setup, "j");
95
100
  await press(setup, key);
96
101
  const frame = await waitForText(setup, "observer - read-only");
102
+
97
103
  // no overlay opened: compose/submit bars never appear
98
104
  expect(frame).not.toContain('comment on "');
99
105
  expect(frame).not.toContain("verdict ←/→");
@@ -114,6 +120,7 @@ describe("observer verbs are blocked", () => {
114
120
 
115
121
  // Assert
116
122
  const frame = setup.captureCharFrame();
123
+
117
124
  expect(frame).toContain("observer - read-only");
118
125
  expect(frame).not.toContain('comment on "');
119
126
  expect(snapshot().annotations).toBe(0);
@@ -133,6 +140,7 @@ describe("observer navigation still works", () => {
133
140
  // Assert
134
141
  const lines = setup.captureCharFrame().split("\n");
135
142
  const cursorLine = lines.find((line) => line.includes("▎"))!;
143
+
136
144
  expect(cursorLine).toContain("persists sessions");
137
145
  });
138
146
 
@@ -165,8 +173,10 @@ function hasBorderColor(setup: Awaited<ReturnType<typeof renderObserver>>, hex:
165
173
  const [red, green, blue] = span.fg.toInts();
166
174
  const rendered =
167
175
  "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
176
+
168
177
  if (rendered === hex) return true;
169
178
  }
170
179
  }
180
+
171
181
  return false;
172
182
  }
@@ -55,9 +55,11 @@ async function frame(): Promise<string> {
55
55
  width: 120,
56
56
  height: 36,
57
57
  });
58
+
58
59
  await waitForText(setup, "Render Plan");
59
60
  // async highlights settle within the visual-idle wait
60
61
  await setup.waitForVisualIdle();
62
+
61
63
  return setup.captureCharFrame();
62
64
  }
63
65
 
@@ -119,6 +121,7 @@ describe("block spacing", () => {
119
121
  width: 120,
120
122
  height: 30,
121
123
  });
124
+
122
125
  await waitForText(setup, "one");
123
126
 
124
127
  // Assert
@@ -127,6 +130,7 @@ describe("block spacing", () => {
127
130
  .split("\n")
128
131
  .map((line) => line.trimEnd());
129
132
  const one = lines.findIndex((line) => line.includes("- one"));
133
+
130
134
  expect(lines[one + 1]).toContain("- two");
131
135
  });
132
136
  });
@@ -42,7 +42,9 @@ async function renderApp() {
42
42
  width: 120,
43
43
  height: 30,
44
44
  });
45
+
45
46
  await waitForText(setup, "Review");
47
+
46
48
  return setup;
47
49
  }
48
50
 
@@ -69,6 +71,7 @@ describe("review panel", () => {
69
71
  // Assert: compact drops the submit button and widens the plan (rail is a strip)
70
72
  await waitForTextGone(setup, "Submit review");
71
73
  const compactWidth = planWidth();
74
+
72
75
  expect(compactWidth).toBeGreaterThan(expandedWidth);
73
76
 
74
77
  // Act
@@ -77,6 +80,7 @@ describe("review panel", () => {
77
80
 
78
81
  // Assert: hidden spans the plan full width, no rail column
79
82
  const hiddenWidth = planWidth();
83
+
80
84
  expect(hiddenWidth).toBeGreaterThan(compactWidth);
81
85
  expect(hiddenWidth).toBeGreaterThan(115);
82
86
 
@@ -108,12 +112,14 @@ describe("review panel", () => {
108
112
 
109
113
  // Assert
110
114
  const widened = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
115
+
111
116
  expect(widened).toBeGreaterThan(startWidth);
112
117
  });
113
118
 
114
119
  test("submitting from a hidden panel force-opens the rail so the confirm card is reachable", async () => {
115
120
  // Arrange
116
121
  const setup = await renderApp();
122
+
117
123
  await press(setup, "b"); // -> compact
118
124
  await press(setup, "b"); // -> hidden
119
125
 
@@ -127,6 +133,7 @@ describe("review panel", () => {
127
133
  // Assert
128
134
  await waitForText(setup, "submit review");
129
135
  const frame = setup.captureCharFrame();
136
+
130
137
  expect(frame).toContain("submit review"); // the confirm card is visible again
131
138
  expect(frame).toContain("[Approve]");
132
139