@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.79
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/package.json +14 -13
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +22 -5
- package/src/App.export.test.tsx +1 -2
- package/src/App.focus.test.ts +1 -59
- package/src/App.identity.test.tsx +3 -3
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +91 -26
- package/src/App.readonly.test.tsx +6 -5
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +39 -8
- package/src/App.test.tsx +33 -30
- package/src/App.toast.test.tsx +11 -4
- package/src/App.tsx +263 -274
- package/src/App.walk.test.tsx +11 -10
- package/src/App.workbench.test.tsx +86 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +1 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +19 -9
- package/src/app-view-model.ts +3 -1
- package/src/components/AnnotatableFileView.tsx +1 -1
- package/src/components/AnnotationCards.tsx +18 -2
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.stories.tsx +3 -3
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +18 -11
- package/src/components/DiscussionMarkerRail.tsx +9 -4
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +8 -2
- 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/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/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/SettingsDialog.tsx +1 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.tsx +4 -1
- package/src/components/ThreadTree.tsx +12 -7
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -36
- package/src/components/ThreadView.tsx +200 -67
- package/src/components/Toast.stories.tsx +7 -0
- package/src/components/Toast.tsx +15 -8
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
- package/src/components/primitives/Button.tsx +6 -2
- package/src/components/primitives/Card.tsx +5 -2
- package/src/components/primitives/Dialog.tsx +2 -2
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tree.tsx +5 -1
- package/src/components/primitives/frame.ts +5 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +12 -4
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config.test.ts +18 -0
- package/src/config.ts +8 -1
- package/src/diff-fold.ts +1 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/intent-dispatch.test.ts +12 -3
- package/src/intent-dispatch.ts +10 -25
- package/src/key-bindings.ts +0 -6
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +0 -2
- 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/prototype-browser.test.ts +9 -1
- package/src/prototype-browser.ts +22 -0
- package/src/run.ts +39 -25
- package/src/serve.ts +1 -1
- package/src/share.ts +16 -2
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/test-support.ts +14 -0
- package/src/thread-chords.test.ts +55 -178
- package/src/thread-chords.ts +78 -169
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +3 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +60 -5
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +6 -1
- package/src/thread-controller.ts +386 -78
- package/src/use-annotation-surface.tsx +88 -63
- package/src/use-settings-dialog.tsx +14 -3
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/view-plan.test.ts +56 -6
- package/src/view-plan.ts +154 -5
- package/src/App.share-choice.test.tsx +0 -105
- package/src/components/ManageAccessDialog.stories.tsx +0 -36
- package/src/components/ManageAccessDialog.test.tsx +0 -94
- package/src/components/ManageAccessDialog.tsx +0 -120
- package/src/components/ShareChoiceDialog.stories.tsx +0 -34
- package/src/components/ShareChoiceDialog.test.tsx +0 -103
- package/src/components/ShareChoiceDialog.tsx +0 -82
- package/src/components/ThreadsSidebar.stories.tsx +0 -36
- package/src/components/ThreadsSidebar.tsx +0 -71
- package/src/components/agent-launcher.stories.tsx +0 -26
- package/src/components/agent-launcher.test.tsx +0 -50
- package/src/components/agent-launcher.tsx +0 -149
|
@@ -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 { SessionClient } 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
|
+
verdict: 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[]): SessionClient {
|
|
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
|
+
sessionResolve: unimplemented("sessionResolve"),
|
|
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().status).toContain("change rejected");
|
|
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
|
+
});
|
|
@@ -58,7 +58,9 @@ const shareTransport: ShareTransport = {
|
|
|
58
58
|
pull: mock(async () => sessionFixture()),
|
|
59
59
|
push: mock(async () => {}),
|
|
60
60
|
watch: () => () => {},
|
|
61
|
+
revoke: async () => {},
|
|
61
62
|
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
63
|
+
formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
|
|
62
64
|
collaboratorAnnotations: (session) => session.annotations.filter((entry) => entry.author),
|
|
63
65
|
mergeFromShare,
|
|
64
66
|
};
|
|
@@ -95,6 +97,7 @@ function fakeClient(session: Thread) {
|
|
|
95
97
|
sessionSetViewed: unimplemented("sessionSetViewed"),
|
|
96
98
|
sessionSetTitle: unimplemented("sessionSetTitle"),
|
|
97
99
|
sessionSetShareId: mock(async (_id: string, _shareId: string) => session),
|
|
100
|
+
sessionSetShares: mock(async () => session),
|
|
98
101
|
sessionMergeShared: unimplemented("sessionMergeShared"),
|
|
99
102
|
sessionDelete: unimplemented("sessionDelete"),
|
|
100
103
|
sessionSetSelfName: unimplemented("sessionSetSelfName"),
|
|
@@ -250,7 +253,9 @@ describe("tree primitives", () => {
|
|
|
250
253
|
expect(forked.controller.getSnapshot().session!.id).toBe("ses_1_fork");
|
|
251
254
|
expect(shared.controller.getSnapshot().session!.id).toBe("ses_1");
|
|
252
255
|
expect(publish).toHaveBeenCalledWith(expect.objectContaining({ id: "ses_1_fork" }));
|
|
253
|
-
expect(shared.client.
|
|
256
|
+
expect(shared.client.sessionSetShares).toHaveBeenCalledWith("ses_1_fork", [
|
|
257
|
+
{ id: "p_ses_1_fork", requireAuth: false, allowlist: [], shareBranch: "main" },
|
|
258
|
+
]);
|
|
254
259
|
expect(shared.controller.getSnapshot().toast?.title).toBe("fork shared - link copied");
|
|
255
260
|
});
|
|
256
261
|
});
|