@cueloop/client 0.1.0-alpha.4 → 0.1.0-alpha.41

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