@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- 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 +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -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 +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** The default prototype is a Markdown design doc: with no experimental flag it renders through the thread/markdown path (not the kitty pixel view), its headings and fenced sections show as text, and a quote-anchored comment lands on a line - the same as a plan. A real in-process daemon; char-frame assertions over the real App. */
|
|
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 { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import { DARK } from "./theme";
|
|
12
|
+
import { isolateUserConfig, renderReadyApp, settle, waitForText } from "./test-support";
|
|
13
|
+
|
|
14
|
+
const DOC = `# PromoCard
|
|
15
|
+
|
|
16
|
+
A promotional card for a growth surface.
|
|
17
|
+
|
|
18
|
+
## API
|
|
19
|
+
|
|
20
|
+
\`\`\`ts
|
|
21
|
+
interface PromoCardProps {
|
|
22
|
+
title: string
|
|
23
|
+
onDismiss?: () => void
|
|
24
|
+
}
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
## Composition
|
|
28
|
+
|
|
29
|
+
\`\`\`
|
|
30
|
+
PromoCard
|
|
31
|
+
└─ TextGroup reuse: Text
|
|
32
|
+
\`\`\`
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
let home: string;
|
|
36
|
+
let server: DaemonServer;
|
|
37
|
+
let session: Thread;
|
|
38
|
+
let restoreUserConfig: () => void;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-prototype-md-"));
|
|
42
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
43
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
44
|
+
server.start();
|
|
45
|
+
session = server.core.sessionCreate({
|
|
46
|
+
workspace: { repoRoot: "/repo", branch: "promo-card" },
|
|
47
|
+
artifact: { type: "prototype", content: DOC, meta: { title: "PromoCard", agent: "pi" } },
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
restoreUserConfig();
|
|
52
|
+
server.stop();
|
|
53
|
+
rmSync(home, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("markdown prototype review", () => {
|
|
57
|
+
test("syntax-highlights TypeScript fences without changing their text", async () => {
|
|
58
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
59
|
+
width: 120,
|
|
60
|
+
height: 40,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await waitForText(setup, "PromoCardProps");
|
|
64
|
+
let keywordHighlighted = false;
|
|
65
|
+
|
|
66
|
+
for (let attempt = 0; attempt < 60 && !keywordHighlighted; attempt++) {
|
|
67
|
+
await setup.renderOnce();
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
69
|
+
const accentText = setup
|
|
70
|
+
.captureSpans()
|
|
71
|
+
.lines.flatMap((line) =>
|
|
72
|
+
line.spans
|
|
73
|
+
.filter((span) => {
|
|
74
|
+
const [red, green, blue] = span.fg.toInts();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
`#${[red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("")}` ===
|
|
78
|
+
DARK.accent
|
|
79
|
+
);
|
|
80
|
+
})
|
|
81
|
+
.map((span) => span.text),
|
|
82
|
+
)
|
|
83
|
+
.join("");
|
|
84
|
+
|
|
85
|
+
keywordHighlighted = accentText.includes("interface");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(keywordHighlighted).toBe(true);
|
|
89
|
+
expect(setup.captureCharFrame()).toContain("interface PromoCardProps {");
|
|
90
|
+
}, 25000);
|
|
91
|
+
|
|
92
|
+
test("renders as a markdown design doc, not the pixel view", async () => {
|
|
93
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
94
|
+
width: 120,
|
|
95
|
+
height: 40,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await waitForText(setup, "PromoCard");
|
|
99
|
+
await settle(setup);
|
|
100
|
+
const frame = setup.captureCharFrame();
|
|
101
|
+
|
|
102
|
+
// the markdown path renders the doc's headings and interface as text
|
|
103
|
+
expect(frame).toContain("API");
|
|
104
|
+
expect(frame).toContain("PromoCardProps");
|
|
105
|
+
expect(frame).toContain("Composition");
|
|
106
|
+
// and never falls into the pixel view's status line
|
|
107
|
+
expect(frame).not.toContain("rendering prototype");
|
|
108
|
+
expect(frame).not.toContain("graphics terminal");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a quote-anchored comment stores against the doc, like a plan", async () => {
|
|
112
|
+
server.core.sessionAnnotate(session.id, {
|
|
113
|
+
id: "own_1",
|
|
114
|
+
kind: "comment",
|
|
115
|
+
anchor: { quote: "onDismiss?: () => void", prefix: "", suffix: "" },
|
|
116
|
+
body: "make dismiss required for a modal surface?",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
120
|
+
width: 120,
|
|
121
|
+
height: 40,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await waitForText(setup, "make dismiss required");
|
|
125
|
+
expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
|
|
126
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.selector).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Prototype review end to end, in the opt-in EXPERIMENTAL pixel mode ([experimental] prototype_pixels): a click on the rendered page selects a DOM element and opens the compose card directly - the same inline composer plan and diff use - a typed comment saves through the controller on cmd+enter, and the annotation anchors to the element's selector. The headless browser is replaced with a fake so no Chrome or kitty output is needed. Char-frame assertions over the real App and a real in-process daemon. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
setPrototypeRendererFactory,
|
|
13
|
+
type PrototypeElement,
|
|
14
|
+
type PrototypeRenderer,
|
|
15
|
+
} from "./prototype-browser";
|
|
16
|
+
import {
|
|
17
|
+
isolateUserConfig,
|
|
18
|
+
press,
|
|
19
|
+
renderReadyApp,
|
|
20
|
+
settle,
|
|
21
|
+
typeText,
|
|
22
|
+
waitForState,
|
|
23
|
+
waitForText,
|
|
24
|
+
waitForTextGone,
|
|
25
|
+
} from "./test-support";
|
|
26
|
+
|
|
27
|
+
const HTML = "<main><div class='card'><h2>Pro</h2><p>$24/mo</p></div></main>";
|
|
28
|
+
|
|
29
|
+
const FAKE_ELEMENT: PrototypeElement = {
|
|
30
|
+
selector: "main > div.card",
|
|
31
|
+
quote: "Pro $24/mo",
|
|
32
|
+
// mid-page, so the popover anchors well away from the click point below
|
|
33
|
+
box: { x: 440, y: 320, width: 320, height: 220 },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let rendered = false;
|
|
37
|
+
let screenshotCount = 0;
|
|
38
|
+
let scrollDeltas: number[] = [];
|
|
39
|
+
const fakeRenderer: PrototypeRenderer = {
|
|
40
|
+
viewport: { width: 1280, height: 800 },
|
|
41
|
+
screenshot: async () => {
|
|
42
|
+
rendered = true;
|
|
43
|
+
screenshotCount += 1;
|
|
44
|
+
|
|
45
|
+
return new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
46
|
+
},
|
|
47
|
+
elementAt: async () => FAKE_ELEMENT,
|
|
48
|
+
scrollBy: async (deltaY) => {
|
|
49
|
+
scrollDeltas.push(deltaY);
|
|
50
|
+
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
close: async () => undefined,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let home: string;
|
|
57
|
+
let server: DaemonServer;
|
|
58
|
+
let session: Thread;
|
|
59
|
+
let restoreUserConfig: () => void;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-prototype-"));
|
|
63
|
+
// the pixel mockup is opt-in; these tests exercise that experimental path
|
|
64
|
+
writeFileSync(join(home, "config.toml"), "[experimental]\nprototype_pixels = true\n");
|
|
65
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
66
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
67
|
+
server.start();
|
|
68
|
+
session = server.core.sessionCreate({
|
|
69
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
70
|
+
artifact: {
|
|
71
|
+
type: "prototype",
|
|
72
|
+
content: HTML,
|
|
73
|
+
meta: { title: "Pricing cards", prototypePath: "/repo/cards.html" },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
rendered = false;
|
|
77
|
+
screenshotCount = 0;
|
|
78
|
+
scrollDeltas = [];
|
|
79
|
+
setPrototypeRendererFactory(async () => fakeRenderer);
|
|
80
|
+
});
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
setPrototypeRendererFactory(null);
|
|
83
|
+
restoreUserConfig();
|
|
84
|
+
server.stop();
|
|
85
|
+
rmSync(home, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
async function renderApp() {
|
|
89
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
90
|
+
width: 120,
|
|
91
|
+
height: 32,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await waitForText(setup, "cueloop");
|
|
95
|
+
|
|
96
|
+
return setup;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("prototype review", () => {
|
|
100
|
+
test("click an element, comment on it, and it saves to the daemon", async () => {
|
|
101
|
+
// Arrange - wait until the fake browser produced its first screenshot, so
|
|
102
|
+
// the sheet is interactive before the click
|
|
103
|
+
const setup = await renderApp();
|
|
104
|
+
|
|
105
|
+
await waitForState(setup, () => rendered);
|
|
106
|
+
await settle(setup);
|
|
107
|
+
// selecting an element must not re-screenshot the page (the compose-open
|
|
108
|
+
// lag regression): the click opens the composer, not a fresh Chromium capture
|
|
109
|
+
const screenshotsAfterLoad = screenshotCount;
|
|
110
|
+
|
|
111
|
+
// Act - a click opens the compose card directly on the element
|
|
112
|
+
await setup.mockMouse.click(6, 6);
|
|
113
|
+
await waitForText(setup, "●");
|
|
114
|
+
expect(screenshotCount).toBe(screenshotsAfterLoad);
|
|
115
|
+
// the keymap-suppression flag must propagate before typing so keys reach the
|
|
116
|
+
// textarea, not the global keymap
|
|
117
|
+
await settle(setup);
|
|
118
|
+
await typeText(setup, "tighten the padding");
|
|
119
|
+
// cmd+enter sends, matching the plan and diff composer; plain enter is a newline
|
|
120
|
+
setup.mockInput.pressKey("RETURN", { meta: true });
|
|
121
|
+
|
|
122
|
+
// Assert - the annotation is stored against the element's selector
|
|
123
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
124
|
+
const stored = server.core.sessionGet(session.id).annotations[0]!;
|
|
125
|
+
|
|
126
|
+
expect(stored.kind).toBe("comment");
|
|
127
|
+
expect(stored.body).toBe("tighten the padding");
|
|
128
|
+
expect(stored.anchor.selector).toBe("main > div.card");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("escape cancels the compose, matching the plan composer", async () => {
|
|
132
|
+
// Arrange
|
|
133
|
+
const setup = await renderApp();
|
|
134
|
+
|
|
135
|
+
await waitForState(setup, () => rendered);
|
|
136
|
+
await settle(setup);
|
|
137
|
+
await setup.mockMouse.click(6, 6);
|
|
138
|
+
await waitForText(setup, "●");
|
|
139
|
+
await settle(setup);
|
|
140
|
+
|
|
141
|
+
// Act
|
|
142
|
+
await press(setup, "escape");
|
|
143
|
+
|
|
144
|
+
// Assert - the composer closes and nothing was saved
|
|
145
|
+
await waitForTextGone(setup, "●");
|
|
146
|
+
expect(server.core.sessionGet(session.id).annotations).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("wheel over the preview scrolls the page down, then up, and clears a selection", async () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const setup = await renderApp();
|
|
152
|
+
|
|
153
|
+
await waitForState(setup, () => rendered);
|
|
154
|
+
await settle(setup);
|
|
155
|
+
|
|
156
|
+
// open a selection, then scroll: the scroll drops it (it would drift once
|
|
157
|
+
// the page moves under it)
|
|
158
|
+
await setup.mockMouse.click(6, 6);
|
|
159
|
+
await waitForText(setup, "●");
|
|
160
|
+
// scrolling moves the page, so it DOES re-screenshot (unlike a click)
|
|
161
|
+
const screenshotsBeforeScroll = screenshotCount;
|
|
162
|
+
|
|
163
|
+
await setup.mockMouse.scroll(6, 6, "down");
|
|
164
|
+
await waitForState(setup, () => scrollDeltas.length === 1);
|
|
165
|
+
await waitForTextGone(setup, "●");
|
|
166
|
+
await waitForState(setup, () => screenshotCount > screenshotsBeforeScroll);
|
|
167
|
+
|
|
168
|
+
// Act - scroll back up
|
|
169
|
+
await setup.mockMouse.scroll(6, 6, "up");
|
|
170
|
+
await waitForState(setup, () => scrollDeltas.length === 2);
|
|
171
|
+
|
|
172
|
+
// Assert - down was a positive delta, up was negative
|
|
173
|
+
expect(scrollDeltas[0]!).toBeGreaterThan(0);
|
|
174
|
+
expect(scrollDeltas[1]!).toBeLessThan(0);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Observer mode (tier 2): the App with readOnly renders and navigates like the
|
|
3
|
-
* controller's, but every mutating verb answers "observer - read-only" and
|
|
4
|
-
* leaves daemon state untouched.
|
|
5
|
-
*/
|
|
1
|
+
/** Observer mode (tier 2): the App with readOnly renders and navigates like the controller's, but every mutating primitive answers "observer - read-only" and leaves daemon state untouched. */
|
|
6
2
|
|
|
7
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
5
|
import { tmpdir } from "node:os";
|
|
10
6
|
import { join } from "node:path";
|
|
11
7
|
import React from "react";
|
|
12
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
13
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
14
|
-
import { makeAnchor, parseBlocks, type
|
|
9
|
+
import { makeAnchor, parseBlocks, type Thread } from "@cueloop/schema";
|
|
15
10
|
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
clickText,
|
|
13
|
+
dragText,
|
|
14
|
+
frameRow,
|
|
15
|
+
isolateUserConfig,
|
|
16
|
+
navCommand,
|
|
17
|
+
pressKey,
|
|
18
|
+
renderReadyApp,
|
|
19
|
+
typeText,
|
|
20
|
+
waitForText,
|
|
21
|
+
} from "./test-support";
|
|
16
22
|
|
|
17
23
|
const PLAN = `# Migration Plan
|
|
18
24
|
|
|
@@ -27,113 +33,164 @@ The daemon persists sessions to disk atomically.
|
|
|
27
33
|
`;
|
|
28
34
|
|
|
29
35
|
let home: string;
|
|
36
|
+
let restoreUserConfig: () => void;
|
|
30
37
|
let server: DaemonServer;
|
|
31
|
-
let session:
|
|
38
|
+
let session: Thread;
|
|
32
39
|
|
|
33
40
|
beforeEach(() => {
|
|
34
41
|
home = mkdtempSync(join(tmpdir(), "cueloop-observer-"));
|
|
42
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
43
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
44
|
server.start();
|
|
37
45
|
session = server.core.sessionCreate({
|
|
38
46
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
-
artifact: {
|
|
47
|
+
artifact: {
|
|
48
|
+
type: "plan",
|
|
49
|
+
content: PLAN,
|
|
50
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
51
|
+
},
|
|
40
52
|
});
|
|
41
53
|
});
|
|
42
54
|
afterEach(() => {
|
|
55
|
+
restoreUserConfig();
|
|
43
56
|
server.stop();
|
|
44
57
|
rmSync(home, { recursive: true, force: true });
|
|
45
58
|
});
|
|
46
59
|
|
|
47
60
|
async function renderObserver() {
|
|
48
|
-
const setup = await
|
|
61
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
49
62
|
width: 120,
|
|
50
63
|
height: 32,
|
|
51
64
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
await setup.renderOnce();
|
|
65
|
+
|
|
66
|
+
await waitForText(setup, "cueloop");
|
|
67
|
+
|
|
57
68
|
return setup;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
type Setup = Awaited<ReturnType<typeof renderObserver>>;
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
64
|
-
else await setup.mockInput.typeText(k);
|
|
65
|
-
await Bun.sleep(15);
|
|
66
|
-
await setup.renderOnce();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Session state that any mutating verb would change. */
|
|
73
|
+
/** Session state that any mutating primitive would change. */
|
|
70
74
|
function snapshot() {
|
|
71
|
-
const
|
|
72
|
-
|
|
75
|
+
const stored = server.core.sessionGet(session.id);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
annotations: stored.annotations.length,
|
|
79
|
+
workingCopy: stored.workingCopy,
|
|
80
|
+
status: stored.status,
|
|
81
|
+
};
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
describe("observer rendering", () => {
|
|
76
|
-
test("
|
|
85
|
+
test("the minimal header mirrors the thread name, with no observer badge", async () => {
|
|
86
|
+
// Arrange
|
|
77
87
|
const setup = await renderObserver();
|
|
88
|
+
|
|
89
|
+
// Assert - the header shows the thread title only; read-only is enforced by
|
|
90
|
+
// the blocked primitives below, not a header badge
|
|
78
91
|
const frame = setup.captureCharFrame();
|
|
79
|
-
|
|
80
|
-
expect(frame).toContain("
|
|
81
|
-
expect(frame).not.toContain("
|
|
92
|
+
|
|
93
|
+
expect(frame).toContain("Migration Plan");
|
|
94
|
+
expect(frame).not.toContain("· observer");
|
|
82
95
|
});
|
|
83
96
|
});
|
|
84
97
|
|
|
85
|
-
describe("observer
|
|
86
|
-
|
|
87
|
-
["
|
|
88
|
-
["
|
|
89
|
-
["
|
|
90
|
-
["
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
describe("observer primitives are blocked", () => {
|
|
99
|
+
const attempts: Array<[string, (setup: Setup) => Promise<void>]> = [
|
|
100
|
+
["comment (typing)", (setup) => typeText(setup, "c")],
|
|
101
|
+
["cut (option+x)", (setup) => navCommand(setup, "x")],
|
|
102
|
+
["edit (ctrl+e)", (setup) => pressKey(setup, "e", { ctrl: true })],
|
|
103
|
+
["submit (cmd+enter)", (setup) => pressKey(setup, "RETURN", { meta: true })],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const [primitive, attempt] of attempts) {
|
|
107
|
+
test(`${primitive} answers observer - read-only and mutates nothing`, async () => {
|
|
94
108
|
const setup = await renderObserver();
|
|
95
109
|
const before = snapshot();
|
|
96
|
-
|
|
97
|
-
await
|
|
98
|
-
await
|
|
99
|
-
await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(frame).toContain("
|
|
103
|
-
|
|
104
|
-
expect(frame).not.toContain("COMMENT ON");
|
|
105
|
-
expect(frame).not.toContain("SUGGEST REPLACEMENT");
|
|
106
|
-
expect(frame).not.toContain("verdict ←/→");
|
|
110
|
+
|
|
111
|
+
await clickText(setup, "daemon");
|
|
112
|
+
await attempt(setup);
|
|
113
|
+
const frame = await waitForText(setup, "observer - read-only");
|
|
114
|
+
|
|
115
|
+
// no draft card and no submit card ever appear
|
|
116
|
+
expect(frame).not.toContain("● c");
|
|
117
|
+
expect(frame).not.toContain("[approve]");
|
|
107
118
|
expect(snapshot()).toEqual(before);
|
|
108
119
|
});
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
test("
|
|
122
|
+
test("typing over a marked span is blocked too", async () => {
|
|
123
|
+
// Arrange
|
|
112
124
|
const setup = await renderObserver();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
|
|
126
|
+
// Act
|
|
127
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
128
|
+
await typeText(setup, "c");
|
|
129
|
+
|
|
130
|
+
// Assert
|
|
131
|
+
const frame = await waitForText(setup, "observer - read-only");
|
|
132
|
+
|
|
133
|
+
expect(frame).not.toContain("● c");
|
|
121
134
|
expect(snapshot().annotations).toBe(0);
|
|
122
135
|
});
|
|
123
136
|
});
|
|
124
137
|
|
|
138
|
+
describe("a resolved review is read-only for its owner too", () => {
|
|
139
|
+
test("typing, deleting a card, and editing answer review submitted - read-only", async () => {
|
|
140
|
+
// Arrange: a comment exists and the message is in
|
|
141
|
+
server.core.sessionAnnotate(session.id, {
|
|
142
|
+
id: "a_done",
|
|
143
|
+
kind: "comment",
|
|
144
|
+
anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
|
|
145
|
+
body: "settled",
|
|
146
|
+
});
|
|
147
|
+
server.core.sessionSendMessage(session.id, "approved", "");
|
|
148
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
149
|
+
width: 120,
|
|
150
|
+
height: 32,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await waitForText(setup, "settled");
|
|
154
|
+
const before = snapshot();
|
|
155
|
+
|
|
156
|
+
// Act + Assert: a draft never opens
|
|
157
|
+
await clickText(setup, "daemon");
|
|
158
|
+
await typeText(setup, "c");
|
|
159
|
+
await waitForText(setup, "review submitted - read-only");
|
|
160
|
+
expect(setup.captureCharFrame()).not.toContain("● c");
|
|
161
|
+
|
|
162
|
+
// Act + Assert: the focused card cannot be deleted or edited
|
|
163
|
+
await navCommand(setup, "n");
|
|
164
|
+
await pressKey(setup, "BACKSPACE", { meta: true });
|
|
165
|
+
await pressKey(setup, "e", { ctrl: true });
|
|
166
|
+
await navCommand(setup, "x");
|
|
167
|
+
expect(snapshot()).toEqual(before);
|
|
168
|
+
expect(server.core.sessionGet(session.id).annotations).toHaveLength(1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
125
172
|
describe("observer navigation still works", () => {
|
|
126
|
-
test("
|
|
173
|
+
test("↓ moves the caret between blocks", async () => {
|
|
174
|
+
// Arrange
|
|
127
175
|
const setup = await renderObserver();
|
|
128
|
-
|
|
129
|
-
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
179
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
130
180
|
await setup.renderOnce();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
|
|
182
|
+
// Assert: the caret cell is painted on the paragraph's row
|
|
183
|
+
const caretRow = setup
|
|
184
|
+
.captureSpans()
|
|
185
|
+
.lines.findIndex((line) =>
|
|
186
|
+
line.spans.some((span) => span.bg.toInts().slice(0, 3).join() === "86,91,104"),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(caretRow).toBe(frameRow(setup, "persists sessions"));
|
|
134
190
|
});
|
|
135
191
|
|
|
136
|
-
test("n focuses annotations made by the controller", async () => {
|
|
192
|
+
test("option+n focuses annotations made by the controller", async () => {
|
|
193
|
+
// Arrange
|
|
137
194
|
server.core.sessionAnnotate(session.id, {
|
|
138
195
|
id: "a_obs1",
|
|
139
196
|
kind: "comment",
|
|
@@ -141,8 +198,13 @@ describe("observer navigation still works", () => {
|
|
|
141
198
|
body: "From the controller.",
|
|
142
199
|
});
|
|
143
200
|
const setup = await renderObserver();
|
|
144
|
-
|
|
201
|
+
|
|
202
|
+
// Act - the observer navigates to the annotation without mutating anything
|
|
203
|
+
await navCommand(setup, "n");
|
|
145
204
|
await setup.renderOnce();
|
|
146
|
-
|
|
205
|
+
|
|
206
|
+
// Assert - the observer reads the controller's comment inline in the thread
|
|
207
|
+
await waitForText(setup, "From the controller.");
|
|
208
|
+
expect(snapshot().annotations).toBe(1);
|
|
147
209
|
});
|
|
148
210
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Regressions from the focus/perf round: ctrl+q must quit from any keyboard state, and clicking a
|
|
2
|
+
* sidebar thread must give it the selected accent at once (the cursor follows the click). */
|
|
3
|
+
|
|
4
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
5
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
clickText,
|
|
13
|
+
isolateUserConfig,
|
|
14
|
+
pressKey,
|
|
15
|
+
renderReadyApp,
|
|
16
|
+
waitForText,
|
|
17
|
+
} from "./test-support";
|
|
18
|
+
|
|
19
|
+
const ACCENT_DARK: [number, number, number] = [203, 166, 247];
|
|
20
|
+
|
|
21
|
+
let home: string;
|
|
22
|
+
let restoreUserConfig: () => void;
|
|
23
|
+
let server: DaemonServer;
|
|
24
|
+
|
|
25
|
+
function plan(title: string): void {
|
|
26
|
+
server.core.sessionCreate({
|
|
27
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
28
|
+
artifact: { type: "plan", content: `# ${title}\n`, meta: { title, planPath: "plan.md" } },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-regression-"));
|
|
34
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
|
+
server.start();
|
|
37
|
+
plan("First review");
|
|
38
|
+
plan("Second review");
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
restoreUserConfig();
|
|
42
|
+
server.stop();
|
|
43
|
+
rmSync(home, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The text of the sidebar title span currently painted in the accent colour, or undefined. */
|
|
47
|
+
function accentTitle(setup: Awaited<ReturnType<typeof renderReadyApp>>): string | undefined {
|
|
48
|
+
for (const line of setup.captureSpans().lines) {
|
|
49
|
+
for (const span of line.spans) {
|
|
50
|
+
const rgb = span.fg?.toInts();
|
|
51
|
+
if (
|
|
52
|
+
rgb &&
|
|
53
|
+
rgb[0] === ACCENT_DARK[0] &&
|
|
54
|
+
rgb[1] === ACCENT_DARK[1] &&
|
|
55
|
+
rgb[2] === ACCENT_DARK[2]
|
|
56
|
+
) {
|
|
57
|
+
const text = span.text.trim();
|
|
58
|
+
if (text.includes("review")) return text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test("ctrl+q quits even when a menu holds the keyboard", async () => {
|
|
67
|
+
let exitCode: number | undefined;
|
|
68
|
+
const setup = await renderReadyApp(<App home={home} onExit={(code) => (exitCode = code)} />, {
|
|
69
|
+
width: 120,
|
|
70
|
+
height: 32,
|
|
71
|
+
});
|
|
72
|
+
await waitForText(setup, "review");
|
|
73
|
+
|
|
74
|
+
// open the settings dialog from the top-left gear - it takes the keyboard
|
|
75
|
+
await setup.mockMouse.click(1, 0);
|
|
76
|
+
await waitForText(setup, "Keybinds");
|
|
77
|
+
|
|
78
|
+
await pressKey(setup, "q", { ctrl: true });
|
|
79
|
+
|
|
80
|
+
expect(exitCode).toBe(0);
|
|
81
|
+
setup.renderer.destroy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("clicking a sidebar thread gives it the selected accent at once", async () => {
|
|
85
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
86
|
+
await waitForText(setup, "Second review");
|
|
87
|
+
|
|
88
|
+
// click whichever thread is not already the selected (accent) one
|
|
89
|
+
const target = accentTitle(setup)?.includes("Second") ? "First review" : "Second review";
|
|
90
|
+
await clickText(setup, target);
|
|
91
|
+
|
|
92
|
+
expect(accentTitle(setup)).toContain(target.split(" ")[0]!);
|
|
93
|
+
setup.renderer.destroy();
|
|
94
|
+
});
|