@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81
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 +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -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 +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -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 +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -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 +574 -0
- package/src/app-view-model.ts +162 -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/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.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -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 +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -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 +876 -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/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 +59 -0
- package/src/components/ProjectTreeView.tsx +111 -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 +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -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 +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -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 +56 -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 +517 -33
- package/src/config.ts +467 -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 +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -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 +64 -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 +78 -13
- 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 +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -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 +1893 -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 +148 -0
- package/src/use-annotation-surface.tsx +915 -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-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -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 +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- 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 -312
package/src/keymap.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The keyboard grammar as a pure reducer
|
|
2
|
+
* The keyboard grammar as a pure reducer: view state in, intents out.
|
|
3
3
|
* App builds a KeyState per key event, calls reduceKey, and dispatches the
|
|
4
|
-
* intents to controller
|
|
4
|
+
* intents to controller primitives and view-state setters - no branch bodies live
|
|
5
5
|
* in the key handler. Diff and plan reviews share one path for annotation
|
|
6
6
|
* navigation, deletion, and submit; the read-only rule is one gate here.
|
|
7
7
|
*/
|
|
@@ -9,42 +9,92 @@
|
|
|
9
9
|
import { actionFor } from "./config";
|
|
10
10
|
|
|
11
11
|
export type Intent =
|
|
12
|
-
| {
|
|
13
|
-
| {
|
|
14
|
-
| {
|
|
15
|
-
| {
|
|
16
|
-
| {
|
|
17
|
-
| {
|
|
18
|
-
| {
|
|
19
|
-
| {
|
|
20
|
-
| {
|
|
21
|
-
| {
|
|
22
|
-
| {
|
|
23
|
-
| {
|
|
24
|
-
| {
|
|
25
|
-
| {
|
|
26
|
-
| {
|
|
27
|
-
| {
|
|
28
|
-
| {
|
|
29
|
-
| {
|
|
30
|
-
| {
|
|
31
|
-
| {
|
|
32
|
-
| {
|
|
12
|
+
| { type: "exit" }
|
|
13
|
+
| { type: "status"; message: string }
|
|
14
|
+
| { type: "move"; to: "down" | "up" | "top" | "bottom" }
|
|
15
|
+
| { type: "inboxMove"; to: "down" | "up" }
|
|
16
|
+
| { type: "openSession" }
|
|
17
|
+
| { type: "requestDeleteSession" }
|
|
18
|
+
| { type: "openRename" }
|
|
19
|
+
| { type: "confirmDialog" }
|
|
20
|
+
| { type: "startSpan" }
|
|
21
|
+
| { type: "spanKey"; name: string }
|
|
22
|
+
| { type: "spanCut" }
|
|
23
|
+
| { type: "openSpanActions" }
|
|
24
|
+
| { type: "moveSpanAction"; direction: -1 | 1 }
|
|
25
|
+
| { type: "pickSpanAction"; index?: number }
|
|
26
|
+
| { type: "closeSpanActions" }
|
|
27
|
+
| { type: "openCompose"; kind: "comment"; from: "cursor" | "span" }
|
|
28
|
+
| { type: "openSubmit" }
|
|
29
|
+
| { type: "share" }
|
|
30
|
+
| { type: "cut" }
|
|
31
|
+
| { type: "edit" }
|
|
32
|
+
| { type: "editCard" }
|
|
33
|
+
| { type: "rejectHunk" }
|
|
34
|
+
| { type: "rejectChange" }
|
|
35
|
+
| { type: "restoreCuration" }
|
|
36
|
+
| { type: "foldFile" }
|
|
37
|
+
| { type: "unfoldFile" }
|
|
38
|
+
| { type: "toggleDiffView" }
|
|
39
|
+
| { type: "nextAnnotation" }
|
|
40
|
+
| { type: "prevAnnotation" }
|
|
41
|
+
| { type: "walkStart" }
|
|
42
|
+
| { type: "walkForward" }
|
|
43
|
+
| { type: "walkBack" }
|
|
44
|
+
| { type: "walkLeave" }
|
|
45
|
+
| { type: "removeAnnotation" }
|
|
46
|
+
| { type: "deselect" }
|
|
47
|
+
| { type: "closeOverlay" }
|
|
48
|
+
| { type: "saveCompose" }
|
|
49
|
+
| { type: "submitMessage" }
|
|
50
|
+
| { type: "cycleMessage"; direction: -1 | 1 }
|
|
51
|
+
| { type: "finishReview" }
|
|
52
|
+
| { type: "optInAutoClose" }
|
|
53
|
+
| { type: "dismissCompletion" }
|
|
54
|
+
| { type: "toggleTree" }
|
|
55
|
+
| { type: "treeMove"; direction: -1 | 1 }
|
|
56
|
+
| { type: "treeGo" }
|
|
57
|
+
| { type: "treeBranch" }
|
|
58
|
+
| { type: "treeLabel" }
|
|
59
|
+
| { type: "treeFork" }
|
|
60
|
+
| { type: "treeForkShare" };
|
|
33
61
|
|
|
34
62
|
export interface KeyInput {
|
|
35
63
|
name: string;
|
|
36
64
|
shift: boolean;
|
|
65
|
+
/** Option/Alt arrives as `meta` in this terminal stack (never as a raw alt). */
|
|
66
|
+
meta?: boolean;
|
|
37
67
|
}
|
|
38
68
|
|
|
39
69
|
export interface KeyState {
|
|
40
70
|
/** Loaded keymap (config.ts): action -> key combos. */
|
|
41
71
|
keys: Record<string, string[]>;
|
|
42
72
|
readOnly: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Owner-only primitives a share collaborator lacks (undefined = owner, allowed).
|
|
75
|
+
* A collaborator annotates but cannot edit the plan (cut / $EDITOR runs on
|
|
76
|
+
* the gateway) or submit an agent message (there is no agent on a share).
|
|
77
|
+
*/
|
|
78
|
+
canEditPlan?: boolean;
|
|
79
|
+
canSubmitMessage?: boolean;
|
|
80
|
+
/** Owner-only: publish the plan as a share. A collaborator never re-shares. */
|
|
81
|
+
canShare?: boolean;
|
|
43
82
|
/** Layer that owns keys before the grammar runs. */
|
|
44
|
-
overlay:
|
|
83
|
+
overlay:
|
|
84
|
+
| "none"
|
|
85
|
+
| "walk"
|
|
86
|
+
| "compose"
|
|
87
|
+
| "submit"
|
|
88
|
+
| "confirm"
|
|
89
|
+
| "prompt"
|
|
90
|
+
| "spanActions"
|
|
91
|
+
| "completion-prompt"
|
|
92
|
+
| "completion-counting";
|
|
45
93
|
view: "inbox" | "plan" | "diff";
|
|
46
94
|
/** Plan-only span selection sub-mode. */
|
|
47
95
|
spanMode: boolean;
|
|
96
|
+
/** The walk cursor sits on the end card - return offers the submit action. */
|
|
97
|
+
walkAtEnd: boolean;
|
|
48
98
|
resolved: boolean;
|
|
49
99
|
hasInboxItems: boolean;
|
|
50
100
|
annotationCount: number;
|
|
@@ -53,102 +103,280 @@ export interface KeyState {
|
|
|
53
103
|
cursorAnnotatable: boolean;
|
|
54
104
|
}
|
|
55
105
|
|
|
56
|
-
/**
|
|
57
|
-
const MUTATING_ACTIONS = new Set([
|
|
106
|
+
/** Primitives that write session state; an observer never reaches their handlers. */
|
|
107
|
+
const MUTATING_ACTIONS = new Set([
|
|
108
|
+
"comment",
|
|
109
|
+
"cut",
|
|
110
|
+
"edit",
|
|
111
|
+
"delete_annotation",
|
|
112
|
+
"submit",
|
|
113
|
+
"walk",
|
|
114
|
+
"share",
|
|
115
|
+
"reject_hunk",
|
|
116
|
+
"restore_curation",
|
|
117
|
+
]);
|
|
58
118
|
|
|
59
|
-
const SPAN_KEYS = ["l", "h", "w", "b", "$", "0"];
|
|
119
|
+
const SPAN_KEYS = new Set(["l", "h", "w", "b", "$", "0"]);
|
|
60
120
|
|
|
61
|
-
function status(
|
|
62
|
-
return [{
|
|
121
|
+
function status(message: string): Intent[] {
|
|
122
|
+
return [{ type: "status", message }];
|
|
63
123
|
}
|
|
64
124
|
|
|
65
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Reduce one key event to intents. `resolvedAction` is the binding-layer
|
|
127
|
+
* resolution (key-bindings.ts over @opentui/keymap) when the caller has one;
|
|
128
|
+
* without it the reducer falls back to the plain reverse lookup, so the
|
|
129
|
+
* grammar stays testable as a pure function.
|
|
130
|
+
*/
|
|
131
|
+
export function reduceKey(state: KeyState, key: KeyInput, resolvedAction?: string): Intent[] {
|
|
66
132
|
const name = key.name;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (state.overlay === "submit" && (name === "left" || name === "right")) {
|
|
75
|
-
return [{ t: "cycleVerdict", dir: name === "left" ? -1 : 1 }];
|
|
76
|
-
}
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
if (state.overlay === "completion-prompt" || state.overlay === "completion-counting") {
|
|
80
|
-
if (name === "return" || name === "enter" || name === "q") return [{ t: "finishReview" }];
|
|
81
|
-
if (name === "a" && state.overlay === "completion-prompt") return [{ t: "optInAutoClose" }];
|
|
82
|
-
if (name === "escape") return [{ t: "dismissCompletion" }];
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
const action = actionFor(state.keys, name, key.shift);
|
|
86
|
-
if (action === "quit") return [{ t: "exit" }];
|
|
133
|
+
const overlayGrammar = overlayGrammars[state.overlay];
|
|
134
|
+
|
|
135
|
+
if (overlayGrammar) return overlayGrammar(state, key);
|
|
136
|
+
const action = resolvedAction ?? actionFor(state.keys, name, key.shift);
|
|
137
|
+
|
|
138
|
+
if (action === "quit") return [{ type: "exit" }];
|
|
87
139
|
// the ONE read-only rule: any mutating attempt answers instead of acting
|
|
88
|
-
// (span-mode c
|
|
89
|
-
const mutating =
|
|
140
|
+
// (span-mode c, x, and a are hardwired keys, so they gate by name as well)
|
|
141
|
+
const mutating =
|
|
142
|
+
MUTATING_ACTIONS.has(action ?? "") ||
|
|
143
|
+
(state.spanMode && (name === "c" || name === "x" || name === "a"));
|
|
144
|
+
|
|
90
145
|
if (state.readOnly && mutating) return status("observer - read-only");
|
|
91
146
|
|
|
147
|
+
// share is a session-level primitive: it works from any view, owner only
|
|
148
|
+
if (action === "share") {
|
|
149
|
+
if (state.canShare === false) return status("only the plan owner can share");
|
|
150
|
+
|
|
151
|
+
return [{ type: "share" }];
|
|
152
|
+
}
|
|
153
|
+
|
|
92
154
|
if (state.view === "inbox") return inboxGrammar(state, name);
|
|
155
|
+
// span mode owns its single-letter keys (b slides the span back)
|
|
156
|
+
if (state.spanMode) return spanGrammar(state, name);
|
|
93
157
|
if (state.view === "diff") return diffGrammar(state, action);
|
|
94
|
-
|
|
95
|
-
return planGrammar(state, action);
|
|
158
|
+
|
|
159
|
+
return planGrammar(state, action, name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type OverlayGrammar = (state: KeyState, key: KeyInput) => Intent[];
|
|
163
|
+
|
|
164
|
+
const overlayGrammars: Partial<Record<KeyState["overlay"], OverlayGrammar>> = {
|
|
165
|
+
compose: composeOverlayGrammar,
|
|
166
|
+
submit: submitOverlayGrammar,
|
|
167
|
+
confirm: confirmOverlayGrammar,
|
|
168
|
+
prompt: promptOverlayGrammar,
|
|
169
|
+
spanActions: spanActionsOverlayGrammar,
|
|
170
|
+
"completion-prompt": completionOverlayGrammar,
|
|
171
|
+
"completion-counting": completionOverlayGrammar,
|
|
172
|
+
walk: walkOverlayGrammar,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function composeOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
176
|
+
const name = key.name;
|
|
177
|
+
|
|
178
|
+
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
179
|
+
if (name === "return" || name === "enter") {
|
|
180
|
+
// In the composer, ⌥/Alt+⏎ (meta) and shift+⏎ insert a newline - the
|
|
181
|
+
// focused textarea owns that; only a bare ⏎ saves. The submit overlay
|
|
182
|
+
// keeps its plain ⏎ submit.
|
|
183
|
+
if (key.shift || key.meta) return [];
|
|
184
|
+
|
|
185
|
+
return [{ type: "saveCompose" }];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function submitOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
192
|
+
const name = key.name;
|
|
193
|
+
|
|
194
|
+
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
195
|
+
if (name === "left" || name === "right") {
|
|
196
|
+
return [{ type: "cycleMessage", direction: name === "left" ? -1 : 1 }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function confirmOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
203
|
+
const name = key.name;
|
|
204
|
+
|
|
205
|
+
if (name === "return" || name === "enter") return [{ type: "confirmDialog" }];
|
|
206
|
+
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
207
|
+
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function promptOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
212
|
+
const name = key.name;
|
|
213
|
+
|
|
214
|
+
if (name === "return" || name === "enter") return [{ type: "confirmDialog" }];
|
|
215
|
+
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
216
|
+
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function spanActionsOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
221
|
+
const name = key.name;
|
|
222
|
+
|
|
223
|
+
if (name === "j" || name === "down") return [{ type: "moveSpanAction", direction: 1 }];
|
|
224
|
+
if (name === "k" || name === "up") return [{ type: "moveSpanAction", direction: -1 }];
|
|
225
|
+
if (name === "return" || name === "enter") return [{ type: "pickSpanAction" }];
|
|
226
|
+
if (name === "escape") return [{ type: "closeSpanActions" }];
|
|
227
|
+
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function completionOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
232
|
+
const name = key.name;
|
|
233
|
+
|
|
234
|
+
if (name === "return" || name === "enter" || name === "q") return [{ type: "finishReview" }];
|
|
235
|
+
if (name === "a") return [{ type: "optInAutoClose" }];
|
|
236
|
+
if (name === "escape") return [{ type: "dismissCompletion" }];
|
|
237
|
+
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function walkOverlayGrammar(state: KeyState, key: KeyInput): Intent[] {
|
|
242
|
+
const name = key.name;
|
|
243
|
+
|
|
244
|
+
if (name === "]") return [{ type: "walkForward" }];
|
|
245
|
+
if (name === "[") return [{ type: "walkBack" }];
|
|
246
|
+
if (name === "escape") return [{ type: "walkLeave" }];
|
|
247
|
+
if ((name === "return" || name === "enter") && state.walkAtEnd) {
|
|
248
|
+
return [{ type: "walkLeave" }, { type: "openSubmit" }];
|
|
249
|
+
}
|
|
250
|
+
if (name === "q") return [{ type: "exit" }];
|
|
251
|
+
|
|
252
|
+
return [];
|
|
96
253
|
}
|
|
97
254
|
|
|
98
255
|
function inboxGrammar(state: KeyState, name: string): Intent[] {
|
|
99
256
|
if (!state.hasInboxItems) return [];
|
|
100
|
-
if (name === "j" || name === "down") return [{
|
|
101
|
-
if (name === "k" || name === "up") return [{
|
|
102
|
-
if (name === "return" || name === "enter") return [{
|
|
257
|
+
if (name === "j" || name === "down") return [{ type: "inboxMove", to: "down" }];
|
|
258
|
+
if (name === "k" || name === "up") return [{ type: "inboxMove", to: "up" }];
|
|
259
|
+
if (name === "return" || name === "enter") return [{ type: "openSession" }];
|
|
260
|
+
if (name === "d") return [{ type: "requestDeleteSession" }];
|
|
261
|
+
|
|
103
262
|
return [];
|
|
104
263
|
}
|
|
105
264
|
|
|
106
265
|
function diffGrammar(state: KeyState, action: string | undefined): Intent[] {
|
|
107
|
-
const
|
|
108
|
-
|
|
266
|
+
const navigation = navigationIntent(action);
|
|
267
|
+
|
|
268
|
+
if (navigation) return navigation;
|
|
269
|
+
if (action === "walk") {
|
|
270
|
+
// marking viewed writes the session record, so a resolved review answers
|
|
271
|
+
if (state.resolved) return status("review submitted - read-only");
|
|
272
|
+
|
|
273
|
+
return [{ type: "walkStart" }];
|
|
274
|
+
}
|
|
109
275
|
if (action === "comment") {
|
|
110
276
|
if (state.resolved) return status("review submitted - read-only");
|
|
111
277
|
if (!state.cursorAnnotatable) return status("move to a code line to comment");
|
|
112
|
-
|
|
278
|
+
|
|
279
|
+
return [{ type: "openCompose", kind: "comment", from: "cursor" }];
|
|
280
|
+
}
|
|
281
|
+
// cut rejects the change under the cursor, reject_hunk the whole hunk; both
|
|
282
|
+
// write the working copy, so they gate on owner like a plan edit.
|
|
283
|
+
if (action === "cut" || action === "reject_hunk") {
|
|
284
|
+
if (state.resolved) return status("review submitted - read-only");
|
|
285
|
+
if (state.canEditPlan === false) return status("only the diff owner can curate hunks");
|
|
286
|
+
|
|
287
|
+
return [{ type: action === "reject_hunk" ? "rejectHunk" : "rejectChange" }];
|
|
288
|
+
}
|
|
289
|
+
const viewToggle = viewToggleIntent(action);
|
|
290
|
+
|
|
291
|
+
if (viewToggle) return viewToggle;
|
|
292
|
+
// restore un-does a curated-out rejection from the rail; same owner gate as reject
|
|
293
|
+
if (action === "restore_curation") {
|
|
294
|
+
if (state.resolved) return status("review submitted - read-only");
|
|
295
|
+
if (state.canEditPlan === false) return status("only the diff owner can curate hunks");
|
|
296
|
+
|
|
297
|
+
return [{ type: "restoreCuration" }];
|
|
113
298
|
}
|
|
114
299
|
const shared = annotationCluster(state, action);
|
|
300
|
+
|
|
115
301
|
if (shared) return shared;
|
|
116
|
-
if (action === "span" || action === "
|
|
117
|
-
return status("plan-only
|
|
302
|
+
if (action === "span" || action === "edit") {
|
|
303
|
+
return status("plan-only primitive - diff review uses c on a line");
|
|
118
304
|
}
|
|
305
|
+
|
|
119
306
|
return [];
|
|
120
307
|
}
|
|
121
308
|
|
|
122
|
-
function spanGrammar(name: string): Intent[] {
|
|
123
|
-
if (name === "escape") return [{
|
|
124
|
-
if (SPAN_KEYS.
|
|
125
|
-
|
|
126
|
-
|
|
309
|
+
function spanGrammar(state: KeyState, name: string): Intent[] {
|
|
310
|
+
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
311
|
+
if (SPAN_KEYS.has(name)) return [{ type: "spanKey", name }];
|
|
312
|
+
// c comments, x cuts, a opens quick-actions - all mutate, so a resolved
|
|
313
|
+
// review is read-only, like the plan grammar
|
|
314
|
+
if (name === "c" || name === "x" || name === "a") {
|
|
315
|
+
if (state.resolved) return status("review submitted - read-only");
|
|
316
|
+
if (name === "c") return [{ type: "openCompose", kind: "comment", from: "span" }];
|
|
317
|
+
if (name === "a") return [{ type: "openSpanActions" }];
|
|
318
|
+
// partial-span cut is not in the working-copy model, so cut removes the
|
|
319
|
+
// whole block the span sits in; owner-only, like plan cut
|
|
320
|
+
if (state.canEditPlan === false) return [];
|
|
321
|
+
|
|
322
|
+
return [{ type: "spanCut" }];
|
|
127
323
|
}
|
|
324
|
+
|
|
128
325
|
return [];
|
|
129
326
|
}
|
|
130
327
|
|
|
131
|
-
function planGrammar(state: KeyState, action: string | undefined): Intent[] {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
if (action === "
|
|
328
|
+
function planGrammar(state: KeyState, action: string | undefined, name: string): Intent[] {
|
|
329
|
+
const navigation = navigationIntent(action);
|
|
330
|
+
|
|
331
|
+
if (navigation) return navigation;
|
|
332
|
+
if (action === "walk") return status("the guided walk is a diff-review mode");
|
|
333
|
+
if (name === "escape") return [{ type: "deselect" }];
|
|
334
|
+
if (action === "span") return state.cursorAnnotatable ? [{ type: "startSpan" }] : [];
|
|
335
|
+
if (action === "comment") {
|
|
136
336
|
if (state.resolved) return status("review submitted - read-only");
|
|
137
337
|
if (!state.cursorAnnotatable) return status("text is cut - restore it first");
|
|
138
|
-
|
|
338
|
+
|
|
339
|
+
return [{ type: "openCompose", kind: "comment", from: "cursor" }];
|
|
340
|
+
}
|
|
341
|
+
// restore un-does a rail removal (a cut block); a plan edit, so owner-only
|
|
342
|
+
if (action === "restore_curation") {
|
|
343
|
+
if (state.resolved) return status("review submitted - read-only");
|
|
344
|
+
if (state.canEditPlan === false) return [];
|
|
345
|
+
|
|
346
|
+
return [{ type: "restoreCuration" }];
|
|
139
347
|
}
|
|
140
348
|
if (action === "cut" || action === "edit") {
|
|
141
349
|
if (state.resolved) return status("review submitted - read-only");
|
|
142
|
-
|
|
350
|
+
// the document selects, the rail edits: with a card selected, Cut deletes
|
|
351
|
+
// the annotation and edit rewrites the card body in place
|
|
352
|
+
if (state.hasFocusedAnnotation)
|
|
353
|
+
return [action === "cut" ? { type: "removeAnnotation" } : { type: "editCard" }];
|
|
354
|
+
// editing the plan itself is the owner's primitive; a share viewer only annotates,
|
|
355
|
+
// and has no edit affordance, so the key is silent rather than a nag
|
|
356
|
+
if (state.canEditPlan === false) return [];
|
|
357
|
+
|
|
358
|
+
return [{ type: action }];
|
|
143
359
|
}
|
|
360
|
+
|
|
144
361
|
return annotationCluster(state, action) ?? [];
|
|
145
362
|
}
|
|
146
363
|
|
|
147
364
|
/** Cursor movement is the same intent everywhere; views clamp their own bounds. */
|
|
148
|
-
function
|
|
365
|
+
function navigationIntent(action: string | undefined): Intent[] | null {
|
|
149
366
|
if (action === "down" || action === "up" || action === "top" || action === "bottom") {
|
|
150
|
-
return [{
|
|
367
|
+
return [{ type: "move", to: action }];
|
|
151
368
|
}
|
|
369
|
+
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Ungated view toggles in the diff: right folds a file to its band, left unfolds it, s flips
|
|
374
|
+
* split/stacked (split lays out only when the Changes pane is wide/zoomed). */
|
|
375
|
+
function viewToggleIntent(action: string | undefined): Intent[] | null {
|
|
376
|
+
if (action === "collapse_file") return [{ type: "foldFile" }];
|
|
377
|
+
if (action === "expand_file") return [{ type: "unfoldFile" }];
|
|
378
|
+
if (action === "split_diff") return [{ type: "toggleDiffView" }];
|
|
379
|
+
|
|
152
380
|
return null;
|
|
153
381
|
}
|
|
154
382
|
|
|
@@ -156,12 +384,26 @@ function navIntent(action: string | undefined): Intent[] | null {
|
|
|
156
384
|
function annotationCluster(state: KeyState, action: string | undefined): Intent[] | null {
|
|
157
385
|
if (action === "next_annotation" || action === "prev_annotation") {
|
|
158
386
|
if (!state.annotationCount) return status("no annotations");
|
|
159
|
-
|
|
387
|
+
|
|
388
|
+
return [{ type: action === "next_annotation" ? "nextAnnotation" : "prevAnnotation" }];
|
|
160
389
|
}
|
|
161
390
|
if (action === "delete_annotation") {
|
|
162
391
|
if (state.resolved || !state.hasFocusedAnnotation) return [];
|
|
163
|
-
|
|
392
|
+
|
|
393
|
+
return [{ type: "removeAnnotation" }];
|
|
164
394
|
}
|
|
165
|
-
if (action === "
|
|
395
|
+
if (action === "rename") {
|
|
396
|
+
if (!state.hasFocusedAnnotation) return status("select a collaborator's note to rename them");
|
|
397
|
+
|
|
398
|
+
return [{ type: "openRename" }];
|
|
399
|
+
}
|
|
400
|
+
if (action === "submit") {
|
|
401
|
+
// a collaborator's notes union back as they go; there is no message to submit
|
|
402
|
+
if (state.canSubmitMessage === false)
|
|
403
|
+
return status("shared view - your notes save as you go; q to leave");
|
|
404
|
+
|
|
405
|
+
return state.resolved ? [] : [{ type: "openSubmit" }];
|
|
406
|
+
}
|
|
407
|
+
|
|
166
408
|
return null;
|
|
167
409
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Kitty emission guards: the image sits beneath painted cells, and every put reuses one placement id so repeated frames REPLACE the placement instead of stacking a new one each frame (the prototype's per-frame lag/ghosting regression). Transmit medium is file locally and base64 over ssh. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import { placeKittyImage, resolveTransmitMedium, transmitKittyImage } from "./kitty-image";
|
|
5
|
+
|
|
6
|
+
const REGION = { column: 2, row: 3, columns: 40, rows: 12 };
|
|
7
|
+
|
|
8
|
+
describe("kitty image placement", () => {
|
|
9
|
+
test("places the prototype below cells with painted backgrounds", () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const writes: string[] = [];
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
transmitKittyImage((chunk) => writes.push(chunk), new Uint8Array([1]), REGION, 811);
|
|
15
|
+
placeKittyImage((chunk) => writes.push(chunk), REGION, 811);
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(writes).toHaveLength(2);
|
|
19
|
+
expect(writes[0]).toContain("z=-1073741825");
|
|
20
|
+
expect(writes[1]).toContain("z=-1073741825");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("every frame reuses one placement id, so repeated puts replace rather than stack", () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const writes: string[] = [];
|
|
26
|
+
const write = (chunk: string): void => void writes.push(chunk);
|
|
27
|
+
|
|
28
|
+
// Act - one transmit then many re-places, as the per-frame paint loop does
|
|
29
|
+
transmitKittyImage(write, new Uint8Array([1]), REGION, 811, "base64");
|
|
30
|
+
for (let frame = 0; frame < 5; frame++) placeKittyImage(write, REGION, 811);
|
|
31
|
+
|
|
32
|
+
// Assert - a stable placement id p=1 on the transmit and every place
|
|
33
|
+
expect(writes[0]).toContain("i=811,p=1");
|
|
34
|
+
for (const placement of writes.slice(1)) expect(placement).toContain("i=811,p=1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("base64 transmit chunks the payload; file transmit hands over a path once", () => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const big = new Uint8Array(9000).fill(1);
|
|
40
|
+
const base64Writes: string[] = [];
|
|
41
|
+
const fileWrites: string[] = [];
|
|
42
|
+
|
|
43
|
+
// Act
|
|
44
|
+
transmitKittyImage((chunk) => base64Writes.push(chunk), big, REGION, 811, "base64");
|
|
45
|
+
transmitKittyImage((chunk) => fileWrites.push(chunk), big, REGION, 811, "file");
|
|
46
|
+
|
|
47
|
+
// Assert - base64 inlines the data (a=T without t=), file references a temp path (t=t)
|
|
48
|
+
expect(base64Writes[0]).toContain("a=T");
|
|
49
|
+
expect(base64Writes[0]).not.toContain("t=t");
|
|
50
|
+
expect(fileWrites[0]).toContain("t=t");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("resolveTransmitMedium", () => {
|
|
55
|
+
test("defaults to base64; file transfer is opt-in and never over ssh", () => {
|
|
56
|
+
// Assert
|
|
57
|
+
expect(resolveTransmitMedium({})).toBe("base64");
|
|
58
|
+
expect(resolveTransmitMedium({ CUELOOP_KITTY_FILE: "1" })).toBe("file");
|
|
59
|
+
expect(
|
|
60
|
+
resolveTransmitMedium({
|
|
61
|
+
CUELOOP_KITTY_FILE: "1",
|
|
62
|
+
SSH_CONNECTION: "1.2.3.4 5 6 7",
|
|
63
|
+
}),
|
|
64
|
+
).toBe("base64");
|
|
65
|
+
});
|
|
66
|
+
});
|