@cueloop/client 0.1.0-alpha.66 → 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 +71 -39
- package/src/App.plan-review.test.tsx +20 -21
- package/src/App.readonly.test.tsx +7 -3
- package/src/App.test.tsx +73 -14
- package/src/App.tsx +375 -129
- package/src/App.walk.test.tsx +14 -15
- 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 +176 -45
- package/src/app-view-model.ts +7 -65
- 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 +47 -42
- package/src/components/AppShell.tsx +212 -100
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/ChangesColumn.tsx +49 -26
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +55 -53
- 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.tsx +159 -30
- 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.tsx +8 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.tsx +2 -1
- 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 +2 -2
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +12 -7
- package/src/components/ThreadView.tsx +62 -1440
- package/src/components/ThreadsSidebar.tsx +14 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +509 -559
- 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 +15 -0
- package/src/components/file-tree.ts +59 -4
- package/src/components/pages/Workspace.stories.tsx +2 -2
- package/src/components/primitives/IconButton.tsx +21 -2
- package/src/components/primitives/Tree.tsx +13 -4
- package/src/components/primitives/icons.ts +27 -4
- package/src/components/session-tree.test.ts +20 -0
- package/src/components/session-tree.ts +16 -2
- package/src/components/stories-app.tsx +2 -2
- 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 +153 -29
- 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/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 +14 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- 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
|
-
const stored = server.core.sessionGet(session.id);
|
|
101
|
+
await waitForText(setup, "feedback sent");
|
|
102
|
+
const resolved = server.core.sessionGet(session.id);
|
|
91
103
|
|
|
92
|
-
expect(
|
|
93
|
-
expect(
|
|
104
|
+
expect(resolved.verdict!.feedback).toContain("new Map()");
|
|
105
|
+
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
106
|
+
});
|
|
107
|
+
|
|
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");
|
|
@@ -99,14 +99,13 @@ describe("share button", () => {
|
|
|
99
99
|
const setup = await renderApp();
|
|
100
100
|
|
|
101
101
|
// Assert
|
|
102
|
-
// both word-buttons ride the
|
|
102
|
+
// both word-buttons ride the header's thread segment, so share sits next to edit
|
|
103
103
|
const headerLine = setup
|
|
104
104
|
.captureCharFrame()
|
|
105
105
|
.split("\n")
|
|
106
|
-
.find((line) => line.includes("
|
|
106
|
+
.find((line) => line.includes("edit"));
|
|
107
107
|
|
|
108
|
-
expect(headerLine).toContain("
|
|
109
|
-
expect(headerLine).toContain("Share");
|
|
108
|
+
expect(headerLine).toContain("share");
|
|
110
109
|
});
|
|
111
110
|
|
|
112
111
|
test("a read-only viewer (a plan shared over ssh) never sees the Share button", async () => {
|
|
@@ -119,7 +118,7 @@ describe("share button", () => {
|
|
|
119
118
|
await waitForText(viewer, "cueloop");
|
|
120
119
|
|
|
121
120
|
// Assert
|
|
122
|
-
expect(viewer.captureCharFrame()).not.toContain("
|
|
121
|
+
expect(viewer.captureCharFrame()).not.toContain(" share ");
|
|
123
122
|
});
|
|
124
123
|
|
|
125
124
|
test("a resolved plan hides Edit and Share (no re-sharing a finished review)", async () => {
|
|
@@ -129,16 +128,13 @@ describe("share button", () => {
|
|
|
129
128
|
// Act
|
|
130
129
|
const setup = await renderApp();
|
|
131
130
|
|
|
132
|
-
await waitForText(setup, "
|
|
131
|
+
await waitForText(setup, "Migration Plan");
|
|
133
132
|
|
|
134
133
|
// Assert - the owner toolbar is gone once the review is resolved
|
|
135
|
-
const
|
|
136
|
-
.captureCharFrame()
|
|
137
|
-
.split("\n")
|
|
138
|
-
.find((line) => line.includes("submitted by"));
|
|
134
|
+
const frame = setup.captureCharFrame();
|
|
139
135
|
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
136
|
+
expect(frame).not.toContain(" edit ");
|
|
137
|
+
expect(frame).not.toContain(" share ");
|
|
142
138
|
});
|
|
143
139
|
});
|
|
144
140
|
|
|
@@ -148,7 +144,7 @@ describe("edit affordance", () => {
|
|
|
148
144
|
const owner = await renderApp();
|
|
149
145
|
|
|
150
146
|
// Assert
|
|
151
|
-
expect(owner.captureCharFrame()).toContain("
|
|
147
|
+
expect(owner.captureCharFrame()).toContain(" edit ");
|
|
152
148
|
});
|
|
153
149
|
|
|
154
150
|
test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
|
|
@@ -161,7 +157,7 @@ describe("edit affordance", () => {
|
|
|
161
157
|
await waitForText(viewer, "cueloop");
|
|
162
158
|
|
|
163
159
|
// Assert
|
|
164
|
-
expect(viewer.captureCharFrame()).not.toContain("
|
|
160
|
+
expect(viewer.captureCharFrame()).not.toContain(" edit ");
|
|
165
161
|
});
|
|
166
162
|
});
|
|
167
163
|
|
|
@@ -274,17 +270,19 @@ describe("compose newline convention", () => {
|
|
|
274
270
|
});
|
|
275
271
|
|
|
276
272
|
describe("sheet header", () => {
|
|
277
|
-
test("
|
|
273
|
+
test("mirrors the thread name and carries the Edit and Share word-buttons", async () => {
|
|
278
274
|
// Arrange
|
|
279
275
|
const setup = await renderApp();
|
|
280
276
|
|
|
281
|
-
// Assert
|
|
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
|
|
282
279
|
const frame = setup.captureCharFrame();
|
|
283
280
|
|
|
284
|
-
expect(frame).toContain("
|
|
285
|
-
expect(frame).toContain("
|
|
286
|
-
expect(frame).toContain("
|
|
287
|
-
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 ");
|
|
288
286
|
});
|
|
289
287
|
});
|
|
290
288
|
|
|
@@ -294,7 +292,8 @@ describe("quick-actions settings editor", () => {
|
|
|
294
292
|
const setup = await renderApp();
|
|
295
293
|
|
|
296
294
|
// Act - open Settings from the top-left gear, enter Actions, expand the first action, type
|
|
297
|
-
await setup.mockMouse.click(1,
|
|
295
|
+
await setup.mockMouse.click(1, 0);
|
|
296
|
+
await waitForText(setup, "Keybinds");
|
|
298
297
|
await clickText(setup, "Settings");
|
|
299
298
|
await clickText(setup, "Actions");
|
|
300
299
|
await clickText(setup, "Zoom out, research in depth");
|
|
@@ -81,12 +81,16 @@ function snapshot() {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
describe("observer rendering", () => {
|
|
84
|
-
test("
|
|
84
|
+
test("the minimal header mirrors the thread name, with no observer badge", async () => {
|
|
85
85
|
// Arrange
|
|
86
86
|
const setup = await renderObserver();
|
|
87
87
|
|
|
88
|
-
// Assert
|
|
89
|
-
|
|
88
|
+
// Assert - the header shows the thread title only; read-only is enforced by
|
|
89
|
+
// the blocked primitives below, not a header badge
|
|
90
|
+
const frame = setup.captureCharFrame();
|
|
91
|
+
|
|
92
|
+
expect(frame).toContain("Migration Plan");
|
|
93
|
+
expect(frame).not.toContain("· observer");
|
|
90
94
|
});
|
|
91
95
|
});
|
|
92
96
|
|
package/src/App.test.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
dragText,
|
|
15
15
|
frameRow,
|
|
16
16
|
isolateUserConfig,
|
|
17
|
+
locateText,
|
|
17
18
|
press,
|
|
18
19
|
pressKey,
|
|
19
20
|
typeText as type,
|
|
@@ -94,7 +95,7 @@ describe("plan rendering", () => {
|
|
|
94
95
|
const setup = await renderApp();
|
|
95
96
|
|
|
96
97
|
// Act - open the collapsed Threads sidebar (the mirrored panel toggle by the gear)
|
|
97
|
-
await setup.mockMouse.click(
|
|
98
|
+
await setup.mockMouse.click(4, 0);
|
|
98
99
|
|
|
99
100
|
// Assert - the sidebar lists the other pending review, so you can jump to it
|
|
100
101
|
await waitForText(setup, "Other Plan");
|
|
@@ -243,19 +244,19 @@ describe("submit", () => {
|
|
|
243
244
|
});
|
|
244
245
|
});
|
|
245
246
|
|
|
246
|
-
describe("
|
|
247
|
-
test("
|
|
247
|
+
describe("no-thread shell", () => {
|
|
248
|
+
test("opening with nothing selected lands in the shell with a Welcome tab and the Threads sidebar", async () => {
|
|
248
249
|
// Arrange
|
|
249
250
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
250
251
|
|
|
251
252
|
await waitForText(setup, "cueloop");
|
|
252
253
|
|
|
253
|
-
// Assert - the same header
|
|
254
|
+
// Assert - the same shell header, the disposable Welcome tab, and the pending thread
|
|
254
255
|
const frame = setup.captureCharFrame();
|
|
255
256
|
|
|
256
|
-
expect(frame).toContain("
|
|
257
|
+
expect(frame).toContain("Welcome to cueloop");
|
|
257
258
|
expect(frame).toContain("Migration Plan");
|
|
258
|
-
expect(frame).not.toContain("
|
|
259
|
+
expect(frame).not.toContain("· resume"); // the bespoke inbox header is retired
|
|
259
260
|
|
|
260
261
|
// Act
|
|
261
262
|
await press(setup, "enter");
|
|
@@ -264,20 +265,78 @@ describe("inbox", () => {
|
|
|
264
265
|
await waitForText(setup, "send message");
|
|
265
266
|
});
|
|
266
267
|
|
|
267
|
-
test("
|
|
268
|
+
test("picking a thread from the no-thread shell keeps the Threads sidebar open", async () => {
|
|
269
|
+
// Arrange - a second pending thread so the sidebar has a row that never shows
|
|
270
|
+
// in the opened thread's own view
|
|
271
|
+
server.core.sessionCreate({
|
|
272
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
273
|
+
artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
|
|
274
|
+
});
|
|
275
|
+
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
276
|
+
|
|
277
|
+
await waitForText(setup, "Welcome to cueloop");
|
|
278
|
+
await waitForText(setup, "Other Plan"); // the sidebar opens by default here
|
|
279
|
+
|
|
280
|
+
// Act - open the thread under the cursor
|
|
281
|
+
await press(setup, "enter");
|
|
282
|
+
await waitForText(setup, "send message");
|
|
283
|
+
|
|
284
|
+
// Assert - the sidebar stayed open across the swap: both titles are on screen,
|
|
285
|
+
// and the non-opened one can only come from the still-open sidebar
|
|
286
|
+
const frame = setup.captureCharFrame();
|
|
287
|
+
|
|
288
|
+
expect(frame).toContain("Migration Plan");
|
|
289
|
+
expect(frame).toContain("Other Plan");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("the row kebab menu pins a thread into a Pinned section", async () => {
|
|
293
|
+
// Arrange
|
|
294
|
+
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
295
|
+
|
|
296
|
+
await waitForText(setup, "Migration Plan");
|
|
297
|
+
|
|
298
|
+
// Act - hover the row to reveal its kebab, open the menu, and pick Pin
|
|
299
|
+
const row = locateText(setup, "Migration Plan");
|
|
300
|
+
|
|
301
|
+
await setup.mockMouse.moveTo(row.column, row.row);
|
|
302
|
+
await waitForText(setup, "⋮");
|
|
303
|
+
await clickText(setup, "⋮");
|
|
304
|
+
await waitForText(setup, "Pin");
|
|
305
|
+
await clickText(setup, "Pin");
|
|
306
|
+
|
|
307
|
+
// Assert - a Pinned section now holds the thread
|
|
308
|
+
await waitForText(setup, "Pinned");
|
|
309
|
+
expect(frameRow(setup, "Migration Plan")).toBeGreaterThan(frameRow(setup, "Pinned"));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("a bare launch shows the Welcome surface with the right region collapsed", async () => {
|
|
313
|
+
// Arrange
|
|
314
|
+
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
315
|
+
|
|
316
|
+
await waitForText(setup, "Welcome to cueloop");
|
|
317
|
+
|
|
318
|
+
// Assert - Welcome fills the thread pane (not a Changes tab); the right region stays collapsed
|
|
319
|
+
const frame = setup.captureCharFrame();
|
|
320
|
+
|
|
321
|
+
expect(frame).toContain("Welcome to cueloop");
|
|
322
|
+
expect(frame).not.toContain("Changes"); // the Changes editor is closed on a bare launch
|
|
323
|
+
expect(frame).toContain("Migration Plan"); // the sidebar lists the pending thread
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("the menu opens from the shell gear and escape is not a trap", async () => {
|
|
268
327
|
// Arrange
|
|
269
328
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
270
329
|
|
|
271
|
-
await waitForText(setup, "
|
|
330
|
+
await waitForText(setup, "Welcome to cueloop");
|
|
272
331
|
|
|
273
|
-
// Act - open the
|
|
274
|
-
await setup.mockMouse.click(1,
|
|
332
|
+
// Act - open the settings dialog from the top-left gear (the Threads panel header, row 0)
|
|
333
|
+
await setup.mockMouse.click(1, 0);
|
|
275
334
|
|
|
276
|
-
// Assert - the
|
|
335
|
+
// Assert - the settings dialog with its Keybinds leaf appears
|
|
277
336
|
await waitForText(setup, "Keybinds");
|
|
278
337
|
expect(setup.captureCharFrame()).toContain("Settings");
|
|
279
338
|
|
|
280
|
-
// Act - escape closes the menu (not a trap), and the
|
|
339
|
+
// Act - escape closes the menu (not a trap), and the thread nav still works
|
|
281
340
|
await press(setup, "escape");
|
|
282
341
|
await waitForState(setup, () => !setup.captureCharFrame().includes("Keybinds"));
|
|
283
342
|
await press(setup, "enter");
|
|
@@ -295,7 +354,7 @@ describe("the thread view and the menu", () => {
|
|
|
295
354
|
await waitForText(setup, "The daemon persists");
|
|
296
355
|
|
|
297
356
|
// Act - open the menu from the top-left settings gear, then the keybinds dialog
|
|
298
|
-
await setup.mockMouse.click(1,
|
|
357
|
+
await setup.mockMouse.click(1, 0);
|
|
299
358
|
await waitForText(setup, "Keybinds");
|
|
300
359
|
const dropUp = setup.captureCharFrame().split("\n");
|
|
301
360
|
const keybindsRow = dropUp.findIndex((line) => line.includes("Keybinds"));
|
|
@@ -324,7 +383,7 @@ describe("the thread view and the menu", () => {
|
|
|
324
383
|
// Arrange - open the settings dialog from the gear
|
|
325
384
|
const setup = await renderApp();
|
|
326
385
|
|
|
327
|
-
await setup.mockMouse.click(1,
|
|
386
|
+
await setup.mockMouse.click(1, 0);
|
|
328
387
|
await waitForText(setup, "Keybinds");
|
|
329
388
|
|
|
330
389
|
// the nav folder is the last "Settings" on screen (the first is the dialog title)
|