@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67
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 +6 -5
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +79 -112
- package/src/App.plan-review.test.tsx +23 -205
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +9 -27
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +136 -31
- package/src/App.tsx +491 -322
- package/src/App.walk.test.tsx +15 -16
- package/src/App.workbench.test.tsx +390 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +61 -0
- package/src/app-screens.tsx +197 -77
- package/src/app-view-model.ts +9 -69
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +1 -1
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +239 -0
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +170 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +56 -54
- package/src/components/DiffSheet.syntax.test.tsx +24 -7
- package/src/components/DiffSheet.tsx +607 -276
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +105 -0
- package/src/components/EditorGrid.tsx +338 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +78 -0
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +188 -33
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +82 -0
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PromptDialog.tsx +8 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +114 -67
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SurfaceScrollbar.stories.tsx +34 -0
- package/src/components/SurfaceScrollbar.test.tsx +111 -0
- package/src/components/SurfaceScrollbar.tsx +93 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +62 -1352
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +70 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/diff-sheet-layout.test.ts +125 -55
- package/src/components/diff-sheet-layout.ts +3 -103
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +96 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +1 -1
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/config.test.ts +20 -49
- package/src/config.ts +26 -28
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-syntax.test.ts +16 -0
- package/src/diff-syntax.ts +6 -3
- package/src/discussions.ts +79 -0
- package/src/intent-dispatch.test.ts +31 -11
- package/src/intent-dispatch.ts +93 -48
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +3 -25
- package/src/keymap.ts +17 -18
- package/src/mark-runs.ts +120 -0
- package/src/pty.ts +10 -2
- package/src/run.ts +17 -7
- package/src/serve.test.ts +4 -8
- package/src/session-controller.curate.test.ts +75 -0
- package/src/session-controller.share.test.ts +1 -0
- package/src/session-controller.tree.test.ts +26 -0
- package/src/session-controller.ts +160 -31
- package/src/slash-palette.ts +83 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +3 -3
- package/src/test-support.ts +39 -8
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +25 -9
- package/src/thread-chords.ts +33 -8
- package/src/use-annotation-surface.tsx +837 -0
- package/src/use-changes-workbench.ts +151 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +23 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
- package/src/components/ReviewPanel.stories.tsx +0 -100
- package/src/components/ReviewPanel.tsx +0 -103
- package/src/components/ReviewRail.stories.tsx +0 -232
- package/src/components/ReviewRail.tsx +0 -327
- package/src/review-panel.test.ts +0 -113
- package/src/review-panel.ts +0 -83
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.67",
|
|
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,15 +26,16 @@
|
|
|
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.67",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.67",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.67",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.67",
|
|
33
33
|
"@opentui/core": "^0.5.8",
|
|
34
34
|
"@opentui/keymap": "0.5.8",
|
|
35
35
|
"@opentui/react": "^0.5.8",
|
|
36
36
|
"@opentui/ssh": "0.5.8",
|
|
37
37
|
"@pierre/diffs": "^1.3.6",
|
|
38
|
+
"@tanstack/virtual-core": "^3.17.9",
|
|
38
39
|
"diff": "^9",
|
|
39
40
|
"puppeteer-core": "^25.8.0",
|
|
40
41
|
"react": "^19.2.0",
|
|
@@ -118,8 +118,8 @@ describe("completion overlay", () => {
|
|
|
118
118
|
// window before it is delivered - the frame wait absorbs it
|
|
119
119
|
await press(setup, "escape");
|
|
120
120
|
|
|
121
|
-
// Assert
|
|
122
|
-
const frame = await waitForText(setup, "
|
|
121
|
+
// Assert - the countdown is cancelled and we are back in the (resolved) thread view
|
|
122
|
+
const frame = await waitForText(setup, "Ship the thing");
|
|
123
123
|
|
|
124
124
|
expect(frame).not.toContain("closing in");
|
|
125
125
|
});
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Diff-review flow in the virtual terminal
|
|
1
|
+
/** Diff-review flow in the virtual terminal: the diff sheet annotates like the thread view. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
@@ -9,7 +9,17 @@ 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 {
|
|
12
|
+
import { NERD } from "./components/primitives/icons";
|
|
13
|
+
import {
|
|
14
|
+
clickText,
|
|
15
|
+
dragText,
|
|
16
|
+
isolateUserConfig,
|
|
17
|
+
press,
|
|
18
|
+
pressKey,
|
|
19
|
+
typeText,
|
|
20
|
+
waitForState,
|
|
21
|
+
waitForText,
|
|
22
|
+
} from "./test-support";
|
|
13
23
|
|
|
14
24
|
const PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
15
25
|
index 111..222 100644
|
|
@@ -43,13 +53,13 @@ afterEach(() => {
|
|
|
43
53
|
rmSync(home, { recursive: true, force: true });
|
|
44
54
|
});
|
|
45
55
|
|
|
46
|
-
async function renderApp() {
|
|
47
|
-
const setup = await testRender(<App home={home} sessionId={
|
|
56
|
+
async function renderApp(sessionId = session.id) {
|
|
57
|
+
const setup = await testRender(<App home={home} sessionId={sessionId} />, {
|
|
48
58
|
width: 120,
|
|
49
59
|
height: 30,
|
|
50
60
|
});
|
|
51
61
|
|
|
52
|
-
await waitForText(setup, "
|
|
62
|
+
await waitForText(setup, "new Map()");
|
|
53
63
|
|
|
54
64
|
return setup;
|
|
55
65
|
}
|
|
@@ -62,46 +72,74 @@ describe("diff review", () => {
|
|
|
62
72
|
// Assert
|
|
63
73
|
const frame = setup.captureCharFrame();
|
|
64
74
|
|
|
65
|
-
expect(frame).toContain("
|
|
75
|
+
expect(frame).toContain("src/store.ts");
|
|
66
76
|
expect(frame).toContain("@@ -1,4 +1,4 @@");
|
|
67
|
-
expect(frame).
|
|
68
|
-
expect(frame).
|
|
77
|
+
expect(frame).toMatch(/- {2,}private items = \[\];/);
|
|
78
|
+
expect(frame).toMatch(/\+ {2,}private items = new Map\(\);/);
|
|
69
79
|
});
|
|
70
80
|
|
|
71
|
-
test("
|
|
81
|
+
test("a drag marks the code, typing comments on it, and the comment lands in feedback", async () => {
|
|
72
82
|
// Arrange
|
|
73
83
|
const setup = await renderApp();
|
|
74
84
|
|
|
75
|
-
// Act
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await
|
|
85
|
+
// Act - mark "new Map()" on the added line and just start typing, as in the thread view
|
|
86
|
+
await dragText(setup, "new Map()", "new Map()", "new Map()".length);
|
|
87
|
+
await typeText(setup, "Map needs an eviction story.");
|
|
88
|
+
await waitForText(setup, "● Map needs an eviction story.");
|
|
89
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
79
90
|
|
|
80
|
-
// Assert
|
|
81
|
-
await
|
|
91
|
+
// Assert - the discussion card hangs under the line; the anchor is the marked words
|
|
92
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
93
|
+
await waitForText(setup, "● Map needs an eviction story.");
|
|
94
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("new Map()");
|
|
82
95
|
|
|
83
|
-
// Act
|
|
84
|
-
await setup
|
|
96
|
+
// Act - submit with the session chord (cmd+enter, no composer open), confirm request_changes
|
|
97
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
85
98
|
await press(setup, "enter");
|
|
86
99
|
|
|
87
100
|
// Assert
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
await waitForText(setup, "feedback sent");
|
|
102
|
+
const resolved = server.core.sessionGet(session.id);
|
|
103
|
+
|
|
104
|
+
expect(resolved.verdict!.feedback).toContain("new Map()");
|
|
105
|
+
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
106
|
+
});
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
test("a comment reopens with its mark painted on the code", async () => {
|
|
109
|
+
// Arrange - a stored comment anchored to the added line's words
|
|
110
|
+
server.core.sessionAnnotate(session.id, {
|
|
111
|
+
id: "a_existing",
|
|
112
|
+
kind: "comment",
|
|
113
|
+
anchor: { quote: "new Map()", prefix: " private items = ", suffix: ";" },
|
|
114
|
+
body: "Existing note.",
|
|
115
|
+
});
|
|
94
116
|
|
|
95
117
|
// Act
|
|
96
|
-
await
|
|
97
|
-
await press(setup, "enter"); // confirm request_changes
|
|
118
|
+
const setup = await renderApp();
|
|
98
119
|
|
|
99
120
|
// Assert
|
|
100
|
-
await waitForText(setup, "
|
|
101
|
-
|
|
121
|
+
await waitForText(setup, "● Existing note.");
|
|
122
|
+
});
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
test("option+c folds the caret's file to its band; the chevron restores its body", async () => {
|
|
125
|
+
// Arrange - the caret opens on the file's first code line
|
|
126
|
+
const setup = await renderApp();
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
await pressKey(setup, "c", { meta: true });
|
|
130
|
+
|
|
131
|
+
// Assert - the body is gone but the band (with its counts) remains
|
|
132
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("new Map()"));
|
|
133
|
+
const collapsed = setup.captureCharFrame();
|
|
134
|
+
|
|
135
|
+
expect(collapsed).toContain("src/store.ts");
|
|
136
|
+
expect(collapsed).toContain("+1");
|
|
137
|
+
|
|
138
|
+
// Act - the band's chevron unfolds it again
|
|
139
|
+
await clickText(setup, NERD.chevronRight);
|
|
140
|
+
|
|
141
|
+
// Assert
|
|
142
|
+
await waitForText(setup, "new Map()");
|
|
105
143
|
});
|
|
106
144
|
|
|
107
145
|
test("curation needs full file contents; a legacy diff answers", async () => {
|
|
@@ -109,7 +147,7 @@ describe("diff review", () => {
|
|
|
109
147
|
const setup = await renderApp();
|
|
110
148
|
|
|
111
149
|
// Act
|
|
112
|
-
await
|
|
150
|
+
await pressKey(setup, "x", { meta: true });
|
|
113
151
|
|
|
114
152
|
// Assert
|
|
115
153
|
await waitForText(setup, "hunk curation needs full file contents");
|
|
@@ -133,17 +171,11 @@ describe("diff review", () => {
|
|
|
133
171
|
],
|
|
134
172
|
},
|
|
135
173
|
});
|
|
136
|
-
const setup = await
|
|
137
|
-
width: 120,
|
|
138
|
-
height: 30,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await waitForText(setup, "cueloop");
|
|
174
|
+
const setup = await renderApp(withFiles.id);
|
|
142
175
|
|
|
143
|
-
// Act -
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await press(setup, "x");
|
|
176
|
+
// Act - place the caret on the added line and reject its change
|
|
177
|
+
await clickText(setup, "new Map()");
|
|
178
|
+
await pressKey(setup, "x", { meta: true });
|
|
147
179
|
|
|
148
180
|
// Assert - the single change is gone, so the curated working copy is empty
|
|
149
181
|
await waitForText(setup, "change rejected");
|
|
@@ -152,8 +184,9 @@ describe("diff review", () => {
|
|
|
152
184
|
expect(stored.workingCopy).toBe("");
|
|
153
185
|
});
|
|
154
186
|
|
|
155
|
-
test("a
|
|
156
|
-
// Arrange -
|
|
187
|
+
test("a diff opens with the Changes column listing every changed file", async () => {
|
|
188
|
+
// Arrange - two changed files; config.ts is not in the patch text, so it can
|
|
189
|
+
// only appear here by way of the Changes column reading artifact.files
|
|
157
190
|
const withFiles = server.core.sessionCreate({
|
|
158
191
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
159
192
|
artifact: {
|
|
@@ -161,12 +194,8 @@ describe("diff review", () => {
|
|
|
161
194
|
content: PATCH,
|
|
162
195
|
meta: { title: "working tree" },
|
|
163
196
|
files: [
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
oldContents: "export class Store {\n private items = [];\n}\n",
|
|
167
|
-
newContents: "export class Store {\n private items = new Map();\n}\n",
|
|
168
|
-
status: "modified",
|
|
169
|
-
},
|
|
197
|
+
{ path: "src/store.ts", oldContents: "a", newContents: "b", status: "modified" },
|
|
198
|
+
{ path: "src/config.ts", oldContents: "", newContents: "c", status: "added" },
|
|
170
199
|
],
|
|
171
200
|
},
|
|
172
201
|
});
|
|
@@ -177,70 +206,8 @@ describe("diff review", () => {
|
|
|
177
206
|
|
|
178
207
|
await waitForText(setup, "cueloop");
|
|
179
208
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await press(setup, "x");
|
|
184
|
-
|
|
185
|
-
// Assert - the rail shows a removal card titled REJECT · me for the change
|
|
186
|
-
await waitForText(setup, "REJECT · me");
|
|
187
|
-
expect(server.core.sessionGet(withFiles.id).workingCopy).toBe("");
|
|
188
|
-
|
|
189
|
-
// Act - undo restores it (no selection, so the last item)
|
|
190
|
-
await press(setup, "u");
|
|
191
|
-
|
|
192
|
-
// Assert - the change returns to the curated diff (working copy cleared)
|
|
193
|
-
await waitForText(setup, "removal restored");
|
|
194
|
-
expect(server.core.sessionGet(withFiles.id).workingCopy).toBeUndefined();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("a selected removal card shows an undo button that restores it on click", async () => {
|
|
198
|
-
// Arrange - a diff session that carries full file contents
|
|
199
|
-
const withFiles = server.core.sessionCreate({
|
|
200
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
201
|
-
artifact: {
|
|
202
|
-
type: "diff",
|
|
203
|
-
content: PATCH,
|
|
204
|
-
meta: { title: "working tree" },
|
|
205
|
-
files: [
|
|
206
|
-
{
|
|
207
|
-
path: "src/store.ts",
|
|
208
|
-
oldContents: "export class Store {\n private items = [];\n}\n",
|
|
209
|
-
newContents: "export class Store {\n private items = new Map();\n}\n",
|
|
210
|
-
status: "modified",
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
const setup = await testRender(<App home={home} sessionId={withFiles.id} />, {
|
|
216
|
-
width: 120,
|
|
217
|
-
height: 30,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
await waitForText(setup, "cueloop");
|
|
221
|
-
|
|
222
|
-
// Act - reject the change; the removal card lands unselected (no undo button)
|
|
223
|
-
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
224
|
-
await press(setup, "x");
|
|
225
|
-
await waitForText(setup, "REJECT · me");
|
|
226
|
-
expect(setup.captureCharFrame()).not.toContain("undo");
|
|
227
|
-
|
|
228
|
-
// Act - click the card to select it; the undo button appears
|
|
229
|
-
const clickText = async (needle: string): Promise<void> => {
|
|
230
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
231
|
-
const row = lines.findIndex((line) => line.includes(needle));
|
|
232
|
-
|
|
233
|
-
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
await clickText("REJECT · me");
|
|
237
|
-
await waitForText(setup, "undo");
|
|
238
|
-
|
|
239
|
-
// Act - click the undo button
|
|
240
|
-
await clickText("undo");
|
|
241
|
-
|
|
242
|
-
// Assert - the change returns to the curated diff (working copy cleared)
|
|
243
|
-
await waitForText(setup, "removal restored");
|
|
244
|
-
expect(server.core.sessionGet(withFiles.id).workingCopy).toBeUndefined();
|
|
209
|
+
// Assert - the context default opens the column for a diff, so both files show
|
|
210
|
+
await waitForText(setup, "config.ts");
|
|
211
|
+
expect(setup.captureCharFrame()).toContain("store.ts");
|
|
245
212
|
});
|
|
246
213
|
});
|
|
@@ -10,7 +10,6 @@ import { DaemonServer } from "@cueloop/daemon";
|
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
12
|
import { DEFAULT_QUICK_ACTIONS, quickActionBody } from "./config";
|
|
13
|
-
import { DARK } from "./theme";
|
|
14
13
|
import {
|
|
15
14
|
clickText,
|
|
16
15
|
dragText,
|
|
@@ -100,14 +99,13 @@ describe("share button", () => {
|
|
|
100
99
|
const setup = await renderApp();
|
|
101
100
|
|
|
102
101
|
// Assert
|
|
103
|
-
// both word-buttons ride the
|
|
102
|
+
// both word-buttons ride the header's thread segment, so share sits next to edit
|
|
104
103
|
const headerLine = setup
|
|
105
104
|
.captureCharFrame()
|
|
106
105
|
.split("\n")
|
|
107
|
-
.find((line) => line.includes("
|
|
106
|
+
.find((line) => line.includes("edit"));
|
|
108
107
|
|
|
109
|
-
expect(headerLine).toContain("
|
|
110
|
-
expect(headerLine).toContain("Share");
|
|
108
|
+
expect(headerLine).toContain("share");
|
|
111
109
|
});
|
|
112
110
|
|
|
113
111
|
test("a read-only viewer (a plan shared over ssh) never sees the Share button", async () => {
|
|
@@ -120,7 +118,7 @@ describe("share button", () => {
|
|
|
120
118
|
await waitForText(viewer, "cueloop");
|
|
121
119
|
|
|
122
120
|
// Assert
|
|
123
|
-
expect(viewer.captureCharFrame()).not.toContain("
|
|
121
|
+
expect(viewer.captureCharFrame()).not.toContain(" share ");
|
|
124
122
|
});
|
|
125
123
|
|
|
126
124
|
test("a resolved plan hides Edit and Share (no re-sharing a finished review)", async () => {
|
|
@@ -130,16 +128,13 @@ describe("share button", () => {
|
|
|
130
128
|
// Act
|
|
131
129
|
const setup = await renderApp();
|
|
132
130
|
|
|
133
|
-
await waitForText(setup, "
|
|
131
|
+
await waitForText(setup, "Migration Plan");
|
|
134
132
|
|
|
135
133
|
// Assert - the owner toolbar is gone once the review is resolved
|
|
136
|
-
const
|
|
137
|
-
.captureCharFrame()
|
|
138
|
-
.split("\n")
|
|
139
|
-
.find((line) => line.includes("submitted by"));
|
|
134
|
+
const frame = setup.captureCharFrame();
|
|
140
135
|
|
|
141
|
-
expect(
|
|
142
|
-
expect(
|
|
136
|
+
expect(frame).not.toContain(" edit ");
|
|
137
|
+
expect(frame).not.toContain(" share ");
|
|
143
138
|
});
|
|
144
139
|
});
|
|
145
140
|
|
|
@@ -149,7 +144,7 @@ describe("edit affordance", () => {
|
|
|
149
144
|
const owner = await renderApp();
|
|
150
145
|
|
|
151
146
|
// Assert
|
|
152
|
-
expect(owner.captureCharFrame()).toContain("
|
|
147
|
+
expect(owner.captureCharFrame()).toContain(" edit ");
|
|
153
148
|
});
|
|
154
149
|
|
|
155
150
|
test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
|
|
@@ -162,32 +157,7 @@ describe("edit affordance", () => {
|
|
|
162
157
|
await waitForText(viewer, "cueloop");
|
|
163
158
|
|
|
164
159
|
// Assert
|
|
165
|
-
expect(viewer.captureCharFrame()).not.toContain("
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe("attribution", () => {
|
|
170
|
-
test("a collaborator's note shows their name in the card border", async () => {
|
|
171
|
-
// Arrange - a pulled collaborator note carries an author fingerprint
|
|
172
|
-
const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
|
|
173
|
-
const blocks = parseBlocks(PLAN);
|
|
174
|
-
const contextBlockIndex = blocks.findIndex((block) =>
|
|
175
|
-
block.text.startsWith("The daemon persists"),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
server.core.sessionAnnotate(session.id, {
|
|
179
|
-
id: "collab-1",
|
|
180
|
-
kind: "comment",
|
|
181
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
|
|
182
|
-
body: "who owns retries?",
|
|
183
|
-
author: "SHA256:1a2b3c4d5e6f",
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Act
|
|
187
|
-
const setup = await renderApp();
|
|
188
|
-
|
|
189
|
-
// Assert - the short author handle rides the card border
|
|
190
|
-
await waitForText(setup, "1a2b3c4d");
|
|
160
|
+
expect(viewer.captureCharFrame()).not.toContain(" edit ");
|
|
191
161
|
});
|
|
192
162
|
});
|
|
193
163
|
|
|
@@ -299,171 +269,20 @@ describe("compose newline convention", () => {
|
|
|
299
269
|
});
|
|
300
270
|
});
|
|
301
271
|
|
|
302
|
-
describe("the document selects, the rail edits", () => {
|
|
303
|
-
test("option+e edits the focused card in place; enter saves through the controller", async () => {
|
|
304
|
-
// Arrange
|
|
305
|
-
const setup = await renderApp();
|
|
306
|
-
|
|
307
|
-
await clickText(setup, "daemon");
|
|
308
|
-
await type(setup, "Needs a citation.");
|
|
309
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
310
|
-
await waitForText(setup, "COMMENT · me");
|
|
311
|
-
|
|
312
|
-
// Act: focus the card, then turn its body into an input in place
|
|
313
|
-
await pressKey(setup, "n", { meta: true });
|
|
314
|
-
await pressKey(setup, "e", { meta: true });
|
|
315
|
-
await type(setup, " And a link.");
|
|
316
|
-
await press(setup, "enter");
|
|
317
|
-
|
|
318
|
-
// Assert
|
|
319
|
-
await waitForState(
|
|
320
|
-
setup,
|
|
321
|
-
() =>
|
|
322
|
-
server.core.sessionGet(session.id).annotations[0]!.body === "Needs a citation. And a link.",
|
|
323
|
-
);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("activating a collaborator's card opens rename, not a body edit", async () => {
|
|
327
|
-
// Arrange - a pulled collaborator note carries an author fingerprint
|
|
328
|
-
const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
|
|
329
|
-
const blocks = parseBlocks(PLAN);
|
|
330
|
-
const contextBlockIndex = blocks.findIndex((block) =>
|
|
331
|
-
block.text.startsWith("The daemon persists"),
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
server.core.sessionAnnotate(session.id, {
|
|
335
|
-
id: "collab-1",
|
|
336
|
-
kind: "comment",
|
|
337
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 0, 10),
|
|
338
|
-
body: "who owns retries?",
|
|
339
|
-
author: "SHA256:1a2b3c4d5e6f",
|
|
340
|
-
});
|
|
341
|
-
const setup = await renderApp();
|
|
342
|
-
|
|
343
|
-
await waitForText(setup, "1a2b3c4d");
|
|
344
|
-
|
|
345
|
-
// Act - focus the collaborator card, then activate it
|
|
346
|
-
await pressKey(setup, "n", { meta: true });
|
|
347
|
-
await pressKey(setup, "e", { meta: true });
|
|
348
|
-
|
|
349
|
-
// Assert - the rename prompt opens; the body never becomes an editor
|
|
350
|
-
await waitForText(setup, "Rename author");
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
test("option+backspace deletes the focused card and un-paints the document mark", async () => {
|
|
354
|
-
// Arrange
|
|
355
|
-
const setup = await renderApp();
|
|
356
|
-
|
|
357
|
-
await dragText(setup, "daemon", "daemon persists", "daemon".length);
|
|
358
|
-
await type(setup, "Delete me.");
|
|
359
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
360
|
-
await waitForText(setup, "COMMENT · me");
|
|
361
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
362
|
-
await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
|
|
363
|
-
|
|
364
|
-
// Act
|
|
365
|
-
await pressKey(setup, "n", { meta: true });
|
|
366
|
-
await pressKey(setup, "BACKSPACE", { meta: true });
|
|
367
|
-
|
|
368
|
-
// Assert
|
|
369
|
-
await waitForTextGone(setup, "COMMENT · me");
|
|
370
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
371
|
-
expect(backgroundsOf(setup, "daemon")).not.toContain(THREAD_MARK);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe("plan cut removals", () => {
|
|
376
|
-
test("option+x cuts the caret's block into a rail removal card and option+u restores it", async () => {
|
|
377
|
-
// Arrange
|
|
378
|
-
const setup = await renderApp();
|
|
379
|
-
|
|
380
|
-
await clickText(setup, "daemon");
|
|
381
|
-
|
|
382
|
-
// Act - no card is focused, so the cut takes the block under the caret
|
|
383
|
-
await pressKey(setup, "x", { meta: true });
|
|
384
|
-
|
|
385
|
-
// Assert - the rail shows a removal card titled CUT · me for the cut block
|
|
386
|
-
await waitForText(setup, "CUT · me");
|
|
387
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy !== undefined);
|
|
388
|
-
|
|
389
|
-
// Act - undo restores it (no selection, so the last removal)
|
|
390
|
-
await pressKey(setup, "u", { meta: true });
|
|
391
|
-
|
|
392
|
-
// Assert - the block returns and the working copy is pristine again
|
|
393
|
-
await waitForText(setup, "removal restored");
|
|
394
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
describe("addressed annotations leave the open list", () => {
|
|
399
|
-
test("a revision that addresses one card hides it, un-paints it, and shows the count", async () => {
|
|
400
|
-
// Arrange: two annotations through the daemon, then a revision addressing one
|
|
401
|
-
const { makeAnchor, parseBlocks } = await import("@cueloop/schema");
|
|
402
|
-
const blocks = parseBlocks(PLAN);
|
|
403
|
-
const contextBlockIndex = blocks.findIndex((block) =>
|
|
404
|
-
block.text.startsWith("The daemon persists"),
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
server.core.sessionAnnotate(session.id, {
|
|
408
|
-
id: "a_settled",
|
|
409
|
-
kind: "comment",
|
|
410
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 0, 10), // "The daemon"
|
|
411
|
-
body: "settled note",
|
|
412
|
-
});
|
|
413
|
-
server.core.sessionAnnotate(session.id, {
|
|
414
|
-
id: "a_open",
|
|
415
|
-
kind: "comment",
|
|
416
|
-
anchor: makeAnchor(blocks, contextBlockIndex, 11, 19), // "persists"
|
|
417
|
-
body: "still open note",
|
|
418
|
-
});
|
|
419
|
-
server.core.sessionSubmitRevision(session.id, PLAN, ["a_settled"]);
|
|
420
|
-
|
|
421
|
-
// Act
|
|
422
|
-
const setup = await renderApp();
|
|
423
|
-
|
|
424
|
-
await waitForText(setup, "still open note");
|
|
425
|
-
|
|
426
|
-
// Assert: the open card renders, the addressed one is gone behind the count
|
|
427
|
-
const frame = setup.captureCharFrame();
|
|
428
|
-
|
|
429
|
-
expect(frame).toContain("✓ 1 addressed by revision");
|
|
430
|
-
expect(frame).not.toContain("settled note");
|
|
431
|
-
expect(frame).toContain("still open note"); // the open card survives; the addressed one does not
|
|
432
|
-
expect(backgroundsOf(setup, "The daemon")).not.toContain(DARK.markCommentBackground); // no highlight paint
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
272
|
describe("sheet header", () => {
|
|
437
|
-
test("
|
|
438
|
-
// Arrange
|
|
439
|
-
const setup = await renderApp();
|
|
440
|
-
|
|
441
|
-
// Assert
|
|
442
|
-
const frame = setup.captureCharFrame();
|
|
443
|
-
|
|
444
|
-
expect(frame).toContain("submitted by agent/worker-3");
|
|
445
|
-
expect(frame).toContain("rev 1");
|
|
446
|
-
expect(frame).toContain("Edit");
|
|
447
|
-
expect(frame).toContain("Share");
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test("the Agent tab shows the text-only harness launcher buttons", async () => {
|
|
273
|
+
test("mirrors the thread name and carries the Edit and Share word-buttons", async () => {
|
|
451
274
|
// Arrange
|
|
452
275
|
const setup = await renderApp();
|
|
453
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
454
|
-
const tabRow = lines.findIndex((line) => line.includes("Review") && line.includes("Agent"));
|
|
455
|
-
const tabColumn = lines[tabRow]!.indexOf("Agent");
|
|
456
|
-
|
|
457
|
-
// Act
|
|
458
|
-
await setup.mockMouse.click(tabColumn + 1, tabRow);
|
|
459
|
-
await setup.renderOnce();
|
|
460
276
|
|
|
461
|
-
// Assert - the
|
|
462
|
-
|
|
277
|
+
// Assert - the minimal header shows the thread title (mirror) plus the owner
|
|
278
|
+
// actions; the revision and submitter metadata are no longer in the header
|
|
463
279
|
const frame = setup.captureCharFrame();
|
|
464
280
|
|
|
465
|
-
expect(frame).toContain("
|
|
466
|
-
expect(frame).toContain("
|
|
281
|
+
expect(frame).toContain("Migration Plan");
|
|
282
|
+
expect(frame).not.toContain("submitted by");
|
|
283
|
+
expect(frame).not.toContain("rev 1");
|
|
284
|
+
expect(frame).toContain(" edit ");
|
|
285
|
+
expect(frame).toContain(" share ");
|
|
467
286
|
});
|
|
468
287
|
});
|
|
469
288
|
|
|
@@ -472,8 +291,9 @@ describe("quick-actions settings editor", () => {
|
|
|
472
291
|
// Arrange
|
|
473
292
|
const setup = await renderApp();
|
|
474
293
|
|
|
475
|
-
// Act - open Settings, enter Actions, expand the first action, type
|
|
476
|
-
await
|
|
294
|
+
// Act - open Settings from the top-left gear, enter Actions, expand the first action, type
|
|
295
|
+
await setup.mockMouse.click(1, 0);
|
|
296
|
+
await waitForText(setup, "Keybinds");
|
|
477
297
|
await clickText(setup, "Settings");
|
|
478
298
|
await clickText(setup, "Actions");
|
|
479
299
|
await clickText(setup, "Zoom out, research in depth");
|
|
@@ -502,15 +322,13 @@ describe("edit-exit reconciliation", () => {
|
|
|
502
322
|
await clickText(setup, "daemon");
|
|
503
323
|
await type(setup, "Anchor me to the doomed passage.");
|
|
504
324
|
await pressKey(setup, "RETURN", { meta: true });
|
|
505
|
-
await
|
|
325
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
506
326
|
|
|
507
327
|
// Act
|
|
508
328
|
await pressKey(setup, "e", { ctrl: true });
|
|
509
329
|
|
|
510
|
-
// Assert
|
|
330
|
+
// Assert - the thread view banner reports the orphaned annotation
|
|
511
331
|
await waitForText(setup, "1 annotation no longer match - the passage was removed.");
|
|
512
|
-
// the rail card flips to ORPHANED once the working-copy refresh lands
|
|
513
|
-
await waitForText(setup, "· ORPHANED");
|
|
514
332
|
// the annotation is NOT deleted: the feedback serializer handles orphans
|
|
515
333
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
516
334
|
} finally {
|
|
@@ -105,7 +105,7 @@ async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
describe("prototype review", () => {
|
|
108
|
-
test("click an element, comment on it, and it
|
|
108
|
+
test("click an element, comment on it, and it saves to the daemon", async () => {
|
|
109
109
|
// Arrange - wait until the fake browser produced its first screenshot, so
|
|
110
110
|
// the sheet is interactive before the click
|
|
111
111
|
const setup = await renderApp();
|
|
@@ -135,13 +135,6 @@ describe("prototype review", () => {
|
|
|
135
135
|
expect(stored.kind).toBe("comment");
|
|
136
136
|
expect(stored.body).toBe("tighten the padding");
|
|
137
137
|
expect(stored.anchor.selector).toBe("main > div.card");
|
|
138
|
-
|
|
139
|
-
// and it renders in the rail
|
|
140
|
-
await settle(setup);
|
|
141
|
-
const frame = setup.captureCharFrame();
|
|
142
|
-
|
|
143
|
-
expect(frame).toContain("tighten the padding");
|
|
144
|
-
expect(frame).toContain("COMMENT");
|
|
145
138
|
});
|
|
146
139
|
|
|
147
140
|
test("escape cancels the compose, matching the plan composer", async () => {
|