@cueloop/client 0.1.0-alpha.5 → 0.1.0-alpha.50

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