@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60
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 +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- package/src/walk.ts +4 -0
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.60",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.60",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.60",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.60",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.60",
|
|
33
33
|
"@opentui/core": "^0.5.8",
|
|
34
34
|
"@opentui/keymap": "0.5.8",
|
|
35
35
|
"@opentui/react": "^0.5.8",
|
|
@@ -47,7 +47,9 @@ async function renderApp(onExit?: (code: number) => void) {
|
|
|
47
47
|
height: 30,
|
|
48
48
|
},
|
|
49
49
|
);
|
|
50
|
+
|
|
50
51
|
await waitForText(setup, "cueloop");
|
|
52
|
+
|
|
51
53
|
return setup;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -70,6 +72,7 @@ describe("completion overlay", () => {
|
|
|
70
72
|
|
|
71
73
|
// Assert
|
|
72
74
|
const frame = await waitForText(setup, "closing in 5s");
|
|
75
|
+
|
|
73
76
|
expect(frame).toContain("review approved");
|
|
74
77
|
expect(frame).toContain("close [return]");
|
|
75
78
|
expect(frame).toContain("return to plan [esc]");
|
|
@@ -84,6 +87,7 @@ describe("completion overlay", () => {
|
|
|
84
87
|
test("a remembers the countdown as the persisted default", async () => {
|
|
85
88
|
// Arrange
|
|
86
89
|
const setup = await renderApp();
|
|
90
|
+
|
|
87
91
|
await submitApprove(setup);
|
|
88
92
|
await waitForText(setup, "closing in 5s");
|
|
89
93
|
|
|
@@ -105,6 +109,7 @@ describe("completion overlay", () => {
|
|
|
105
109
|
test("esc dismisses to the resolved read-only view", async () => {
|
|
106
110
|
// Arrange
|
|
107
111
|
const setup = await renderApp();
|
|
112
|
+
|
|
108
113
|
await submitApprove(setup);
|
|
109
114
|
await waitForText(setup, "closing in 5s");
|
|
110
115
|
|
|
@@ -115,6 +120,7 @@ describe("completion overlay", () => {
|
|
|
115
120
|
|
|
116
121
|
// Assert
|
|
117
122
|
const frame = await waitForText(setup, "resolved: approve");
|
|
123
|
+
|
|
118
124
|
expect(frame).not.toContain("closing in");
|
|
119
125
|
});
|
|
120
126
|
|
|
@@ -123,6 +129,7 @@ describe("completion overlay", () => {
|
|
|
123
129
|
writeFileSync(configPath, "[ui]\nauto_close = 1\n");
|
|
124
130
|
let exited = -1;
|
|
125
131
|
const setup = await renderApp((code) => (exited = code));
|
|
132
|
+
|
|
126
133
|
await submitApprove(setup);
|
|
127
134
|
await waitForText(setup, "closing in 1s");
|
|
128
135
|
expect(exited).toBe(-1);
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -48,7 +48,9 @@ async function renderApp() {
|
|
|
48
48
|
width: 120,
|
|
49
49
|
height: 30,
|
|
50
50
|
});
|
|
51
|
+
|
|
51
52
|
await waitForText(setup, "cueloop");
|
|
53
|
+
|
|
52
54
|
return setup;
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -59,6 +61,7 @@ describe("diff review", () => {
|
|
|
59
61
|
|
|
60
62
|
// Assert
|
|
61
63
|
const frame = setup.captureCharFrame();
|
|
64
|
+
|
|
62
65
|
expect(frame).toContain("■ src/store.ts");
|
|
63
66
|
expect(frame).toContain("@@ -1,4 +1,4 @@");
|
|
64
67
|
expect(frame).toContain("- private items = [];");
|
|
@@ -85,6 +88,7 @@ describe("diff review", () => {
|
|
|
85
88
|
// inline annotation card rendered under the line
|
|
86
89
|
await waitForText(setup, "◆ Map needs an eviction story.");
|
|
87
90
|
const stored = server.core.sessionGet(session.id);
|
|
91
|
+
|
|
88
92
|
expect(stored.annotations.length).toBe(1);
|
|
89
93
|
expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
|
|
90
94
|
|
|
@@ -95,6 +99,7 @@ describe("diff review", () => {
|
|
|
95
99
|
// Assert
|
|
96
100
|
await waitForText(setup, "feedback sent");
|
|
97
101
|
const resolved = server.core.sessionGet(session.id);
|
|
102
|
+
|
|
98
103
|
expect(resolved.verdict!.feedback).toContain("new Map()");
|
|
99
104
|
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
100
105
|
});
|
|
@@ -132,6 +137,7 @@ describe("diff review", () => {
|
|
|
132
137
|
width: 120,
|
|
133
138
|
height: 30,
|
|
134
139
|
});
|
|
140
|
+
|
|
135
141
|
await waitForText(setup, "cueloop");
|
|
136
142
|
|
|
137
143
|
// Act - move to the added line and reject its change
|
|
@@ -142,6 +148,7 @@ describe("diff review", () => {
|
|
|
142
148
|
// Assert - the single change is gone, so the curated working copy is empty
|
|
143
149
|
await waitForText(setup, "change rejected");
|
|
144
150
|
const stored = server.core.sessionGet(withFiles.id);
|
|
151
|
+
|
|
145
152
|
expect(stored.workingCopy).toBe("");
|
|
146
153
|
});
|
|
147
154
|
|
|
@@ -167,6 +174,7 @@ describe("diff review", () => {
|
|
|
167
174
|
width: 120,
|
|
168
175
|
height: 30,
|
|
169
176
|
});
|
|
177
|
+
|
|
170
178
|
await waitForText(setup, "cueloop");
|
|
171
179
|
|
|
172
180
|
// Act - reject the change under the added line
|
|
@@ -208,6 +216,7 @@ describe("diff review", () => {
|
|
|
208
216
|
width: 120,
|
|
209
217
|
height: 30,
|
|
210
218
|
});
|
|
219
|
+
|
|
211
220
|
await waitForText(setup, "cueloop");
|
|
212
221
|
|
|
213
222
|
// Act - reject the change; the removal card lands unselected (no undo button)
|
|
@@ -220,8 +229,10 @@ describe("diff review", () => {
|
|
|
220
229
|
const clickText = async (needle: string): Promise<void> => {
|
|
221
230
|
const lines = setup.captureCharFrame().split("\n");
|
|
222
231
|
const row = lines.findIndex((line) => line.includes(needle));
|
|
232
|
+
|
|
223
233
|
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
224
234
|
};
|
|
235
|
+
|
|
225
236
|
await clickText("REJECT · me");
|
|
226
237
|
await waitForText(setup, "undo");
|
|
227
238
|
|
package/src/App.export.test.tsx
CHANGED
|
@@ -35,6 +35,7 @@ beforeEach(() => {
|
|
|
35
35
|
vault = join(home, "vault");
|
|
36
36
|
mkdirSync(vault);
|
|
37
37
|
const configPath = join(home, "config.toml");
|
|
38
|
+
|
|
38
39
|
writeFileSync(
|
|
39
40
|
configPath,
|
|
40
41
|
`[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`,
|
|
@@ -64,6 +65,7 @@ describe("obsidian export on resolve", () => {
|
|
|
64
65
|
width: 120,
|
|
65
66
|
height: 32,
|
|
66
67
|
});
|
|
68
|
+
|
|
67
69
|
await waitForText(setup, "cueloop");
|
|
68
70
|
|
|
69
71
|
// Act
|
|
@@ -79,13 +81,16 @@ describe("obsidian export on resolve", () => {
|
|
|
79
81
|
// the export is an async round-trip after resolve; the status line lands
|
|
80
82
|
// on a later render than the file write
|
|
81
83
|
const dir = join(vault, "cueloop");
|
|
84
|
+
|
|
82
85
|
await waitForState(setup, () => existsSync(dir));
|
|
83
86
|
await waitForText(setup, "exported to");
|
|
84
87
|
|
|
85
88
|
const notes = readdirSync(dir);
|
|
89
|
+
|
|
86
90
|
expect(notes.length).toBe(1);
|
|
87
91
|
expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
|
|
88
92
|
const written = readFileSync(join(dir, notes[0]!), "utf8");
|
|
93
|
+
|
|
89
94
|
expect(written).toContain("source: cueloop");
|
|
90
95
|
expect(written).toContain(`session: ${session.id}`);
|
|
91
96
|
expect(written).toContain("verdict: approve");
|
|
@@ -63,7 +63,9 @@ async function renderApp() {
|
|
|
63
63
|
width: 120,
|
|
64
64
|
height: 32,
|
|
65
65
|
});
|
|
66
|
+
|
|
66
67
|
await waitForText(setup, "cueloop");
|
|
68
|
+
|
|
67
69
|
return setup;
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -75,29 +77,35 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
|
75
77
|
*/
|
|
76
78
|
function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
77
79
|
const backgrounds: string[] = [];
|
|
80
|
+
|
|
78
81
|
for (const line of setup.captureSpans().lines) {
|
|
79
82
|
for (const span of line.spans) {
|
|
80
83
|
if (!span.text.includes(needle)) continue;
|
|
81
84
|
const [red, green, blue] = span.bg.toInts();
|
|
85
|
+
|
|
82
86
|
backgrounds.push(
|
|
83
87
|
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
84
88
|
);
|
|
85
89
|
}
|
|
86
90
|
}
|
|
91
|
+
|
|
87
92
|
return backgrounds;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
91
96
|
const foregrounds: string[] = [];
|
|
97
|
+
|
|
92
98
|
for (const line of setup.captureSpans().lines) {
|
|
93
99
|
for (const span of line.spans) {
|
|
94
100
|
if (!span.text.includes(needle)) continue;
|
|
95
101
|
const [red, green, blue] = span.fg.toInts();
|
|
102
|
+
|
|
96
103
|
foregrounds.push(
|
|
97
104
|
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
98
105
|
);
|
|
99
106
|
}
|
|
100
107
|
}
|
|
108
|
+
|
|
101
109
|
return foregrounds;
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -111,6 +119,7 @@ async function toContextParagraph(setup: Setup): Promise<void> {
|
|
|
111
119
|
async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
112
120
|
const lines = setup.captureCharFrame().split("\n");
|
|
113
121
|
const row = lines.findIndex((line) => line.includes(needle));
|
|
122
|
+
|
|
114
123
|
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
115
124
|
await setup.renderOnce();
|
|
116
125
|
}
|
|
@@ -126,6 +135,7 @@ describe("share button", () => {
|
|
|
126
135
|
.captureCharFrame()
|
|
127
136
|
.split("\n")
|
|
128
137
|
.find((line) => line.includes("submitted by"));
|
|
138
|
+
|
|
129
139
|
expect(headerLine).toContain("Edit");
|
|
130
140
|
expect(headerLine).toContain("Share");
|
|
131
141
|
});
|
|
@@ -136,6 +146,7 @@ describe("share button", () => {
|
|
|
136
146
|
width: 120,
|
|
137
147
|
height: 32,
|
|
138
148
|
});
|
|
149
|
+
|
|
139
150
|
await waitForText(viewer, "cueloop");
|
|
140
151
|
|
|
141
152
|
// Assert
|
|
@@ -148,6 +159,7 @@ describe("share button", () => {
|
|
|
148
159
|
|
|
149
160
|
// Act
|
|
150
161
|
const setup = await renderApp();
|
|
162
|
+
|
|
151
163
|
await waitForText(setup, "submitted by");
|
|
152
164
|
|
|
153
165
|
// Assert - the owner toolbar is gone once the review is resolved
|
|
@@ -155,6 +167,7 @@ describe("share button", () => {
|
|
|
155
167
|
.captureCharFrame()
|
|
156
168
|
.split("\n")
|
|
157
169
|
.find((line) => line.includes("submitted by"));
|
|
170
|
+
|
|
158
171
|
expect(headerLine).not.toContain("Edit");
|
|
159
172
|
expect(headerLine).not.toContain("Share");
|
|
160
173
|
});
|
|
@@ -175,6 +188,7 @@ describe("edit affordance", () => {
|
|
|
175
188
|
width: 120,
|
|
176
189
|
height: 32,
|
|
177
190
|
});
|
|
191
|
+
|
|
178
192
|
await waitForText(viewer, "cueloop");
|
|
179
193
|
|
|
180
194
|
// Assert
|
|
@@ -190,6 +204,7 @@ describe("attribution", () => {
|
|
|
190
204
|
const contextBlockIndex = blocks.findIndex((block) =>
|
|
191
205
|
block.text.startsWith("The daemon persists"),
|
|
192
206
|
);
|
|
207
|
+
|
|
193
208
|
server.core.sessionAnnotate(session.id, {
|
|
194
209
|
id: "collab-1",
|
|
195
210
|
kind: "comment",
|
|
@@ -210,6 +225,7 @@ describe("selection feeds the annotation quote", () => {
|
|
|
210
225
|
test("v anchors, l extends, c composes with the char-precise quote", async () => {
|
|
211
226
|
// Arrange
|
|
212
227
|
const setup = await renderApp();
|
|
228
|
+
|
|
213
229
|
await toContextParagraph(setup);
|
|
214
230
|
|
|
215
231
|
// Act
|
|
@@ -227,6 +243,7 @@ describe("selection feeds the annotation quote", () => {
|
|
|
227
243
|
// Assert
|
|
228
244
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
229
245
|
const stored = server.core.sessionGet(session.id);
|
|
246
|
+
|
|
230
247
|
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
231
248
|
});
|
|
232
249
|
|
|
@@ -236,6 +253,7 @@ describe("selection feeds the annotation quote", () => {
|
|
|
236
253
|
const lines = setup.captureCharFrame().split("\n");
|
|
237
254
|
const row = lines.findIndex((line) => line.includes("persists sessions"));
|
|
238
255
|
const startColumn = lines[row]!.indexOf("persists");
|
|
256
|
+
|
|
239
257
|
expect(row).toBeGreaterThan(-1);
|
|
240
258
|
|
|
241
259
|
// Act
|
|
@@ -254,6 +272,7 @@ describe("selection feeds the annotation quote", () => {
|
|
|
254
272
|
// Assert
|
|
255
273
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
256
274
|
const stored = server.core.sessionGet(session.id);
|
|
275
|
+
|
|
257
276
|
expect(stored.annotations[0]!.anchor.quote).toBe("persists sessions");
|
|
258
277
|
});
|
|
259
278
|
});
|
|
@@ -262,6 +281,7 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
262
281
|
test("the selection stays painted while composing; cancel un-paints; save converts to the comment highlight", async () => {
|
|
263
282
|
// Arrange
|
|
264
283
|
const setup = await renderApp();
|
|
284
|
+
|
|
265
285
|
await toContextParagraph(setup);
|
|
266
286
|
|
|
267
287
|
// Act
|
|
@@ -296,6 +316,7 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
296
316
|
await settle(setup);
|
|
297
317
|
// the whole cursor block is the anchor here; check the highlight landed
|
|
298
318
|
const stored = server.core.sessionGet(session.id);
|
|
319
|
+
|
|
299
320
|
expect(stored.annotations.length).toBe(1);
|
|
300
321
|
expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(
|
|
301
322
|
DARK.markCommentBackground,
|
|
@@ -308,6 +329,7 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
308
329
|
test("clicking the Cancel word-button closes the compose box", async () => {
|
|
309
330
|
// Arrange
|
|
310
331
|
const setup = await renderApp();
|
|
332
|
+
|
|
311
333
|
await toContextParagraph(setup);
|
|
312
334
|
await press(setup, "v");
|
|
313
335
|
await press(setup, "l");
|
|
@@ -324,6 +346,7 @@ describe("inline compose keeps the anchor painted", () => {
|
|
|
324
346
|
test("clicking Cancel in a saved card's edit composer closes it", async () => {
|
|
325
347
|
// Arrange
|
|
326
348
|
const setup = await renderApp();
|
|
349
|
+
|
|
327
350
|
await toContextParagraph(setup);
|
|
328
351
|
await press(setup, "v");
|
|
329
352
|
await press(setup, "l");
|
|
@@ -355,7 +378,9 @@ describe("compose newline convention", () => {
|
|
|
355
378
|
height: 32,
|
|
356
379
|
otherModifiersMode: true,
|
|
357
380
|
});
|
|
381
|
+
|
|
358
382
|
await waitForText(setup, "cueloop");
|
|
383
|
+
|
|
359
384
|
return setup;
|
|
360
385
|
}
|
|
361
386
|
|
|
@@ -370,6 +395,7 @@ describe("compose newline convention", () => {
|
|
|
370
395
|
test("option+return inserts a newline; a bare return submits the multiline body", async () => {
|
|
371
396
|
// Arrange
|
|
372
397
|
const setup = await renderModifierAwareApp();
|
|
398
|
+
|
|
373
399
|
await toContextParagraph(setup);
|
|
374
400
|
await press(setup, "v");
|
|
375
401
|
await press(setup, "l");
|
|
@@ -396,6 +422,7 @@ describe("compose newline convention", () => {
|
|
|
396
422
|
test("shift+return still inserts a newline (existing muscle memory)", async () => {
|
|
397
423
|
// Arrange
|
|
398
424
|
const setup = await renderModifierAwareApp();
|
|
425
|
+
|
|
399
426
|
await toContextParagraph(setup);
|
|
400
427
|
await press(setup, "c");
|
|
401
428
|
|
|
@@ -418,6 +445,7 @@ describe("compose newline convention", () => {
|
|
|
418
445
|
test("escape cancels the composer without saving", async () => {
|
|
419
446
|
// Arrange
|
|
420
447
|
const setup = await renderModifierAwareApp();
|
|
448
|
+
|
|
421
449
|
await toContextParagraph(setup);
|
|
422
450
|
await press(setup, "c");
|
|
423
451
|
await waitForText(setup, 'comment on "');
|
|
@@ -436,6 +464,7 @@ describe("the document selects, the rail edits", () => {
|
|
|
436
464
|
test("e edits the selected card in place; enter saves through the controller", async () => {
|
|
437
465
|
// Arrange
|
|
438
466
|
const setup = await renderApp();
|
|
467
|
+
|
|
439
468
|
await toContextParagraph(setup);
|
|
440
469
|
await press(setup, "c");
|
|
441
470
|
await type(setup, "Needs a citation.");
|
|
@@ -463,6 +492,7 @@ describe("the document selects, the rail edits", () => {
|
|
|
463
492
|
const contextBlockIndex = blocks.findIndex((block) =>
|
|
464
493
|
block.text.startsWith("The daemon persists"),
|
|
465
494
|
);
|
|
495
|
+
|
|
466
496
|
server.core.sessionAnnotate(session.id, {
|
|
467
497
|
id: "collab-1",
|
|
468
498
|
kind: "comment",
|
|
@@ -471,6 +501,7 @@ describe("the document selects, the rail edits", () => {
|
|
|
471
501
|
author: "SHA256:1a2b3c4d5e6f",
|
|
472
502
|
});
|
|
473
503
|
const setup = await renderApp();
|
|
504
|
+
|
|
474
505
|
await waitForText(setup, "1a2b3c4d");
|
|
475
506
|
|
|
476
507
|
// Act - focus the collaborator card, then activate it
|
|
@@ -484,6 +515,7 @@ describe("the document selects, the rail edits", () => {
|
|
|
484
515
|
test("x deletes the selected card and un-paints the document highlight", async () => {
|
|
485
516
|
// Arrange
|
|
486
517
|
const setup = await renderApp();
|
|
518
|
+
|
|
487
519
|
await toContextParagraph(setup);
|
|
488
520
|
await press(setup, "c");
|
|
489
521
|
await type(setup, "Delete me.");
|
|
@@ -506,6 +538,7 @@ describe("plan cut removals", () => {
|
|
|
506
538
|
test("a cut block becomes a rail removal card and u restores it", async () => {
|
|
507
539
|
// Arrange
|
|
508
540
|
const setup = await renderApp();
|
|
541
|
+
|
|
509
542
|
await toContextParagraph(setup);
|
|
510
543
|
|
|
511
544
|
// Act - no card is selected, so x cuts the block under the cursor
|
|
@@ -532,6 +565,7 @@ describe("addressed annotations leave the open list", () => {
|
|
|
532
565
|
const contextBlockIndex = blocks.findIndex((block) =>
|
|
533
566
|
block.text.startsWith("The daemon persists"),
|
|
534
567
|
);
|
|
568
|
+
|
|
535
569
|
server.core.sessionAnnotate(session.id, {
|
|
536
570
|
id: "a_settled",
|
|
537
571
|
kind: "comment",
|
|
@@ -548,10 +582,12 @@ describe("addressed annotations leave the open list", () => {
|
|
|
548
582
|
|
|
549
583
|
// Act
|
|
550
584
|
const setup = await renderApp();
|
|
585
|
+
|
|
551
586
|
await waitForText(setup, "still open note");
|
|
552
587
|
|
|
553
588
|
// Assert: the open card renders, the addressed one is gone behind the count
|
|
554
589
|
const frame = setup.captureCharFrame();
|
|
590
|
+
|
|
555
591
|
expect(frame).toContain("✓ 1 addressed by revision");
|
|
556
592
|
expect(frame).not.toContain("settled note");
|
|
557
593
|
expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
|
|
@@ -566,6 +602,7 @@ describe("sheet header", () => {
|
|
|
566
602
|
|
|
567
603
|
// Assert
|
|
568
604
|
const frame = setup.captureCharFrame();
|
|
605
|
+
|
|
569
606
|
expect(frame).toContain("submitted by agent/worker-3");
|
|
570
607
|
expect(frame).toContain("rev 1");
|
|
571
608
|
expect(frame).toContain("Edit");
|
|
@@ -586,6 +623,7 @@ describe("sheet header", () => {
|
|
|
586
623
|
// Assert - the three launcher buttons, in order
|
|
587
624
|
await waitForText(setup, "Claude Code");
|
|
588
625
|
const frame = setup.captureCharFrame();
|
|
626
|
+
|
|
589
627
|
expect(frame).toContain("Pi");
|
|
590
628
|
expect(frame).toContain("OpenAI Codex");
|
|
591
629
|
});
|
|
@@ -615,12 +653,14 @@ describe("quick-actions settings editor", () => {
|
|
|
615
653
|
describe("edit-exit reconciliation", () => {
|
|
616
654
|
test("an edit that removes an anchored passage orphans the annotation and shows the banner", async () => {
|
|
617
655
|
const script = join(home, "fake-editor.sh");
|
|
656
|
+
|
|
618
657
|
await Bun.write(script, `#!/bin/sh\nsed -i '' '/^The daemon persists/d' "$1"\n`);
|
|
619
658
|
Bun.spawnSync(["chmod", "+x", script]);
|
|
620
659
|
process.env.CUELOOP_EDITOR = script;
|
|
621
660
|
try {
|
|
622
661
|
// Arrange
|
|
623
662
|
const setup = await renderApp();
|
|
663
|
+
|
|
624
664
|
await toContextParagraph(setup);
|
|
625
665
|
await press(setup, "c");
|
|
626
666
|
await type(setup, "Anchor me to the doomed passage.");
|
|
@@ -650,6 +690,7 @@ describe("marker-actions popover", () => {
|
|
|
650
690
|
test("v shows the toolbar, a opens quick-actions, enter inserts the preset comment", async () => {
|
|
651
691
|
// Arrange
|
|
652
692
|
const setup = await renderApp();
|
|
693
|
+
|
|
653
694
|
await toContextParagraph(setup);
|
|
654
695
|
|
|
655
696
|
// Act - marking a span reveals the inline toolbar
|
|
@@ -666,6 +707,7 @@ describe("marker-actions popover", () => {
|
|
|
666
707
|
// Assert - a comment annotation was created with the second default's body
|
|
667
708
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
668
709
|
const stored = server.core.sessionGet(session.id);
|
|
710
|
+
|
|
669
711
|
expect(stored.annotations[0]!.kind).toBe("comment");
|
|
670
712
|
expect(stored.annotations[0]!.body).toBe(quickActionBody(DEFAULT_QUICK_ACTIONS[1]!));
|
|
671
713
|
});
|
|
@@ -41,11 +41,13 @@ const fakeRenderer: PrototypeRenderer = {
|
|
|
41
41
|
screenshot: async () => {
|
|
42
42
|
rendered = true;
|
|
43
43
|
screenshotCount += 1;
|
|
44
|
+
|
|
44
45
|
return new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
45
46
|
},
|
|
46
47
|
elementAt: async () => FAKE_ELEMENT,
|
|
47
48
|
scrollBy: async (deltaY) => {
|
|
48
49
|
scrollDeltas.push(deltaY);
|
|
50
|
+
|
|
49
51
|
return true;
|
|
50
52
|
},
|
|
51
53
|
close: async () => undefined,
|
|
@@ -86,7 +88,9 @@ async function renderApp() {
|
|
|
86
88
|
width: 120,
|
|
87
89
|
height: 32,
|
|
88
90
|
});
|
|
91
|
+
|
|
89
92
|
await waitForText(setup, "cueloop");
|
|
93
|
+
|
|
90
94
|
return setup;
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -95,6 +99,7 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
|
95
99
|
async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
96
100
|
const lines = setup.captureCharFrame().split("\n");
|
|
97
101
|
const row = lines.findIndex((line) => line.includes(needle));
|
|
102
|
+
|
|
98
103
|
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
99
104
|
await setup.renderOnce();
|
|
100
105
|
}
|
|
@@ -104,6 +109,7 @@ describe("prototype review", () => {
|
|
|
104
109
|
// Arrange - wait until the fake browser produced its first screenshot, so
|
|
105
110
|
// the sheet is interactive before the click
|
|
106
111
|
const setup = await renderApp();
|
|
112
|
+
|
|
107
113
|
await waitForState(setup, () => rendered);
|
|
108
114
|
await settle(setup);
|
|
109
115
|
// selecting an element must not re-screenshot the page (the popover-click
|
|
@@ -125,6 +131,7 @@ describe("prototype review", () => {
|
|
|
125
131
|
// Assert - the annotation is stored against the element's selector
|
|
126
132
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
127
133
|
const stored = server.core.sessionGet(session.id).annotations[0]!;
|
|
134
|
+
|
|
128
135
|
expect(stored.kind).toBe("comment");
|
|
129
136
|
expect(stored.body).toBe("tighten the padding");
|
|
130
137
|
expect(stored.anchor.selector).toBe("main > div.card");
|
|
@@ -132,6 +139,7 @@ describe("prototype review", () => {
|
|
|
132
139
|
// and it renders in the rail
|
|
133
140
|
await settle(setup);
|
|
134
141
|
const frame = setup.captureCharFrame();
|
|
142
|
+
|
|
135
143
|
expect(frame).toContain("tighten the padding");
|
|
136
144
|
expect(frame).toContain("COMMENT");
|
|
137
145
|
});
|
|
@@ -139,6 +147,7 @@ describe("prototype review", () => {
|
|
|
139
147
|
test("escape cancels the compose, matching the plan composer", async () => {
|
|
140
148
|
// Arrange
|
|
141
149
|
const setup = await renderApp();
|
|
150
|
+
|
|
142
151
|
await waitForState(setup, () => rendered);
|
|
143
152
|
await settle(setup);
|
|
144
153
|
await setup.mockMouse.click(6, 6);
|
|
@@ -158,6 +167,7 @@ describe("prototype review", () => {
|
|
|
158
167
|
test("wheel over the preview scrolls the page down, then up, and clears a selection", async () => {
|
|
159
168
|
// Arrange
|
|
160
169
|
const setup = await renderApp();
|
|
170
|
+
|
|
161
171
|
await waitForState(setup, () => rendered);
|
|
162
172
|
await settle(setup);
|
|
163
173
|
|
|
@@ -167,6 +177,7 @@ describe("prototype review", () => {
|
|
|
167
177
|
await waitForText(setup, "comment");
|
|
168
178
|
// scrolling moves the page, so it DOES re-screenshot (unlike a click)
|
|
169
179
|
const screenshotsBeforeScroll = screenshotCount;
|
|
180
|
+
|
|
170
181
|
await setup.mockMouse.scroll(6, 6, "down");
|
|
171
182
|
await waitForState(setup, () => scrollDeltas.length === 1);
|
|
172
183
|
await waitForTextGone(setup, "comment");
|
|
@@ -54,13 +54,16 @@ async function renderObserver() {
|
|
|
54
54
|
width: 120,
|
|
55
55
|
height: 32,
|
|
56
56
|
});
|
|
57
|
+
|
|
57
58
|
await waitForText(setup, "cueloop");
|
|
59
|
+
|
|
58
60
|
return setup;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/** Session state that any mutating verb would change. */
|
|
62
64
|
function snapshot() {
|
|
63
65
|
const stored = server.core.sessionGet(session.id);
|
|
66
|
+
|
|
64
67
|
return {
|
|
65
68
|
annotations: stored.annotations.length,
|
|
66
69
|
workingCopy: stored.workingCopy,
|
|
@@ -75,6 +78,7 @@ describe("observer rendering", () => {
|
|
|
75
78
|
|
|
76
79
|
// Assert
|
|
77
80
|
const frame = setup.captureCharFrame();
|
|
81
|
+
|
|
78
82
|
expect(frame).toContain("· observer");
|
|
79
83
|
expect(frame).toContain("menu");
|
|
80
84
|
});
|
|
@@ -90,10 +94,12 @@ describe("observer verbs are blocked", () => {
|
|
|
90
94
|
test(`${verb} (${key}) answers observer - read-only and mutates nothing`, async () => {
|
|
91
95
|
const setup = await renderObserver();
|
|
92
96
|
const before = snapshot();
|
|
97
|
+
|
|
93
98
|
await press(setup, "j");
|
|
94
99
|
await press(setup, "j");
|
|
95
100
|
await press(setup, key);
|
|
96
101
|
const frame = await waitForText(setup, "observer - read-only");
|
|
102
|
+
|
|
97
103
|
// no overlay opened: compose/submit bars never appear
|
|
98
104
|
expect(frame).not.toContain('comment on "');
|
|
99
105
|
expect(frame).not.toContain("verdict ←/→");
|
|
@@ -114,6 +120,7 @@ describe("observer verbs are blocked", () => {
|
|
|
114
120
|
|
|
115
121
|
// Assert
|
|
116
122
|
const frame = setup.captureCharFrame();
|
|
123
|
+
|
|
117
124
|
expect(frame).toContain("observer - read-only");
|
|
118
125
|
expect(frame).not.toContain('comment on "');
|
|
119
126
|
expect(snapshot().annotations).toBe(0);
|
|
@@ -133,6 +140,7 @@ describe("observer navigation still works", () => {
|
|
|
133
140
|
// Assert
|
|
134
141
|
const lines = setup.captureCharFrame().split("\n");
|
|
135
142
|
const cursorLine = lines.find((line) => line.includes("▎"))!;
|
|
143
|
+
|
|
136
144
|
expect(cursorLine).toContain("persists sessions");
|
|
137
145
|
});
|
|
138
146
|
|
|
@@ -165,8 +173,10 @@ function hasBorderColor(setup: Awaited<ReturnType<typeof renderObserver>>, hex:
|
|
|
165
173
|
const [red, green, blue] = span.fg.toInts();
|
|
166
174
|
const rendered =
|
|
167
175
|
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
176
|
+
|
|
168
177
|
if (rendered === hex) return true;
|
|
169
178
|
}
|
|
170
179
|
}
|
|
180
|
+
|
|
171
181
|
return false;
|
|
172
182
|
}
|
|
@@ -55,9 +55,11 @@ async function frame(): Promise<string> {
|
|
|
55
55
|
width: 120,
|
|
56
56
|
height: 36,
|
|
57
57
|
});
|
|
58
|
+
|
|
58
59
|
await waitForText(setup, "Render Plan");
|
|
59
60
|
// async highlights settle within the visual-idle wait
|
|
60
61
|
await setup.waitForVisualIdle();
|
|
62
|
+
|
|
61
63
|
return setup.captureCharFrame();
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -119,6 +121,7 @@ describe("block spacing", () => {
|
|
|
119
121
|
width: 120,
|
|
120
122
|
height: 30,
|
|
121
123
|
});
|
|
124
|
+
|
|
122
125
|
await waitForText(setup, "one");
|
|
123
126
|
|
|
124
127
|
// Assert
|
|
@@ -127,6 +130,7 @@ describe("block spacing", () => {
|
|
|
127
130
|
.split("\n")
|
|
128
131
|
.map((line) => line.trimEnd());
|
|
129
132
|
const one = lines.findIndex((line) => line.includes("- one"));
|
|
133
|
+
|
|
130
134
|
expect(lines[one + 1]).toContain("- two");
|
|
131
135
|
});
|
|
132
136
|
});
|
|
@@ -42,7 +42,9 @@ async function renderApp() {
|
|
|
42
42
|
width: 120,
|
|
43
43
|
height: 30,
|
|
44
44
|
});
|
|
45
|
+
|
|
45
46
|
await waitForText(setup, "Review");
|
|
47
|
+
|
|
46
48
|
return setup;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -69,6 +71,7 @@ describe("review panel", () => {
|
|
|
69
71
|
// Assert: compact drops the submit button and widens the plan (rail is a strip)
|
|
70
72
|
await waitForTextGone(setup, "Submit review");
|
|
71
73
|
const compactWidth = planWidth();
|
|
74
|
+
|
|
72
75
|
expect(compactWidth).toBeGreaterThan(expandedWidth);
|
|
73
76
|
|
|
74
77
|
// Act
|
|
@@ -77,6 +80,7 @@ describe("review panel", () => {
|
|
|
77
80
|
|
|
78
81
|
// Assert: hidden spans the plan full width, no rail column
|
|
79
82
|
const hiddenWidth = planWidth();
|
|
83
|
+
|
|
80
84
|
expect(hiddenWidth).toBeGreaterThan(compactWidth);
|
|
81
85
|
expect(hiddenWidth).toBeGreaterThan(115);
|
|
82
86
|
|
|
@@ -108,12 +112,14 @@ describe("review panel", () => {
|
|
|
108
112
|
|
|
109
113
|
// Assert
|
|
110
114
|
const widened = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
|
|
115
|
+
|
|
111
116
|
expect(widened).toBeGreaterThan(startWidth);
|
|
112
117
|
});
|
|
113
118
|
|
|
114
119
|
test("submitting from a hidden panel force-opens the rail so the confirm card is reachable", async () => {
|
|
115
120
|
// Arrange
|
|
116
121
|
const setup = await renderApp();
|
|
122
|
+
|
|
117
123
|
await press(setup, "b"); // -> compact
|
|
118
124
|
await press(setup, "b"); // -> hidden
|
|
119
125
|
|
|
@@ -127,6 +133,7 @@ describe("review panel", () => {
|
|
|
127
133
|
// Assert
|
|
128
134
|
await waitForText(setup, "submit review");
|
|
129
135
|
const frame = setup.captureCharFrame();
|
|
136
|
+
|
|
130
137
|
expect(frame).toContain("submit review"); // the confirm card is visible again
|
|
131
138
|
expect(frame).toContain("[Approve]");
|
|
132
139
|
|