@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
|
@@ -9,7 +9,6 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import { DARK } from "./theme";
|
|
13
12
|
import {
|
|
14
13
|
clickText,
|
|
15
14
|
dragText,
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
17
16
|
isolateUserConfig,
|
|
18
17
|
pressKey,
|
|
19
18
|
typeText,
|
|
20
|
-
waitForState,
|
|
21
19
|
waitForText,
|
|
22
20
|
} from "./test-support";
|
|
23
21
|
|
|
@@ -83,15 +81,16 @@ function snapshot() {
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
describe("observer rendering", () => {
|
|
86
|
-
test("
|
|
84
|
+
test("the minimal header mirrors the thread name, with no observer badge", async () => {
|
|
87
85
|
// Arrange
|
|
88
86
|
const setup = await renderObserver();
|
|
89
87
|
|
|
90
|
-
// Assert
|
|
88
|
+
// Assert - the header shows the thread title only; read-only is enforced by
|
|
89
|
+
// the blocked primitives below, not a header badge
|
|
91
90
|
const frame = setup.captureCharFrame();
|
|
92
91
|
|
|
93
|
-
expect(frame).toContain("
|
|
94
|
-
expect(frame).toContain("
|
|
92
|
+
expect(frame).toContain("Migration Plan");
|
|
93
|
+
expect(frame).not.toContain("· observer");
|
|
95
94
|
});
|
|
96
95
|
});
|
|
97
96
|
|
|
@@ -199,29 +198,12 @@ describe("observer navigation still works", () => {
|
|
|
199
198
|
});
|
|
200
199
|
const setup = await renderObserver();
|
|
201
200
|
|
|
202
|
-
// Act
|
|
201
|
+
// Act - the observer navigates to the annotation without mutating anything
|
|
203
202
|
await pressKey(setup, "n", { meta: true });
|
|
204
203
|
await setup.renderOnce();
|
|
205
204
|
|
|
206
|
-
// Assert -
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await waitForState(setup, () => hasBorderColor(setup, DARK.blue));
|
|
205
|
+
// Assert - the observer reads the controller's comment inline in the thread
|
|
206
|
+
await waitForText(setup, "From the controller.");
|
|
207
|
+
expect(snapshot().annotations).toBe(1);
|
|
210
208
|
});
|
|
211
209
|
});
|
|
212
|
-
|
|
213
|
-
/** Whether any border character (box-drawing) is painted in the given foreground hex. */
|
|
214
|
-
function hasBorderColor(setup: Awaited<ReturnType<typeof renderObserver>>, hex: string): boolean {
|
|
215
|
-
for (const line of setup.captureSpans().lines) {
|
|
216
|
-
for (const span of line.spans) {
|
|
217
|
-
if (!/[╭╮╰╯│─┌┐└┘]/.test(span.text)) continue;
|
|
218
|
-
const [red, green, blue] = span.fg.toInts();
|
|
219
|
-
const rendered =
|
|
220
|
-
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
221
|
-
|
|
222
|
-
if (rendered === hex) return true;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The
|
|
1
|
+
/** The send-message confirm: cmd+enter opens a centered overlay with a verdict selector, summary input, and word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
@@ -83,29 +83,27 @@ function seedAnnotations(count: number): void {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
describe("
|
|
87
|
-
test("
|
|
86
|
+
describe("send message confirm", () => {
|
|
87
|
+
test("cmd+enter opens the send-message confirm overlay", async () => {
|
|
88
88
|
// Arrange
|
|
89
89
|
const setup = await renderApp();
|
|
90
90
|
|
|
91
|
-
// Assert
|
|
92
|
-
expect(setup.captureCharFrame()).toContain("
|
|
91
|
+
// Assert - the footer carries the send control before the overlay opens
|
|
92
|
+
expect(setup.captureCharFrame()).toContain("send message");
|
|
93
93
|
|
|
94
94
|
// Act
|
|
95
95
|
await pressKey(setup, "RETURN", { meta: true });
|
|
96
96
|
|
|
97
|
-
// Assert
|
|
98
|
-
await waitForText(setup, "
|
|
97
|
+
// Assert - the overlay: verdict selector and word-buttons
|
|
98
|
+
await waitForText(setup, "[Approve]"); // nothing pending: approve default
|
|
99
99
|
const frame = setup.captureCharFrame();
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
expect(frame).toContain("
|
|
103
|
-
expect(frame).toContain("[Approve]"); // nothing pending: approve default
|
|
104
|
-
expect(frame).toContain(" Submit ");
|
|
101
|
+
expect(frame).toContain("[Approve]");
|
|
102
|
+
expect(frame).toContain(" send message ");
|
|
105
103
|
expect(frame).toContain(" Cancel ");
|
|
106
104
|
});
|
|
107
105
|
|
|
108
|
-
test("left/right cycles the verdict selector in the
|
|
106
|
+
test("left/right cycles the verdict selector in the overlay", async () => {
|
|
109
107
|
// Arrange
|
|
110
108
|
const setup = await renderApp();
|
|
111
109
|
|
|
@@ -133,7 +131,7 @@ describe("rail submit confirm", () => {
|
|
|
133
131
|
await waitForText(setup, "[Changes]");
|
|
134
132
|
});
|
|
135
133
|
|
|
136
|
-
test("esc
|
|
134
|
+
test("esc closes the overlay", async () => {
|
|
137
135
|
// Arrange
|
|
138
136
|
const setup = await renderApp();
|
|
139
137
|
|
|
@@ -145,15 +143,13 @@ describe("rail submit confirm", () => {
|
|
|
145
143
|
// Act
|
|
146
144
|
await press(setup, "escape");
|
|
147
145
|
|
|
148
|
-
// Assert
|
|
149
|
-
// a bare ESC settles after the parser's escape-sequence window
|
|
146
|
+
// Assert - a bare ESC settles after the parser's escape-sequence window
|
|
150
147
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
151
148
|
|
|
152
|
-
expect(frame).not.toContain("
|
|
153
|
-
expect(frame).toContain("Submit review");
|
|
149
|
+
expect(frame).not.toContain(" Cancel ");
|
|
154
150
|
});
|
|
155
151
|
|
|
156
|
-
test("enter in the
|
|
152
|
+
test("enter in the overlay resolves the session through the controller", async () => {
|
|
157
153
|
// Arrange
|
|
158
154
|
seedAnnotations(1);
|
|
159
155
|
const setup = await renderApp();
|
|
@@ -163,16 +159,12 @@ describe("rail submit confirm", () => {
|
|
|
163
159
|
|
|
164
160
|
// Assert
|
|
165
161
|
await waitForText(setup, "[Changes]"); // pending items: request changes default
|
|
166
|
-
const frame = setup.captureCharFrame();
|
|
167
|
-
|
|
168
|
-
expect(frame).toContain("submit review");
|
|
169
162
|
|
|
170
163
|
// Act
|
|
171
164
|
await setup.mockInput.typeText("Tighten the steps.");
|
|
172
165
|
await press(setup, "enter");
|
|
173
166
|
|
|
174
|
-
// Assert
|
|
175
|
-
// the completion flow after submit is unchanged
|
|
167
|
+
// Assert - the completion flow after submit is unchanged
|
|
176
168
|
await waitForText(setup, "feedback sent");
|
|
177
169
|
const stored = server.core.sessionGet(session.id);
|
|
178
170
|
|
|
@@ -180,65 +172,20 @@ describe("rail submit confirm", () => {
|
|
|
180
172
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
181
173
|
});
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
function railText(frame: string): string {
|
|
185
|
-
const rows = frame.split("\n");
|
|
186
|
-
const railColumn = rows.find((row) => row.includes("Review Tree Agent"))!.indexOf("│");
|
|
187
|
-
|
|
188
|
-
return rows.map((row) => row.slice(railColumn)).join("\n");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
test("the annotation stack stays scrollable while the card is open", async () => {
|
|
192
|
-
// Arrange
|
|
193
|
-
seedAnnotations(12);
|
|
194
|
-
const setup = await renderApp();
|
|
195
|
-
|
|
196
|
-
// Act
|
|
197
|
-
// walk card focus to the last one: the rail scrolls it into view
|
|
198
|
-
for (let index = 0; index < 12; index++) await pressKey(setup, "n", { meta: true });
|
|
199
|
-
|
|
200
|
-
// Assert
|
|
201
|
-
await waitForText(setup, "note 12");
|
|
202
|
-
expect(railText(setup.captureCharFrame())).not.toContain("note 01");
|
|
203
|
-
|
|
204
|
-
// Act
|
|
205
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
206
|
-
|
|
207
|
-
// Assert
|
|
208
|
-
await waitForText(setup, "[Changes]");
|
|
209
|
-
// the card is pinned outside the scrollbox; the stack stays scrolled off the top
|
|
210
|
-
expect(railText(setup.captureCharFrame())).not.toContain("note 01");
|
|
211
|
-
|
|
212
|
-
// Act
|
|
213
|
-
// the stack still scrolls with the card open: wheel over the rail
|
|
214
|
-
// brings the last card back into view while the confirm card stays put
|
|
215
|
-
for (let turn = 0; turn < 12; turn++) await setup.mockMouse.scroll(100, 10, "down");
|
|
216
|
-
await settle(setup);
|
|
217
|
-
|
|
218
|
-
// Assert
|
|
219
|
-
await waitForText(setup, "note 12");
|
|
220
|
-
const scrolledWithCard = railText(setup.captureCharFrame());
|
|
221
|
-
|
|
222
|
-
expect(scrolledWithCard).toContain("submit review");
|
|
223
|
-
expect(scrolledWithCard).not.toContain("note 01");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("read-only observers never see the confirm card", async () => {
|
|
175
|
+
test("read-only observers cannot open the confirm overlay", async () => {
|
|
227
176
|
// Arrange
|
|
228
177
|
seedAnnotations(1);
|
|
229
178
|
const setup = await renderApp({ readOnly: true });
|
|
230
179
|
|
|
231
180
|
// Act
|
|
232
181
|
await pressKey(setup, "RETURN", { meta: true });
|
|
182
|
+
await settle(setup);
|
|
233
183
|
|
|
234
|
-
// Assert
|
|
235
|
-
await waitForText(setup, "observer - read-only");
|
|
184
|
+
// Assert - no overlay, and the session stays pending
|
|
236
185
|
const frame = setup.captureCharFrame();
|
|
237
186
|
|
|
238
|
-
expect(frame).toContain("observer - read-only");
|
|
239
|
-
expect(frame).not.toContain("1 annotations");
|
|
240
187
|
expect(frame).not.toContain("[Changes]");
|
|
241
|
-
expect(
|
|
188
|
+
expect(frame).not.toContain(" Cancel ");
|
|
242
189
|
expect(server.core.sessionGet(session.id).status).toBe("pending");
|
|
243
190
|
});
|
|
244
191
|
});
|
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,
|
|
@@ -71,7 +72,7 @@ async function renderApp(sessionId?: string) {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
describe("plan rendering", () => {
|
|
74
|
-
test("renders the plan with headings, list markers, and the
|
|
75
|
+
test("renders the plan with headings, list markers, and the footer", async () => {
|
|
75
76
|
// Arrange
|
|
76
77
|
const setup = await renderApp();
|
|
77
78
|
|
|
@@ -82,8 +83,22 @@ describe("plan rendering", () => {
|
|
|
82
83
|
expect(frame).toContain("Context");
|
|
83
84
|
expect(frame).toContain("persists sessions to disk atomically");
|
|
84
85
|
expect(frame).toContain("· move the store");
|
|
85
|
-
expect(frame).toContain("
|
|
86
|
-
|
|
86
|
+
expect(frame).toContain("send message");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a direct open still populates the Threads sidebar with other pending reviews", async () => {
|
|
90
|
+
// Arrange - a second pending review exists alongside the one opened directly
|
|
91
|
+
server.core.sessionCreate({
|
|
92
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
93
|
+
artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
|
|
94
|
+
});
|
|
95
|
+
const setup = await renderApp();
|
|
96
|
+
|
|
97
|
+
// Act - open the collapsed Threads sidebar (the mirrored panel toggle by the gear)
|
|
98
|
+
await setup.mockMouse.click(4, 0);
|
|
99
|
+
|
|
100
|
+
// Assert - the sidebar lists the other pending review, so you can jump to it
|
|
101
|
+
await waitForText(setup, "Other Plan");
|
|
87
102
|
});
|
|
88
103
|
});
|
|
89
104
|
|
|
@@ -113,7 +128,7 @@ describe("thread view grammar", () => {
|
|
|
113
128
|
await pressKey(setup, "RETURN", { meta: true });
|
|
114
129
|
|
|
115
130
|
// Assert
|
|
116
|
-
await
|
|
131
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
117
132
|
const stored = server.core.sessionGet(session.id);
|
|
118
133
|
|
|
119
134
|
expect(stored.annotations.length).toBe(1);
|
|
@@ -144,9 +159,11 @@ describe("thread view grammar", () => {
|
|
|
144
159
|
// Act
|
|
145
160
|
await pressKey(setup, "x", { meta: true });
|
|
146
161
|
|
|
147
|
-
// Assert - the
|
|
148
|
-
await
|
|
149
|
-
|
|
162
|
+
// Assert - the cut lands in the working copy
|
|
163
|
+
await waitForState(
|
|
164
|
+
setup,
|
|
165
|
+
() => !(server.core.sessionGet(session.id).workingCopy ?? "").includes("move the store"),
|
|
166
|
+
);
|
|
150
167
|
|
|
151
168
|
// Act
|
|
152
169
|
await pressKey(setup, "x", { meta: true });
|
|
@@ -227,51 +244,105 @@ describe("submit", () => {
|
|
|
227
244
|
});
|
|
228
245
|
});
|
|
229
246
|
|
|
230
|
-
describe("
|
|
231
|
-
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 () => {
|
|
232
249
|
// Arrange
|
|
233
250
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
234
251
|
|
|
235
252
|
await waitForText(setup, "cueloop");
|
|
236
253
|
|
|
237
|
-
// Assert - the same header
|
|
254
|
+
// Assert - the same shell header, the disposable Welcome tab, and the pending thread
|
|
238
255
|
const frame = setup.captureCharFrame();
|
|
239
256
|
|
|
240
|
-
expect(frame).toContain("
|
|
257
|
+
expect(frame).toContain("Welcome to cueloop");
|
|
241
258
|
expect(frame).toContain("Migration Plan");
|
|
242
|
-
expect(frame).toContain("
|
|
243
|
-
expect(frame).not.toContain("inbox ("); // the old inline header is gone
|
|
259
|
+
expect(frame).not.toContain("· resume"); // the bespoke inbox header is retired
|
|
244
260
|
|
|
245
261
|
// Act
|
|
246
262
|
await press(setup, "enter");
|
|
247
263
|
|
|
248
264
|
// Assert
|
|
249
|
-
await waitForText(setup, "
|
|
265
|
+
await waitForText(setup, "send message");
|
|
266
|
+
});
|
|
267
|
+
|
|
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");
|
|
250
290
|
});
|
|
251
291
|
|
|
252
|
-
test("the
|
|
292
|
+
test("the row kebab menu pins a thread into a Pinned section", async () => {
|
|
253
293
|
// Arrange
|
|
254
294
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
255
295
|
|
|
256
|
-
await waitForText(setup, "
|
|
257
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
258
|
-
const menuRow = lines.findIndex((line) => line.includes("menu"));
|
|
259
|
-
const menuColumn = lines[menuRow]!.indexOf("menu");
|
|
296
|
+
await waitForText(setup, "Migration Plan");
|
|
260
297
|
|
|
261
|
-
// Act -
|
|
262
|
-
|
|
298
|
+
// Act - hover the row to reveal its kebab, open the menu, and pick Pin
|
|
299
|
+
const row = locateText(setup, "Migration Plan");
|
|
263
300
|
|
|
264
|
-
|
|
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 () => {
|
|
327
|
+
// Arrange
|
|
328
|
+
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
329
|
+
|
|
330
|
+
await waitForText(setup, "Welcome to cueloop");
|
|
331
|
+
|
|
332
|
+
// Act - open the settings dialog from the top-left gear (the Threads panel header, row 0)
|
|
333
|
+
await setup.mockMouse.click(1, 0);
|
|
334
|
+
|
|
335
|
+
// Assert - the settings dialog with its Keybinds leaf appears
|
|
265
336
|
await waitForText(setup, "Keybinds");
|
|
266
337
|
expect(setup.captureCharFrame()).toContain("Settings");
|
|
267
338
|
|
|
268
|
-
// 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
|
|
269
340
|
await press(setup, "escape");
|
|
270
341
|
await waitForState(setup, () => !setup.captureCharFrame().includes("Keybinds"));
|
|
271
342
|
await press(setup, "enter");
|
|
272
343
|
|
|
273
344
|
// Assert
|
|
274
|
-
await waitForText(setup, "
|
|
345
|
+
await waitForText(setup, "send message");
|
|
275
346
|
});
|
|
276
347
|
});
|
|
277
348
|
|
|
@@ -281,12 +352,9 @@ describe("the thread view and the menu", () => {
|
|
|
281
352
|
const setup = await renderApp();
|
|
282
353
|
|
|
283
354
|
await waitForText(setup, "The daemon persists");
|
|
284
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
285
|
-
const menuRow = lines.findIndex((line) => line.includes("menu"));
|
|
286
|
-
const menuColumn = lines[menuRow]!.indexOf("menu");
|
|
287
355
|
|
|
288
|
-
// Act - open the menu, then the keybinds dialog
|
|
289
|
-
await setup.mockMouse.click(
|
|
356
|
+
// Act - open the menu from the top-left settings gear, then the keybinds dialog
|
|
357
|
+
await setup.mockMouse.click(1, 0);
|
|
290
358
|
await waitForText(setup, "Keybinds");
|
|
291
359
|
const dropUp = setup.captureCharFrame().split("\n");
|
|
292
360
|
const keybindsRow = dropUp.findIndex((line) => line.includes("Keybinds"));
|
|
@@ -298,7 +366,7 @@ describe("the thread view and the menu", () => {
|
|
|
298
366
|
const dialog = setup.captureCharFrame();
|
|
299
367
|
|
|
300
368
|
expect(dialog).toContain("⌘⌥m");
|
|
301
|
-
expect(dialog).toContain("
|
|
369
|
+
expect(dialog).toContain("place the caret");
|
|
302
370
|
expect(dialog).not.toContain("grow/shrink");
|
|
303
371
|
|
|
304
372
|
// Act - a printable behind the dialog must not open a comment; escape closes it
|
|
@@ -307,7 +375,44 @@ describe("the thread view and the menu", () => {
|
|
|
307
375
|
await press(setup, "escape");
|
|
308
376
|
|
|
309
377
|
// Assert
|
|
310
|
-
await waitForState(setup, () => !setup.captureCharFrame().includes("
|
|
378
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("mark text, across blocks"));
|
|
311
379
|
expect(setup.captureCharFrame()).not.toContain("● x");
|
|
312
380
|
});
|
|
381
|
+
|
|
382
|
+
test("clicking the Settings nav group is inert and never corrupts the active category", async () => {
|
|
383
|
+
// Arrange - open the settings dialog from the gear
|
|
384
|
+
const setup = await renderApp();
|
|
385
|
+
|
|
386
|
+
await setup.mockMouse.click(1, 0);
|
|
387
|
+
await waitForText(setup, "Keybinds");
|
|
388
|
+
|
|
389
|
+
// the nav folder is the last "Settings" on screen (the first is the dialog title)
|
|
390
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
391
|
+
let folderRow = -1;
|
|
392
|
+
let folderColumn = -1;
|
|
393
|
+
|
|
394
|
+
lines.forEach((line, row) => {
|
|
395
|
+
const column = line.indexOf("Settings");
|
|
396
|
+
|
|
397
|
+
if (column !== -1) {
|
|
398
|
+
folderColumn = column;
|
|
399
|
+
folderRow = row;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Act - click the synthetic Settings group folder, then press a nav key. The
|
|
404
|
+
// key handler dereferences the active category, so a corrupt id would throw
|
|
405
|
+
// (the global key handler swallows the throw into console.error).
|
|
406
|
+
const keyHandlerErrors: string[] = [];
|
|
407
|
+
const originalConsoleError = console.error;
|
|
408
|
+
|
|
409
|
+
console.error = (...args: unknown[]) => keyHandlerErrors.push(args.map(String).join(" "));
|
|
410
|
+
await setup.mockMouse.click(folderColumn + 1, folderRow);
|
|
411
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
412
|
+
console.error = originalConsoleError;
|
|
413
|
+
|
|
414
|
+
// Assert - no key-handler crash, and the dialog is still up
|
|
415
|
+
expect(keyHandlerErrors.filter((line) => line.includes("keypress handler"))).toEqual([]);
|
|
416
|
+
expect(setup.captureCharFrame()).toContain("Keybinds");
|
|
417
|
+
});
|
|
313
418
|
});
|