@cueloop/client 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +17 -11
- package/src/App.autoclose.test.tsx +133 -0
- package/src/App.diff.test.tsx +27 -20
- package/src/App.export.test.tsx +19 -28
- package/src/App.plan-review.test.tsx +507 -0
- package/src/App.readonly.test.tsx +26 -27
- package/src/App.rendering.test.tsx +119 -0
- package/src/App.review-panel.test.tsx +123 -0
- package/src/App.submit-confirm.test.tsx +216 -0
- package/src/App.test.tsx +83 -64
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +265 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +32 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +26 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +75 -0
- package/src/components/AnnotationCard.tsx +208 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +38 -0
- package/src/components/CodeBlock.stories.tsx +25 -0
- package/src/components/CodeBlock.tsx +68 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +49 -0
- package/src/components/ComposeBar.stories.tsx +18 -0
- package/src/components/ComposeBar.tsx +54 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +117 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +56 -0
- package/src/components/DiffSheet.stories.tsx +44 -0
- package/src/components/DiffSheet.tsx +243 -0
- package/src/components/InboxList.stories.tsx +18 -0
- package/src/components/InboxList.tsx +55 -0
- package/src/components/PlanSheet.stories.tsx +81 -0
- package/src/components/PlanSheet.tsx +329 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +62 -0
- package/src/components/ReviewPanel.stories.tsx +87 -0
- package/src/components/ReviewPanel.tsx +116 -0
- package/src/components/ReviewRail.stories.tsx +107 -0
- package/src/components/ReviewRail.tsx +180 -0
- package/src/components/SettingsDialog.stories.tsx +63 -0
- package/src/components/SettingsDialog.tsx +132 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +92 -0
- package/src/components/WalkWizard.stories.tsx +89 -0
- package/src/components/WalkWizard.tsx +146 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1519 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +44 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +54 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +97 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/stories-app.tsx +95 -0
- package/src/components/stories.test.tsx +60 -0
- package/src/components/story-fixtures.ts +90 -0
- package/src/components/story.ts +66 -0
- package/src/components/syntax-highlight.ts +57 -0
- package/src/components/theme-context.stories.tsx +41 -0
- package/src/components/theme-context.tsx +30 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +196 -30
- package/src/config.ts +128 -16
- package/src/editor.test.ts +134 -0
- package/src/editor.ts +101 -13
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +21 -0
- package/src/index.ts +2 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +28 -0
- package/src/intent-dispatch.test.ts +308 -0
- package/src/intent-dispatch.ts +295 -0
- package/src/key-bindings.test.ts +142 -0
- package/src/key-bindings.ts +363 -0
- package/src/keymap.test.ts +477 -0
- package/src/keymap.ts +272 -0
- package/src/review-panel.test.ts +111 -0
- package/src/review-panel.ts +76 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +13 -6
- package/src/serve.ts +7 -7
- package/src/session-controller.share.test.ts +248 -0
- package/src/session-controller.ts +607 -0
- package/src/share.ts +80 -0
- package/src/test-support.ts +87 -0
- package/src/theme.ts +26 -3
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +22 -22
- package/src/view-plan.test.ts +260 -0
- package/src/view-plan.ts +365 -0
- package/src/walk.test.ts +116 -0
- package/src/walk.ts +65 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
package/package.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.31",
|
|
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.
|
|
14
|
-
"@opentui/core": "^0.
|
|
15
|
-
"@opentui/
|
|
10
|
+
"@cueloop/daemon": "0.1.0-alpha.31",
|
|
11
|
+
"@cueloop/extension-api": "0.1.0-alpha.31",
|
|
12
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.31",
|
|
13
|
+
"@cueloop/schema": "0.1.0-alpha.31",
|
|
14
|
+
"@opentui/core": "^0.5.2",
|
|
15
|
+
"@opentui/keymap": "0.5.2",
|
|
16
|
+
"@opentui/react": "^0.5.2",
|
|
17
|
+
"@opentui/ssh": "0.5.2",
|
|
16
18
|
"@pierre/diffs": "^1.3.5",
|
|
17
|
-
"react": "^19.2.0"
|
|
18
|
-
"@opentui/ssh": "0.4.5"
|
|
19
|
+
"react": "^19.2.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@types/react": "^19.2.0",
|
|
@@ -36,5 +37,10 @@
|
|
|
36
37
|
"type": "git",
|
|
37
38
|
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
38
39
|
"directory": "packages/client"
|
|
39
|
-
}
|
|
40
|
-
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
44
|
+
},
|
|
45
|
+
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail"
|
|
46
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** Post-submit completion overlay: prompt, opt-in persistence, countdown, dismissal. Countdown seconds tick on an injected ManualClock, so tests advance time instead of waiting it out. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } 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 { ManualClock } from "@opentui/core/testing";
|
|
10
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
11
|
+
import type { ReviewSession } from "@cueloop/schema";
|
|
12
|
+
import { App } from "./App";
|
|
13
|
+
import { press, settle, waitForText } from "./test-support";
|
|
14
|
+
|
|
15
|
+
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
|
+
|
|
17
|
+
let home: string;
|
|
18
|
+
let server: DaemonServer;
|
|
19
|
+
let session: ReviewSession;
|
|
20
|
+
let configPath: string;
|
|
21
|
+
let clock: ManualClock;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
|
|
25
|
+
configPath = join(home, "config.toml");
|
|
26
|
+
process.env.CUELOOP_CONFIG = configPath;
|
|
27
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
28
|
+
server.start();
|
|
29
|
+
session = server.core.sessionCreate({
|
|
30
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
31
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
32
|
+
});
|
|
33
|
+
clock = new ManualClock();
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
delete process.env.CUELOOP_CONFIG;
|
|
37
|
+
server.stop();
|
|
38
|
+
rmSync(home, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function renderApp(onExit?: (code: number) => void) {
|
|
42
|
+
const setup = await testRender(<App home={home} sessionId={session.id} onExit={onExit} clock={clock} />, {
|
|
43
|
+
width: 120,
|
|
44
|
+
height: 30,
|
|
45
|
+
});
|
|
46
|
+
await waitForText(setup, "cueloop");
|
|
47
|
+
return setup;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
51
|
+
|
|
52
|
+
async function submitApprove(setup: Setup): Promise<void> {
|
|
53
|
+
await press(setup, "enter"); // open submit (approve default: no pending items)
|
|
54
|
+
await press(setup, "enter"); // confirm
|
|
55
|
+
await waitForText(setup, "review approved");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("completion overlay", () => {
|
|
59
|
+
test("submit with auto-close off counts down from 5; ⏎ exits now", async () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
let exited = -1;
|
|
62
|
+
const setup = await renderApp((code) => (exited = code));
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
await submitApprove(setup);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
const frame = await waitForText(setup, "closing in 5s");
|
|
69
|
+
expect(frame).toContain("review approved");
|
|
70
|
+
expect(frame).toContain("close [return]");
|
|
71
|
+
expect(frame).toContain("return to plan [esc]");
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
await press(setup, "enter");
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect(exited).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a remembers the countdown as the persisted default", async () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const setup = await renderApp();
|
|
83
|
+
await submitApprove(setup);
|
|
84
|
+
await waitForText(setup, "closing in 5s");
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
await press(setup, "a");
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
expect(existsSync(configPath)).toBe(true);
|
|
91
|
+
expect(readFileSync(configPath, "utf8")).toContain("auto_close = 5");
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
// the countdown keeps running; each second is one manual-clock tick
|
|
95
|
+
clock.advance(1000);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
await waitForText(setup, "closing in 4s");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("esc dismisses to the resolved read-only view", async () => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const setup = await renderApp();
|
|
104
|
+
await submitApprove(setup);
|
|
105
|
+
await waitForText(setup, "closing in 5s");
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
// a bare ESC sits in the input parser's escape-sequence disambiguation
|
|
109
|
+
// window before it is delivered - the frame wait absorbs it
|
|
110
|
+
await press(setup, "escape");
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
const frame = await waitForText(setup, "resolved: approve");
|
|
114
|
+
expect(frame).not.toContain("closing in");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("configured auto_close counts down and exits without interaction", async () => {
|
|
118
|
+
// Arrange
|
|
119
|
+
writeFileSync(configPath, "[ui]\nauto_close = 1\n");
|
|
120
|
+
let exited = -1;
|
|
121
|
+
const setup = await renderApp((code) => (exited = code));
|
|
122
|
+
await submitApprove(setup);
|
|
123
|
+
await waitForText(setup, "closing in 1s");
|
|
124
|
+
expect(exited).toBe(-1);
|
|
125
|
+
|
|
126
|
+
// Act
|
|
127
|
+
clock.advance(1000);
|
|
128
|
+
await settle(setup);
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
expect(exited).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -9,6 +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
13
|
|
|
13
14
|
const PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
14
15
|
index 111..222 100644
|
|
@@ -41,25 +42,16 @@ afterEach(() => {
|
|
|
41
42
|
|
|
42
43
|
async function renderApp() {
|
|
43
44
|
const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 30 });
|
|
44
|
-
|
|
45
|
-
await Bun.sleep(25);
|
|
46
|
-
await setup.renderOnce();
|
|
47
|
-
}
|
|
48
|
-
await setup.renderOnce();
|
|
45
|
+
await waitForText(setup, "cueloop");
|
|
49
46
|
return setup;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
53
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
54
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
55
|
-
else await setup.mockInput.typeText(k);
|
|
56
|
-
await Bun.sleep(15);
|
|
57
|
-
await setup.renderOnce();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
49
|
describe("diff review", () => {
|
|
61
50
|
test("renders file header, hunks, and signed lines", async () => {
|
|
51
|
+
// Arrange
|
|
62
52
|
const setup = await renderApp();
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
63
55
|
const frame = setup.captureCharFrame();
|
|
64
56
|
expect(frame).toContain("■ src/store.ts");
|
|
65
57
|
expect(frame).toContain("@@ -1,4 +1,4 @@");
|
|
@@ -68,32 +60,47 @@ describe("diff review", () => {
|
|
|
68
60
|
});
|
|
69
61
|
|
|
70
62
|
test("line comment: quote-anchored, lands in feedback with the code line", async () => {
|
|
63
|
+
// Arrange
|
|
71
64
|
const setup = await renderApp();
|
|
65
|
+
|
|
66
|
+
// Act
|
|
72
67
|
// rows: file(0), hunk(1), ctx(2), del(3), add(4)
|
|
73
68
|
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
74
69
|
await press(setup, "c");
|
|
75
|
-
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
await waitForText(setup, "COMMENT ON");
|
|
73
|
+
|
|
74
|
+
// Act
|
|
76
75
|
await setup.mockInput.typeText("Map needs an eviction story.");
|
|
77
76
|
await press(setup, "enter");
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
// inline annotation card rendered under the line
|
|
80
|
+
await waitForText(setup, "◆ Map needs an eviction story.");
|
|
80
81
|
const stored = server.core.sessionGet(session.id);
|
|
81
82
|
expect(stored.annotations.length).toBe(1);
|
|
82
83
|
expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
|
|
83
|
-
// inline annotation card rendered under the line
|
|
84
|
-
expect(setup.captureCharFrame()).toContain("◆ Map needs an eviction story.");
|
|
85
84
|
|
|
85
|
+
// Act
|
|
86
86
|
await press(setup, "enter"); // submit
|
|
87
87
|
await press(setup, "enter"); // confirm request_changes
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
await waitForText(setup, "feedback sent");
|
|
89
91
|
const resolved = server.core.sessionGet(session.id);
|
|
90
92
|
expect(resolved.verdict!.feedback).toContain("new Map()");
|
|
91
93
|
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
test("plan verbs are guarded in diff sessions", async () => {
|
|
97
|
+
// Arrange
|
|
95
98
|
const setup = await renderApp();
|
|
99
|
+
|
|
100
|
+
// Act
|
|
96
101
|
await press(setup, "x");
|
|
97
|
-
|
|
102
|
+
|
|
103
|
+
// Assert
|
|
104
|
+
await waitForText(setup, "plan-only verb");
|
|
98
105
|
});
|
|
99
106
|
});
|
package/src/App.export.test.tsx
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier-2 wiring test for the notes-vault export: with [integrations.obsidian]
|
|
3
|
-
* configured to export on resolve, submitting a review writes the note into
|
|
4
|
-
* the vault and surfaces the path in the status line.
|
|
5
|
-
*/
|
|
1
|
+
/** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
|
|
6
2
|
|
|
7
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
4
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -13,6 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
13
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
14
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
15
11
|
import { App } from "./App";
|
|
12
|
+
import { press, waitForState, waitForText } from "./test-support";
|
|
16
13
|
|
|
17
14
|
const PLAN = `# Migration Plan
|
|
18
15
|
|
|
@@ -49,31 +46,25 @@ afterEach(() => {
|
|
|
49
46
|
|
|
50
47
|
describe("obsidian export on resolve", () => {
|
|
51
48
|
test("submitting a review writes the plan into the vault and shows the path", async () => {
|
|
49
|
+
// Arrange
|
|
52
50
|
const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 32 });
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
await
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
setup
|
|
63
|
-
|
|
64
|
-
//
|
|
51
|
+
await waitForText(setup, "cueloop");
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
await press(setup, "enter"); // open the submit overlay, keep the default verdict (approve)
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
await waitForText(setup, "verdict");
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
await press(setup, "enter"); // submit
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
// the export is an async round-trip after resolve; the status line lands
|
|
64
|
+
// on a later render than the file write
|
|
65
65
|
const dir = join(vault, "cueloop");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await Bun.sleep(25);
|
|
69
|
-
await setup.renderOnce();
|
|
70
|
-
}
|
|
71
|
-
// the status line lands on a later render than the file write
|
|
72
|
-
const statusDeadline = Date.now() + 30_000;
|
|
73
|
-
while (Date.now() < statusDeadline && !setup.captureCharFrame().includes("exported to")) {
|
|
74
|
-
await Bun.sleep(25);
|
|
75
|
-
await setup.renderOnce();
|
|
76
|
-
}
|
|
66
|
+
await waitForState(setup, () => existsSync(dir));
|
|
67
|
+
await waitForText(setup, "exported to");
|
|
77
68
|
|
|
78
69
|
const notes = readdirSync(dir);
|
|
79
70
|
expect(notes.length).toBe(1);
|