@cueloop/client 0.1.0-alpha.65 → 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.diff.test.tsx +8 -73
- package/src/App.plan-review.test.tsx +4 -185
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +6 -28
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +70 -24
- package/src/App.tsx +177 -254
- package/src/App.walk.test.tsx +1 -1
- package/src/app-screens.tsx +40 -51
- package/src/app-view-model.ts +2 -4
- package/src/components/AnnotationCard.stories.tsx +1 -1
- 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/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 +1 -1
- package/src/components/ReviewRail.stories.tsx +1 -1
- 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.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +99 -11
- 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 +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +348 -138
- 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/session-controller.ts +9 -4
- package/src/theme.ts +4 -4
- package/src/use-settings-dialog.tsx +9 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.66",
|
|
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.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.66",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.66",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.66",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.66",
|
|
33
33
|
"@opentui/core": "^0.5.8",
|
|
34
34
|
"@opentui/keymap": "0.5.8",
|
|
35
35
|
"@opentui/react": "^0.5.8",
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -152,50 +152,9 @@ describe("diff review", () => {
|
|
|
152
152
|
expect(stored.workingCopy).toBe("");
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
test("a
|
|
156
|
-
// Arrange -
|
|
157
|
-
|
|
158
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
159
|
-
artifact: {
|
|
160
|
-
type: "diff",
|
|
161
|
-
content: PATCH,
|
|
162
|
-
meta: { title: "working tree" },
|
|
163
|
-
files: [
|
|
164
|
-
{
|
|
165
|
-
path: "src/store.ts",
|
|
166
|
-
oldContents: "export class Store {\n private items = [];\n}\n",
|
|
167
|
-
newContents: "export class Store {\n private items = new Map();\n}\n",
|
|
168
|
-
status: "modified",
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
const setup = await testRender(<App home={home} sessionId={withFiles.id} />, {
|
|
174
|
-
width: 120,
|
|
175
|
-
height: 30,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
await waitForText(setup, "cueloop");
|
|
179
|
-
|
|
180
|
-
// Act - reject the change under the added line
|
|
181
|
-
// rows: file(0), hunk(1), ctx(2), del(3), add(4)
|
|
182
|
-
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
183
|
-
await press(setup, "x");
|
|
184
|
-
|
|
185
|
-
// Assert - the rail shows a removal card titled REJECT · me for the change
|
|
186
|
-
await waitForText(setup, "REJECT · me");
|
|
187
|
-
expect(server.core.sessionGet(withFiles.id).workingCopy).toBe("");
|
|
188
|
-
|
|
189
|
-
// Act - undo restores it (no selection, so the last item)
|
|
190
|
-
await press(setup, "u");
|
|
191
|
-
|
|
192
|
-
// Assert - the change returns to the curated diff (working copy cleared)
|
|
193
|
-
await waitForText(setup, "removal restored");
|
|
194
|
-
expect(server.core.sessionGet(withFiles.id).workingCopy).toBeUndefined();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("a selected removal card shows an undo button that restores it on click", async () => {
|
|
198
|
-
// Arrange - a diff session that carries full file contents
|
|
155
|
+
test("a diff opens with the Changes column listing every changed file", async () => {
|
|
156
|
+
// Arrange - two changed files; config.ts is not in the patch text, so it can
|
|
157
|
+
// only appear here by way of the Changes column reading artifact.files
|
|
199
158
|
const withFiles = server.core.sessionCreate({
|
|
200
159
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
201
160
|
artifact: {
|
|
@@ -203,12 +162,8 @@ describe("diff review", () => {
|
|
|
203
162
|
content: PATCH,
|
|
204
163
|
meta: { title: "working tree" },
|
|
205
164
|
files: [
|
|
206
|
-
{
|
|
207
|
-
|
|
208
|
-
oldContents: "export class Store {\n private items = [];\n}\n",
|
|
209
|
-
newContents: "export class Store {\n private items = new Map();\n}\n",
|
|
210
|
-
status: "modified",
|
|
211
|
-
},
|
|
165
|
+
{ path: "src/store.ts", oldContents: "a", newContents: "b", status: "modified" },
|
|
166
|
+
{ path: "src/config.ts", oldContents: "", newContents: "c", status: "added" },
|
|
212
167
|
],
|
|
213
168
|
},
|
|
214
169
|
});
|
|
@@ -219,28 +174,8 @@ describe("diff review", () => {
|
|
|
219
174
|
|
|
220
175
|
await waitForText(setup, "cueloop");
|
|
221
176
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await waitForText(setup, "REJECT · me");
|
|
226
|
-
expect(setup.captureCharFrame()).not.toContain("undo");
|
|
227
|
-
|
|
228
|
-
// Act - click the card to select it; the undo button appears
|
|
229
|
-
const clickText = async (needle: string): Promise<void> => {
|
|
230
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
231
|
-
const row = lines.findIndex((line) => line.includes(needle));
|
|
232
|
-
|
|
233
|
-
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
await clickText("REJECT · me");
|
|
237
|
-
await waitForText(setup, "undo");
|
|
238
|
-
|
|
239
|
-
// Act - click the undo button
|
|
240
|
-
await clickText("undo");
|
|
241
|
-
|
|
242
|
-
// Assert - the change returns to the curated diff (working copy cleared)
|
|
243
|
-
await waitForText(setup, "removal restored");
|
|
244
|
-
expect(server.core.sessionGet(withFiles.id).workingCopy).toBeUndefined();
|
|
177
|
+
// Assert - the context default opens the column for a diff, so both files show
|
|
178
|
+
await waitForText(setup, "config.ts");
|
|
179
|
+
expect(setup.captureCharFrame()).toContain("store.ts");
|
|
245
180
|
});
|
|
246
181
|
});
|
|
@@ -10,7 +10,6 @@ import { DaemonServer } from "@cueloop/daemon";
|
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
12
|
import { DEFAULT_QUICK_ACTIONS, quickActionBody } from "./config";
|
|
13
|
-
import { DARK } from "./theme";
|
|
14
13
|
import {
|
|
15
14
|
clickText,
|
|
16
15
|
dragText,
|
|
@@ -166,31 +165,6 @@ describe("edit affordance", () => {
|
|
|
166
165
|
});
|
|
167
166
|
});
|
|
168
167
|
|
|
169
|
-
describe("attribution", () => {
|
|
170
|
-
test("a collaborator's note shows their name in the card border", async () => {
|
|
171
|
-
// Arrange - a pulled collaborator note carries an author fingerprint
|
|
172
|
-
const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
|
|
173
|
-
const blocks = parseBlocks(PLAN);
|
|
174
|
-
const contextBlockIndex = blocks.findIndex((block) =>
|
|
175
|
-
block.text.startsWith("The daemon persists"),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
server.core.sessionAnnotate(session.id, {
|
|
179
|
-
id: "collab-1",
|
|
180
|
-
kind: "comment",
|
|
181
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
|
|
182
|
-
body: "who owns retries?",
|
|
183
|
-
author: "SHA256:1a2b3c4d5e6f",
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Act
|
|
187
|
-
const setup = await renderApp();
|
|
188
|
-
|
|
189
|
-
// Assert - the short author handle rides the card border
|
|
190
|
-
await waitForText(setup, "1a2b3c4d");
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
168
|
describe("marks feed the comment anchor", () => {
|
|
195
169
|
test("a drag marks a character-precise span and typing anchors the comment to it", async () => {
|
|
196
170
|
// Arrange
|
|
@@ -299,140 +273,6 @@ describe("compose newline convention", () => {
|
|
|
299
273
|
});
|
|
300
274
|
});
|
|
301
275
|
|
|
302
|
-
describe("the document selects, the rail edits", () => {
|
|
303
|
-
test("option+e edits the focused card in place; enter saves through the controller", async () => {
|
|
304
|
-
// Arrange
|
|
305
|
-
const setup = await renderApp();
|
|
306
|
-
|
|
307
|
-
await clickText(setup, "daemon");
|
|
308
|
-
await type(setup, "Needs a citation.");
|
|
309
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
310
|
-
await waitForText(setup, "COMMENT · me");
|
|
311
|
-
|
|
312
|
-
// Act: focus the card, then turn its body into an input in place
|
|
313
|
-
await pressKey(setup, "n", { meta: true });
|
|
314
|
-
await pressKey(setup, "e", { meta: true });
|
|
315
|
-
await type(setup, " And a link.");
|
|
316
|
-
await press(setup, "enter");
|
|
317
|
-
|
|
318
|
-
// Assert
|
|
319
|
-
await waitForState(
|
|
320
|
-
setup,
|
|
321
|
-
() =>
|
|
322
|
-
server.core.sessionGet(session.id).annotations[0]!.body === "Needs a citation. And a link.",
|
|
323
|
-
);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("activating a collaborator's card opens rename, not a body edit", async () => {
|
|
327
|
-
// Arrange - a pulled collaborator note carries an author fingerprint
|
|
328
|
-
const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
|
|
329
|
-
const blocks = parseBlocks(PLAN);
|
|
330
|
-
const contextBlockIndex = blocks.findIndex((block) =>
|
|
331
|
-
block.text.startsWith("The daemon persists"),
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
server.core.sessionAnnotate(session.id, {
|
|
335
|
-
id: "collab-1",
|
|
336
|
-
kind: "comment",
|
|
337
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
|
|
338
|
-
body: "who owns retries?",
|
|
339
|
-
author: "SHA256:1a2b3c4d5e6f",
|
|
340
|
-
});
|
|
341
|
-
const setup = await renderApp();
|
|
342
|
-
|
|
343
|
-
await waitForText(setup, "1a2b3c4d");
|
|
344
|
-
|
|
345
|
-
// Act - focus the collaborator card, then activate it
|
|
346
|
-
await pressKey(setup, "n", { meta: true });
|
|
347
|
-
await pressKey(setup, "e", { meta: true });
|
|
348
|
-
|
|
349
|
-
// Assert - the rename prompt opens; the body never becomes an editor
|
|
350
|
-
await waitForText(setup, "Rename author");
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
test("option+backspace deletes the focused card and un-paints the document mark", async () => {
|
|
354
|
-
// Arrange
|
|
355
|
-
const setup = await renderApp();
|
|
356
|
-
|
|
357
|
-
await dragText(setup, "daemon", "daemon persists", "daemon".length);
|
|
358
|
-
await type(setup, "Delete me.");
|
|
359
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
360
|
-
await waitForText(setup, "COMMENT · me");
|
|
361
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
362
|
-
await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
|
|
363
|
-
|
|
364
|
-
// Act
|
|
365
|
-
await pressKey(setup, "n", { meta: true });
|
|
366
|
-
await pressKey(setup, "BACKSPACE", { meta: true });
|
|
367
|
-
|
|
368
|
-
// Assert
|
|
369
|
-
await waitForTextGone(setup, "COMMENT · me");
|
|
370
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
371
|
-
expect(backgroundsOf(setup, "daemon")).not.toContain(THREAD_MARK);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe("plan cut removals", () => {
|
|
376
|
-
test("option+x cuts the caret's block into a rail removal card and option+u restores it", async () => {
|
|
377
|
-
// Arrange
|
|
378
|
-
const setup = await renderApp();
|
|
379
|
-
|
|
380
|
-
await clickText(setup, "daemon");
|
|
381
|
-
|
|
382
|
-
// Act - no card is focused, so the cut takes the block under the caret
|
|
383
|
-
await pressKey(setup, "x", { meta: true });
|
|
384
|
-
|
|
385
|
-
// Assert - the rail shows a removal card titled CUT · me for the cut block
|
|
386
|
-
await waitForText(setup, "CUT · me");
|
|
387
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy !== undefined);
|
|
388
|
-
|
|
389
|
-
// Act - undo restores it (no selection, so the last removal)
|
|
390
|
-
await pressKey(setup, "u", { meta: true });
|
|
391
|
-
|
|
392
|
-
// Assert - the block returns and the working copy is pristine again
|
|
393
|
-
await waitForText(setup, "removal restored");
|
|
394
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
describe("addressed annotations leave the open list", () => {
|
|
399
|
-
test("a revision that addresses one card hides it, un-paints it, and shows the count", async () => {
|
|
400
|
-
// Arrange: two annotations through the daemon, then a revision addressing one
|
|
401
|
-
const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
|
|
402
|
-
const blocks = parseBlocks(PLAN);
|
|
403
|
-
const contextBlockIndex = blocks.findIndex((block) =>
|
|
404
|
-
block.text.startsWith("The daemon persists"),
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
server.core.sessionAnnotate(session.id, {
|
|
408
|
-
id: "a_settled",
|
|
409
|
-
kind: "comment",
|
|
410
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 0, 10), // "The daemon"
|
|
411
|
-
body: "settled note",
|
|
412
|
-
});
|
|
413
|
-
server.core.sessionAnnotate(session.id, {
|
|
414
|
-
id: "a_open",
|
|
415
|
-
kind: "comment",
|
|
416
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 11, 19), // "persists"
|
|
417
|
-
body: "still open note",
|
|
418
|
-
});
|
|
419
|
-
server.core.sessionSubmitRevision(session.id, PLAN, ["a_settled"]);
|
|
420
|
-
|
|
421
|
-
// Act
|
|
422
|
-
const setup = await renderApp();
|
|
423
|
-
|
|
424
|
-
await waitForText(setup, "still open note");
|
|
425
|
-
|
|
426
|
-
// Assert: the open card renders, the addressed one is gone behind the count
|
|
427
|
-
const frame = setup.captureCharFrame();
|
|
428
|
-
|
|
429
|
-
expect(frame).toContain("✓ 1 addressed by revision");
|
|
430
|
-
expect(frame).not.toContain("settled note");
|
|
431
|
-
expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
|
|
432
|
-
expect(backgroundsOf(setup, "The daemon")).not.toContain(DARK.markCommentBackground); // no highlight paint
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
276
|
describe("sheet header", () => {
|
|
437
277
|
test("shows the submitting agent, the revision, and the Edit and Share word-buttons", async () => {
|
|
438
278
|
// Arrange
|
|
@@ -446,25 +286,6 @@ describe("sheet header", () => {
|
|
|
446
286
|
expect(frame).toContain("Edit");
|
|
447
287
|
expect(frame).toContain("Share");
|
|
448
288
|
});
|
|
449
|
-
|
|
450
|
-
test("the Agent tab shows the text-only harness launcher buttons", async () => {
|
|
451
|
-
// Arrange
|
|
452
|
-
const setup = await renderApp();
|
|
453
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
454
|
-
const tabRow = lines.findIndex((line) => line.includes("Review") && line.includes("Agent"));
|
|
455
|
-
const tabColumn = lines[tabRow]!.indexOf("Agent");
|
|
456
|
-
|
|
457
|
-
// Act
|
|
458
|
-
await setup.mockMouse.click(tabColumn + 1, tabRow);
|
|
459
|
-
await setup.renderOnce();
|
|
460
|
-
|
|
461
|
-
// Assert - the three launcher buttons, in order
|
|
462
|
-
await waitForText(setup, "Claude Code");
|
|
463
|
-
const frame = setup.captureCharFrame();
|
|
464
|
-
|
|
465
|
-
expect(frame).toContain("Pi");
|
|
466
|
-
expect(frame).toContain("OpenAI Codex");
|
|
467
|
-
});
|
|
468
289
|
});
|
|
469
290
|
|
|
470
291
|
describe("quick-actions settings editor", () => {
|
|
@@ -472,8 +293,8 @@ describe("quick-actions settings editor", () => {
|
|
|
472
293
|
// Arrange
|
|
473
294
|
const setup = await renderApp();
|
|
474
295
|
|
|
475
|
-
// Act - open Settings, enter Actions, expand the first action, type
|
|
476
|
-
await
|
|
296
|
+
// Act - open Settings from the top-left gear, enter Actions, expand the first action, type
|
|
297
|
+
await setup.mockMouse.click(1, 1);
|
|
477
298
|
await clickText(setup, "Settings");
|
|
478
299
|
await clickText(setup, "Actions");
|
|
479
300
|
await clickText(setup, "Zoom out, research in depth");
|
|
@@ -502,15 +323,13 @@ describe("edit-exit reconciliation", () => {
|
|
|
502
323
|
await clickText(setup, "daemon");
|
|
503
324
|
await type(setup, "Anchor me to the doomed passage.");
|
|
504
325
|
await pressKey(setup, "RETURN", { meta: true });
|
|
505
|
-
await
|
|
326
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
506
327
|
|
|
507
328
|
// Act
|
|
508
329
|
await pressKey(setup, "e", { ctrl: true });
|
|
509
330
|
|
|
510
|
-
// Assert
|
|
331
|
+
// Assert - the thread view banner reports the orphaned annotation
|
|
511
332
|
await waitForText(setup, "1 annotation no longer match - the passage was removed.");
|
|
512
|
-
// the rail card flips to ORPHANED once the working-copy refresh lands
|
|
513
|
-
await waitForText(setup, "· ORPHANED");
|
|
514
333
|
// the annotation is NOT deleted: the feedback serializer handles orphans
|
|
515
334
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
516
335
|
} finally {
|
|
@@ -105,7 +105,7 @@ async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
describe("prototype review", () => {
|
|
108
|
-
test("click an element, comment on it, and it
|
|
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 () => {
|
|
@@ -9,7 +9,6 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import { DARK } from "./theme";
|
|
13
12
|
import {
|
|
14
13
|
clickText,
|
|
15
14
|
dragText,
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
17
16
|
isolateUserConfig,
|
|
18
17
|
pressKey,
|
|
19
18
|
typeText,
|
|
20
|
-
waitForState,
|
|
21
19
|
waitForText,
|
|
22
20
|
} from "./test-support";
|
|
23
21
|
|
|
@@ -83,15 +81,12 @@ function snapshot() {
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
describe("observer rendering", () => {
|
|
86
|
-
test("shows the observer badge
|
|
84
|
+
test("shows the observer badge in the header", async () => {
|
|
87
85
|
// Arrange
|
|
88
86
|
const setup = await renderObserver();
|
|
89
87
|
|
|
90
88
|
// Assert
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
expect(frame).toContain("· observer");
|
|
94
|
-
expect(frame).toContain("menu");
|
|
89
|
+
expect(setup.captureCharFrame()).toContain("· observer");
|
|
95
90
|
});
|
|
96
91
|
});
|
|
97
92
|
|
|
@@ -199,29 +194,12 @@ describe("observer navigation still works", () => {
|
|
|
199
194
|
});
|
|
200
195
|
const setup = await renderObserver();
|
|
201
196
|
|
|
202
|
-
// Act
|
|
197
|
+
// Act - the observer navigates to the annotation without mutating anything
|
|
203
198
|
await pressKey(setup, "n", { meta: true });
|
|
204
199
|
await setup.renderOnce();
|
|
205
200
|
|
|
206
|
-
// Assert -
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await waitForState(setup, () => hasBorderColor(setup, DARK.blue));
|
|
201
|
+
// Assert - the observer reads the controller's comment inline in the thread
|
|
202
|
+
await waitForText(setup, "From the controller.");
|
|
203
|
+
expect(snapshot().annotations).toBe(1);
|
|
210
204
|
});
|
|
211
205
|
});
|
|
212
|
-
|
|
213
|
-
/** Whether any border character (box-drawing) is painted in the given foreground hex. */
|
|
214
|
-
function hasBorderColor(setup: Awaited<ReturnType<typeof renderObserver>>, hex: string): boolean {
|
|
215
|
-
for (const line of setup.captureSpans().lines) {
|
|
216
|
-
for (const span of line.spans) {
|
|
217
|
-
if (!/[╭╮╰╯│─┌┐└┘]/.test(span.text)) continue;
|
|
218
|
-
const [red, green, blue] = span.fg.toInts();
|
|
219
|
-
const rendered =
|
|
220
|
-
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
221
|
-
|
|
222
|
-
if (rendered === hex) return true;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The
|
|
1
|
+
/** The send-message confirm: cmd+enter opens a centered overlay with a verdict selector, summary input, and word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
@@ -83,29 +83,27 @@ function seedAnnotations(count: number): void {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
describe("
|
|
87
|
-
test("
|
|
86
|
+
describe("send message confirm", () => {
|
|
87
|
+
test("cmd+enter opens the send-message confirm overlay", async () => {
|
|
88
88
|
// Arrange
|
|
89
89
|
const setup = await renderApp();
|
|
90
90
|
|
|
91
|
-
// Assert
|
|
92
|
-
expect(setup.captureCharFrame()).toContain("
|
|
91
|
+
// Assert - the footer carries the send control before the overlay opens
|
|
92
|
+
expect(setup.captureCharFrame()).toContain("send message");
|
|
93
93
|
|
|
94
94
|
// Act
|
|
95
95
|
await pressKey(setup, "RETURN", { meta: true });
|
|
96
96
|
|
|
97
|
-
// Assert
|
|
98
|
-
await waitForText(setup, "
|
|
97
|
+
// Assert - the overlay: verdict selector and word-buttons
|
|
98
|
+
await waitForText(setup, "[Approve]"); // nothing pending: approve default
|
|
99
99
|
const frame = setup.captureCharFrame();
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
expect(frame).toContain("
|
|
103
|
-
expect(frame).toContain("[Approve]"); // nothing pending: approve default
|
|
104
|
-
expect(frame).toContain(" Submit ");
|
|
101
|
+
expect(frame).toContain("[Approve]");
|
|
102
|
+
expect(frame).toContain(" send message ");
|
|
105
103
|
expect(frame).toContain(" Cancel ");
|
|
106
104
|
});
|
|
107
105
|
|
|
108
|
-
test("left/right cycles the verdict selector in the
|
|
106
|
+
test("left/right cycles the verdict selector in the overlay", async () => {
|
|
109
107
|
// Arrange
|
|
110
108
|
const setup = await renderApp();
|
|
111
109
|
|
|
@@ -133,7 +131,7 @@ describe("rail submit confirm", () => {
|
|
|
133
131
|
await waitForText(setup, "[Changes]");
|
|
134
132
|
});
|
|
135
133
|
|
|
136
|
-
test("esc
|
|
134
|
+
test("esc closes the overlay", async () => {
|
|
137
135
|
// Arrange
|
|
138
136
|
const setup = await renderApp();
|
|
139
137
|
|
|
@@ -145,15 +143,13 @@ describe("rail submit confirm", () => {
|
|
|
145
143
|
// Act
|
|
146
144
|
await press(setup, "escape");
|
|
147
145
|
|
|
148
|
-
// Assert
|
|
149
|
-
// a bare ESC settles after the parser's escape-sequence window
|
|
146
|
+
// Assert - a bare ESC settles after the parser's escape-sequence window
|
|
150
147
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
151
148
|
|
|
152
|
-
expect(frame).not.toContain("
|
|
153
|
-
expect(frame).toContain("Submit review");
|
|
149
|
+
expect(frame).not.toContain(" Cancel ");
|
|
154
150
|
});
|
|
155
151
|
|
|
156
|
-
test("enter in the
|
|
152
|
+
test("enter in the overlay resolves the session through the controller", async () => {
|
|
157
153
|
// Arrange
|
|
158
154
|
seedAnnotations(1);
|
|
159
155
|
const setup = await renderApp();
|
|
@@ -163,16 +159,12 @@ describe("rail submit confirm", () => {
|
|
|
163
159
|
|
|
164
160
|
// Assert
|
|
165
161
|
await waitForText(setup, "[Changes]"); // pending items: request changes default
|
|
166
|
-
const frame = setup.captureCharFrame();
|
|
167
|
-
|
|
168
|
-
expect(frame).toContain("submit review");
|
|
169
162
|
|
|
170
163
|
// Act
|
|
171
164
|
await setup.mockInput.typeText("Tighten the steps.");
|
|
172
165
|
await press(setup, "enter");
|
|
173
166
|
|
|
174
|
-
// Assert
|
|
175
|
-
// the completion flow after submit is unchanged
|
|
167
|
+
// Assert - the completion flow after submit is unchanged
|
|
176
168
|
await waitForText(setup, "feedback sent");
|
|
177
169
|
const stored = server.core.sessionGet(session.id);
|
|
178
170
|
|
|
@@ -180,65 +172,20 @@ describe("rail submit confirm", () => {
|
|
|
180
172
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
181
173
|
});
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
function railText(frame: string): string {
|
|
185
|
-
const rows = frame.split("\n");
|
|
186
|
-
const railColumn = rows.find((row) => row.includes("Review Tree Agent"))!.indexOf("│");
|
|
187
|
-
|
|
188
|
-
return rows.map((row) => row.slice(railColumn)).join("\n");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
test("the annotation stack stays scrollable while the card is open", async () => {
|
|
192
|
-
// Arrange
|
|
193
|
-
seedAnnotations(12);
|
|
194
|
-
const setup = await renderApp();
|
|
195
|
-
|
|
196
|
-
// Act
|
|
197
|
-
// walk card focus to the last one: the rail scrolls it into view
|
|
198
|
-
for (let index = 0; index < 12; index++) await pressKey(setup, "n", { meta: true });
|
|
199
|
-
|
|
200
|
-
// Assert
|
|
201
|
-
await waitForText(setup, "note 12");
|
|
202
|
-
expect(railText(setup.captureCharFrame())).not.toContain("note 01");
|
|
203
|
-
|
|
204
|
-
// Act
|
|
205
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
206
|
-
|
|
207
|
-
// Assert
|
|
208
|
-
await waitForText(setup, "[Changes]");
|
|
209
|
-
// the card is pinned outside the scrollbox; the stack stays scrolled off the top
|
|
210
|
-
expect(railText(setup.captureCharFrame())).not.toContain("note 01");
|
|
211
|
-
|
|
212
|
-
// Act
|
|
213
|
-
// the stack still scrolls with the card open: wheel over the rail
|
|
214
|
-
// brings the last card back into view while the confirm card stays put
|
|
215
|
-
for (let turn = 0; turn < 12; turn++) await setup.mockMouse.scroll(100, 10, "down");
|
|
216
|
-
await settle(setup);
|
|
217
|
-
|
|
218
|
-
// Assert
|
|
219
|
-
await waitForText(setup, "note 12");
|
|
220
|
-
const scrolledWithCard = railText(setup.captureCharFrame());
|
|
221
|
-
|
|
222
|
-
expect(scrolledWithCard).toContain("submit review");
|
|
223
|
-
expect(scrolledWithCard).not.toContain("note 01");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("read-only observers never see the confirm card", async () => {
|
|
175
|
+
test("read-only observers cannot open the confirm overlay", async () => {
|
|
227
176
|
// Arrange
|
|
228
177
|
seedAnnotations(1);
|
|
229
178
|
const setup = await renderApp({ readOnly: true });
|
|
230
179
|
|
|
231
180
|
// Act
|
|
232
181
|
await pressKey(setup, "RETURN", { meta: true });
|
|
182
|
+
await settle(setup);
|
|
233
183
|
|
|
234
|
-
// Assert
|
|
235
|
-
await waitForText(setup, "observer - read-only");
|
|
184
|
+
// Assert - no overlay, and the session stays pending
|
|
236
185
|
const frame = setup.captureCharFrame();
|
|
237
186
|
|
|
238
|
-
expect(frame).toContain("observer - read-only");
|
|
239
|
-
expect(frame).not.toContain("1 annotations");
|
|
240
187
|
expect(frame).not.toContain("[Changes]");
|
|
241
|
-
expect(
|
|
188
|
+
expect(frame).not.toContain(" Cancel ");
|
|
242
189
|
expect(server.core.sessionGet(session.id).status).toBe("pending");
|
|
243
190
|
});
|
|
244
191
|
});
|