@cueloop/client 0.1.0-alpha.64 → 0.1.0-alpha.66

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 (109) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +8 -73
  4. package/src/App.export.test.tsx +2 -2
  5. package/src/App.plan-review.test.tsx +60 -412
  6. package/src/App.prototype.test.tsx +1 -8
  7. package/src/App.readonly.test.tsx +85 -62
  8. package/src/App.rendering.test.tsx +5 -6
  9. package/src/App.submit-confirm.test.tsx +32 -70
  10. package/src/App.test.tsx +136 -66
  11. package/src/App.toast.test.tsx +17 -8
  12. package/src/App.tsx +243 -284
  13. package/src/App.walk.test.tsx +1 -1
  14. package/src/app-screens.tsx +67 -52
  15. package/src/app-view-model.ts +6 -100
  16. package/src/components/AnnotationCard.stories.tsx +1 -1
  17. package/src/components/AnnotationCard.tsx +2 -2
  18. package/src/components/AppShell.stories.tsx +84 -0
  19. package/src/components/AppShell.tsx +127 -0
  20. package/src/components/Breadcrumb.stories.tsx +1 -1
  21. package/src/components/ChangesColumn.stories.tsx +39 -0
  22. package/src/components/ChangesColumn.tsx +147 -0
  23. package/src/components/CodeBlock.stories.tsx +1 -1
  24. package/src/components/CompletionOverlay.stories.tsx +1 -1
  25. package/src/components/ConfirmCard.stories.tsx +1 -1
  26. package/src/components/ConfirmCard.tsx +3 -4
  27. package/src/components/ConfirmDialog.stories.tsx +1 -1
  28. package/src/components/DiffSheet.stories.tsx +1 -1
  29. package/src/components/InboxList.stories.tsx +23 -8
  30. package/src/components/InboxList.tsx +45 -19
  31. package/src/components/KeybindsDialog.stories.tsx +1 -1
  32. package/src/components/KeybindsDialog.tsx +61 -17
  33. package/src/components/MarkerPopover.stories.tsx +1 -1
  34. package/src/components/MenuBar.stories.tsx +3 -12
  35. package/src/components/MenuBar.tsx +25 -56
  36. package/src/components/PromptDialog.stories.tsx +1 -1
  37. package/src/components/PrototypeSheet.stories.tsx +1 -1
  38. package/src/components/ReviewPanel.stories.tsx +15 -1
  39. package/src/components/ReviewRail.stories.tsx +15 -1
  40. package/src/components/ReviewRail.tsx +13 -6
  41. package/src/components/SettingsDialog.stories.tsx +7 -1
  42. package/src/components/SettingsDialog.tsx +112 -66
  43. package/src/components/SettingsRows.stories.tsx +1 -1
  44. package/src/components/ShellHeader.stories.tsx +50 -0
  45. package/src/components/ShellHeader.tsx +137 -0
  46. package/src/components/ThreadFooter.stories.tsx +30 -0
  47. package/src/components/ThreadFooter.tsx +49 -0
  48. package/src/components/ThreadView.selection.test.tsx +812 -0
  49. package/src/components/ThreadView.slash.test.tsx +113 -0
  50. package/src/components/ThreadView.stories.tsx +26 -0
  51. package/src/components/ThreadView.tsx +1708 -0
  52. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  53. package/src/components/ThreadsSidebar.tsx +57 -0
  54. package/src/components/Toast.stories.tsx +1 -1
  55. package/src/components/TreePane.stories.tsx +106 -0
  56. package/src/components/TreePane.tsx +156 -0
  57. package/src/components/WalkWizard.stories.tsx +1 -1
  58. package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
  59. package/src/components/agent-launcher.stories.tsx +1 -1
  60. package/src/components/file-tree.test.ts +45 -0
  61. package/src/components/file-tree.ts +57 -0
  62. package/src/components/pages/Workspace.stories.tsx +181 -0
  63. package/src/components/primitives/Button.stories.tsx +1 -1
  64. package/src/components/primitives/Card.stories.tsx +1 -1
  65. package/src/components/primitives/Dialog.stories.tsx +1 -1
  66. package/src/components/primitives/IconButton.stories.tsx +36 -0
  67. package/src/components/primitives/IconButton.tsx +43 -0
  68. package/src/components/primitives/StatusBar.stories.tsx +1 -1
  69. package/src/components/primitives/Tabs.stories.tsx +1 -1
  70. package/src/components/primitives/Toolbar.stories.tsx +1 -1
  71. package/src/components/primitives/Tree.stories.tsx +124 -0
  72. package/src/components/primitives/Tree.tsx +87 -0
  73. package/src/components/primitives/icons.ts +38 -0
  74. package/src/components/primitives/tree-model.test.ts +99 -0
  75. package/src/components/primitives/tree-model.ts +137 -0
  76. package/src/components/quick-actions-editor.stories.tsx +1 -1
  77. package/src/components/session-tree.test.ts +114 -0
  78. package/src/components/session-tree.ts +94 -0
  79. package/src/components/stories-app.tsx +142 -82
  80. package/src/components/story-tree.test.ts +100 -0
  81. package/src/components/story.ts +55 -0
  82. package/src/components/theme-context.stories.tsx +1 -1
  83. package/src/diff-hunk-curate.ts +4 -104
  84. package/src/index.ts +1 -0
  85. package/src/intent-dispatch.test.ts +136 -3
  86. package/src/intent-dispatch.ts +92 -24
  87. package/src/key-bindings.test.ts +1 -1
  88. package/src/keymap.test.ts +4 -4
  89. package/src/keymap.ts +14 -7
  90. package/src/session-controller.curate.test.ts +89 -2
  91. package/src/session-controller.share.test.ts +195 -55
  92. package/src/session-controller.tree.test.ts +228 -0
  93. package/src/session-controller.ts +331 -72
  94. package/src/share.ts +114 -16
  95. package/src/test-support.test.ts +193 -1
  96. package/src/test-support.ts +64 -0
  97. package/src/theme.ts +4 -4
  98. package/src/thread-chords.test.ts +154 -0
  99. package/src/thread-chords.ts +144 -0
  100. package/src/thread-selection.test.ts +210 -0
  101. package/src/thread-selection.ts +243 -0
  102. package/src/tree-view.test.ts +128 -0
  103. package/src/tree-view.ts +148 -0
  104. package/src/use-settings-dialog.tsx +9 -0
  105. package/src/view-plan.ts +29 -12
  106. package/src/App.review-panel.test.tsx +0 -143
  107. package/src/components/PlanSheet.stories.tsx +0 -113
  108. package/src/components/PlanSheet.tsx +0 -419
  109. package/src/components/plan-sheet-run-style.ts +0 -88
@@ -10,11 +10,12 @@ 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 {
14
+ clickText,
15
+ dragText,
15
16
  isolateUserConfig,
16
17
  press,
17
- settle,
18
+ pressKey,
18
19
  typeText as type,
19
20
  waitForState,
20
21
  waitForText,
@@ -92,38 +93,6 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
92
93
  return backgrounds;
93
94
  }
94
95
 
95
- function foregroundsOf(setup: Setup, needle: string): string[] {
96
- const foregrounds: string[] = [];
97
-
98
- for (const line of setup.captureSpans().lines) {
99
- for (const span of line.spans) {
100
- if (!span.text.includes(needle)) continue;
101
- const [red, green, blue] = span.fg.toInts();
102
-
103
- foregrounds.push(
104
- "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
105
- );
106
- }
107
- }
108
-
109
- return foregrounds;
110
- }
111
-
112
- /** Move the cursor to "The daemon persists sessions to disk atomically." */
113
- async function toContextParagraph(setup: Setup): Promise<void> {
114
- await press(setup, "j");
115
- await press(setup, "j");
116
- }
117
-
118
- /** Click the first on-screen occurrence of a string, by its char-frame position. */
119
- async function clickText(setup: Setup, needle: string): Promise<void> {
120
- const lines = setup.captureCharFrame().split("\n");
121
- const row = lines.findIndex((line) => line.includes(needle));
122
-
123
- await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
124
- await setup.renderOnce();
125
- }
126
-
127
96
  describe("share button", () => {
128
97
  test("the owner's plan header shows the Share button next to Edit", async () => {
129
98
  // Arrange / Act
@@ -196,405 +165,114 @@ describe("edit affordance", () => {
196
165
  });
197
166
  });
198
167
 
199
- describe("attribution", () => {
200
- test("a collaborator's note shows their name in the card border", async () => {
201
- // Arrange - a pulled collaborator note carries an author fingerprint
202
- const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
203
- const blocks = parseBlocks(PLAN);
204
- const contextBlockIndex = blocks.findIndex((block) =>
205
- block.text.startsWith("The daemon persists"),
206
- );
207
-
208
- server.core.sessionAnnotate(session.id, {
209
- id: "collab-1",
210
- kind: "comment",
211
- anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
212
- body: "who owns retries?",
213
- author: "SHA256:1a2b3c4d5e6f",
214
- });
215
-
216
- // Act
217
- const setup = await renderApp();
218
-
219
- // Assert - the short author handle rides the card border
220
- await waitForText(setup, "1a2b3c4d");
221
- });
222
- });
223
-
224
- describe("selection feeds the annotation quote", () => {
225
- test("v anchors, l extends, c composes with the char-precise quote", async () => {
168
+ describe("marks feed the comment anchor", () => {
169
+ test("a drag marks a character-precise span and typing anchors the comment to it", async () => {
226
170
  // Arrange
227
171
  const setup = await renderApp();
228
172
 
229
- await toContextParagraph(setup);
230
-
231
- // Act
232
- await press(setup, "v");
233
- await press(setup, "l"); // "The daemon"
234
- await press(setup, "c");
235
-
236
- // Assert
237
- await waitForText(setup, 'comment on "The daemon"');
238
-
239
173
  // Act
174
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
240
175
  await type(setup, "Which daemon?");
241
- await press(setup, "enter");
176
+ await pressKey(setup, "RETURN", { meta: true });
242
177
 
243
178
  // Assert
244
179
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
245
- const stored = server.core.sessionGet(session.id);
246
-
247
- expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
180
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
248
181
  });
249
182
 
250
- test("a mouse drag over the text becomes the compose quote", async () => {
183
+ test("a drag that ends mid-sentence keeps exactly the dragged words", async () => {
251
184
  // Arrange
252
185
  const setup = await renderApp();
253
- const lines = setup.captureCharFrame().split("\n");
254
- const row = lines.findIndex((line) => line.includes("persists sessions"));
255
- const startColumn = lines[row]!.indexOf("persists");
256
-
257
- expect(row).toBeGreaterThan(-1);
258
-
259
- // Act
260
- // the drag's end cell is exclusive: release one past the last character
261
- await setup.mockMouse.drag(startColumn, row, startColumn + "persists sessions".length, row);
262
- await settle(setup);
263
- await press(setup, "c");
264
186
 
265
- // Assert
266
- await waitForText(setup, 'comment on "persists sessions"');
267
-
268
- // Act
187
+ // Act: release one past the last character of "sessions"
188
+ await dragText(setup, "persists", "sessions", "sessions".length);
269
189
  await type(setup, "sessions plural?");
270
- await press(setup, "enter");
190
+ await pressKey(setup, "RETURN", { meta: true });
271
191
 
272
192
  // Assert
273
193
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
274
- const stored = server.core.sessionGet(session.id);
275
-
276
- expect(stored.annotations[0]!.anchor.quote).toBe("persists sessions");
194
+ expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe(
195
+ "persists sessions",
196
+ );
277
197
  });
278
198
  });
279
199
 
280
- describe("inline compose keeps the anchor painted", () => {
281
- test("the selection stays painted while composing; cancel un-paints; save converts to the comment highlight", async () => {
200
+ /** The thread view's mark backdrop in the dark palette. */
201
+ const THREAD_MARK = "#463852";
202
+
203
+ describe("the mark stays painted while composing", () => {
204
+ test("the mark paints while composing, escape un-paints, save keeps it as the comment's mark", async () => {
282
205
  // Arrange
283
206
  const setup = await renderApp();
284
207
 
285
- await toContextParagraph(setup);
286
-
287
208
  // Act
288
- await press(setup, "v");
289
- await press(setup, "l");
290
- await press(setup, "c");
209
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
210
+ await type(setup, "x");
291
211
 
292
- // Assert
293
- // compose open: the anchor is painted selection-style. The box can render
294
- // a frame before the anchor repaint settles, so wait on the color itself.
295
- await waitForText(setup, "Save");
296
- expect(setup.captureCharFrame()).toContain("Cancel");
297
- await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(DARK.accent));
298
- expect(backgroundsOf(setup, "The daemon")).toContain(DARK.accent);
212
+ // Assert: composing, the marked words carry the mark backdrop
213
+ await waitForText(setup, "● x");
214
+ expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
299
215
 
300
- // Act
301
- // cancel un-paints (a bare ESC settles after the parser's escape window)
216
+ // Act: escape discards the draft; the mark stays for re-typing, a second escape drops it
217
+ await press(setup, "escape");
218
+ await waitForTextGone(setup, "● x");
219
+ expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
302
220
  await press(setup, "escape");
303
221
 
304
222
  // Assert
305
- await waitForTextGone(setup, 'comment on "');
306
- expect(backgroundsOf(setup, "The daemon")).not.toContain(DARK.accent);
223
+ await waitForState(setup, () => !backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
307
224
 
308
- // Act
309
- // save converts the paint to the kind-colored annotation highlight
310
- await press(setup, "c");
225
+ // Act: mark again, save
226
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
311
227
  await type(setup, "Which daemon?");
312
- await press(setup, "enter");
228
+ await pressKey(setup, "RETURN", { meta: true });
313
229
 
314
- // Assert
230
+ // Assert: the saved comment keeps the passage marked
315
231
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
316
- await settle(setup);
317
- // the whole cursor block is the anchor here; check the highlight landed
318
- const stored = server.core.sessionGet(session.id);
319
-
320
- expect(stored.annotations.length).toBe(1);
321
- expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(
322
- DARK.markCommentBackground,
323
- );
324
- // renderApp plus this many frame-waits grazes the 5s default on a loaded CI
325
- // runner, and the whole-suite publish lane has timed even 15s out; give the
326
- // heaviest frame-wait chain generous headroom so runner load cannot flake it.
232
+ await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
327
233
  }, 60_000);
328
-
329
- test("clicking the Cancel word-button closes the compose box", async () => {
330
- // Arrange
331
- const setup = await renderApp();
332
-
333
- await toContextParagraph(setup);
334
- await press(setup, "v");
335
- await press(setup, "l");
336
- await press(setup, "c");
337
- await waitForText(setup, "Cancel");
338
-
339
- // Act
340
- await clickText(setup, "Cancel");
341
-
342
- // Assert
343
- await waitForTextGone(setup, 'comment on "');
344
- });
345
-
346
- test("clicking Cancel in a saved card's edit composer closes it", async () => {
347
- // Arrange
348
- const setup = await renderApp();
349
-
350
- await toContextParagraph(setup);
351
- await press(setup, "v");
352
- await press(setup, "l");
353
- await press(setup, "c");
354
- await waitForText(setup, "Cancel");
355
- await type(setup, "note body");
356
- await press(setup, "enter");
357
- await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
358
- // select the saved card, then activate it to open the in-place editor
359
- await clickText(setup, "note body");
360
- await clickText(setup, "note body");
361
- await waitForText(setup, "Cancel");
362
-
363
- // Act
364
- await clickText(setup, "Cancel");
365
-
366
- // Assert - the editor closes back to the plain saved card
367
- await waitForTextGone(setup, "Cancel");
368
- });
369
234
  });
370
235
 
371
236
  describe("compose newline convention", () => {
372
- // Option/Alt and Shift reach the App as `meta`/`shift` on the return key.
373
- // modifyOtherKeys encoding preserves those modifiers so the grammar and the
374
- // focused textarea can tell a bare submit from a newline.
375
- async function renderModifierAwareApp() {
376
- const setup = await testRender(<App home={home} sessionId={session.id} />, {
377
- width: 120,
378
- height: 32,
379
- otherModifiersMode: true,
380
- });
381
-
382
- await waitForText(setup, "cueloop");
383
-
384
- return setup;
385
- }
386
-
387
- async function pressReturnWith(
388
- setup: Setup,
389
- modifiers: { shift?: boolean; meta?: boolean },
390
- ): Promise<void> {
391
- setup.mockInput.pressKey("RETURN", modifiers);
392
- await settle(setup);
393
- }
394
-
395
- test("option+return inserts a newline; a bare return submits the multiline body", async () => {
237
+ test("enter breaks the line; cmd+enter saves the multiline body verbatim", async () => {
396
238
  // Arrange
397
- const setup = await renderModifierAwareApp();
239
+ const setup = await renderApp();
398
240
 
399
- await toContextParagraph(setup);
400
- await press(setup, "v");
401
- await press(setup, "l");
402
- await press(setup, "c");
403
- await waitForText(setup, 'comment on "The daemon"');
241
+ await clickText(setup, "daemon");
404
242
 
405
243
  // Act
406
244
  await type(setup, "first line");
407
- await pressReturnWith(setup, { meta: true }); // option/alt+return -> newline
245
+ await press(setup, "enter");
408
246
  await type(setup, "second line");
409
247
 
410
- // Assert
411
- // still composing: the newline did not submit
248
+ // Assert: still composing, the newline did not save
412
249
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
413
250
 
414
251
  // Act
415
- await press(setup, "enter"); // bare return -> save
252
+ await pressKey(setup, "RETURN", { meta: true });
416
253
 
417
254
  // Assert
418
255
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
419
256
  expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("first line\nsecond line");
420
257
  });
421
258
 
422
- test("shift+return still inserts a newline (existing muscle memory)", async () => {
423
- // Arrange
424
- const setup = await renderModifierAwareApp();
425
-
426
- await toContextParagraph(setup);
427
- await press(setup, "c");
428
-
429
- // Act
430
- await type(setup, "alpha");
431
- await pressReturnWith(setup, { shift: true }); // shift+return -> newline
432
- await type(setup, "beta");
433
-
434
- // Assert
435
- expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
436
-
437
- // Act
438
- await press(setup, "enter");
439
-
440
- // Assert
441
- await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
442
- expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("alpha\nbeta");
443
- });
444
-
445
259
  test("escape cancels the composer without saving", async () => {
446
260
  // Arrange
447
- const setup = await renderModifierAwareApp();
261
+ const setup = await renderApp();
448
262
 
449
- await toContextParagraph(setup);
450
- await press(setup, "c");
451
- await waitForText(setup, 'comment on "');
263
+ await clickText(setup, "daemon");
452
264
  await type(setup, "never saved");
265
+ await waitForText(setup, "● never saved");
453
266
 
454
267
  // Act
455
268
  await press(setup, "escape");
456
269
 
457
270
  // Assert
458
- await waitForTextGone(setup, 'comment on "');
271
+ await waitForTextGone(setup, "● never saved");
459
272
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
460
273
  });
461
274
  });
462
275
 
463
- describe("the document selects, the rail edits", () => {
464
- test("e edits the selected card in place; enter saves through the controller", async () => {
465
- // Arrange
466
- const setup = await renderApp();
467
-
468
- await toContextParagraph(setup);
469
- await press(setup, "c");
470
- await type(setup, "Needs a citation.");
471
- await press(setup, "enter");
472
- await waitForText(setup, "COMMENT · me");
473
-
474
- // Act
475
- // the saved card is selected; e turns its body into an input in place
476
- await press(setup, "e");
477
- await type(setup, " And a link.");
478
- await press(setup, "enter");
479
-
480
- // Assert
481
- await waitForState(
482
- setup,
483
- () =>
484
- server.core.sessionGet(session.id).annotations[0]!.body === "Needs a citation. And a link.",
485
- );
486
- });
487
-
488
- test("activating a collaborator's card opens rename, not a body edit", async () => {
489
- // Arrange - a pulled collaborator note carries an author fingerprint
490
- const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
491
- const blocks = parseBlocks(PLAN);
492
- const contextBlockIndex = blocks.findIndex((block) =>
493
- block.text.startsWith("The daemon persists"),
494
- );
495
-
496
- server.core.sessionAnnotate(session.id, {
497
- id: "collab-1",
498
- kind: "comment",
499
- anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
500
- body: "who owns retries?",
501
- author: "SHA256:1a2b3c4d5e6f",
502
- });
503
- const setup = await renderApp();
504
-
505
- await waitForText(setup, "1a2b3c4d");
506
-
507
- // Act - focus the collaborator card, then activate it
508
- await press(setup, "n");
509
- await press(setup, "e");
510
-
511
- // Assert - the rename prompt opens; the body never becomes an editor
512
- await waitForText(setup, "Rename author");
513
- });
514
-
515
- test("x deletes the selected card and un-paints the document highlight", async () => {
516
- // Arrange
517
- const setup = await renderApp();
518
-
519
- await toContextParagraph(setup);
520
- await press(setup, "c");
521
- await type(setup, "Delete me.");
522
- await press(setup, "enter");
523
- await waitForText(setup, "COMMENT · me");
524
- expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
525
- expect(backgroundsOf(setup, "persists sessions")).toContain(DARK.markCommentBackground);
526
-
527
- // Act
528
- await press(setup, "x");
529
-
530
- // Assert
531
- await waitForTextGone(setup, "COMMENT · me");
532
- expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
533
- expect(backgroundsOf(setup, "persists sessions")).not.toContain(DARK.markCommentBackground);
534
- });
535
- });
536
-
537
- describe("plan cut removals", () => {
538
- test("a cut block becomes a rail removal card and u restores it", async () => {
539
- // Arrange
540
- const setup = await renderApp();
541
-
542
- await toContextParagraph(setup);
543
-
544
- // Act - no card is selected, so x cuts the block under the cursor
545
- await press(setup, "x");
546
-
547
- // Assert - the rail shows a removal card titled CUT · me for the cut block
548
- await waitForText(setup, "CUT · me");
549
- await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy !== undefined);
550
-
551
- // Act - undo restores it (no selection, so the last removal)
552
- await press(setup, "u");
553
-
554
- // Assert - the block returns and the working copy is pristine again
555
- await waitForText(setup, "removal restored");
556
- await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
557
- });
558
- });
559
-
560
- describe("addressed annotations leave the open list", () => {
561
- test("a revision that addresses one card hides it, un-paints it, and shows the count", async () => {
562
- // Arrange: two annotations through the daemon, then a revision addressing one
563
- const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
564
- const blocks = parseBlocks(PLAN);
565
- const contextBlockIndex = blocks.findIndex((block) =>
566
- block.text.startsWith("The daemon persists"),
567
- );
568
-
569
- server.core.sessionAnnotate(session.id, {
570
- id: "a_settled",
571
- kind: "comment",
572
- anchor: makeAnchor(blocks, contextBlockIndex, 0, 10), // "The daemon"
573
- body: "settled note",
574
- });
575
- server.core.sessionAnnotate(session.id, {
576
- id: "a_open",
577
- kind: "comment",
578
- anchor: makeAnchor(blocks, contextBlockIndex, 11, 19), // "persists"
579
- body: "still open note",
580
- });
581
- server.core.sessionSubmitRevision(session.id, PLAN, ["a_settled"]);
582
-
583
- // Act
584
- const setup = await renderApp();
585
-
586
- await waitForText(setup, "still open note");
587
-
588
- // Assert: the open card renders, the addressed one is gone behind the count
589
- const frame = setup.captureCharFrame();
590
-
591
- expect(frame).toContain("✓ 1 addressed by revision");
592
- expect(frame).not.toContain("settled note");
593
- expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
594
- expect(backgroundsOf(setup, "The daemon")).not.toContain(DARK.markCommentBackground); // no highlight paint
595
- });
596
- });
597
-
598
276
  describe("sheet header", () => {
599
277
  test("shows the submitting agent, the revision, and the Edit and Share word-buttons", async () => {
600
278
  // Arrange
@@ -608,25 +286,6 @@ describe("sheet header", () => {
608
286
  expect(frame).toContain("Edit");
609
287
  expect(frame).toContain("Share");
610
288
  });
611
-
612
- test("the Agent tab shows the text-only harness launcher buttons", async () => {
613
- // Arrange
614
- const setup = await renderApp();
615
- const lines = setup.captureCharFrame().split("\n");
616
- const tabRow = lines.findIndex((line) => line.includes("Review") && line.includes("Agent"));
617
- const tabColumn = lines[tabRow]!.indexOf("Agent");
618
-
619
- // Act
620
- await setup.mockMouse.click(tabColumn + 1, tabRow);
621
- await setup.renderOnce();
622
-
623
- // Assert - the three launcher buttons, in order
624
- await waitForText(setup, "Claude Code");
625
- const frame = setup.captureCharFrame();
626
-
627
- expect(frame).toContain("Pi");
628
- expect(frame).toContain("OpenAI Codex");
629
- });
630
289
  });
631
290
 
632
291
  describe("quick-actions settings editor", () => {
@@ -634,8 +293,8 @@ describe("quick-actions settings editor", () => {
634
293
  // Arrange
635
294
  const setup = await renderApp();
636
295
 
637
- // Act - open Settings, enter Actions, expand the first action, type into its input
638
- await clickText(setup, "menu");
296
+ // Act - open Settings from the top-left gear, enter Actions, expand the first action, type
297
+ await setup.mockMouse.click(1, 1);
639
298
  await clickText(setup, "Settings");
640
299
  await clickText(setup, "Actions");
641
300
  await clickText(setup, "Zoom out, research in depth");
@@ -661,23 +320,16 @@ describe("edit-exit reconciliation", () => {
661
320
  // Arrange
662
321
  const setup = await renderApp();
663
322
 
664
- await toContextParagraph(setup);
665
- await press(setup, "c");
323
+ await clickText(setup, "daemon");
666
324
  await type(setup, "Anchor me to the doomed passage.");
667
- await press(setup, "enter");
668
- await waitForText(setup, "COMMENT · me");
325
+ await pressKey(setup, "RETURN", { meta: true });
326
+ await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
669
327
 
670
328
  // Act
671
- // deselect the card so e reaches the editor hand-off, then edit; deselection
672
- // shows as the card's quote line dropping the selection tone (blue)
673
- await press(setup, "escape");
674
- await waitForState(setup, () => !foregroundsOf(setup, "The daemon").includes(DARK.blue));
675
- await press(setup, "e");
329
+ await pressKey(setup, "e", { ctrl: true });
676
330
 
677
- // Assert
331
+ // Assert - the thread view banner reports the orphaned annotation
678
332
  await waitForText(setup, "1 annotation no longer match - the passage was removed.");
679
- // the rail card flips to ORPHANED once the working-copy refresh lands
680
- await waitForText(setup, "· ORPHANED");
681
333
  // the annotation is NOT deleted: the feedback serializer handles orphans
682
334
  expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
683
335
  } finally {
@@ -686,23 +338,19 @@ describe("edit-exit reconciliation", () => {
686
338
  });
687
339
  });
688
340
 
689
- describe("marker-actions popover", () => {
690
- test("v shows the toolbar, a opens quick-actions, enter inserts the preset comment", async () => {
341
+ describe("the quick-action palette", () => {
342
+ test("/ lists the quick actions and a pick seeds the comment with the preset body", async () => {
691
343
  // Arrange
692
344
  const setup = await renderApp();
693
345
 
694
- await toContextParagraph(setup);
695
-
696
- // Act - marking a span reveals the inline toolbar
697
- await press(setup, "v");
698
- await waitForText(setup, "actions");
346
+ await dragText(setup, "The daemon", "daemon persists", "daemon".length);
699
347
 
700
- // open the quick-actions list; step to the second default and pick it. The
701
- // list renders as a floating overlay the virtual terminal cannot composite,
702
- // so drive the grammar directly rather than waiting on its painted text.
703
- await press(setup, "a");
704
- await press(setup, "j");
348
+ // Act - a leading slash opens the palette; step to the second default and pick it
349
+ await type(setup, "/");
350
+ await waitForText(setup, "zoom-out-research-in-depth");
351
+ await pressKey(setup, "ARROW_DOWN");
705
352
  await press(setup, "enter");
353
+ await pressKey(setup, "RETURN", { meta: true });
706
354
 
707
355
  // Assert - a comment annotation was created with the second default's body
708
356
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
@@ -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 () => {