@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
package/src/App.export.test.tsx
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier-2 wiring test for the notes-vault export: with [integrations.obsidian]
|
|
3
|
-
* configured to export on resolve, submitting a review writes the note into
|
|
4
|
-
* the vault and surfaces the path in the status line.
|
|
5
|
-
*/
|
|
1
|
+
/** Tier-2 wiring for the notes-vault export: with [integrations.obsidian] set to export on resolve, submitting a review writes the note into the vault and surfaces the path in the status line. */
|
|
6
2
|
|
|
7
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
9
13
|
import { tmpdir } from "node:os";
|
|
10
14
|
import { join } from "node:path";
|
|
11
15
|
import React from "react";
|
|
12
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
13
16
|
import { DaemonServer } from "@cueloop/daemon";
|
|
14
|
-
import type {
|
|
17
|
+
import type { Thread } from "@cueloop/schema";
|
|
15
18
|
import { App } from "./App";
|
|
19
|
+
import {
|
|
20
|
+
isolateUserConfig,
|
|
21
|
+
waitForState,
|
|
22
|
+
waitForText,
|
|
23
|
+
pressKey,
|
|
24
|
+
renderReadyApp,
|
|
25
|
+
} from "./test-support";
|
|
16
26
|
|
|
17
27
|
const PLAN = `# Migration Plan
|
|
18
28
|
|
|
@@ -22,66 +32,73 @@ Move the store atomically.
|
|
|
22
32
|
let home: string;
|
|
23
33
|
let vault: string;
|
|
24
34
|
let server: DaemonServer;
|
|
25
|
-
let session:
|
|
26
|
-
let
|
|
35
|
+
let session: Thread;
|
|
36
|
+
let restoreUserConfig: () => void;
|
|
27
37
|
|
|
28
38
|
beforeEach(() => {
|
|
29
39
|
home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
|
|
30
40
|
vault = join(home, "vault");
|
|
31
41
|
mkdirSync(vault);
|
|
32
42
|
const configPath = join(home, "config.toml");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
|
|
44
|
+
writeFileSync(
|
|
45
|
+
configPath,
|
|
46
|
+
`[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "message"\n`,
|
|
47
|
+
);
|
|
48
|
+
restoreUserConfig = isolateUserConfig(home, "config.toml");
|
|
36
49
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
37
50
|
server.start();
|
|
38
51
|
session = server.core.sessionCreate({
|
|
39
52
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
40
|
-
artifact: {
|
|
53
|
+
artifact: {
|
|
54
|
+
type: "plan",
|
|
55
|
+
content: PLAN,
|
|
56
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
57
|
+
},
|
|
41
58
|
});
|
|
42
59
|
});
|
|
43
60
|
afterEach(() => {
|
|
44
|
-
|
|
45
|
-
else process.env.CUELOOP_CONFIG = savedConfigEnv;
|
|
61
|
+
restoreUserConfig();
|
|
46
62
|
server.stop();
|
|
47
63
|
rmSync(home, { recursive: true, force: true });
|
|
48
64
|
});
|
|
49
65
|
|
|
50
66
|
describe("obsidian export on resolve", () => {
|
|
51
67
|
test("submitting a review writes the plan into the vault and shows the path", async () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setup
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
68
|
+
// Arrange
|
|
69
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
70
|
+
width: 120,
|
|
71
|
+
height: 32,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await waitForText(setup, "cueloop");
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
await pressKey(setup, "RETURN", { meta: true }); // open the submit overlay, keep the default message (approve)
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
await waitForText(setup, "[approve]");
|
|
81
|
+
|
|
82
|
+
// Act
|
|
83
|
+
await pressKey(setup, "RETURN", { meta: true }); // submit
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
// the export is an async round-trip after resolve; the status line lands
|
|
87
|
+
// on a later render than the file write
|
|
65
88
|
const dir = join(vault, "cueloop");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await setup.renderOnce();
|
|
70
|
-
}
|
|
71
|
-
// the status line lands on a later render than the file write
|
|
72
|
-
const statusDeadline = Date.now() + 30_000;
|
|
73
|
-
while (Date.now() < statusDeadline && !setup.captureCharFrame().includes("exported to")) {
|
|
74
|
-
await Bun.sleep(25);
|
|
75
|
-
await setup.renderOnce();
|
|
76
|
-
}
|
|
89
|
+
|
|
90
|
+
await waitForState(setup, () => existsSync(dir));
|
|
91
|
+
await waitForText(setup, "exported to");
|
|
77
92
|
|
|
78
93
|
const notes = readdirSync(dir);
|
|
94
|
+
|
|
79
95
|
expect(notes.length).toBe(1);
|
|
80
96
|
expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
|
|
81
97
|
const written = readFileSync(join(dir, notes[0]!), "utf8");
|
|
98
|
+
|
|
82
99
|
expect(written).toContain("source: cueloop");
|
|
83
100
|
expect(written).toContain(`session: ${session.id}`);
|
|
84
|
-
expect(written).toContain("
|
|
101
|
+
expect(written).toContain("message: approved");
|
|
85
102
|
expect(written).toContain("Move the store atomically.");
|
|
86
103
|
expect(setup.captureCharFrame()).toContain("exported to");
|
|
87
104
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { nextFocusPane, reconciledFocus, threadsNavHandled, visiblePanes } from "./App";
|
|
3
|
+
|
|
4
|
+
describe("threads keyboard nav", () => {
|
|
5
|
+
const base = () => {
|
|
6
|
+
let cursor = 1;
|
|
7
|
+
let opened = 0;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
setInboxCursor: (update: (cursor: number) => number) => (cursor = update(cursor)),
|
|
11
|
+
openSession: () => (opened += 1),
|
|
12
|
+
read: () => ({
|
|
13
|
+
get cursor() {
|
|
14
|
+
return cursor;
|
|
15
|
+
},
|
|
16
|
+
get opened() {
|
|
17
|
+
return opened;
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
test("moves the cursor and opens only when the Threads pane is quiet and focused", () => {
|
|
24
|
+
const spy = base();
|
|
25
|
+
const call = (name: string) =>
|
|
26
|
+
threadsNavHandled({
|
|
27
|
+
focusedPane: "threads",
|
|
28
|
+
quiet: true,
|
|
29
|
+
key: { name },
|
|
30
|
+
count: 3,
|
|
31
|
+
setInboxCursor: spy.setInboxCursor,
|
|
32
|
+
openSession: spy.openSession,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(call("down")).toBe(true);
|
|
36
|
+
expect(spy.read().cursor).toBe(2);
|
|
37
|
+
expect(call("down")).toBe(true); // clamps at the last row
|
|
38
|
+
expect(spy.read().cursor).toBe(2);
|
|
39
|
+
expect(call("k")).toBe(true);
|
|
40
|
+
expect(spy.read().cursor).toBe(1);
|
|
41
|
+
expect(call("return")).toBe(true);
|
|
42
|
+
expect(spy.read().opened).toBe(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("defers when another pane is focused, keys are owned elsewhere, or the inbox is empty", () => {
|
|
46
|
+
const spy = base();
|
|
47
|
+
const params = {
|
|
48
|
+
quiet: true,
|
|
49
|
+
key: { name: "down" },
|
|
50
|
+
count: 3,
|
|
51
|
+
setInboxCursor: spy.setInboxCursor,
|
|
52
|
+
openSession: spy.openSession,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(threadsNavHandled({ ...params, focusedPane: "changes" })).toBe(false);
|
|
56
|
+
expect(threadsNavHandled({ ...params, focusedPane: "threads", quiet: false })).toBe(false);
|
|
57
|
+
expect(threadsNavHandled({ ...params, focusedPane: "threads", count: 0 })).toBe(false);
|
|
58
|
+
expect(spy.read().cursor).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("pane focus cycle", () => {
|
|
63
|
+
test("visiblePanes lists only the open panes, in reading order", () => {
|
|
64
|
+
expect(visiblePanes(true, true, true, true)).toEqual([
|
|
65
|
+
"threads",
|
|
66
|
+
"thread",
|
|
67
|
+
"changes",
|
|
68
|
+
"project",
|
|
69
|
+
]);
|
|
70
|
+
expect(visiblePanes(true, false, true, true)).toEqual(["threads", "changes", "project"]);
|
|
71
|
+
expect(visiblePanes(false, false, true, false)).toEqual(["changes"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("nextFocusPane wraps forward and back over the visible panes", () => {
|
|
75
|
+
const panes = visiblePanes(true, false, true, true);
|
|
76
|
+
|
|
77
|
+
expect(nextFocusPane("threads", panes, false)).toBe("changes");
|
|
78
|
+
expect(nextFocusPane("project", panes, false)).toBe("threads");
|
|
79
|
+
expect(nextFocusPane("threads", panes, true)).toBe("project");
|
|
80
|
+
expect(nextFocusPane("changes", [], false)).toBe("changes");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("reconciledFocus falls back only when the focused pane is not navigable", () => {
|
|
84
|
+
expect(reconciledFocus(["threads", "changes"], "threads")).toBeNull();
|
|
85
|
+
expect(reconciledFocus(["threads", "changes"], "thread")).toBe("threads");
|
|
86
|
+
expect(reconciledFocus(["changes", "project"], "thread")).toBe("changes");
|
|
87
|
+
expect(reconciledFocus([], "thread")).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Reviewer identity surfaces: the Account settings tab and the author-name tooltip on a comment dot. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import { App } from "./App";
|
|
10
|
+
import { isolateUserConfig, locateText, renderReadyApp, waitForText } from "./test-support";
|
|
11
|
+
|
|
12
|
+
const PLAN = "# Migration Plan\n\nThe daemon persists sessions to disk atomically.\n";
|
|
13
|
+
|
|
14
|
+
let home: string;
|
|
15
|
+
let restoreUserConfig: () => void;
|
|
16
|
+
let server: DaemonServer;
|
|
17
|
+
let sessionId: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-identity-"));
|
|
21
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
22
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
23
|
+
server.start();
|
|
24
|
+
const session = server.core.sessionCreate({
|
|
25
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
26
|
+
artifact: {
|
|
27
|
+
type: "plan",
|
|
28
|
+
content: PLAN,
|
|
29
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
sessionId = session.id;
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
restoreUserConfig();
|
|
36
|
+
server.stop();
|
|
37
|
+
rmSync(home, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function renderApp() {
|
|
41
|
+
return renderReadyApp(<App home={home} sessionId={sessionId} />, { width: 120, height: 32 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("the Account settings tab shows the display name row and the GitHub sync action", async () => {
|
|
45
|
+
const setup = await renderApp();
|
|
46
|
+
|
|
47
|
+
await setup.mockMouse.click(1, 0);
|
|
48
|
+
await waitForText(setup, "settings");
|
|
49
|
+
const menu = setup.captureCharFrame().split("\n");
|
|
50
|
+
const settingsRow = menu.findIndex((line) => line.includes("settings"));
|
|
51
|
+
|
|
52
|
+
await setup.mockMouse.click(menu[settingsRow]!.indexOf("settings") + 1, settingsRow);
|
|
53
|
+
await waitForText(setup, "Account");
|
|
54
|
+
const dialog = setup.captureCharFrame().split("\n");
|
|
55
|
+
const accountRow = dialog.findIndex((line) => line.includes("Account"));
|
|
56
|
+
|
|
57
|
+
await setup.mockMouse.click(dialog[accountRow]!.indexOf("Account") + 1, accountRow);
|
|
58
|
+
await waitForText(setup, "Display name");
|
|
59
|
+
expect(setup.captureCharFrame()).toContain("Sync from GitHub");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("hovering an own comment's dot surfaces the author tooltip", async () => {
|
|
63
|
+
server.core.sessionAnnotate(sessionId, {
|
|
64
|
+
id: "a_own",
|
|
65
|
+
kind: "comment",
|
|
66
|
+
anchor: { quote: "atomically", prefix: "sessions to disk ", suffix: "." },
|
|
67
|
+
body: "My note.",
|
|
68
|
+
});
|
|
69
|
+
const setup = await renderApp();
|
|
70
|
+
|
|
71
|
+
await waitForText(setup, "● My note.");
|
|
72
|
+
// the tooltip is not painted until the pointer is over the dot
|
|
73
|
+
expect(setup.captureCharFrame()).not.toContain("you");
|
|
74
|
+
const dot = locateText(setup, "● My note.");
|
|
75
|
+
|
|
76
|
+
await setup.mockMouse.moveTo(dot.column, dot.row);
|
|
77
|
+
await waitForText(setup, "you");
|
|
78
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-only plan render: h1/h2 sit over
|
|
3
|
+
* a rule, links show their label in the link color with the URL concealed,
|
|
4
|
+
* inline code grays with its backticks gone, GFM tables render as an aligned grid
|
|
5
|
+
* and leading YAML frontmatter as a bordered key/value table. Because rendered
|
|
6
|
+
* text now differs from the source, a comment dragged over a concealed-markup
|
|
7
|
+
* passage must still anchor to the source and repaint on the visible label.
|
|
8
|
+
* Char-frame + styled-span assertions over the real App and an in-process daemon.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
17
|
+
import type { Thread } from "@cueloop/schema";
|
|
18
|
+
import { App } from "./App";
|
|
19
|
+
import { DARK } from "./theme";
|
|
20
|
+
import {
|
|
21
|
+
dragText,
|
|
22
|
+
isolateUserConfig,
|
|
23
|
+
pressKey,
|
|
24
|
+
renderReadyApp,
|
|
25
|
+
settle,
|
|
26
|
+
typeText as type,
|
|
27
|
+
waitForState,
|
|
28
|
+
waitForText,
|
|
29
|
+
} from "./test-support";
|
|
30
|
+
|
|
31
|
+
const PLAN = `---
|
|
32
|
+
title: Migration Plan
|
|
33
|
+
owner: platform team
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Migration Plan
|
|
37
|
+
|
|
38
|
+
## Rollout
|
|
39
|
+
|
|
40
|
+
Ship it behind a \`flag\` and read [OpenTUI](https://opentui) first.
|
|
41
|
+
|
|
42
|
+
| Name | Size |
|
|
43
|
+
| :--- | ---: |
|
|
44
|
+
| daemon | 12 |
|
|
45
|
+
| client | 7 |
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
let home: string;
|
|
49
|
+
let server: DaemonServer;
|
|
50
|
+
let session: Thread;
|
|
51
|
+
let restoreUserConfig: () => void;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-plan-render-"));
|
|
55
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
56
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
57
|
+
server.start();
|
|
58
|
+
session = server.core.sessionCreate({
|
|
59
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
60
|
+
artifact: {
|
|
61
|
+
type: "plan",
|
|
62
|
+
content: PLAN,
|
|
63
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
restoreUserConfig();
|
|
69
|
+
server.stop();
|
|
70
|
+
rmSync(home, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
type Setup = Awaited<ReturnType<typeof renderPlan>>;
|
|
74
|
+
|
|
75
|
+
async function renderPlan() {
|
|
76
|
+
const setup = await renderReadyApp(<App home={home} sessionId={session.id} />, {
|
|
77
|
+
width: 90,
|
|
78
|
+
height: 40,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await waitForText(setup, "cueloop");
|
|
82
|
+
await settle(setup);
|
|
83
|
+
|
|
84
|
+
return setup;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Hex foregrounds of every styled span containing the needle. */
|
|
88
|
+
function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
89
|
+
const foregrounds: string[] = [];
|
|
90
|
+
|
|
91
|
+
for (const line of setup.captureSpans().lines) {
|
|
92
|
+
for (const span of line.spans) {
|
|
93
|
+
if (span.fg === undefined || !span.text.includes(needle)) continue;
|
|
94
|
+
const [red, green, blue] = span.fg.toInts();
|
|
95
|
+
|
|
96
|
+
foregrounds.push(
|
|
97
|
+
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return foregrounds;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Hex backgrounds of every styled span containing the needle. */
|
|
106
|
+
function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
107
|
+
const backgrounds: string[] = [];
|
|
108
|
+
|
|
109
|
+
for (const line of setup.captureSpans().lines) {
|
|
110
|
+
for (const span of line.spans) {
|
|
111
|
+
if (!span.text.includes(needle)) continue;
|
|
112
|
+
const [red, green, blue] = span.bg.toInts();
|
|
113
|
+
|
|
114
|
+
backgrounds.push(
|
|
115
|
+
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return backgrounds;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const THREAD_MARK = "#463852";
|
|
124
|
+
|
|
125
|
+
describe("read-only markdown render", () => {
|
|
126
|
+
test("h1 and h2 sit over a rule", async () => {
|
|
127
|
+
const setup = await renderPlan();
|
|
128
|
+
const rows = setup.captureCharFrame().split("\n");
|
|
129
|
+
// "Migration Plan" also rides the header bar; a heading is any body row directly over a rule
|
|
130
|
+
const ruleBelow = (heading: string): boolean =>
|
|
131
|
+
rows.some((row, index) => row.includes(heading) && (rows[index + 1] ?? "").includes("──"));
|
|
132
|
+
|
|
133
|
+
expect(ruleBelow("Migration Plan")).toBe(true);
|
|
134
|
+
expect(ruleBelow("Rollout")).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a link shows its label in the link color with the URL concealed", async () => {
|
|
138
|
+
const setup = await renderPlan();
|
|
139
|
+
const frame = setup.captureCharFrame();
|
|
140
|
+
|
|
141
|
+
expect(frame).toContain("OpenTUI");
|
|
142
|
+
expect(frame).not.toContain("https://opentui");
|
|
143
|
+
expect(frame).not.toContain("[OpenTUI]");
|
|
144
|
+
expect(foregroundsOf(setup, "OpenTUI")).toContain(DARK.blue);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("inline code grays with its backticks concealed", async () => {
|
|
148
|
+
const setup = await renderPlan();
|
|
149
|
+
const frame = setup.captureCharFrame();
|
|
150
|
+
|
|
151
|
+
expect(frame).toContain("flag");
|
|
152
|
+
expect(frame).not.toContain("`flag`");
|
|
153
|
+
expect(foregroundsOf(setup, "flag")).toContain(DARK.textDim);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("a table renders as an aligned grid without pipes", async () => {
|
|
157
|
+
const setup = await renderPlan();
|
|
158
|
+
const rows = setup.captureCharFrame().split("\n");
|
|
159
|
+
const header = rows.find((row) => row.includes("Name") && row.includes("Size"))!;
|
|
160
|
+
|
|
161
|
+
expect(header).not.toContain("|");
|
|
162
|
+
// the numeric column is right-aligned, so 12 and 7 line up on their last digit
|
|
163
|
+
const daemon = rows.find((row) => row.includes("daemon"))!;
|
|
164
|
+
const client = rows.find((row) => row.includes("client"))!;
|
|
165
|
+
|
|
166
|
+
expect(daemon.indexOf("12") + 1).toBe(client.indexOf("7"));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("frontmatter renders as a bordered key/value grid", async () => {
|
|
170
|
+
const setup = await renderPlan();
|
|
171
|
+
const frame = setup.captureCharFrame();
|
|
172
|
+
|
|
173
|
+
expect(frame).toContain("┌");
|
|
174
|
+
expect(frame).toContain("└");
|
|
175
|
+
expect(frame).toContain("title");
|
|
176
|
+
expect(frame).toContain("platform team");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("annotating a grid block", () => {
|
|
181
|
+
test("a comment dragged over a table cell anchors on the grid and marks it", async () => {
|
|
182
|
+
const setup = await renderPlan();
|
|
183
|
+
|
|
184
|
+
// the table renders as a grid, yet it still takes a comment like any other block
|
|
185
|
+
await dragText(setup, "daemon", "daemon", "daemon".length);
|
|
186
|
+
await type(setup, "which daemon?");
|
|
187
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
188
|
+
|
|
189
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
190
|
+
// the grid carries the mark backdrop, so an annotation resolving into it stays visible
|
|
191
|
+
await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
|
|
192
|
+
}, 60_000);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("anchoring over concealed markup", () => {
|
|
196
|
+
test("a comment dragged over a rendered link label anchors to the source and repaints", async () => {
|
|
197
|
+
const setup = await renderPlan();
|
|
198
|
+
|
|
199
|
+
// drag the visible label "OpenTUI" (its brackets and URL are concealed)
|
|
200
|
+
await dragText(setup, "OpenTUI", "OpenTUI", "OpenTUI".length);
|
|
201
|
+
await type(setup, "which build?");
|
|
202
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
203
|
+
|
|
204
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
205
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("OpenTUI");
|
|
206
|
+
// the saved comment keeps the visible label marked
|
|
207
|
+
await waitForState(setup, () => backgroundsOf(setup, "OpenTUI").includes(THREAD_MARK));
|
|
208
|
+
}, 60_000);
|
|
209
|
+
});
|