@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,493 @@
1
+ /**
2
+ * The intent reducer in isolation: with the App component's dependencies
3
+ * mocked, each Intent maps to exactly the controller call or state update it
4
+ * should - the navigation clamps, the annotation wrap-around, the verdict
5
+ * cycle, and the submit-reveal fix, none of which needed a rendered TUI.
6
+ */
7
+
8
+ import { describe, expect, mock, test } from "bun:test";
9
+ import type { ReviewSession } from "@cueloop/schema";
10
+ import { createIntentDispatch, type IntentDispatchDeps } from "./intent-dispatch";
11
+ import type { DisplayBlock } from "./view-plan";
12
+
13
+ function block(text: string): DisplayBlock {
14
+ return { text, kind: "text", work: true } as unknown as DisplayBlock;
15
+ }
16
+
17
+ function sessionWith(annotationIds: string[]): ReviewSession {
18
+ return {
19
+ annotations: annotationIds.map((id) => ({ id })),
20
+ workingCopy: undefined,
21
+ } as unknown as ReviewSession;
22
+ }
23
+
24
+ /** A controller where every verb is a mock; annotate returns undefined by default. */
25
+ function baseController() {
26
+ return {
27
+ setStatus: mock(),
28
+ open: mock(),
29
+ deleteSession: mock(),
30
+ setSelfName: mock(),
31
+ cut: mock(),
32
+ curationItems: mock(() => []),
33
+ restoreCuration: mock(),
34
+ annotate: mock(),
35
+ annotatePrototype: mock(),
36
+ updateAnnotation: mock(),
37
+ removeAnnotation: mock(),
38
+ walkStart: mock(),
39
+ walkForward: mock(),
40
+ walkBack: mock(),
41
+ walkLeave: mock(),
42
+ submit: mock(),
43
+ share: mock(),
44
+ finishReview: mock(),
45
+ optInAutoClose: mock(),
46
+ dismissCompletion: mock(),
47
+ saveReviewPanel: mock(),
48
+ };
49
+ }
50
+
51
+ /** A deps bag where every effect is a mock and every read has a plain default. */
52
+ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDeps {
53
+ const controller = baseController();
54
+ return {
55
+ controller: controller as unknown as IntentDispatchDeps["controller"],
56
+ onExit: mock(),
57
+ isDiff: false,
58
+ display: [],
59
+ rows: [],
60
+ cursor: 0,
61
+ inbox: null,
62
+ inboxCursor: 0,
63
+ mode: { type: "normal" },
64
+ session: null,
65
+ reviewMode: "expanded",
66
+ reviewWidth: 34,
67
+ terminalWidth: 120,
68
+ focusedAnnotationId: undefined,
69
+ selectedCurationId: undefined,
70
+ authorNames: {},
71
+ quickActions: [],
72
+ renameAuthor: mock(),
73
+ liveInput: { current: "" },
74
+ reviewWidthRef: { current: 34 },
75
+ planSheetRef: { current: null },
76
+ setCursor: mock(),
77
+ setInboxCursor: mock(),
78
+ setMode: mock(),
79
+ setReviewMode: mock(),
80
+ setReviewWidth: mock(),
81
+ setRailTab: mock(),
82
+ setFocusedAnnotationId: mock(),
83
+ setSelectedCurationId: mock(),
84
+ setPulsedAnnotationId: mock(),
85
+ selectCardFromDocument: mock(),
86
+ runEditorHandOff: mock(),
87
+ openCardEdit: mock(),
88
+ ...overrides,
89
+ };
90
+ }
91
+
92
+ describe("move", () => {
93
+ test("down clamps at the last row and up clamps at the first", () => {
94
+ // Arrange
95
+ const deps = makeDeps({ display: [block("a"), block("b"), block("c")] });
96
+ const dispatch = createIntentDispatch(deps);
97
+
98
+ // Act
99
+ dispatch({ type: "move", to: "down" });
100
+
101
+ // Assert
102
+ const advance = (deps.setCursor as ReturnType<typeof mock>).mock.calls[0]![0] as (
103
+ current: number,
104
+ ) => number;
105
+ expect(advance(2)).toBe(2); // already at the end, stays
106
+ expect(advance(0)).toBe(1);
107
+ });
108
+
109
+ test("bottom jumps to the last navigable row", () => {
110
+ // Arrange
111
+ const deps = makeDeps({ display: [block("a"), block("b"), block("c")] });
112
+ const dispatch = createIntentDispatch(deps);
113
+
114
+ // Act
115
+ dispatch({ type: "move", to: "bottom" });
116
+
117
+ // Assert
118
+ expect(deps.setCursor).toHaveBeenCalledWith(2);
119
+ });
120
+ });
121
+
122
+ describe("annotation navigation", () => {
123
+ test("nextAnnotation wraps from the last annotation back to the first", () => {
124
+ // Arrange
125
+ const deps = makeDeps({ session: sessionWith(["a1", "a2", "a3"]), focusedAnnotationId: "a3" });
126
+ const dispatch = createIntentDispatch(deps);
127
+
128
+ // Act
129
+ dispatch({ type: "nextAnnotation" });
130
+
131
+ // Assert
132
+ expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a1");
133
+ });
134
+
135
+ test("prevAnnotation wraps from the first annotation to the last", () => {
136
+ // Arrange
137
+ const deps = makeDeps({ session: sessionWith(["a1", "a2", "a3"]), focusedAnnotationId: "a1" });
138
+ const dispatch = createIntentDispatch(deps);
139
+
140
+ // Act
141
+ dispatch({ type: "prevAnnotation" });
142
+
143
+ // Assert
144
+ expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a3");
145
+ });
146
+
147
+ test("cycling skips annotations a revision already addressed", () => {
148
+ // Arrange: a2 is addressed, so next from a1 lands on a3
149
+ const session = sessionWith(["a1", "a2", "a3"]);
150
+ (session.annotations[1] as { resolution?: object }).resolution = {
151
+ revision: 2,
152
+ source: "agent",
153
+ };
154
+ const deps = makeDeps({ session, focusedAnnotationId: "a1" });
155
+ const dispatch = createIntentDispatch(deps);
156
+
157
+ // Act
158
+ dispatch({ type: "nextAnnotation" });
159
+
160
+ // Assert
161
+ expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a3");
162
+ });
163
+ });
164
+
165
+ describe("cycleVerdict", () => {
166
+ test("advances through the verdict list in the submit overlay", () => {
167
+ // Arrange
168
+ const deps = makeDeps({ mode: { type: "submit", verdict: "approve", summary: "" } });
169
+ const dispatch = createIntentDispatch(deps);
170
+
171
+ // Act
172
+ dispatch({ type: "cycleVerdict", direction: 1 });
173
+
174
+ // Assert
175
+ expect(deps.setMode).toHaveBeenCalledWith({
176
+ type: "submit",
177
+ verdict: "request_changes",
178
+ summary: "",
179
+ });
180
+ });
181
+ });
182
+
183
+ describe("marker-actions popover", () => {
184
+ const span = { displayIndex: 3, wordIndex: 0, wordEnd: 0, start: 2, end: 9 };
185
+
186
+ test("spanCut cuts the span's block and closes the popover", () => {
187
+ // Arrange
188
+ const deps = makeDeps({ mode: { type: "span", span } });
189
+ const dispatch = createIntentDispatch(deps);
190
+
191
+ // Act
192
+ dispatch({ type: "spanCut" });
193
+
194
+ // Assert
195
+ expect(deps.controller.cut).toHaveBeenCalledWith(3);
196
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
197
+ });
198
+
199
+ test("openSpanActions enters the list at index 0", () => {
200
+ // Arrange
201
+ const deps = makeDeps({ mode: { type: "span", span } });
202
+ const dispatch = createIntentDispatch(deps);
203
+
204
+ // Act
205
+ dispatch({ type: "openSpanActions" });
206
+
207
+ // Assert
208
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "spanActions", span, index: 0 });
209
+ });
210
+
211
+ test("moveSpanAction clamps within the actions list", () => {
212
+ // Arrange
213
+ const deps = makeDeps({
214
+ mode: { type: "spanActions", span, index: 1 },
215
+ quickActions: [{ prompt: "one" }, { prompt: "two" }],
216
+ });
217
+ const dispatch = createIntentDispatch(deps);
218
+
219
+ // Act
220
+ dispatch({ type: "moveSpanAction", direction: 1 });
221
+
222
+ // Assert - already at the last row, so the index holds
223
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "spanActions", span, index: 1 });
224
+ });
225
+
226
+ test("pickSpanAction inserts the preset comment and returns to normal", () => {
227
+ // Arrange
228
+ const annotate = mock(() => "an_new");
229
+ const deps = makeDeps({
230
+ mode: { type: "spanActions", span, index: 1 },
231
+ session: sessionWith([]),
232
+ quickActions: [{ prompt: "Needs a test" }, { prompt: "YAGNI", metadata: "cut scope" }],
233
+ controller: { ...baseController(), annotate } as unknown as IntentDispatchDeps["controller"],
234
+ });
235
+ const dispatch = createIntentDispatch(deps);
236
+
237
+ // Act
238
+ dispatch({ type: "pickSpanAction" });
239
+
240
+ // Assert - metadata joins the prompt with a blank line
241
+ expect(annotate).toHaveBeenCalledWith("comment", 3, 2, 9, "YAGNI\n\ncut scope");
242
+ expect(deps.setFocusedAnnotationId).toHaveBeenCalledWith("an_new");
243
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
244
+ });
245
+
246
+ test("pickSpanAction honors an explicit index (mouse click)", () => {
247
+ // Arrange
248
+ const annotate = mock(() => "an_new");
249
+ const deps = makeDeps({
250
+ mode: { type: "spanActions", span, index: 0 },
251
+ session: sessionWith([]),
252
+ quickActions: [{ prompt: "Needs a test" }, { prompt: "Extract the duplication" }],
253
+ controller: { ...baseController(), annotate } as unknown as IntentDispatchDeps["controller"],
254
+ });
255
+ const dispatch = createIntentDispatch(deps);
256
+
257
+ // Act
258
+ dispatch({ type: "pickSpanAction", index: 1 });
259
+
260
+ // Assert
261
+ expect(annotate).toHaveBeenCalledWith("comment", 3, 2, 9, "Extract the duplication");
262
+ });
263
+
264
+ test("closeSpanActions returns to the span toolbar", () => {
265
+ // Arrange
266
+ const deps = makeDeps({ mode: { type: "spanActions", span, index: 2 } });
267
+ const dispatch = createIntentDispatch(deps);
268
+
269
+ // Act
270
+ dispatch({ type: "closeSpanActions" });
271
+
272
+ // Assert
273
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "span", span });
274
+ });
275
+ });
276
+
277
+ describe("openSubmit", () => {
278
+ test("force-opens the review rail so the confirm card can never be hidden", () => {
279
+ // Arrange
280
+ const deps = makeDeps({ session: sessionWith([]), reviewMode: "hidden" });
281
+ const dispatch = createIntentDispatch(deps);
282
+
283
+ // Act
284
+ dispatch({ type: "openSubmit" });
285
+
286
+ // Assert
287
+ expect(deps.setReviewMode).toHaveBeenCalledWith("expanded");
288
+ expect(deps.setRailTab).toHaveBeenCalledWith("review");
289
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", verdict: "approve", summary: "" });
290
+ });
291
+
292
+ test("does nothing without a session", () => {
293
+ // Arrange
294
+ const deps = makeDeps({ session: null });
295
+ const dispatch = createIntentDispatch(deps);
296
+
297
+ // Act
298
+ dispatch({ type: "openSubmit" });
299
+
300
+ // Assert
301
+ expect(deps.setMode).not.toHaveBeenCalled();
302
+ });
303
+ });
304
+
305
+ describe("share", () => {
306
+ test("dispatches to the controller's share", () => {
307
+ // Arrange
308
+ const deps = makeDeps();
309
+ const dispatch = createIntentDispatch(deps);
310
+
311
+ // Act
312
+ dispatch({ type: "share" });
313
+
314
+ // Assert
315
+ expect(deps.controller.share).toHaveBeenCalled();
316
+ });
317
+ });
318
+
319
+ describe("restoreCuration", () => {
320
+ const items = [
321
+ {
322
+ id: "diff:f#0#hunk",
323
+ source: "diff" as const,
324
+ label: "f:1 - hunk",
325
+ preview: [],
326
+ revealIndex: 1,
327
+ },
328
+ {
329
+ id: "diff:f#1#2",
330
+ source: "diff" as const,
331
+ label: "f:9 - change",
332
+ preview: [],
333
+ revealIndex: 9,
334
+ },
335
+ ];
336
+
337
+ test("restores the selected item and clears the selection", () => {
338
+ // Arrange
339
+ const deps = makeDeps({ selectedCurationId: "diff:f#1#2" });
340
+ (deps.controller.curationItems as ReturnType<typeof mock>).mockReturnValue(items);
341
+ const dispatch = createIntentDispatch(deps);
342
+
343
+ // Act
344
+ dispatch({ type: "restoreCuration" });
345
+
346
+ // Assert
347
+ expect(deps.controller.restoreCuration).toHaveBeenCalledWith("diff:f#1#2");
348
+ expect(deps.setSelectedCurationId).toHaveBeenCalledWith(undefined);
349
+ });
350
+
351
+ test("with nothing selected, undoes the last removal", () => {
352
+ // Arrange
353
+ const deps = makeDeps({ selectedCurationId: undefined });
354
+ (deps.controller.curationItems as ReturnType<typeof mock>).mockReturnValue(items);
355
+ const dispatch = createIntentDispatch(deps);
356
+
357
+ // Act
358
+ dispatch({ type: "restoreCuration" });
359
+
360
+ // Assert
361
+ expect(deps.controller.restoreCuration).toHaveBeenCalledWith("diff:f#1#2");
362
+ });
363
+
364
+ test("does nothing when there is nothing curated out", () => {
365
+ // Arrange
366
+ const deps = makeDeps();
367
+ (deps.controller.curationItems as ReturnType<typeof mock>).mockReturnValue([]);
368
+ const dispatch = createIntentDispatch(deps);
369
+
370
+ // Act
371
+ dispatch({ type: "restoreCuration" });
372
+
373
+ // Assert
374
+ expect(deps.controller.restoreCuration).not.toHaveBeenCalled();
375
+ });
376
+ });
377
+
378
+ describe("exit", () => {
379
+ test("calls the exit hook with 0", () => {
380
+ // Arrange
381
+ const deps = makeDeps();
382
+ const dispatch = createIntentDispatch(deps);
383
+
384
+ // Act
385
+ dispatch({ type: "exit" });
386
+
387
+ // Assert
388
+ expect(deps.onExit).toHaveBeenCalledWith(0);
389
+ });
390
+ });
391
+
392
+ describe("inbox delete", () => {
393
+ test("requestDeleteSession opens the confirm on the cursor row", () => {
394
+ // Arrange
395
+ const inbox = [{ id: "ses_1", artifact: { meta: { title: "Plan A" } } }] as never;
396
+ const deps = makeDeps({ inbox, inboxCursor: 0 });
397
+
398
+ // Act
399
+ createIntentDispatch(deps)({ type: "requestDeleteSession" });
400
+
401
+ // Assert
402
+ expect(deps.setMode).toHaveBeenCalledWith({
403
+ type: "confirmDelete",
404
+ sessionId: "ses_1",
405
+ title: "Plan A",
406
+ });
407
+ });
408
+
409
+ test("confirmDialog deletes the session and closes", () => {
410
+ // Arrange
411
+ const deps = makeDeps({ mode: { type: "confirmDelete", sessionId: "ses_1", title: "Plan A" } });
412
+
413
+ // Act
414
+ createIntentDispatch(deps)({ type: "confirmDialog" });
415
+
416
+ // Assert
417
+ expect(deps.controller.deleteSession).toHaveBeenCalledWith("ses_1");
418
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
419
+ });
420
+ });
421
+
422
+ describe("rename author", () => {
423
+ test("openRename seeds the prompt with the author's current local name", () => {
424
+ // Arrange
425
+ const session = { annotations: [{ id: "a1", author: "SHA256:x" }] } as never;
426
+ const deps = makeDeps({
427
+ session,
428
+ focusedAnnotationId: "a1",
429
+ authorNames: { "SHA256:x": "Alex" },
430
+ });
431
+
432
+ // Act
433
+ createIntentDispatch(deps)({ type: "openRename" });
434
+
435
+ // Assert
436
+ expect(deps.setMode).toHaveBeenCalledWith({
437
+ type: "rename",
438
+ authorId: "SHA256:x",
439
+ text: "Alex",
440
+ });
441
+ });
442
+
443
+ test("openRename on your own note does nothing but explain", () => {
444
+ // Arrange
445
+ const session = { annotations: [{ id: "a1" }] } as never;
446
+ const deps = makeDeps({ session, focusedAnnotationId: "a1" });
447
+
448
+ // Act
449
+ createIntentDispatch(deps)({ type: "openRename" });
450
+
451
+ // Assert
452
+ expect(deps.controller.setStatus).toHaveBeenCalled();
453
+ expect(deps.setMode).not.toHaveBeenCalled();
454
+ });
455
+
456
+ test("confirmDialog in rename mode persists the trimmed name and closes", () => {
457
+ // Arrange
458
+ const deps = makeDeps({ mode: { type: "rename", authorId: "SHA256:x", text: " Alex " } });
459
+
460
+ // Act
461
+ createIntentDispatch(deps)({ type: "confirmDialog" });
462
+
463
+ // Assert
464
+ expect(deps.renameAuthor).toHaveBeenCalledWith("SHA256:x", "Alex");
465
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
466
+ });
467
+ });
468
+
469
+ describe("collaborator self-name", () => {
470
+ test("confirmDialog in nameSelf mode records the trimmed name and closes", () => {
471
+ // Arrange
472
+ const deps = makeDeps({ mode: { type: "nameSelf", text: " Robin " } });
473
+
474
+ // Act
475
+ createIntentDispatch(deps)({ type: "confirmDialog" });
476
+
477
+ // Assert
478
+ expect(deps.controller.setSelfName).toHaveBeenCalledWith("Robin");
479
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
480
+ });
481
+
482
+ test("an empty name still closes - the collaborator stays anonymous", () => {
483
+ // Arrange
484
+ const deps = makeDeps({ mode: { type: "nameSelf", text: " " } });
485
+
486
+ // Act
487
+ createIntentDispatch(deps)({ type: "confirmDialog" });
488
+
489
+ // Assert
490
+ expect(deps.controller.setSelfName).toHaveBeenCalledWith("");
491
+ expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
492
+ });
493
+ });