@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
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/** The review panel end to end: the b key cycles expanded -> compact -> hidden -> expanded on the live App, and the collapse mode persists to the user config so it survives a restart. Rendering details are locked by the ReviewPanel stories; this suite proves the App wiring and the persistence. */
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
-
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
-
import type { ReviewSession } from "@cueloop/schema";
|
|
11
|
-
import { App } from "./App";
|
|
12
|
-
import { loadConfig } from "./config";
|
|
13
|
-
import { isolateUserConfig, settle, waitForText, waitForTextGone, pressKey } from "./test-support";
|
|
14
|
-
|
|
15
|
-
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
|
-
|
|
17
|
-
let home: string;
|
|
18
|
-
let server: DaemonServer;
|
|
19
|
-
let session: ReviewSession;
|
|
20
|
-
let configPath: string;
|
|
21
|
-
let restoreUserConfig: () => void;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
home = mkdtempSync(join(tmpdir(), "cueloop-panel-"));
|
|
25
|
-
configPath = join(home, "config.toml");
|
|
26
|
-
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
27
|
-
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
28
|
-
server.start();
|
|
29
|
-
session = server.core.sessionCreate({
|
|
30
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
31
|
-
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
restoreUserConfig();
|
|
36
|
-
server.stop();
|
|
37
|
-
rmSync(home, { recursive: true, force: true });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
async function renderApp() {
|
|
41
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
42
|
-
width: 120,
|
|
43
|
-
height: 30,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
await waitForText(setup, "Review");
|
|
47
|
-
|
|
48
|
-
return setup;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("review panel", () => {
|
|
52
|
-
test("ctrl+r cycles expanded -> compact -> hidden -> expanded", async () => {
|
|
53
|
-
// Arrange
|
|
54
|
-
const setup = await renderApp();
|
|
55
|
-
// each mode has its own signature: expanded draws the tab box and the
|
|
56
|
-
// submit button, compact keeps only the count strip, hidden draws nothing
|
|
57
|
-
const railSignature = () => {
|
|
58
|
-
const frame = setup.captureCharFrame();
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
tabs: frame.includes("Review Tree Agent"),
|
|
62
|
-
submit: frame.includes("Submit review"),
|
|
63
|
-
strip: /\s0\s*\n/.test(frame) && frame.includes("<"),
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Assert: expanded
|
|
68
|
-
expect(railSignature()).toEqual({ tabs: true, submit: true, strip: false });
|
|
69
|
-
|
|
70
|
-
// Act
|
|
71
|
-
await pressKey(setup, "r", { ctrl: true });
|
|
72
|
-
|
|
73
|
-
// Assert: compact drops the tabs and the button, keeps the strip
|
|
74
|
-
await waitForTextGone(setup, "Submit review");
|
|
75
|
-
expect(railSignature()).toEqual({ tabs: false, submit: false, strip: true });
|
|
76
|
-
|
|
77
|
-
// Act
|
|
78
|
-
await pressKey(setup, "r", { ctrl: true });
|
|
79
|
-
await settle(setup);
|
|
80
|
-
|
|
81
|
-
// Assert: hidden draws no rail at all
|
|
82
|
-
expect(railSignature()).toEqual({ tabs: false, submit: false, strip: false });
|
|
83
|
-
|
|
84
|
-
// Act
|
|
85
|
-
await pressKey(setup, "r", { ctrl: true });
|
|
86
|
-
|
|
87
|
-
// Assert
|
|
88
|
-
await waitForText(setup, "Submit review"); // back to expanded
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("the collapse mode persists to the user config", async () => {
|
|
92
|
-
// Arrange
|
|
93
|
-
const setup = await renderApp();
|
|
94
|
-
|
|
95
|
-
// Act
|
|
96
|
-
await pressKey(setup, "r", { ctrl: true }); // -> compact
|
|
97
|
-
|
|
98
|
-
// Assert
|
|
99
|
-
expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("compact");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("option+w widens the rail and persists the new width", async () => {
|
|
103
|
-
// Arrange
|
|
104
|
-
const setup = await renderApp();
|
|
105
|
-
const startWidth = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
|
|
106
|
-
|
|
107
|
-
// Act
|
|
108
|
-
await pressKey(setup, "w", { meta: true });
|
|
109
|
-
|
|
110
|
-
// Assert
|
|
111
|
-
const widened = loadConfig({ userConfigPath: configPath }).ui.reviewWidth;
|
|
112
|
-
|
|
113
|
-
expect(widened).toBeGreaterThan(startWidth);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("submitting from a hidden panel force-opens the rail so the confirm card is reachable", async () => {
|
|
117
|
-
// Arrange
|
|
118
|
-
const setup = await renderApp();
|
|
119
|
-
|
|
120
|
-
await pressKey(setup, "r", { ctrl: true }); // -> compact
|
|
121
|
-
await pressKey(setup, "r", { ctrl: true }); // -> hidden
|
|
122
|
-
|
|
123
|
-
// Assert
|
|
124
|
-
await waitForTextGone(setup, "Submit review"); // rail is gone
|
|
125
|
-
expect(setup.captureCharFrame()).not.toContain("Submit review");
|
|
126
|
-
|
|
127
|
-
// Act
|
|
128
|
-
await pressKey(setup, "RETURN", { meta: true }); // submit while hidden must not trap in an invisible modal
|
|
129
|
-
|
|
130
|
-
// Assert
|
|
131
|
-
await waitForText(setup, "submit review");
|
|
132
|
-
const frame = setup.captureCharFrame();
|
|
133
|
-
|
|
134
|
-
expect(frame).toContain("submit review"); // the confirm card is visible again
|
|
135
|
-
expect(frame).toContain("[Approve]");
|
|
136
|
-
|
|
137
|
-
// the reveal is live-only: the saved panel preference stays as the user left it
|
|
138
|
-
expect(loadConfig({ userConfigPath: configPath }).ui.reviewState).toBe("hidden");
|
|
139
|
-
});
|
|
140
|
-
});
|
package/src/App.tree.test.tsx
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rail's Tree tab over the real App and an in-process daemon: the option
|
|
3
|
-
* chords and the mouse reach the same primitives, the pane paints the path,
|
|
4
|
-
* the tips, and the checkpoints, and the thread view shows a moved path in the
|
|
5
|
-
* frame after the key.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import React from "react";
|
|
13
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
14
|
-
import { DaemonServer } from "@cueloop/daemon";
|
|
15
|
-
import type { ReviewSession } from "@cueloop/schema";
|
|
16
|
-
import { App } from "./App";
|
|
17
|
-
import {
|
|
18
|
-
clickText,
|
|
19
|
-
isolateUserConfig,
|
|
20
|
-
press,
|
|
21
|
-
pressKey,
|
|
22
|
-
typeText as type,
|
|
23
|
-
waitForState,
|
|
24
|
-
waitForText,
|
|
25
|
-
waitForTextGone,
|
|
26
|
-
} from "./test-support";
|
|
27
|
-
|
|
28
|
-
const PLAN = `# Migration Plan
|
|
29
|
-
|
|
30
|
-
## Context
|
|
31
|
-
|
|
32
|
-
The daemon persists sessions to disk atomically.
|
|
33
|
-
|
|
34
|
-
## Steps
|
|
35
|
-
|
|
36
|
-
- move the store
|
|
37
|
-
- add recovery
|
|
38
|
-
`;
|
|
39
|
-
|
|
40
|
-
let home: string;
|
|
41
|
-
let server: DaemonServer;
|
|
42
|
-
let session: ReviewSession;
|
|
43
|
-
let restoreUserConfig: () => void;
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
home = mkdtempSync(join(tmpdir(), "cueloop-tree-"));
|
|
47
|
-
restoreUserConfig = isolateUserConfig(home);
|
|
48
|
-
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
49
|
-
server.start();
|
|
50
|
-
session = server.core.sessionCreate({
|
|
51
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
52
|
-
artifact: {
|
|
53
|
-
type: "plan",
|
|
54
|
-
content: PLAN,
|
|
55
|
-
meta: { title: "Migration Plan", planPath: "plan.md", agent: "agent/worker-3" },
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
restoreUserConfig();
|
|
61
|
-
server.stop();
|
|
62
|
-
rmSync(home, { recursive: true, force: true });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
async function renderApp(role: "owner" | "collaborator" = "owner") {
|
|
66
|
-
const setup = await testRender(<App home={home} sessionId={session.id} role={role} />, {
|
|
67
|
-
width: 120,
|
|
68
|
-
height: 32,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
await waitForText(setup, "cueloop");
|
|
72
|
-
|
|
73
|
-
return setup;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
77
|
-
|
|
78
|
-
/** Leave a comment on the word, sent with the terminal-safe chord. */
|
|
79
|
-
async function comment(setup: Setup, word: string, body: string): Promise<void> {
|
|
80
|
-
await clickText(setup, word);
|
|
81
|
-
await type(setup, body);
|
|
82
|
-
await pressKey(setup, "RETURN", { meta: true });
|
|
83
|
-
await waitForText(setup, body);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Frames painted until the needle leaves the screen, starting right after a key with no settle. */
|
|
87
|
-
async function framesUntilGone(setup: Setup, needle: string, limit = 4): Promise<number> {
|
|
88
|
-
for (let frame = 1; frame <= limit; frame++) {
|
|
89
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
90
|
-
await setup.renderOnce();
|
|
91
|
-
if (!setup.captureCharFrame().includes(needle)) return frame;
|
|
92
|
-
}
|
|
93
|
-
throw new Error(`"${needle}" never left the frame`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
describe("the Tree tab", () => {
|
|
97
|
-
test("option+t shows the tree: the revision, the comments, the current tip, a checkpoint", async () => {
|
|
98
|
-
// Arrange
|
|
99
|
-
const setup = await renderApp();
|
|
100
|
-
|
|
101
|
-
await comment(setup, "daemon", "Which daemon?");
|
|
102
|
-
|
|
103
|
-
// Act
|
|
104
|
-
await pressKey(setup, "t", { meta: true });
|
|
105
|
-
await waitForText(setup, "● comment");
|
|
106
|
-
await pressKey(setup, "l", { meta: true });
|
|
107
|
-
await waitForText(setup, "Checkpoint");
|
|
108
|
-
await type(setup, "first pass");
|
|
109
|
-
await press(setup, "enter");
|
|
110
|
-
|
|
111
|
-
// Assert: the tip row carries the branch and the new label
|
|
112
|
-
await waitForText(setup, "⚑ first pass");
|
|
113
|
-
expect(setup.captureCharFrame()).toContain("◉ revision 1");
|
|
114
|
-
expect(setup.captureCharFrame()).toContain("← main");
|
|
115
|
-
await waitForState(
|
|
116
|
-
setup,
|
|
117
|
-
() => Object.values(server.core.sessionGet(session.id).history!.labels)[0] === "first pass",
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("option+b branches; option+p / option+g move back and the path repaints in one frame", async () => {
|
|
122
|
-
// Arrange: a comment on main, a branch, a second comment on it
|
|
123
|
-
const setup = await renderApp();
|
|
124
|
-
|
|
125
|
-
await comment(setup, "daemon", "Which daemon?");
|
|
126
|
-
await pressKey(setup, "t", { meta: true });
|
|
127
|
-
await waitForText(setup, "● comment");
|
|
128
|
-
await pressKey(setup, "b", { meta: true });
|
|
129
|
-
await waitForText(setup, "Branch");
|
|
130
|
-
await type(setup, "alt");
|
|
131
|
-
await press(setup, "enter");
|
|
132
|
-
await waitForText(setup, "← main, alt");
|
|
133
|
-
await comment(setup, "recovery", "Only on alt.");
|
|
134
|
-
await waitForText(setup, "● comment ← alt");
|
|
135
|
-
|
|
136
|
-
// Act: stand on the revision (two rows up from alt's tip) and move back to it
|
|
137
|
-
await pressKey(setup, "p", { meta: true });
|
|
138
|
-
await pressKey(setup, "p", { meta: true });
|
|
139
|
-
await pressKey(setup, "g", { meta: true });
|
|
140
|
-
await waitForText(setup, "Move back");
|
|
141
|
-
setup.mockInput.pressKey("RETURN");
|
|
142
|
-
|
|
143
|
-
// Assert: both comments leave the document in the frame after the key; the record follows
|
|
144
|
-
expect(await framesUntilGone(setup, "Only on alt.")).toBeLessThanOrEqual(1);
|
|
145
|
-
expect(setup.captureCharFrame()).not.toContain("Which daemon?");
|
|
146
|
-
await waitForState(setup, () => {
|
|
147
|
-
const record = server.core.sessionGet(session.id);
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
record.history!.branch === "alt" &&
|
|
151
|
-
record.annotations.length === 0 &&
|
|
152
|
-
record.shelvedAnnotations?.length === 2
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Act: main's tip is the first comment - going there is a switch, no prompt
|
|
157
|
-
await pressKey(setup, "n", { meta: true });
|
|
158
|
-
await pressKey(setup, "g", { meta: true });
|
|
159
|
-
|
|
160
|
-
// Assert
|
|
161
|
-
await waitForText(setup, "on branch main");
|
|
162
|
-
await waitForText(setup, "Which daemon?");
|
|
163
|
-
expect(setup.captureCharFrame()).not.toContain("Only on alt.");
|
|
164
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).history!.branch === "main");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("the mouse reaches the same primitives: the tab, a row, and the buttons", async () => {
|
|
168
|
-
// Arrange
|
|
169
|
-
const setup = await renderApp();
|
|
170
|
-
|
|
171
|
-
await comment(setup, "daemon", "Which daemon?");
|
|
172
|
-
|
|
173
|
-
// Act: open the tab, select the revision row, open the label prompt from its button
|
|
174
|
-
await clickText(setup, "Tree");
|
|
175
|
-
await waitForText(setup, "◉ revision 1");
|
|
176
|
-
await clickText(setup, "◉ revision 1");
|
|
177
|
-
await clickText(setup, "Label");
|
|
178
|
-
|
|
179
|
-
// Assert
|
|
180
|
-
await waitForText(setup, "Checkpoint");
|
|
181
|
-
await press(setup, "escape");
|
|
182
|
-
await waitForTextGone(setup, "Checkpoint");
|
|
183
|
-
|
|
184
|
-
// Act: a second click on the selected row goes there - a move back asks for a summary
|
|
185
|
-
await clickText(setup, "◉ revision 1");
|
|
186
|
-
await waitForText(setup, "Move back");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("a collaborator reads the tree and cannot move it", async () => {
|
|
190
|
-
// Arrange
|
|
191
|
-
const setup = await renderApp("collaborator");
|
|
192
|
-
|
|
193
|
-
// Act
|
|
194
|
-
await pressKey(setup, "t", { meta: true });
|
|
195
|
-
await waitForText(setup, "● revision 1");
|
|
196
|
-
await pressKey(setup, "b", { meta: true });
|
|
197
|
-
|
|
198
|
-
// Assert: no prompt, the read-only answer, no buttons
|
|
199
|
-
await waitForText(setup, "observer - read-only");
|
|
200
|
-
expect(setup.captureCharFrame()).not.toContain("Fork+share");
|
|
201
|
-
|
|
202
|
-
// Act: two clicks on the revision row select it and nothing more
|
|
203
|
-
await clickText(setup, "● revision 1");
|
|
204
|
-
await clickText(setup, "● revision 1");
|
|
205
|
-
|
|
206
|
-
// Assert: no prompt opened, the record stands
|
|
207
|
-
expect(setup.captureCharFrame()).not.toContain("Move back");
|
|
208
|
-
expect(server.core.sessionGet(session.id).history!.entries).toHaveLength(1);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("a resolved review keeps the fork buttons and drops the moving ones", async () => {
|
|
212
|
-
// Arrange
|
|
213
|
-
server.core.sessionResolve(session.id, "approve", "ship it");
|
|
214
|
-
const setup = await renderApp();
|
|
215
|
-
|
|
216
|
-
// Act
|
|
217
|
-
await pressKey(setup, "t", { meta: true });
|
|
218
|
-
await waitForText(setup, "Fork+share");
|
|
219
|
-
|
|
220
|
-
// Assert
|
|
221
|
-
expect(setup.captureCharFrame()).not.toContain("Branch");
|
|
222
|
-
expect(setup.captureCharFrame()).not.toContain("Label");
|
|
223
|
-
});
|
|
224
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { DARK } from "../theme";
|
|
3
|
-
import { REVIEW_MAX_WIDTH, REVIEW_MIN_WIDTH } from "../review-panel";
|
|
4
|
-
import type { Story, StoryMeta } from "./story";
|
|
5
|
-
import { ReviewPanel } from "./ReviewPanel";
|
|
6
|
-
import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
|
|
7
|
-
|
|
8
|
-
export const meta: StoryMeta = { title: "ReviewPanel" };
|
|
9
|
-
|
|
10
|
-
/** An empty Tree tab: the stories here show the review tab. */
|
|
11
|
-
const tree = {
|
|
12
|
-
rows: [],
|
|
13
|
-
canMove: true,
|
|
14
|
-
canFork: true,
|
|
15
|
-
onSelect: () => {},
|
|
16
|
-
onGo: () => {},
|
|
17
|
-
onBranch: () => {},
|
|
18
|
-
onLabel: () => {},
|
|
19
|
-
onFork: () => {},
|
|
20
|
-
onForkAndShare: () => {},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const rail = {
|
|
24
|
-
session: fixturePlanSession(),
|
|
25
|
-
tree,
|
|
26
|
-
authorNames: {},
|
|
27
|
-
selectedId: FIXTURE_ANNOTATIONS[0]!.id,
|
|
28
|
-
resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
|
|
29
|
-
curationItems: [],
|
|
30
|
-
railTab: "review" as const,
|
|
31
|
-
pendingCount: 2,
|
|
32
|
-
cardEdit: null,
|
|
33
|
-
submitConfirm: null,
|
|
34
|
-
onTabChange: () => {},
|
|
35
|
-
onSelectCard: () => {},
|
|
36
|
-
onActivateCard: () => {},
|
|
37
|
-
onSelectCuration: () => {},
|
|
38
|
-
onUndoCuration: () => {},
|
|
39
|
-
onSubmitRequest: () => {},
|
|
40
|
-
onLaunchHarness: () => {},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
|
|
44
|
-
|
|
45
|
-
/** The plan area to the left of the divider, so the layout reads in context. */
|
|
46
|
-
function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
47
|
-
return (
|
|
48
|
-
<box
|
|
49
|
-
style={{
|
|
50
|
-
flexDirection: "row",
|
|
51
|
-
width: "100%",
|
|
52
|
-
height: "100%",
|
|
53
|
-
backgroundColor: DARK.background,
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
<box style={{ flexGrow: 1, paddingLeft: 1 }}>
|
|
57
|
-
<text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
|
|
58
|
-
</box>
|
|
59
|
-
{children}
|
|
60
|
-
</box>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const ExpandedNarrow: Story = {
|
|
65
|
-
render: () => (
|
|
66
|
-
<PanelFrame>
|
|
67
|
-
<ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} rail={rail} {...panelCallbacks} />
|
|
68
|
-
</PanelFrame>
|
|
69
|
-
),
|
|
70
|
-
size: { width: 80, height: 24 },
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export const ExpandedWide: Story = {
|
|
74
|
-
render: () => (
|
|
75
|
-
<PanelFrame>
|
|
76
|
-
<ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} rail={rail} {...panelCallbacks} />
|
|
77
|
-
</PanelFrame>
|
|
78
|
-
),
|
|
79
|
-
expectedColors: [DARK.accent],
|
|
80
|
-
size: { width: 90, height: 24 },
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export const Compact: Story = {
|
|
84
|
-
render: () => (
|
|
85
|
-
<PanelFrame>
|
|
86
|
-
<ReviewPanel mode="compact" width={34} rail={rail} {...panelCallbacks} />
|
|
87
|
-
</PanelFrame>
|
|
88
|
-
),
|
|
89
|
-
expectedColors: [DARK.accent],
|
|
90
|
-
size: { width: 60, height: 24 },
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export const Hidden: Story = {
|
|
94
|
-
render: () => (
|
|
95
|
-
<PanelFrame>
|
|
96
|
-
<ReviewPanel mode="hidden" width={34} rail={rail} {...panelCallbacks} />
|
|
97
|
-
</PanelFrame>
|
|
98
|
-
),
|
|
99
|
-
size: { width: 60, height: 24 },
|
|
100
|
-
};
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The review surface's outer shell: the grabbable divider plus the rail in one
|
|
3
|
-
* of three modes - expanded (the full ReviewRail, resizable), compact (a narrow
|
|
4
|
-
* count + dots strip that hands width back to the plan), or hidden (nothing at
|
|
5
|
-
* all, no leftover tab). The keybinding cycles all three; the clickable chevron
|
|
6
|
-
* toggles expanded and compact only. App owns the width/mode state and the drag
|
|
7
|
-
* on the container; this file only reads them. The geometry (width clamp, the
|
|
8
|
-
* divider glyph column, the cycle) lives in review-panel.ts so this stays
|
|
9
|
-
* declarative.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import React from "react";
|
|
13
|
-
import type { Annotation } from "@cueloop/schema";
|
|
14
|
-
import type { Theme } from "../theme";
|
|
15
|
-
import { useComponentTheme } from "./theme-context";
|
|
16
|
-
import { ReviewRail, type ReviewRailHandle, type ReviewRailProps } from "./ReviewRail";
|
|
17
|
-
import { REVIEW_COMPACT_WIDTH, type ReviewPanelMode } from "../review-panel";
|
|
18
|
-
|
|
19
|
-
export interface ReviewPanelProps {
|
|
20
|
-
mode: ReviewPanelMode;
|
|
21
|
-
/** Expanded-rail width in columns (already clamped by the app). */
|
|
22
|
-
width: number;
|
|
23
|
-
/** Arm a divider drag; the app tracks the drag itself on the container. */
|
|
24
|
-
onDividerGrab: () => void;
|
|
25
|
-
/** Chevron click: expanded <-> compact (never hidden). */
|
|
26
|
-
onToggle: () => void;
|
|
27
|
-
/** The expanded ReviewRail's props; width and onCollapse are supplied here. */
|
|
28
|
-
rail: Omit<ReviewRailProps, "width" | "onCollapse" | "theme">;
|
|
29
|
-
railRef?: React.Ref<ReviewRailHandle>;
|
|
30
|
-
theme?: Theme;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** An invisible, full-height grab column: the plan's right border is the seam. */
|
|
34
|
-
function ReviewDivider({ onGrab }: { onGrab: () => void }): React.ReactNode {
|
|
35
|
-
// childless: it stretches to the row height like the plan and rail beside it,
|
|
36
|
-
// so the three columns share one bottom edge. Invisible, but full-height grab.
|
|
37
|
-
return <box style={{ width: 1 }} onMouseDown={onGrab} />;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** The compact strip: the card count, one kind-colored dot per card, and the
|
|
41
|
-
* `<` expand chevron, bottom-padded to sit at the same row as the expanded `>`. */
|
|
42
|
-
function CompactRail({
|
|
43
|
-
annotations,
|
|
44
|
-
onExpand,
|
|
45
|
-
theme,
|
|
46
|
-
}: {
|
|
47
|
-
annotations: Annotation[];
|
|
48
|
-
onExpand: () => void;
|
|
49
|
-
theme?: Theme;
|
|
50
|
-
}): React.ReactNode {
|
|
51
|
-
const tokens = useComponentTheme(theme);
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<box
|
|
55
|
-
style={{
|
|
56
|
-
width: REVIEW_COMPACT_WIDTH,
|
|
57
|
-
backgroundColor: tokens.panel,
|
|
58
|
-
flexDirection: "column",
|
|
59
|
-
paddingTop: 1,
|
|
60
|
-
paddingBottom: 1,
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
<box style={{ alignItems: "center" }}>
|
|
64
|
-
<text fg={tokens.accent}>{String(annotations.length)}</text>
|
|
65
|
-
</box>
|
|
66
|
-
<box style={{ height: 1 }} />
|
|
67
|
-
<box style={{ flexDirection: "column", alignItems: "center" }}>
|
|
68
|
-
{annotations.map((annotation) => (
|
|
69
|
-
<text key={annotation.id} fg={tokens.accent}>
|
|
70
|
-
●
|
|
71
|
-
</text>
|
|
72
|
-
))}
|
|
73
|
-
</box>
|
|
74
|
-
<box style={{ flexGrow: 1 }} />
|
|
75
|
-
<box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
|
|
76
|
-
<text fg={tokens.textDim}>{"<"}</text>
|
|
77
|
-
</box>
|
|
78
|
-
</box>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function ReviewPanel({
|
|
83
|
-
mode,
|
|
84
|
-
width,
|
|
85
|
-
onDividerGrab,
|
|
86
|
-
onToggle,
|
|
87
|
-
rail,
|
|
88
|
-
railRef,
|
|
89
|
-
theme,
|
|
90
|
-
}: ReviewPanelProps): React.ReactNode {
|
|
91
|
-
if (mode === "hidden") return null;
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<>
|
|
95
|
-
<ReviewDivider onGrab={onDividerGrab} />
|
|
96
|
-
{mode === "expanded" ? (
|
|
97
|
-
<ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
|
|
98
|
-
) : (
|
|
99
|
-
<CompactRail annotations={rail.session.annotations} onExpand={onToggle} theme={theme} />
|
|
100
|
-
)}
|
|
101
|
-
</>
|
|
102
|
-
);
|
|
103
|
-
}
|