@cueloop/client 0.1.0-alpha.3 → 0.1.0-alpha.31

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