@cueloop/client 0.1.0-alpha.68 → 0.1.0-alpha.69
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 +7 -6
- package/src/App.appearance.test.tsx +8 -6
- package/src/App.diff.test.tsx +3 -3
- package/src/App.export.test.tsx +9 -3
- package/src/App.plan-review.test.tsx +10 -8
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +17 -28
- package/src/App.readonly.test.tsx +3 -3
- package/src/App.rendering.test.tsx +3 -4
- package/src/App.test.tsx +14 -18
- package/src/App.tsx +117 -42
- package/src/App.walk.test.tsx +2 -2
- package/src/App.workbench.test.tsx +85 -14
- package/src/app-screens.tsx +26 -10
- package/src/app-view-model.ts +17 -3
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +126 -0
- package/src/components/AnnotationCards.tsx +6 -6
- package/src/components/AppShell.tsx +62 -15
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +69 -0
- package/src/components/ChangesColumn.tsx +3 -2
- package/src/components/{DiffSheet.scroll.test.tsx → DiffContentView.scroll.test.tsx} +2 -2
- package/src/components/{DiffSheet.split.test.tsx → DiffContentView.split.test.tsx} +3 -3
- package/src/components/{DiffSheet.stories.tsx → DiffContentView.stories.tsx} +3 -3
- package/src/components/{DiffSheet.syntax.test.tsx → DiffContentView.syntax.test.tsx} +2 -2
- package/src/components/{DiffSheet.tsx → DiffContentView.tsx} +187 -28
- package/src/components/DiscussionMarkerRail.tsx +2 -2
- package/src/components/EditorGrid.tabs.test.tsx +17 -4
- package/src/components/EditorGrid.tsx +49 -19
- package/src/components/FileContentsView.test.tsx +3 -3
- package/src/components/FileContentsView.tsx +37 -17
- package/src/components/{SurfaceScrollbar.stories.tsx → OverlayScrollbar.stories.tsx} +5 -6
- package/src/components/{SurfaceScrollbar.test.tsx → OverlayScrollbar.test.tsx} +41 -13
- package/src/components/{SurfaceScrollbar.tsx → OverlayScrollbar.tsx} +49 -13
- package/src/components/ProjectTreeView.tsx +3 -2
- package/src/components/{PrototypeSheet.stories.tsx → PrototypeContentView.stories.tsx} +4 -5
- package/src/components/{PrototypeSheet.tsx → PrototypeContentView.tsx} +58 -58
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.tsx +3 -2
- package/src/components/{InboxList.stories.tsx → ThreadTree.stories.tsx} +5 -5
- package/src/components/{InboxList.tsx → ThreadTree.tsx} +6 -6
- package/src/components/ThreadsSidebar.tsx +5 -4
- package/src/components/TreePane.tsx +3 -2
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +163 -283
- package/src/components/{diff-sheet-layout.test.ts → diff-content-view-layout.test.ts} +1 -1
- package/src/components/{diff-sheet-layout.ts → diff-content-view-layout.ts} +1 -1
- package/src/config.test.ts +19 -6
- package/src/config.ts +26 -6
- package/src/intent-dispatch.test.ts +2 -0
- package/src/intent-dispatch.ts +1 -1
- package/src/key-bindings.test.ts +1 -1
- package/src/key-bindings.ts +5 -1
- package/src/keymap.ts +1 -1
- package/src/prototype-browser.ts +8 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/session-controller.live-diff.test.ts +198 -0
- package/src/session-controller.ts +141 -26
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +32 -18
- package/src/test-support.ts +42 -3
- package/src/theme-presets.ts +20 -0
- package/src/theme.ts +18 -0
- package/src/thread-chords.ts +1 -1
- package/src/use-annotation-surface.tsx +12 -2
- package/src/use-settings-dialog.tsx +4 -4
- package/src/view-diff.test.ts +44 -1
- package/src/view-diff.ts +81 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +0 -49
- package/src/components/AnnotationCard.stories.tsx +0 -94
- package/src/components/AnnotationCard.tsx +0 -250
- package/src/components/MarkerPopover.stories.tsx +0 -58
- package/src/components/MarkerPopover.tsx +0 -109
- package/src/components/WelcomeSurface.stories.tsx +0 -12
- package/src/components/WelcomeSurface.tsx +0 -52
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.69",
|
|
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": {
|
|
@@ -20,16 +20,17 @@
|
|
|
20
20
|
"type": "module",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./src/index.ts",
|
|
23
|
-
"./config": "./src/config.ts"
|
|
23
|
+
"./config": "./src/config.ts",
|
|
24
|
+
"./slash-palette": "./src/slash-palette.ts"
|
|
24
25
|
},
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|
|
27
28
|
},
|
|
28
29
|
"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.
|
|
30
|
+
"@cueloop/daemon": "0.1.0-alpha.69",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.69",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.69",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.69",
|
|
33
34
|
"@opentui/core": "^0.5.8",
|
|
34
35
|
"@opentui/keymap": "0.5.8",
|
|
35
36
|
"@opentui/react": "^0.5.8",
|
|
@@ -5,11 +5,10 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
12
|
-
import { isolateUserConfig, settle, waitForText } from "./test-support";
|
|
11
|
+
import { isolateUserConfig, settle, waitForText, renderReadyApp } from "./test-support";
|
|
13
12
|
|
|
14
13
|
const PLAN = "# Sync Plan\n\n## Steps\n\n- flush the queue\n";
|
|
15
14
|
|
|
@@ -37,10 +36,13 @@ afterEach(() => {
|
|
|
37
36
|
describe("appearance", () => {
|
|
38
37
|
test("renders the review on a light terminal", async () => {
|
|
39
38
|
// Arrange + Act
|
|
40
|
-
const setup = await
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const setup = await renderReadyApp(
|
|
40
|
+
<App home={home} sessionId={session.id} appearance="light" />,
|
|
41
|
+
{
|
|
42
|
+
width: 120,
|
|
43
|
+
height: 32,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
44
46
|
|
|
45
47
|
// Assert
|
|
46
48
|
await waitForText(setup, "cueloop");
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
isolateUserConfig,
|
|
17
16
|
press,
|
|
18
17
|
pressKey,
|
|
18
|
+
renderReadyApp,
|
|
19
19
|
typeText,
|
|
20
20
|
waitForState,
|
|
21
21
|
waitForText,
|
|
@@ -54,7 +54,7 @@ afterEach(() => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
async function renderApp(sessionId = session.id) {
|
|
57
|
-
const setup = await
|
|
57
|
+
const setup = await renderReadyApp(<App home={home} sessionId={sessionId} />, {
|
|
58
58
|
width: 120,
|
|
59
59
|
height: 30,
|
|
60
60
|
});
|
|
@@ -199,7 +199,7 @@ describe("diff review", () => {
|
|
|
199
199
|
],
|
|
200
200
|
},
|
|
201
201
|
});
|
|
202
|
-
const setup = await
|
|
202
|
+
const setup = await renderReadyApp(<App home={home} sessionId={withFiles.id} />, {
|
|
203
203
|
width: 120,
|
|
204
204
|
height: 30,
|
|
205
205
|
});
|
package/src/App.export.test.tsx
CHANGED
|
@@ -13,11 +13,17 @@ import {
|
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import React from "react";
|
|
16
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
17
16
|
import { DaemonServer } from "@cueloop/daemon";
|
|
18
17
|
import type { ReviewSession } from "@cueloop/schema";
|
|
19
18
|
import { App } from "./App";
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
isolateUserConfig,
|
|
21
|
+
press,
|
|
22
|
+
waitForState,
|
|
23
|
+
waitForText,
|
|
24
|
+
pressKey,
|
|
25
|
+
renderReadyApp,
|
|
26
|
+
} from "./test-support";
|
|
21
27
|
|
|
22
28
|
const PLAN = `# Migration Plan
|
|
23
29
|
|
|
@@ -61,7 +67,7 @@ afterEach(() => {
|
|
|
61
67
|
describe("obsidian export on resolve", () => {
|
|
62
68
|
test("submitting a review writes the plan into the vault and shows the path", async () => {
|
|
63
69
|
// Arrange
|
|
64
|
-
const setup = await
|
|
70
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
65
71
|
width: 120,
|
|
66
72
|
height: 32,
|
|
67
73
|
});
|
|
@@ -5,17 +5,18 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
12
|
-
import { DEFAULT_QUICK_ACTIONS
|
|
11
|
+
import { DEFAULT_QUICK_ACTIONS } from "./config";
|
|
12
|
+
import { slashItemsFrom } from "./slash-palette";
|
|
13
13
|
import {
|
|
14
14
|
clickText,
|
|
15
15
|
dragText,
|
|
16
16
|
isolateUserConfig,
|
|
17
17
|
press,
|
|
18
18
|
pressKey,
|
|
19
|
+
renderReadyApp,
|
|
19
20
|
typeText as type,
|
|
20
21
|
waitForState,
|
|
21
22
|
waitForText,
|
|
@@ -60,7 +61,7 @@ afterEach(() => {
|
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
async function renderApp() {
|
|
63
|
-
const setup = await
|
|
64
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
64
65
|
width: 120,
|
|
65
66
|
height: 32,
|
|
66
67
|
});
|
|
@@ -110,7 +111,7 @@ describe("share button", () => {
|
|
|
110
111
|
|
|
111
112
|
test("a read-only viewer (a plan shared over ssh) never sees the Share button", async () => {
|
|
112
113
|
// Arrange / Act
|
|
113
|
-
const viewer = await
|
|
114
|
+
const viewer = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
114
115
|
width: 120,
|
|
115
116
|
height: 32,
|
|
116
117
|
});
|
|
@@ -149,7 +150,7 @@ describe("edit affordance", () => {
|
|
|
149
150
|
|
|
150
151
|
test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
|
|
151
152
|
// Arrange / Act
|
|
152
|
-
const viewer = await
|
|
153
|
+
const viewer = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
153
154
|
width: 120,
|
|
154
155
|
height: 32,
|
|
155
156
|
});
|
|
@@ -338,7 +339,7 @@ describe("edit-exit reconciliation", () => {
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
describe("the quick-action palette", () => {
|
|
341
|
-
test("/ lists the quick actions and a pick
|
|
342
|
+
test("/ lists the quick actions and a pick inserts the reference, not the body", async () => {
|
|
342
343
|
// Arrange
|
|
343
344
|
const setup = await renderApp();
|
|
344
345
|
|
|
@@ -351,11 +352,12 @@ describe("the quick-action palette", () => {
|
|
|
351
352
|
await press(setup, "enter");
|
|
352
353
|
await pressKey(setup, "RETURN", { meta: true });
|
|
353
354
|
|
|
354
|
-
// Assert -
|
|
355
|
+
// Assert - the comment holds the /name reference, not the expanded body
|
|
355
356
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
356
357
|
const stored = server.core.sessionGet(session.id);
|
|
358
|
+
const secondName = slashItemsFrom(DEFAULT_QUICK_ACTIONS)[1]!.name;
|
|
357
359
|
|
|
358
360
|
expect(stored.annotations[0]!.kind).toBe("comment");
|
|
359
|
-
expect(stored.annotations[0]!.body).toBe(
|
|
361
|
+
expect(stored.annotations[0]!.body.trim()).toBe(`/${secondName}`);
|
|
360
362
|
});
|
|
361
363
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** The default prototype is a Markdown design doc: with no experimental flag it renders through the thread/markdown path (not the kitty pixel view), its headings and fenced sections show as text, and a quote-anchored comment lands on a line - the same as a plan. A real in-process daemon; char-frame assertions over the real App. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import type { ReviewSession } from "@cueloop/schema";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import { isolateUserConfig, renderReadyApp, settle, waitForText } from "./test-support";
|
|
12
|
+
|
|
13
|
+
const DOC = `# PromoCard
|
|
14
|
+
|
|
15
|
+
A promotional card for a growth surface.
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
\`\`\`ts
|
|
20
|
+
interface PromoCardProps {
|
|
21
|
+
title: string
|
|
22
|
+
onDismiss?: () => void
|
|
23
|
+
}
|
|
24
|
+
\`\`\`
|
|
25
|
+
|
|
26
|
+
## Composition
|
|
27
|
+
|
|
28
|
+
\`\`\`
|
|
29
|
+
PromoCard
|
|
30
|
+
└─ TextGroup reuse: Text
|
|
31
|
+
\`\`\`
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
let home: string;
|
|
35
|
+
let server: DaemonServer;
|
|
36
|
+
let session: ReviewSession;
|
|
37
|
+
let restoreUserConfig: () => void;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-prototype-md-"));
|
|
41
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
42
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
43
|
+
server.start();
|
|
44
|
+
session = server.core.sessionCreate({
|
|
45
|
+
workspace: { repoRoot: "/repo", branch: "promo-card" },
|
|
46
|
+
artifact: { type: "prototype", content: DOC, meta: { title: "PromoCard", agent: "pi" } },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
restoreUserConfig();
|
|
51
|
+
server.stop();
|
|
52
|
+
rmSync(home, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("markdown prototype review", () => {
|
|
56
|
+
test("renders as a markdown design doc, not the pixel view", async () => {
|
|
57
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
58
|
+
width: 120,
|
|
59
|
+
height: 40,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await waitForText(setup, "PromoCard");
|
|
63
|
+
await settle(setup);
|
|
64
|
+
const frame = setup.captureCharFrame();
|
|
65
|
+
|
|
66
|
+
// the markdown path renders the doc's headings and interface as text
|
|
67
|
+
expect(frame).toContain("API");
|
|
68
|
+
expect(frame).toContain("PromoCardProps");
|
|
69
|
+
expect(frame).toContain("Composition");
|
|
70
|
+
// and never falls into the pixel view's status line
|
|
71
|
+
expect(frame).not.toContain("rendering prototype");
|
|
72
|
+
expect(frame).not.toContain("graphics terminal");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a quote-anchored comment stores against the doc, like a plan", async () => {
|
|
76
|
+
server.core.sessionAnnotate(session.id, {
|
|
77
|
+
id: "own_1",
|
|
78
|
+
kind: "comment",
|
|
79
|
+
anchor: { quote: "onDismiss?: () => void", prefix: "", suffix: "" },
|
|
80
|
+
body: "make dismiss required for a modal surface?",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
84
|
+
width: 120,
|
|
85
|
+
height: 40,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await waitForText(setup, "make dismiss required");
|
|
89
|
+
expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
|
|
90
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.selector).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
/** Prototype review end to end: a click on the rendered page selects a DOM element
|
|
1
|
+
/** Prototype review end to end, in the opt-in EXPERIMENTAL pixel mode ([experimental] prototype_pixels): a click on the rendered page selects a DOM element and opens the compose card directly - the same inline composer plan and diff use - a typed comment saves through the controller on cmd+enter, and the annotation anchors to the element's selector. The headless browser is replaced with a fake so no Chrome or kitty output is needed. Char-frame assertions over the real App and a real in-process daemon. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
import {
|
|
18
17
|
isolateUserConfig,
|
|
19
18
|
press,
|
|
19
|
+
renderReadyApp,
|
|
20
20
|
settle,
|
|
21
21
|
typeText,
|
|
22
22
|
waitForState,
|
|
@@ -60,7 +60,9 @@ let restoreUserConfig: () => void;
|
|
|
60
60
|
|
|
61
61
|
beforeEach(() => {
|
|
62
62
|
home = mkdtempSync(join(tmpdir(), "cueloop-prototype-"));
|
|
63
|
-
|
|
63
|
+
// the pixel mockup is opt-in; these tests exercise that experimental path
|
|
64
|
+
writeFileSync(join(home, "config.toml"), "[experimental]\nprototype_pixels = true\n");
|
|
65
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
64
66
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
65
67
|
server.start();
|
|
66
68
|
session = server.core.sessionCreate({
|
|
@@ -84,7 +86,7 @@ afterEach(() => {
|
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
async function renderApp() {
|
|
87
|
-
const setup = await
|
|
89
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
88
90
|
width: 120,
|
|
89
91
|
height: 32,
|
|
90
92
|
});
|
|
@@ -94,16 +96,6 @@ async function renderApp() {
|
|
|
94
96
|
return setup;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
98
|
-
|
|
99
|
-
async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
100
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
101
|
-
const row = lines.findIndex((line) => line.includes(needle));
|
|
102
|
-
|
|
103
|
-
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
104
|
-
await setup.renderOnce();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
99
|
describe("prototype review", () => {
|
|
108
100
|
test("click an element, comment on it, and it saves to the daemon", async () => {
|
|
109
101
|
// Arrange - wait until the fake browser produced its first screenshot, so
|
|
@@ -112,21 +104,20 @@ describe("prototype review", () => {
|
|
|
112
104
|
|
|
113
105
|
await waitForState(setup, () => rendered);
|
|
114
106
|
await settle(setup);
|
|
115
|
-
// selecting an element must not re-screenshot the page (the
|
|
116
|
-
// lag regression): the
|
|
107
|
+
// selecting an element must not re-screenshot the page (the compose-open
|
|
108
|
+
// lag regression): the click opens the composer, not a fresh Chromium capture
|
|
117
109
|
const screenshotsAfterLoad = screenshotCount;
|
|
118
110
|
|
|
119
|
-
// Act
|
|
111
|
+
// Act - a click opens the compose card directly on the element
|
|
120
112
|
await setup.mockMouse.click(6, 6);
|
|
121
|
-
await waitForText(setup, "
|
|
113
|
+
await waitForText(setup, "●");
|
|
122
114
|
expect(screenshotCount).toBe(screenshotsAfterLoad);
|
|
123
|
-
await clickText(setup, "comment");
|
|
124
|
-
await waitForText(setup, "write a note");
|
|
125
115
|
// the keymap-suppression flag must propagate before typing so keys reach the
|
|
126
116
|
// textarea, not the global keymap
|
|
127
117
|
await settle(setup);
|
|
128
118
|
await typeText(setup, "tighten the padding");
|
|
129
|
-
|
|
119
|
+
// cmd+enter sends, matching the plan and diff composer; plain enter is a newline
|
|
120
|
+
setup.mockInput.pressKey("RETURN", { meta: true });
|
|
130
121
|
|
|
131
122
|
// Assert - the annotation is stored against the element's selector
|
|
132
123
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
@@ -144,16 +135,14 @@ describe("prototype review", () => {
|
|
|
144
135
|
await waitForState(setup, () => rendered);
|
|
145
136
|
await settle(setup);
|
|
146
137
|
await setup.mockMouse.click(6, 6);
|
|
147
|
-
await waitForText(setup, "
|
|
148
|
-
await clickText(setup, "comment");
|
|
149
|
-
await waitForText(setup, "write a note");
|
|
138
|
+
await waitForText(setup, "●");
|
|
150
139
|
await settle(setup);
|
|
151
140
|
|
|
152
141
|
// Act
|
|
153
142
|
await press(setup, "escape");
|
|
154
143
|
|
|
155
144
|
// Assert - the composer closes and nothing was saved
|
|
156
|
-
await waitForTextGone(setup, "
|
|
145
|
+
await waitForTextGone(setup, "●");
|
|
157
146
|
expect(server.core.sessionGet(session.id).annotations).toHaveLength(0);
|
|
158
147
|
});
|
|
159
148
|
|
|
@@ -167,13 +156,13 @@ describe("prototype review", () => {
|
|
|
167
156
|
// open a selection, then scroll: the scroll drops it (it would drift once
|
|
168
157
|
// the page moves under it)
|
|
169
158
|
await setup.mockMouse.click(6, 6);
|
|
170
|
-
await waitForText(setup, "
|
|
159
|
+
await waitForText(setup, "●");
|
|
171
160
|
// scrolling moves the page, so it DOES re-screenshot (unlike a click)
|
|
172
161
|
const screenshotsBeforeScroll = screenshotCount;
|
|
173
162
|
|
|
174
163
|
await setup.mockMouse.scroll(6, 6, "down");
|
|
175
164
|
await waitForState(setup, () => scrollDeltas.length === 1);
|
|
176
|
-
await waitForTextGone(setup, "
|
|
165
|
+
await waitForTextGone(setup, "●");
|
|
177
166
|
await waitForState(setup, () => screenshotCount > screenshotsBeforeScroll);
|
|
178
167
|
|
|
179
168
|
// Act - scroll back up
|
|
@@ -5,7 +5,6 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
frameRow,
|
|
16
15
|
isolateUserConfig,
|
|
17
16
|
pressKey,
|
|
17
|
+
renderReadyApp,
|
|
18
18
|
typeText,
|
|
19
19
|
waitForText,
|
|
20
20
|
} from "./test-support";
|
|
@@ -57,7 +57,7 @@ afterEach(() => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
async function renderObserver() {
|
|
60
|
-
const setup = await
|
|
60
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
61
61
|
width: 120,
|
|
62
62
|
height: 32,
|
|
63
63
|
});
|
|
@@ -144,7 +144,7 @@ describe("a resolved review is read-only for its owner too", () => {
|
|
|
144
144
|
body: "settled",
|
|
145
145
|
});
|
|
146
146
|
server.core.sessionResolve(session.id, "approve", "");
|
|
147
|
-
const setup = await
|
|
147
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
148
148
|
width: 120,
|
|
149
149
|
height: 32,
|
|
150
150
|
});
|
|
@@ -5,11 +5,10 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
12
|
-
import { isolateUserConfig, waitForText } from "./test-support";
|
|
11
|
+
import { isolateUserConfig, waitForText, renderReadyApp } from "./test-support";
|
|
13
12
|
|
|
14
13
|
const PLAN = `# Render Plan
|
|
15
14
|
|
|
@@ -51,7 +50,7 @@ afterEach(() => {
|
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
async function frame(): Promise<string> {
|
|
54
|
-
const setup = await
|
|
53
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
55
54
|
width: 120,
|
|
56
55
|
height: 36,
|
|
57
56
|
});
|
|
@@ -116,7 +115,7 @@ describe("block spacing", () => {
|
|
|
116
115
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
117
116
|
artifact: { type: "plan", content: listPlan, meta: {} },
|
|
118
117
|
});
|
|
119
|
-
const setup = await
|
|
118
|
+
const setup = await renderReadyApp(<App home={home} sessionId={s2.id} />, {
|
|
120
119
|
width: 120,
|
|
121
120
|
height: 30,
|
|
122
121
|
});
|
package/src/App.test.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
9
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
locateText,
|
|
18
17
|
press,
|
|
19
18
|
pressKey,
|
|
19
|
+
renderReadyApp,
|
|
20
20
|
typeText as type,
|
|
21
21
|
waitForState,
|
|
22
22
|
waitForText,
|
|
@@ -60,15 +60,10 @@ afterEach(() => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
async function renderApp(sessionId?: string) {
|
|
63
|
-
|
|
63
|
+
return renderReadyApp(<App home={home} sessionId={sessionId ?? session.id} />, {
|
|
64
64
|
width: 120,
|
|
65
65
|
height: 32,
|
|
66
66
|
});
|
|
67
|
-
|
|
68
|
-
// the async daemon connect + first fetch land within the frame wait
|
|
69
|
-
await waitForText(setup, "cueloop");
|
|
70
|
-
|
|
71
|
-
return setup;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
describe("plan rendering", () => {
|
|
@@ -247,14 +242,15 @@ describe("submit", () => {
|
|
|
247
242
|
describe("no-thread shell", () => {
|
|
248
243
|
test("opening with nothing selected lands in the shell with a Welcome tab and the Threads sidebar", async () => {
|
|
249
244
|
// Arrange
|
|
250
|
-
const setup = await
|
|
245
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
251
246
|
|
|
252
|
-
|
|
247
|
+
// the Welcome playground measures its width before it paints, so wait for its copy, not the header
|
|
248
|
+
await waitForText(setup, "Getting started");
|
|
253
249
|
|
|
254
250
|
// Assert - the same shell header, the disposable Welcome tab, and the pending thread
|
|
255
251
|
const frame = setup.captureCharFrame();
|
|
256
252
|
|
|
257
|
-
expect(frame).toContain("
|
|
253
|
+
expect(frame).toContain("Getting started");
|
|
258
254
|
expect(frame).toContain("Migration Plan");
|
|
259
255
|
expect(frame).not.toContain("· resume"); // the bespoke inbox header is retired
|
|
260
256
|
|
|
@@ -272,9 +268,9 @@ describe("no-thread shell", () => {
|
|
|
272
268
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
273
269
|
artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
|
|
274
270
|
});
|
|
275
|
-
const setup = await
|
|
271
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
276
272
|
|
|
277
|
-
await waitForText(setup, "
|
|
273
|
+
await waitForText(setup, "Getting started");
|
|
278
274
|
await waitForText(setup, "Other Plan"); // the sidebar opens by default here
|
|
279
275
|
|
|
280
276
|
// Act - open the thread under the cursor
|
|
@@ -291,7 +287,7 @@ describe("no-thread shell", () => {
|
|
|
291
287
|
|
|
292
288
|
test("the row kebab menu pins a thread into a Pinned section", async () => {
|
|
293
289
|
// Arrange
|
|
294
|
-
const setup = await
|
|
290
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
295
291
|
|
|
296
292
|
await waitForText(setup, "Migration Plan");
|
|
297
293
|
|
|
@@ -311,23 +307,23 @@ describe("no-thread shell", () => {
|
|
|
311
307
|
|
|
312
308
|
test("a bare launch shows the Welcome surface with the right region collapsed", async () => {
|
|
313
309
|
// Arrange
|
|
314
|
-
const setup = await
|
|
310
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
315
311
|
|
|
316
|
-
await waitForText(setup, "
|
|
312
|
+
await waitForText(setup, "Getting started");
|
|
317
313
|
|
|
318
314
|
// Assert - Welcome fills the thread pane (not a Changes tab); the right region stays collapsed
|
|
319
315
|
const frame = setup.captureCharFrame();
|
|
320
316
|
|
|
321
|
-
expect(frame).toContain("
|
|
317
|
+
expect(frame).toContain("Getting started");
|
|
322
318
|
expect(frame).not.toContain("Changes"); // the Changes editor is closed on a bare launch
|
|
323
319
|
expect(frame).toContain("Migration Plan"); // the sidebar lists the pending thread
|
|
324
320
|
});
|
|
325
321
|
|
|
326
322
|
test("the menu opens from the shell gear and escape is not a trap", async () => {
|
|
327
323
|
// Arrange
|
|
328
|
-
const setup = await
|
|
324
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
329
325
|
|
|
330
|
-
await waitForText(setup, "
|
|
326
|
+
await waitForText(setup, "Getting started");
|
|
331
327
|
|
|
332
328
|
// Act - open the settings dialog from the top-left gear (the Threads panel header, row 0)
|
|
333
329
|
await setup.mockMouse.click(1, 0);
|