@cueloop/client 0.1.0-alpha.20 → 0.1.0-alpha.22

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.
Files changed (47) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +29 -5
  3. package/src/App.diff.test.tsx +20 -0
  4. package/src/App.export.test.tsx +12 -8
  5. package/src/App.plan-review.test.tsx +182 -7
  6. package/src/App.readonly.test.tsx +19 -5
  7. package/src/App.rendering.test.tsx +21 -2
  8. package/src/App.review-panel.test.tsx +120 -0
  9. package/src/App.submit-confirm.test.tsx +57 -6
  10. package/src/App.test.tsx +62 -5
  11. package/src/App.tsx +147 -185
  12. package/src/App.walk.test.tsx +63 -8
  13. package/src/clipboard.test.ts +23 -0
  14. package/src/clipboard.ts +26 -0
  15. package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
  16. package/src/components/AnnotationCard.tsx +66 -13
  17. package/src/components/ReviewPanel.stories.tsx +86 -0
  18. package/src/components/ReviewPanel.tsx +116 -0
  19. package/src/components/ReviewRail.tsx +30 -5
  20. package/src/components/__snapshots__/stories.test.tsx.snap +140 -0
  21. package/src/components/primitives/Card.tsx +2 -1
  22. package/src/components/primitives/Dialog.tsx +2 -1
  23. package/src/components/primitives/frame.test.ts +12 -0
  24. package/src/components/primitives/frame.ts +14 -0
  25. package/src/components/stories-app.tsx +2 -1
  26. package/src/components/stories.test.tsx +5 -0
  27. package/src/config.test.ts +113 -1
  28. package/src/config.ts +58 -22
  29. package/src/editor.test.ts +24 -0
  30. package/src/herdr.test.ts +4 -0
  31. package/src/index.ts +2 -0
  32. package/src/integrations.test.ts +12 -0
  33. package/src/intent-dispatch.test.ts +213 -0
  34. package/src/intent-dispatch.ts +270 -0
  35. package/src/key-bindings.test.ts +16 -5
  36. package/src/keymap.test.ts +141 -9
  37. package/src/keymap.ts +54 -7
  38. package/src/review-panel.test.ts +111 -0
  39. package/src/review-panel.ts +76 -0
  40. package/src/serve.test.ts +7 -0
  41. package/src/session-controller.ts +39 -4
  42. package/src/share.ts +47 -0
  43. package/src/view-diff.test.ts +11 -0
  44. package/src/view-diff.ts +6 -6
  45. package/src/view-plan.test.ts +81 -0
  46. package/src/view-plan.ts +3 -0
  47. package/src/walk.test.ts +6 -0
@@ -0,0 +1,120 @@
1
+ /** The review panel end to end: the b key cycles expanded -> compact -> hidden -> expanded on the live App, and the collapse mode persists to the user config so it survives a restart. Rendering details are locked by the ReviewPanel stories; this suite proves the App wiring and the persistence. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+ import { mkdtempSync, readFileSync, 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
+ import { App } from "./App";
12
+ import { loadConfig } from "./config";
13
+ import { press, 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
+
22
+ beforeEach(() => {
23
+ home = mkdtempSync(join(tmpdir(), "cueloop-panel-"));
24
+ configPath = join(home, "config.toml");
25
+ process.env.CUELOOP_CONFIG = configPath;
26
+ server = new DaemonServer({ home, idleExitMs: 0 });
27
+ server.start();
28
+ session = server.core.sessionCreate({
29
+ workspace: { repoRoot: "/repo", branch: "main" },
30
+ artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
31
+ });
32
+ });
33
+ afterEach(() => {
34
+ delete process.env.CUELOOP_CONFIG;
35
+ server.stop();
36
+ rmSync(home, { recursive: true, force: true });
37
+ });
38
+
39
+ async function renderApp() {
40
+ const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 30 });
41
+ await waitForText(setup, "Review");
42
+ return setup;
43
+ }
44
+
45
+ describe("review panel", () => {
46
+ test("b cycles expanded -> compact -> hidden -> expanded", async () => {
47
+ // Arrange
48
+ const setup = await renderApp();
49
+
50
+ // Assert
51
+ expect(setup.captureCharFrame()).toContain("Submit review"); // expanded rail
52
+
53
+ // Act
54
+ await press(setup, "b");
55
+
56
+ // Assert
57
+ let frame = setup.captureCharFrame();
58
+ expect(frame).toContain("«"); // compact strip shows the expand chevron
59
+ expect(frame).not.toContain("Submit review");
60
+
61
+ // Act
62
+ await press(setup, "b");
63
+
64
+ // Assert
65
+ frame = setup.captureCharFrame();
66
+ expect(frame).not.toContain("«"); // hidden: no strip, no divider
67
+ expect(frame).not.toContain("│");
68
+
69
+ // Act
70
+ await press(setup, "b");
71
+
72
+ // Assert
73
+ expect(setup.captureCharFrame()).toContain("Submit review"); // back to expanded
74
+ });
75
+
76
+ test("the collapse mode persists to the user config", async () => {
77
+ // Arrange
78
+ const setup = await renderApp();
79
+
80
+ // Act
81
+ await press(setup, "b"); // -> compact
82
+
83
+ // Assert
84
+ expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("compact");
85
+ });
86
+
87
+ test("] widens the rail and persists the new width", async () => {
88
+ // Arrange
89
+ const setup = await renderApp();
90
+ const startWidth = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
91
+
92
+ // Act
93
+ await press(setup, "]");
94
+
95
+ // Assert
96
+ const widened = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
97
+ expect(widened).toBeGreaterThan(startWidth);
98
+ });
99
+
100
+ test("submitting from a hidden panel force-opens the rail so the confirm card is reachable", async () => {
101
+ // Arrange
102
+ const setup = await renderApp();
103
+ await press(setup, "b"); // -> compact
104
+ await press(setup, "b"); // -> hidden
105
+
106
+ // Assert
107
+ expect(setup.captureCharFrame()).not.toContain("Submit review"); // rail is gone
108
+
109
+ // Act
110
+ await press(setup, "enter"); // submit while hidden must not trap in an invisible modal
111
+
112
+ // Assert
113
+ const frame = setup.captureCharFrame();
114
+ expect(frame).toContain("submit review"); // the confirm card is visible again
115
+ expect(frame).toContain("[Approve]");
116
+
117
+ // the reveal is live-only: the saved panel preference stays as the user left it
118
+ expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("hidden");
119
+ });
120
+ });
@@ -1,9 +1,4 @@
1
- /**
2
- * The rail submit confirm (tier 2): pressing submit expands the rail's
3
- * Submit button into a bordered confirm card - counts, verdict selector,
4
- * summary input, word-buttons. Char-frame assertions over the real App
5
- * and an in-process daemon, like App.test.tsx.
6
- */
1
+ /** The rail submit confirm (tier 2): pressing submit expands the rail's Submit button into a bordered confirm card - counts, verdict selector, summary input, word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
7
2
 
8
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -72,9 +67,16 @@ function seedAnnotations(count: number): void {
72
67
 
73
68
  describe("rail submit confirm", () => {
74
69
  test("submit expands the Submit button into the confirm card", async () => {
70
+ // Arrange
75
71
  const setup = await renderApp();
72
+
73
+ // Assert
76
74
  expect(setup.captureCharFrame()).toContain("Submit review (0)");
75
+
76
+ // Act
77
77
  await press(setup, "enter");
78
+
79
+ // Assert
78
80
  const frame = setup.captureCharFrame();
79
81
  // the card: title, honest counts, verdict selector, word-buttons
80
82
  expect(frame).toContain("submit review");
@@ -89,22 +91,44 @@ describe("rail submit confirm", () => {
89
91
  });
90
92
 
91
93
  test("left/right cycles the verdict selector in the card", async () => {
94
+ // Arrange
92
95
  const setup = await renderApp();
93
96
  await press(setup, "enter");
97
+
98
+ // Assert
94
99
  expect(setup.captureCharFrame()).toContain("[Approve]");
100
+
101
+ // Act
95
102
  await press(setup, "right");
103
+
104
+ // Assert
96
105
  expect(setup.captureCharFrame()).toContain("[Changes]");
106
+
107
+ // Act
97
108
  await press(setup, "right");
109
+
110
+ // Assert
98
111
  expect(setup.captureCharFrame()).toContain("[Comment]");
112
+
113
+ // Act
99
114
  await press(setup, "left");
115
+
116
+ // Assert
100
117
  expect(setup.captureCharFrame()).toContain("[Changes]");
101
118
  });
102
119
 
103
120
  test("esc cancels the card and restores the plain Submit button", async () => {
121
+ // Arrange
104
122
  const setup = await renderApp();
105
123
  await press(setup, "enter");
124
+
125
+ // Assert
106
126
  expect(setup.captureCharFrame()).toContain("[Approve]");
127
+
128
+ // Act
107
129
  await press(setup, "escape");
130
+
131
+ // Assert
108
132
  // a bare ESC settles after the parser's escape-sequence window
109
133
  const frame = await waitForTextGone(setup, "[Approve]");
110
134
  expect(frame).not.toContain("0 annotations");
@@ -112,14 +136,23 @@ describe("rail submit confirm", () => {
112
136
  });
113
137
 
114
138
  test("enter in the card resolves the session through the controller", async () => {
139
+ // Arrange
115
140
  seedAnnotations(1);
116
141
  const setup = await renderApp();
142
+
143
+ // Act
117
144
  await press(setup, "enter");
145
+
146
+ // Assert
118
147
  const frame = setup.captureCharFrame();
119
148
  expect(frame).toContain("1 annotations · 0 blocking");
120
149
  expect(frame).toContain("[Changes]"); // pending items: request changes default
150
+
151
+ // Act
121
152
  await setup.mockInput.typeText("Tighten the steps.");
122
153
  await press(setup, "enter");
154
+
155
+ // Assert
123
156
  // the completion flow after submit is unchanged
124
157
  await waitForText(setup, "feedback sent");
125
158
  const stored = server.core.sessionGet(session.id);
@@ -128,23 +161,36 @@ describe("rail submit confirm", () => {
128
161
  });
129
162
 
130
163
  test("the annotation stack stays scrollable while the card is open", async () => {
164
+ // Arrange
131
165
  seedAnnotations(12);
132
166
  const setup = await renderApp();
167
+
168
+ // Act
133
169
  // walk annotation focus to the last card: the rail scrolls it into view
134
170
  for (let index = 0; index < 12; index++) await press(setup, "n");
171
+
172
+ // Assert
135
173
  const scrolled = setup.captureCharFrame();
136
174
  expect(scrolled).toContain("note 12");
137
175
  expect(scrolled).not.toContain("note 01");
176
+
177
+ // Act
138
178
  await press(setup, "enter");
179
+
180
+ // Assert
139
181
  const opened = setup.captureCharFrame();
140
182
  // the card is pinned outside the scrollbox; the stack above stays
141
183
  // scrolled away from the top when the card takes its space
142
184
  expect(opened).toContain("12 annotations · 0 blocking");
143
185
  expect(opened).not.toContain("note 01");
186
+
187
+ // Act
144
188
  // the stack still scrolls with the card open: wheel over the rail
145
189
  // brings the last card back into view while the confirm card stays put
146
190
  for (let turn = 0; turn < 12; turn++) await setup.mockMouse.scroll(100, 10, "down");
147
191
  await settle(setup);
192
+
193
+ // Assert
148
194
  const scrolledWithCard = setup.captureCharFrame();
149
195
  expect(scrolledWithCard).toContain("note 12");
150
196
  expect(scrolledWithCard).toContain("12 annotations · 0 blocking");
@@ -152,9 +198,14 @@ describe("rail submit confirm", () => {
152
198
  });
153
199
 
154
200
  test("read-only observers never see the confirm card", async () => {
201
+ // Arrange
155
202
  seedAnnotations(1);
156
203
  const setup = await renderApp({ readOnly: true });
204
+
205
+ // Act
157
206
  await press(setup, "enter");
207
+
208
+ // Assert
158
209
  const frame = setup.captureCharFrame();
159
210
  expect(frame).toContain("observer - read-only");
160
211
  expect(frame).not.toContain("1 annotations");
package/src/App.test.tsx CHANGED
@@ -1,8 +1,4 @@
1
- /**
2
- * Virtual-terminal component tests (tier 2): the real App over a real
3
- * in-process daemon in a temp home. Char-frame assertions + mock keys -
4
- * the whole review loop drivable without a terminal.
5
- */
1
+ /** Virtual-terminal component tests (tier 2): the real App over a real in-process daemon in a temp home. Char-frame assertions + mock keys - the whole review loop drivable without a terminal. */
6
2
 
7
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
8
4
  import { mkdtempSync, rmSync } from "node:fs";
@@ -57,7 +53,10 @@ async function renderApp(sessionId?: string) {
57
53
 
58
54
  describe("plan rendering", () => {
59
55
  test("renders the plan with headings, list markers, and the rail", async () => {
56
+ // Arrange
60
57
  const setup = await renderApp();
58
+
59
+ // Assert
61
60
  const frame = setup.captureCharFrame();
62
61
  expect(frame).toContain("Migration Plan");
63
62
  expect(frame).toContain("Context");
@@ -70,24 +69,38 @@ describe("plan rendering", () => {
70
69
 
71
70
  describe("keyboard grammar", () => {
72
71
  test("j/k moves the cursor glyph between blocks", async () => {
72
+ // Arrange
73
73
  const setup = await renderApp();
74
+
75
+ // Act
74
76
  await press(setup, "j");
75
77
  await press(setup, "j");
76
78
  await setup.renderOnce();
79
+
80
+ // Assert
77
81
  const lines = setup.captureCharFrame().split("\n");
78
82
  const cursorLine = lines.find((line) => line.includes("▎"))!;
79
83
  expect(cursorLine).toContain("persists sessions");
80
84
  });
81
85
 
82
86
  test("comment flow: c types a body, ⏎ saves to the daemon", async () => {
87
+ // Arrange
83
88
  const setup = await renderApp();
89
+
90
+ // Act
84
91
  await press(setup, "j");
85
92
  await press(setup, "j");
86
93
  await press(setup, "c");
87
94
  await setup.renderOnce();
95
+
96
+ // Assert
88
97
  expect(setup.captureCharFrame()).toContain('comment on "The daemon');
98
+
99
+ // Act
89
100
  await type(setup, "Define atomically.");
90
101
  await press(setup, "enter");
102
+
103
+ // Assert
91
104
  await waitForText(setup, "COMMENT");
92
105
  const stored = server.core.sessionGet(session.id);
93
106
  expect(stored.annotations.length).toBe(1);
@@ -95,7 +108,10 @@ describe("keyboard grammar", () => {
95
108
  });
96
109
 
97
110
  test("span mode: v + l selects words, c anchors the exact span", async () => {
111
+ // Arrange
98
112
  const setup = await renderApp();
113
+
114
+ // Act
99
115
  await press(setup, "j");
100
116
  await press(setup, "j");
101
117
  await press(setup, "v");
@@ -103,19 +119,30 @@ describe("keyboard grammar", () => {
103
119
  await press(setup, "c");
104
120
  await type(setup, "Which daemon?");
105
121
  await press(setup, "enter");
122
+
123
+ // Assert
106
124
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
107
125
  const stored = server.core.sessionGet(session.id);
108
126
  expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
109
127
  });
110
128
 
111
129
  test("x cuts a block into the working copy; x restores it", async () => {
130
+ // Arrange
112
131
  const setup = await renderApp();
113
132
  // move to "- move the store" (h1, h2, p, h2 = 4 steps in)
114
133
  for (let i = 0; i < 4; i++) await press(setup, "j");
134
+
135
+ // Act
115
136
  await press(setup, "x");
137
+
138
+ // Assert
116
139
  await waitForText(setup, "[cut]");
117
140
  expect(server.core.sessionGet(session.id).workingCopy).not.toContain("move the store");
141
+
142
+ // Act
118
143
  await press(setup, "x");
144
+
145
+ // Assert
119
146
  await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
120
147
  });
121
148
 
@@ -125,8 +152,13 @@ describe("keyboard grammar", () => {
125
152
  Bun.spawnSync(["chmod", "+x", script]);
126
153
  process.env.CUELOOP_EDITOR = script;
127
154
  try {
155
+ // Arrange
128
156
  const setup = await renderApp();
157
+
158
+ // Act
129
159
  await press(setup, "e");
160
+
161
+ // Assert
130
162
  await waitForText(setup, "[edited]");
131
163
  expect(server.core.sessionGet(session.id).workingCopy).toContain("very atomically");
132
164
  } finally {
@@ -137,6 +169,7 @@ describe("keyboard grammar", () => {
137
169
 
138
170
  describe("submit", () => {
139
171
  test("⏎ opens the rail confirm card; verdict + summary resolve the session", async () => {
172
+ // Arrange
140
173
  const setup = await renderApp();
141
174
  await press(setup, "j");
142
175
  await press(setup, "j");
@@ -144,10 +177,18 @@ describe("submit", () => {
144
177
  await type(setup, "Needs a phase list.");
145
178
  await press(setup, "enter");
146
179
  await waitForText(setup, "Review (1)");
180
+
181
+ // Act
147
182
  await press(setup, "enter"); // open submit
183
+
184
+ // Assert
148
185
  expect(setup.captureCharFrame()).toContain("[Changes]");
186
+
187
+ // Act
149
188
  await type(setup, "Expand the steps.");
150
189
  await press(setup, "enter");
190
+
191
+ // Assert
151
192
  await waitForText(setup, "feedback sent");
152
193
  const stored = server.core.sessionGet(session.id);
153
194
  expect(stored.status).toBe("resolved");
@@ -158,10 +199,19 @@ describe("submit", () => {
158
199
  });
159
200
 
160
201
  test("approve via ←/→ verdict cycling", async () => {
202
+ // Arrange
161
203
  const setup = await renderApp();
204
+
205
+ // Act
162
206
  await press(setup, "enter");
207
+
208
+ // Assert
163
209
  expect(setup.captureCharFrame()).toContain("[Approve]"); // no pending items → approve default
210
+
211
+ // Act
164
212
  await press(setup, "enter");
213
+
214
+ // Assert
165
215
  await waitForState(setup, () => server.core.sessionGet(session.id).verdict !== undefined);
166
216
  expect(server.core.sessionGet(session.id).verdict!.kind).toBe("approve");
167
217
  });
@@ -169,11 +219,18 @@ describe("submit", () => {
169
219
 
170
220
  describe("inbox", () => {
171
221
  test("inbox mode renders and opens a session", async () => {
222
+ // Arrange
172
223
  const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
173
224
  await waitForText(setup, "inbox");
225
+
226
+ // Assert
174
227
  expect(setup.captureCharFrame()).toContain("inbox (1 pending)");
175
228
  expect(setup.captureCharFrame()).toContain("Migration Plan");
229
+
230
+ // Act
176
231
  await press(setup, "enter");
232
+
233
+ // Assert
177
234
  await waitForText(setup, "Submit review (0)");
178
235
  });
179
236
  });