@cueloop/client 0.1.0-alpha.6 → 0.1.0-alpha.61

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 (161) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +37 -33
  8. package/src/App.appearance.test.tsx +51 -0
  9. package/src/App.autoclose.test.tsx +73 -47
  10. package/src/App.diff.test.tsx +169 -22
  11. package/src/App.export.test.tsx +49 -37
  12. package/src/App.plan-review.test.tsx +714 -0
  13. package/src/App.prototype.test.tsx +194 -0
  14. package/src/App.readonly.test.tsx +69 -35
  15. package/src/App.rendering.test.tsx +59 -31
  16. package/src/App.review-panel.test.tsx +143 -0
  17. package/src/App.submit-confirm.test.tsx +229 -0
  18. package/src/App.test.tsx +139 -68
  19. package/src/App.toast.test.tsx +77 -0
  20. package/src/App.tsx +0 -0
  21. package/src/App.walk.test.tsx +299 -0
  22. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  23. package/src/app-key-state.ts +61 -0
  24. package/src/app-screens.tsx +270 -0
  25. package/src/app-view-model.ts +307 -0
  26. package/src/attribution.test.ts +31 -0
  27. package/src/attribution.ts +42 -0
  28. package/src/clipboard.test.ts +23 -0
  29. package/src/clipboard.ts +33 -0
  30. package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
  31. package/src/components/AnnotationCard.stories.tsx +94 -0
  32. package/src/components/AnnotationCard.tsx +250 -0
  33. package/src/components/Breadcrumb.stories.tsx +19 -0
  34. package/src/components/Breadcrumb.tsx +45 -0
  35. package/src/components/CodeBlock.stories.tsx +42 -0
  36. package/src/components/CodeBlock.tsx +84 -0
  37. package/src/components/CompletionOverlay.stories.tsx +23 -0
  38. package/src/components/CompletionOverlay.tsx +60 -0
  39. package/src/components/ConfirmCard.stories.tsx +35 -0
  40. package/src/components/ConfirmCard.tsx +129 -0
  41. package/src/components/ConfirmDialog.stories.tsx +21 -0
  42. package/src/components/ConfirmDialog.tsx +66 -0
  43. package/src/components/DiffSheet.scroll.test.tsx +135 -0
  44. package/src/components/DiffSheet.stories.tsx +78 -0
  45. package/src/components/DiffSheet.syntax.test.tsx +65 -0
  46. package/src/components/DiffSheet.tsx +374 -0
  47. package/src/components/InboxList.stories.tsx +31 -0
  48. package/src/components/InboxList.tsx +64 -0
  49. package/src/components/KeybindsDialog.stories.tsx +31 -0
  50. package/src/components/KeybindsDialog.tsx +59 -0
  51. package/src/components/MarkerPopover.stories.tsx +58 -0
  52. package/src/components/MarkerPopover.tsx +109 -0
  53. package/src/components/MenuBar.stories.tsx +37 -0
  54. package/src/components/MenuBar.tsx +75 -0
  55. package/src/components/PlanSheet.stories.tsx +113 -0
  56. package/src/components/PlanSheet.tsx +422 -0
  57. package/src/components/PromptDialog.stories.tsx +36 -0
  58. package/src/components/PromptDialog.tsx +80 -0
  59. package/src/components/PrototypeSheet.stories.tsx +20 -0
  60. package/src/components/PrototypeSheet.tsx +450 -0
  61. package/src/components/ReviewPanel.stories.tsx +86 -0
  62. package/src/components/ReviewPanel.tsx +103 -0
  63. package/src/components/ReviewRail.stories.tsx +218 -0
  64. package/src/components/ReviewRail.tsx +318 -0
  65. package/src/components/SettingsDialog.stories.tsx +111 -0
  66. package/src/components/SettingsDialog.tsx +147 -0
  67. package/src/components/SettingsRows.stories.tsx +18 -0
  68. package/src/components/SettingsRows.tsx +112 -0
  69. package/src/components/Toast.stories.tsx +12 -0
  70. package/src/components/Toast.tsx +45 -0
  71. package/src/components/WalkWizard.stories.tsx +100 -0
  72. package/src/components/WalkWizard.tsx +152 -0
  73. package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
  74. package/src/components/agent-launcher.stories.tsx +26 -0
  75. package/src/components/agent-launcher.test.tsx +50 -0
  76. package/src/components/agent-launcher.tsx +149 -0
  77. package/src/components/diff-sheet-layout.test.ts +128 -0
  78. package/src/components/diff-sheet-layout.ts +184 -0
  79. package/src/components/plan-sheet-run-style.ts +90 -0
  80. package/src/components/primitives/Button.stories.tsx +32 -0
  81. package/src/components/primitives/Button.tsx +52 -0
  82. package/src/components/primitives/Card.stories.tsx +25 -0
  83. package/src/components/primitives/Card.tsx +60 -0
  84. package/src/components/primitives/Dialog.stories.tsx +24 -0
  85. package/src/components/primitives/Dialog.tsx +66 -0
  86. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  87. package/src/components/primitives/StatusBar.tsx +23 -0
  88. package/src/components/primitives/Tabs.stories.tsx +29 -0
  89. package/src/components/primitives/Tabs.tsx +102 -0
  90. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  91. package/src/components/primitives/Toolbar.tsx +14 -0
  92. package/src/components/primitives/frame.test.ts +12 -0
  93. package/src/components/primitives/frame.ts +14 -0
  94. package/src/components/quick-actions-editor.stories.tsx +54 -0
  95. package/src/components/quick-actions-editor.tsx +138 -0
  96. package/src/components/stories-app.tsx +124 -0
  97. package/src/components/stories.test.tsx +66 -0
  98. package/src/components/story-fixtures.ts +98 -0
  99. package/src/components/story.ts +75 -0
  100. package/src/components/syntax-highlight.ts +102 -0
  101. package/src/components/terminal-pane.test.tsx +40 -0
  102. package/src/components/terminal-pane.ts +193 -0
  103. package/src/components/theme-context.stories.tsx +46 -0
  104. package/src/components/theme-context.tsx +37 -0
  105. package/src/components/truncate-text.ts +7 -0
  106. package/src/config.test.ts +448 -31
  107. package/src/config.ts +339 -57
  108. package/src/diff-hunk-curate.test.ts +315 -0
  109. package/src/diff-hunk-curate.ts +253 -0
  110. package/src/diff-intraline.test.ts +165 -0
  111. package/src/diff-intraline.ts +146 -0
  112. package/src/diff-syntax.test.ts +96 -0
  113. package/src/diff-syntax.ts +176 -0
  114. package/src/editor.test.ts +149 -0
  115. package/src/editor.ts +135 -9
  116. package/src/ghostty-terminal.test.ts +84 -0
  117. package/src/ghostty-terminal.ts +176 -0
  118. package/src/herdr.test.ts +9 -34
  119. package/src/herdr.ts +17 -21
  120. package/src/index.ts +10 -0
  121. package/src/integrations.test.ts +41 -0
  122. package/src/integrations.ts +32 -0
  123. package/src/intent-dispatch.test.ts +499 -0
  124. package/src/intent-dispatch.ts +585 -0
  125. package/src/key-bindings.test.ts +156 -0
  126. package/src/key-bindings.ts +452 -0
  127. package/src/keymap.test.ts +635 -0
  128. package/src/keymap.ts +404 -0
  129. package/src/kitty-image.test.ts +66 -0
  130. package/src/kitty-image.ts +138 -0
  131. package/src/prototype-browser.test.ts +56 -0
  132. package/src/prototype-browser.ts +289 -0
  133. package/src/pty.test.ts +81 -0
  134. package/src/pty.ts +237 -0
  135. package/src/review-panel.test.ts +113 -0
  136. package/src/review-panel.ts +83 -0
  137. package/src/run.ts +16 -5
  138. package/src/serve.test.ts +25 -9
  139. package/src/serve.ts +19 -8
  140. package/src/session-controller.curate.test.ts +260 -0
  141. package/src/session-controller.share.test.ts +320 -0
  142. package/src/session-controller.ts +993 -0
  143. package/src/share.ts +120 -0
  144. package/src/test-support.test.ts +60 -0
  145. package/src/test-support.ts +197 -0
  146. package/src/theme-presets.test.ts +51 -0
  147. package/src/theme-presets.ts +187 -0
  148. package/src/theme.ts +64 -17
  149. package/src/use-settings-dialog.tsx +275 -0
  150. package/src/version.ts +5 -0
  151. package/src/view-diff.test.ts +22 -8
  152. package/src/view-diff.ts +39 -23
  153. package/src/view-plan.regression.test.ts +60 -0
  154. package/src/view-plan.test.ts +406 -0
  155. package/src/view-plan.ts +554 -0
  156. package/src/walk.test.ts +121 -0
  157. package/src/walk.ts +74 -0
  158. package/src/syntax.test.ts +0 -41
  159. package/src/syntax.ts +0 -88
  160. package/src/view.test.ts +0 -157
  161. package/src/view.ts +0 -0
@@ -0,0 +1,320 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import { ManualClock } from "@opentui/core/testing";
3
+ import { SCHEMA_VERSION, type Annotation, type ReviewSession } from "@cueloop/schema";
4
+ import type { SessionClient } from "@cueloop/daemon/client";
5
+
6
+ // Stub the ssh transport so no subprocess spawns; the controller under test
7
+ // imports these from "./share".
8
+ const publishShare = mock(async () => ({ line: "ssh p_abc123xy@cueloop.dev", copied: true }));
9
+ let remote: ReviewSession;
10
+ const pullShare = mock(async () => remote);
11
+ const pushShare = mock(
12
+ async (_shareId: string, _annotations: Array<Omit<Annotation, "createdAt">>) => {},
13
+ );
14
+
15
+ mock.module("./share", () => ({
16
+ publishShare,
17
+ pullShare,
18
+ pushShare,
19
+ shareIdFromLine: (line: string) => line.match(/^ssh (\S+)@/)?.[1],
20
+ collaboratorAnnotations: (session: ReviewSession) =>
21
+ session.annotations.filter((annotation) => annotation.author),
22
+ }));
23
+
24
+ const { createReviewController, SHARE_POLL_MS } = await import("./session-controller");
25
+
26
+ function sessionFixture(overrides: Partial<ReviewSession> = {}): ReviewSession {
27
+ return {
28
+ schemaVersion: SCHEMA_VERSION,
29
+ id: "ses_1",
30
+ workspace: { repoRoot: "/repo", branch: "main" },
31
+ artifact: { type: "plan", content: "# Plan\n", meta: {} },
32
+ revisions: [{ revision: 1, content: "# Plan\n", submittedAt: "2026-01-01T00:00:00.000Z" }],
33
+ annotations: [],
34
+ verdict: null,
35
+ status: "pending",
36
+ createdAt: "2026-01-01T00:00:00.000Z",
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ function annotation(id: string, author: string): Annotation {
42
+ return {
43
+ id,
44
+ kind: "comment",
45
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
46
+ body: "note",
47
+ author,
48
+ createdAt: "2026-01-01T00:00:00.000Z",
49
+ };
50
+ }
51
+
52
+ function fakeClient(session: ReviewSession): SessionClient {
53
+ return {
54
+ onEvent: () => () => {},
55
+ subscribe: async () => {},
56
+ sessionGet: async () => session,
57
+ sessionList: async () => [session],
58
+ sessionAnnotate: mock(async () => session),
59
+ sessionSetShareId: mock(
60
+ async (_id: string, shareId: string) => ((session.shareId = shareId), session),
61
+ ),
62
+ sessionMergeShared: mock(async (_id: string, incoming: { annotations: Annotation[] }) => {
63
+ const known = new Set(session.annotations.map((existing) => existing.id));
64
+
65
+ for (const note of incoming.annotations)
66
+ if (!known.has(note.id)) session.annotations.push(note);
67
+
68
+ return session;
69
+ }),
70
+ close: () => {},
71
+ } as unknown as SessionClient;
72
+ }
73
+
74
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
75
+
76
+ async function connectedController(
77
+ session: ReviewSession,
78
+ clock?: ManualClock,
79
+ ): Promise<{ controller: ReturnType<typeof createReviewController>; client: SessionClient }> {
80
+ const client = fakeClient(session);
81
+ const controller = createReviewController({
82
+ sessionId: session.id,
83
+ openClient: async () => client,
84
+ clock,
85
+ });
86
+
87
+ controller.connect();
88
+ await tick();
89
+
90
+ return { controller, client };
91
+ }
92
+
93
+ describe("share", () => {
94
+ test("stamps the returned share id back on the session", async () => {
95
+ // Arrange
96
+ const { controller, client } = await connectedController(sessionFixture());
97
+
98
+ // Act
99
+ controller.share();
100
+ await tick();
101
+
102
+ // Assert
103
+ expect(client.sessionSetShareId).toHaveBeenCalledWith("ses_1", "p_abc123xy");
104
+ });
105
+
106
+ test("surfaces the ssh line as a toast, not an inline status", async () => {
107
+ // Arrange
108
+ const { controller } = await connectedController(sessionFixture());
109
+
110
+ // Act
111
+ controller.share();
112
+ await tick();
113
+
114
+ // Assert
115
+ expect(controller.getSnapshot().toast).toEqual({
116
+ body: "ssh p_abc123xy@cueloop.dev",
117
+ title: "share link copied",
118
+ });
119
+ expect(controller.getSnapshot().status).not.toContain("ssh p_abc123xy@cueloop.dev");
120
+ });
121
+ });
122
+
123
+ describe("pullShared", () => {
124
+ test("unions collaborator notes and identities in when the plan was shared", async () => {
125
+ // Arrange
126
+ remote = sessionFixture({
127
+ annotations: [annotation("a1", "SHA256:mate")],
128
+ participants: [{ id: "SHA256:mate", provider: "ssh", name: "Sam" }],
129
+ });
130
+ const { controller, client } = await connectedController(
131
+ sessionFixture({ shareId: "p_abc123xy" }),
132
+ );
133
+
134
+ // Act
135
+ controller.pullShared();
136
+ await tick();
137
+
138
+ // Assert
139
+ expect(pullShare).toHaveBeenCalledWith("p_abc123xy");
140
+ expect(client.sessionMergeShared).toHaveBeenCalledWith("ses_1", {
141
+ annotations: remote.annotations,
142
+ participants: remote.participants,
143
+ });
144
+ });
145
+
146
+ test("does nothing for a plan that was never shared", async () => {
147
+ // Arrange
148
+ pullShare.mockClear();
149
+ const { controller, client } = await connectedController(sessionFixture());
150
+
151
+ // Act
152
+ controller.pullShared();
153
+ await tick();
154
+
155
+ // Assert
156
+ expect(pullShare).not.toHaveBeenCalled();
157
+ expect(client.sessionMergeShared).not.toHaveBeenCalled();
158
+ });
159
+ });
160
+
161
+ describe("mirror on annotate", () => {
162
+ test("pushes an edited note up when the plan is shared", async () => {
163
+ // Arrange
164
+ pushShare.mockClear();
165
+ const { controller } = await connectedController(
166
+ sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:me")] }),
167
+ );
168
+
169
+ // Act
170
+ controller.updateAnnotation("a1", "revised body");
171
+ await tick();
172
+
173
+ // Assert
174
+ expect(pushShare).toHaveBeenCalledTimes(1);
175
+ expect(pushShare.mock.calls[0]?.[0]).toBe("p_abc123xy");
176
+ expect(pushShare.mock.calls[0]?.[1]).toEqual([
177
+ {
178
+ id: "a1",
179
+ kind: "comment",
180
+ anchor: { quote: "Plan", prefix: "", suffix: "" },
181
+ body: "revised body",
182
+ },
183
+ ]);
184
+ });
185
+
186
+ test("does not push when the plan was never shared", async () => {
187
+ // Arrange
188
+ pushShare.mockClear();
189
+ const { controller } = await connectedController(
190
+ sessionFixture({ annotations: [annotation("a1", "SHA256:me")] }),
191
+ );
192
+
193
+ // Act
194
+ controller.updateAnnotation("a1", "revised body");
195
+ await tick();
196
+
197
+ // Assert
198
+ expect(pushShare).not.toHaveBeenCalled();
199
+ });
200
+
201
+ test("does not push when the local write is rejected", async () => {
202
+ // Arrange - the daemon write fails (e.g. a resolved session)
203
+ pushShare.mockClear();
204
+ const { controller, client } = await connectedController(
205
+ sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:me")] }),
206
+ );
207
+
208
+ (client.sessionAnnotate as ReturnType<typeof mock>).mockImplementationOnce(async () => {
209
+ throw new Error("session is resolved");
210
+ });
211
+
212
+ // Act
213
+ controller.updateAnnotation("a1", "revised body");
214
+ await tick();
215
+
216
+ // Assert - a failed local write must never leak to the share
217
+ expect(pushShare).not.toHaveBeenCalled();
218
+ });
219
+ });
220
+
221
+ describe("startSharePoll", () => {
222
+ test("pulls now and again each interval until stopped", async () => {
223
+ // Arrange
224
+ pullShare.mockClear();
225
+ remote = sessionFixture({ annotations: [] });
226
+ const clock = new ManualClock();
227
+ const { controller } = await connectedController(
228
+ sessionFixture({ shareId: "p_abc123xy" }),
229
+ clock,
230
+ );
231
+
232
+ // Act - immediate pull
233
+ const stop = controller.startSharePoll();
234
+
235
+ await tick();
236
+
237
+ // Assert
238
+ expect(pullShare).toHaveBeenCalledTimes(1);
239
+
240
+ // Act - one interval later, it pulls again
241
+ clock.advance(SHARE_POLL_MS);
242
+ await tick();
243
+
244
+ // Assert
245
+ expect(pullShare).toHaveBeenCalledTimes(2);
246
+
247
+ // Act - stop, then advance: no further pulls
248
+ stop();
249
+ clock.advance(SHARE_POLL_MS * 3);
250
+ await tick();
251
+
252
+ // Assert
253
+ expect(pullShare).toHaveBeenCalledTimes(2);
254
+ });
255
+
256
+ test("a stop during an in-flight pull leaves no zombie timer", async () => {
257
+ // Arrange - the first pull hangs until released
258
+ pullShare.mockClear();
259
+ remote = sessionFixture({ annotations: [] });
260
+ let release: () => void = () => {};
261
+
262
+ pullShare.mockImplementationOnce(
263
+ () => new Promise<ReviewSession>((resolve) => (release = () => resolve(remote))),
264
+ );
265
+ const clock = new ManualClock();
266
+ const { controller } = await connectedController(
267
+ sessionFixture({ shareId: "p_abc123xy" }),
268
+ clock,
269
+ );
270
+
271
+ // Act - start (pull is in flight), leave, then let the pull settle
272
+ const stop = controller.startSharePoll();
273
+
274
+ await tick();
275
+ expect(pullShare).toHaveBeenCalledTimes(1);
276
+ stop();
277
+ release();
278
+ await tick();
279
+ clock.advance(SHARE_POLL_MS * 3);
280
+ await tick();
281
+
282
+ // Assert - the settled pull's finally must not re-arm the timer
283
+ expect(pullShare).toHaveBeenCalledTimes(1);
284
+ });
285
+
286
+ test("a restart during an in-flight pull does not double the poll", async () => {
287
+ // Arrange - run A's first pull hangs; later pulls resolve immediately
288
+ pullShare.mockClear();
289
+ remote = sessionFixture({ annotations: [] });
290
+ let release: () => void = () => {};
291
+
292
+ pullShare.mockImplementationOnce(
293
+ () => new Promise<ReviewSession>((resolve) => (release = () => resolve(remote))),
294
+ );
295
+ const clock = new ManualClock();
296
+ const { controller } = await connectedController(
297
+ sessionFixture({ shareId: "p_abc123xy" }),
298
+ clock,
299
+ );
300
+
301
+ // Act - run A starts (pull hangs), then run B restarts while A is in flight
302
+ controller.startSharePoll();
303
+ await tick();
304
+ expect(pullShare).toHaveBeenCalledTimes(1);
305
+ const stopB = controller.startSharePoll();
306
+
307
+ await tick();
308
+ expect(pullShare).toHaveBeenCalledTimes(2);
309
+
310
+ // A's stale pull settles - it must not re-arm; then one interval fires only run B
311
+ release();
312
+ await tick();
313
+ clock.advance(SHARE_POLL_MS);
314
+ await tick();
315
+
316
+ // Assert - exactly one more pull (run B), not two
317
+ expect(pullShare).toHaveBeenCalledTimes(3);
318
+ stopB();
319
+ });
320
+ });