@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.
Files changed (130) hide show
  1. package/package.json +14 -13
  2. package/src/App.autoclose.test.tsx +3 -3
  3. package/src/App.diff.test.tsx +22 -5
  4. package/src/App.export.test.tsx +1 -2
  5. package/src/App.focus.test.ts +1 -59
  6. package/src/App.identity.test.tsx +3 -3
  7. package/src/App.plan-render.test.tsx +209 -0
  8. package/src/App.plan-review.test.tsx +91 -26
  9. package/src/App.readonly.test.tsx +6 -5
  10. package/src/App.share-dialog.test.tsx +172 -0
  11. package/src/App.submit-confirm.test.tsx +39 -8
  12. package/src/App.test.tsx +33 -30
  13. package/src/App.toast.test.tsx +11 -4
  14. package/src/App.tsx +263 -274
  15. package/src/App.walk.test.tsx +11 -10
  16. package/src/App.workbench.test.tsx +86 -0
  17. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  18. package/src/annotation-palette.ts +1 -0
  19. package/src/ansi-screen.test.ts +28 -0
  20. package/src/ansi-screen.ts +72 -0
  21. package/src/app-screens.tsx +19 -9
  22. package/src/app-view-model.ts +3 -1
  23. package/src/components/AnnotatableFileView.tsx +1 -1
  24. package/src/components/AnnotationCards.tsx +18 -2
  25. package/src/components/AnsiScreen.stories.tsx +12 -0
  26. package/src/components/AnsiScreen.tsx +28 -0
  27. package/src/components/CompletionOverlay.stories.tsx +15 -1
  28. package/src/components/CompletionOverlay.tsx +22 -5
  29. package/src/components/ConfirmCard.stories.tsx +2 -0
  30. package/src/components/ConfirmCard.tsx +45 -18
  31. package/src/components/ConfirmDialog.stories.tsx +3 -3
  32. package/src/components/ConfirmDialog.tsx +11 -12
  33. package/src/components/DiffContentView.tsx +18 -11
  34. package/src/components/DiscussionMarkerRail.tsx +9 -4
  35. package/src/components/FileContentsView.tsx +1 -1
  36. package/src/components/GridTabContent.tsx +8 -2
  37. package/src/components/HandleChipsInput.stories.tsx +24 -0
  38. package/src/components/HandleChipsInput.test.tsx +35 -0
  39. package/src/components/HandleChipsInput.tsx +145 -0
  40. package/src/components/MarkdownGridBlock.stories.tsx +33 -0
  41. package/src/components/MarkdownGridBlock.tsx +72 -0
  42. package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
  43. package/src/components/MarkdownThreadEditor.test.tsx +98 -0
  44. package/src/components/MarkdownThreadEditor.tsx +162 -0
  45. package/src/components/NavModeHint.stories.tsx +40 -0
  46. package/src/components/NavModeHint.tsx +31 -0
  47. package/src/components/PromptDialog.test.tsx +2 -2
  48. package/src/components/PromptDialog.tsx +9 -10
  49. package/src/components/SettingsDialog.tsx +1 -0
  50. package/src/components/ShareDialog.stories.tsx +121 -0
  51. package/src/components/ShareDialog.tsx +552 -0
  52. package/src/components/SlashComposer.stories.tsx +37 -0
  53. package/src/components/SlashComposer.tsx +87 -0
  54. package/src/components/ThreadFooter.tsx +4 -1
  55. package/src/components/ThreadTree.tsx +12 -7
  56. package/src/components/ThreadView.selection.test.tsx +88 -1
  57. package/src/components/ThreadView.slash.test.tsx +0 -36
  58. package/src/components/ThreadView.tsx +200 -67
  59. package/src/components/Toast.stories.tsx +7 -0
  60. package/src/components/Toast.tsx +15 -8
  61. package/src/components/WalkWizard.stories.tsx +2 -2
  62. package/src/components/WalkWizard.tsx +10 -12
  63. package/src/components/__snapshots__/stories.test.tsx.snap +436 -182
  64. package/src/components/primitives/Button.tsx +6 -2
  65. package/src/components/primitives/Card.tsx +5 -2
  66. package/src/components/primitives/Dialog.tsx +2 -2
  67. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  68. package/src/components/primitives/DialogActions.tsx +39 -0
  69. package/src/components/primitives/Switch.stories.tsx +17 -0
  70. package/src/components/primitives/Switch.tsx +32 -0
  71. package/src/components/primitives/Tree.tsx +5 -1
  72. package/src/components/primitives/frame.ts +5 -0
  73. package/src/components/session-tree.ts +7 -6
  74. package/src/components/share-dialog-store.ts +58 -0
  75. package/src/components/stories-app.tsx +13 -4
  76. package/src/components/stories.test.tsx +12 -4
  77. package/src/components/story-tree.test.ts +32 -24
  78. package/src/components/story.ts +32 -15
  79. package/src/config.test.ts +18 -0
  80. package/src/config.ts +8 -1
  81. package/src/diff-fold.ts +1 -1
  82. package/src/diff-intraline.test.ts +75 -2
  83. package/src/diff-intraline.ts +95 -26
  84. package/src/intent-dispatch.test.ts +12 -3
  85. package/src/intent-dispatch.ts +10 -25
  86. package/src/key-bindings.ts +0 -6
  87. package/src/keymap.test.ts +2 -2
  88. package/src/keymap.ts +0 -2
  89. package/src/markdown-editor-syntax-style.ts +50 -0
  90. package/src/markdown-grid.test.ts +101 -0
  91. package/src/markdown-grid.ts +203 -0
  92. package/src/markdown-highlight.test.ts +61 -0
  93. package/src/markdown-highlight.ts +122 -0
  94. package/src/pasted-image.test.ts +67 -0
  95. package/src/pasted-image.ts +60 -0
  96. package/src/prototype-browser.test.ts +9 -1
  97. package/src/prototype-browser.ts +22 -0
  98. package/src/run.ts +39 -25
  99. package/src/serve.ts +1 -1
  100. package/src/share.ts +16 -2
  101. package/src/slash-palette.test.ts +17 -0
  102. package/src/slash-palette.ts +11 -0
  103. package/src/test-support.ts +14 -0
  104. package/src/thread-chords.test.ts +55 -178
  105. package/src/thread-chords.ts +78 -169
  106. package/src/thread-controller.curate.test.ts +1 -0
  107. package/src/thread-controller.derivation.test.ts +62 -0
  108. package/src/thread-controller.live-diff.test.ts +3 -0
  109. package/src/thread-controller.optimistic.test.ts +69 -0
  110. package/src/thread-controller.share.test.ts +60 -5
  111. package/src/thread-controller.switch-cache.test.ts +176 -0
  112. package/src/thread-controller.tree.test.ts +6 -1
  113. package/src/thread-controller.ts +386 -78
  114. package/src/use-annotation-surface.tsx +88 -63
  115. package/src/use-settings-dialog.tsx +14 -3
  116. package/src/use-thread-body-editing.tsx +58 -0
  117. package/src/view-plan.test.ts +56 -6
  118. package/src/view-plan.ts +154 -5
  119. package/src/App.share-choice.test.tsx +0 -105
  120. package/src/components/ManageAccessDialog.stories.tsx +0 -36
  121. package/src/components/ManageAccessDialog.test.tsx +0 -94
  122. package/src/components/ManageAccessDialog.tsx +0 -120
  123. package/src/components/ShareChoiceDialog.stories.tsx +0 -34
  124. package/src/components/ShareChoiceDialog.test.tsx +0 -103
  125. package/src/components/ShareChoiceDialog.tsx +0 -82
  126. package/src/components/ThreadsSidebar.stories.tsx +0 -36
  127. package/src/components/ThreadsSidebar.tsx +0 -71
  128. package/src/components/agent-launcher.stories.tsx +0 -26
  129. package/src/components/agent-launcher.test.tsx +0 -50
  130. 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.sessionSetShareId).toHaveBeenCalledWith("ses_1_fork", "p_ses_1_fork");
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
  });