@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.80
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 +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -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 +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -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 +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -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 +574 -0
- package/src/app-view-model.ts +162 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -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.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -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 +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -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 +876 -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/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 +59 -0
- package/src/components/ProjectTreeView.tsx +111 -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 +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -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 +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -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 +56 -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 +517 -33
- package/src/config.ts +467 -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 +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -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 +64 -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 +78 -13
- 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 +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -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 +1893 -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 +148 -0
- package/src/use-annotation-surface.tsx +915 -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-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -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 +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- 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 -312
|
@@ -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
|
+
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
/** Plan rendering
|
|
1
|
+
/** Plan rendering: code blocks verbatim + contained, spacing never collapses. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
-
import type {
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
11
10
|
import { App } from "./App";
|
|
11
|
+
import { isolateUserConfig, waitForText, renderReadyApp } from "./test-support";
|
|
12
12
|
|
|
13
13
|
const PLAN = `# Render Plan
|
|
14
14
|
|
|
@@ -29,11 +29,13 @@ Deploys block below the threshold.
|
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
let home: string;
|
|
32
|
+
let restoreUserConfig: () => void;
|
|
32
33
|
let server: DaemonServer;
|
|
33
|
-
let session:
|
|
34
|
+
let session: Thread;
|
|
34
35
|
|
|
35
36
|
beforeEach(() => {
|
|
36
37
|
home = mkdtempSync(join(tmpdir(), "cueloop-render-"));
|
|
38
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
37
39
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
38
40
|
server.start();
|
|
39
41
|
session = server.core.sessionCreate({
|
|
@@ -42,67 +44,91 @@ beforeEach(() => {
|
|
|
42
44
|
});
|
|
43
45
|
});
|
|
44
46
|
afterEach(() => {
|
|
47
|
+
restoreUserConfig();
|
|
45
48
|
server.stop();
|
|
46
49
|
rmSync(home, { recursive: true, force: true });
|
|
47
50
|
});
|
|
48
51
|
|
|
49
52
|
async function frame(): Promise<string> {
|
|
50
|
-
const setup = await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
await
|
|
58
|
-
|
|
53
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
54
|
+
width: 120,
|
|
55
|
+
height: 36,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await waitForText(setup, "Render Plan");
|
|
59
|
+
// async highlights settle within the visual-idle wait
|
|
60
|
+
await setup.waitForVisualIdle();
|
|
61
|
+
|
|
59
62
|
return setup.captureCharFrame();
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
describe("code blocks", () => {
|
|
63
66
|
test("lines render verbatim: indentation preserved, no word-wrap", async () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// Arrange
|
|
68
|
+
const rendered = await frame();
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(rendered).toContain(" const blocked = full < 0.6;");
|
|
72
|
+
expect(rendered).toContain(' return <text>{blocked ? "blocked" : "clear"}</text>;');
|
|
67
73
|
});
|
|
68
74
|
|
|
69
75
|
test("the block carries its language tag", async () => {
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
// Arrange
|
|
77
|
+
const rendered = await frame();
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
expect(rendered).toContain("tsx");
|
|
72
81
|
});
|
|
73
82
|
});
|
|
74
83
|
|
|
75
84
|
describe("block spacing", () => {
|
|
85
|
+
// the review rail paints box edges on the right, so read the document
|
|
86
|
+
// column (everything left of the first rail glyph) before asserting spacing
|
|
87
|
+
const planColumn = (line: string): string => line.split(/[│╭╰]/)[0]!.trimEnd();
|
|
88
|
+
|
|
76
89
|
test("a code block never glues to the list item above it", async () => {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
// Arrange
|
|
91
|
+
const lines = (await frame()).split("\n").map(planColumn);
|
|
92
|
+
const listItemLineIndex = lines.findIndex((line) => line.includes("wire the duck over MQTT"));
|
|
93
|
+
const codeHeader = lines.findIndex((line) => line.trim().startsWith("tsx"));
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
expect(listItemLineIndex).toBeGreaterThan(-1);
|
|
97
|
+
expect(codeHeader).toBeGreaterThan(listItemLineIndex);
|
|
98
|
+
expect(lines.slice(listItemLineIndex + 1, codeHeader).some((line) => line === "")).toBe(true);
|
|
83
99
|
});
|
|
84
100
|
|
|
85
101
|
test("a heading never sits directly on the previous block", async () => {
|
|
86
|
-
|
|
87
|
-
const
|
|
102
|
+
// Arrange
|
|
103
|
+
const lines = (await frame()).split("\n").map(planColumn);
|
|
104
|
+
const gate = lines.findIndex((line) => line.trim() === "Gate");
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
88
107
|
expect(gate).toBeGreaterThan(0);
|
|
89
108
|
expect(lines[gate - 1]).toBe("");
|
|
90
109
|
});
|
|
91
110
|
|
|
92
111
|
test("consecutive list items stay tight", async () => {
|
|
112
|
+
// Arrange
|
|
93
113
|
const listPlan = "# T\n\n- one\n- two\n";
|
|
94
114
|
const s2 = server.core.sessionCreate({
|
|
95
115
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
96
116
|
artifact: { type: "plan", content: listPlan, meta: {} },
|
|
97
117
|
});
|
|
98
|
-
const setup = await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
const setup = await renderReadyApp(<App home={home} sessionId={s2.id} />, {
|
|
119
|
+
width: 120,
|
|
120
|
+
height: 30,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await waitForText(setup, "one");
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
const lines = setup
|
|
127
|
+
.captureCharFrame()
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((line) => line.trimEnd());
|
|
130
|
+
const one = lines.findIndex((line) => line.includes("· one"));
|
|
131
|
+
|
|
132
|
+
expect(lines[one + 1]).toContain("· two");
|
|
107
133
|
});
|
|
108
134
|
});
|