@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.
- package/package.json +5 -5
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +8 -73
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +60 -412
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +85 -62
- package/src/App.rendering.test.tsx +5 -6
- package/src/App.submit-confirm.test.tsx +32 -70
- package/src/App.test.tsx +136 -66
- package/src/App.toast.test.tsx +17 -8
- package/src/App.tsx +243 -284
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +67 -52
- package/src/app-view-model.ts +6 -100
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +2 -2
- package/src/components/AppShell.stories.tsx +84 -0
- package/src/components/AppShell.tsx +127 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +147 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.stories.tsx +1 -1
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +45 -19
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/KeybindsDialog.tsx +61 -17
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/ReviewPanel.stories.tsx +15 -1
- package/src/components/ReviewRail.stories.tsx +15 -1
- package/src/components/ReviewRail.tsx +13 -6
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +112 -66
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.tsx +49 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +1708 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +57 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +156 -0
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +463 -305
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/file-tree.test.ts +45 -0
- package/src/components/file-tree.ts +57 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +43 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +87 -0
- package/src/components/primitives/icons.ts +38 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +1 -1
- package/src/components/session-tree.test.ts +114 -0
- package/src/components/session-tree.ts +94 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/diff-hunk-curate.ts +4 -104
- package/src/index.ts +1 -0
- package/src/intent-dispatch.test.ts +136 -3
- package/src/intent-dispatch.ts +92 -24
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +4 -4
- package/src/keymap.ts +14 -7
- package/src/session-controller.curate.test.ts +89 -2
- package/src/session-controller.share.test.ts +195 -55
- package/src/session-controller.tree.test.ts +228 -0
- package/src/session-controller.ts +331 -72
- package/src/share.ts +114 -16
- package/src/test-support.test.ts +193 -1
- package/src/test-support.ts +64 -0
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +154 -0
- package/src/thread-chords.ts +144 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-settings-dialog.tsx +9 -0
- package/src/view-plan.ts +29 -12
- package/src/App.review-panel.test.tsx +0 -143
- package/src/components/PlanSheet.stories.tsx +0 -113
- package/src/components/PlanSheet.tsx +0 -419
- 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
|
-
|
|
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("
|
|
200
|
-
test("a
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
266
|
-
await
|
|
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
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
194
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe(
|
|
195
|
+
"persists sessions",
|
|
196
|
+
);
|
|
277
197
|
});
|
|
278
198
|
});
|
|
279
199
|
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
289
|
-
await
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
239
|
+
const setup = await renderApp();
|
|
398
240
|
|
|
399
|
-
await
|
|
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
|
|
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
|
|
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
|
|
261
|
+
const setup = await renderApp();
|
|
448
262
|
|
|
449
|
-
await
|
|
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,
|
|
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
|
|
638
|
-
await
|
|
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
|
|
665
|
-
await press(setup, "c");
|
|
323
|
+
await clickText(setup, "daemon");
|
|
666
324
|
await type(setup, "Anchor me to the doomed passage.");
|
|
667
|
-
await
|
|
668
|
-
await
|
|
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
|
-
|
|
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("
|
|
690
|
-
test("
|
|
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
|
|
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
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
await
|
|
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
|
|
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 () => {
|