@cueloop/client 0.1.0-alpha.10 → 0.1.0-alpha.101
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/annotations/annotation-palette.ts +62 -0
- package/src/annotations/components/AnnotationCards.stories.tsx +86 -0
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +369 -0
- package/src/annotations/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +186 -0
- package/src/annotations/discussions.ts +77 -0
- package/src/annotations/thread-selection.test.ts +210 -0
- package/src/annotations/thread-selection.ts +242 -0
- package/src/annotations/use-annotation-surface.tsx +1243 -0
- package/src/app/App.appearance.test.tsx +53 -0
- package/src/{App.autoclose.test.tsx → app/App.autoclose.test.tsx} +77 -51
- package/src/app/App.deleted-scroll.test.tsx +187 -0
- package/src/app/App.diff.test.tsx +313 -0
- package/src/app/App.export.test.tsx +105 -0
- package/src/app/App.focus.test.ts +89 -0
- package/src/app/App.identity.test.tsx +84 -0
- package/src/app/App.plan-render.test.tsx +209 -0
- package/src/app/App.plan-review.test.tsx +428 -0
- package/src/app/App.prototype-markdown.test.tsx +128 -0
- package/src/app/App.prototype.test.tsx +176 -0
- package/src/app/App.readonly.test.tsx +210 -0
- package/src/app/App.regression.test.tsx +99 -0
- package/src/app/App.rendering.test.tsx +134 -0
- package/src/app/App.share-dialog.test.tsx +177 -0
- package/src/app/App.submit-confirm.test.tsx +267 -0
- package/src/app/App.test.tsx +496 -0
- package/src/app/App.thread-agent.test.ts +20 -0
- package/src/app/App.toast.test.tsx +124 -0
- package/src/app/App.tsx +1667 -0
- package/src/app/App.visual.test.tsx +59 -0
- package/src/app/App.walk.test.tsx +299 -0
- package/src/app/App.workbench.test.tsx +1149 -0
- package/src/app/app-key-state.ts +61 -0
- package/src/app/app-screens.tsx +606 -0
- package/src/app/app-view-model.ts +168 -0
- package/src/app/components/AppHeader.stories.tsx +50 -0
- package/src/app/components/AppHeader.tsx +81 -0
- package/src/app/components/AppShell.brand.test.tsx +58 -0
- package/src/app/components/AppShell.extensions.test.tsx +191 -0
- package/src/app/components/AppShell.stories.tsx +89 -0
- package/src/app/components/AppShell.tsx +358 -0
- package/src/app/components/AppShell.zoom.test.tsx +149 -0
- package/src/app/components/Breadcrumb.stories.tsx +19 -0
- package/src/app/components/Breadcrumb.tsx +45 -0
- package/src/app/components/MenuBar.stories.tsx +28 -0
- package/src/app/components/MenuBar.tsx +44 -0
- package/src/app/components/PaneGrid.stories.tsx +84 -0
- package/src/app/components/PaneGrid.tsx +128 -0
- package/src/app/components/PanelColumn.stories.tsx +25 -0
- package/src/app/components/PanelColumn.tsx +93 -0
- package/src/app/components/ShellHeader.stories.tsx +50 -0
- package/src/app/components/ShellHeader.tsx +137 -0
- package/src/app/components/WorkspacePanels.stories.tsx +36 -0
- package/src/app/components/WorkspacePanels.tsx +120 -0
- package/src/app/components/pages/Workspace.stories.tsx +181 -0
- package/src/app/intent-dispatch.test.ts +728 -0
- package/src/app/intent-dispatch.ts +687 -0
- package/src/app/launch-layout.test.ts +78 -0
- package/src/app/launch-layout.ts +57 -0
- package/src/app/ready-signal.test.ts +85 -0
- package/src/app/ready-signal.ts +52 -0
- package/src/app/run.ts +109 -0
- package/src/{serve.test.ts → app/serve.test.ts} +24 -12
- package/src/app/serve.ts +111 -0
- package/src/app/version.ts +5 -0
- package/src/appearance/components/theme-context.stories.tsx +46 -0
- package/src/appearance/components/theme-context.tsx +37 -0
- package/src/appearance/theme-presets.test.ts +51 -0
- package/src/appearance/theme-presets.ts +219 -0
- package/src/appearance/theme.ts +112 -0
- package/src/appearance/visual/contrast.test.ts +35 -0
- package/src/appearance/visual/contrast.ts +33 -0
- package/src/diff/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +157 -0
- package/src/diff/components/AnnotatableFileView.tsx +141 -0
- package/src/diff/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/diff/components/BareWorkbenchFileView.tsx +63 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +434 -0
- package/src/diff/components/DiffContentView.split.test.tsx +140 -0
- package/src/diff/components/DiffContentView.stories.tsx +80 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/diff/components/DiffContentView.tsx +1055 -0
- package/src/diff/components/WalkWizard.stories.tsx +100 -0
- package/src/diff/components/WalkWizard.tsx +146 -0
- package/src/diff/components/diff-content-view-layout.test.ts +198 -0
- package/src/diff/components/diff-content-view-layout.ts +84 -0
- package/src/diff/diff-fold.test.ts +78 -0
- package/src/diff/diff-fold.ts +126 -0
- package/src/diff/diff-hunk-curate.test.ts +315 -0
- package/src/diff/diff-hunk-curate.ts +152 -0
- package/src/diff/diff-intraline.test.ts +239 -0
- package/src/diff/diff-intraline.ts +238 -0
- package/src/diff/diff-syntax.test.ts +125 -0
- package/src/diff/diff-syntax.ts +186 -0
- package/src/diff/split-diff.test.ts +110 -0
- package/src/diff/split-diff.ts +144 -0
- package/src/diff/view-diff.test.ts +325 -0
- package/src/diff/view-diff.ts +522 -0
- package/src/diff/walk.test.ts +121 -0
- package/src/diff/walk.ts +73 -0
- package/src/index.ts +31 -2
- package/src/integrations/attribution.test.ts +31 -0
- package/src/integrations/attribution.ts +42 -0
- package/src/integrations/client-extension-registry.test.tsx +61 -0
- package/src/integrations/client-extension-registry.ts +159 -0
- package/src/integrations/clipboard.test.ts +23 -0
- package/src/integrations/clipboard.ts +33 -0
- package/src/integrations/components/ExtensionZone.stories.tsx +28 -0
- package/src/integrations/components/ExtensionZone.tsx +145 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
- package/src/integrations/components/ShareDialog.stories.tsx +121 -0
- package/src/integrations/components/ShareDialog.tsx +574 -0
- package/src/integrations/components/share-dialog-store.ts +59 -0
- package/src/integrations/editor.test.ts +149 -0
- package/src/integrations/editor.ts +160 -0
- package/src/integrations/github-identity.test.ts +58 -0
- package/src/integrations/github-identity.ts +41 -0
- package/src/integrations/herdr.test.ts +43 -0
- package/src/integrations/herdr.ts +38 -0
- package/src/integrations/integrations.test.ts +41 -0
- package/src/integrations/integrations.ts +32 -0
- package/src/integrations/pasted-image.test.ts +67 -0
- package/src/integrations/pasted-image.ts +60 -0
- package/src/integrations/share.ts +238 -0
- package/src/keyboard/components/CompletionOverlay.stories.tsx +37 -0
- package/src/keyboard/components/CompletionOverlay.tsx +75 -0
- package/src/keyboard/components/HandleChipsInput.stories.tsx +24 -0
- package/src/keyboard/components/HandleChipsInput.test.tsx +35 -0
- package/src/keyboard/components/HandleChipsInput.tsx +143 -0
- package/src/keyboard/components/KeybindsDialog.stories.tsx +31 -0
- package/src/keyboard/components/KeybindsDialog.tsx +103 -0
- package/src/keyboard/components/NavModeHint.stories.tsx +40 -0
- package/src/keyboard/components/NavModeHint.tsx +31 -0
- package/src/keyboard/components/SlashComposer.stories.tsx +37 -0
- package/src/keyboard/components/SlashComposer.tsx +87 -0
- package/src/keyboard/key-bindings.test.ts +156 -0
- package/src/keyboard/key-bindings.ts +459 -0
- package/src/keyboard/keymap.test.ts +611 -0
- package/src/keyboard/keymap.ts +395 -0
- package/src/keyboard/skills.test.ts +74 -0
- package/src/keyboard/skills.ts +75 -0
- package/src/keyboard/slash-palette.test.ts +103 -0
- package/src/keyboard/slash-palette.ts +129 -0
- package/src/keyboard/thread-chords.test.ts +79 -0
- package/src/keyboard/thread-chords.ts +163 -0
- package/src/keyboard/use-shared-keyboard.ts +55 -0
- package/src/markdown/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/markdown/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/markdown/components/MarkdownGridBlock.tsx +72 -0
- package/src/markdown/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/markdown/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +160 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +951 -0
- package/src/markdown/components/ThreadView.slash.test.tsx +146 -0
- package/src/markdown/components/ThreadView.stories.tsx +26 -0
- package/src/markdown/components/ThreadView.tsx +560 -0
- package/src/markdown/mark-runs.ts +121 -0
- package/src/markdown/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown/markdown-grid.test.ts +101 -0
- package/src/markdown/markdown-grid.ts +203 -0
- package/src/markdown/markdown-highlight.test.ts +61 -0
- package/src/markdown/markdown-highlight.ts +124 -0
- package/src/markdown/thread-code-syntax.ts +45 -0
- package/src/markdown/use-thread-body-editing.tsx +54 -0
- package/src/markdown/view-plan.regression.test.ts +60 -0
- package/src/markdown/view-plan.test.ts +545 -0
- package/src/markdown/view-plan.ts +1009 -0
- package/src/performance/otlp-trace.test.ts +119 -0
- package/src/performance/otlp-trace.ts +134 -0
- package/src/performance/perf-report.ts +18 -0
- package/src/performance/perf-timings.test.ts +18 -0
- package/src/performance/perf-timings.ts +39 -0
- package/src/prototype/components/PrototypeContentView.stories.tsx +19 -0
- package/src/prototype/components/PrototypeContentView.tsx +442 -0
- package/src/prototype/prototype-browser.test.ts +81 -0
- package/src/prototype/prototype-browser.ts +321 -0
- package/src/prototype/prototype-pixels.tsx +35 -0
- package/src/settings/components/SettingsDialog.stories.tsx +117 -0
- package/src/settings/components/SettingsDialog.tsx +183 -0
- package/src/settings/components/SettingsRows.stories.tsx +18 -0
- package/src/settings/components/SettingsRows.tsx +112 -0
- package/src/settings/components/quick-actions-editor.stories.tsx +58 -0
- package/src/settings/components/quick-actions-editor.test.tsx +120 -0
- package/src/settings/components/quick-actions-editor.tsx +222 -0
- package/src/settings/config-identity.test.ts +78 -0
- package/src/settings/config.test.ts +676 -0
- package/src/settings/config.ts +628 -0
- package/src/settings/use-settings-dialog.tsx +351 -0
- package/src/stories/__snapshots__/stories.test.tsx.snap +2358 -0
- package/src/stories/stories-app.tsx +196 -0
- package/src/stories/stories.test.tsx +74 -0
- package/src/stories/story-fixtures.ts +102 -0
- package/src/stories/story-tree.test.ts +108 -0
- package/src/stories/story.ts +154 -0
- package/src/terminal/ansi-screen.test.ts +28 -0
- package/src/terminal/ansi-screen.ts +72 -0
- package/src/terminal/components/AnsiScreen.stories.tsx +12 -0
- package/src/terminal/components/AnsiScreen.tsx +28 -0
- package/src/terminal/components/terminal-pane.test.tsx +40 -0
- package/src/terminal/components/terminal-pane.ts +196 -0
- package/src/terminal/ghostty-terminal.test.ts +85 -0
- package/src/terminal/ghostty-terminal.ts +197 -0
- package/src/terminal/kitty-image.test.ts +66 -0
- package/src/terminal/kitty-image.ts +137 -0
- package/src/terminal/pty.test.ts +81 -0
- package/src/terminal/pty.ts +246 -0
- package/src/testing/test-support.test.ts +264 -0
- package/src/testing/test-support.ts +367 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +935 -0
- package/src/thread/components/AgentThreadPane.tsx +377 -0
- package/src/thread/components/ThreadFooter.stories.tsx +30 -0
- package/src/thread/components/ThreadFooter.test.tsx +49 -0
- package/src/thread/components/ThreadFooter.tsx +77 -0
- package/src/thread/components/ThreadTree.stories.tsx +46 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +267 -0
- package/src/thread/components/thread-tree.test.ts +137 -0
- package/src/thread/components/thread-tree.ts +112 -0
- package/src/thread/thread-controller.curate.test.ts +502 -0
- package/src/thread/thread-controller.derivation.test.ts +62 -0
- package/src/thread/thread-controller.live-diff.test.ts +357 -0
- package/src/thread/thread-controller.optimistic.test.ts +69 -0
- package/src/thread/thread-controller.share.test.ts +573 -0
- package/src/thread/thread-controller.switch-cache.test.ts +174 -0
- package/src/thread/thread-controller.tree.test.ts +263 -0
- package/src/thread/thread-controller.ts +2026 -0
- package/src/thread/tree-view.test.ts +128 -0
- package/src/thread/tree-view.ts +146 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +117 -0
- package/src/ui/components/CodeBlock.stories.tsx +42 -0
- package/src/ui/components/CodeBlock.tsx +80 -0
- package/src/ui/components/ConfirmCard.stories.tsx +37 -0
- package/src/ui/components/ConfirmCard.tsx +151 -0
- package/src/ui/components/ConfirmDialog.stories.tsx +21 -0
- package/src/ui/components/ConfirmDialog.tsx +72 -0
- package/src/ui/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +139 -0
- package/src/ui/components/OverlayScrollbar.tsx +129 -0
- package/src/ui/components/PromptDialog.stories.tsx +36 -0
- package/src/ui/components/PromptDialog.test.tsx +65 -0
- package/src/ui/components/PromptDialog.tsx +99 -0
- package/src/ui/components/RootOverlay.stories.tsx +45 -0
- package/src/ui/components/RootOverlay.tsx +49 -0
- package/src/ui/components/ScrollArea.stories.tsx +20 -0
- package/src/ui/components/ScrollArea.tsx +61 -0
- package/src/ui/components/Toast.stories.tsx +19 -0
- package/src/ui/components/Toast.tsx +54 -0
- package/src/ui/components/Tooltip.stories.tsx +26 -0
- package/src/ui/components/Tooltip.tsx +72 -0
- package/src/ui/components/menu-control.stories.tsx +35 -0
- package/src/ui/components/menu-control.tsx +43 -0
- package/src/ui/components/primitives/Button.stories.tsx +32 -0
- package/src/ui/components/primitives/Button.tsx +57 -0
- package/src/ui/components/primitives/Card.stories.tsx +25 -0
- package/src/ui/components/primitives/Card.tsx +62 -0
- package/src/ui/components/primitives/Dialog.stories.tsx +24 -0
- package/src/ui/components/primitives/Dialog.test.tsx +38 -0
- package/src/ui/components/primitives/Dialog.tsx +69 -0
- package/src/ui/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/ui/components/primitives/DialogActions.tsx +39 -0
- package/src/ui/components/primitives/IconButton.stories.tsx +36 -0
- package/src/ui/components/primitives/IconButton.tsx +58 -0
- package/src/ui/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/ui/components/primitives/StatusBar.tsx +23 -0
- package/src/ui/components/primitives/Switch.stories.tsx +17 -0
- package/src/ui/components/primitives/Switch.tsx +32 -0
- package/src/ui/components/primitives/Tabs.stories.tsx +29 -0
- package/src/ui/components/primitives/Tabs.tsx +101 -0
- package/src/ui/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/ui/components/primitives/Toolbar.tsx +14 -0
- package/src/ui/components/primitives/Tree.stories.tsx +124 -0
- package/src/ui/components/primitives/Tree.tsx +133 -0
- package/src/ui/components/primitives/frame.test.ts +12 -0
- package/src/ui/components/primitives/frame.ts +17 -0
- package/src/ui/components/primitives/icons.ts +61 -0
- package/src/ui/components/primitives/tree-model.test.ts +99 -0
- package/src/ui/components/primitives/tree-model.ts +137 -0
- package/src/ui/components/syntax-highlight.ts +133 -0
- package/src/ui/components/truncate-text.ts +7 -0
- package/src/ui/components/truncate-title.test.ts +31 -0
- package/src/ui/components/truncate-title.ts +15 -0
- package/src/ui/use-frame-measure.ts +74 -0
- package/src/ui/use-terminal-virtualizer.ts +259 -0
- package/src/workbench/components/ChangesColumn.stories.tsx +44 -0
- package/src/workbench/components/ChangesColumn.tsx +203 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +181 -0
- package/src/workbench/components/EditorGrid.split.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.stories.tsx +86 -0
- package/src/workbench/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/workbench/components/EditorGrid.tsx +544 -0
- package/src/workbench/components/EditorGrid.zoom.test.tsx +132 -0
- package/src/workbench/components/FileContentsView.stories.tsx +16 -0
- package/src/workbench/components/FileContentsView.test.tsx +29 -0
- package/src/workbench/components/FileContentsView.tsx +100 -0
- package/src/workbench/components/GridTabContent.actions.test.tsx +151 -0
- package/src/workbench/components/GridTabContent.stories.tsx +59 -0
- package/src/workbench/components/GridTabContent.tsx +193 -0
- package/src/workbench/components/ProjectTreeView.stories.tsx +25 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +236 -0
- package/src/workbench/components/ProjectTreeView.tsx +151 -0
- package/src/workbench/components/TreePane.stories.tsx +106 -0
- package/src/workbench/components/TreePane.tsx +155 -0
- package/src/workbench/components/WelcomePlayground.stories.tsx +12 -0
- package/src/workbench/components/WelcomePlayground.test.tsx +83 -0
- package/src/workbench/components/WelcomePlayground.tsx +138 -0
- package/src/workbench/components/editor-grid.test.ts +184 -0
- package/src/workbench/components/editor-grid.ts +281 -0
- package/src/workbench/components/file-tree.test.ts +60 -0
- package/src/workbench/components/file-tree.ts +112 -0
- package/src/workbench/use-changes-workbench.ts +205 -0
- package/src/workbench/use-remember-layout.ts +31 -0
- package/src/workbench/use-repo-changes.ts +91 -0
- package/src/workbench/workbench-snapshot.test.ts +99 -0
- package/src/workbench/workbench-snapshot.ts +59 -0
- package/src/App.diff.test.tsx +0 -99
- package/src/App.export.test.tsx +0 -88
- package/src/App.plan-review.test.tsx +0 -259
- package/src/App.readonly.test.tsx +0 -148
- package/src/App.rendering.test.tsx +0 -108
- package/src/App.submit-confirm.test.tsx +0 -183
- package/src/App.test.tsx +0 -218
- package/src/App.tsx +0 -1249
- package/src/config.test.ts +0 -116
- package/src/config.ts +0 -150
- package/src/editor.ts +0 -34
- package/src/herdr.test.ts +0 -68
- package/src/herdr.ts +0 -34
- package/src/keymap.test.ts +0 -270
- package/src/keymap.ts +0 -173
- package/src/run.ts +0 -30
- package/src/serve.ts +0 -75
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/theme.ts +0 -46
- package/src/view-diff.test.ts +0 -55
- package/src/view-diff.ts +0 -86
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
package/src/App.diff.test.tsx
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/** Diff-review flow in the virtual terminal (slice 3). */
|
|
2
|
-
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import React from "react";
|
|
8
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
-
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
-
import type { ReviewSession } from "@cueloop/schema";
|
|
11
|
-
import { App } from "./App";
|
|
12
|
-
|
|
13
|
-
const PATCH = `diff --git a/src/store.ts b/src/store.ts
|
|
14
|
-
index 111..222 100644
|
|
15
|
-
--- a/src/store.ts
|
|
16
|
-
+++ b/src/store.ts
|
|
17
|
-
@@ -1,4 +1,4 @@
|
|
18
|
-
export class Store {
|
|
19
|
-
- private items = [];
|
|
20
|
-
+ private items = new Map();
|
|
21
|
-
}
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
let home: string;
|
|
25
|
-
let server: DaemonServer;
|
|
26
|
-
let session: ReviewSession;
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
home = mkdtempSync(join(tmpdir(), "cueloop-diff-"));
|
|
30
|
-
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
31
|
-
server.start();
|
|
32
|
-
session = server.core.sessionCreate({
|
|
33
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
34
|
-
artifact: { type: "diff", content: PATCH, meta: { title: "working tree" } },
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
server.stop();
|
|
39
|
-
rmSync(home, { recursive: true, force: true });
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
async function renderApp() {
|
|
43
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 30 });
|
|
44
|
-
for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
|
|
45
|
-
await Bun.sleep(25);
|
|
46
|
-
await setup.renderOnce();
|
|
47
|
-
}
|
|
48
|
-
await setup.renderOnce();
|
|
49
|
-
return setup;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
53
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
54
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
55
|
-
else await setup.mockInput.typeText(k);
|
|
56
|
-
await Bun.sleep(15);
|
|
57
|
-
await setup.renderOnce();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
describe("diff review", () => {
|
|
61
|
-
test("renders file header, hunks, and signed lines", async () => {
|
|
62
|
-
const setup = await renderApp();
|
|
63
|
-
const frame = setup.captureCharFrame();
|
|
64
|
-
expect(frame).toContain("■ src/store.ts");
|
|
65
|
-
expect(frame).toContain("@@ -1,4 +1,4 @@");
|
|
66
|
-
expect(frame).toContain("- private items = [];");
|
|
67
|
-
expect(frame).toContain("+ private items = new Map();");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("line comment: quote-anchored, lands in feedback with the code line", async () => {
|
|
71
|
-
const setup = await renderApp();
|
|
72
|
-
// rows: file(0), hunk(1), ctx(2), del(3), add(4)
|
|
73
|
-
for (let i = 0; i < 4; i++) await press(setup, "j");
|
|
74
|
-
await press(setup, "c");
|
|
75
|
-
expect(setup.captureCharFrame()).toContain("COMMENT ON");
|
|
76
|
-
await setup.mockInput.typeText("Map needs an eviction story.");
|
|
77
|
-
await press(setup, "enter");
|
|
78
|
-
await Bun.sleep(80);
|
|
79
|
-
await setup.renderOnce();
|
|
80
|
-
const stored = server.core.sessionGet(session.id);
|
|
81
|
-
expect(stored.annotations.length).toBe(1);
|
|
82
|
-
expect(stored.annotations[0]!.anchor.quote).toContain("new Map()");
|
|
83
|
-
// inline annotation card rendered under the line
|
|
84
|
-
expect(setup.captureCharFrame()).toContain("◆ Map needs an eviction story.");
|
|
85
|
-
|
|
86
|
-
await press(setup, "enter"); // submit
|
|
87
|
-
await press(setup, "enter"); // confirm request_changes
|
|
88
|
-
await Bun.sleep(100);
|
|
89
|
-
const resolved = server.core.sessionGet(session.id);
|
|
90
|
-
expect(resolved.verdict!.feedback).toContain("new Map()");
|
|
91
|
-
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("plan verbs are guarded in diff sessions", async () => {
|
|
95
|
-
const setup = await renderApp();
|
|
96
|
-
await press(setup, "x");
|
|
97
|
-
expect(setup.captureCharFrame()).toContain("plan-only verb");
|
|
98
|
-
});
|
|
99
|
-
});
|
package/src/App.export.test.tsx
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
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
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import React from "react";
|
|
12
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
13
|
-
import { DaemonServer } from "@cueloop/daemon";
|
|
14
|
-
import type { ReviewSession } from "@cueloop/schema";
|
|
15
|
-
import { App } from "./App";
|
|
16
|
-
|
|
17
|
-
const PLAN = `# Migration Plan
|
|
18
|
-
|
|
19
|
-
Move the store atomically.
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
let home: string;
|
|
23
|
-
let vault: string;
|
|
24
|
-
let server: DaemonServer;
|
|
25
|
-
let session: ReviewSession;
|
|
26
|
-
let savedConfigEnv: string | undefined;
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
|
|
30
|
-
vault = join(home, "vault");
|
|
31
|
-
mkdirSync(vault);
|
|
32
|
-
const configPath = join(home, "config.toml");
|
|
33
|
-
writeFileSync(configPath, `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`);
|
|
34
|
-
savedConfigEnv = process.env.CUELOOP_CONFIG;
|
|
35
|
-
process.env.CUELOOP_CONFIG = configPath;
|
|
36
|
-
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
37
|
-
server.start();
|
|
38
|
-
session = server.core.sessionCreate({
|
|
39
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
40
|
-
artifact: { type: "plan", content: PLAN, meta: { title: "Migration Plan", planPath: "plan.md" } },
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
if (savedConfigEnv === undefined) delete process.env.CUELOOP_CONFIG;
|
|
45
|
-
else process.env.CUELOOP_CONFIG = savedConfigEnv;
|
|
46
|
-
server.stop();
|
|
47
|
-
rmSync(home, { recursive: true, force: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("obsidian export on resolve", () => {
|
|
51
|
-
test("submitting a review writes the plan into the vault and shows the path", async () => {
|
|
52
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 32 });
|
|
53
|
-
for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
|
|
54
|
-
await Bun.sleep(25);
|
|
55
|
-
await setup.renderOnce();
|
|
56
|
-
}
|
|
57
|
-
// open the submit overlay, keep the default verdict (approve), submit
|
|
58
|
-
setup.mockInput.pressKey("RETURN");
|
|
59
|
-
await Bun.sleep(15);
|
|
60
|
-
await setup.renderOnce();
|
|
61
|
-
expect(setup.captureCharFrame()).toContain("verdict");
|
|
62
|
-
setup.mockInput.pressKey("RETURN");
|
|
63
|
-
// wall-clock deadlines: the export is an async round-trip after resolve,
|
|
64
|
-
// and an iteration count times out on a loaded runner
|
|
65
|
-
const dir = join(vault, "cueloop");
|
|
66
|
-
const fileDeadline = Date.now() + 30_000;
|
|
67
|
-
while (Date.now() < fileDeadline && !existsSync(dir)) {
|
|
68
|
-
await Bun.sleep(25);
|
|
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
|
-
}
|
|
77
|
-
|
|
78
|
-
const notes = readdirSync(dir);
|
|
79
|
-
expect(notes.length).toBe(1);
|
|
80
|
-
expect(notes[0]).toMatch(/^\d{4}-\d{2}-\d{2} - Migration Plan\.md$/);
|
|
81
|
-
const written = readFileSync(join(dir, notes[0]!), "utf8");
|
|
82
|
-
expect(written).toContain("source: cueloop");
|
|
83
|
-
expect(written).toContain(`session: ${session.id}`);
|
|
84
|
-
expect(written).toContain("verdict: approve");
|
|
85
|
-
expect(written).toContain("Move the store atomically.");
|
|
86
|
-
expect(setup.captureCharFrame()).toContain("exported to");
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,259 +0,0 @@
|
|
|
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
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import { mkdtempSync, rmSync } 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 { ReviewSession } from "@cueloop/schema";
|
|
17
|
-
import { App } from "./App";
|
|
18
|
-
import { DARK as T } from "./theme";
|
|
19
|
-
|
|
20
|
-
const PLAN = `# Migration Plan
|
|
21
|
-
|
|
22
|
-
## Context
|
|
23
|
-
|
|
24
|
-
The daemon persists sessions to disk atomically.
|
|
25
|
-
|
|
26
|
-
## Steps
|
|
27
|
-
|
|
28
|
-
- move the store
|
|
29
|
-
- add recovery
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
let home: string;
|
|
33
|
-
let server: DaemonServer;
|
|
34
|
-
let session: ReviewSession;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
home = mkdtempSync(join(tmpdir(), "cueloop-plan-review-"));
|
|
38
|
-
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
39
|
-
server.start();
|
|
40
|
-
session = server.core.sessionCreate({
|
|
41
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
42
|
-
artifact: { type: "plan", content: PLAN, meta: { title: "Migration Plan", planPath: "plan.md", agent: "agent/worker-3" } },
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
server.stop();
|
|
47
|
-
rmSync(home, { recursive: true, force: true });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
async function renderApp() {
|
|
51
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
52
|
-
width: 120,
|
|
53
|
-
height: 32,
|
|
54
|
-
});
|
|
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
|
-
|
|
63
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
64
|
-
|
|
65
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
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();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function type(setup: Setup, text: string): Promise<void> {
|
|
77
|
-
await setup.mockInput.typeText(text);
|
|
78
|
-
await Bun.sleep(15);
|
|
79
|
-
await setup.renderOnce();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Background colors (hex) of every styled span containing the needle - the
|
|
84
|
-
* document highlight and the rail's quote excerpt can both match.
|
|
85
|
-
*/
|
|
86
|
-
function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
87
|
-
const backgrounds: string[] = [];
|
|
88
|
-
for (const line of setup.captureSpans().lines) {
|
|
89
|
-
for (const span of line.spans) {
|
|
90
|
-
if (!span.text.includes(needle)) continue;
|
|
91
|
-
const [red, green, blue] = span.bg.toInts();
|
|
92
|
-
backgrounds.push("#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return backgrounds;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Move the cursor to "The daemon persists sessions to disk atomically." */
|
|
99
|
-
async function toContextParagraph(setup: Setup): Promise<void> {
|
|
100
|
-
await press(setup, "j");
|
|
101
|
-
await press(setup, "j");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
describe("selection feeds the annotation quote", () => {
|
|
105
|
-
test("v anchors, l extends, c composes with the char-precise quote", async () => {
|
|
106
|
-
const setup = await renderApp();
|
|
107
|
-
await toContextParagraph(setup);
|
|
108
|
-
await press(setup, "v");
|
|
109
|
-
await press(setup, "l"); // "The daemon"
|
|
110
|
-
await press(setup, "c");
|
|
111
|
-
expect(setup.captureCharFrame()).toContain('comment on "The daemon"');
|
|
112
|
-
await type(setup, "Which daemon?");
|
|
113
|
-
await press(setup, "enter");
|
|
114
|
-
await Bun.sleep(80);
|
|
115
|
-
await setup.renderOnce();
|
|
116
|
-
const stored = server.core.sessionGet(session.id);
|
|
117
|
-
expect(stored.annotations.length).toBe(1);
|
|
118
|
-
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a mouse drag over the text becomes the compose quote", async () => {
|
|
122
|
-
const setup = await renderApp();
|
|
123
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
124
|
-
const row = lines.findIndex((line) => line.includes("persists sessions"));
|
|
125
|
-
const startColumn = lines[row]!.indexOf("persists");
|
|
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"');
|
|
132
|
-
await type(setup, "sessions plural?");
|
|
133
|
-
await press(setup, "enter");
|
|
134
|
-
await Bun.sleep(80);
|
|
135
|
-
const stored = server.core.sessionGet(session.id);
|
|
136
|
-
expect(stored.annotations[0]!.anchor.quote).toBe("persists sessions");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe("inline compose keeps the anchor painted", () => {
|
|
141
|
-
test("the selection stays painted while composing; cancel un-paints; save converts to the comment highlight", async () => {
|
|
142
|
-
const setup = await renderApp();
|
|
143
|
-
await toContextParagraph(setup);
|
|
144
|
-
await press(setup, "v");
|
|
145
|
-
await press(setup, "l");
|
|
146
|
-
await press(setup, "c");
|
|
147
|
-
// compose open: the anchor is painted selection-style
|
|
148
|
-
expect(setup.captureCharFrame()).toContain("Save ⏎");
|
|
149
|
-
expect(setup.captureCharFrame()).toContain("Cancel esc");
|
|
150
|
-
expect(backgroundsOf(setup, "The daemon")).toContain(T.accent);
|
|
151
|
-
// cancel un-paints
|
|
152
|
-
await press(setup, "escape");
|
|
153
|
-
expect(setup.captureCharFrame()).not.toContain('comment on "');
|
|
154
|
-
expect(backgroundsOf(setup, "The daemon")).not.toContain(T.accent);
|
|
155
|
-
// save converts the paint to the kind-colored annotation highlight
|
|
156
|
-
await press(setup, "c");
|
|
157
|
-
await type(setup, "Which daemon?");
|
|
158
|
-
await press(setup, "enter");
|
|
159
|
-
await Bun.sleep(80);
|
|
160
|
-
await setup.renderOnce();
|
|
161
|
-
// the whole cursor block is the anchor here; check the highlight landed
|
|
162
|
-
const stored = server.core.sessionGet(session.id);
|
|
163
|
-
expect(stored.annotations.length).toBe(1);
|
|
164
|
-
expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(T.markCommentBg);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("the document selects, the rail edits", () => {
|
|
169
|
-
test("e edits the selected card in place; enter saves through the controller", async () => {
|
|
170
|
-
const setup = await renderApp();
|
|
171
|
-
await toContextParagraph(setup);
|
|
172
|
-
await press(setup, "c");
|
|
173
|
-
await type(setup, "Needs a citation.");
|
|
174
|
-
await press(setup, "enter");
|
|
175
|
-
await Bun.sleep(80);
|
|
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.");
|
|
181
|
-
await press(setup, "enter");
|
|
182
|
-
await Bun.sleep(80);
|
|
183
|
-
await setup.renderOnce();
|
|
184
|
-
const stored = server.core.sessionGet(session.id);
|
|
185
|
-
expect(stored.annotations.length).toBe(1);
|
|
186
|
-
expect(stored.annotations[0]!.body).toBe("Needs a citation. And a link.");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("x deletes the selected card and un-paints the document highlight", async () => {
|
|
190
|
-
const setup = await renderApp();
|
|
191
|
-
await toContextParagraph(setup);
|
|
192
|
-
await press(setup, "c");
|
|
193
|
-
await type(setup, "Delete me.");
|
|
194
|
-
await press(setup, "enter");
|
|
195
|
-
await Bun.sleep(80);
|
|
196
|
-
await setup.renderOnce();
|
|
197
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
198
|
-
expect(backgroundsOf(setup, "persists sessions")).toContain(T.markCommentBg);
|
|
199
|
-
await press(setup, "x");
|
|
200
|
-
await Bun.sleep(80);
|
|
201
|
-
await setup.renderOnce();
|
|
202
|
-
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
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe("sheet header", () => {
|
|
209
|
-
test("shows the submitting agent, the revision, and the Edit word-button", async () => {
|
|
210
|
-
const setup = await renderApp();
|
|
211
|
-
const frame = setup.captureCharFrame();
|
|
212
|
-
expect(frame).toContain("submitted by agent/worker-3 · revision 1");
|
|
213
|
-
expect(frame).toContain("Edit");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("the Agent tab shows the submitter, status, and revision", async () => {
|
|
217
|
-
const setup = await renderApp();
|
|
218
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
219
|
-
const tabRow = lines.findIndex((line) => line.includes("Review (0)"));
|
|
220
|
-
const tabColumn = lines[tabRow]!.indexOf("Agent");
|
|
221
|
-
await setup.mockMouse.click(tabColumn + 1, tabRow);
|
|
222
|
-
await setup.renderOnce();
|
|
223
|
-
const frame = setup.captureCharFrame();
|
|
224
|
-
expect(frame).toContain("agent/worker-3");
|
|
225
|
-
expect(frame).toContain("status: pending");
|
|
226
|
-
expect(frame).toContain("revision 1");
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe("edit-exit reconciliation", () => {
|
|
231
|
-
test("an edit that removes an anchored passage orphans the annotation and shows the banner", async () => {
|
|
232
|
-
const script = join(home, "fake-editor.sh");
|
|
233
|
-
await Bun.write(script, `#!/bin/sh\nsed -i '' '/^The daemon persists/d' "$1"\n`);
|
|
234
|
-
Bun.spawnSync(["chmod", "+x", script]);
|
|
235
|
-
process.env.CUELOOP_EDITOR = script;
|
|
236
|
-
try {
|
|
237
|
-
const setup = await renderApp();
|
|
238
|
-
await toContextParagraph(setup);
|
|
239
|
-
await press(setup, "c");
|
|
240
|
-
await type(setup, "Anchor me to the doomed passage.");
|
|
241
|
-
await press(setup, "enter");
|
|
242
|
-
await Bun.sleep(80);
|
|
243
|
-
await setup.renderOnce();
|
|
244
|
-
expect(setup.captureCharFrame()).toContain("· pending");
|
|
245
|
-
// deselect the card so e reaches the editor hand-off, then edit
|
|
246
|
-
await press(setup, "escape");
|
|
247
|
-
await press(setup, "e");
|
|
248
|
-
await Bun.sleep(150);
|
|
249
|
-
await setup.renderOnce();
|
|
250
|
-
const frame = setup.captureCharFrame();
|
|
251
|
-
expect(frame).toContain("1 annotation no longer match - the passage was removed.");
|
|
252
|
-
expect(frame).toContain("· ORPHANED");
|
|
253
|
-
// the annotation is NOT deleted: the feedback serializer handles orphans
|
|
254
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
255
|
-
} finally {
|
|
256
|
-
delete process.env.CUELOOP_EDITOR;
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
});
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Observer mode (tier 2): the App with readOnly renders and navigates like the
|
|
3
|
-
* controller's, but every mutating verb answers "observer - read-only" and
|
|
4
|
-
* leaves daemon state untouched.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import React from "react";
|
|
12
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
13
|
-
import { DaemonServer } from "@cueloop/daemon";
|
|
14
|
-
import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
|
|
15
|
-
import { App } from "./App";
|
|
16
|
-
|
|
17
|
-
const PLAN = `# Migration Plan
|
|
18
|
-
|
|
19
|
-
## Context
|
|
20
|
-
|
|
21
|
-
The daemon persists sessions to disk atomically.
|
|
22
|
-
|
|
23
|
-
## Steps
|
|
24
|
-
|
|
25
|
-
- move the store
|
|
26
|
-
- add recovery
|
|
27
|
-
`;
|
|
28
|
-
|
|
29
|
-
let home: string;
|
|
30
|
-
let server: DaemonServer;
|
|
31
|
-
let session: ReviewSession;
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
home = mkdtempSync(join(tmpdir(), "cueloop-observer-"));
|
|
35
|
-
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
|
-
server.start();
|
|
37
|
-
session = server.core.sessionCreate({
|
|
38
|
-
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
-
artifact: { type: "plan", content: PLAN, meta: { title: "Migration Plan", planPath: "plan.md" } },
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
server.stop();
|
|
44
|
-
rmSync(home, { recursive: true, force: true });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
async function renderObserver() {
|
|
48
|
-
const setup = await testRender(<App home={home} sessionId={session.id} readOnly />, {
|
|
49
|
-
width: 120,
|
|
50
|
-
height: 32,
|
|
51
|
-
});
|
|
52
|
-
for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
|
|
53
|
-
await Bun.sleep(25);
|
|
54
|
-
await setup.renderOnce();
|
|
55
|
-
}
|
|
56
|
-
await setup.renderOnce();
|
|
57
|
-
return setup;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
type Setup = Awaited<ReturnType<typeof renderObserver>>;
|
|
61
|
-
|
|
62
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
63
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
64
|
-
else await setup.mockInput.typeText(k);
|
|
65
|
-
await Bun.sleep(15);
|
|
66
|
-
await setup.renderOnce();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Session state that any mutating verb would change. */
|
|
70
|
-
function snapshot() {
|
|
71
|
-
const s = server.core.sessionGet(session.id);
|
|
72
|
-
return { annotations: s.annotations.length, workingCopy: s.workingCopy, status: s.status };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
describe("observer rendering", () => {
|
|
76
|
-
test("shows the observer badge and read-only hint bar", async () => {
|
|
77
|
-
const setup = await renderObserver();
|
|
78
|
-
const frame = setup.captureCharFrame();
|
|
79
|
-
expect(frame).toContain("· observer");
|
|
80
|
-
expect(frame).toContain("observer - read-only · j/k move");
|
|
81
|
-
expect(frame).not.toContain("⏎ submit");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("observer verbs are blocked", () => {
|
|
86
|
-
for (const [key, verb] of [
|
|
87
|
-
["c", "comment"],
|
|
88
|
-
["s", "suggest"],
|
|
89
|
-
["x", "cut"],
|
|
90
|
-
["e", "edit"],
|
|
91
|
-
["enter", "submit"],
|
|
92
|
-
] as const) {
|
|
93
|
-
test(`${verb} (${key}) answers observer - read-only and mutates nothing`, async () => {
|
|
94
|
-
const setup = await renderObserver();
|
|
95
|
-
const before = snapshot();
|
|
96
|
-
await press(setup, "j");
|
|
97
|
-
await press(setup, "j");
|
|
98
|
-
await press(setup, key);
|
|
99
|
-
await Bun.sleep(60);
|
|
100
|
-
await setup.renderOnce();
|
|
101
|
-
const frame = setup.captureCharFrame();
|
|
102
|
-
expect(frame).toContain("observer - read-only");
|
|
103
|
-
// no overlay opened: compose/submit bars never appear
|
|
104
|
-
expect(frame).not.toContain("COMMENT ON");
|
|
105
|
-
expect(frame).not.toContain("SUGGEST REPLACEMENT");
|
|
106
|
-
expect(frame).not.toContain("verdict ←/→");
|
|
107
|
-
expect(snapshot()).toEqual(before);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
test("span mode c/s is blocked too", async () => {
|
|
112
|
-
const setup = await renderObserver();
|
|
113
|
-
await press(setup, "j");
|
|
114
|
-
await press(setup, "j");
|
|
115
|
-
await press(setup, "v");
|
|
116
|
-
await press(setup, "c");
|
|
117
|
-
await setup.renderOnce();
|
|
118
|
-
const frame = setup.captureCharFrame();
|
|
119
|
-
expect(frame).toContain("observer - read-only");
|
|
120
|
-
expect(frame).not.toContain("COMMENT ON");
|
|
121
|
-
expect(snapshot().annotations).toBe(0);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("observer navigation still works", () => {
|
|
126
|
-
test("j/k moves the cursor between blocks", async () => {
|
|
127
|
-
const setup = await renderObserver();
|
|
128
|
-
await press(setup, "j");
|
|
129
|
-
await press(setup, "j");
|
|
130
|
-
await setup.renderOnce();
|
|
131
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
132
|
-
const cursorLine = lines.find((l) => l.includes("▎"))!;
|
|
133
|
-
expect(cursorLine).toContain("persists sessions");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("n focuses annotations made by the controller", async () => {
|
|
137
|
-
server.core.sessionAnnotate(session.id, {
|
|
138
|
-
id: "a_obs1",
|
|
139
|
-
kind: "comment",
|
|
140
|
-
anchor: makeAnchor(parseBlocks(PLAN), 2, 0, 10),
|
|
141
|
-
body: "From the controller.",
|
|
142
|
-
});
|
|
143
|
-
const setup = await renderObserver();
|
|
144
|
-
await press(setup, "n");
|
|
145
|
-
await setup.renderOnce();
|
|
146
|
-
expect(setup.captureCharFrame()).toContain("▸ COMMENT");
|
|
147
|
-
});
|
|
148
|
-
});
|