@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,21 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The plan review surface v2 (#71): native selection feeds the annotation
|
|
3
|
-
* quote, the inline compose box keeps its anchor painted, the rail edits what
|
|
4
|
-
* the document selects, and edit-exit reconciliation orphans annotations
|
|
5
|
-
* whose passage was removed. Char-frame + styled-span assertions over the
|
|
6
|
-
* real App and a real in-process daemon.
|
|
7
|
-
*/
|
|
1
|
+
/** The plan review surface v2: native selection feeds the annotation quote, the inline compose box keeps its anchor painted, the rail edits what the document selects, and edit-exit reconciliation orphans annotations whose passage was removed. Char-frame + styled-span assertions over the real App and a real in-process daemon. */
|
|
8
2
|
|
|
9
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
11
5
|
import { tmpdir } from "node:os";
|
|
12
6
|
import { join } from "node:path";
|
|
13
7
|
import React from "react";
|
|
14
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
15
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
16
|
-
import type {
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
17
10
|
import { App } from "./App";
|
|
18
|
-
import {
|
|
11
|
+
import { DEFAULT_QUICK_ACTIONS } from "./config";
|
|
12
|
+
import { slashItemsFrom } from "./slash-palette";
|
|
13
|
+
import {
|
|
14
|
+
clickText,
|
|
15
|
+
dragText,
|
|
16
|
+
isolateUserConfig,
|
|
17
|
+
navCommand,
|
|
18
|
+
press,
|
|
19
|
+
pressKey,
|
|
20
|
+
renderReadyApp,
|
|
21
|
+
typeText as type,
|
|
22
|
+
waitForState,
|
|
23
|
+
waitForText,
|
|
24
|
+
waitForTextGone,
|
|
25
|
+
} from "./test-support";
|
|
19
26
|
|
|
20
27
|
const PLAN = `# Migration Plan
|
|
21
28
|
|
|
@@ -31,53 +38,41 @@ The daemon persists sessions to disk atomically.
|
|
|
31
38
|
|
|
32
39
|
let home: string;
|
|
33
40
|
let server: DaemonServer;
|
|
34
|
-
let session:
|
|
41
|
+
let session: Thread;
|
|
42
|
+
let restoreUserConfig: () => void;
|
|
35
43
|
|
|
36
44
|
beforeEach(() => {
|
|
37
45
|
home = mkdtempSync(join(tmpdir(), "cueloop-plan-review-"));
|
|
46
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
38
47
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
39
48
|
server.start();
|
|
40
49
|
session = server.core.sessionCreate({
|
|
41
50
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
42
|
-
artifact: {
|
|
51
|
+
artifact: {
|
|
52
|
+
type: "plan",
|
|
53
|
+
content: PLAN,
|
|
54
|
+
meta: { title: "Migration Plan", planPath: "plan.md", agent: "agent/worker-3" },
|
|
55
|
+
},
|
|
43
56
|
});
|
|
44
57
|
});
|
|
45
58
|
afterEach(() => {
|
|
59
|
+
restoreUserConfig();
|
|
46
60
|
server.stop();
|
|
47
61
|
rmSync(home, { recursive: true, force: true });
|
|
48
62
|
});
|
|
49
63
|
|
|
50
64
|
async function renderApp() {
|
|
51
|
-
const setup = await
|
|
65
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
52
66
|
width: 120,
|
|
53
67
|
height: 32,
|
|
54
68
|
});
|
|
55
|
-
for (let attempt = 0; attempt < 40 && !setup.captureCharFrame().includes("cueloop"); attempt++) {
|
|
56
|
-
await Bun.sleep(25);
|
|
57
|
-
await setup.renderOnce();
|
|
58
|
-
}
|
|
59
|
-
await setup.renderOnce();
|
|
60
|
-
return setup;
|
|
61
|
-
}
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
await waitForText(setup, "cueloop");
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
67
|
-
else if (k === "escape") {
|
|
68
|
-
setup.mockInput.pressKey("ESCAPE");
|
|
69
|
-
// a bare ESC byte is an ambiguous sequence prefix; the parser settles late
|
|
70
|
-
await Bun.sleep(120);
|
|
71
|
-
} else await type(setup, k);
|
|
72
|
-
await Bun.sleep(15);
|
|
73
|
-
await setup.renderOnce();
|
|
72
|
+
return setup;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
await setup.mockInput.typeText(text);
|
|
78
|
-
await Bun.sleep(15);
|
|
79
|
-
await setup.renderOnce();
|
|
80
|
-
}
|
|
75
|
+
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
81
76
|
|
|
82
77
|
/**
|
|
83
78
|
* Background colors (hex) of every styled span containing the needle - the
|
|
@@ -85,175 +80,349 @@ async function type(setup: Setup, text: string): Promise<void> {
|
|
|
85
80
|
*/
|
|
86
81
|
function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
87
82
|
const backgrounds: string[] = [];
|
|
83
|
+
|
|
88
84
|
for (const line of setup.captureSpans().lines) {
|
|
89
85
|
for (const span of line.spans) {
|
|
90
86
|
if (!span.text.includes(needle)) continue;
|
|
91
87
|
const [red, green, blue] = span.bg.toInts();
|
|
92
|
-
|
|
88
|
+
|
|
89
|
+
backgrounds.push(
|
|
90
|
+
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
91
|
+
);
|
|
93
92
|
}
|
|
94
93
|
}
|
|
94
|
+
|
|
95
95
|
return backgrounds;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
async
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
98
|
+
describe("share button", () => {
|
|
99
|
+
test("the owner's plan header shows the Share button next to Edit", async () => {
|
|
100
|
+
// Arrange / Act
|
|
101
|
+
const setup = await renderApp();
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
// Assert
|
|
104
|
+
// both word-buttons ride the header's thread segment, so share sits next to edit
|
|
105
|
+
const headerLine = setup
|
|
106
|
+
.captureCharFrame()
|
|
107
|
+
.split("\n")
|
|
108
|
+
.find((line) => line.includes("edit"));
|
|
109
|
+
|
|
110
|
+
expect(headerLine).toContain("share");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a read-only viewer (a plan shared over ssh) never sees the Share button", async () => {
|
|
114
|
+
// Arrange / Act
|
|
115
|
+
const viewer = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
116
|
+
width: 120,
|
|
117
|
+
height: 32,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await waitForText(viewer, "cueloop");
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(viewer.captureCharFrame()).not.toContain(" share ");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a resolved plan hides Edit and Share (no re-sharing a finished review)", async () => {
|
|
127
|
+
// Arrange - resolve the session before opening it
|
|
128
|
+
server.core.sessionSendMessage(session.id, "approved", "");
|
|
129
|
+
|
|
130
|
+
// Act
|
|
106
131
|
const setup = await renderApp();
|
|
107
|
-
|
|
108
|
-
await
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
|
|
133
|
+
await waitForText(setup, "Migration Plan");
|
|
134
|
+
|
|
135
|
+
// Assert - the owner toolbar is gone once the review is resolved
|
|
136
|
+
const frame = setup.captureCharFrame();
|
|
137
|
+
|
|
138
|
+
expect(frame).not.toContain(" edit ");
|
|
139
|
+
expect(frame).not.toContain(" share ");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("edit affordance", () => {
|
|
144
|
+
test("the owner sees the Edit button", async () => {
|
|
145
|
+
// Arrange / Act
|
|
146
|
+
const owner = await renderApp();
|
|
147
|
+
|
|
148
|
+
// Assert
|
|
149
|
+
expect(owner.captureCharFrame()).toContain(" edit ");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a read-only viewer (a plan shared over ssh) never sees the Edit button", async () => {
|
|
153
|
+
// Arrange / Act
|
|
154
|
+
const viewer = await renderReadyApp(<App home={home} sessionId={session.id} readOnly />, {
|
|
155
|
+
width: 120,
|
|
156
|
+
height: 32,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await waitForText(viewer, "cueloop");
|
|
160
|
+
|
|
161
|
+
// Assert
|
|
162
|
+
expect(viewer.captureCharFrame()).not.toContain(" edit ");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("marks feed the comment anchor", () => {
|
|
167
|
+
test("a drag marks a character-precise span and typing anchors the comment to it", async () => {
|
|
168
|
+
// Arrange
|
|
169
|
+
const setup = await renderApp();
|
|
170
|
+
|
|
171
|
+
// Act
|
|
172
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
112
173
|
await type(setup, "Which daemon?");
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(
|
|
118
|
-
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
174
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
175
|
+
|
|
176
|
+
// Assert
|
|
177
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
178
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
|
|
119
179
|
});
|
|
120
180
|
|
|
121
|
-
test("a
|
|
181
|
+
test("a drag that ends mid-sentence keeps exactly the dragged words", async () => {
|
|
182
|
+
// Arrange
|
|
122
183
|
const setup = await renderApp();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
expect(row).toBeGreaterThan(-1);
|
|
127
|
-
// the drag's end cell is exclusive: release one past the last character
|
|
128
|
-
await setup.mockMouse.drag(startColumn, row, startColumn + "persists sessions".length, row);
|
|
129
|
-
await setup.renderOnce();
|
|
130
|
-
await press(setup, "c");
|
|
131
|
-
expect(setup.captureCharFrame()).toContain('comment on "persists sessions"');
|
|
184
|
+
|
|
185
|
+
// Act: release one past the last character of "sessions"
|
|
186
|
+
await dragText(setup, "persists", "sessions", "sessions".length);
|
|
132
187
|
await type(setup, "sessions plural?");
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
192
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe(
|
|
193
|
+
"persists sessions",
|
|
194
|
+
);
|
|
137
195
|
});
|
|
138
196
|
});
|
|
139
197
|
|
|
140
|
-
|
|
141
|
-
|
|
198
|
+
/** The thread view's mark backdrop in the dark palette. */
|
|
199
|
+
const THREAD_MARK = "#463852";
|
|
200
|
+
|
|
201
|
+
describe("the mark stays painted while composing", () => {
|
|
202
|
+
test("the mark paints while composing, escape un-paints, save keeps it as the comment's mark", async () => {
|
|
203
|
+
// Arrange
|
|
142
204
|
const setup = await renderApp();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await
|
|
146
|
-
await
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
expect(backgroundsOf(setup, "The daemon")).toContain(
|
|
151
|
-
|
|
205
|
+
|
|
206
|
+
// Act
|
|
207
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
208
|
+
await type(setup, "x");
|
|
209
|
+
|
|
210
|
+
// Assert: composing, the marked words carry the mark backdrop
|
|
211
|
+
await waitForText(setup, "● x");
|
|
212
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
213
|
+
|
|
214
|
+
// Act: escape discards the draft and the mark stays for re-typing; the next escape
|
|
215
|
+
// enters nav mode with the mark still held; only an escape in nav drops it
|
|
216
|
+
await press(setup, "escape");
|
|
217
|
+
await waitForTextGone(setup, "● x");
|
|
218
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
152
219
|
await press(setup, "escape");
|
|
153
|
-
|
|
154
|
-
expect(backgroundsOf(setup, "The daemon")).
|
|
155
|
-
|
|
156
|
-
|
|
220
|
+
await waitForText(setup, "type to leave");
|
|
221
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
222
|
+
await press(setup, "escape");
|
|
223
|
+
|
|
224
|
+
// Assert
|
|
225
|
+
await waitForState(setup, () => !backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
|
|
226
|
+
|
|
227
|
+
// Act: mark again, save
|
|
228
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
157
229
|
await type(setup, "Which daemon?");
|
|
158
|
-
await
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
230
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
231
|
+
|
|
232
|
+
// Assert: the saved comment keeps the passage marked
|
|
233
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
234
|
+
await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
|
|
235
|
+
}, 60_000);
|
|
236
|
+
|
|
237
|
+
test("backspace on an empty draft dismisses the composer back to the mark", async () => {
|
|
238
|
+
// Arrange
|
|
239
|
+
const setup = await renderApp();
|
|
240
|
+
|
|
241
|
+
// Act: mark, type one character, then delete it and backspace again on the now-empty draft
|
|
242
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
243
|
+
await type(setup, "x");
|
|
244
|
+
await waitForText(setup, "● x");
|
|
245
|
+
await press(setup, "backspace");
|
|
246
|
+
await press(setup, "backspace");
|
|
247
|
+
|
|
248
|
+
// Assert: the composer is gone and the mark stays, ready to re-type
|
|
249
|
+
await waitForTextGone(setup, "● x");
|
|
250
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
251
|
+
}, 60_000);
|
|
252
|
+
|
|
253
|
+
test("esc from type mode keeps the mark, so c in nav comments on the marked text", async () => {
|
|
254
|
+
// Arrange
|
|
255
|
+
const setup = await renderApp();
|
|
256
|
+
|
|
257
|
+
// Act: mark, enter nav with esc, open the composer with c, save
|
|
258
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
259
|
+
await navCommand(setup, "c");
|
|
260
|
+
await type(setup, "Which daemon?");
|
|
261
|
+
await waitForText(setup, "● Which daemon?");
|
|
262
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
263
|
+
|
|
264
|
+
// Assert: the comment anchors to the mark, not to the word under the caret
|
|
265
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
266
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
|
|
267
|
+
}, 60_000);
|
|
166
268
|
});
|
|
167
269
|
|
|
168
|
-
describe("
|
|
169
|
-
test("
|
|
270
|
+
describe("compose newline convention", () => {
|
|
271
|
+
test("enter breaks the line; cmd+enter saves the multiline body verbatim", async () => {
|
|
272
|
+
// Arrange
|
|
170
273
|
const setup = await renderApp();
|
|
171
|
-
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
await
|
|
176
|
-
await setup.renderOnce();
|
|
177
|
-
expect(setup.captureCharFrame()).toContain("COMMENT · pending");
|
|
178
|
-
// the saved card is selected; e turns its body into an input in place
|
|
179
|
-
await press(setup, "e");
|
|
180
|
-
await type(setup, " And a link.");
|
|
274
|
+
|
|
275
|
+
await clickText(setup, "daemon");
|
|
276
|
+
|
|
277
|
+
// Act
|
|
278
|
+
await type(setup, "first line");
|
|
181
279
|
await press(setup, "enter");
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
expect(
|
|
186
|
-
|
|
280
|
+
await type(setup, "second line");
|
|
281
|
+
|
|
282
|
+
// Assert: still composing, the newline did not save
|
|
283
|
+
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
284
|
+
|
|
285
|
+
// Act
|
|
286
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
287
|
+
|
|
288
|
+
// Assert
|
|
289
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
290
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("first line\nsecond line");
|
|
187
291
|
});
|
|
188
292
|
|
|
189
|
-
test("
|
|
293
|
+
test("escape cancels the composer without saving", async () => {
|
|
294
|
+
// Arrange
|
|
190
295
|
const setup = await renderApp();
|
|
191
|
-
|
|
192
|
-
await
|
|
193
|
-
await type(setup, "
|
|
194
|
-
await
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await
|
|
201
|
-
await setup.renderOnce();
|
|
296
|
+
|
|
297
|
+
await clickText(setup, "daemon");
|
|
298
|
+
await type(setup, "never saved");
|
|
299
|
+
await waitForText(setup, "● never saved");
|
|
300
|
+
|
|
301
|
+
// Act
|
|
302
|
+
await press(setup, "escape");
|
|
303
|
+
|
|
304
|
+
// Assert
|
|
305
|
+
await waitForTextGone(setup, "● never saved");
|
|
202
306
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
203
|
-
expect(setup.captureCharFrame()).not.toContain("COMMENT · pending");
|
|
204
|
-
expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.markCommentBg);
|
|
205
307
|
});
|
|
206
308
|
});
|
|
207
309
|
|
|
208
310
|
describe("sheet header", () => {
|
|
209
|
-
test("
|
|
311
|
+
test("mirrors the thread name and carries the Edit and Share word-buttons", async () => {
|
|
312
|
+
// Arrange
|
|
210
313
|
const setup = await renderApp();
|
|
314
|
+
|
|
315
|
+
// Assert - the minimal header shows the thread title (mirror) plus the owner
|
|
316
|
+
// actions; the revision and submitter metadata are no longer in the header
|
|
211
317
|
const frame = setup.captureCharFrame();
|
|
212
|
-
|
|
213
|
-
expect(frame).toContain("
|
|
318
|
+
|
|
319
|
+
expect(frame).toContain("Migration Plan");
|
|
320
|
+
expect(frame).not.toContain("submitted by");
|
|
321
|
+
expect(frame).not.toContain("rev 1");
|
|
322
|
+
expect(frame).toContain(" edit ");
|
|
323
|
+
expect(frame).toContain(" share ");
|
|
214
324
|
});
|
|
325
|
+
});
|
|
215
326
|
|
|
216
|
-
|
|
327
|
+
describe("quick-actions settings editor", () => {
|
|
328
|
+
test("expanding an action edits its system prompt inline and persists it", async () => {
|
|
329
|
+
// Arrange
|
|
217
330
|
const setup = await renderApp();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await setup
|
|
331
|
+
|
|
332
|
+
// Act - open Settings from the top-left gear, enter Actions, expand the first action, type
|
|
333
|
+
await setup.mockMouse.click(1, 0);
|
|
334
|
+
await waitForText(setup, "Keybinds");
|
|
335
|
+
await clickText(setup, "settings");
|
|
336
|
+
await clickText(setup, "Actions");
|
|
337
|
+
await clickText(setup, "Zoom out, research in depth");
|
|
338
|
+
await type(setup, "CUSTOM");
|
|
222
339
|
await setup.renderOnce();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
expect(
|
|
226
|
-
|
|
340
|
+
|
|
341
|
+
// Assert - the keystrokes reached the focused input (not swallowed by nav) and persisted
|
|
342
|
+
expect(setup.captureCharFrame()).toContain("CUSTOM");
|
|
343
|
+
await waitForState(setup, () =>
|
|
344
|
+
readFileSync(join(home, "no-config.toml"), "utf8").includes("CUSTOM"),
|
|
345
|
+
);
|
|
227
346
|
});
|
|
228
347
|
});
|
|
229
348
|
|
|
230
349
|
describe("edit-exit reconciliation", () => {
|
|
231
350
|
test("an edit that removes an anchored passage orphans the annotation and shows the banner", async () => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
351
|
+
// Arrange - anchor a comment to a passage
|
|
352
|
+
const setup = await renderApp();
|
|
353
|
+
|
|
354
|
+
await clickText(setup, "daemon");
|
|
355
|
+
await type(setup, "Anchor me to the doomed passage.");
|
|
356
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
357
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
358
|
+
|
|
359
|
+
// Act - open the inline editor, replace the whole body (dropping the passage), and leave
|
|
360
|
+
await pressKey(setup, "e", { ctrl: true });
|
|
361
|
+
await waitForText(setup, "save & close");
|
|
362
|
+
await pressKey(setup, "a", { meta: true });
|
|
363
|
+
await type(setup, "A fresh body with no anchored passage.");
|
|
364
|
+
await clickText(setup, "normal");
|
|
365
|
+
|
|
366
|
+
// Assert - the thread view banner reports the orphaned annotation
|
|
367
|
+
await waitForText(setup, "1 annotation no longer match - the passage was removed.");
|
|
368
|
+
// the annotation is NOT deleted: the feedback serializer handles orphans
|
|
369
|
+
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("thread switch preserves an open edit", () => {
|
|
374
|
+
test("clicking another thread while editing saves the body into the leaving thread", async () => {
|
|
375
|
+
// Arrange - a second thread sits in the sidebar to switch to
|
|
376
|
+
server.core.sessionCreate({
|
|
377
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
378
|
+
artifact: {
|
|
379
|
+
type: "plan",
|
|
380
|
+
content: "# Second Thread\n\nAnother body.\n",
|
|
381
|
+
meta: { title: "Second Thread", planPath: "plan.md", agent: "agent/worker-3" },
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const setup = await renderApp();
|
|
385
|
+
|
|
386
|
+
// open the Threads sidebar so the other thread is on screen to click
|
|
387
|
+
await setup.mockMouse.click(4, 0);
|
|
388
|
+
await waitForText(setup, "Second Thread");
|
|
389
|
+
|
|
390
|
+
// Act - open the inline editor, rewrite the body, then click away before an explicit save
|
|
391
|
+
await pressKey(setup, "e", { ctrl: true });
|
|
392
|
+
await waitForText(setup, "save & close");
|
|
393
|
+
await pressKey(setup, "a", { meta: true });
|
|
394
|
+
await type(setup, "Body kept across a thread switch.");
|
|
395
|
+
await clickText(setup, "Second Thread");
|
|
396
|
+
|
|
397
|
+
// Assert - the leaving thread's working copy holds the edit rather than dropping it
|
|
398
|
+
await waitForState(setup, () =>
|
|
399
|
+
(server.core.sessionGet(session.id).workingCopy ?? "").includes(
|
|
400
|
+
"kept across a thread switch",
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe("the quick-action palette", () => {
|
|
407
|
+
test("/ lists the quick actions and a pick inserts the reference, not the body", async () => {
|
|
408
|
+
// Arrange
|
|
409
|
+
const setup = await renderApp();
|
|
410
|
+
|
|
411
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
412
|
+
|
|
413
|
+
// Act - a leading slash opens the palette; step to the second default and pick it
|
|
414
|
+
await type(setup, "/");
|
|
415
|
+
await waitForText(setup, "zoom-out-research-in-depth");
|
|
416
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
417
|
+
await press(setup, "enter");
|
|
418
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
419
|
+
|
|
420
|
+
// Assert - the comment holds the /name reference, not the expanded body
|
|
421
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
422
|
+
const stored = server.core.sessionGet(session.id);
|
|
423
|
+
const secondName = slashItemsFrom(DEFAULT_QUICK_ACTIONS)[1]!.name;
|
|
424
|
+
|
|
425
|
+
expect(stored.annotations[0]!.kind).toBe("comment");
|
|
426
|
+
expect(stored.annotations[0]!.body.trim()).toBe(`/${secondName}`);
|
|
258
427
|
});
|
|
259
428
|
});
|