@cueloop/client 0.1.0-alpha.31 → 0.1.0-alpha.33
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 +4 -3
- package/src/App.diff.test.tsx +5 -2
- package/src/App.export.test.tsx +5 -7
- package/src/App.plan-review.test.tsx +14 -17
- package/src/App.readonly.test.tsx +9 -7
- package/src/App.rendering.test.tsx +6 -2
- package/src/App.review-panel.test.tsx +19 -14
- package/src/App.submit-confirm.test.tsx +6 -7
- package/src/App.test.tsx +8 -5
- package/src/App.toast.test.tsx +68 -0
- package/src/App.tsx +154 -50
- package/src/App.walk.test.tsx +7 -4
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +18 -13
- package/src/components/CompletionOverlay.tsx +1 -1
- package/src/components/ConfirmCard.tsx +1 -0
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/ConfirmDialog.tsx +1 -1
- package/src/components/DiffSheet.stories.tsx +21 -3
- package/src/components/DiffSheet.tsx +45 -19
- package/src/components/InboxList.stories.tsx +1 -1
- package/src/components/InboxList.tsx +3 -3
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +42 -0
- package/src/components/MenuBar.stories.tsx +33 -0
- package/src/components/MenuBar.tsx +58 -0
- package/src/components/PlanSheet.stories.tsx +2 -2
- package/src/components/PlanSheet.tsx +26 -75
- package/src/components/PromptDialog.stories.tsx +2 -2
- package/src/components/PromptDialog.tsx +1 -1
- package/src/components/ReviewPanel.stories.tsx +5 -17
- package/src/components/ReviewPanel.tsx +10 -29
- package/src/components/ReviewRail.stories.tsx +39 -0
- package/src/components/ReviewRail.tsx +62 -57
- package/src/components/SettingsDialog.stories.tsx +1 -1
- package/src/components/SettingsDialog.tsx +4 -3
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/SettingsRows.tsx +1 -1
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +29 -0
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/WalkWizard.tsx +1 -1
- package/src/components/__snapshots__/stories.test.tsx.snap +598 -566
- package/src/components/primitives/Button.stories.tsx +2 -2
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/Dialog.tsx +7 -5
- package/src/components/primitives/Tabs.tsx +19 -38
- package/src/components/stories-app.tsx +1 -1
- package/src/components/stories.test.tsx +2 -0
- package/src/key-bindings.test.ts +4 -4
- package/src/key-bindings.ts +41 -2
- package/src/serve.test.ts +2 -2
- package/src/session-controller.share.test.ts +13 -0
- package/src/session-controller.ts +19 -1
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +113 -23
- package/src/theme.ts +16 -16
- package/src/version.ts +5 -0
- package/src/components/ComposeBar.stories.tsx +0 -18
- package/src/components/ComposeBar.tsx +0 -54
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.33",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
11
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
12
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
13
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
10
|
+
"@cueloop/daemon": "0.1.0-alpha.33",
|
|
11
|
+
"@cueloop/extension-api": "0.1.0-alpha.33",
|
|
12
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.33",
|
|
13
|
+
"@cueloop/schema": "0.1.0-alpha.33",
|
|
14
14
|
"@opentui/core": "^0.5.2",
|
|
15
15
|
"@opentui/keymap": "0.5.2",
|
|
16
16
|
"@opentui/react": "^0.5.2",
|
|
@@ -10,7 +10,7 @@ import { ManualClock } from "@opentui/core/testing";
|
|
|
10
10
|
import { DaemonServer } from "@cueloop/daemon";
|
|
11
11
|
import type { ReviewSession } from "@cueloop/schema";
|
|
12
12
|
import { App } from "./App";
|
|
13
|
-
import { press, settle, waitForText } from "./test-support";
|
|
13
|
+
import { isolateUserConfig, press, settle, waitForText } from "./test-support";
|
|
14
14
|
|
|
15
15
|
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
16
|
|
|
@@ -18,12 +18,13 @@ let home: string;
|
|
|
18
18
|
let server: DaemonServer;
|
|
19
19
|
let session: ReviewSession;
|
|
20
20
|
let configPath: string;
|
|
21
|
+
let restoreUserConfig: () => void;
|
|
21
22
|
let clock: ManualClock;
|
|
22
23
|
|
|
23
24
|
beforeEach(() => {
|
|
24
25
|
home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
|
|
25
26
|
configPath = join(home, "config.toml");
|
|
26
|
-
|
|
27
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
27
28
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
28
29
|
server.start();
|
|
29
30
|
session = server.core.sessionCreate({
|
|
@@ -33,7 +34,7 @@ beforeEach(() => {
|
|
|
33
34
|
clock = new ManualClock();
|
|
34
35
|
});
|
|
35
36
|
afterEach(() => {
|
|
36
|
-
|
|
37
|
+
restoreUserConfig();
|
|
37
38
|
server.stop();
|
|
38
39
|
rmSync(home, { recursive: true, force: true });
|
|
39
40
|
});
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import { press, waitForText } from "./test-support";
|
|
12
|
+
import { isolateUserConfig, press, waitForText } from "./test-support";
|
|
13
13
|
|
|
14
14
|
const PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
15
15
|
index 111..222 100644
|
|
@@ -23,11 +23,13 @@ index 111..222 100644
|
|
|
23
23
|
`;
|
|
24
24
|
|
|
25
25
|
let home: string;
|
|
26
|
+
let restoreUserConfig: () => void;
|
|
26
27
|
let server: DaemonServer;
|
|
27
28
|
let session: ReviewSession;
|
|
28
29
|
|
|
29
30
|
beforeEach(() => {
|
|
30
31
|
home = mkdtempSync(join(tmpdir(), "cueloop-diff-"));
|
|
32
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
31
33
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
32
34
|
server.start();
|
|
33
35
|
session = server.core.sessionCreate({
|
|
@@ -36,6 +38,7 @@ beforeEach(() => {
|
|
|
36
38
|
});
|
|
37
39
|
});
|
|
38
40
|
afterEach(() => {
|
|
41
|
+
restoreUserConfig();
|
|
39
42
|
server.stop();
|
|
40
43
|
rmSync(home, { recursive: true, force: true });
|
|
41
44
|
});
|
|
@@ -69,7 +72,7 @@ describe("diff review", () => {
|
|
|
69
72
|
await press(setup, "c");
|
|
70
73
|
|
|
71
74
|
// Assert
|
|
72
|
-
await waitForText(setup,
|
|
75
|
+
await waitForText(setup, 'comment on "');
|
|
73
76
|
|
|
74
77
|
// Act
|
|
75
78
|
await setup.mockInput.typeText("Map needs an eviction story.");
|
package/src/App.export.test.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import { press, waitForState, waitForText } from "./test-support";
|
|
12
|
+
import { isolateUserConfig, press, waitForState, waitForText } from "./test-support";
|
|
13
13
|
|
|
14
14
|
const PLAN = `# Migration Plan
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ let home: string;
|
|
|
20
20
|
let vault: string;
|
|
21
21
|
let server: DaemonServer;
|
|
22
22
|
let session: ReviewSession;
|
|
23
|
-
let
|
|
23
|
+
let restoreUserConfig: () => void;
|
|
24
24
|
|
|
25
25
|
beforeEach(() => {
|
|
26
26
|
home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
|
|
@@ -28,8 +28,7 @@ beforeEach(() => {
|
|
|
28
28
|
mkdirSync(vault);
|
|
29
29
|
const configPath = join(home, "config.toml");
|
|
30
30
|
writeFileSync(configPath, `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`);
|
|
31
|
-
|
|
32
|
-
process.env.CUELOOP_CONFIG = configPath;
|
|
31
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
33
32
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
34
33
|
server.start();
|
|
35
34
|
session = server.core.sessionCreate({
|
|
@@ -38,8 +37,7 @@ beforeEach(() => {
|
|
|
38
37
|
});
|
|
39
38
|
});
|
|
40
39
|
afterEach(() => {
|
|
41
|
-
|
|
42
|
-
else process.env.CUELOOP_CONFIG = savedConfigEnv;
|
|
40
|
+
restoreUserConfig();
|
|
43
41
|
server.stop();
|
|
44
42
|
rmSync(home, { recursive: true, force: true });
|
|
45
43
|
});
|
|
@@ -54,7 +52,7 @@ describe("obsidian export on resolve", () => {
|
|
|
54
52
|
await press(setup, "enter"); // open the submit overlay, keep the default verdict (approve)
|
|
55
53
|
|
|
56
54
|
// Assert
|
|
57
|
-
await waitForText(setup, "
|
|
55
|
+
await waitForText(setup, "[Approve]");
|
|
58
56
|
|
|
59
57
|
// Act
|
|
60
58
|
await press(setup, "enter"); // submit
|
|
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
|
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
12
|
import { DARK as T } from "./theme";
|
|
13
|
-
import { press, settle, typeText as type, waitForState, waitForText, waitForTextGone } from "./test-support";
|
|
13
|
+
import { isolateUserConfig, press, settle, typeText as type, waitForState, waitForText, waitForTextGone } from "./test-support";
|
|
14
14
|
|
|
15
15
|
const PLAN = `# Migration Plan
|
|
16
16
|
|
|
@@ -27,14 +27,11 @@ The daemon persists sessions to disk atomically.
|
|
|
27
27
|
let home: string;
|
|
28
28
|
let server: DaemonServer;
|
|
29
29
|
let session: ReviewSession;
|
|
30
|
-
let
|
|
30
|
+
let restoreUserConfig: () => void;
|
|
31
31
|
|
|
32
32
|
beforeEach(() => {
|
|
33
33
|
home = mkdtempSync(join(tmpdir(), "cueloop-plan-review-"));
|
|
34
|
-
|
|
35
|
-
// not whatever review_state they last persisted (CI runs clean already).
|
|
36
|
-
priorConfig = process.env.CUELOOP_CONFIG;
|
|
37
|
-
process.env.CUELOOP_CONFIG = join(home, "no-config.toml");
|
|
34
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
38
35
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
39
36
|
server.start();
|
|
40
37
|
session = server.core.sessionCreate({
|
|
@@ -43,8 +40,7 @@ beforeEach(() => {
|
|
|
43
40
|
});
|
|
44
41
|
});
|
|
45
42
|
afterEach(() => {
|
|
46
|
-
|
|
47
|
-
else process.env.CUELOOP_CONFIG = priorConfig;
|
|
43
|
+
restoreUserConfig();
|
|
48
44
|
server.stop();
|
|
49
45
|
rmSync(home, { recursive: true, force: true });
|
|
50
46
|
});
|
|
@@ -225,7 +221,7 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
225
221
|
// Assert
|
|
226
222
|
// compose open: the anchor is painted selection-style. The box can render
|
|
227
223
|
// a frame before the anchor repaint settles, so wait on the color itself.
|
|
228
|
-
await waitForText(setup, "Save
|
|
224
|
+
await waitForText(setup, "Save");
|
|
229
225
|
expect(setup.captureCharFrame()).toContain("Cancel esc");
|
|
230
226
|
await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(T.accent));
|
|
231
227
|
expect(backgroundsOf(setup, "The daemon")).toContain(T.accent);
|
|
@@ -250,11 +246,11 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
250
246
|
// the whole cursor block is the anchor here; check the highlight landed
|
|
251
247
|
const stored = server.core.sessionGet(session.id);
|
|
252
248
|
expect(stored.annotations.length).toBe(1);
|
|
253
|
-
expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(T.
|
|
249
|
+
expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(T.markCommentBackground);
|
|
254
250
|
// renderApp plus this many frame-waits grazes the 5s default on a loaded CI
|
|
255
251
|
// runner, and the whole-suite publish lane has timed even 15s out; give the
|
|
256
252
|
// heaviest frame-wait chain generous headroom so runner load cannot flake it.
|
|
257
|
-
},
|
|
253
|
+
}, 60_000);
|
|
258
254
|
});
|
|
259
255
|
|
|
260
256
|
describe("compose newline convention", () => {
|
|
@@ -396,7 +392,7 @@ describe("the document selects, the rail edits", () => {
|
|
|
396
392
|
await press(setup, "enter");
|
|
397
393
|
await waitForText(setup, "COMMENT · pending");
|
|
398
394
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
399
|
-
expect(backgroundsOf(setup, "persists sessions")).toContain(T.
|
|
395
|
+
expect(backgroundsOf(setup, "persists sessions")).toContain(T.markCommentBackground);
|
|
400
396
|
|
|
401
397
|
// Act
|
|
402
398
|
await press(setup, "x");
|
|
@@ -404,7 +400,7 @@ describe("the document selects, the rail edits", () => {
|
|
|
404
400
|
// Assert
|
|
405
401
|
await waitForTextGone(setup, "COMMENT · pending");
|
|
406
402
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
407
|
-
expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.
|
|
403
|
+
expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.markCommentBackground);
|
|
408
404
|
});
|
|
409
405
|
});
|
|
410
406
|
|
|
@@ -436,8 +432,8 @@ describe("addressed annotations leave the open list", () => {
|
|
|
436
432
|
const frame = setup.captureCharFrame();
|
|
437
433
|
expect(frame).toContain("✓ 1 addressed by revision");
|
|
438
434
|
expect(frame).not.toContain("settled note");
|
|
439
|
-
expect(frame).toContain("
|
|
440
|
-
expect(backgroundsOf(setup, "The daemon")).not.toContain(T.
|
|
435
|
+
expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
|
|
436
|
+
expect(backgroundsOf(setup, "The daemon")).not.toContain(T.markCommentBackground); // no highlight paint
|
|
441
437
|
});
|
|
442
438
|
});
|
|
443
439
|
|
|
@@ -448,7 +444,8 @@ describe("sheet header", () => {
|
|
|
448
444
|
|
|
449
445
|
// Assert
|
|
450
446
|
const frame = setup.captureCharFrame();
|
|
451
|
-
expect(frame).toContain("submitted by agent/worker-3
|
|
447
|
+
expect(frame).toContain("submitted by agent/worker-3");
|
|
448
|
+
expect(frame).toContain("rev 1");
|
|
452
449
|
expect(frame).toContain("Edit");
|
|
453
450
|
expect(frame).toContain("Share");
|
|
454
451
|
});
|
|
@@ -457,7 +454,7 @@ describe("sheet header", () => {
|
|
|
457
454
|
// Arrange
|
|
458
455
|
const setup = await renderApp();
|
|
459
456
|
const lines = setup.captureCharFrame().split("\n");
|
|
460
|
-
const tabRow = lines.findIndex((line) => line.includes("Review (
|
|
457
|
+
const tabRow = lines.findIndex((line) => line.includes("Review") && line.includes("Agent"));
|
|
461
458
|
const tabColumn = lines[tabRow]!.indexOf("Agent");
|
|
462
459
|
|
|
463
460
|
// Act
|
|
@@ -9,7 +9,7 @@ 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 { press, waitForText } from "./test-support";
|
|
12
|
+
import { isolateUserConfig, press, waitForText } from "./test-support";
|
|
13
13
|
|
|
14
14
|
const PLAN = `# Migration Plan
|
|
15
15
|
|
|
@@ -24,11 +24,13 @@ The daemon persists sessions to disk atomically.
|
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
26
|
let home: string;
|
|
27
|
+
let restoreUserConfig: () => void;
|
|
27
28
|
let server: DaemonServer;
|
|
28
29
|
let session: ReviewSession;
|
|
29
30
|
|
|
30
31
|
beforeEach(() => {
|
|
31
32
|
home = mkdtempSync(join(tmpdir(), "cueloop-observer-"));
|
|
33
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
32
34
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
33
35
|
server.start();
|
|
34
36
|
session = server.core.sessionCreate({
|
|
@@ -37,6 +39,7 @@ beforeEach(() => {
|
|
|
37
39
|
});
|
|
38
40
|
});
|
|
39
41
|
afterEach(() => {
|
|
42
|
+
restoreUserConfig();
|
|
40
43
|
server.stop();
|
|
41
44
|
rmSync(home, { recursive: true, force: true });
|
|
42
45
|
});
|
|
@@ -57,15 +60,14 @@ function snapshot() {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
describe("observer rendering", () => {
|
|
60
|
-
test("shows the observer badge and
|
|
63
|
+
test("shows the observer badge and the bottom menu", async () => {
|
|
61
64
|
// Arrange
|
|
62
65
|
const setup = await renderObserver();
|
|
63
66
|
|
|
64
67
|
// Assert
|
|
65
68
|
const frame = setup.captureCharFrame();
|
|
66
69
|
expect(frame).toContain("· observer");
|
|
67
|
-
expect(frame).toContain("
|
|
68
|
-
expect(frame).not.toContain("⏎ submit");
|
|
70
|
+
expect(frame).toContain("menu");
|
|
69
71
|
});
|
|
70
72
|
});
|
|
71
73
|
|
|
@@ -85,8 +87,8 @@ describe("observer verbs are blocked", () => {
|
|
|
85
87
|
await press(setup, key);
|
|
86
88
|
const frame = await waitForText(setup, "observer - read-only");
|
|
87
89
|
// no overlay opened: compose/submit bars never appear
|
|
88
|
-
expect(frame).not.toContain(
|
|
89
|
-
expect(frame).not.toContain(
|
|
90
|
+
expect(frame).not.toContain('comment on "');
|
|
91
|
+
expect(frame).not.toContain('suggest replacement for "');
|
|
90
92
|
expect(frame).not.toContain("verdict ←/→");
|
|
91
93
|
expect(snapshot()).toEqual(before);
|
|
92
94
|
});
|
|
@@ -106,7 +108,7 @@ describe("observer verbs are blocked", () => {
|
|
|
106
108
|
// Assert
|
|
107
109
|
const frame = setup.captureCharFrame();
|
|
108
110
|
expect(frame).toContain("observer - read-only");
|
|
109
|
-
expect(frame).not.toContain(
|
|
111
|
+
expect(frame).not.toContain('comment on "');
|
|
110
112
|
expect(snapshot().annotations).toBe(0);
|
|
111
113
|
});
|
|
112
114
|
});
|
|
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import { waitForText } from "./test-support";
|
|
12
|
+
import { isolateUserConfig, waitForText } from "./test-support";
|
|
13
13
|
|
|
14
14
|
const PLAN = `# Render Plan
|
|
15
15
|
|
|
@@ -30,11 +30,13 @@ Deploys block below the threshold.
|
|
|
30
30
|
`;
|
|
31
31
|
|
|
32
32
|
let home: string;
|
|
33
|
+
let restoreUserConfig: () => void;
|
|
33
34
|
let server: DaemonServer;
|
|
34
35
|
let session: ReviewSession;
|
|
35
36
|
|
|
36
37
|
beforeEach(() => {
|
|
37
38
|
home = mkdtempSync(join(tmpdir(), "cueloop-render-"));
|
|
39
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
38
40
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
39
41
|
server.start();
|
|
40
42
|
session = server.core.sessionCreate({
|
|
@@ -43,6 +45,7 @@ beforeEach(() => {
|
|
|
43
45
|
});
|
|
44
46
|
});
|
|
45
47
|
afterEach(() => {
|
|
48
|
+
restoreUserConfig();
|
|
46
49
|
server.stop();
|
|
47
50
|
rmSync(home, { recursive: true, force: true });
|
|
48
51
|
});
|
|
@@ -77,7 +80,8 @@ describe("code blocks", () => {
|
|
|
77
80
|
describe("block spacing", () => {
|
|
78
81
|
// the review panel's full-height divider paints a `│` on every row, so read
|
|
79
82
|
// the plan column (everything left of the divider) before asserting spacing
|
|
80
|
-
|
|
83
|
+
// the plan body is framed: content sits between the left border and the next │
|
|
84
|
+
const planColumn = (line: string): string => line.split("│")[1]?.trimEnd() ?? "";
|
|
81
85
|
|
|
82
86
|
test("a code block never glues to the list item above it", async () => {
|
|
83
87
|
// Arrange
|
|
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
|
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
12
|
import { loadConfig } from "./config";
|
|
13
|
-
import { press, waitForText, waitForTextGone } from "./test-support";
|
|
13
|
+
import { isolateUserConfig, press, settle, waitForText, waitForTextGone } from "./test-support";
|
|
14
14
|
|
|
15
15
|
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
16
|
|
|
@@ -18,11 +18,12 @@ let home: string;
|
|
|
18
18
|
let server: DaemonServer;
|
|
19
19
|
let session: ReviewSession;
|
|
20
20
|
let configPath: string;
|
|
21
|
+
let restoreUserConfig: () => void;
|
|
21
22
|
|
|
22
23
|
beforeEach(() => {
|
|
23
24
|
home = mkdtempSync(join(tmpdir(), "cueloop-panel-"));
|
|
24
25
|
configPath = join(home, "config.toml");
|
|
25
|
-
|
|
26
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
26
27
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
27
28
|
server.start();
|
|
28
29
|
session = server.core.sessionCreate({
|
|
@@ -31,7 +32,7 @@ beforeEach(() => {
|
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
afterEach(() => {
|
|
34
|
-
|
|
35
|
+
restoreUserConfig();
|
|
35
36
|
server.stop();
|
|
36
37
|
rmSync(home, { recursive: true, force: true });
|
|
37
38
|
});
|
|
@@ -46,26 +47,30 @@ describe("review panel", () => {
|
|
|
46
47
|
test("b cycles expanded -> compact -> hidden -> expanded", async () => {
|
|
47
48
|
// Arrange
|
|
48
49
|
const setup = await renderApp();
|
|
50
|
+
// the plan frame widens as the rail shrinks: expanded < compact < hidden.
|
|
51
|
+
// its width is the column of its top-right corner (the first ╮ on the row).
|
|
52
|
+
const planWidth = () => setup.captureCharFrame().split("\n").find((line) => line.includes("╮"))?.indexOf("╮") ?? 0;
|
|
49
53
|
|
|
50
|
-
// Assert
|
|
51
|
-
expect(setup.captureCharFrame()).toContain("Submit review");
|
|
54
|
+
// Assert: expanded shows the submit button and leaves room for the rail
|
|
55
|
+
expect(setup.captureCharFrame()).toContain("Submit review");
|
|
56
|
+
const expandedWidth = planWidth();
|
|
52
57
|
|
|
53
58
|
// Act
|
|
54
59
|
await press(setup, "b");
|
|
55
60
|
|
|
56
|
-
// Assert
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
expect(
|
|
60
|
-
expect(frame).not.toContain("Submit review");
|
|
61
|
+
// Assert: compact drops the submit button and widens the plan (rail is a strip)
|
|
62
|
+
await waitForTextGone(setup, "Submit review");
|
|
63
|
+
const compactWidth = planWidth();
|
|
64
|
+
expect(compactWidth).toBeGreaterThan(expandedWidth);
|
|
61
65
|
|
|
62
66
|
// Act
|
|
63
67
|
await press(setup, "b");
|
|
68
|
+
await settle(setup);
|
|
64
69
|
|
|
65
|
-
// Assert
|
|
66
|
-
|
|
67
|
-
expect(
|
|
68
|
-
expect(
|
|
70
|
+
// Assert: hidden spans the plan full width, no rail column
|
|
71
|
+
const hiddenWidth = planWidth();
|
|
72
|
+
expect(hiddenWidth).toBeGreaterThan(compactWidth);
|
|
73
|
+
expect(hiddenWidth).toBeGreaterThan(115);
|
|
69
74
|
|
|
70
75
|
// Act
|
|
71
76
|
await press(setup, "b");
|
|
@@ -9,7 +9,7 @@ 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 { press, settle, waitForText, waitForTextGone } from "./test-support";
|
|
12
|
+
import { isolateUserConfig, press, settle, waitForText, waitForTextGone } from "./test-support";
|
|
13
13
|
|
|
14
14
|
const PLAN = `# Migration Plan
|
|
15
15
|
|
|
@@ -24,11 +24,13 @@ The daemon persists sessions to disk atomically.
|
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
26
|
let home: string;
|
|
27
|
+
let restoreUserConfig: () => void;
|
|
27
28
|
let server: DaemonServer;
|
|
28
29
|
let session: ReviewSession;
|
|
29
30
|
|
|
30
31
|
beforeEach(() => {
|
|
31
32
|
home = mkdtempSync(join(tmpdir(), "cueloop-confirm-"));
|
|
33
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
32
34
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
33
35
|
server.start();
|
|
34
36
|
session = server.core.sessionCreate({
|
|
@@ -37,6 +39,7 @@ beforeEach(() => {
|
|
|
37
39
|
});
|
|
38
40
|
});
|
|
39
41
|
afterEach(() => {
|
|
42
|
+
restoreUserConfig();
|
|
40
43
|
server.stop();
|
|
41
44
|
rmSync(home, { recursive: true, force: true });
|
|
42
45
|
});
|
|
@@ -71,7 +74,7 @@ describe("rail submit confirm", () => {
|
|
|
71
74
|
const setup = await renderApp();
|
|
72
75
|
|
|
73
76
|
// Assert
|
|
74
|
-
expect(setup.captureCharFrame()).toContain("Submit review
|
|
77
|
+
expect(setup.captureCharFrame()).toContain("Submit review");
|
|
75
78
|
|
|
76
79
|
// Act
|
|
77
80
|
await press(setup, "enter");
|
|
@@ -85,10 +88,6 @@ describe("rail submit confirm", () => {
|
|
|
85
88
|
expect(frame).toContain("[Approve]"); // nothing pending: approve default
|
|
86
89
|
expect(frame).toContain(" Submit ");
|
|
87
90
|
expect(frame).toContain(" Cancel ");
|
|
88
|
-
// key hints live in the status line, not on the buttons
|
|
89
|
-
expect(frame).toContain("verdict ←/→ · ⏎ submit · esc cancel");
|
|
90
|
-
// the bottom bar stayed a one-line hint: no detached verdict bar
|
|
91
|
-
expect(frame).not.toContain("Submit review (0) on ⏎");
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
test("left/right cycles the verdict selector in the card", async () => {
|
|
@@ -133,7 +132,7 @@ describe("rail submit confirm", () => {
|
|
|
133
132
|
// a bare ESC settles after the parser's escape-sequence window
|
|
134
133
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
135
134
|
expect(frame).not.toContain("0 annotations");
|
|
136
|
-
expect(frame).toContain("Submit review
|
|
135
|
+
expect(frame).toContain("Submit review");
|
|
137
136
|
});
|
|
138
137
|
|
|
139
138
|
test("enter in the card resolves the session through the controller", async () => {
|
package/src/App.test.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import { press, typeText as type, waitForState, waitForText } from "./test-support";
|
|
12
|
+
import { isolateUserConfig, press, typeText as type, waitForState, waitForText } from "./test-support";
|
|
13
13
|
|
|
14
14
|
const PLAN = `# Migration Plan
|
|
15
15
|
|
|
@@ -24,11 +24,13 @@ The daemon persists sessions to disk atomically.
|
|
|
24
24
|
`;
|
|
25
25
|
|
|
26
26
|
let home: string;
|
|
27
|
+
let restoreUserConfig: () => void;
|
|
27
28
|
let server: DaemonServer;
|
|
28
29
|
let session: ReviewSession;
|
|
29
30
|
|
|
30
31
|
beforeEach(() => {
|
|
31
32
|
home = mkdtempSync(join(tmpdir(), "cueloop-app-"));
|
|
33
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
32
34
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
33
35
|
server.start();
|
|
34
36
|
session = server.core.sessionCreate({
|
|
@@ -37,6 +39,7 @@ beforeEach(() => {
|
|
|
37
39
|
});
|
|
38
40
|
});
|
|
39
41
|
afterEach(() => {
|
|
42
|
+
restoreUserConfig();
|
|
40
43
|
server.stop();
|
|
41
44
|
rmSync(home, { recursive: true, force: true });
|
|
42
45
|
});
|
|
@@ -62,8 +65,8 @@ describe("plan rendering", () => {
|
|
|
62
65
|
expect(frame).toContain("Context");
|
|
63
66
|
expect(frame).toContain("persists sessions to disk atomically");
|
|
64
67
|
expect(frame).toContain("- move the store");
|
|
65
|
-
expect(frame).toContain("Review
|
|
66
|
-
expect(frame).toContain("Submit review
|
|
68
|
+
expect(frame).toContain("Review");
|
|
69
|
+
expect(frame).toContain("Submit review");
|
|
67
70
|
});
|
|
68
71
|
});
|
|
69
72
|
|
|
@@ -176,7 +179,7 @@ describe("submit", () => {
|
|
|
176
179
|
await press(setup, "c");
|
|
177
180
|
await type(setup, "Needs a phase list.");
|
|
178
181
|
await press(setup, "enter");
|
|
179
|
-
await waitForText(setup, "
|
|
182
|
+
await waitForText(setup, "Needs a phase list");
|
|
180
183
|
|
|
181
184
|
// Act
|
|
182
185
|
await press(setup, "enter"); // open submit
|
|
@@ -231,6 +234,6 @@ describe("inbox", () => {
|
|
|
231
234
|
await press(setup, "enter");
|
|
232
235
|
|
|
233
236
|
// Assert
|
|
234
|
-
await waitForText(setup, "Submit review
|
|
237
|
+
await waitForText(setup, "Submit review");
|
|
235
238
|
});
|
|
236
239
|
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** The share toast is non-modal: while it is up, escape still cancels an open overlay. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, mock, 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 { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
+
import type { ReviewSession } from "@cueloop/schema";
|
|
11
|
+
|
|
12
|
+
// the controller reaches the gateway through "./share"; stub it so `S` yields a toast
|
|
13
|
+
const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
|
|
14
|
+
mock.module("./share", () => ({
|
|
15
|
+
publishShare,
|
|
16
|
+
pullShare: mock(async () => null),
|
|
17
|
+
pushShare: mock(async () => {}),
|
|
18
|
+
shareIdFromLine: (line: string) => line.match(/^ssh (\S+)@/)?.[1],
|
|
19
|
+
collaboratorAnnotations: () => [],
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { App } = await import("./App");
|
|
23
|
+
const { isolateUserConfig, press, waitForText, waitForTextGone } = await import("./test-support");
|
|
24
|
+
|
|
25
|
+
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
26
|
+
|
|
27
|
+
let home: string;
|
|
28
|
+
let restoreUserConfig: () => void;
|
|
29
|
+
let server: DaemonServer;
|
|
30
|
+
let session: ReviewSession;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-toast-"));
|
|
34
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
|
+
server.start();
|
|
37
|
+
session = server.core.sessionCreate({
|
|
38
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Migration Plan", planPath: "plan.md" } },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
restoreUserConfig();
|
|
44
|
+
server.stop();
|
|
45
|
+
rmSync(home, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("share toast", () => {
|
|
49
|
+
test("escape cancels an open composer even while the toast is up", async () => {
|
|
50
|
+
// Arrange
|
|
51
|
+
const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 32 });
|
|
52
|
+
await waitForText(setup, "cueloop");
|
|
53
|
+
|
|
54
|
+
// Act: share raises the toast, then open a composer under it
|
|
55
|
+
await press(setup, "S");
|
|
56
|
+
await waitForText(setup, "share link copied");
|
|
57
|
+
await press(setup, "j");
|
|
58
|
+
await press(setup, "j");
|
|
59
|
+
await press(setup, "c");
|
|
60
|
+
await waitForText(setup, 'comment on "');
|
|
61
|
+
|
|
62
|
+
// Act: escape must reach the composer, not get eaten by the toast
|
|
63
|
+
await press(setup, "escape");
|
|
64
|
+
|
|
65
|
+
// Assert: the composer closed on the first escape
|
|
66
|
+
await waitForTextGone(setup, 'comment on "');
|
|
67
|
+
});
|
|
68
|
+
});
|