@cueloop/client 0.1.0-alpha.7 → 0.1.0-alpha.71
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 +39 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +76 -50
- package/src/App.diff.test.tsx +156 -42
- package/src/App.export.test.tsx +58 -40
- package/src/App.plan-review.test.tsx +363 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +128 -67
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.submit-confirm.test.tsx +191 -0
- package/src/App.test.tsx +310 -114
- package/src/App.toast.test.tsx +89 -0
- package/src/App.tsx +1043 -798
- package/src/App.walk.test.tsx +298 -0
- package/src/App.workbench.test.tsx +495 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +61 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +432 -0
- package/src/app-view-model.ts +159 -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 +126 -0
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +290 -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 +39 -0
- package/src/components/ChangesColumn.tsx +171 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +128 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -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 +859 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +368 -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/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -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 +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +83 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +87 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -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 +195 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +219 -0
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.slash.test.tsx +158 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +330 -0
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +71 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -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 +152 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +140 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -0
- package/src/components/diff-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/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -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 +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -0
- package/src/components/primitives/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/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 +96 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -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 +54 -0
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +150 -0
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +184 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +142 -0
- package/src/components/syntax-highlight.ts +110 -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/truncate-text.ts +7 -0
- package/src/config.test.ts +461 -31
- package/src/config.ts +389 -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 +165 -0
- package/src/diff-intraline.ts +146 -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/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +11 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +698 -0
- package/src/intent-dispatch.ts +700 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +465 -0
- package/src/keymap.test.ts +611 -0
- package/src/keymap.ts +410 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/mark-runs.ts +120 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +307 -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 +30 -9
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +20 -9
- package/src/share.ts +215 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.test.ts +54 -0
- package/src/slash-palette.ts +104 -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 +355 -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 +170 -0
- package/src/thread-chords.ts +169 -0
- package/src/thread-controller.curate.test.ts +438 -0
- package/src/thread-controller.live-diff.test.ts +199 -0
- package/src/thread-controller.share.test.ts +472 -0
- package/src/thread-controller.tree.test.ts +255 -0
- package/src/thread-controller.ts +1509 -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 +851 -0
- package/src/use-changes-workbench.ts +153 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +274 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +66 -9
- package/src/view-diff.ts +231 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +570 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -312
package/src/theme.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Semantic theme tokens
|
|
2
|
+
* Semantic theme tokens. Values carry over from the design-system
|
|
3
3
|
* prototypes (tokens.css). One built-in dark theme for now; the theme
|
|
4
4
|
* registry (palette-derived themes, terminal theme, user overrides) layers
|
|
5
5
|
* on top of these slots without touching consumers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export interface Theme {
|
|
9
|
-
|
|
9
|
+
background: string;
|
|
10
10
|
panel: string;
|
|
11
11
|
elevated: string;
|
|
12
12
|
border: string;
|
|
@@ -18,29 +18,94 @@ export interface Theme {
|
|
|
18
18
|
green: string;
|
|
19
19
|
red: string;
|
|
20
20
|
blue: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
cursorBackground: string;
|
|
22
|
+
markCommentBackground: string;
|
|
23
|
+
insertedForeground: string;
|
|
24
|
+
deletedForeground: string;
|
|
25
|
+
/** The band behind a whole added line, and the stronger tint under its intra-line changed words. */
|
|
26
|
+
insertedBackground: string;
|
|
27
|
+
insertedEmphasisBackground: string;
|
|
28
|
+
/** The band behind a whole removed line, and the stronger tint under its intra-line changed words. */
|
|
29
|
+
deletedBackground: string;
|
|
30
|
+
deletedEmphasisBackground: string;
|
|
31
|
+
/** Layer behind centered dialogs; transparent keeps the session visible. */
|
|
32
|
+
backdrop: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The dimmed variant behind the walk wizard: every reading color drops to
|
|
37
|
+
* the dim token so the file list stays legible context, not competition.
|
|
38
|
+
* Layout tokens (backgrounds, cursor) keep their values.
|
|
39
|
+
*/
|
|
40
|
+
export function dimmedTheme(theme: Theme): Theme {
|
|
41
|
+
return {
|
|
42
|
+
...theme,
|
|
43
|
+
text: theme.textDim,
|
|
44
|
+
textMuted: theme.textDim,
|
|
45
|
+
accent: theme.textDim,
|
|
46
|
+
green: theme.textDim,
|
|
47
|
+
red: theme.textDim,
|
|
48
|
+
blue: theme.textDim,
|
|
49
|
+
insertedForeground: theme.textDim,
|
|
50
|
+
deletedForeground: theme.textDim,
|
|
51
|
+
insertedBackground: theme.panel,
|
|
52
|
+
insertedEmphasisBackground: theme.elevated,
|
|
53
|
+
deletedBackground: theme.panel,
|
|
54
|
+
deletedEmphasisBackground: theme.elevated,
|
|
55
|
+
};
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
export const DARK: Theme = {
|
|
29
|
-
|
|
30
|
-
|
|
59
|
+
/* Unpainted so the terminal background shows through; override in [theme]. */
|
|
60
|
+
background: "transparent",
|
|
61
|
+
panel: "transparent",
|
|
31
62
|
elevated: "#23232f",
|
|
32
63
|
border: "#3a3a4a",
|
|
33
64
|
text: "#e4e6ec",
|
|
34
65
|
textMuted: "#b1b6c4",
|
|
35
66
|
textDim: "#6b7280",
|
|
36
|
-
accent: "#
|
|
37
|
-
accentInk: "#
|
|
67
|
+
accent: "#cba6f7",
|
|
68
|
+
accentInk: "#17171a",
|
|
38
69
|
green: "#62d96b",
|
|
39
|
-
red: "#
|
|
40
|
-
blue: "#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
red: "#f08080",
|
|
71
|
+
blue: "#a9c8f5",
|
|
72
|
+
cursorBackground: "#33262a",
|
|
73
|
+
markCommentBackground: "#3d2a2e",
|
|
74
|
+
insertedForeground: "#62d96b",
|
|
75
|
+
deletedForeground: "#ff6b6b",
|
|
76
|
+
insertedBackground: "#16321d",
|
|
77
|
+
insertedEmphasisBackground: "#245c30",
|
|
78
|
+
deletedBackground: "#341c1f",
|
|
79
|
+
deletedEmphasisBackground: "#5a2a2e",
|
|
80
|
+
backdrop: "transparent",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The branded transparent theme for a LIGHT terminal: same unpainted
|
|
85
|
+
* background, but every reading color darkens so text is legible on a white
|
|
86
|
+
* terminal instead of light-on-light. Chosen when the terminal reports a light
|
|
87
|
+
* background; the dark variant above is the default and the fallback.
|
|
88
|
+
*/
|
|
89
|
+
export const LIGHT: Theme = {
|
|
90
|
+
background: "transparent",
|
|
91
|
+
panel: "transparent",
|
|
92
|
+
elevated: "#ececf2",
|
|
93
|
+
border: "#c7c7d2",
|
|
94
|
+
text: "#1c1d24",
|
|
95
|
+
textMuted: "#4b5162",
|
|
96
|
+
textDim: "#868c9c",
|
|
97
|
+
accent: "#5b3ea6",
|
|
98
|
+
accentInk: "#fbfbfa",
|
|
99
|
+
green: "#2f8a3e",
|
|
100
|
+
red: "#c0392b",
|
|
101
|
+
blue: "#2f6fb0",
|
|
102
|
+
cursorBackground: "#e6e2ea",
|
|
103
|
+
markCommentBackground: "#f2e2e8",
|
|
104
|
+
insertedForeground: "#2f8a3e",
|
|
105
|
+
deletedForeground: "#c0392b",
|
|
106
|
+
insertedBackground: "#d8f5dd",
|
|
107
|
+
insertedEmphasisBackground: "#a9e6b6",
|
|
108
|
+
deletedBackground: "#fbdcdc",
|
|
109
|
+
deletedEmphasisBackground: "#f5b7b7",
|
|
110
|
+
backdrop: "transparent",
|
|
111
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveThreadChord } from "./thread-chords";
|
|
3
|
+
|
|
4
|
+
const owner = { composing: false, isOwner: true, resolved: false, treeActive: false };
|
|
5
|
+
|
|
6
|
+
describe("resolveThreadChord", () => {
|
|
7
|
+
test("ctrl+enter opens the submit overlay for an owner with an open review", () => {
|
|
8
|
+
// Assert: the terminal-safe chord and the cmd forms all submit
|
|
9
|
+
expect(resolveThreadChord({ name: "return", ctrl: true }, owner)).toEqual({
|
|
10
|
+
type: "openSubmit",
|
|
11
|
+
});
|
|
12
|
+
expect(resolveThreadChord({ name: "return", super: true }, owner)).toEqual({
|
|
13
|
+
type: "openSubmit",
|
|
14
|
+
});
|
|
15
|
+
expect(resolveThreadChord({ name: "enter", meta: true }, owner)).toEqual({
|
|
16
|
+
type: "openSubmit",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("session chords map to their intents", () => {
|
|
21
|
+
// Assert
|
|
22
|
+
expect(resolveThreadChord({ name: "e", ctrl: true }, owner)).toEqual({ type: "edit" });
|
|
23
|
+
expect(resolveThreadChord({ name: "s", ctrl: true }, owner)).toEqual({ type: "share" });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("in a diff, option chords act on the caret's row and file; the rest still reach the rail", () => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const diff = { ...owner, isDiff: true };
|
|
29
|
+
|
|
30
|
+
// Assert - the code-row primitives
|
|
31
|
+
expect(resolveThreadChord({ name: "x", meta: true }, diff)).toEqual({ type: "rejectChange" });
|
|
32
|
+
expect(resolveThreadChord({ name: "X", meta: true }, diff)).toEqual({ type: "rejectHunk" });
|
|
33
|
+
expect(resolveThreadChord({ name: "u", meta: true }, diff)).toEqual({
|
|
34
|
+
type: "restoreCuration",
|
|
35
|
+
});
|
|
36
|
+
expect(resolveThreadChord({ name: "c", meta: true }, diff)).toEqual({ type: "foldFile" });
|
|
37
|
+
expect(resolveThreadChord({ name: "d", meta: true }, diff)).toEqual({ type: "toggleDiffView" });
|
|
38
|
+
expect(resolveThreadChord({ name: "k", meta: true }, diff)).toEqual({ type: "walkStart" });
|
|
39
|
+
// the rail letters are untouched
|
|
40
|
+
expect(resolveThreadChord({ name: "n", meta: true }, diff)).toEqual({ type: "nextAnnotation" });
|
|
41
|
+
// rejecting is the owner's, and closed once resolved
|
|
42
|
+
expect(resolveThreadChord({ name: "x", meta: true }, { ...diff, isOwner: false })).toEqual({
|
|
43
|
+
type: "status",
|
|
44
|
+
message: "observer - read-only",
|
|
45
|
+
});
|
|
46
|
+
expect(resolveThreadChord({ name: "k", meta: true }, { ...diff, resolved: true })).toEqual({
|
|
47
|
+
type: "status",
|
|
48
|
+
message: "review submitted - read-only",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("option chords drive the rail and curation with the plan sheet's letters", () => {
|
|
53
|
+
// Assert
|
|
54
|
+
expect(resolveThreadChord({ name: "n", meta: true }, owner)).toEqual({
|
|
55
|
+
type: "nextAnnotation",
|
|
56
|
+
});
|
|
57
|
+
expect(resolveThreadChord({ name: "p", meta: true }, owner)).toEqual({
|
|
58
|
+
type: "prevAnnotation",
|
|
59
|
+
});
|
|
60
|
+
expect(resolveThreadChord({ name: "e", meta: true }, owner)).toEqual({ type: "editCard" });
|
|
61
|
+
expect(resolveThreadChord({ name: "backspace", meta: true }, owner)).toEqual({
|
|
62
|
+
type: "removeAnnotation",
|
|
63
|
+
});
|
|
64
|
+
expect(resolveThreadChord({ name: "r", meta: true }, owner)).toEqual({ type: "openRename" });
|
|
65
|
+
expect(resolveThreadChord({ name: "x", meta: true }, owner)).toEqual({ type: "cut" });
|
|
66
|
+
expect(resolveThreadChord({ name: "u", meta: true }, owner)).toEqual({
|
|
67
|
+
type: "restoreCuration",
|
|
68
|
+
});
|
|
69
|
+
expect(resolveThreadChord({ name: "x", meta: true }, { ...owner, isOwner: false })).toEqual({
|
|
70
|
+
type: "status",
|
|
71
|
+
message: "observer - read-only",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an open composer swallows every chord, so typing is never hijacked", () => {
|
|
76
|
+
// Assert
|
|
77
|
+
expect(
|
|
78
|
+
resolveThreadChord({ name: "return", ctrl: true }, { ...owner, composing: true }),
|
|
79
|
+
).toBeNull();
|
|
80
|
+
expect(resolveThreadChord({ name: "s", ctrl: true }, { ...owner, composing: true })).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("plain keys and unbound chords belong to the document grammar", () => {
|
|
84
|
+
// Assert
|
|
85
|
+
expect(resolveThreadChord({ name: "return" }, owner)).toBeNull();
|
|
86
|
+
expect(resolveThreadChord({ name: "s" }, owner)).toBeNull();
|
|
87
|
+
expect(resolveThreadChord({ name: "x", ctrl: true }, owner)).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a collaborator is told a primitive is read-only, and a resolved review cannot be resubmitted", () => {
|
|
91
|
+
// Assert
|
|
92
|
+
const collaborator = { ...owner, isOwner: false };
|
|
93
|
+
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
94
|
+
|
|
95
|
+
expect(resolveThreadChord({ name: "return", ctrl: true }, collaborator)).toEqual(readOnly);
|
|
96
|
+
expect(resolveThreadChord({ name: "e", ctrl: true }, collaborator)).toEqual(readOnly);
|
|
97
|
+
expect(resolveThreadChord({ name: "s", ctrl: true }, collaborator)).toEqual(readOnly);
|
|
98
|
+
expect(
|
|
99
|
+
resolveThreadChord({ name: "return", ctrl: true }, { ...owner, resolved: true }),
|
|
100
|
+
).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a resolved review refuses every change and says so; renaming an author stays open", () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const resolved = { ...owner, resolved: true };
|
|
106
|
+
const submitted = { type: "status" as const, message: "review submitted - read-only" };
|
|
107
|
+
|
|
108
|
+
// Assert
|
|
109
|
+
expect(resolveThreadChord({ name: "e", ctrl: true }, resolved)).toEqual(submitted);
|
|
110
|
+
expect(resolveThreadChord({ name: "e", meta: true }, resolved)).toEqual(submitted);
|
|
111
|
+
expect(resolveThreadChord({ name: "backspace", meta: true }, resolved)).toEqual(submitted);
|
|
112
|
+
expect(resolveThreadChord({ name: "x", meta: true }, resolved)).toEqual(submitted);
|
|
113
|
+
expect(resolveThreadChord({ name: "u", meta: true }, resolved)).toEqual(submitted);
|
|
114
|
+
expect(resolveThreadChord({ name: "r", meta: true }, resolved)).toEqual({ type: "openRename" });
|
|
115
|
+
expect(resolveThreadChord({ name: "s", ctrl: true }, resolved)).toEqual({ type: "share" });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("the tree chords: toggle, move on the Tree tab, go, branch, label, fork, hand off", () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const onTree = { ...owner, treeActive: true };
|
|
121
|
+
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
122
|
+
const submitted = { type: "status" as const, message: "review submitted - read-only" };
|
|
123
|
+
|
|
124
|
+
// Assert: next / previous follow the tab that shows
|
|
125
|
+
expect(resolveThreadChord({ name: "t", meta: true }, owner)).toEqual({ type: "toggleTree" });
|
|
126
|
+
expect(resolveThreadChord({ name: "n", meta: true }, onTree)).toEqual({
|
|
127
|
+
type: "treeMove",
|
|
128
|
+
direction: 1,
|
|
129
|
+
});
|
|
130
|
+
expect(resolveThreadChord({ name: "p", meta: true }, onTree)).toEqual({
|
|
131
|
+
type: "treeMove",
|
|
132
|
+
direction: -1,
|
|
133
|
+
});
|
|
134
|
+
expect(resolveThreadChord({ name: "n", meta: true }, owner)).toEqual({
|
|
135
|
+
type: "nextAnnotation",
|
|
136
|
+
});
|
|
137
|
+
expect(resolveThreadChord({ name: "g", meta: true }, owner)).toEqual({ type: "treeGo" });
|
|
138
|
+
expect(resolveThreadChord({ name: "b", meta: true }, owner)).toEqual({ type: "treeBranch" });
|
|
139
|
+
expect(resolveThreadChord({ name: "l", meta: true }, owner)).toEqual({ type: "treeLabel" });
|
|
140
|
+
expect(resolveThreadChord({ name: "f", meta: true }, owner)).toEqual({ type: "treeFork" });
|
|
141
|
+
expect(resolveThreadChord({ name: "h", meta: true }, owner)).toEqual({ type: "treeForkShare" });
|
|
142
|
+
// moving the tree is the owner's; a fork is open after a verdict, a move is not
|
|
143
|
+
expect(resolveThreadChord({ name: "g", meta: true }, { ...owner, isOwner: false })).toEqual(
|
|
144
|
+
readOnly,
|
|
145
|
+
);
|
|
146
|
+
expect(resolveThreadChord({ name: "f", meta: true }, { ...owner, isOwner: false })).toEqual(
|
|
147
|
+
readOnly,
|
|
148
|
+
);
|
|
149
|
+
expect(resolveThreadChord({ name: "b", meta: true }, { ...owner, resolved: true })).toEqual(
|
|
150
|
+
submitted,
|
|
151
|
+
);
|
|
152
|
+
expect(resolveThreadChord({ name: "f", meta: true }, { ...owner, resolved: true })).toEqual({
|
|
153
|
+
type: "treeFork",
|
|
154
|
+
});
|
|
155
|
+
expect(resolveThreadChord({ name: "t", meta: true }, { ...owner, isOwner: false })).toEqual({
|
|
156
|
+
type: "toggleTree",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("an observer cannot edit or delete cards either", () => {
|
|
161
|
+
// Arrange
|
|
162
|
+
const observer = { ...owner, isOwner: false };
|
|
163
|
+
const readOnly = { type: "status" as const, message: "observer - read-only" };
|
|
164
|
+
|
|
165
|
+
// Assert
|
|
166
|
+
expect(resolveThreadChord({ name: "e", meta: true }, observer)).toEqual(readOnly);
|
|
167
|
+
expect(resolveThreadChord({ name: "backspace", meta: true }, observer)).toEqual(readOnly);
|
|
168
|
+
expect(resolveThreadChord({ name: "r", meta: true }, observer)).toEqual({ type: "openRename" });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-level chords of the thread view. Letters in the thread view
|
|
3
|
+
* type a comment, so everything that acts on the session as a whole - submit,
|
|
4
|
+
* share, edit, the rail - lives on ctrl chords (cmd where the terminal
|
|
5
|
+
* delivers it). The guided walk is a diff-review mode and has no chord here. Pure: the view reports whether a composer is open, this maps
|
|
6
|
+
* a key to an intent or to nothing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Intent } from "./keymap";
|
|
10
|
+
|
|
11
|
+
export interface ThreadChordKey {
|
|
12
|
+
name: string;
|
|
13
|
+
ctrl?: boolean;
|
|
14
|
+
meta?: boolean;
|
|
15
|
+
super?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ThreadChordContext {
|
|
19
|
+
/** A composer owns the keyboard: chords never fire while one is open. */
|
|
20
|
+
composing: boolean;
|
|
21
|
+
/** Collaborators annotate only; submit, edit, and share are the owner's. */
|
|
22
|
+
isOwner: boolean;
|
|
23
|
+
/** A resolved session has nothing left to submit. */
|
|
24
|
+
resolved: boolean;
|
|
25
|
+
/** The rail shows the tree: next / previous move its selection instead of the cards. */
|
|
26
|
+
treeActive: boolean;
|
|
27
|
+
/** A diff review: the block chords act on the code row under the caret (reject, fold, walk). */
|
|
28
|
+
isDiff?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The diff-review chords: option plus a letter, acting on the caret's row or file. */
|
|
32
|
+
export const DIFF_CHORD_ENTRIES = [
|
|
33
|
+
{ keys: "⌥x / ⌥X", label: "reject the change / the hunk" },
|
|
34
|
+
{ keys: "⌥u", label: "restore the last rejection" },
|
|
35
|
+
{ keys: "⌥c", label: "collapse the file to its band" },
|
|
36
|
+
{ keys: "⌥d", label: "split / stacked (when wide)" },
|
|
37
|
+
{ keys: "⌥k", label: "start the guided walk" },
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
/** The cheatsheet rows for the chords, in the same order they are resolved. */
|
|
41
|
+
export const THREAD_CHORD_ENTRIES = [
|
|
42
|
+
{ keys: "⌃enter", label: "submit the review" },
|
|
43
|
+
{ keys: "⌃e", label: "edit in $EDITOR" },
|
|
44
|
+
{ keys: "⌃s", label: "share" },
|
|
45
|
+
{ keys: "⌃r", label: "cycle the rail" },
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
/** The rail and curation chords: option (alt) plus the plan sheet's old letter. */
|
|
49
|
+
export const RAIL_CHORD_ENTRIES = [
|
|
50
|
+
{ keys: "⌥n / ⌥p", label: "next / previous card" },
|
|
51
|
+
{ keys: "⌥e", label: "edit the card" },
|
|
52
|
+
{ keys: "⌥⌫", label: "delete the card" },
|
|
53
|
+
{ keys: "⌥r", label: "rename the author" },
|
|
54
|
+
{ keys: "⌥x", label: "cut the block" },
|
|
55
|
+
{ keys: "⌥u", label: "restore the last cut" },
|
|
56
|
+
{ keys: "⌥w / ⌥s", label: "widen / narrow the rail" },
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
/** The tree chords: option plus a letter, all on the rail's Tree tab. */
|
|
60
|
+
export const TREE_CHORD_ENTRIES = [
|
|
61
|
+
{ keys: "⌥t", label: "show / hide the tree" },
|
|
62
|
+
{ keys: "⌥n / ⌥p", label: "next / previous entry" },
|
|
63
|
+
{ keys: "⌥g", label: "go to the entry" },
|
|
64
|
+
{ keys: "⌥b", label: "branch off the tip" },
|
|
65
|
+
{ keys: "⌥l", label: "label a checkpoint" },
|
|
66
|
+
{ keys: "⌥f", label: "fork the path" },
|
|
67
|
+
{ keys: "⌥h", label: "fork and hand off" },
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
export function resolveThreadChord(
|
|
71
|
+
key: ThreadChordKey,
|
|
72
|
+
context: ThreadChordContext,
|
|
73
|
+
): Intent | null {
|
|
74
|
+
if (context.composing) return null;
|
|
75
|
+
const modified = Boolean(key.ctrl || key.meta || key.super);
|
|
76
|
+
|
|
77
|
+
if (!modified) return null;
|
|
78
|
+
if (key.name === "return" || key.name === "enter") {
|
|
79
|
+
if (!context.isOwner) return READ_ONLY;
|
|
80
|
+
|
|
81
|
+
return context.resolved ? null : { type: "openSubmit" };
|
|
82
|
+
}
|
|
83
|
+
if (key.ctrl) return resolveSessionChord(key.name, context);
|
|
84
|
+
if (key.meta)
|
|
85
|
+
return (
|
|
86
|
+
(context.isDiff && resolveDiffChord(key.name, context)) || resolveRailChord(key.name, context)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The diff's row and file chords; null lets the rail chords answer the letter. */
|
|
93
|
+
function resolveDiffChord(name: string, context: ThreadChordContext): Intent | null {
|
|
94
|
+
switch (name) {
|
|
95
|
+
case "x":
|
|
96
|
+
return mutating({ type: "rejectChange" }, context);
|
|
97
|
+
case "X":
|
|
98
|
+
return mutating({ type: "rejectHunk" }, context);
|
|
99
|
+
case "c":
|
|
100
|
+
return { type: "foldFile" };
|
|
101
|
+
case "d":
|
|
102
|
+
return { type: "toggleDiffView" };
|
|
103
|
+
case "k":
|
|
104
|
+
return context.resolved ? RESOLVED : { type: "walkStart" };
|
|
105
|
+
default:
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The answers a blocked primitive gets - the same words the keymap uses. */
|
|
111
|
+
const READ_ONLY: Intent = { type: "status", message: "observer - read-only" };
|
|
112
|
+
const RESOLVED: Intent = { type: "status", message: "review submitted - read-only" };
|
|
113
|
+
|
|
114
|
+
/** Editing, deleting, cutting, and restoring change the review: gated by role and by a verdict. */
|
|
115
|
+
function mutating(intent: Intent, context: ThreadChordContext): Intent {
|
|
116
|
+
if (!context.isOwner) return READ_ONLY;
|
|
117
|
+
if (context.resolved) return RESOLVED;
|
|
118
|
+
|
|
119
|
+
return intent;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveSessionChord(name: string, context: ThreadChordContext): Intent | null {
|
|
123
|
+
switch (name) {
|
|
124
|
+
case "e":
|
|
125
|
+
return mutating({ type: "edit" }, context);
|
|
126
|
+
case "s":
|
|
127
|
+
return context.isOwner ? { type: "share" } : READ_ONLY;
|
|
128
|
+
default:
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// option arrives as meta in this terminal stack; the letters mirror the
|
|
134
|
+
// plan sheet's keymap so the rail keeps its muscle memory
|
|
135
|
+
function resolveRailChord(name: string, context: ThreadChordContext): Intent | null {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case "n":
|
|
138
|
+
return context.treeActive ? { type: "treeMove", direction: 1 } : { type: "nextAnnotation" };
|
|
139
|
+
case "p":
|
|
140
|
+
return context.treeActive ? { type: "treeMove", direction: -1 } : { type: "prevAnnotation" };
|
|
141
|
+
case "t":
|
|
142
|
+
return { type: "toggleTree" };
|
|
143
|
+
// the tree is the owner's: a move, a branch, or a label changes what everyone sees
|
|
144
|
+
case "g":
|
|
145
|
+
return mutating({ type: "treeGo" }, context);
|
|
146
|
+
case "b":
|
|
147
|
+
return mutating({ type: "treeBranch" }, context);
|
|
148
|
+
case "l":
|
|
149
|
+
return mutating({ type: "treeLabel" }, context);
|
|
150
|
+
// a fork can be taken from a resolved review: only the role gates it
|
|
151
|
+
case "f":
|
|
152
|
+
return context.isOwner ? { type: "treeFork" } : READ_ONLY;
|
|
153
|
+
case "h":
|
|
154
|
+
return context.isOwner ? { type: "treeForkShare" } : READ_ONLY;
|
|
155
|
+
case "e":
|
|
156
|
+
return mutating({ type: "editCard" }, context);
|
|
157
|
+
case "backspace":
|
|
158
|
+
return mutating({ type: "removeAnnotation" }, context);
|
|
159
|
+
// renaming an author is a local display choice, open to every role
|
|
160
|
+
case "r":
|
|
161
|
+
return { type: "openRename" };
|
|
162
|
+
case "x":
|
|
163
|
+
return mutating({ type: "cut" }, context);
|
|
164
|
+
case "u":
|
|
165
|
+
return mutating({ type: "restoreCuration" }, context);
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|