@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.
- package/package.json +5 -5
- package/src/App.tsx +174 -574
- package/src/app-key-state.ts +60 -0
- package/src/app-screens.tsx +266 -0
- package/src/app-view-model.ts +286 -0
- package/src/components/PlanSheet.tsx +2 -62
- package/src/components/PrototypeSheet.tsx +19 -2
- package/src/components/plan-sheet-run-style.ts +86 -0
- package/src/config.ts +61 -47
- package/src/intent-dispatch.ts +427 -307
- package/src/keymap.ts +81 -59
- package/src/prototype-browser.ts +6 -1
- package/src/use-settings-dialog.tsx +263 -0
package/src/intent-dispatch.ts
CHANGED
|
@@ -114,315 +114,435 @@ export interface IntentDispatchDeps {
|
|
|
114
114
|
openCardEdit: (annotationId: string) => void;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
}
|