@cueloop/client 0.1.0-alpha.56 → 0.1.0-alpha.59

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.
@@ -114,315 +114,435 @@ export interface IntentDispatchDeps {
114
114
  openCardEdit: (annotationId: string) => void;
115
115
  }
116
116
 
117
- /** Build the intent handler for one render from its dependency bag. */
118
- export function createIntentDispatch(deps: IntentDispatchDeps): (intent: Intent) => void {
119
- const {
120
- controller,
121
- onExit,
122
- isDiff,
123
- display,
124
- rows,
125
- cursor,
126
- inbox,
127
- inboxCursor,
128
- mode,
129
- session,
130
- reviewMode,
131
- reviewWidth,
132
- terminalWidth,
133
- focusedAnnotationId,
134
- selectedCurationId,
135
- authorNames,
136
- quickActions,
137
- renameAuthor,
138
- liveInput,
139
- reviewWidthRef,
140
- planSheetRef,
141
- setCursor,
142
- setInboxCursor,
143
- setMode,
144
- setReviewMode,
145
- setReviewWidth,
146
- setRailTab,
147
- setFocusedAnnotationId,
148
- setSelectedCurationId,
149
- setPulsedAnnotationId,
150
- selectCardFromDocument,
151
- runEditorHandOff,
152
- openCardEdit,
153
- } = deps;
117
+ type IntentOfType<Kind extends Intent["type"]> = Extract<Intent, { type: Kind }>;
154
118
 
155
- return (intent: Intent): void => {
156
- switch (intent.type) {
157
- case "exit":
158
- return void onExit?.(0);
159
- case "status":
160
- return controller.setStatus(intent.message);
161
- case "move": {
162
- const navigableCount = isDiff ? rows.length : display.length;
163
- if (intent.to === "down") setCursor((current) => Math.min(navigableCount - 1, current + 1));
164
- else if (intent.to === "up") setCursor((current) => Math.max(0, current - 1));
165
- else if (intent.to === "top") setCursor(0);
166
- else setCursor(navigableCount - 1);
167
- return;
168
- }
169
- case "inboxMove": {
170
- const navigableCount = inbox?.length ?? 0;
171
- setInboxCursor((current) =>
172
- intent.to === "down"
173
- ? Math.min(navigableCount - 1, current + 1)
174
- : Math.max(0, current - 1),
175
- );
176
- return;
177
- }
178
- case "openSession": {
179
- const selected = inbox?.[inboxCursor];
180
- if (selected) controller.open(selected.id);
181
- return;
182
- }
183
- case "requestDeleteSession": {
184
- const selected = inbox?.[inboxCursor];
185
- if (selected)
186
- setMode({
187
- type: "confirmDelete",
188
- sessionId: selected.id,
189
- title: selected.artifact.meta.title ?? selected.id,
190
- });
191
- return;
192
- }
193
- case "openRename": {
194
- const focused = session?.annotations.find(
195
- (annotation) => annotation.id === focusedAnnotationId,
196
- );
197
- if (!focused?.author)
198
- return void controller.setStatus("that is your own note - nothing to rename");
199
- return void setMode({
200
- type: "rename",
201
- authorId: focused.author,
202
- text: authorNames[focused.author] ?? "",
119
+ function handleExit(_intent: IntentOfType<"exit">, deps: IntentDispatchDeps): void {
120
+ deps.onExit?.(0);
121
+ }
122
+
123
+ function handleStatus(intent: IntentOfType<"status">, deps: IntentDispatchDeps): void {
124
+ deps.controller.setStatus(intent.message);
125
+ }
126
+
127
+ function handleMove(intent: IntentOfType<"move">, deps: IntentDispatchDeps): void {
128
+ const navigableCount = deps.isDiff ? deps.rows.length : deps.display.length;
129
+ if (intent.to === "down") deps.setCursor((current) => Math.min(navigableCount - 1, current + 1));
130
+ else if (intent.to === "up") deps.setCursor((current) => Math.max(0, current - 1));
131
+ else if (intent.to === "top") deps.setCursor(0);
132
+ else deps.setCursor(navigableCount - 1);
133
+ }
134
+
135
+ function handleInboxMove(intent: IntentOfType<"inboxMove">, deps: IntentDispatchDeps): void {
136
+ const navigableCount = deps.inbox?.length ?? 0;
137
+ deps.setInboxCursor((current) =>
138
+ intent.to === "down" ? Math.min(navigableCount - 1, current + 1) : Math.max(0, current - 1),
139
+ );
140
+ }
141
+
142
+ function handleOpenSession(_intent: IntentOfType<"openSession">, deps: IntentDispatchDeps): void {
143
+ const selected = deps.inbox?.[deps.inboxCursor];
144
+ if (selected) deps.controller.open(selected.id);
145
+ }
146
+
147
+ function handleRequestDeleteSession(
148
+ _intent: IntentOfType<"requestDeleteSession">,
149
+ deps: IntentDispatchDeps,
150
+ ): void {
151
+ const selected = deps.inbox?.[deps.inboxCursor];
152
+ if (selected)
153
+ deps.setMode({
154
+ type: "confirmDelete",
155
+ sessionId: selected.id,
156
+ title: selected.artifact.meta.title ?? selected.id,
157
+ });
158
+ }
159
+
160
+ function handleOpenRename(_intent: IntentOfType<"openRename">, deps: IntentDispatchDeps): void {
161
+ const focused = deps.session?.annotations.find(
162
+ (annotation) => annotation.id === deps.focusedAnnotationId,
163
+ );
164
+ if (!focused?.author) {
165
+ deps.controller.setStatus("that is your own note - nothing to rename");
166
+ return;
167
+ }
168
+ deps.setMode({
169
+ type: "rename",
170
+ authorId: focused.author,
171
+ text: deps.authorNames[focused.author] ?? "",
172
+ });
173
+ }
174
+
175
+ function handleConfirmDialog(
176
+ _intent: IntentOfType<"confirmDialog">,
177
+ deps: IntentDispatchDeps,
178
+ ): void {
179
+ const { mode, controller } = deps;
180
+ if (mode.type === "confirmDelete") controller.deleteSession(mode.sessionId);
181
+ else if (mode.type === "rename") deps.renameAuthor(mode.authorId, mode.text.trim());
182
+ else if (mode.type === "nameSelf") controller.setSelfName(mode.text.trim());
183
+ deps.setMode({ type: "normal" });
184
+ }
185
+
186
+ function handleStartSpan(_intent: IntentOfType<"startSpan">, deps: IntentDispatchDeps): void {
187
+ const block = deps.display[deps.cursor];
188
+ if (!block?.work) return;
189
+ const span = startSpan(deps.cursor, displayText(block));
190
+ if (span) deps.setMode({ type: "span", span });
191
+ }
192
+
193
+ function handleSpanKey(intent: IntentOfType<"spanKey">, deps: IntentDispatchDeps): void {
194
+ const { mode } = deps;
195
+ if (mode.type === "span") {
196
+ const span = spanKey(
197
+ mode.span,
198
+ intent.name,
199
+ displayText(deps.display[mode.span.displayIndex]!),
200
+ );
201
+ deps.setMode({ type: "span", span });
202
+ }
203
+ }
204
+
205
+ function handleSpanCut(_intent: IntentOfType<"spanCut">, deps: IntentDispatchDeps): void {
206
+ // the block the span sits in, cut whole (partial-span cut is not modeled)
207
+ const { mode } = deps;
208
+ if (mode.type === "span") {
209
+ deps.controller.cut(mode.span.displayIndex);
210
+ deps.setMode({ type: "normal" });
211
+ }
212
+ }
213
+
214
+ function handleOpenSpanActions(
215
+ _intent: IntentOfType<"openSpanActions">,
216
+ deps: IntentDispatchDeps,
217
+ ): void {
218
+ const { mode } = deps;
219
+ if (mode.type === "span") deps.setMode({ type: "spanActions", span: mode.span, index: 0 });
220
+ }
221
+
222
+ function handleMoveSpanAction(
223
+ intent: IntentOfType<"moveSpanAction">,
224
+ deps: IntentDispatchDeps,
225
+ ): void {
226
+ const { mode } = deps;
227
+ if (mode.type === "spanActions") {
228
+ const index = Math.max(
229
+ 0,
230
+ Math.min(deps.quickActions.length - 1, mode.index + intent.direction),
231
+ );
232
+ deps.setMode({ ...mode, index });
233
+ }
234
+ }
235
+
236
+ function handleCloseSpanActions(
237
+ _intent: IntentOfType<"closeSpanActions">,
238
+ deps: IntentDispatchDeps,
239
+ ): void {
240
+ const { mode } = deps;
241
+ if (mode.type === "spanActions") deps.setMode({ type: "span", span: mode.span });
242
+ }
243
+
244
+ function handlePickSpanAction(
245
+ intent: IntentOfType<"pickSpanAction">,
246
+ deps: IntentDispatchDeps,
247
+ ): void {
248
+ const { mode, session, controller } = deps;
249
+ if (mode.type !== "spanActions") return;
250
+ const action = deps.quickActions[intent.index ?? mode.index];
251
+ if (session && action) {
252
+ const body = quickActionBody(action);
253
+ const annotationId = controller.annotate(
254
+ "comment",
255
+ mode.span.displayIndex,
256
+ mode.span.start,
257
+ mode.span.end,
258
+ body,
259
+ );
260
+ if (annotationId) deps.setFocusedAnnotationId(annotationId);
261
+ }
262
+ deps.setMode({ type: "normal" });
263
+ }
264
+
265
+ function handleOpenCompose(intent: IntentOfType<"openCompose">, deps: IntentDispatchDeps): void {
266
+ const { mode } = deps;
267
+ deps.liveInput.current = "";
268
+ if (intent.from === "span" && mode.type === "span") {
269
+ deps.setMode({
270
+ type: "compose",
271
+ kind: intent.kind,
272
+ displayIndex: mode.span.displayIndex,
273
+ start: mode.span.start,
274
+ end: mode.span.end,
275
+ text: "",
276
+ });
277
+ } else if (deps.isDiff) {
278
+ const row = deps.rows[deps.cursor];
279
+ if (row)
280
+ deps.setMode({
281
+ type: "compose",
282
+ kind: intent.kind,
283
+ displayIndex: deps.cursor,
284
+ start: 0,
285
+ end: row.text.length,
286
+ text: "",
287
+ });
288
+ } else {
289
+ // a mouse drag leaves a native selection; it wins over the cursor block
290
+ const native = deps.planSheetRef.current?.readSelection() ?? null;
291
+ if (native) {
292
+ deps.setMode({ type: "compose", kind: intent.kind, ...native, text: "" });
293
+ } else {
294
+ const block = deps.display[deps.cursor];
295
+ if (block)
296
+ deps.setMode({
297
+ type: "compose",
298
+ kind: intent.kind,
299
+ displayIndex: deps.cursor,
300
+ start: 0,
301
+ end: displayText(block).length,
302
+ text: "",
203
303
  });
204
- }
205
- case "confirmDialog": {
206
- if (mode.type === "confirmDelete") controller.deleteSession(mode.sessionId);
207
- else if (mode.type === "rename") renameAuthor(mode.authorId, mode.text.trim());
208
- else if (mode.type === "nameSelf") controller.setSelfName(mode.text.trim());
209
- return void setMode({ type: "normal" });
210
- }
211
- case "startSpan": {
212
- const block = display[cursor];
213
- if (!block?.work) return;
214
- const span = startSpan(cursor, displayText(block));
215
- if (span) setMode({ type: "span", span });
216
- return;
217
- }
218
- case "spanKey":
219
- if (mode.type === "span") {
220
- const span = spanKey(
221
- mode.span,
222
- intent.name,
223
- displayText(display[mode.span.displayIndex]!),
224
- );
225
- setMode({ type: "span", span });
226
- }
227
- return;
228
- case "spanCut":
229
- // the block the span sits in, cut whole (partial-span cut is not modeled)
230
- if (mode.type === "span") {
231
- controller.cut(mode.span.displayIndex);
232
- setMode({ type: "normal" });
233
- }
234
- return;
235
- case "openSpanActions":
236
- if (mode.type === "span") setMode({ type: "spanActions", span: mode.span, index: 0 });
237
- return;
238
- case "moveSpanAction":
239
- if (mode.type === "spanActions") {
240
- const index = Math.max(
241
- 0,
242
- Math.min(quickActions.length - 1, mode.index + intent.direction),
243
- );
244
- setMode({ ...mode, index });
245
- }
246
- return;
247
- case "closeSpanActions":
248
- if (mode.type === "spanActions") setMode({ type: "span", span: mode.span });
249
- return;
250
- case "pickSpanAction": {
251
- if (mode.type !== "spanActions") return;
252
- const action = quickActions[intent.index ?? mode.index];
253
- if (session && action) {
254
- const body = quickActionBody(action);
255
- const annotationId = controller.annotate(
256
- "comment",
257
- mode.span.displayIndex,
258
- mode.span.start,
259
- mode.span.end,
260
- body,
261
- );
262
- if (annotationId) setFocusedAnnotationId(annotationId);
263
- }
264
- return void setMode({ type: "normal" });
265
- }
266
- case "openCompose": {
267
- liveInput.current = "";
268
- if (intent.from === "span" && mode.type === "span") {
269
- setMode({
270
- type: "compose",
271
- kind: intent.kind,
272
- displayIndex: mode.span.displayIndex,
273
- start: mode.span.start,
274
- end: mode.span.end,
275
- text: "",
276
- });
277
- } else if (isDiff) {
278
- const row = rows[cursor];
279
- if (row)
280
- setMode({
281
- type: "compose",
282
- kind: intent.kind,
283
- displayIndex: cursor,
284
- start: 0,
285
- end: row.text.length,
286
- text: "",
287
- });
288
- } else {
289
- // a mouse drag leaves a native selection; it wins over the cursor block
290
- const native = planSheetRef.current?.readSelection() ?? null;
291
- if (native) {
292
- setMode({ type: "compose", kind: intent.kind, ...native, text: "" });
293
- } else {
294
- const block = display[cursor];
295
- if (block)
296
- setMode({
297
- type: "compose",
298
- kind: intent.kind,
299
- displayIndex: cursor,
300
- start: 0,
301
- end: displayText(block).length,
302
- text: "",
303
- });
304
- }
305
- }
306
- return;
307
- }
308
- case "openSubmit":
309
- if (!session) return;
310
- liveInput.current = "";
311
- // the confirm card lives in the expanded review rail; a compact or hidden
312
- // panel would swallow the whole submit flow, so force the rail open (live
313
- // only - the saved panel preference is left untouched)
314
- setReviewMode("expanded");
315
- setRailTab("review");
316
- return void setMode({ type: "submit", verdict: defaultVerdict(session), summary: "" });
317
- case "share":
318
- return controller.share();
319
- case "cut":
320
- return controller.cut(cursor);
321
- case "rejectHunk":
322
- return controller.toggleRejectHunk(cursor);
323
- case "rejectChange":
324
- return controller.toggleRejectChange(cursor);
325
- case "restoreCuration": {
326
- // undo the selected curated-out item, or the last rejected when none is
327
- // selected, so a bare `u` reads as "undo my last curation"
328
- const items = controller.curationItems();
329
- if (!items.length) return;
330
- const targetId = selectedCurationId ?? items[items.length - 1]!.id;
331
- controller.restoreCuration(targetId);
332
- return void setSelectedCurationId(undefined);
333
- }
334
- case "edit":
335
- return runEditorHandOff();
336
- case "editCard":
337
- if (focusedAnnotationId) openCardEdit(focusedAnnotationId);
338
- return;
339
- case "nextAnnotation":
340
- case "prevAnnotation": {
341
- // cycle open cards only - addressed ones are out of the rail
342
- const annotations = (session?.annotations ?? []).filter(
343
- (annotation) => !isAddressed(annotation),
344
- );
345
- if (!annotations.length) return;
346
- const focusedIndex = annotations.findIndex(
347
- (annotation) => annotation.id === focusedAnnotationId,
348
- );
349
- const nextIndex =
350
- focusedIndex === -1
351
- ? 0
352
- : (focusedIndex + (intent.type === "nextAnnotation" ? 1 : -1) + annotations.length) %
353
- annotations.length;
354
- return void selectCardFromDocument(annotations[nextIndex]!.id);
355
- }
356
- case "walkStart":
357
- return controller.walkStart();
358
- case "walkForward":
359
- return controller.walkForward();
360
- case "walkBack":
361
- return controller.walkBack();
362
- case "walkLeave":
363
- return controller.walkLeave();
364
- case "removeAnnotation":
365
- if (focusedAnnotationId) {
366
- controller.removeAnnotation(focusedAnnotationId);
367
- setFocusedAnnotationId(undefined);
368
- }
369
- return;
370
- case "deselect":
371
- planSheetRef.current?.clearSelection();
372
- setFocusedAnnotationId(undefined);
373
- setPulsedAnnotationId(null);
374
- return;
375
- case "closeOverlay":
376
- return void setMode({ type: "normal" });
377
- case "saveCompose": {
378
- const body = liveInput.current.trim();
379
- if (mode.type === "railEdit") {
380
- if (session && body) controller.updateAnnotation(mode.id, body);
381
- return void setMode({ type: "normal" });
382
- }
383
- if (mode.type !== "compose") return;
384
- if (session && body) {
385
- const annotationId = controller.annotate(
386
- mode.kind,
387
- mode.displayIndex,
388
- mode.start,
389
- mode.end,
390
- body,
391
- );
392
- if (annotationId) setFocusedAnnotationId(annotationId);
393
- }
394
- return void setMode({ type: "normal" });
395
- }
396
- case "submitVerdict":
397
- if (mode.type === "submit") controller.submit(mode.verdict, liveInput.current);
398
- return void setMode({ type: "normal" });
399
- case "cycleVerdict": {
400
- if (mode.type !== "submit") return;
401
- const verdictIndex =
402
- (VERDICTS.indexOf(mode.verdict) + intent.direction + VERDICTS.length) % VERDICTS.length;
403
- return void setMode({ ...mode, verdict: VERDICTS[verdictIndex]! });
404
- }
405
- case "finishReview":
406
- return controller.finishReview();
407
- case "optInAutoClose":
408
- return controller.optInAutoClose();
409
- case "dismissCompletion":
410
- return controller.dismissCompletion();
411
- case "cycleReviewPanel": {
412
- const next = cycleReviewPanelMode(reviewMode);
413
- setReviewMode(next);
414
- return controller.saveReviewPanel({ mode: next });
415
- }
416
- case "resizeReviewPanel": {
417
- if (reviewMode !== "expanded") return;
418
- const next = resolveReviewWidth(
419
- reviewWidth + intent.direction * REVIEW_RESIZE_STEP,
420
- terminalWidth,
421
- );
422
- reviewWidthRef.current = next;
423
- setReviewWidth(next);
424
- return controller.saveReviewPanel({ width: next });
425
- }
426
304
  }
305
+ }
306
+ }
307
+
308
+ function handleOpenSubmit(_intent: IntentOfType<"openSubmit">, deps: IntentDispatchDeps): void {
309
+ const { session } = deps;
310
+ if (!session) return;
311
+ deps.liveInput.current = "";
312
+ // the confirm card lives in the expanded review rail; a compact or hidden
313
+ // panel would swallow the whole submit flow, so force the rail open (live
314
+ // only - the saved panel preference is left untouched)
315
+ deps.setReviewMode("expanded");
316
+ deps.setRailTab("review");
317
+ deps.setMode({ type: "submit", verdict: defaultVerdict(session), summary: "" });
318
+ }
319
+
320
+ function handleShare(_intent: IntentOfType<"share">, deps: IntentDispatchDeps): void {
321
+ deps.controller.share();
322
+ }
323
+
324
+ function handleCut(_intent: IntentOfType<"cut">, deps: IntentDispatchDeps): void {
325
+ deps.controller.cut(deps.cursor);
326
+ }
327
+
328
+ function handleRejectHunk(_intent: IntentOfType<"rejectHunk">, deps: IntentDispatchDeps): void {
329
+ deps.controller.toggleRejectHunk(deps.cursor);
330
+ }
331
+
332
+ function handleRejectChange(_intent: IntentOfType<"rejectChange">, deps: IntentDispatchDeps): void {
333
+ deps.controller.toggleRejectChange(deps.cursor);
334
+ }
335
+
336
+ function handleRestoreCuration(
337
+ _intent: IntentOfType<"restoreCuration">,
338
+ deps: IntentDispatchDeps,
339
+ ): void {
340
+ // undo the selected curated-out item, or the last rejected when none is
341
+ // selected, so a bare `u` reads as "undo my last curation"
342
+ const items = deps.controller.curationItems();
343
+ if (!items.length) return;
344
+ const targetId = deps.selectedCurationId ?? items[items.length - 1]!.id;
345
+ deps.controller.restoreCuration(targetId);
346
+ deps.setSelectedCurationId(undefined);
347
+ }
348
+
349
+ function handleEdit(_intent: IntentOfType<"edit">, deps: IntentDispatchDeps): void {
350
+ deps.runEditorHandOff();
351
+ }
352
+
353
+ function handleEditCard(_intent: IntentOfType<"editCard">, deps: IntentDispatchDeps): void {
354
+ if (deps.focusedAnnotationId) deps.openCardEdit(deps.focusedAnnotationId);
355
+ }
356
+
357
+ function handleAnnotationCycle(
358
+ intent: IntentOfType<"nextAnnotation" | "prevAnnotation">,
359
+ deps: IntentDispatchDeps,
360
+ ): void {
361
+ // cycle open cards only - addressed ones are out of the rail
362
+ const annotations = (deps.session?.annotations ?? []).filter(
363
+ (annotation) => !isAddressed(annotation),
364
+ );
365
+ if (!annotations.length) return;
366
+ const focusedIndex = annotations.findIndex(
367
+ (annotation) => annotation.id === deps.focusedAnnotationId,
368
+ );
369
+ const nextIndex =
370
+ focusedIndex === -1
371
+ ? 0
372
+ : (focusedIndex + (intent.type === "nextAnnotation" ? 1 : -1) + annotations.length) %
373
+ annotations.length;
374
+ deps.selectCardFromDocument(annotations[nextIndex]!.id);
375
+ }
376
+
377
+ function handleWalkStart(_intent: IntentOfType<"walkStart">, deps: IntentDispatchDeps): void {
378
+ deps.controller.walkStart();
379
+ }
380
+
381
+ function handleWalkForward(_intent: IntentOfType<"walkForward">, deps: IntentDispatchDeps): void {
382
+ deps.controller.walkForward();
383
+ }
384
+
385
+ function handleWalkBack(_intent: IntentOfType<"walkBack">, deps: IntentDispatchDeps): void {
386
+ deps.controller.walkBack();
387
+ }
388
+
389
+ function handleWalkLeave(_intent: IntentOfType<"walkLeave">, deps: IntentDispatchDeps): void {
390
+ deps.controller.walkLeave();
391
+ }
392
+
393
+ function handleRemoveAnnotation(
394
+ _intent: IntentOfType<"removeAnnotation">,
395
+ deps: IntentDispatchDeps,
396
+ ): void {
397
+ if (deps.focusedAnnotationId) {
398
+ deps.controller.removeAnnotation(deps.focusedAnnotationId);
399
+ deps.setFocusedAnnotationId(undefined);
400
+ }
401
+ }
402
+
403
+ function handleDeselect(_intent: IntentOfType<"deselect">, deps: IntentDispatchDeps): void {
404
+ deps.planSheetRef.current?.clearSelection();
405
+ deps.setFocusedAnnotationId(undefined);
406
+ deps.setPulsedAnnotationId(null);
407
+ }
408
+
409
+ function handleCloseOverlay(_intent: IntentOfType<"closeOverlay">, deps: IntentDispatchDeps): void {
410
+ deps.setMode({ type: "normal" });
411
+ }
412
+
413
+ function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDispatchDeps): void {
414
+ const { mode, session, controller } = deps;
415
+ const body = deps.liveInput.current.trim();
416
+ if (mode.type === "railEdit") {
417
+ if (session && body) controller.updateAnnotation(mode.id, body);
418
+ deps.setMode({ type: "normal" });
419
+ return;
420
+ }
421
+ if (mode.type !== "compose") return;
422
+ if (session && body) {
423
+ const annotationId = controller.annotate(
424
+ mode.kind,
425
+ mode.displayIndex,
426
+ mode.start,
427
+ mode.end,
428
+ body,
429
+ );
430
+ if (annotationId) deps.setFocusedAnnotationId(annotationId);
431
+ }
432
+ deps.setMode({ type: "normal" });
433
+ }
434
+
435
+ function handleSubmitVerdict(
436
+ _intent: IntentOfType<"submitVerdict">,
437
+ deps: IntentDispatchDeps,
438
+ ): void {
439
+ const { mode } = deps;
440
+ if (mode.type === "submit") deps.controller.submit(mode.verdict, deps.liveInput.current);
441
+ deps.setMode({ type: "normal" });
442
+ }
443
+
444
+ function handleCycleVerdict(intent: IntentOfType<"cycleVerdict">, deps: IntentDispatchDeps): void {
445
+ const { mode } = deps;
446
+ if (mode.type !== "submit") return;
447
+ const verdictIndex =
448
+ (VERDICTS.indexOf(mode.verdict) + intent.direction + VERDICTS.length) % VERDICTS.length;
449
+ deps.setMode({ ...mode, verdict: VERDICTS[verdictIndex]! });
450
+ }
451
+
452
+ function handleFinishReview(_intent: IntentOfType<"finishReview">, deps: IntentDispatchDeps): void {
453
+ deps.controller.finishReview();
454
+ }
455
+
456
+ function handleOptInAutoClose(
457
+ _intent: IntentOfType<"optInAutoClose">,
458
+ deps: IntentDispatchDeps,
459
+ ): void {
460
+ deps.controller.optInAutoClose();
461
+ }
462
+
463
+ function handleDismissCompletion(
464
+ _intent: IntentOfType<"dismissCompletion">,
465
+ deps: IntentDispatchDeps,
466
+ ): void {
467
+ deps.controller.dismissCompletion();
468
+ }
469
+
470
+ function handleCycleReviewPanel(
471
+ _intent: IntentOfType<"cycleReviewPanel">,
472
+ deps: IntentDispatchDeps,
473
+ ): void {
474
+ const next = cycleReviewPanelMode(deps.reviewMode);
475
+ deps.setReviewMode(next);
476
+ deps.controller.saveReviewPanel({ mode: next });
477
+ }
478
+
479
+ function handleResizeReviewPanel(
480
+ intent: IntentOfType<"resizeReviewPanel">,
481
+ deps: IntentDispatchDeps,
482
+ ): void {
483
+ if (deps.reviewMode !== "expanded") return;
484
+ const next = resolveReviewWidth(
485
+ deps.reviewWidth + intent.direction * REVIEW_RESIZE_STEP,
486
+ deps.terminalWidth,
487
+ );
488
+ deps.reviewWidthRef.current = next;
489
+ deps.setReviewWidth(next);
490
+ deps.controller.saveReviewPanel({ width: next });
491
+ }
492
+
493
+ const intentHandlers: {
494
+ [Kind in Intent["type"]]: (intent: IntentOfType<Kind>, deps: IntentDispatchDeps) => void;
495
+ } = {
496
+ exit: handleExit,
497
+ status: handleStatus,
498
+ move: handleMove,
499
+ inboxMove: handleInboxMove,
500
+ openSession: handleOpenSession,
501
+ requestDeleteSession: handleRequestDeleteSession,
502
+ openRename: handleOpenRename,
503
+ confirmDialog: handleConfirmDialog,
504
+ startSpan: handleStartSpan,
505
+ spanKey: handleSpanKey,
506
+ spanCut: handleSpanCut,
507
+ openSpanActions: handleOpenSpanActions,
508
+ moveSpanAction: handleMoveSpanAction,
509
+ closeSpanActions: handleCloseSpanActions,
510
+ pickSpanAction: handlePickSpanAction,
511
+ openCompose: handleOpenCompose,
512
+ openSubmit: handleOpenSubmit,
513
+ share: handleShare,
514
+ cut: handleCut,
515
+ rejectHunk: handleRejectHunk,
516
+ rejectChange: handleRejectChange,
517
+ restoreCuration: handleRestoreCuration,
518
+ edit: handleEdit,
519
+ editCard: handleEditCard,
520
+ nextAnnotation: handleAnnotationCycle,
521
+ prevAnnotation: handleAnnotationCycle,
522
+ walkStart: handleWalkStart,
523
+ walkForward: handleWalkForward,
524
+ walkBack: handleWalkBack,
525
+ walkLeave: handleWalkLeave,
526
+ removeAnnotation: handleRemoveAnnotation,
527
+ deselect: handleDeselect,
528
+ closeOverlay: handleCloseOverlay,
529
+ saveCompose: handleSaveCompose,
530
+ submitVerdict: handleSubmitVerdict,
531
+ cycleVerdict: handleCycleVerdict,
532
+ finishReview: handleFinishReview,
533
+ optInAutoClose: handleOptInAutoClose,
534
+ dismissCompletion: handleDismissCompletion,
535
+ cycleReviewPanel: handleCycleReviewPanel,
536
+ resizeReviewPanel: handleResizeReviewPanel,
537
+ };
538
+
539
+ /** Build the intent handler for one render from its dependency bag. */
540
+ export function createIntentDispatch(deps: IntentDispatchDeps): (intent: Intent) => void {
541
+ return (intent: Intent): void => {
542
+ const handler = intentHandlers[intent.type] as (
543
+ intent: Intent,
544
+ deps: IntentDispatchDeps,
545
+ ) => void;
546
+ handler(intent, deps);
427
547
  };
428
548
  }