@cueloop/client 0.1.0-alpha.6 → 0.1.0-alpha.61
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +37 -33
- package/src/App.appearance.test.tsx +51 -0
- package/src/App.autoclose.test.tsx +73 -47
- package/src/App.diff.test.tsx +169 -22
- package/src/App.export.test.tsx +49 -37
- package/src/App.plan-review.test.tsx +714 -0
- package/src/App.prototype.test.tsx +194 -0
- package/src/App.readonly.test.tsx +69 -35
- package/src/App.rendering.test.tsx +59 -31
- package/src/App.review-panel.test.tsx +143 -0
- package/src/App.submit-confirm.test.tsx +229 -0
- package/src/App.test.tsx +139 -68
- package/src/App.toast.test.tsx +77 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +270 -0
- package/src/app-view-model.ts +307 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +250 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +129 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffSheet.scroll.test.tsx +135 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +65 -0
- package/src/components/DiffSheet.tsx +374 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +64 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +59 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +109 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +75 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +422 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +80 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +450 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +103 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +318 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +147 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -0
- package/src/components/diff-sheet-layout.test.ts +128 -0
- package/src/components/diff-sheet-layout.ts +184 -0
- package/src/components/plan-sheet-run-style.ts +90 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +138 -0
- package/src/components/stories-app.tsx +124 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +75 -0
- package/src/components/syntax-highlight.ts +102 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +193 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/config.test.ts +448 -31
- package/src/config.ts +339 -57
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +253 -0
- package/src/diff-intraline.test.ts +165 -0
- package/src/diff-intraline.ts +146 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +176 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +135 -9
- package/src/ghostty-terminal.test.ts +84 -0
- package/src/ghostty-terminal.ts +176 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +17 -21
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +499 -0
- package/src/intent-dispatch.ts +585 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +452 -0
- package/src/keymap.test.ts +635 -0
- package/src/keymap.ts +404 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +289 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +237 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +16 -5
- package/src/serve.test.ts +25 -9
- package/src/serve.ts +19 -8
- package/src/session-controller.curate.test.ts +260 -0
- package/src/session-controller.share.test.ts +320 -0
- package/src/session-controller.ts +993 -0
- package/src/share.ts +120 -0
- package/src/test-support.test.ts +60 -0
- package/src/test-support.ts +197 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +187 -0
- package/src/theme.ts +64 -17
- package/src/use-settings-dialog.tsx +275 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +22 -8
- package/src/view-diff.ts +39 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +554 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The intent reducer: one effectful handler per keymap Intent, lifted out of
|
|
3
|
+
* the App component so the 30-case grammar can be read and tested on its own.
|
|
4
|
+
* keymap.ts turns keys into Intents (pure); this turns an Intent into the
|
|
5
|
+
* matching controller call or view-state update. App builds the dependency
|
|
6
|
+
* bag once per render and hands it here; nothing in this module reaches back
|
|
7
|
+
* into React beyond the setters it is given.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Dispatch, MutableRefObject, RefObject, SetStateAction } from "react";
|
|
11
|
+
import { isAddressed, isAgentNote, type ReviewSession, type VerdictKind } from "@cueloop/schema";
|
|
12
|
+
import { displayText, spanKey, startSpan, type DisplayBlock, type SpanState } from "./view-plan";
|
|
13
|
+
import type { DiffRow } from "./view-diff";
|
|
14
|
+
import type { ReviewController } from "./session-controller";
|
|
15
|
+
import type { Intent } from "./keymap";
|
|
16
|
+
import { quickActionBody, type QuickAction } from "./config";
|
|
17
|
+
import type { PlanSheetHandle } from "./components/PlanSheet";
|
|
18
|
+
import { VERDICTS } from "./components/ConfirmCard";
|
|
19
|
+
import {
|
|
20
|
+
REVIEW_RESIZE_STEP,
|
|
21
|
+
cycleReviewPanelMode,
|
|
22
|
+
resolveReviewWidth,
|
|
23
|
+
type ReviewPanelMode,
|
|
24
|
+
} from "./review-panel";
|
|
25
|
+
|
|
26
|
+
/** The one overlay/mode the TUI is in; every compose/submit/edit flow is a variant. */
|
|
27
|
+
export type Mode =
|
|
28
|
+
| { type: "normal" }
|
|
29
|
+
| { type: "span"; span: SpanState }
|
|
30
|
+
| { type: "spanActions"; span: SpanState; index: number }
|
|
31
|
+
| {
|
|
32
|
+
type: "compose";
|
|
33
|
+
kind: "comment";
|
|
34
|
+
displayIndex: number;
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
| { type: "railEdit"; id: string; text: string }
|
|
40
|
+
| { type: "submit"; verdict: VerdictKind; summary: string }
|
|
41
|
+
| { type: "confirmDelete"; sessionId: string; title: string }
|
|
42
|
+
| { type: "rename"; authorId: string; text: string }
|
|
43
|
+
| { type: "nameSelf"; text: string };
|
|
44
|
+
|
|
45
|
+
/** The marked span for span mode and its quick-actions sub-mode; null otherwise. */
|
|
46
|
+
export function activeSpanState(mode: Mode): SpanState | null {
|
|
47
|
+
return mode.type === "span" || mode.type === "spanActions" ? mode.span : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Annotations that still count as feedback: agent notes never do, and an
|
|
52
|
+
* annotation a revision already addressed is settled - it neither blocks the
|
|
53
|
+
* verdict default nor re-enters the next feedback document.
|
|
54
|
+
*/
|
|
55
|
+
export function reviewerAnnotations(session: ReviewSession) {
|
|
56
|
+
return session.annotations.filter(
|
|
57
|
+
(annotation) => !isAgentNote(annotation) && !isAddressed(annotation),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function defaultVerdict(session: ReviewSession): VerdictKind {
|
|
62
|
+
return reviewerAnnotations(session).length || session.workingCopy !== undefined
|
|
63
|
+
? "request_changes"
|
|
64
|
+
: "approve";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Everything the reducer reads or writes, supplied fresh each render: the
|
|
69
|
+
* controller and exit hook, the derived reads (view rows, cursor, mode,
|
|
70
|
+
* session), the refs it pokes, the state setters, and the three App-owned
|
|
71
|
+
* selection helpers it composes.
|
|
72
|
+
*/
|
|
73
|
+
export interface IntentDispatchDeps {
|
|
74
|
+
controller: ReviewController;
|
|
75
|
+
onExit?: (code: number) => void;
|
|
76
|
+
|
|
77
|
+
isDiff: boolean;
|
|
78
|
+
display: DisplayBlock[];
|
|
79
|
+
rows: DiffRow[];
|
|
80
|
+
cursor: number;
|
|
81
|
+
inbox: ReviewSession[] | null;
|
|
82
|
+
inboxCursor: number;
|
|
83
|
+
mode: Mode;
|
|
84
|
+
session: ReviewSession | null;
|
|
85
|
+
reviewMode: ReviewPanelMode;
|
|
86
|
+
reviewWidth: number;
|
|
87
|
+
terminalWidth: number;
|
|
88
|
+
focusedAnnotationId: string | undefined;
|
|
89
|
+
/** The curation item selected in the rail, if any; the undo target when set. */
|
|
90
|
+
selectedCurationId: string | undefined;
|
|
91
|
+
/** Planner-local author renames, for seeding the rename prompt. */
|
|
92
|
+
authorNames: Record<string, string>;
|
|
93
|
+
/** Marker-popover quick actions, in list order; picking one inserts a preset comment. */
|
|
94
|
+
quickActions: QuickAction[];
|
|
95
|
+
/** Persist an author rename and update the live overrides (App-owned). */
|
|
96
|
+
renameAuthor: (id: string, name: string) => void;
|
|
97
|
+
|
|
98
|
+
liveInput: MutableRefObject<string>;
|
|
99
|
+
reviewWidthRef: MutableRefObject<number>;
|
|
100
|
+
planSheetRef: RefObject<PlanSheetHandle | null>;
|
|
101
|
+
|
|
102
|
+
setCursor: Dispatch<SetStateAction<number>>;
|
|
103
|
+
setInboxCursor: Dispatch<SetStateAction<number>>;
|
|
104
|
+
setMode: Dispatch<SetStateAction<Mode>>;
|
|
105
|
+
setReviewMode: Dispatch<SetStateAction<ReviewPanelMode>>;
|
|
106
|
+
setReviewWidth: Dispatch<SetStateAction<number>>;
|
|
107
|
+
setRailTab: Dispatch<SetStateAction<"review" | "agent">>;
|
|
108
|
+
setFocusedAnnotationId: Dispatch<SetStateAction<string | undefined>>;
|
|
109
|
+
setSelectedCurationId: Dispatch<SetStateAction<string | undefined>>;
|
|
110
|
+
setPulsedAnnotationId: Dispatch<SetStateAction<string | null>>;
|
|
111
|
+
|
|
112
|
+
selectCardFromDocument: (annotationId: string) => void;
|
|
113
|
+
runEditorHandOff: () => void;
|
|
114
|
+
openCardEdit: (annotationId: string) => void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type IntentOfType<Kind extends Intent["type"]> = Extract<Intent, { type: Kind }>;
|
|
118
|
+
|
|
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
|
+
|
|
130
|
+
if (intent.to === "down") deps.setCursor((current) => Math.min(navigableCount - 1, current + 1));
|
|
131
|
+
else if (intent.to === "up") deps.setCursor((current) => Math.max(0, current - 1));
|
|
132
|
+
else if (intent.to === "top") deps.setCursor(0);
|
|
133
|
+
else deps.setCursor(navigableCount - 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleInboxMove(intent: IntentOfType<"inboxMove">, deps: IntentDispatchDeps): void {
|
|
137
|
+
const navigableCount = deps.inbox?.length ?? 0;
|
|
138
|
+
|
|
139
|
+
deps.setInboxCursor((current) =>
|
|
140
|
+
intent.to === "down" ? Math.min(navigableCount - 1, current + 1) : Math.max(0, current - 1),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleOpenSession(_intent: IntentOfType<"openSession">, deps: IntentDispatchDeps): void {
|
|
145
|
+
const selected = deps.inbox?.[deps.inboxCursor];
|
|
146
|
+
|
|
147
|
+
if (selected) deps.controller.open(selected.id);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleRequestDeleteSession(
|
|
151
|
+
_intent: IntentOfType<"requestDeleteSession">,
|
|
152
|
+
deps: IntentDispatchDeps,
|
|
153
|
+
): void {
|
|
154
|
+
const selected = deps.inbox?.[deps.inboxCursor];
|
|
155
|
+
|
|
156
|
+
if (selected)
|
|
157
|
+
deps.setMode({
|
|
158
|
+
type: "confirmDelete",
|
|
159
|
+
sessionId: selected.id,
|
|
160
|
+
title: selected.artifact.meta.title ?? selected.id,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handleOpenRename(_intent: IntentOfType<"openRename">, deps: IntentDispatchDeps): void {
|
|
165
|
+
const focused = deps.session?.annotations.find(
|
|
166
|
+
(annotation) => annotation.id === deps.focusedAnnotationId,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!focused?.author) {
|
|
170
|
+
deps.controller.setStatus("that is your own note - nothing to rename");
|
|
171
|
+
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
deps.setMode({
|
|
175
|
+
type: "rename",
|
|
176
|
+
authorId: focused.author,
|
|
177
|
+
text: deps.authorNames[focused.author] ?? "",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleConfirmDialog(
|
|
182
|
+
_intent: IntentOfType<"confirmDialog">,
|
|
183
|
+
deps: IntentDispatchDeps,
|
|
184
|
+
): void {
|
|
185
|
+
const { mode, controller } = deps;
|
|
186
|
+
|
|
187
|
+
if (mode.type === "confirmDelete") controller.deleteSession(mode.sessionId);
|
|
188
|
+
else if (mode.type === "rename") deps.renameAuthor(mode.authorId, mode.text.trim());
|
|
189
|
+
else if (mode.type === "nameSelf") controller.setSelfName(mode.text.trim());
|
|
190
|
+
deps.setMode({ type: "normal" });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleStartSpan(_intent: IntentOfType<"startSpan">, deps: IntentDispatchDeps): void {
|
|
194
|
+
const block = deps.display[deps.cursor];
|
|
195
|
+
|
|
196
|
+
if (!block?.work) return;
|
|
197
|
+
const span = startSpan(deps.cursor, displayText(block));
|
|
198
|
+
|
|
199
|
+
if (span) deps.setMode({ type: "span", span });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleSpanKey(intent: IntentOfType<"spanKey">, deps: IntentDispatchDeps): void {
|
|
203
|
+
const { mode } = deps;
|
|
204
|
+
|
|
205
|
+
if (mode.type === "span") {
|
|
206
|
+
const span = spanKey(
|
|
207
|
+
mode.span,
|
|
208
|
+
intent.name,
|
|
209
|
+
displayText(deps.display[mode.span.displayIndex]!),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
deps.setMode({ type: "span", span });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function handleSpanCut(_intent: IntentOfType<"spanCut">, deps: IntentDispatchDeps): void {
|
|
217
|
+
// the block the span sits in, cut whole (partial-span cut is not modeled)
|
|
218
|
+
const { mode } = deps;
|
|
219
|
+
|
|
220
|
+
if (mode.type === "span") {
|
|
221
|
+
deps.controller.cut(mode.span.displayIndex);
|
|
222
|
+
deps.setMode({ type: "normal" });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function handleOpenSpanActions(
|
|
227
|
+
_intent: IntentOfType<"openSpanActions">,
|
|
228
|
+
deps: IntentDispatchDeps,
|
|
229
|
+
): void {
|
|
230
|
+
const { mode } = deps;
|
|
231
|
+
|
|
232
|
+
if (mode.type === "span") deps.setMode({ type: "spanActions", span: mode.span, index: 0 });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleMoveSpanAction(
|
|
236
|
+
intent: IntentOfType<"moveSpanAction">,
|
|
237
|
+
deps: IntentDispatchDeps,
|
|
238
|
+
): void {
|
|
239
|
+
const { mode } = deps;
|
|
240
|
+
|
|
241
|
+
if (mode.type === "spanActions") {
|
|
242
|
+
const index = Math.max(
|
|
243
|
+
0,
|
|
244
|
+
Math.min(deps.quickActions.length - 1, mode.index + intent.direction),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
deps.setMode({ ...mode, index });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function handleCloseSpanActions(
|
|
252
|
+
_intent: IntentOfType<"closeSpanActions">,
|
|
253
|
+
deps: IntentDispatchDeps,
|
|
254
|
+
): void {
|
|
255
|
+
const { mode } = deps;
|
|
256
|
+
|
|
257
|
+
if (mode.type === "spanActions") deps.setMode({ type: "span", span: mode.span });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function handlePickSpanAction(
|
|
261
|
+
intent: IntentOfType<"pickSpanAction">,
|
|
262
|
+
deps: IntentDispatchDeps,
|
|
263
|
+
): void {
|
|
264
|
+
const { mode, session, controller } = deps;
|
|
265
|
+
|
|
266
|
+
if (mode.type !== "spanActions") return;
|
|
267
|
+
const action = deps.quickActions[intent.index ?? mode.index];
|
|
268
|
+
|
|
269
|
+
if (session && action) {
|
|
270
|
+
const body = quickActionBody(action);
|
|
271
|
+
const annotationId = controller.annotate(
|
|
272
|
+
"comment",
|
|
273
|
+
mode.span.displayIndex,
|
|
274
|
+
mode.span.start,
|
|
275
|
+
mode.span.end,
|
|
276
|
+
body,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
280
|
+
}
|
|
281
|
+
deps.setMode({ type: "normal" });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function handleOpenCompose(intent: IntentOfType<"openCompose">, deps: IntentDispatchDeps): void {
|
|
285
|
+
const { mode } = deps;
|
|
286
|
+
|
|
287
|
+
deps.liveInput.current = "";
|
|
288
|
+
if (intent.from === "span" && mode.type === "span") {
|
|
289
|
+
deps.setMode({
|
|
290
|
+
type: "compose",
|
|
291
|
+
kind: intent.kind,
|
|
292
|
+
displayIndex: mode.span.displayIndex,
|
|
293
|
+
start: mode.span.start,
|
|
294
|
+
end: mode.span.end,
|
|
295
|
+
text: "",
|
|
296
|
+
});
|
|
297
|
+
} else if (deps.isDiff) {
|
|
298
|
+
const row = deps.rows[deps.cursor];
|
|
299
|
+
|
|
300
|
+
if (row)
|
|
301
|
+
deps.setMode({
|
|
302
|
+
type: "compose",
|
|
303
|
+
kind: intent.kind,
|
|
304
|
+
displayIndex: deps.cursor,
|
|
305
|
+
start: 0,
|
|
306
|
+
end: row.text.length,
|
|
307
|
+
text: "",
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
// a mouse drag leaves a native selection; it wins over the cursor block
|
|
311
|
+
const native = deps.planSheetRef.current?.readSelection() ?? null;
|
|
312
|
+
|
|
313
|
+
if (native) {
|
|
314
|
+
deps.setMode({ type: "compose", kind: intent.kind, ...native, text: "" });
|
|
315
|
+
} else {
|
|
316
|
+
const block = deps.display[deps.cursor];
|
|
317
|
+
|
|
318
|
+
if (block)
|
|
319
|
+
deps.setMode({
|
|
320
|
+
type: "compose",
|
|
321
|
+
kind: intent.kind,
|
|
322
|
+
displayIndex: deps.cursor,
|
|
323
|
+
start: 0,
|
|
324
|
+
end: displayText(block).length,
|
|
325
|
+
text: "",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function handleOpenSubmit(_intent: IntentOfType<"openSubmit">, deps: IntentDispatchDeps): void {
|
|
332
|
+
const { session } = deps;
|
|
333
|
+
|
|
334
|
+
if (!session) return;
|
|
335
|
+
deps.liveInput.current = "";
|
|
336
|
+
// the confirm card lives in the expanded review rail; a compact or hidden
|
|
337
|
+
// panel would swallow the whole submit flow, so force the rail open (live
|
|
338
|
+
// only - the saved panel preference is left untouched)
|
|
339
|
+
deps.setReviewMode("expanded");
|
|
340
|
+
deps.setRailTab("review");
|
|
341
|
+
deps.setMode({ type: "submit", verdict: defaultVerdict(session), summary: "" });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function handleShare(_intent: IntentOfType<"share">, deps: IntentDispatchDeps): void {
|
|
345
|
+
deps.controller.share();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function handleCut(_intent: IntentOfType<"cut">, deps: IntentDispatchDeps): void {
|
|
349
|
+
deps.controller.cut(deps.cursor);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function handleRejectHunk(_intent: IntentOfType<"rejectHunk">, deps: IntentDispatchDeps): void {
|
|
353
|
+
deps.controller.toggleRejectHunk(deps.cursor);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function handleRejectChange(_intent: IntentOfType<"rejectChange">, deps: IntentDispatchDeps): void {
|
|
357
|
+
deps.controller.toggleRejectChange(deps.cursor);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleRestoreCuration(
|
|
361
|
+
_intent: IntentOfType<"restoreCuration">,
|
|
362
|
+
deps: IntentDispatchDeps,
|
|
363
|
+
): void {
|
|
364
|
+
// undo the selected curated-out item, or the last rejected when none is
|
|
365
|
+
// selected, so a bare `u` reads as "undo my last curation"
|
|
366
|
+
const items = deps.controller.curationItems();
|
|
367
|
+
|
|
368
|
+
if (!items.length) return;
|
|
369
|
+
const targetId = deps.selectedCurationId ?? items[items.length - 1]!.id;
|
|
370
|
+
|
|
371
|
+
deps.controller.restoreCuration(targetId);
|
|
372
|
+
deps.setSelectedCurationId(undefined);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function handleEdit(_intent: IntentOfType<"edit">, deps: IntentDispatchDeps): void {
|
|
376
|
+
deps.runEditorHandOff();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleEditCard(_intent: IntentOfType<"editCard">, deps: IntentDispatchDeps): void {
|
|
380
|
+
if (deps.focusedAnnotationId) deps.openCardEdit(deps.focusedAnnotationId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function handleAnnotationCycle(
|
|
384
|
+
intent: IntentOfType<"nextAnnotation" | "prevAnnotation">,
|
|
385
|
+
deps: IntentDispatchDeps,
|
|
386
|
+
): void {
|
|
387
|
+
// cycle open cards only - addressed ones are out of the rail
|
|
388
|
+
const annotations = (deps.session?.annotations ?? []).filter(
|
|
389
|
+
(annotation) => !isAddressed(annotation),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (!annotations.length) return;
|
|
393
|
+
const focusedIndex = annotations.findIndex(
|
|
394
|
+
(annotation) => annotation.id === deps.focusedAnnotationId,
|
|
395
|
+
);
|
|
396
|
+
const nextIndex =
|
|
397
|
+
focusedIndex === -1
|
|
398
|
+
? 0
|
|
399
|
+
: (focusedIndex + (intent.type === "nextAnnotation" ? 1 : -1) + annotations.length) %
|
|
400
|
+
annotations.length;
|
|
401
|
+
|
|
402
|
+
deps.selectCardFromDocument(annotations[nextIndex]!.id);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function handleWalkStart(_intent: IntentOfType<"walkStart">, deps: IntentDispatchDeps): void {
|
|
406
|
+
deps.controller.walkStart();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function handleWalkForward(_intent: IntentOfType<"walkForward">, deps: IntentDispatchDeps): void {
|
|
410
|
+
deps.controller.walkForward();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function handleWalkBack(_intent: IntentOfType<"walkBack">, deps: IntentDispatchDeps): void {
|
|
414
|
+
deps.controller.walkBack();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function handleWalkLeave(_intent: IntentOfType<"walkLeave">, deps: IntentDispatchDeps): void {
|
|
418
|
+
deps.controller.walkLeave();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function handleRemoveAnnotation(
|
|
422
|
+
_intent: IntentOfType<"removeAnnotation">,
|
|
423
|
+
deps: IntentDispatchDeps,
|
|
424
|
+
): void {
|
|
425
|
+
if (deps.focusedAnnotationId) {
|
|
426
|
+
deps.controller.removeAnnotation(deps.focusedAnnotationId);
|
|
427
|
+
deps.setFocusedAnnotationId(undefined);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function handleDeselect(_intent: IntentOfType<"deselect">, deps: IntentDispatchDeps): void {
|
|
432
|
+
deps.planSheetRef.current?.clearSelection();
|
|
433
|
+
deps.setFocusedAnnotationId(undefined);
|
|
434
|
+
deps.setPulsedAnnotationId(null);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function handleCloseOverlay(_intent: IntentOfType<"closeOverlay">, deps: IntentDispatchDeps): void {
|
|
438
|
+
deps.setMode({ type: "normal" });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDispatchDeps): void {
|
|
442
|
+
const { mode, session, controller } = deps;
|
|
443
|
+
const body = deps.liveInput.current.trim();
|
|
444
|
+
|
|
445
|
+
if (mode.type === "railEdit") {
|
|
446
|
+
if (session && body) controller.updateAnnotation(mode.id, body);
|
|
447
|
+
deps.setMode({ type: "normal" });
|
|
448
|
+
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (mode.type !== "compose") return;
|
|
452
|
+
if (session && body) {
|
|
453
|
+
const annotationId = controller.annotate(
|
|
454
|
+
mode.kind,
|
|
455
|
+
mode.displayIndex,
|
|
456
|
+
mode.start,
|
|
457
|
+
mode.end,
|
|
458
|
+
body,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
462
|
+
}
|
|
463
|
+
deps.setMode({ type: "normal" });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function handleSubmitVerdict(
|
|
467
|
+
_intent: IntentOfType<"submitVerdict">,
|
|
468
|
+
deps: IntentDispatchDeps,
|
|
469
|
+
): void {
|
|
470
|
+
const { mode } = deps;
|
|
471
|
+
|
|
472
|
+
if (mode.type === "submit") deps.controller.submit(mode.verdict, deps.liveInput.current);
|
|
473
|
+
deps.setMode({ type: "normal" });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function handleCycleVerdict(intent: IntentOfType<"cycleVerdict">, deps: IntentDispatchDeps): void {
|
|
477
|
+
const { mode } = deps;
|
|
478
|
+
|
|
479
|
+
if (mode.type !== "submit") return;
|
|
480
|
+
const verdictIndex =
|
|
481
|
+
(VERDICTS.indexOf(mode.verdict) + intent.direction + VERDICTS.length) % VERDICTS.length;
|
|
482
|
+
|
|
483
|
+
deps.setMode({ ...mode, verdict: VERDICTS[verdictIndex]! });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function handleFinishReview(_intent: IntentOfType<"finishReview">, deps: IntentDispatchDeps): void {
|
|
487
|
+
deps.controller.finishReview();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function handleOptInAutoClose(
|
|
491
|
+
_intent: IntentOfType<"optInAutoClose">,
|
|
492
|
+
deps: IntentDispatchDeps,
|
|
493
|
+
): void {
|
|
494
|
+
deps.controller.optInAutoClose();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function handleDismissCompletion(
|
|
498
|
+
_intent: IntentOfType<"dismissCompletion">,
|
|
499
|
+
deps: IntentDispatchDeps,
|
|
500
|
+
): void {
|
|
501
|
+
deps.controller.dismissCompletion();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function handleCycleReviewPanel(
|
|
505
|
+
_intent: IntentOfType<"cycleReviewPanel">,
|
|
506
|
+
deps: IntentDispatchDeps,
|
|
507
|
+
): void {
|
|
508
|
+
const next = cycleReviewPanelMode(deps.reviewMode);
|
|
509
|
+
|
|
510
|
+
deps.setReviewMode(next);
|
|
511
|
+
deps.controller.saveReviewPanel({ mode: next });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function handleResizeReviewPanel(
|
|
515
|
+
intent: IntentOfType<"resizeReviewPanel">,
|
|
516
|
+
deps: IntentDispatchDeps,
|
|
517
|
+
): void {
|
|
518
|
+
if (deps.reviewMode !== "expanded") return;
|
|
519
|
+
const next = resolveReviewWidth(
|
|
520
|
+
deps.reviewWidth + intent.direction * REVIEW_RESIZE_STEP,
|
|
521
|
+
deps.terminalWidth,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
deps.reviewWidthRef.current = next;
|
|
525
|
+
deps.setReviewWidth(next);
|
|
526
|
+
deps.controller.saveReviewPanel({ width: next });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const intentHandlers: {
|
|
530
|
+
[Kind in Intent["type"]]: (intent: IntentOfType<Kind>, deps: IntentDispatchDeps) => void;
|
|
531
|
+
} = {
|
|
532
|
+
exit: handleExit,
|
|
533
|
+
status: handleStatus,
|
|
534
|
+
move: handleMove,
|
|
535
|
+
inboxMove: handleInboxMove,
|
|
536
|
+
openSession: handleOpenSession,
|
|
537
|
+
requestDeleteSession: handleRequestDeleteSession,
|
|
538
|
+
openRename: handleOpenRename,
|
|
539
|
+
confirmDialog: handleConfirmDialog,
|
|
540
|
+
startSpan: handleStartSpan,
|
|
541
|
+
spanKey: handleSpanKey,
|
|
542
|
+
spanCut: handleSpanCut,
|
|
543
|
+
openSpanActions: handleOpenSpanActions,
|
|
544
|
+
moveSpanAction: handleMoveSpanAction,
|
|
545
|
+
closeSpanActions: handleCloseSpanActions,
|
|
546
|
+
pickSpanAction: handlePickSpanAction,
|
|
547
|
+
openCompose: handleOpenCompose,
|
|
548
|
+
openSubmit: handleOpenSubmit,
|
|
549
|
+
share: handleShare,
|
|
550
|
+
cut: handleCut,
|
|
551
|
+
rejectHunk: handleRejectHunk,
|
|
552
|
+
rejectChange: handleRejectChange,
|
|
553
|
+
restoreCuration: handleRestoreCuration,
|
|
554
|
+
edit: handleEdit,
|
|
555
|
+
editCard: handleEditCard,
|
|
556
|
+
nextAnnotation: handleAnnotationCycle,
|
|
557
|
+
prevAnnotation: handleAnnotationCycle,
|
|
558
|
+
walkStart: handleWalkStart,
|
|
559
|
+
walkForward: handleWalkForward,
|
|
560
|
+
walkBack: handleWalkBack,
|
|
561
|
+
walkLeave: handleWalkLeave,
|
|
562
|
+
removeAnnotation: handleRemoveAnnotation,
|
|
563
|
+
deselect: handleDeselect,
|
|
564
|
+
closeOverlay: handleCloseOverlay,
|
|
565
|
+
saveCompose: handleSaveCompose,
|
|
566
|
+
submitVerdict: handleSubmitVerdict,
|
|
567
|
+
cycleVerdict: handleCycleVerdict,
|
|
568
|
+
finishReview: handleFinishReview,
|
|
569
|
+
optInAutoClose: handleOptInAutoClose,
|
|
570
|
+
dismissCompletion: handleDismissCompletion,
|
|
571
|
+
cycleReviewPanel: handleCycleReviewPanel,
|
|
572
|
+
resizeReviewPanel: handleResizeReviewPanel,
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
/** Build the intent handler for one render from its dependency bag. */
|
|
576
|
+
export function createIntentDispatch(deps: IntentDispatchDeps): (intent: Intent) => void {
|
|
577
|
+
return (intent: Intent): void => {
|
|
578
|
+
const handler = intentHandlers[intent.type] as (
|
|
579
|
+
intent: Intent,
|
|
580
|
+
deps: IntentDispatchDeps,
|
|
581
|
+
) => void;
|
|
582
|
+
|
|
583
|
+
handler(intent, deps);
|
|
584
|
+
};
|
|
585
|
+
}
|