@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
SCHEMA_VERSION,
|
|
4
|
+
type DiffFileContents,
|
|
5
|
+
type HunkRejection,
|
|
6
|
+
type Thread,
|
|
7
|
+
} from "@cueloop/schema";
|
|
8
|
+
import { curateDiff } from "@cueloop/daemon/curate";
|
|
9
|
+
import type { ThreadClient } from "@cueloop/daemon/client";
|
|
10
|
+
import { createReviewController } from "./thread-controller";
|
|
11
|
+
|
|
12
|
+
const PATCH_A = `diff --git a/src/a.ts b/src/a.ts
|
|
13
|
+
index 111..222 100644
|
|
14
|
+
--- a/src/a.ts
|
|
15
|
+
+++ b/src/a.ts
|
|
16
|
+
@@ -1,3 +1,3 @@
|
|
17
|
+
export class A {
|
|
18
|
+
- value = 1;
|
|
19
|
+
+ value = 2;
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const PATCH_B = `diff --git a/src/b.ts b/src/b.ts
|
|
24
|
+
index 333..444 100644
|
|
25
|
+
--- a/src/b.ts
|
|
26
|
+
+++ b/src/b.ts
|
|
27
|
+
@@ -1,3 +1,3 @@
|
|
28
|
+
export class B {
|
|
29
|
+
- flag = false;
|
|
30
|
+
+ flag = true;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function diffSession(id: string, patch: string, files: DiffFileContents[]): Thread {
|
|
35
|
+
return {
|
|
36
|
+
schemaVersion: SCHEMA_VERSION,
|
|
37
|
+
id,
|
|
38
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
+
artifact: { type: "diff", content: patch, meta: { title: id }, files },
|
|
40
|
+
revisions: [{ revision: 1, content: patch, submittedAt: "2026-01-01T00:00:00.000Z" }],
|
|
41
|
+
annotations: [],
|
|
42
|
+
message: null,
|
|
43
|
+
status: "pending",
|
|
44
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const FILES_A: DiffFileContents[] = [
|
|
49
|
+
{
|
|
50
|
+
path: "src/a.ts",
|
|
51
|
+
oldContents: "export class A {\n value = 1;\n}\n",
|
|
52
|
+
newContents: "export class A {\n value = 2;\n}\n",
|
|
53
|
+
status: "modified",
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const FILES_B: DiffFileContents[] = [
|
|
57
|
+
{
|
|
58
|
+
path: "src/b.ts",
|
|
59
|
+
oldContents: "export class B {\n flag = false;\n}\n",
|
|
60
|
+
newContents: "export class B {\n flag = true;\n}\n",
|
|
61
|
+
status: "modified",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const unimplemented = (member: string) => () =>
|
|
66
|
+
Promise.reject(new Error(`fakeClient does not implement ${member}`));
|
|
67
|
+
|
|
68
|
+
/** A fake client over a fixed set of sessions, so open(id) can switch between them from the inbox. */
|
|
69
|
+
function fakeClient(sessions: Thread[]): ThreadClient {
|
|
70
|
+
const byId = new Map(sessions.map((session) => [session.id, session]));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
onEvent: () => () => {},
|
|
74
|
+
subscribe: async () => {},
|
|
75
|
+
sessionGet: async (id: string) => byId.get(id)!,
|
|
76
|
+
sessionList: async () => [...byId.values()],
|
|
77
|
+
sessionCurate: mock(async (id: string, rejections: HunkRejection[]) => {
|
|
78
|
+
const session = byId.get(id)!;
|
|
79
|
+
const content = rejections.length
|
|
80
|
+
? curateDiff(session.artifact.files!, rejections)
|
|
81
|
+
: undefined;
|
|
82
|
+
const next = {
|
|
83
|
+
...session,
|
|
84
|
+
workingCopy: content,
|
|
85
|
+
curation: rejections.length ? rejections : undefined,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
byId.set(id, next);
|
|
89
|
+
|
|
90
|
+
return next;
|
|
91
|
+
}),
|
|
92
|
+
sessionComment: unimplemented("sessionComment"),
|
|
93
|
+
sessionAnnotate: unimplemented("sessionAnnotate"),
|
|
94
|
+
sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
|
|
95
|
+
sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
|
|
96
|
+
sessionCutBlock: unimplemented("sessionCutBlock"),
|
|
97
|
+
sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
|
|
98
|
+
sessionSetAccess: unimplemented("sessionSetAccess"),
|
|
99
|
+
sessionNavigate: unimplemented("sessionNavigate"),
|
|
100
|
+
sessionBranch: unimplemented("sessionBranch"),
|
|
101
|
+
sessionSwitch: unimplemented("sessionSwitch"),
|
|
102
|
+
sessionLabel: unimplemented("sessionLabel"),
|
|
103
|
+
sessionFork: unimplemented("sessionFork"),
|
|
104
|
+
sessionSetViewed: unimplemented("sessionSetViewed"),
|
|
105
|
+
sessionSetTitle: unimplemented("sessionSetTitle"),
|
|
106
|
+
sessionSetShareId: unimplemented("sessionSetShareId"),
|
|
107
|
+
sessionSetShares: unimplemented("sessionSetShares"),
|
|
108
|
+
sessionMergeShared: unimplemented("sessionMergeShared"),
|
|
109
|
+
sessionDelete: unimplemented("sessionDelete"),
|
|
110
|
+
sessionSetSelfName: unimplemented("sessionSetSelfName"),
|
|
111
|
+
sessionSendMessage: unimplemented("sessionSendMessage"),
|
|
112
|
+
close: () => {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
117
|
+
|
|
118
|
+
// rows for each single-change patch: file(0), hunk(1), ctx(2), del(3), add(4), ctx(5)
|
|
119
|
+
describe("switching threads keeps the cached projection curatable", () => {
|
|
120
|
+
test("curation still works after switching away and back to a diff thread", async () => {
|
|
121
|
+
// Arrange - two diff threads in one inbox
|
|
122
|
+
const controller = createReviewController({
|
|
123
|
+
sessionId: "ses_a",
|
|
124
|
+
openClient: async () =>
|
|
125
|
+
fakeClient([
|
|
126
|
+
diffSession("ses_a", PATCH_A, FILES_A),
|
|
127
|
+
diffSession("ses_b", PATCH_B, FILES_B),
|
|
128
|
+
]),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
controller.connect();
|
|
132
|
+
await tick();
|
|
133
|
+
|
|
134
|
+
// Act - visit B, then return to A (A's projection now comes from the per-thread cache)
|
|
135
|
+
controller.open("ses_b");
|
|
136
|
+
await tick();
|
|
137
|
+
controller.open("ses_a");
|
|
138
|
+
await tick();
|
|
139
|
+
|
|
140
|
+
// Assert - the returned thread's file model parses lazily and curation lands
|
|
141
|
+
controller.toggleRejectChange(4);
|
|
142
|
+
await tick();
|
|
143
|
+
|
|
144
|
+
expect(controller.getSnapshot().toast).toBeNull();
|
|
145
|
+
expect(controller.getSnapshot().session?.workingCopy).toBe("");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("returning to a diff thread shows the daemon's latest, not the stale sidebar copy", async () => {
|
|
149
|
+
// The sidebar list trails the daemon for a hot-reloading diff (its session.updated
|
|
150
|
+
// refreshes the open thread, not the list). Returning to it must revalidate.
|
|
151
|
+
const staleA = diffSession("ses_a", PATCH_A, FILES_A);
|
|
152
|
+
const freshA = diffSession("ses_a", PATCH_B, FILES_B);
|
|
153
|
+
const other = diffSession("ses_b", PATCH_B, FILES_B);
|
|
154
|
+
const base = fakeClient([staleA, other]);
|
|
155
|
+
const controller = createReviewController({
|
|
156
|
+
sessionId: "ses_a",
|
|
157
|
+
openClient: async () => ({
|
|
158
|
+
...base,
|
|
159
|
+
// the sidebar keeps the stale copy; the daemon serves the fresh one
|
|
160
|
+
sessionList: async () => [staleA, other],
|
|
161
|
+
sessionGet: async (id: string) => (id === "ses_a" ? freshA : other),
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
controller.connect();
|
|
166
|
+
await tick();
|
|
167
|
+
|
|
168
|
+
// Visit B, then return to A - open() paints the stale sidebar copy, then revalidates
|
|
169
|
+
controller.open("ses_b");
|
|
170
|
+
await tick();
|
|
171
|
+
controller.open("ses_a");
|
|
172
|
+
await tick();
|
|
173
|
+
|
|
174
|
+
expect(controller.getSnapshot().session?.artifact.content).toBe(PATCH_B);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controller's tree primitives over a fake client: a move shows its path
|
|
3
|
+
* before the daemon answers, refusals surface as status, a fork opens the fork,
|
|
4
|
+
* and fork-and-share shares the fork while the current session stays put.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
appendEntry,
|
|
10
|
+
createBranch,
|
|
11
|
+
historyFromLinear,
|
|
12
|
+
SCHEMA_VERSION,
|
|
13
|
+
switchBranch,
|
|
14
|
+
type Annotation,
|
|
15
|
+
type Thread,
|
|
16
|
+
} from "@cueloop/schema";
|
|
17
|
+
import type { ThreadClient } from "@cueloop/daemon/client";
|
|
18
|
+
import { createReviewController, type ShareTransport } from "./thread-controller";
|
|
19
|
+
import { mergeFromShare } from "./share";
|
|
20
|
+
|
|
21
|
+
const AT = "2026-01-01T00:00:00.000Z";
|
|
22
|
+
|
|
23
|
+
function annotation(id: string, body: string): Annotation {
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
kind: "comment",
|
|
27
|
+
anchor: { quote: "Plan", prefix: "", suffix: "" },
|
|
28
|
+
body,
|
|
29
|
+
createdAt: AT,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A plan with one comment on main and its history. */
|
|
34
|
+
function sessionFixture(): Thread {
|
|
35
|
+
const session: Thread = {
|
|
36
|
+
schemaVersion: SCHEMA_VERSION,
|
|
37
|
+
id: "ses_1",
|
|
38
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
+
artifact: { type: "plan", content: "# Plan\n", meta: {} },
|
|
40
|
+
revisions: [{ revision: 1, content: "# Plan\n", submittedAt: AT }],
|
|
41
|
+
annotations: [annotation("a1", "first")],
|
|
42
|
+
message: null,
|
|
43
|
+
status: "pending",
|
|
44
|
+
createdAt: AT,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
session.history = historyFromLinear(session);
|
|
48
|
+
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const publish = mock(async (session: Thread) => ({
|
|
53
|
+
line: `ssh p_${session.id}@cueloop.dev`,
|
|
54
|
+
copied: true,
|
|
55
|
+
}));
|
|
56
|
+
const shareTransport: ShareTransport = {
|
|
57
|
+
publish,
|
|
58
|
+
pull: mock(async () => sessionFixture()),
|
|
59
|
+
push: mock(async () => {}),
|
|
60
|
+
watch: () => () => {},
|
|
61
|
+
revoke: async () => {},
|
|
62
|
+
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
63
|
+
formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
|
|
64
|
+
collaboratorAnnotations: (session) => session.annotations.filter((entry) => entry.author),
|
|
65
|
+
mergeFromShare,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const unimplemented = (member: string) => () =>
|
|
69
|
+
Promise.reject(new Error(`fakeClient does not implement ${member}`));
|
|
70
|
+
|
|
71
|
+
/** Tree requests never resolve here: what the controller shows is its own guess. */
|
|
72
|
+
const pending = () => new Promise<Thread>(() => {});
|
|
73
|
+
|
|
74
|
+
function fakeClient(session: Thread) {
|
|
75
|
+
return {
|
|
76
|
+
onEvent: () => () => {},
|
|
77
|
+
subscribe: async () => {},
|
|
78
|
+
sessionGet: async () => session,
|
|
79
|
+
sessionList: async () => [session],
|
|
80
|
+
sessionComment: unimplemented("sessionComment"),
|
|
81
|
+
sessionAnnotate: unimplemented("sessionAnnotate"),
|
|
82
|
+
sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
|
|
83
|
+
sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
|
|
84
|
+
sessionCutBlock: unimplemented("sessionCutBlock"),
|
|
85
|
+
sessionRestoreBlock: unimplemented("sessionRestoreBlock"),
|
|
86
|
+
sessionCurate: unimplemented("sessionCurate"),
|
|
87
|
+
sessionSetAccess: unimplemented("sessionSetAccess"),
|
|
88
|
+
sessionNavigate: mock(pending),
|
|
89
|
+
sessionBranch: mock(pending),
|
|
90
|
+
sessionSwitch: mock(pending),
|
|
91
|
+
sessionLabel: mock(pending),
|
|
92
|
+
sessionFork: mock(async (id: string) => ({
|
|
93
|
+
...sessionFixture(),
|
|
94
|
+
id: `${id}_fork`,
|
|
95
|
+
parentSessionId: id,
|
|
96
|
+
})),
|
|
97
|
+
sessionSetViewed: unimplemented("sessionSetViewed"),
|
|
98
|
+
sessionSetTitle: unimplemented("sessionSetTitle"),
|
|
99
|
+
sessionSetShareId: mock(async (_id: string, _shareId: string) => session),
|
|
100
|
+
sessionSetShares: mock(async () => session),
|
|
101
|
+
sessionMergeShared: unimplemented("sessionMergeShared"),
|
|
102
|
+
sessionDelete: unimplemented("sessionDelete"),
|
|
103
|
+
sessionSetSelfName: unimplemented("sessionSetSelfName"),
|
|
104
|
+
sessionSendMessage: unimplemented("sessionSendMessage"),
|
|
105
|
+
close: () => {},
|
|
106
|
+
} satisfies ThreadClient;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
110
|
+
|
|
111
|
+
async function connected(session = sessionFixture()) {
|
|
112
|
+
const client = fakeClient(session);
|
|
113
|
+
const controller = createReviewController({
|
|
114
|
+
sessionId: session.id,
|
|
115
|
+
openClient: async () => client,
|
|
116
|
+
shareTransport,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
controller.connect();
|
|
120
|
+
await tick();
|
|
121
|
+
|
|
122
|
+
return { controller, client, session };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("read-only guards", () => {
|
|
126
|
+
test("an observer's rename and delete never reach the daemon", async () => {
|
|
127
|
+
// Arrange - a read-only controller over a client whose mutations would throw
|
|
128
|
+
const session = sessionFixture();
|
|
129
|
+
const client = fakeClient(session);
|
|
130
|
+
const controller = createReviewController({
|
|
131
|
+
sessionId: session.id,
|
|
132
|
+
openClient: async () => client,
|
|
133
|
+
readOnly: true,
|
|
134
|
+
shareTransport,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
controller.connect();
|
|
138
|
+
await tick();
|
|
139
|
+
|
|
140
|
+
// Act - the sidebar's rename and delete, invoked directly
|
|
141
|
+
controller.renameSession(session.id, "hijacked");
|
|
142
|
+
controller.deleteSession(session.id);
|
|
143
|
+
await tick();
|
|
144
|
+
|
|
145
|
+
// Assert - both answer read-only and the throwing client stubs were never called
|
|
146
|
+
expect(controller.getSnapshot().status).toBe("observer - read-only");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("tree primitives", () => {
|
|
151
|
+
test("a move back shows the earlier path at once and asks the daemon for the same move", async () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const { controller, client, session } = await connected();
|
|
154
|
+
const revision = session.history!.entries[0]!.id;
|
|
155
|
+
|
|
156
|
+
// Act
|
|
157
|
+
controller.goToEntry(revision, "too soon");
|
|
158
|
+
|
|
159
|
+
// Assert: the comment is off the shown path before any answer; the summary travels
|
|
160
|
+
expect(controller.getSnapshot().session!.annotations).toEqual([]);
|
|
161
|
+
expect(controller.getSnapshot().session!.shelvedAnnotations?.map((entry) => entry.id)).toEqual([
|
|
162
|
+
"a1",
|
|
163
|
+
]);
|
|
164
|
+
expect(controller.treeRows().find((row) => row.glyph === "↩")?.isCurrentTip).toBe(true);
|
|
165
|
+
expect(client.sessionNavigate).toHaveBeenCalledWith("ses_1", revision, "too soon", undefined);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("a move on another branch is one request that names the branch", async () => {
|
|
169
|
+
// Arrange: alt with its own comment entry; the owner stands on main
|
|
170
|
+
const session = sessionFixture();
|
|
171
|
+
let history = createBranch(session.history!, "alt");
|
|
172
|
+
|
|
173
|
+
history = appendEntry(history, { type: "comment", annotationId: "b1", createdAt: AT }).history;
|
|
174
|
+
history = appendEntry(history, { type: "comment", annotationId: "b2", createdAt: AT }).history;
|
|
175
|
+
session.history = switchBranch(history, "main");
|
|
176
|
+
session.shelvedAnnotations = [annotation("b1", "alt one"), annotation("b2", "alt two")];
|
|
177
|
+
const { controller, client } = await connected(session);
|
|
178
|
+
const altFirst = history.entries.find(
|
|
179
|
+
(entry) => entry.type === "comment" && entry.annotationId === "b1",
|
|
180
|
+
)!;
|
|
181
|
+
|
|
182
|
+
// Act
|
|
183
|
+
controller.goToEntry(altFirst.id);
|
|
184
|
+
|
|
185
|
+
// Assert
|
|
186
|
+
expect(client.sessionSwitch).not.toHaveBeenCalled();
|
|
187
|
+
expect(client.sessionNavigate).toHaveBeenCalledWith("ses_1", altFirst.id, undefined, "alt");
|
|
188
|
+
expect(controller.getSnapshot().session!.annotations.map((entry) => entry.id)).toEqual([
|
|
189
|
+
"a1",
|
|
190
|
+
"b1",
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("a refused move gives way to the daemon's record", async () => {
|
|
195
|
+
// Arrange: the daemon refuses; a re-read hands back the untouched record
|
|
196
|
+
const session = sessionFixture();
|
|
197
|
+
const { controller, client } = await connected(session);
|
|
198
|
+
|
|
199
|
+
client.sessionNavigate.mockImplementationOnce(() => Promise.reject(new Error("resolved")));
|
|
200
|
+
|
|
201
|
+
// Act
|
|
202
|
+
controller.goToEntry(session.history!.entries[0]!.id);
|
|
203
|
+
const guessed = controller.getSnapshot().session!.annotations.length;
|
|
204
|
+
|
|
205
|
+
await tick();
|
|
206
|
+
|
|
207
|
+
// Assert: the guess showed, then the record came back
|
|
208
|
+
expect(guessed).toBe(0);
|
|
209
|
+
expect(controller.getSnapshot().session!.annotations.map((entry) => entry.id)).toEqual(["a1"]);
|
|
210
|
+
expect(controller.getSnapshot().status).toBe("resolved");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("branch and label refuse an empty name and a taken branch name", async () => {
|
|
214
|
+
// Arrange
|
|
215
|
+
const { controller, client } = await connected();
|
|
216
|
+
|
|
217
|
+
// Act
|
|
218
|
+
controller.branch(" ");
|
|
219
|
+
controller.branch("main");
|
|
220
|
+
controller.labelTip("");
|
|
221
|
+
|
|
222
|
+
// Assert
|
|
223
|
+
expect(client.sessionBranch).not.toHaveBeenCalled();
|
|
224
|
+
expect(client.sessionLabel).not.toHaveBeenCalled();
|
|
225
|
+
expect(controller.getSnapshot().status).toBe("a checkpoint needs a name");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("a branch shows up on the tip and becomes the current branch", async () => {
|
|
229
|
+
// Arrange
|
|
230
|
+
const { controller, client } = await connected();
|
|
231
|
+
|
|
232
|
+
// Act
|
|
233
|
+
controller.branch("alt");
|
|
234
|
+
|
|
235
|
+
// Assert
|
|
236
|
+
expect(controller.getSnapshot().session!.history!.branch).toBe("alt");
|
|
237
|
+
expect(controller.treeRows().find((row) => row.isCurrentTip)?.tips).toEqual(["main", "alt"]);
|
|
238
|
+
expect(client.sessionBranch).toHaveBeenCalledWith("ses_1", "alt");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("fork opens the fork; fork-and-share shares the fork and stays on the original", async () => {
|
|
242
|
+
// Arrange
|
|
243
|
+
const forked = await connected();
|
|
244
|
+
const shared = await connected();
|
|
245
|
+
|
|
246
|
+
// Act
|
|
247
|
+
forked.controller.fork();
|
|
248
|
+
shared.controller.forkAndShare();
|
|
249
|
+
await tick();
|
|
250
|
+
await tick();
|
|
251
|
+
|
|
252
|
+
// Assert
|
|
253
|
+
expect(forked.controller.getSnapshot().session!.id).toBe("ses_1_fork");
|
|
254
|
+
expect(shared.controller.getSnapshot().session!.id).toBe("ses_1");
|
|
255
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({ id: "ses_1_fork" }));
|
|
256
|
+
expect(shared.client.sessionSetShares).toHaveBeenCalledWith("ses_1_fork", [
|
|
257
|
+
{ id: "p_ses_1_fork", requireAuth: false, allowlist: [], shareBranch: "main" },
|
|
258
|
+
]);
|
|
259
|
+
expect(shared.controller.getSnapshot().toast?.title).toBe("fork shared - link copied");
|
|
260
|
+
});
|
|
261
|
+
});
|