@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +1406 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +787 -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 +29 -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 +78 -0
- package/src/launch-layout.ts +62 -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
package/src/keymap.test.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The keyboard grammar as data: every mode x key produces a known intent
|
|
3
|
-
* list, the read-only rule is one gate, and plan and diff reviews share one
|
|
4
|
-
* path for annotation navigation, deletion, and submit.
|
|
5
|
-
*/
|
|
1
|
+
/** The keyboard grammar as data: every mode x key maps to a known intent list, gated once by the read-only rule. */
|
|
6
2
|
|
|
7
3
|
import { describe, expect, test } from "bun:test";
|
|
8
4
|
import { DEFAULT_KEYS } from "./config";
|
|
@@ -20,224 +16,512 @@ function state(patch: Partial<KeyState> = {}): KeyState {
|
|
|
20
16
|
annotationCount: 1,
|
|
21
17
|
hasFocusedAnnotation: true,
|
|
22
18
|
cursorAnnotatable: true,
|
|
19
|
+
walkAtEnd: false,
|
|
23
20
|
...patch,
|
|
24
21
|
};
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
function key(name: string, shift = false
|
|
28
|
-
return { name, shift };
|
|
24
|
+
function key(name: string, shift = false, meta = false) {
|
|
25
|
+
return { name, shift, meta };
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
describe("compose overlay", () => {
|
|
32
|
-
|
|
29
|
+
// Arrange
|
|
30
|
+
const keyState = state({ overlay: "compose" });
|
|
33
31
|
const table: [string, Intent[]][] = [
|
|
34
|
-
["escape", [{
|
|
35
|
-
["return", [{
|
|
36
|
-
["enter", [{
|
|
32
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
33
|
+
["return", [{ type: "saveCompose" }]],
|
|
34
|
+
["enter", [{ type: "saveCompose" }]],
|
|
37
35
|
["left", []],
|
|
38
36
|
["right", []],
|
|
39
37
|
["j", []],
|
|
40
38
|
["q", []],
|
|
41
39
|
];
|
|
40
|
+
|
|
42
41
|
for (const [name, expected] of table) {
|
|
43
42
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
44
|
-
expect(reduceKey(
|
|
43
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
45
44
|
});
|
|
46
45
|
}
|
|
46
|
+
|
|
47
|
+
// ⌥/Alt+⏎ (meta) and shift+⏎ insert a newline in the composer, so the
|
|
48
|
+
// grammar must not save on them; only a bare ⏎ saves.
|
|
49
|
+
test("option/alt+return (meta) does not save - the textarea inserts a newline", () => {
|
|
50
|
+
expect(reduceKey(keyState, key("return", false, true))).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
test("shift+return does not save - the textarea inserts a newline", () => {
|
|
53
|
+
expect(reduceKey(keyState, key("return", true, false))).toEqual([]);
|
|
54
|
+
});
|
|
47
55
|
});
|
|
48
56
|
|
|
49
57
|
describe("submit overlay", () => {
|
|
50
|
-
|
|
58
|
+
// Arrange
|
|
59
|
+
const keyState = state({ overlay: "submit" });
|
|
51
60
|
const table: [string, Intent[]][] = [
|
|
52
|
-
["escape", [{
|
|
53
|
-
["return", [
|
|
54
|
-
["enter", [
|
|
55
|
-
["left", [{
|
|
56
|
-
["right", [{
|
|
61
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
62
|
+
["return", []],
|
|
63
|
+
["enter", []],
|
|
64
|
+
["left", [{ type: "cycleMessage", direction: -1 }]],
|
|
65
|
+
["right", [{ type: "cycleMessage", direction: 1 }]],
|
|
57
66
|
["j", []],
|
|
58
67
|
["q", []],
|
|
59
68
|
];
|
|
69
|
+
|
|
60
70
|
for (const [name, expected] of table) {
|
|
61
71
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
62
|
-
expect(reduceKey(
|
|
72
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
63
73
|
});
|
|
64
74
|
}
|
|
65
75
|
});
|
|
66
76
|
|
|
67
77
|
describe("completion overlay", () => {
|
|
78
|
+
// Arrange
|
|
68
79
|
const prompt = state({ overlay: "completion-prompt" });
|
|
69
80
|
const counting = state({ overlay: "completion-counting" });
|
|
70
81
|
const table: [KeyState, string, Intent[]][] = [
|
|
71
|
-
[prompt, "return", [{
|
|
72
|
-
[prompt, "enter", [{
|
|
73
|
-
[prompt, "q", [{
|
|
74
|
-
[prompt, "a", [{
|
|
75
|
-
[prompt, "escape", [{
|
|
82
|
+
[prompt, "return", [{ type: "finishReview" }]],
|
|
83
|
+
[prompt, "enter", [{ type: "finishReview" }]],
|
|
84
|
+
[prompt, "q", [{ type: "finishReview" }]],
|
|
85
|
+
[prompt, "a", [{ type: "optInAutoClose" }]],
|
|
86
|
+
[prompt, "escape", [{ type: "dismissCompletion" }]],
|
|
76
87
|
[prompt, "j", []],
|
|
77
|
-
[counting, "return", [{
|
|
78
|
-
[counting, "q", [{
|
|
79
|
-
[counting, "a", []],
|
|
80
|
-
[counting, "escape", [{
|
|
88
|
+
[counting, "return", [{ type: "finishReview" }]],
|
|
89
|
+
[counting, "q", [{ type: "finishReview" }]],
|
|
90
|
+
[counting, "a", [{ type: "optInAutoClose" }]],
|
|
91
|
+
[counting, "escape", [{ type: "dismissCompletion" }]],
|
|
81
92
|
];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
|
|
94
|
+
for (const [overlayState, name, expected] of table) {
|
|
95
|
+
test(`${overlayState.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
|
|
96
|
+
expect(reduceKey(overlayState, key(name))).toEqual(expected);
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
describe("inbox mode", () => {
|
|
90
|
-
|
|
102
|
+
// Arrange
|
|
103
|
+
const keyState = state({ view: "inbox" });
|
|
91
104
|
const table: [string, Intent[]][] = [
|
|
92
|
-
["j", [{
|
|
93
|
-
["down", [{
|
|
94
|
-
["k", [{
|
|
95
|
-
["up", [{
|
|
96
|
-
["return", [{
|
|
97
|
-
["enter", [{
|
|
98
|
-
["q", [{
|
|
105
|
+
["j", [{ type: "inboxMove", to: "down" }]],
|
|
106
|
+
["down", [{ type: "inboxMove", to: "down" }]],
|
|
107
|
+
["k", [{ type: "inboxMove", to: "up" }]],
|
|
108
|
+
["up", [{ type: "inboxMove", to: "up" }]],
|
|
109
|
+
["return", [{ type: "openSession" }]],
|
|
110
|
+
["enter", [{ type: "openSession" }]],
|
|
111
|
+
["q", [{ type: "exit" }]],
|
|
99
112
|
["c", []],
|
|
100
113
|
["escape", []],
|
|
101
114
|
];
|
|
115
|
+
|
|
102
116
|
for (const [name, expected] of table) {
|
|
103
117
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
104
|
-
expect(reduceKey(
|
|
118
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
105
119
|
});
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
test("an empty inbox swallows every non-quit key", () => {
|
|
123
|
+
// Arrange
|
|
109
124
|
const empty = state({ view: "inbox", hasInboxItems: false });
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
110
127
|
for (const name of ["j", "k", "down", "up", "return", "enter"]) {
|
|
111
128
|
expect(reduceKey(empty, key(name))).toEqual([]);
|
|
112
129
|
}
|
|
113
|
-
expect(reduceKey(empty, key("q"))).toEqual([{
|
|
130
|
+
expect(reduceKey(empty, key("q"))).toEqual([{ type: "exit" }]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("collaborator (share) capabilities", () => {
|
|
135
|
+
test("can annotate: comment opens the composer", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const collab = state({ canEditPlan: false, canSubmitMessage: false });
|
|
138
|
+
|
|
139
|
+
// Act / Assert
|
|
140
|
+
expect(reduceKey(collab, key("c"), "comment")).toEqual([
|
|
141
|
+
{ type: "openCompose", kind: "comment", from: "cursor" },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("can edit their own card: edit with a focused annotation rewrites it", () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
const collab = state({ canEditPlan: false, hasFocusedAnnotation: true });
|
|
148
|
+
|
|
149
|
+
// Act / Assert
|
|
150
|
+
expect(reduceKey(collab, key("e"), "edit")).toEqual([{ type: "editCard" }]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("cannot edit the plan: cut/edit with no card is silent (no nag, no button)", () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
const collab = state({ canEditPlan: false, hasFocusedAnnotation: false });
|
|
156
|
+
|
|
157
|
+
// Act / Assert
|
|
158
|
+
expect(reduceKey(collab, key("e"), "edit")).toEqual([]);
|
|
159
|
+
expect(reduceKey(collab, key("c"), "cut")).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("cannot submit a message: there is no agent on a share", () => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const collab = state({ canSubmitMessage: false });
|
|
165
|
+
|
|
166
|
+
// Act / Assert
|
|
167
|
+
expect(reduceKey(collab, key("x"), "submit")).toEqual([
|
|
168
|
+
{ type: "status", message: "shared view - your notes save as you go; q to leave" },
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("cannot re-share: only the owner shares", () => {
|
|
173
|
+
// Arrange
|
|
174
|
+
const collab = state({ canShare: false });
|
|
175
|
+
|
|
176
|
+
// Act / Assert
|
|
177
|
+
expect(reduceKey(collab, key("S", true), "share")).toEqual([
|
|
178
|
+
{ type: "status", message: "only the plan owner can share" },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("share", () => {
|
|
184
|
+
test("the owner's share key publishes", () => {
|
|
185
|
+
// Act / Assert
|
|
186
|
+
expect(reduceKey(state(), key("S", true), "share")).toEqual([{ type: "share" }]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an observer cannot share", () => {
|
|
190
|
+
// Act / Assert
|
|
191
|
+
expect(reduceKey(state({ readOnly: true }), key("S", true), "share")).toEqual([
|
|
192
|
+
{ type: "status", message: "observer - read-only" },
|
|
193
|
+
]);
|
|
114
194
|
});
|
|
115
195
|
});
|
|
116
196
|
|
|
117
197
|
describe("plan normal mode", () => {
|
|
118
|
-
|
|
198
|
+
// Arrange
|
|
199
|
+
const keyState = state();
|
|
119
200
|
const table: [string, boolean, Intent[]][] = [
|
|
120
|
-
["j", false, [{
|
|
121
|
-
["down", false, [{
|
|
122
|
-
["k", false, [{
|
|
123
|
-
["up", false, [{
|
|
124
|
-
["g", false, [{
|
|
125
|
-
["g", true, [{
|
|
126
|
-
["v", false, [{
|
|
127
|
-
["c", false, [{
|
|
128
|
-
["s", false, [{ t: "openCompose", kind: "suggestion", from: "cursor" }]],
|
|
201
|
+
["j", false, [{ type: "move", to: "down" }]],
|
|
202
|
+
["down", false, [{ type: "move", to: "down" }]],
|
|
203
|
+
["k", false, [{ type: "move", to: "up" }]],
|
|
204
|
+
["up", false, [{ type: "move", to: "up" }]],
|
|
205
|
+
["g", false, [{ type: "move", to: "top" }]],
|
|
206
|
+
["g", true, [{ type: "move", to: "bottom" }]],
|
|
207
|
+
["v", false, [{ type: "startSpan" }]],
|
|
208
|
+
["c", false, [{ type: "openCompose", kind: "comment", from: "cursor" }]],
|
|
129
209
|
// the default state has a selected card, so Cut and edit act on the card
|
|
130
|
-
["x", false, [{
|
|
131
|
-
["e", false, [{
|
|
132
|
-
["n", false, [{
|
|
133
|
-
["p", false, [{
|
|
134
|
-
["backspace", false, [{
|
|
135
|
-
["return", false, [{
|
|
136
|
-
["q", false, [{
|
|
137
|
-
["escape", false, [{
|
|
210
|
+
["x", false, [{ type: "removeAnnotation" }]],
|
|
211
|
+
["e", false, [{ type: "editCard" }]],
|
|
212
|
+
["n", false, [{ type: "nextAnnotation" }]],
|
|
213
|
+
["p", false, [{ type: "prevAnnotation" }]],
|
|
214
|
+
["backspace", false, [{ type: "removeAnnotation" }]],
|
|
215
|
+
["return", false, [{ type: "openSubmit" }]],
|
|
216
|
+
["q", false, [{ type: "exit" }]],
|
|
217
|
+
["escape", false, [{ type: "deselect" }]],
|
|
138
218
|
["z", false, []],
|
|
139
219
|
];
|
|
220
|
+
|
|
140
221
|
for (const [name, shift, expected] of table) {
|
|
141
222
|
test(`${shift ? "shift+" : ""}${name} -> ${JSON.stringify(expected)}`, () => {
|
|
142
|
-
expect(reduceKey(
|
|
223
|
+
expect(reduceKey(keyState, key(name, shift))).toEqual(expected);
|
|
143
224
|
});
|
|
144
225
|
}
|
|
145
226
|
|
|
146
|
-
test("resolved sessions guard the mutating
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
227
|
+
test("resolved sessions guard the mutating primitives", () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
const resolvedState = state({ resolved: true });
|
|
230
|
+
|
|
231
|
+
// Assert
|
|
232
|
+
for (const name of ["c", "x", "e"]) {
|
|
233
|
+
expect(reduceKey(resolvedState, key(name))).toEqual([
|
|
234
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
235
|
+
]);
|
|
150
236
|
}
|
|
151
|
-
expect(reduceKey(
|
|
152
|
-
expect(reduceKey(
|
|
237
|
+
expect(reduceKey(resolvedState, key("return"))).toEqual([]);
|
|
238
|
+
expect(reduceKey(resolvedState, key("backspace"))).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("u restores a rail removal (a cut block); a share viewer's is silent", () => {
|
|
242
|
+
// Assert - the undo intent leaves the target to the dispatcher
|
|
243
|
+
expect(reduceKey(state(), key("u"))).toEqual([{ type: "restoreCuration" }]);
|
|
244
|
+
expect(reduceKey(state({ resolved: true }), key("u"))).toEqual([
|
|
245
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
246
|
+
]);
|
|
247
|
+
expect(reduceKey(state({ canEditPlan: false }), key("u"))).toEqual([]);
|
|
248
|
+
expect(reduceKey(state({ readOnly: true }), key("u"))).toEqual([
|
|
249
|
+
{ type: "status", message: "observer - read-only" },
|
|
250
|
+
]);
|
|
153
251
|
});
|
|
154
252
|
|
|
155
253
|
test("cut text cannot host a comment or a span", () => {
|
|
254
|
+
// Arrange
|
|
156
255
|
const cut = state({ cursorAnnotatable: false });
|
|
157
|
-
|
|
158
|
-
|
|
256
|
+
|
|
257
|
+
// Assert
|
|
258
|
+
expect(reduceKey(cut, key("c"))).toEqual([
|
|
259
|
+
{ type: "status", message: "text is cut - restore it first" },
|
|
260
|
+
]);
|
|
159
261
|
expect(reduceKey(cut, key("v"))).toEqual([]);
|
|
160
262
|
});
|
|
161
263
|
|
|
162
264
|
test("annotation navigation without annotations reports, delete without focus is silent", () => {
|
|
265
|
+
// Arrange
|
|
163
266
|
const none = state({ annotationCount: 0, hasFocusedAnnotation: false });
|
|
164
|
-
|
|
165
|
-
|
|
267
|
+
|
|
268
|
+
// Assert
|
|
269
|
+
expect(reduceKey(none, key("n"))).toEqual([{ type: "status", message: "no annotations" }]);
|
|
270
|
+
expect(reduceKey(none, key("p"))).toEqual([{ type: "status", message: "no annotations" }]);
|
|
166
271
|
expect(reduceKey(none, key("backspace"))).toEqual([]);
|
|
167
272
|
});
|
|
168
273
|
|
|
169
274
|
test("without a selected card x cuts the block and e opens the editor", () => {
|
|
275
|
+
// Arrange
|
|
170
276
|
const unfocused = state({ hasFocusedAnnotation: false });
|
|
171
|
-
|
|
172
|
-
|
|
277
|
+
|
|
278
|
+
// Assert
|
|
279
|
+
expect(reduceKey(unfocused, key("x"))).toEqual([{ type: "cut" }]);
|
|
280
|
+
expect(reduceKey(unfocused, key("e"))).toEqual([{ type: "edit" }]);
|
|
173
281
|
});
|
|
174
282
|
});
|
|
175
283
|
|
|
176
284
|
describe("span mode", () => {
|
|
177
|
-
|
|
285
|
+
// Arrange
|
|
286
|
+
const keyState = state({ spanMode: true });
|
|
178
287
|
const table: [string, Intent[]][] = [
|
|
179
|
-
["l", [{
|
|
180
|
-
["h", [{
|
|
181
|
-
["w", [{
|
|
182
|
-
["b", [{
|
|
183
|
-
["$", [{
|
|
184
|
-
["0", [{
|
|
185
|
-
["c", [{
|
|
186
|
-
["
|
|
187
|
-
["
|
|
188
|
-
["
|
|
288
|
+
["l", [{ type: "spanKey", name: "l" }]],
|
|
289
|
+
["h", [{ type: "spanKey", name: "h" }]],
|
|
290
|
+
["w", [{ type: "spanKey", name: "w" }]],
|
|
291
|
+
["b", [{ type: "spanKey", name: "b" }]],
|
|
292
|
+
["$", [{ type: "spanKey", name: "$" }]],
|
|
293
|
+
["0", [{ type: "spanKey", name: "0" }]],
|
|
294
|
+
["c", [{ type: "openCompose", kind: "comment", from: "span" }]],
|
|
295
|
+
["x", [{ type: "spanCut" }]],
|
|
296
|
+
["a", [{ type: "openSpanActions" }]],
|
|
297
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
298
|
+
["q", [{ type: "exit" }]],
|
|
189
299
|
["j", []],
|
|
190
300
|
];
|
|
301
|
+
|
|
302
|
+
for (const [name, expected] of table) {
|
|
303
|
+
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
304
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
test("an observer's span-mode mutations answer read-only", () => {
|
|
309
|
+
// Arrange
|
|
310
|
+
const observer = state({ spanMode: true, readOnly: true });
|
|
311
|
+
|
|
312
|
+
// Assert
|
|
313
|
+
expect(reduceKey(observer, key("x"))).toEqual([
|
|
314
|
+
{ type: "status", message: "observer - read-only" },
|
|
315
|
+
]);
|
|
316
|
+
expect(reduceKey(observer, key("a"))).toEqual([
|
|
317
|
+
{ type: "status", message: "observer - read-only" },
|
|
318
|
+
]);
|
|
319
|
+
expect(reduceKey(observer, key("c"))).toEqual([
|
|
320
|
+
{ type: "status", message: "observer - read-only" },
|
|
321
|
+
]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("span-mode cut is owner-only, like the normal plan cut", () => {
|
|
325
|
+
// Arrange
|
|
326
|
+
const collaborator = state({ spanMode: true, canEditPlan: false });
|
|
327
|
+
|
|
328
|
+
// Assert
|
|
329
|
+
expect(reduceKey(collaborator, key("x"))).toEqual([]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("all span-mode mutations are blocked once the review is resolved", () => {
|
|
333
|
+
// Arrange
|
|
334
|
+
const resolved = state({ spanMode: true, resolved: true });
|
|
335
|
+
const readOnlyStatus: Intent[] = [{ type: "status", message: "review submitted - read-only" }];
|
|
336
|
+
|
|
337
|
+
// Assert - comment, cut, and quick-actions all answer read-only
|
|
338
|
+
expect(reduceKey(resolved, key("x"))).toEqual(readOnlyStatus);
|
|
339
|
+
expect(reduceKey(resolved, key("c"))).toEqual(readOnlyStatus);
|
|
340
|
+
expect(reduceKey(resolved, key("a"))).toEqual(readOnlyStatus);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("span-mode cut gates for an observer even when cut is rebound off x", () => {
|
|
344
|
+
// Arrange - the span sub-grammar hardwires x, so the read-only gate must catch it by name
|
|
345
|
+
const rebound = { ...DEFAULT_KEYS, cut: ["d"] };
|
|
346
|
+
const observer = state({ spanMode: true, readOnly: true, keys: rebound });
|
|
347
|
+
|
|
348
|
+
// Assert
|
|
349
|
+
expect(reduceKey(observer, key("x"))).toEqual([
|
|
350
|
+
{ type: "status", message: "observer - read-only" },
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("span actions overlay", () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const keyState = state({ overlay: "spanActions" });
|
|
358
|
+
const table: [string, Intent[]][] = [
|
|
359
|
+
["j", [{ type: "moveSpanAction", direction: 1 }]],
|
|
360
|
+
["down", [{ type: "moveSpanAction", direction: 1 }]],
|
|
361
|
+
["k", [{ type: "moveSpanAction", direction: -1 }]],
|
|
362
|
+
["up", [{ type: "moveSpanAction", direction: -1 }]],
|
|
363
|
+
["return", [{ type: "pickSpanAction" }]],
|
|
364
|
+
["enter", [{ type: "pickSpanAction" }]],
|
|
365
|
+
["escape", [{ type: "closeSpanActions" }]],
|
|
366
|
+
["c", []],
|
|
367
|
+
["q", []],
|
|
368
|
+
];
|
|
369
|
+
|
|
191
370
|
for (const [name, expected] of table) {
|
|
192
371
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
193
|
-
expect(reduceKey(
|
|
372
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
194
373
|
});
|
|
195
374
|
}
|
|
196
375
|
});
|
|
197
376
|
|
|
198
377
|
describe("diff mode", () => {
|
|
199
|
-
|
|
378
|
+
// Arrange
|
|
379
|
+
const keyState = state({ view: "diff" });
|
|
200
380
|
const table: [string, Intent[]][] = [
|
|
201
|
-
["j", [{
|
|
202
|
-
["k", [{
|
|
203
|
-
["g", [{
|
|
204
|
-
["c", [{
|
|
205
|
-
["n", [{
|
|
206
|
-
["p", [{
|
|
207
|
-
["backspace", [{
|
|
208
|
-
["return", [{
|
|
209
|
-
["v", [{
|
|
210
|
-
["x", [{
|
|
211
|
-
["e", [{
|
|
212
|
-
["s", [{
|
|
213
|
-
["q", [{
|
|
381
|
+
["j", [{ type: "move", to: "down" }]],
|
|
382
|
+
["k", [{ type: "move", to: "up" }]],
|
|
383
|
+
["g", [{ type: "move", to: "top" }]],
|
|
384
|
+
["c", [{ type: "openCompose", kind: "comment", from: "cursor" }]],
|
|
385
|
+
["n", [{ type: "nextAnnotation" }]],
|
|
386
|
+
["p", [{ type: "prevAnnotation" }]],
|
|
387
|
+
["backspace", [{ type: "removeAnnotation" }]],
|
|
388
|
+
["return", [{ type: "openSubmit" }]],
|
|
389
|
+
["v", [{ type: "status", message: "plan-only primitive - diff review uses c on a line" }]],
|
|
390
|
+
["x", [{ type: "rejectChange" }]],
|
|
391
|
+
["e", [{ type: "status", message: "plan-only primitive - diff review uses c on a line" }]],
|
|
392
|
+
["s", [{ type: "toggleDiffView" }]],
|
|
393
|
+
["q", [{ type: "exit" }]],
|
|
214
394
|
];
|
|
395
|
+
|
|
215
396
|
for (const [name, expected] of table) {
|
|
216
397
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
217
|
-
expect(reduceKey(
|
|
398
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
218
399
|
});
|
|
219
400
|
}
|
|
220
401
|
|
|
402
|
+
test("shift+x rejects the whole hunk under the cursor", () => {
|
|
403
|
+
// Assert
|
|
404
|
+
expect(reduceKey(keyState, key("x", true))).toEqual([{ type: "rejectHunk" }]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("curation is owner-only and locked once resolved", () => {
|
|
408
|
+
// Assert - a resolved diff answers before curating
|
|
409
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("x"))).toEqual([
|
|
410
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
411
|
+
]);
|
|
412
|
+
// a collaborator (canEditPlan false) cannot curate hunks
|
|
413
|
+
expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("x"))).toEqual([
|
|
414
|
+
{ type: "status", message: "only the diff owner can curate hunks" },
|
|
415
|
+
]);
|
|
416
|
+
// an observer's mutating attempt is stopped by the read-only gate
|
|
417
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("x"))).toEqual([
|
|
418
|
+
{ type: "status", message: "observer - read-only" },
|
|
419
|
+
]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("u restores a curated-out item, owner-only and locked once resolved", () => {
|
|
423
|
+
// Assert - the undo intent leaves the target to the dispatcher
|
|
424
|
+
expect(reduceKey(keyState, key("u"))).toEqual([{ type: "restoreCuration" }]);
|
|
425
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("u"))).toEqual([
|
|
426
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
427
|
+
]);
|
|
428
|
+
expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("u"))).toEqual([
|
|
429
|
+
{ type: "status", message: "only the diff owner can curate hunks" },
|
|
430
|
+
]);
|
|
431
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("u"))).toEqual([
|
|
432
|
+
{ type: "status", message: "observer - read-only" },
|
|
433
|
+
]);
|
|
434
|
+
});
|
|
435
|
+
|
|
221
436
|
test("comment guards: resolved first, then non-code rows", () => {
|
|
437
|
+
// Assert
|
|
222
438
|
expect(reduceKey(state({ view: "diff", resolved: true }), key("c"))).toEqual([
|
|
223
|
-
{
|
|
439
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
224
440
|
]);
|
|
225
441
|
expect(reduceKey(state({ view: "diff", cursorAnnotatable: false }), key("c"))).toEqual([
|
|
226
|
-
{
|
|
442
|
+
{ type: "status", message: "move to a code line to comment" },
|
|
227
443
|
]);
|
|
228
444
|
});
|
|
229
445
|
});
|
|
230
446
|
|
|
447
|
+
describe("guided walk", () => {
|
|
448
|
+
test("w enters the walk in diff view; resolved and read-only sessions answer", () => {
|
|
449
|
+
// Assert
|
|
450
|
+
expect(reduceKey(state({ view: "diff" }), key("w"))).toEqual([{ type: "walkStart" }]);
|
|
451
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("w"))).toEqual([
|
|
452
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
453
|
+
]);
|
|
454
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("w"))).toEqual([
|
|
455
|
+
{ type: "status", message: "observer - read-only" },
|
|
456
|
+
]);
|
|
457
|
+
expect(reduceKey(state(), key("w"))).toEqual([
|
|
458
|
+
{ type: "status", message: "the guided walk is a diff-review mode" },
|
|
459
|
+
]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Arrange
|
|
463
|
+
const walking = state({ view: "diff", overlay: "walk" });
|
|
464
|
+
const table: [string, Intent[]][] = [
|
|
465
|
+
["]", [{ type: "walkForward" }]],
|
|
466
|
+
["[", [{ type: "walkBack" }]],
|
|
467
|
+
["escape", [{ type: "walkLeave" }]],
|
|
468
|
+
["q", [{ type: "exit" }]],
|
|
469
|
+
// mid-walk return does nothing: submit lives on the end card only
|
|
470
|
+
["return", []],
|
|
471
|
+
["j", []],
|
|
472
|
+
["c", []],
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
for (const [name, expected] of table) {
|
|
476
|
+
test(`walking ${name} -> ${JSON.stringify(expected)}`, () => {
|
|
477
|
+
expect(reduceKey(walking, key(name))).toEqual(expected);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
test("the end card's return leaves the walk and opens the submit confirm", () => {
|
|
482
|
+
// Arrange
|
|
483
|
+
const atEnd = state({ view: "diff", overlay: "walk", walkAtEnd: true });
|
|
484
|
+
|
|
485
|
+
// Assert
|
|
486
|
+
expect(reduceKey(atEnd, key("return"))).toEqual([
|
|
487
|
+
{ type: "walkLeave" },
|
|
488
|
+
{ type: "openSubmit" },
|
|
489
|
+
]);
|
|
490
|
+
expect(reduceKey(atEnd, key("enter"))).toEqual([{ type: "walkLeave" }, { type: "openSubmit" }]);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("span and walk own their keys", () => {
|
|
495
|
+
test("span mode owns b as a span primitive", () => {
|
|
496
|
+
// Assert
|
|
497
|
+
expect(reduceKey(state({ spanMode: true }), key("b"))).toEqual([
|
|
498
|
+
{ type: "spanKey", name: "b" },
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("the walk overlay keeps the brackets for stepping", () => {
|
|
503
|
+
// Arrange
|
|
504
|
+
const walking = state({ view: "diff", overlay: "walk" });
|
|
505
|
+
|
|
506
|
+
// Assert
|
|
507
|
+
expect(reduceKey(walking, key("]"))).toEqual([{ type: "walkForward" }]);
|
|
508
|
+
expect(reduceKey(walking, key("["))).toEqual([{ type: "walkBack" }]);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
231
512
|
describe("duplicated-branch collapse", () => {
|
|
232
513
|
test("plan and diff produce the same intents for annotation nav, delete, and submit", () => {
|
|
233
|
-
|
|
514
|
+
const patches: Partial<KeyState>[] = [
|
|
234
515
|
{},
|
|
235
516
|
{ annotationCount: 0, hasFocusedAnnotation: false },
|
|
236
517
|
{ resolved: true },
|
|
237
|
-
]
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
for (const patch of patches) {
|
|
238
521
|
for (const name of ["n", "p", "backspace", "return"]) {
|
|
239
522
|
const plan = reduceKey(state(patch), key(name));
|
|
240
523
|
const diff = reduceKey(state({ ...patch, view: "diff" }), key(name));
|
|
524
|
+
|
|
241
525
|
expect(diff).toEqual(plan);
|
|
242
526
|
}
|
|
243
527
|
}
|
|
@@ -247,24 +531,81 @@ describe("duplicated-branch collapse", () => {
|
|
|
247
531
|
describe("read-only filter", () => {
|
|
248
532
|
test("every mutating key answers observer - read-only, in plan and diff", () => {
|
|
249
533
|
for (const view of ["plan", "diff"] as const) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
534
|
+
const keyState = state({ view, readOnly: true });
|
|
535
|
+
|
|
536
|
+
for (const name of ["c", "x", "e", "backspace", "return", "enter"]) {
|
|
537
|
+
expect(reduceKey(keyState, key(name))).toEqual([
|
|
538
|
+
{ type: "status", message: "observer - read-only" },
|
|
539
|
+
]);
|
|
253
540
|
}
|
|
254
541
|
}
|
|
255
542
|
});
|
|
256
543
|
|
|
257
544
|
test("navigation and annotation focus still work for observers", () => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
545
|
+
// Arrange
|
|
546
|
+
const keyState = state({ readOnly: true });
|
|
547
|
+
|
|
548
|
+
// Assert
|
|
549
|
+
expect(reduceKey(keyState, key("j"))).toEqual([{ type: "move", to: "down" }]);
|
|
550
|
+
expect(reduceKey(keyState, key("n"))).toEqual([{ type: "nextAnnotation" }]);
|
|
551
|
+
expect(reduceKey(keyState, key("q"))).toEqual([{ type: "exit" }]);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("span-mode c is gated by key name even under a rebound keymap", () => {
|
|
555
|
+
// Arrange
|
|
556
|
+
const rebound = { ...DEFAULT_KEYS, comment: ["m"] };
|
|
557
|
+
const keyState = state({ spanMode: true, readOnly: true, keys: rebound });
|
|
558
|
+
|
|
559
|
+
// Assert
|
|
560
|
+
expect(reduceKey(keyState, key("c"))).toEqual([
|
|
561
|
+
{ type: "status", message: "observer - read-only" },
|
|
562
|
+
]);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("inbox delete", () => {
|
|
567
|
+
test("d on a session requests delete", () => {
|
|
568
|
+
// Arrange / Act / Assert
|
|
569
|
+
expect(reduceKey(state({ view: "inbox" }), key("d"))).toEqual([
|
|
570
|
+
{ type: "requestDeleteSession" },
|
|
571
|
+
]);
|
|
262
572
|
});
|
|
573
|
+
});
|
|
263
574
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
575
|
+
describe("confirm overlay", () => {
|
|
576
|
+
// Arrange
|
|
577
|
+
const keyState = state({ overlay: "confirm" });
|
|
578
|
+
const table: [string, Intent[]][] = [
|
|
579
|
+
["return", [{ type: "confirmDialog" }]],
|
|
580
|
+
["enter", [{ type: "confirmDialog" }]],
|
|
581
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
582
|
+
["j", []],
|
|
583
|
+
["d", []],
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
for (const [name, expected] of table) {
|
|
587
|
+
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
588
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe("rename grammar", () => {
|
|
594
|
+
test("r on a focused note opens rename", () => {
|
|
595
|
+
// Arrange / Act / Assert
|
|
596
|
+
expect(reduceKey(state({ hasFocusedAnnotation: true }), key("r"))).toEqual([
|
|
597
|
+
{ type: "openRename" },
|
|
598
|
+
]);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("prompt overlay routes ⏎ to confirm and esc to cancel", () => {
|
|
602
|
+
// Arrange / Act / Assert
|
|
603
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("return"))).toEqual([
|
|
604
|
+
{ type: "confirmDialog" },
|
|
605
|
+
]);
|
|
606
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("escape"))).toEqual([
|
|
607
|
+
{ type: "closeOverlay" },
|
|
608
|
+
]);
|
|
609
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("a"))).toEqual([]);
|
|
269
610
|
});
|
|
270
611
|
});
|