@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
|
@@ -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
|
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** The share shortcut opens the two-column share dialog reachable from any view. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, mock, 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 { Thread } from "@cueloop/schema";
|
|
11
|
+
import { App } from "./App";
|
|
12
|
+
import type { ShareTransport } from "./thread-controller";
|
|
13
|
+
import {
|
|
14
|
+
isolateUserConfig,
|
|
15
|
+
press,
|
|
16
|
+
pressKey,
|
|
17
|
+
settle,
|
|
18
|
+
waitForText,
|
|
19
|
+
waitForTextGone,
|
|
20
|
+
} from "./test-support";
|
|
21
|
+
|
|
22
|
+
const shareTransport: ShareTransport = {
|
|
23
|
+
publish: mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true })),
|
|
24
|
+
pull: mock(async () => {
|
|
25
|
+
throw new Error("Unexpected share pull");
|
|
26
|
+
}),
|
|
27
|
+
push: mock(async () => {}),
|
|
28
|
+
watch: () => () => {},
|
|
29
|
+
revoke: async () => {},
|
|
30
|
+
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
31
|
+
formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
|
|
32
|
+
collaboratorAnnotations: () => [],
|
|
33
|
+
mergeFromShare: () => ({ annotations: [] }),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
37
|
+
|
|
38
|
+
let home: string;
|
|
39
|
+
let restoreUserConfig: () => void;
|
|
40
|
+
let server: DaemonServer;
|
|
41
|
+
let session: Thread;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-share-choice-"));
|
|
45
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
46
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
47
|
+
server.start();
|
|
48
|
+
session = server.core.sessionCreate({
|
|
49
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
50
|
+
artifact: {
|
|
51
|
+
type: "plan",
|
|
52
|
+
content: PLAN,
|
|
53
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
restoreUserConfig();
|
|
59
|
+
server.stop();
|
|
60
|
+
rmSync(home, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("share dialog", () => {
|
|
64
|
+
test("the shortcut opens the two-column share dialog", async () => {
|
|
65
|
+
// Arrange
|
|
66
|
+
const setup = await testRender(
|
|
67
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
68
|
+
{ width: 120, height: 32 },
|
|
69
|
+
);
|
|
70
|
+
await waitForText(setup, "cueloop");
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
74
|
+
|
|
75
|
+
// Assert - the nav categories and the empty-state new-link affordance
|
|
76
|
+
await waitForText(setup, "share externally");
|
|
77
|
+
await waitForText(setup, "export");
|
|
78
|
+
await waitForText(setup, "+ new link");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("new link opens the wizard, and require auth reveals the allowlist", async () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const setup = await testRender(
|
|
84
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
85
|
+
{ width: 120, height: 32 },
|
|
86
|
+
);
|
|
87
|
+
await waitForText(setup, "cueloop");
|
|
88
|
+
|
|
89
|
+
// Act: open the dialog, step into the body, activate "+ new link"
|
|
90
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
91
|
+
await waitForText(setup, "+ new link");
|
|
92
|
+
await press(setup, "enter");
|
|
93
|
+
await press(setup, "enter");
|
|
94
|
+
|
|
95
|
+
// Assert - the wizard's fields
|
|
96
|
+
await waitForText(setup, "link name");
|
|
97
|
+
await waitForText(setup, "require auth");
|
|
98
|
+
|
|
99
|
+
// Act: focus the auth row and switch it on (enter toggles the focused row)
|
|
100
|
+
await press(setup, "down");
|
|
101
|
+
await press(setup, "enter");
|
|
102
|
+
|
|
103
|
+
// Assert - the handles input appears with its grayed placeholder
|
|
104
|
+
await waitForText(setup, "handle");
|
|
105
|
+
|
|
106
|
+
// Act: backspace on the empty allowlist collapses require auth back off
|
|
107
|
+
await press(setup, "backspace");
|
|
108
|
+
|
|
109
|
+
// Assert - the handles section is gone
|
|
110
|
+
await waitForTextGone(setup, "handle");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a private link with no handles cannot be created", async () => {
|
|
114
|
+
// Arrange
|
|
115
|
+
const setup = await testRender(
|
|
116
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
117
|
+
{ width: 120, height: 32 },
|
|
118
|
+
);
|
|
119
|
+
await waitForText(setup, "cueloop");
|
|
120
|
+
|
|
121
|
+
// Act: open the wizard, turn auth on but add no handles, then try to create
|
|
122
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
123
|
+
await waitForText(setup, "+ new link");
|
|
124
|
+
await press(setup, "enter"); // into the body
|
|
125
|
+
await press(setup, "enter"); // open the wizard
|
|
126
|
+
await waitForText(setup, "require auth");
|
|
127
|
+
await press(setup, "down"); // focus the auth row
|
|
128
|
+
await press(setup, "enter"); // toggle auth on (focus moves to the empty allowlist)
|
|
129
|
+
await press(setup, "down"); // move to the create action
|
|
130
|
+
await press(setup, "enter"); // create is blocked while the allowlist is empty
|
|
131
|
+
await settle(setup);
|
|
132
|
+
|
|
133
|
+
// Assert - the wizard is still open (nothing was created and the dialog did not fall back to the list)
|
|
134
|
+
expect(setup.captureCharFrame()).toContain("require auth");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("right enters the links body and left returns to the nav", async () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
const setup = await testRender(
|
|
140
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
141
|
+
{ width: 120, height: 32 },
|
|
142
|
+
);
|
|
143
|
+
await waitForText(setup, "cueloop");
|
|
144
|
+
|
|
145
|
+
// Act + Assert: right arrow crosses into the body (its hint appears)
|
|
146
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
147
|
+
await waitForText(setup, "+ new link");
|
|
148
|
+
await press(setup, "right");
|
|
149
|
+
await waitForText(setup, "h back");
|
|
150
|
+
|
|
151
|
+
// Act + Assert: left arrow returns to the nav (its hint returns)
|
|
152
|
+
await press(setup, "left");
|
|
153
|
+
await waitForText(setup, "l/enter open");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("escape closes the dialog", async () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
const setup = await testRender(
|
|
159
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
160
|
+
{ width: 120, height: 32 },
|
|
161
|
+
);
|
|
162
|
+
await waitForText(setup, "cueloop");
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
166
|
+
await waitForText(setup, "share externally");
|
|
167
|
+
await press(setup, "escape");
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
await waitForTextGone(setup, "share externally");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/** The send-message confirm: cmd+enter opens a centered overlay with a message selector, summary input, and word-buttons. Char-frame assertions over the real App and an in-process daemon, like App.test.tsx. */
|
|
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 { makeAnchor, parseBlocks, type Thread } from "@cueloop/schema";
|
|
11
|
+
import { App } from "./App";
|
|
12
|
+
import {
|
|
13
|
+
isolateUserConfig,
|
|
14
|
+
press,
|
|
15
|
+
pressKey,
|
|
16
|
+
settle,
|
|
17
|
+
waitForText,
|
|
18
|
+
waitForTextGone,
|
|
19
|
+
} from "./test-support";
|
|
20
|
+
|
|
21
|
+
const PLAN = `# Migration Plan
|
|
22
|
+
|
|
23
|
+
## Context
|
|
24
|
+
|
|
25
|
+
The daemon persists sessions to disk atomically.
|
|
26
|
+
|
|
27
|
+
## Steps
|
|
28
|
+
|
|
29
|
+
- move the store
|
|
30
|
+
- add recovery
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
let home: string;
|
|
34
|
+
let restoreUserConfig: () => void;
|
|
35
|
+
let server: DaemonServer;
|
|
36
|
+
let session: Thread;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-confirm-"));
|
|
40
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
41
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
42
|
+
server.start();
|
|
43
|
+
session = server.core.sessionCreate({
|
|
44
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
45
|
+
artifact: {
|
|
46
|
+
type: "plan",
|
|
47
|
+
content: PLAN,
|
|
48
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
restoreUserConfig();
|
|
54
|
+
server.stop();
|
|
55
|
+
rmSync(home, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function renderApp(options: { readOnly?: boolean } = {}) {
|
|
59
|
+
const setup = await testRender(
|
|
60
|
+
<App home={home} sessionId={session.id} readOnly={options.readOnly ?? false} />,
|
|
61
|
+
{
|
|
62
|
+
width: 120,
|
|
63
|
+
height: 32,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await waitForText(setup, "cueloop");
|
|
68
|
+
|
|
69
|
+
return setup;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Seed annotations directly through the daemon core (all on one block). */
|
|
73
|
+
function seedAnnotations(count: number): void {
|
|
74
|
+
const blocks = parseBlocks(PLAN);
|
|
75
|
+
|
|
76
|
+
for (let index = 1; index <= count; index++) {
|
|
77
|
+
server.core.sessionAnnotate(session.id, {
|
|
78
|
+
id: `a_confirm_${index}`,
|
|
79
|
+
kind: "comment",
|
|
80
|
+
anchor: makeAnchor(blocks, 2, 0, 10),
|
|
81
|
+
body: `note ${String(index).padStart(2, "0")}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("send message confirm", () => {
|
|
87
|
+
test("cmd+enter opens the send-message confirm overlay with stable message choices", async () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const setup = await renderApp();
|
|
90
|
+
|
|
91
|
+
// Assert - the footer carries the send control before the overlay opens
|
|
92
|
+
expect(setup.captureCharFrame()).toContain("Send message (0)");
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
96
|
+
|
|
97
|
+
// Assert - the overlay: message selector and word-buttons
|
|
98
|
+
await waitForText(setup, "[approve]"); // nothing pending: approve default
|
|
99
|
+
const initialFrame = setup.captureCharFrame();
|
|
100
|
+
const initialSelector = initialFrame
|
|
101
|
+
.split("\n")
|
|
102
|
+
.find((row) => row.includes("[comment] [approve] [changes]"));
|
|
103
|
+
|
|
104
|
+
expect(initialSelector).toBeDefined();
|
|
105
|
+
expect(initialFrame).toContain(" send ");
|
|
106
|
+
expect(initialFrame).toContain(" cancel ");
|
|
107
|
+
|
|
108
|
+
// Act - move the active choice across the selector
|
|
109
|
+
await press(setup, "right");
|
|
110
|
+
const changesSelector = setup
|
|
111
|
+
.captureCharFrame()
|
|
112
|
+
.split("\n")
|
|
113
|
+
.find((row) => row.includes("[comment] [approve] [changes]"));
|
|
114
|
+
await press(setup, "right");
|
|
115
|
+
const commentSelector = setup
|
|
116
|
+
.captureCharFrame()
|
|
117
|
+
.split("\n")
|
|
118
|
+
.find((row) => row.includes("[comment] [approve] [changes]"));
|
|
119
|
+
|
|
120
|
+
// Assert - active styling does not move or resize any choice
|
|
121
|
+
expect(changesSelector).toBe(initialSelector);
|
|
122
|
+
expect(commentSelector).toBe(initialSelector);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("left/right cycles the message selector in the overlay", async () => {
|
|
126
|
+
// Arrange
|
|
127
|
+
const setup = await renderApp();
|
|
128
|
+
|
|
129
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
await waitForText(setup, "[approve]");
|
|
133
|
+
|
|
134
|
+
// Act
|
|
135
|
+
await press(setup, "right");
|
|
136
|
+
|
|
137
|
+
// Assert
|
|
138
|
+
await waitForText(setup, "[changes]");
|
|
139
|
+
|
|
140
|
+
// Act
|
|
141
|
+
await press(setup, "right");
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
await waitForText(setup, "[comment]");
|
|
145
|
+
|
|
146
|
+
// Act
|
|
147
|
+
await press(setup, "left");
|
|
148
|
+
|
|
149
|
+
// Assert
|
|
150
|
+
await waitForText(setup, "[changes]");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("esc closes the overlay", async () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
const setup = await renderApp();
|
|
156
|
+
|
|
157
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
158
|
+
|
|
159
|
+
// Assert
|
|
160
|
+
await waitForText(setup, "[approve]");
|
|
161
|
+
|
|
162
|
+
// Act
|
|
163
|
+
await press(setup, "escape");
|
|
164
|
+
|
|
165
|
+
// Assert - a bare ESC settles after the parser's escape-sequence window
|
|
166
|
+
const frame = await waitForTextGone(setup, "[approve]");
|
|
167
|
+
|
|
168
|
+
expect(frame).not.toContain(" cancel ");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("enter in the overlay resolves the session through the controller", async () => {
|
|
172
|
+
// Arrange
|
|
173
|
+
seedAnnotations(1);
|
|
174
|
+
const setup = await renderApp();
|
|
175
|
+
|
|
176
|
+
// Act
|
|
177
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
178
|
+
|
|
179
|
+
// Assert - opens on the default message, then cycle to request changes
|
|
180
|
+
await waitForText(setup, "[approve]");
|
|
181
|
+
await press(setup, "right");
|
|
182
|
+
await waitForText(setup, "[changes]");
|
|
183
|
+
|
|
184
|
+
// Act
|
|
185
|
+
await setup.mockInput.typeText("Tighten the steps.");
|
|
186
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
187
|
+
|
|
188
|
+
// Assert - the completion flow after submit is unchanged
|
|
189
|
+
await waitForText(setup, "feedback sent");
|
|
190
|
+
const stored = server.core.sessionGet(session.id);
|
|
191
|
+
|
|
192
|
+
expect(stored.status).toBe("resolved");
|
|
193
|
+
expect(stored.message!.outcome).toBe("changes_requested");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("Comment sends feedback while leaving the Thread open", async () => {
|
|
197
|
+
seedAnnotations(1);
|
|
198
|
+
const setup = await renderApp();
|
|
199
|
+
|
|
200
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
201
|
+
await waitForText(setup, "[approve]");
|
|
202
|
+
await press(setup, "left");
|
|
203
|
+
await waitForText(setup, "[comment]");
|
|
204
|
+
|
|
205
|
+
await setup.mockInput.typeText("For now.");
|
|
206
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
207
|
+
|
|
208
|
+
await waitForText(setup, "comment sent - thread stays open");
|
|
209
|
+
const stored = server.core.sessionGet(session.id);
|
|
210
|
+
|
|
211
|
+
expect(stored.status).toBe("pending");
|
|
212
|
+
expect(stored.message!.outcome).toBe("comment");
|
|
213
|
+
expect(stored.message!.annotations?.map((annotation) => annotation.id)).toEqual([
|
|
214
|
+
"a_confirm_1",
|
|
215
|
+
]);
|
|
216
|
+
expect(setup.captureCharFrame()).not.toContain("feedback sent");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("typing / in the summary opens the skills/actions palette", async () => {
|
|
220
|
+
// Arrange
|
|
221
|
+
seedAnnotations(1);
|
|
222
|
+
const setup = await renderApp();
|
|
223
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
224
|
+
await waitForText(setup, "[approve]");
|
|
225
|
+
|
|
226
|
+
// Act
|
|
227
|
+
await setup.mockInput.typeText("/restate");
|
|
228
|
+
|
|
229
|
+
// Assert - the same slash palette the inline composer shows
|
|
230
|
+
await waitForText(setup, "restate-simplified");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("pasting an image into the summary drops in an [Image #n] placeholder", async () => {
|
|
234
|
+
// Arrange
|
|
235
|
+
const setup = await renderApp();
|
|
236
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
237
|
+
await waitForText(setup, "[approve]");
|
|
238
|
+
|
|
239
|
+
// Act - a burst of control bytes stands in for the binary an image paste delivers
|
|
240
|
+
await setup.mockInput.typeText("look at this ");
|
|
241
|
+
await setup.mockInput.pasteBracketedText("");
|
|
242
|
+
|
|
243
|
+
// Assert - the raw bytes never reach the draft; a numbered placeholder does
|
|
244
|
+
await waitForText(setup, "[Image #1]");
|
|
245
|
+
expect(setup.captureCharFrame()).toContain("look at this [Image #1]");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("read-only observers cannot open the confirm overlay", async () => {
|
|
249
|
+
// Arrange
|
|
250
|
+
seedAnnotations(1);
|
|
251
|
+
const setup = await renderApp({ readOnly: true });
|
|
252
|
+
|
|
253
|
+
// Act
|
|
254
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
255
|
+
await settle(setup);
|
|
256
|
+
|
|
257
|
+
// Assert - no overlay, and the session stays pending
|
|
258
|
+
const frame = setup.captureCharFrame();
|
|
259
|
+
|
|
260
|
+
expect(frame).not.toContain("[approve]");
|
|
261
|
+
expect(frame).not.toContain(" cancel ");
|
|
262
|
+
expect(server.core.sessionGet(session.id).status).toBe("pending");
|
|
263
|
+
});
|
|
264
|
+
});
|