@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -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 +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -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 +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -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 +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- 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
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/** The four-pane workbench in the virtual terminal, mirroring the browser prototype's interactions:
|
|
2
|
+
* a diff session lays out Threads | Thread | Changes editor | Project sidebar; the diff (±) and tree
|
|
3
|
+
* toggles are mutually exclusive and switching to tree keeps the Changes editor open; a project-tree
|
|
4
|
+
* file opens as a read-only contents tab; the split control offers four directions and Split Right makes
|
|
5
|
+
* two editor groups; dismissing the Changes tab collapses the Changes pane but keeps the Project sidebar;
|
|
6
|
+
* the right-sidebar toggle collapses the region to a thin rail and reopens it; zoom hides the Thread pane
|
|
7
|
+
* while the sidebars stay. */
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
15
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
16
|
+
import type { Thread } from "@cueloop/schema";
|
|
17
|
+
import { App } from "./App";
|
|
18
|
+
import {
|
|
19
|
+
dragText,
|
|
20
|
+
isolateUserConfig,
|
|
21
|
+
locateText,
|
|
22
|
+
pressKey,
|
|
23
|
+
typeText,
|
|
24
|
+
waitForState,
|
|
25
|
+
waitForText,
|
|
26
|
+
renderReadyApp,
|
|
27
|
+
} from "./test-support";
|
|
28
|
+
import { NERD } from "./components/primitives/icons";
|
|
29
|
+
|
|
30
|
+
const PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
31
|
+
index 111..222 100644
|
|
32
|
+
--- a/src/store.ts
|
|
33
|
+
+++ b/src/store.ts
|
|
34
|
+
@@ -1,3 +1,3 @@
|
|
35
|
+
export class Store {
|
|
36
|
+
- private items = [];
|
|
37
|
+
+ private items = new Map();
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
let home: string;
|
|
42
|
+
let repo: string;
|
|
43
|
+
let restoreUserConfig: () => void;
|
|
44
|
+
let server: DaemonServer;
|
|
45
|
+
let session: Thread;
|
|
46
|
+
|
|
47
|
+
/** A throwaway git repo so the Project tree (git ls-files) and file contents resolve without a real checkout. */
|
|
48
|
+
function makeRepo(): string {
|
|
49
|
+
const root = mkdtempSync(join(tmpdir(), "cueloop-workbench-repo-"));
|
|
50
|
+
|
|
51
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
52
|
+
writeFileSync(join(root, "README.md"), "# Workbench Fixture\n\nA tiny tracked repo.\n");
|
|
53
|
+
writeFileSync(join(root, "src", "store.ts"), "export class Store {}\n");
|
|
54
|
+
writeFileSync(join(root, "src", "util.ts"), "export const noop = () => {};\n");
|
|
55
|
+
const git = (args: string[]) => Bun.spawnSync(["git", "-C", root, ...args]);
|
|
56
|
+
|
|
57
|
+
git(["init", "-q"]);
|
|
58
|
+
git(["config", "user.email", "fixture@example.com"]);
|
|
59
|
+
git(["config", "user.name", "Fixture"]);
|
|
60
|
+
git(["add", "-A"]);
|
|
61
|
+
git(["commit", "-q", "-m", "seed"]);
|
|
62
|
+
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-workbench-"));
|
|
68
|
+
repo = makeRepo();
|
|
69
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
70
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
71
|
+
server.start();
|
|
72
|
+
session = server.core.sessionCreate({
|
|
73
|
+
workspace: { repoRoot: repo, branch: "main" },
|
|
74
|
+
artifact: { type: "diff", content: PATCH, meta: { title: "working tree" } },
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
restoreUserConfig();
|
|
79
|
+
server.stop();
|
|
80
|
+
rmSync(home, { recursive: true, force: true });
|
|
81
|
+
rmSync(repo, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
async function renderApp() {
|
|
85
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
86
|
+
width: 160,
|
|
87
|
+
height: 20,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await waitForText(setup, "store.ts");
|
|
91
|
+
|
|
92
|
+
return setup;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
96
|
+
|
|
97
|
+
/** The header controls sit on the top content row of the compact header. */
|
|
98
|
+
const HEADER_ROW = 0;
|
|
99
|
+
|
|
100
|
+
/** Column of a Project-header toggle, located by its own glyph so spacing changes never break the click. */
|
|
101
|
+
function toggleColumn(setup: Setup, glyph: string): number {
|
|
102
|
+
return setup.captureCharFrame().split("\n")[HEADER_ROW]!.lastIndexOf(glyph);
|
|
103
|
+
}
|
|
104
|
+
const diffToggleColumn = (setup: Setup): number => toggleColumn(setup, "changes");
|
|
105
|
+
const treeToggleColumn = (setup: Setup): number => toggleColumn(setup, "project");
|
|
106
|
+
const rightToggleColumn = (setup: Setup): number => toggleColumn(setup, NERD.sidebarRight);
|
|
107
|
+
// the collapsed rail shows the outline (off) variant, mirroring the left toggle's filled/outline states
|
|
108
|
+
const railToggleColumn = (setup: Setup): number => toggleColumn(setup, NERD.sidebarRightOff);
|
|
109
|
+
|
|
110
|
+
describe("the four-pane workbench", () => {
|
|
111
|
+
test("a diff session lays out the four panes with the diff toggle active", async () => {
|
|
112
|
+
const setup = await renderApp();
|
|
113
|
+
|
|
114
|
+
const frame = setup.captureCharFrame();
|
|
115
|
+
expect(frame).toContain("cueloop");
|
|
116
|
+
expect(frame).toContain("Changes");
|
|
117
|
+
expect(frame).toContain("store.ts");
|
|
118
|
+
// the diff (±) toggle exists in the Project header
|
|
119
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("switching to the project tree keeps the Changes editor open", async () => {
|
|
123
|
+
const setup = await renderApp();
|
|
124
|
+
|
|
125
|
+
// the diff and tree toggles are mutually exclusive; switch to the full project tree
|
|
126
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
127
|
+
await waitForText(setup, "README.md");
|
|
128
|
+
|
|
129
|
+
const frame = setup.captureCharFrame();
|
|
130
|
+
// the full tree is showing...
|
|
131
|
+
expect(frame).toContain("README.md");
|
|
132
|
+
expect(frame).toContain("src");
|
|
133
|
+
// ...and the Changes editor stayed open through the mode switch
|
|
134
|
+
expect(frame).toContain("Changes");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("opening a project-tree file adds a read-only contents tab", async () => {
|
|
138
|
+
const setup = await renderApp();
|
|
139
|
+
|
|
140
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
141
|
+
await waitForText(setup, "README.md");
|
|
142
|
+
|
|
143
|
+
const readme = locateText(setup, "README.md");
|
|
144
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
145
|
+
await waitForText(setup, "Workbench Fixture");
|
|
146
|
+
|
|
147
|
+
const frame = setup.captureCharFrame();
|
|
148
|
+
// the contents tab carries the file's text with no diff markers
|
|
149
|
+
expect(frame).toContain("A tiny tracked repo.");
|
|
150
|
+
expect(frame.split("\n")[HEADER_ROW]!).toContain("README.md");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("the split control offers four directions and Split Right makes two groups", async () => {
|
|
154
|
+
const setup = await renderApp();
|
|
155
|
+
|
|
156
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
157
|
+
await waitForText(setup, "README.md");
|
|
158
|
+
const readme = locateText(setup, "README.md");
|
|
159
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
160
|
+
await waitForText(setup, "Workbench Fixture");
|
|
161
|
+
|
|
162
|
+
// the split control appears only on a file tab
|
|
163
|
+
const split = locateText(setup, "split");
|
|
164
|
+
await setup.mockMouse.click(split.column, split.row);
|
|
165
|
+
await waitForText(setup, "Split Right");
|
|
166
|
+
|
|
167
|
+
const menu = setup.captureCharFrame();
|
|
168
|
+
expect(menu).toContain("Split Left");
|
|
169
|
+
expect(menu).toContain("Split Right");
|
|
170
|
+
expect(menu).toContain("Split Up");
|
|
171
|
+
expect(menu).toContain("Split Down");
|
|
172
|
+
|
|
173
|
+
const right = locateText(setup, "Split Right");
|
|
174
|
+
await setup.mockMouse.click(right.column, right.row);
|
|
175
|
+
// two editor groups now ride the header, each with its own split control
|
|
176
|
+
await waitForState(setup, () => setup.captureCharFrame().split("split").length - 1 >= 2);
|
|
177
|
+
expect((setup.captureCharFrame().match(/README\.md/g) ?? []).length).toBeGreaterThanOrEqual(2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("dismissing the Changes tab collapses the Changes pane but keeps the Project sidebar", async () => {
|
|
181
|
+
const setup = await renderApp();
|
|
182
|
+
|
|
183
|
+
const changes = locateText(setup, "Changes");
|
|
184
|
+
// the tab's close box sits just past the label; the ✕ is a hover affordance, the box always clicks
|
|
185
|
+
await setup.mockMouse.click(changes.column + 8, changes.row);
|
|
186
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("store.ts"));
|
|
187
|
+
|
|
188
|
+
const frame = setup.captureCharFrame();
|
|
189
|
+
// the Changes editor is gone...
|
|
190
|
+
expect(frame).not.toContain("store.ts");
|
|
191
|
+
// ...but the Project sidebar and its toggles remain
|
|
192
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("the right-sidebar toggle closes the region and its header toggle reopens it", async () => {
|
|
196
|
+
const setup = await renderApp();
|
|
197
|
+
|
|
198
|
+
await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
|
|
199
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("store.ts"));
|
|
200
|
+
|
|
201
|
+
// the right region is closed: no Changes editor, no Project tree toggles, no gutter
|
|
202
|
+
expect(setup.captureCharFrame()).not.toContain("store.ts");
|
|
203
|
+
expect(diffToggleColumn(setup)).toBe(-1);
|
|
204
|
+
|
|
205
|
+
// the reopen toggle (outline/off variant) rides the thread header; clicking it reopens the region
|
|
206
|
+
await setup.mockMouse.click(railToggleColumn(setup), HEADER_ROW);
|
|
207
|
+
await waitForText(setup, "store.ts");
|
|
208
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("the Threads toggle opens and closes the sidebar, relocating the brand", async () => {
|
|
212
|
+
const setup = await renderApp();
|
|
213
|
+
|
|
214
|
+
// deep-linked into a session, the Threads sidebar starts collapsed: the brand rides in the
|
|
215
|
+
// Thread header and the inbox groups are hidden
|
|
216
|
+
const collapsed = setup.captureCharFrame();
|
|
217
|
+
expect(collapsed).toContain("cueloop");
|
|
218
|
+
expect(collapsed).not.toContain("Threads");
|
|
219
|
+
|
|
220
|
+
// the sidebar toggle sits just left of the brand (one glyph plus its margin)
|
|
221
|
+
const brand = locateText(setup, "cueloop");
|
|
222
|
+
await setup.mockMouse.click(brand.column - 3, brand.row);
|
|
223
|
+
await waitForText(setup, "Threads");
|
|
224
|
+
|
|
225
|
+
// the Threads pane is open with its inbox groups, and the brand moved into its header
|
|
226
|
+
expect(setup.captureCharFrame()).toContain("Threads");
|
|
227
|
+
|
|
228
|
+
// toggling again collapses it back
|
|
229
|
+
const reopened = locateText(setup, "cueloop");
|
|
230
|
+
await setup.mockMouse.click(reopened.column - 3, reopened.row);
|
|
231
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Threads"));
|
|
232
|
+
expect(setup.captureCharFrame()).not.toContain("Threads");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("zoom hides the Thread pane while the sidebars stay", async () => {
|
|
236
|
+
const setup = await renderApp();
|
|
237
|
+
|
|
238
|
+
const zoom = locateText(setup, NERD.zoom);
|
|
239
|
+
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
240
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("review the changes"));
|
|
241
|
+
|
|
242
|
+
const frame = setup.captureCharFrame();
|
|
243
|
+
// the Thread pane's prompt is gone...
|
|
244
|
+
expect(frame).not.toContain("review the changes");
|
|
245
|
+
// ...while the Changes editor and Project sidebar stay
|
|
246
|
+
expect(frame).toContain("store.ts");
|
|
247
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("selecting a non-diff thread while zoomed exits zoom and restores the Thread pane", async () => {
|
|
251
|
+
// a second, non-diff thread to navigate to; its sync has no Changes editor to fill a zoom
|
|
252
|
+
server.core.sessionCreate({
|
|
253
|
+
workspace: { repoRoot: repo, branch: "main" },
|
|
254
|
+
artifact: {
|
|
255
|
+
type: "plan",
|
|
256
|
+
content: "# Zoomed Plan\n\nReview this plan.\n",
|
|
257
|
+
meta: { title: "Zoomed Plan" },
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const setup = await renderApp();
|
|
261
|
+
|
|
262
|
+
// open the collapsed Threads sidebar first (its toggle sits three cells left of the brand); the
|
|
263
|
+
// brand rides the Thread header, which zoom then hides, so it must be reached before zooming
|
|
264
|
+
const brand = locateText(setup, "cueloop");
|
|
265
|
+
await setup.mockMouse.click(brand.column - 3, brand.row);
|
|
266
|
+
await waitForText(setup, "Zoomed Plan");
|
|
267
|
+
|
|
268
|
+
// zoom the diff - the Thread pane hides and the footer rides the Changes pane
|
|
269
|
+
const zoom = locateText(setup, NERD.zoom);
|
|
270
|
+
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
271
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("review the changes"));
|
|
272
|
+
|
|
273
|
+
// switch to the plan thread: it has no Changes editor, so a stranded zoom would blank the middle
|
|
274
|
+
const planRow = locateText(setup, "Zoomed Plan");
|
|
275
|
+
await setup.mockMouse.click(planRow.column, planRow.row);
|
|
276
|
+
await waitForText(setup, "Review this plan.");
|
|
277
|
+
|
|
278
|
+
const frame = setup.captureCharFrame();
|
|
279
|
+
// the Thread pane is back with the plan, and the diff's Changes editor is gone - never a blank middle
|
|
280
|
+
expect(frame).toContain("Review this plan.");
|
|
281
|
+
expect(frame).not.toContain("store.ts");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("toggling the right sidebar while zoomed exits zoom and restores the Thread pane", async () => {
|
|
285
|
+
const setup = await renderApp();
|
|
286
|
+
|
|
287
|
+
// zoom the Changes editor - the Thread pane hides
|
|
288
|
+
const zoom = locateText(setup, NERD.zoom);
|
|
289
|
+
await setup.mockMouse.click(zoom.column, zoom.row);
|
|
290
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("review the changes"));
|
|
291
|
+
|
|
292
|
+
// toggling the right sidebar off must not strand a blank screen: zoom exits, the Thread pane returns
|
|
293
|
+
await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
|
|
294
|
+
await waitForText(setup, "review the changes");
|
|
295
|
+
const frame = setup.captureCharFrame();
|
|
296
|
+
expect(frame).toContain("review the changes");
|
|
297
|
+
// the right region is collapsed and the Changes editor is gone
|
|
298
|
+
expect(frame).not.toContain("store.ts");
|
|
299
|
+
expect(diffToggleColumn(setup)).toBe(-1);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("hovering the right-sidebar icon surfaces its tooltip label at the screen root", async () => {
|
|
303
|
+
const setup = await renderApp();
|
|
304
|
+
|
|
305
|
+
// the tip is not painted until the pointer is over the control
|
|
306
|
+
expect(setup.captureCharFrame()).not.toContain("Toggle Right Sidebar");
|
|
307
|
+
|
|
308
|
+
await setup.mockMouse.moveTo(rightToggleColumn(setup), HEADER_ROW);
|
|
309
|
+
// the label surfaces at the root, escaping the header cell that would otherwise clip it
|
|
310
|
+
await waitForText(setup, "Toggle Right Sidebar");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/** The bare-launch (no thread) shell is the same four panes as a thread: the Thread pane waits in its
|
|
315
|
+
* empty state and a disposable Welcome tab rides in the Changes editor. The right-region toggles behave
|
|
316
|
+
* like the thread shell, dismissing Welcome collapses the editor without stranding an empty screen, and
|
|
317
|
+
* a closed right region moves its reopen toggle into the header rather than leaving a gutter. */
|
|
318
|
+
describe("the bare-launch welcome shell", () => {
|
|
319
|
+
let welcomeHome: string;
|
|
320
|
+
let welcomeRepo: string;
|
|
321
|
+
let restoreWelcome: () => void;
|
|
322
|
+
let welcomeServer: DaemonServer;
|
|
323
|
+
|
|
324
|
+
beforeEach(() => {
|
|
325
|
+
welcomeHome = mkdtempSync(join(tmpdir(), "cueloop-welcome-"));
|
|
326
|
+
// a small, controlled launch repo so the welcome tree/changes resolve fast and deterministically
|
|
327
|
+
welcomeRepo = makeRepo();
|
|
328
|
+
writeFileSync(join(welcomeRepo, "README.md"), "# Fixture\n\nedited in the working tree\n");
|
|
329
|
+
restoreWelcome = isolateUserConfig(welcomeHome);
|
|
330
|
+
welcomeServer = new DaemonServer({ home: welcomeHome, idleExitMs: 0 });
|
|
331
|
+
welcomeServer.start();
|
|
332
|
+
});
|
|
333
|
+
afterEach(() => {
|
|
334
|
+
restoreWelcome();
|
|
335
|
+
welcomeServer.stop();
|
|
336
|
+
rmSync(welcomeHome, { recursive: true, force: true });
|
|
337
|
+
rmSync(welcomeRepo, { recursive: true, force: true });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
async function renderWelcome() {
|
|
341
|
+
const setup = await testRender(
|
|
342
|
+
<App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
|
|
343
|
+
{ width: 180, height: 14 },
|
|
344
|
+
);
|
|
345
|
+
await waitForText(setup, "cueloop");
|
|
346
|
+
|
|
347
|
+
return setup;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
test("the project tree shows the launch repo's files even with no thread open", async () => {
|
|
351
|
+
const setup = await renderWelcome();
|
|
352
|
+
|
|
353
|
+
// the tree toggle loads the git repo the client launched in
|
|
354
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
355
|
+
await waitForText(setup, "README.md");
|
|
356
|
+
expect(setup.captureCharFrame()).toContain("README.md");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("the first comment in the bare shell creates and persists a per-repo workbench thread", async () => {
|
|
360
|
+
const setup = await renderWelcome();
|
|
361
|
+
|
|
362
|
+
// open a project file in the bare shell (no thread yet - nothing on disk)
|
|
363
|
+
expect(welcomeServer.core.sessionList().length).toBe(0);
|
|
364
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
365
|
+
await waitForText(setup, "README.md");
|
|
366
|
+
const readme = locateText(setup, "README.md");
|
|
367
|
+
await setup.mockMouse.click(readme.column, readme.row);
|
|
368
|
+
await waitForText(setup, "edited in the working tree");
|
|
369
|
+
|
|
370
|
+
// comment on a line - the first note find-or-creates the workbench thread
|
|
371
|
+
const line = "edited in the working tree";
|
|
372
|
+
await dragText(setup, line, line, line.length);
|
|
373
|
+
await typeText(setup, "workbench note");
|
|
374
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
375
|
+
|
|
376
|
+
// the daemon persisted one per-repo workbench thread carrying the note
|
|
377
|
+
await waitForState(setup, () =>
|
|
378
|
+
welcomeServer.core.sessionList().some((thread) => thread.artifact.meta.workbench === true),
|
|
379
|
+
);
|
|
380
|
+
const workbench = welcomeServer.core
|
|
381
|
+
.sessionList()
|
|
382
|
+
.find((thread) => thread.artifact.meta.workbench === true)!;
|
|
383
|
+
expect(workbench.workspace.rootCommit).toBeTruthy();
|
|
384
|
+
await waitForState(setup, () =>
|
|
385
|
+
welcomeServer.core
|
|
386
|
+
.sessionGet(workbench.id)
|
|
387
|
+
.annotations.some((note) => note.body.includes("workbench note")),
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("the changes tree lists the launch repo's working-tree changes", async () => {
|
|
392
|
+
const setup = await renderWelcome();
|
|
393
|
+
|
|
394
|
+
// changes mode is the default; the edited README shows with its status
|
|
395
|
+
await waitForText(setup, "README.md");
|
|
396
|
+
expect(setup.captureCharFrame()).toContain("README.md");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("a bare launch rides a Welcome tab in the editor while the Thread pane waits empty", async () => {
|
|
400
|
+
const setup = await renderWelcome();
|
|
401
|
+
|
|
402
|
+
// the Welcome playground measures its width before it paints, so wait for its copy
|
|
403
|
+
await waitForText(setup, "Getting started");
|
|
404
|
+
const frame = setup.captureCharFrame();
|
|
405
|
+
// the getting-started surface is an editor tab, not the thread pane
|
|
406
|
+
expect(frame.split("\n")[HEADER_ROW]!).toContain("Welcome");
|
|
407
|
+
expect(frame).toContain("Getting started");
|
|
408
|
+
// the Thread pane shows its empty state until a thread is opened
|
|
409
|
+
expect(frame).toContain("no threads");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("typing in the welcome composer never reaches the inbox keys", async () => {
|
|
413
|
+
// a pending thread in the sidebar so the inbox keys (delete, open) have a target to act on
|
|
414
|
+
welcomeServer.core.sessionCreate({
|
|
415
|
+
workspace: { repoRoot: welcomeRepo, branch: "main" },
|
|
416
|
+
artifact: { type: "plan", content: "# Seeded\n", meta: { title: "Seeded Plan" } },
|
|
417
|
+
});
|
|
418
|
+
const setup = await testRender(
|
|
419
|
+
<App home={welcomeHome} sessionId={undefined} cwd={welcomeRepo} />,
|
|
420
|
+
{ width: 160, height: 28 },
|
|
421
|
+
);
|
|
422
|
+
await waitForText(setup, "quick brown fox");
|
|
423
|
+
|
|
424
|
+
// open the playground composer, then type letters that are also inbox keys (j, k, d), one at a
|
|
425
|
+
// time as a person would - each keystroke settles a frame, the way real input arrives
|
|
426
|
+
await dragText(setup, "quick brown fox", "quick brown fox", "quick brown fox".length);
|
|
427
|
+
for (const character of "note: jkd") await typeText(setup, character);
|
|
428
|
+
await waitForText(setup, "note: jkd");
|
|
429
|
+
|
|
430
|
+
// the letters landed in the note, not the inbox: no delete confirmation opened
|
|
431
|
+
expect(setup.captureCharFrame()).not.toContain("This removes the plan");
|
|
432
|
+
|
|
433
|
+
// Cmd+Enter saves the note; it must not dispatch openSession and replace the playground
|
|
434
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
435
|
+
expect(setup.captureCharFrame()).toContain("Getting started");
|
|
436
|
+
|
|
437
|
+
setup.renderer.destroy();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("dismissing the Welcome tab collapses the editor without stranding an empty screen", async () => {
|
|
441
|
+
const setup = await renderWelcome();
|
|
442
|
+
|
|
443
|
+
const welcome = locateText(setup, "Welcome");
|
|
444
|
+
// the close box sits just past the label
|
|
445
|
+
await setup.mockMouse.click(welcome.column + 8, welcome.row);
|
|
446
|
+
await waitForState(
|
|
447
|
+
setup,
|
|
448
|
+
() => !setup.captureCharFrame().includes("comment on agent-authored work"),
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// the Threads sidebar and the Thread empty state remain - never a blank shell
|
|
452
|
+
const frame = setup.captureCharFrame();
|
|
453
|
+
expect(frame).toContain("cueloop");
|
|
454
|
+
expect(frame).toContain("no threads");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("the changed-files and tree toggles switch navigator mode without collapsing", async () => {
|
|
458
|
+
const setup = await renderWelcome();
|
|
459
|
+
|
|
460
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
461
|
+
|
|
462
|
+
// the tree toggle switches to the project view (the launch repo's tree) without collapsing
|
|
463
|
+
await setup.mockMouse.click(treeToggleColumn(setup), HEADER_ROW);
|
|
464
|
+
await waitForText(setup, "src");
|
|
465
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
466
|
+
|
|
467
|
+
// the changed-files toggle switches back, still open
|
|
468
|
+
await setup.mockMouse.click(diffToggleColumn(setup), HEADER_ROW);
|
|
469
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("src"));
|
|
470
|
+
expect(diffToggleColumn(setup)).toBeGreaterThan(0);
|
|
471
|
+
|
|
472
|
+
// the sidebar toggle closes the region
|
|
473
|
+
await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
|
|
474
|
+
await waitForState(setup, () => diffToggleColumn(setup) === -1);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("a closed region draws its reopen divider in the header only, not full-height", async () => {
|
|
478
|
+
const setup = await renderWelcome();
|
|
479
|
+
|
|
480
|
+
// close the right region so only the header toggle remains
|
|
481
|
+
await setup.mockMouse.click(rightToggleColumn(setup), HEADER_ROW);
|
|
482
|
+
await waitForState(setup, () => diffToggleColumn(setup) === -1);
|
|
483
|
+
|
|
484
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
485
|
+
// the reopen toggle's divider sits at the far right; find its column on the header row
|
|
486
|
+
const headerDivider = lines[HEADER_ROW]!.lastIndexOf("│");
|
|
487
|
+
expect(headerDivider).toBeGreaterThan(0);
|
|
488
|
+
// that column stays clear below the two-row header - no full-height column rule
|
|
489
|
+
const bodyRows = lines.slice(3).filter((line) => line.length > headerDivider);
|
|
490
|
+
expect(bodyRows.length).toBeGreaterThan(0);
|
|
491
|
+
for (const line of bodyRows) {
|
|
492
|
+
expect(line[headerDivider]).not.toBe("│");
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`view-plan regression harness (M0) blockRuns output is stable across the representative blocks 1`] = `
|
|
4
|
+
[
|
|
5
|
+
{
|
|
6
|
+
"runs": [
|
|
7
|
+
{
|
|
8
|
+
"role": "plain",
|
|
9
|
+
"start": 0,
|
|
10
|
+
"text": "Heading with ",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"role": "strong",
|
|
14
|
+
"start": 15,
|
|
15
|
+
"text": "bold",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
"text": "Heading with **bold**",
|
|
19
|
+
"type": "same",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"runs": [
|
|
23
|
+
{
|
|
24
|
+
"role": "plain",
|
|
25
|
+
"start": 0,
|
|
26
|
+
"text": "A paragraph with ",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"role": "strong",
|
|
30
|
+
"start": 19,
|
|
31
|
+
"text": "strong",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"role": "plain",
|
|
35
|
+
"start": 27,
|
|
36
|
+
"text": ", ",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"role": "em",
|
|
40
|
+
"start": 30,
|
|
41
|
+
"text": "em",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"role": "plain",
|
|
45
|
+
"start": 33,
|
|
46
|
+
"text": ", ",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"role": "code",
|
|
50
|
+
"start": 36,
|
|
51
|
+
"text": "code",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"role": "plain",
|
|
55
|
+
"start": 41,
|
|
56
|
+
"text": ", ",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"role": "strike",
|
|
60
|
+
"start": 45,
|
|
61
|
+
"text": "strike",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"role": "plain",
|
|
65
|
+
"start": 53,
|
|
66
|
+
"text": " and a ",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"href": "https://example.com",
|
|
70
|
+
"role": "link",
|
|
71
|
+
"start": 61,
|
|
72
|
+
"text": "label",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"role": "plain",
|
|
76
|
+
"start": 88,
|
|
77
|
+
"text": ".",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
"text": "A paragraph with **strong**, *em*, \`code\`, ~~strike~~ and a [label](https://example.com).",
|
|
81
|
+
"type": "same",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"runs": [
|
|
85
|
+
{
|
|
86
|
+
"role": "plain",
|
|
87
|
+
"start": 0,
|
|
88
|
+
"text": "a list item with ",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"role": "em",
|
|
92
|
+
"start": 18,
|
|
93
|
+
"text": "emphasis",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
"text": "a list item with *emphasis*",
|
|
97
|
+
"type": "same",
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared annotation palette: how a marked stretch, the caret cell, and a
|
|
3
|
+
* discussion card's accent edge paint, in every surface that hosts inline
|
|
4
|
+
* comments (the plan thread view and the diff sheet). One source, so a mark
|
|
5
|
+
* looks the same wherever it is drawn.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createTextAttributes } from "@opentui/core";
|
|
9
|
+
import type { Theme } from "./theme";
|
|
10
|
+
|
|
11
|
+
export const UNDERLINE = createTextAttributes({ underline: true });
|
|
12
|
+
export const CUT = createTextAttributes({ strikethrough: true, dim: true });
|
|
13
|
+
export const BOLD = createTextAttributes({ bold: true });
|
|
14
|
+
|
|
15
|
+
/** The marked-words treatment: a violet backdrop under an underline. */
|
|
16
|
+
const MARK_BACKDROP_DARK = "#463852";
|
|
17
|
+
const MARK_BACKDROP_LIGHT = "#e6ddf5";
|
|
18
|
+
/** The discussion card's accent left edge. */
|
|
19
|
+
const CARD_EDGE_DARK = "#ab7aca";
|
|
20
|
+
const CARD_EDGE_LIGHT = "#8b5fd6";
|
|
21
|
+
/** The caret cell: a visible cursor painted on the character under the caret. */
|
|
22
|
+
const CARET_CELL_DARK = "#565b68";
|
|
23
|
+
const CARET_CELL_LIGHT = "#b8bcc8";
|
|
24
|
+
|
|
25
|
+
export interface AnnotationPalette {
|
|
26
|
+
markBackdrop: string;
|
|
27
|
+
cardEdge: string;
|
|
28
|
+
caretCell: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Dark tokens carry light text; use that to pick the palette variant. */
|
|
32
|
+
export function annotationPaletteFor(tokens: Theme): AnnotationPalette {
|
|
33
|
+
const dark = Number.parseInt(tokens.text.slice(1, 3) || "e4", 16) > 128;
|
|
34
|
+
|
|
35
|
+
return dark
|
|
36
|
+
? { markBackdrop: MARK_BACKDROP_DARK, cardEdge: CARD_EDGE_DARK, caretCell: CARET_CELL_DARK }
|
|
37
|
+
: {
|
|
38
|
+
markBackdrop: MARK_BACKDROP_LIGHT,
|
|
39
|
+
cardEdge: CARD_EDGE_LIGHT,
|
|
40
|
+
caretCell: CARET_CELL_LIGHT,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Focus cue: mix a hex color toward white, keeping its hue readable. */
|
|
45
|
+
export function lighten(hex: string, amount = 0.25): string {
|
|
46
|
+
const channels = hex.match(/^#(..)(..)(..)$/);
|
|
47
|
+
|
|
48
|
+
if (!channels) return hex;
|
|
49
|
+
const lifted = channels
|
|
50
|
+
.slice(1)
|
|
51
|
+
.map((channel) => {
|
|
52
|
+
const value = Number.parseInt(channel, 16);
|
|
53
|
+
|
|
54
|
+
return Math.round(value + (255 - value) * amount)
|
|
55
|
+
.toString(16)
|
|
56
|
+
.padStart(2, "0");
|
|
57
|
+
})
|
|
58
|
+
.join("");
|
|
59
|
+
|
|
60
|
+
return `#${lifted}`;
|
|
61
|
+
}
|