@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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 +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -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/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- 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 -364
|
@@ -0,0 +1,690 @@
|
|
|
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, SetStateAction } from "react";
|
|
11
|
+
import { isAddressed, MESSAGE_OUTCOMES, type Thread, type MessageOutcome } 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 "./thread-controller";
|
|
15
|
+
import type { Intent } from "./keymap";
|
|
16
|
+
import type { TreeRow } from "./tree-view";
|
|
17
|
+
import { quickActionBody, type QuickAction } from "./config";
|
|
18
|
+
|
|
19
|
+
/** Which pane of the session tree / review the keyboard grammar is aimed at. */
|
|
20
|
+
export type RailTab = "review" | "tree";
|
|
21
|
+
|
|
22
|
+
/** The one overlay/mode the TUI is in; every compose/submit/edit flow is a variant. */
|
|
23
|
+
export type Mode =
|
|
24
|
+
| { type: "normal" }
|
|
25
|
+
| { type: "span"; span: SpanState }
|
|
26
|
+
| { type: "spanActions"; span: SpanState; index: number }
|
|
27
|
+
| {
|
|
28
|
+
type: "compose";
|
|
29
|
+
kind: "comment";
|
|
30
|
+
displayIndex: number;
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
| { type: "railEdit"; id: string; text: string }
|
|
36
|
+
| { type: "submit"; message: MessageOutcome; summary: string }
|
|
37
|
+
| { type: "confirmDelete"; sessionId: string; title: string }
|
|
38
|
+
| { type: "rename"; authorId: string; text: string }
|
|
39
|
+
| { type: "renameThread"; sessionId: string; text: string }
|
|
40
|
+
| { type: "nameSelf"; text: string }
|
|
41
|
+
| { type: "renameSelf"; text: string }
|
|
42
|
+
| { type: "treePrompt"; ask: TreeAsk; entryId?: string; text: string };
|
|
43
|
+
|
|
44
|
+
/** What a tree prompt asks for: a branch name, a checkpoint name, or the summary a move back leaves. */
|
|
45
|
+
export type TreeAsk = "branch" | "label" | "navigate";
|
|
46
|
+
|
|
47
|
+
/** The marked span for span mode and its quick-actions sub-mode; null otherwise. */
|
|
48
|
+
export function activeSpanState(mode: Mode): SpanState | null {
|
|
49
|
+
return mode.type === "span" || mode.type === "spanActions" ? mode.span : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Everything the reducer reads or writes, supplied fresh each render: the
|
|
54
|
+
* controller and exit hook, the derived reads (view rows, cursor, mode,
|
|
55
|
+
* session), the refs it pokes, the state setters, and the three App-owned
|
|
56
|
+
* selection helpers it composes.
|
|
57
|
+
*/
|
|
58
|
+
export interface IntentDispatchDeps {
|
|
59
|
+
controller: ReviewController;
|
|
60
|
+
onExit?: (code: number) => void;
|
|
61
|
+
|
|
62
|
+
isDiff: boolean;
|
|
63
|
+
display: DisplayBlock[];
|
|
64
|
+
rows: DiffRow[];
|
|
65
|
+
cursor: number;
|
|
66
|
+
inbox: Thread[] | null;
|
|
67
|
+
inboxCursor: number;
|
|
68
|
+
mode: Mode;
|
|
69
|
+
session: Thread | null;
|
|
70
|
+
defaultMessage: MessageOutcome;
|
|
71
|
+
focusedAnnotationId: string | undefined;
|
|
72
|
+
/** The curation item selected for undo, if any. */
|
|
73
|
+
selectedCurationId: string | undefined;
|
|
74
|
+
railTab: RailTab;
|
|
75
|
+
/** The tree row selected in the session tree; the target of go. */
|
|
76
|
+
selectedEntryId: string | undefined;
|
|
77
|
+
/** Planner-local author renames, for seeding the rename prompt. */
|
|
78
|
+
authorNames: Record<string, string>;
|
|
79
|
+
/** Marker-popover quick actions, in list order; picking one inserts a preset comment. */
|
|
80
|
+
quickActions: QuickAction[];
|
|
81
|
+
/** Persist an author rename and update the live overrides (App-owned). */
|
|
82
|
+
renameAuthor: (id: string, name: string) => void;
|
|
83
|
+
/** Rename a thread's title through the daemon. */
|
|
84
|
+
renameThread: (id: string, title: string) => void;
|
|
85
|
+
/** Set the local reviewer's typed display name and persist it (App-owned). */
|
|
86
|
+
setLocalIdentityName: (name: string) => void;
|
|
87
|
+
|
|
88
|
+
liveInput: MutableRefObject<string>;
|
|
89
|
+
|
|
90
|
+
setCursor: Dispatch<SetStateAction<number>>;
|
|
91
|
+
setInboxCursor: Dispatch<SetStateAction<number>>;
|
|
92
|
+
setMode: Dispatch<SetStateAction<Mode>>;
|
|
93
|
+
setRailTab: Dispatch<SetStateAction<RailTab>>;
|
|
94
|
+
setSelectedEntryId: Dispatch<SetStateAction<string | undefined>>;
|
|
95
|
+
setFocusedAnnotationId: Dispatch<SetStateAction<string | undefined>>;
|
|
96
|
+
setSelectedCurationId: Dispatch<SetStateAction<string | undefined>>;
|
|
97
|
+
setPulsedAnnotationId: Dispatch<SetStateAction<string | null>>;
|
|
98
|
+
|
|
99
|
+
selectCardFromDocument: (annotationId: string) => void;
|
|
100
|
+
openBodyEditor: () => void;
|
|
101
|
+
openCardEdit: (annotationId: string) => void;
|
|
102
|
+
/** Flip split/stacked diff and persist it (App-owned); split lays out only when wide/zoomed. */
|
|
103
|
+
toggleDiffView: () => void;
|
|
104
|
+
/** Open the share dialog (App-owned); the dialog owns its own keys and links. */
|
|
105
|
+
openShareDialog: () => void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type IntentOfType<Kind extends Intent["type"]> = Extract<Intent, { type: Kind }>;
|
|
109
|
+
|
|
110
|
+
function handleExit(_intent: IntentOfType<"exit">, deps: IntentDispatchDeps): void {
|
|
111
|
+
deps.onExit?.(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleStatus(intent: IntentOfType<"status">, deps: IntentDispatchDeps): void {
|
|
115
|
+
deps.controller.setStatus(intent.message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A diff cursor rests on code lines only; file and hunk headers are structural dividers. */
|
|
119
|
+
function isCodeRow(row: DiffRow | undefined): boolean {
|
|
120
|
+
return row?.kind === "ctx" || row?.kind === "add" || row?.kind === "del";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A landable diff row: any code line, plus a collapsed file's band (a file row with
|
|
125
|
+
* no body before the next file or the end) so the cursor can reach it to expand it.
|
|
126
|
+
*/
|
|
127
|
+
function isLandableRow(rows: DiffRow[], index: number): boolean {
|
|
128
|
+
const row = rows[index];
|
|
129
|
+
|
|
130
|
+
if (isCodeRow(row)) return true;
|
|
131
|
+
if (row?.kind !== "file") return false;
|
|
132
|
+
const next = rows[index + 1];
|
|
133
|
+
|
|
134
|
+
return next === undefined || next.kind === "file";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The next landable index in the move direction, skipping expanded headers; stays put at an edge. */
|
|
138
|
+
function nextCodeRowIndex(rows: DiffRow[], from: number, to: IntentOfType<"move">["to"]): number {
|
|
139
|
+
if (to === "top") {
|
|
140
|
+
for (let index = 0; index < rows.length; index++) if (isLandableRow(rows, index)) return index;
|
|
141
|
+
|
|
142
|
+
return from;
|
|
143
|
+
}
|
|
144
|
+
if (to === "bottom") {
|
|
145
|
+
for (let index = rows.length - 1; index >= 0; index--) {
|
|
146
|
+
if (isLandableRow(rows, index)) return index;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return from;
|
|
150
|
+
}
|
|
151
|
+
const step = to === "down" ? 1 : -1;
|
|
152
|
+
|
|
153
|
+
for (let index = from + step; index >= 0 && index < rows.length; index += step) {
|
|
154
|
+
if (isLandableRow(rows, index)) return index;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return from;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleMove(intent: IntentOfType<"move">, deps: IntentDispatchDeps): void {
|
|
161
|
+
if (deps.isDiff) {
|
|
162
|
+
deps.setCursor((current) => nextCodeRowIndex(deps.rows, current, intent.to));
|
|
163
|
+
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const navigableCount = deps.display.length;
|
|
167
|
+
|
|
168
|
+
if (intent.to === "down") deps.setCursor((current) => Math.min(navigableCount - 1, current + 1));
|
|
169
|
+
else if (intent.to === "up") deps.setCursor((current) => Math.max(0, current - 1));
|
|
170
|
+
else if (intent.to === "top") deps.setCursor(0);
|
|
171
|
+
else deps.setCursor(navigableCount - 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handleInboxMove(intent: IntentOfType<"inboxMove">, deps: IntentDispatchDeps): void {
|
|
175
|
+
const navigableCount = deps.inbox?.length ?? 0;
|
|
176
|
+
|
|
177
|
+
deps.setInboxCursor((current) =>
|
|
178
|
+
intent.to === "down" ? Math.min(navigableCount - 1, current + 1) : Math.max(0, current - 1),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleOpenSession(_intent: IntentOfType<"openSession">, deps: IntentDispatchDeps): void {
|
|
183
|
+
const selected = deps.inbox?.[deps.inboxCursor];
|
|
184
|
+
|
|
185
|
+
if (selected) deps.controller.open(selected.id);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleRequestDeleteSession(
|
|
189
|
+
_intent: IntentOfType<"requestDeleteSession">,
|
|
190
|
+
deps: IntentDispatchDeps,
|
|
191
|
+
): void {
|
|
192
|
+
const selected = deps.inbox?.[deps.inboxCursor];
|
|
193
|
+
|
|
194
|
+
if (selected)
|
|
195
|
+
deps.setMode({
|
|
196
|
+
type: "confirmDelete",
|
|
197
|
+
sessionId: selected.id,
|
|
198
|
+
title: selected.artifact.meta.title ?? selected.id,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleOpenRename(_intent: IntentOfType<"openRename">, deps: IntentDispatchDeps): void {
|
|
203
|
+
const focused = deps.session?.annotations.find(
|
|
204
|
+
(annotation) => annotation.id === deps.focusedAnnotationId,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (!focused?.author) {
|
|
208
|
+
deps.controller.setStatus("that is your own note - nothing to rename");
|
|
209
|
+
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
deps.setMode({
|
|
213
|
+
type: "rename",
|
|
214
|
+
authorId: focused.author,
|
|
215
|
+
text: deps.authorNames[focused.author] ?? "",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleConfirmDialog(
|
|
220
|
+
_intent: IntentOfType<"confirmDialog">,
|
|
221
|
+
deps: IntentDispatchDeps,
|
|
222
|
+
): void {
|
|
223
|
+
const { mode, controller } = deps;
|
|
224
|
+
|
|
225
|
+
if (mode.type === "confirmDelete") controller.deleteSession(mode.sessionId);
|
|
226
|
+
else if (mode.type === "rename") deps.renameAuthor(mode.authorId, mode.text.trim());
|
|
227
|
+
else if (mode.type === "renameThread") deps.renameThread(mode.sessionId, mode.text.trim());
|
|
228
|
+
else if (mode.type === "nameSelf") controller.setSelfName(mode.text.trim());
|
|
229
|
+
else if (mode.type === "renameSelf") deps.setLocalIdentityName(mode.text.trim());
|
|
230
|
+
else if (mode.type === "treePrompt") confirmTreePrompt(mode, deps);
|
|
231
|
+
deps.setMode({ type: "normal" });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleStartSpan(_intent: IntentOfType<"startSpan">, deps: IntentDispatchDeps): void {
|
|
235
|
+
const block = deps.display[deps.cursor];
|
|
236
|
+
|
|
237
|
+
if (!block?.work) return;
|
|
238
|
+
const span = startSpan(deps.cursor, displayText(block));
|
|
239
|
+
|
|
240
|
+
if (span) deps.setMode({ type: "span", span });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function handleSpanKey(intent: IntentOfType<"spanKey">, deps: IntentDispatchDeps): void {
|
|
244
|
+
const { mode } = deps;
|
|
245
|
+
|
|
246
|
+
if (mode.type === "span") {
|
|
247
|
+
const span = spanKey(
|
|
248
|
+
mode.span,
|
|
249
|
+
intent.name,
|
|
250
|
+
displayText(deps.display[mode.span.displayIndex]!),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
deps.setMode({ type: "span", span });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function handleSpanCut(_intent: IntentOfType<"spanCut">, deps: IntentDispatchDeps): void {
|
|
258
|
+
const { mode } = deps;
|
|
259
|
+
|
|
260
|
+
if (mode.type === "span") {
|
|
261
|
+
deps.controller.cut(mode.span.displayIndex, mode.span.start, mode.span.end);
|
|
262
|
+
deps.setMode({ type: "normal" });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function handleOpenSpanActions(
|
|
267
|
+
_intent: IntentOfType<"openSpanActions">,
|
|
268
|
+
deps: IntentDispatchDeps,
|
|
269
|
+
): void {
|
|
270
|
+
const { mode } = deps;
|
|
271
|
+
|
|
272
|
+
if (mode.type === "span") deps.setMode({ type: "spanActions", span: mode.span, index: 0 });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function handleMoveSpanAction(
|
|
276
|
+
intent: IntentOfType<"moveSpanAction">,
|
|
277
|
+
deps: IntentDispatchDeps,
|
|
278
|
+
): void {
|
|
279
|
+
const { mode } = deps;
|
|
280
|
+
|
|
281
|
+
if (mode.type === "spanActions") {
|
|
282
|
+
const index = Math.max(
|
|
283
|
+
0,
|
|
284
|
+
Math.min(deps.quickActions.length - 1, mode.index + intent.direction),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
deps.setMode({ ...mode, index });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function handleCloseSpanActions(
|
|
292
|
+
_intent: IntentOfType<"closeSpanActions">,
|
|
293
|
+
deps: IntentDispatchDeps,
|
|
294
|
+
): void {
|
|
295
|
+
const { mode } = deps;
|
|
296
|
+
|
|
297
|
+
if (mode.type === "spanActions") deps.setMode({ type: "span", span: mode.span });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handlePickSpanAction(
|
|
301
|
+
intent: IntentOfType<"pickSpanAction">,
|
|
302
|
+
deps: IntentDispatchDeps,
|
|
303
|
+
): void {
|
|
304
|
+
const { mode, session, controller } = deps;
|
|
305
|
+
|
|
306
|
+
if (mode.type !== "spanActions") return;
|
|
307
|
+
const action = deps.quickActions[intent.index ?? mode.index];
|
|
308
|
+
|
|
309
|
+
if (session && action) {
|
|
310
|
+
const body = quickActionBody(action);
|
|
311
|
+
const annotationId = controller.annotate(
|
|
312
|
+
"comment",
|
|
313
|
+
mode.span.displayIndex,
|
|
314
|
+
mode.span.start,
|
|
315
|
+
mode.span.end,
|
|
316
|
+
body,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
320
|
+
}
|
|
321
|
+
deps.setMode({ type: "normal" });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function handleOpenCompose(intent: IntentOfType<"openCompose">, deps: IntentDispatchDeps): void {
|
|
325
|
+
const { mode } = deps;
|
|
326
|
+
|
|
327
|
+
deps.liveInput.current = "";
|
|
328
|
+
if (intent.from === "span" && mode.type === "span") {
|
|
329
|
+
deps.setMode({
|
|
330
|
+
type: "compose",
|
|
331
|
+
kind: intent.kind,
|
|
332
|
+
displayIndex: mode.span.displayIndex,
|
|
333
|
+
start: mode.span.start,
|
|
334
|
+
end: mode.span.end,
|
|
335
|
+
text: "",
|
|
336
|
+
});
|
|
337
|
+
} else if (deps.isDiff) {
|
|
338
|
+
const row = deps.rows[deps.cursor];
|
|
339
|
+
|
|
340
|
+
if (row)
|
|
341
|
+
deps.setMode({
|
|
342
|
+
type: "compose",
|
|
343
|
+
kind: intent.kind,
|
|
344
|
+
displayIndex: deps.cursor,
|
|
345
|
+
start: 0,
|
|
346
|
+
end: row.text.length,
|
|
347
|
+
text: "",
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
const block = deps.display[deps.cursor];
|
|
351
|
+
|
|
352
|
+
if (block)
|
|
353
|
+
deps.setMode({
|
|
354
|
+
type: "compose",
|
|
355
|
+
kind: intent.kind,
|
|
356
|
+
displayIndex: deps.cursor,
|
|
357
|
+
start: 0,
|
|
358
|
+
end: displayText(block).length,
|
|
359
|
+
text: "",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function handleOpenSubmit(_intent: IntentOfType<"openSubmit">, deps: IntentDispatchDeps): void {
|
|
365
|
+
const { session } = deps;
|
|
366
|
+
|
|
367
|
+
if (!session) return;
|
|
368
|
+
deps.liveInput.current = "";
|
|
369
|
+
deps.setMode({ type: "submit", message: deps.defaultMessage, summary: "" });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// share opens the share dialog; it owns its own keys, links list, and publish wizard
|
|
373
|
+
function handleShare(_intent: IntentOfType<"share">, deps: IntentDispatchDeps): void {
|
|
374
|
+
deps.openShareDialog();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function handleCut(_intent: IntentOfType<"cut">, deps: IntentDispatchDeps): void {
|
|
378
|
+
deps.controller.cut(deps.cursor);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function handleRejectHunk(_intent: IntentOfType<"rejectHunk">, deps: IntentDispatchDeps): void {
|
|
382
|
+
deps.controller.toggleRejectHunk(deps.cursor);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function handleRejectChange(_intent: IntentOfType<"rejectChange">, deps: IntentDispatchDeps): void {
|
|
386
|
+
deps.controller.toggleRejectChange(deps.cursor);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function handleFoldFile(_intent: IntentOfType<"foldFile">, deps: IntentDispatchDeps): void {
|
|
390
|
+
const file = deps.rows[deps.cursor]?.file;
|
|
391
|
+
|
|
392
|
+
if (!file || deps.controller.isFileCollapsed(file)) return;
|
|
393
|
+
deps.controller.setFileCollapsed(file, true);
|
|
394
|
+
// the file band is unchanged in index by its own collapse, so land the cursor on it
|
|
395
|
+
const headerIndex = deps.controller
|
|
396
|
+
.rows()
|
|
397
|
+
.findIndex((row) => row.kind === "file" && row.file === file);
|
|
398
|
+
|
|
399
|
+
if (headerIndex >= 0) deps.setCursor(headerIndex);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function handleUnfoldFile(_intent: IntentOfType<"unfoldFile">, deps: IntentDispatchDeps): void {
|
|
403
|
+
const file = deps.rows[deps.cursor]?.file;
|
|
404
|
+
|
|
405
|
+
if (!file || !deps.controller.isFileCollapsed(file)) return;
|
|
406
|
+
deps.controller.setFileCollapsed(file, false);
|
|
407
|
+
// drop the cursor onto the file's first code line now that its body is back
|
|
408
|
+
const firstCode = deps.controller.rows().findIndex((row) => row.file === file && isCodeRow(row));
|
|
409
|
+
|
|
410
|
+
if (firstCode >= 0) deps.setCursor(firstCode);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function handleToggleDiffView(
|
|
414
|
+
_intent: IntentOfType<"toggleDiffView">,
|
|
415
|
+
deps: IntentDispatchDeps,
|
|
416
|
+
): void {
|
|
417
|
+
deps.toggleDiffView();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function handleRestoreCuration(
|
|
421
|
+
_intent: IntentOfType<"restoreCuration">,
|
|
422
|
+
deps: IntentDispatchDeps,
|
|
423
|
+
): void {
|
|
424
|
+
// undo the selected curated-out item, or the last rejected when none is
|
|
425
|
+
// selected, so a bare `u` reads as "undo my last curation"
|
|
426
|
+
const items = deps.controller.curationItems();
|
|
427
|
+
|
|
428
|
+
if (!items.length) return;
|
|
429
|
+
const targetId = deps.selectedCurationId ?? items[items.length - 1]!.id;
|
|
430
|
+
|
|
431
|
+
deps.controller.restoreCuration(targetId);
|
|
432
|
+
deps.setSelectedCurationId(undefined);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function handleEdit(_intent: IntentOfType<"edit">, deps: IntentDispatchDeps): void {
|
|
436
|
+
deps.openBodyEditor();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function handleEditCard(_intent: IntentOfType<"editCard">, deps: IntentDispatchDeps): void {
|
|
440
|
+
if (deps.focusedAnnotationId) deps.openCardEdit(deps.focusedAnnotationId);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function handleAnnotationCycle(
|
|
444
|
+
intent: IntentOfType<"nextAnnotation" | "prevAnnotation">,
|
|
445
|
+
deps: IntentDispatchDeps,
|
|
446
|
+
): void {
|
|
447
|
+
// cycle open cards only - addressed ones are out of the rail
|
|
448
|
+
const annotations = (deps.session?.annotations ?? []).filter(
|
|
449
|
+
(annotation) => !isAddressed(annotation),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
if (!annotations.length) return;
|
|
453
|
+
const focusedIndex = annotations.findIndex(
|
|
454
|
+
(annotation) => annotation.id === deps.focusedAnnotationId,
|
|
455
|
+
);
|
|
456
|
+
const nextIndex =
|
|
457
|
+
focusedIndex === -1
|
|
458
|
+
? 0
|
|
459
|
+
: (focusedIndex + (intent.type === "nextAnnotation" ? 1 : -1) + annotations.length) %
|
|
460
|
+
annotations.length;
|
|
461
|
+
|
|
462
|
+
deps.selectCardFromDocument(annotations[nextIndex]!.id);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function handleWalkStart(_intent: IntentOfType<"walkStart">, deps: IntentDispatchDeps): void {
|
|
466
|
+
deps.controller.walkStart();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function handleWalkForward(_intent: IntentOfType<"walkForward">, deps: IntentDispatchDeps): void {
|
|
470
|
+
deps.controller.walkForward();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function handleWalkBack(_intent: IntentOfType<"walkBack">, deps: IntentDispatchDeps): void {
|
|
474
|
+
deps.controller.walkBack();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function handleWalkLeave(_intent: IntentOfType<"walkLeave">, deps: IntentDispatchDeps): void {
|
|
478
|
+
deps.controller.walkLeave();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function handleRemoveAnnotation(
|
|
482
|
+
_intent: IntentOfType<"removeAnnotation">,
|
|
483
|
+
deps: IntentDispatchDeps,
|
|
484
|
+
): void {
|
|
485
|
+
if (deps.focusedAnnotationId) {
|
|
486
|
+
deps.controller.removeAnnotation(deps.focusedAnnotationId);
|
|
487
|
+
deps.setFocusedAnnotationId(undefined);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function handleDeselect(_intent: IntentOfType<"deselect">, deps: IntentDispatchDeps): void {
|
|
492
|
+
deps.setFocusedAnnotationId(undefined);
|
|
493
|
+
deps.setPulsedAnnotationId(null);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function handleCloseOverlay(_intent: IntentOfType<"closeOverlay">, deps: IntentDispatchDeps): void {
|
|
497
|
+
deps.setMode({ type: "normal" });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDispatchDeps): void {
|
|
501
|
+
const { mode, session, controller } = deps;
|
|
502
|
+
const body = deps.liveInput.current.trim();
|
|
503
|
+
|
|
504
|
+
if (mode.type === "railEdit") {
|
|
505
|
+
if (session && body) controller.updateAnnotation(mode.id, body);
|
|
506
|
+
deps.setMode({ type: "normal" });
|
|
507
|
+
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (mode.type !== "compose") return;
|
|
511
|
+
if (session && body) {
|
|
512
|
+
const annotationId = controller.annotate(
|
|
513
|
+
mode.kind,
|
|
514
|
+
mode.displayIndex,
|
|
515
|
+
mode.start,
|
|
516
|
+
mode.end,
|
|
517
|
+
body,
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
521
|
+
}
|
|
522
|
+
deps.setMode({ type: "normal" });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function handleSubmitMessage(
|
|
526
|
+
_intent: IntentOfType<"submitMessage">,
|
|
527
|
+
deps: IntentDispatchDeps,
|
|
528
|
+
): void {
|
|
529
|
+
const { mode } = deps;
|
|
530
|
+
|
|
531
|
+
if (mode.type === "submit") deps.controller.submit(mode.message, deps.liveInput.current);
|
|
532
|
+
deps.setMode({ type: "normal" });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function handleCycleMessage(intent: IntentOfType<"cycleMessage">, deps: IntentDispatchDeps): void {
|
|
536
|
+
const { mode } = deps;
|
|
537
|
+
|
|
538
|
+
if (mode.type !== "submit") return;
|
|
539
|
+
const messageIndex =
|
|
540
|
+
(MESSAGE_OUTCOMES.indexOf(mode.message) + intent.direction + MESSAGE_OUTCOMES.length) %
|
|
541
|
+
MESSAGE_OUTCOMES.length;
|
|
542
|
+
|
|
543
|
+
deps.setMode({ ...mode, message: MESSAGE_OUTCOMES[messageIndex]! });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function handleFinishReview(_intent: IntentOfType<"finishReview">, deps: IntentDispatchDeps): void {
|
|
547
|
+
deps.controller.finishReview();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function handleOptInAutoClose(
|
|
551
|
+
_intent: IntentOfType<"optInAutoClose">,
|
|
552
|
+
deps: IntentDispatchDeps,
|
|
553
|
+
): void {
|
|
554
|
+
deps.controller.optInAutoClose();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function handleDismissCompletion(
|
|
558
|
+
_intent: IntentOfType<"dismissCompletion">,
|
|
559
|
+
deps: IntentDispatchDeps,
|
|
560
|
+
): void {
|
|
561
|
+
deps.controller.dismissCompletion();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function confirmTreePrompt(
|
|
565
|
+
mode: Extract<Mode, { type: "treePrompt" }>,
|
|
566
|
+
deps: IntentDispatchDeps,
|
|
567
|
+
): void {
|
|
568
|
+
if (mode.ask === "branch") deps.controller.branch(mode.text);
|
|
569
|
+
else if (mode.ask === "label") deps.controller.labelTip(mode.text);
|
|
570
|
+
else if (mode.entryId !== undefined) deps.controller.goToEntry(mode.entryId, mode.text.trim());
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function handleToggleTree(_intent: IntentOfType<"toggleTree">, deps: IntentDispatchDeps): void {
|
|
574
|
+
deps.setRailTab(deps.railTab === "tree" ? "review" : "tree");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The tree rows in drawing order; the selection falls back to the current tip. */
|
|
578
|
+
function selectedTreeIndex(deps: IntentDispatchDeps, rows: TreeRow[]): number {
|
|
579
|
+
const byId = rows.findIndex((row) => row.entryId === deps.selectedEntryId);
|
|
580
|
+
|
|
581
|
+
return byId === -1 ? rows.findIndex((row) => row.isCurrentTip) : byId;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function handleTreeMove(intent: IntentOfType<"treeMove">, deps: IntentDispatchDeps): void {
|
|
585
|
+
const rows = deps.controller.treeRows();
|
|
586
|
+
|
|
587
|
+
if (rows.length === 0) return;
|
|
588
|
+
const current = selectedTreeIndex(deps, rows);
|
|
589
|
+
const next = Math.min(rows.length - 1, Math.max(0, current + intent.direction));
|
|
590
|
+
|
|
591
|
+
deps.setSelectedEntryId(rows[next]!.entryId);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function handleTreeGo(_intent: IntentOfType<"treeGo">, deps: IntentDispatchDeps): void {
|
|
595
|
+
const rows = deps.controller.treeRows();
|
|
596
|
+
const row = rows[selectedTreeIndex(deps, rows)];
|
|
597
|
+
|
|
598
|
+
if (!row) return deps.controller.setStatus("select an entry in the tree first");
|
|
599
|
+
if (row.isCurrentTip) return deps.controller.setStatus("already at the tip");
|
|
600
|
+
// a switch needs no summary; a move back may leave one, so it asks
|
|
601
|
+
if (row.tips.length > 0) return deps.controller.goToEntry(row.entryId);
|
|
602
|
+
deps.setMode({ type: "treePrompt", ask: "navigate", entryId: row.entryId, text: "" });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function handleTreeBranch(_intent: IntentOfType<"treeBranch">, deps: IntentDispatchDeps): void {
|
|
606
|
+
deps.setMode({ type: "treePrompt", ask: "branch", text: "" });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function handleTreeLabel(_intent: IntentOfType<"treeLabel">, deps: IntentDispatchDeps): void {
|
|
610
|
+
deps.setMode({ type: "treePrompt", ask: "label", text: "" });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function handleTreeFork(_intent: IntentOfType<"treeFork">, deps: IntentDispatchDeps): void {
|
|
614
|
+
deps.controller.fork();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function handleTreeForkShare(
|
|
618
|
+
_intent: IntentOfType<"treeForkShare">,
|
|
619
|
+
deps: IntentDispatchDeps,
|
|
620
|
+
): void {
|
|
621
|
+
deps.controller.forkAndShare();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
type IntentHandlers = {
|
|
625
|
+
[Kind in Intent["type"]]: (intent: IntentOfType<Kind>, deps: IntentDispatchDeps) => void;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const intentHandlers: IntentHandlers = {
|
|
629
|
+
exit: handleExit,
|
|
630
|
+
status: handleStatus,
|
|
631
|
+
move: handleMove,
|
|
632
|
+
inboxMove: handleInboxMove,
|
|
633
|
+
openSession: handleOpenSession,
|
|
634
|
+
requestDeleteSession: handleRequestDeleteSession,
|
|
635
|
+
openRename: handleOpenRename,
|
|
636
|
+
confirmDialog: handleConfirmDialog,
|
|
637
|
+
startSpan: handleStartSpan,
|
|
638
|
+
spanKey: handleSpanKey,
|
|
639
|
+
spanCut: handleSpanCut,
|
|
640
|
+
openSpanActions: handleOpenSpanActions,
|
|
641
|
+
moveSpanAction: handleMoveSpanAction,
|
|
642
|
+
closeSpanActions: handleCloseSpanActions,
|
|
643
|
+
pickSpanAction: handlePickSpanAction,
|
|
644
|
+
openCompose: handleOpenCompose,
|
|
645
|
+
openSubmit: handleOpenSubmit,
|
|
646
|
+
share: handleShare,
|
|
647
|
+
cut: handleCut,
|
|
648
|
+
rejectHunk: handleRejectHunk,
|
|
649
|
+
rejectChange: handleRejectChange,
|
|
650
|
+
foldFile: handleFoldFile,
|
|
651
|
+
unfoldFile: handleUnfoldFile,
|
|
652
|
+
toggleDiffView: handleToggleDiffView,
|
|
653
|
+
restoreCuration: handleRestoreCuration,
|
|
654
|
+
edit: handleEdit,
|
|
655
|
+
editCard: handleEditCard,
|
|
656
|
+
nextAnnotation: handleAnnotationCycle,
|
|
657
|
+
prevAnnotation: handleAnnotationCycle,
|
|
658
|
+
walkStart: handleWalkStart,
|
|
659
|
+
walkForward: handleWalkForward,
|
|
660
|
+
walkBack: handleWalkBack,
|
|
661
|
+
walkLeave: handleWalkLeave,
|
|
662
|
+
removeAnnotation: handleRemoveAnnotation,
|
|
663
|
+
deselect: handleDeselect,
|
|
664
|
+
closeOverlay: handleCloseOverlay,
|
|
665
|
+
saveCompose: handleSaveCompose,
|
|
666
|
+
submitMessage: handleSubmitMessage,
|
|
667
|
+
cycleMessage: handleCycleMessage,
|
|
668
|
+
finishReview: handleFinishReview,
|
|
669
|
+
optInAutoClose: handleOptInAutoClose,
|
|
670
|
+
dismissCompletion: handleDismissCompletion,
|
|
671
|
+
toggleTree: handleToggleTree,
|
|
672
|
+
treeMove: handleTreeMove,
|
|
673
|
+
treeGo: handleTreeGo,
|
|
674
|
+
treeBranch: handleTreeBranch,
|
|
675
|
+
treeLabel: handleTreeLabel,
|
|
676
|
+
treeFork: handleTreeFork,
|
|
677
|
+
treeForkShare: handleTreeForkShare,
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
function dispatchIntent<Kind extends Intent["type"]>(
|
|
681
|
+
intent: IntentOfType<Kind>,
|
|
682
|
+
deps: IntentDispatchDeps,
|
|
683
|
+
): void {
|
|
684
|
+
intentHandlers[intent.type](intent, deps);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** Build the intent handler for one render from its dependency bag. */
|
|
688
|
+
export function createIntentDispatch(deps: IntentDispatchDeps): (intent: Intent) => void {
|
|
689
|
+
return (intent: Intent): void => dispatchIntent(intent, deps);
|
|
690
|
+
}
|