@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.
Files changed (248) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +39 -32
  8. package/src/App.appearance.test.tsx +53 -0
  9. package/src/App.autoclose.test.tsx +76 -50
  10. package/src/App.diff.test.tsx +156 -42
  11. package/src/App.export.test.tsx +58 -40
  12. package/src/App.plan-review.test.tsx +363 -0
  13. package/src/App.prototype-markdown.test.tsx +92 -0
  14. package/src/App.prototype.test.tsx +176 -0
  15. package/src/App.readonly.test.tsx +128 -67
  16. package/src/App.rendering.test.tsx +61 -35
  17. package/src/App.submit-confirm.test.tsx +191 -0
  18. package/src/App.test.tsx +310 -114
  19. package/src/App.toast.test.tsx +89 -0
  20. package/src/App.tsx +1043 -798
  21. package/src/App.walk.test.tsx +298 -0
  22. package/src/App.workbench.test.tsx +495 -0
  23. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  24. package/src/annotation-palette.ts +61 -0
  25. package/src/app-key-state.ts +61 -0
  26. package/src/app-screens.tsx +432 -0
  27. package/src/app-view-model.ts +159 -0
  28. package/src/attribution.test.ts +31 -0
  29. package/src/attribution.ts +42 -0
  30. package/src/clipboard.test.ts +23 -0
  31. package/src/clipboard.ts +33 -0
  32. package/src/components/AnnotatableFileView.stories.tsx +42 -0
  33. package/src/components/AnnotatableFileView.test.tsx +102 -0
  34. package/src/components/AnnotatableFileView.tsx +126 -0
  35. package/src/components/AnnotationCards.stories.tsx +87 -0
  36. package/src/components/AnnotationCards.tsx +303 -0
  37. package/src/components/AppHeader.stories.tsx +50 -0
  38. package/src/components/AppHeader.tsx +83 -0
  39. package/src/components/AppShell.stories.tsx +89 -0
  40. package/src/components/AppShell.tsx +290 -0
  41. package/src/components/AppShell.zoom.test.tsx +135 -0
  42. package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
  43. package/src/components/BareWorkbenchFileView.tsx +64 -0
  44. package/src/components/Breadcrumb.stories.tsx +19 -0
  45. package/src/components/Breadcrumb.tsx +45 -0
  46. package/src/components/ChangesColumn.stories.tsx +39 -0
  47. package/src/components/ChangesColumn.tsx +171 -0
  48. package/src/components/CodeBlock.stories.tsx +42 -0
  49. package/src/components/CodeBlock.tsx +84 -0
  50. package/src/components/CompletionOverlay.stories.tsx +23 -0
  51. package/src/components/CompletionOverlay.tsx +60 -0
  52. package/src/components/ConfirmCard.stories.tsx +35 -0
  53. package/src/components/ConfirmCard.tsx +128 -0
  54. package/src/components/ConfirmDialog.stories.tsx +21 -0
  55. package/src/components/ConfirmDialog.tsx +66 -0
  56. package/src/components/DiffContentView.scroll.test.tsx +121 -0
  57. package/src/components/DiffContentView.split.test.tsx +140 -0
  58. package/src/components/DiffContentView.stories.tsx +80 -0
  59. package/src/components/DiffContentView.syntax.test.tsx +82 -0
  60. package/src/components/DiffContentView.tsx +859 -0
  61. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  62. package/src/components/DiscussionMarkerRail.tsx +181 -0
  63. package/src/components/EditorGrid.stories.tsx +59 -0
  64. package/src/components/EditorGrid.tabs.test.tsx +118 -0
  65. package/src/components/EditorGrid.tsx +368 -0
  66. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  67. package/src/components/FileContentsView.stories.tsx +16 -0
  68. package/src/components/FileContentsView.test.tsx +29 -0
  69. package/src/components/FileContentsView.tsx +98 -0
  70. package/src/components/KeybindsDialog.stories.tsx +31 -0
  71. package/src/components/KeybindsDialog.tsx +103 -0
  72. package/src/components/MenuBar.stories.tsx +28 -0
  73. package/src/components/MenuBar.tsx +44 -0
  74. package/src/components/OverlayScrollbar.stories.tsx +33 -0
  75. package/src/components/OverlayScrollbar.test.tsx +139 -0
  76. package/src/components/OverlayScrollbar.tsx +129 -0
  77. package/src/components/PaneGrid.stories.tsx +84 -0
  78. package/src/components/PaneGrid.tsx +128 -0
  79. package/src/components/PanelColumn.stories.tsx +25 -0
  80. package/src/components/PanelColumn.tsx +95 -0
  81. package/src/components/ProjectTreeView.stories.tsx +25 -0
  82. package/src/components/ProjectTreeView.test.tsx +32 -0
  83. package/src/components/ProjectTreeView.tsx +83 -0
  84. package/src/components/PromptDialog.stories.tsx +36 -0
  85. package/src/components/PromptDialog.tsx +87 -0
  86. package/src/components/PrototypeContentView.stories.tsx +19 -0
  87. package/src/components/PrototypeContentView.tsx +446 -0
  88. package/src/components/RootOverlay.stories.tsx +44 -0
  89. package/src/components/RootOverlay.tsx +35 -0
  90. package/src/components/ScrollArea.stories.tsx +20 -0
  91. package/src/components/ScrollArea.tsx +44 -0
  92. package/src/components/SettingsDialog.stories.tsx +117 -0
  93. package/src/components/SettingsDialog.tsx +195 -0
  94. package/src/components/SettingsRows.stories.tsx +18 -0
  95. package/src/components/SettingsRows.tsx +112 -0
  96. package/src/components/ShellHeader.stories.tsx +50 -0
  97. package/src/components/ShellHeader.tsx +137 -0
  98. package/src/components/ThreadFooter.stories.tsx +30 -0
  99. package/src/components/ThreadFooter.test.tsx +31 -0
  100. package/src/components/ThreadFooter.tsx +54 -0
  101. package/src/components/ThreadTree.stories.tsx +46 -0
  102. package/src/components/ThreadTree.tsx +219 -0
  103. package/src/components/ThreadView.selection.test.tsx +812 -0
  104. package/src/components/ThreadView.slash.test.tsx +158 -0
  105. package/src/components/ThreadView.stories.tsx +26 -0
  106. package/src/components/ThreadView.tsx +330 -0
  107. package/src/components/ThreadsSidebar.stories.tsx +36 -0
  108. package/src/components/ThreadsSidebar.tsx +71 -0
  109. package/src/components/Toast.stories.tsx +12 -0
  110. package/src/components/Toast.tsx +45 -0
  111. package/src/components/Tooltip.stories.tsx +26 -0
  112. package/src/components/Tooltip.tsx +72 -0
  113. package/src/components/TreePane.stories.tsx +106 -0
  114. package/src/components/TreePane.tsx +157 -0
  115. package/src/components/WalkWizard.stories.tsx +100 -0
  116. package/src/components/WalkWizard.tsx +152 -0
  117. package/src/components/WelcomePlayground.stories.tsx +12 -0
  118. package/src/components/WelcomePlayground.test.tsx +47 -0
  119. package/src/components/WelcomePlayground.tsx +140 -0
  120. package/src/components/__snapshots__/stories.test.tsx.snap +1845 -0
  121. package/src/components/agent-launcher.stories.tsx +26 -0
  122. package/src/components/agent-launcher.test.tsx +50 -0
  123. package/src/components/agent-launcher.tsx +149 -0
  124. package/src/components/diff-content-view-layout.test.ts +198 -0
  125. package/src/components/diff-content-view-layout.ts +84 -0
  126. package/src/components/editor-grid.test.ts +86 -0
  127. package/src/components/editor-grid.ts +138 -0
  128. package/src/components/fade-title.test.ts +49 -0
  129. package/src/components/fade-title.ts +61 -0
  130. package/src/components/file-tree.test.ts +60 -0
  131. package/src/components/file-tree.ts +112 -0
  132. package/src/components/pages/Workspace.stories.tsx +181 -0
  133. package/src/components/primitives/Button.stories.tsx +32 -0
  134. package/src/components/primitives/Button.tsx +52 -0
  135. package/src/components/primitives/Card.stories.tsx +25 -0
  136. package/src/components/primitives/Card.tsx +60 -0
  137. package/src/components/primitives/Dialog.stories.tsx +24 -0
  138. package/src/components/primitives/Dialog.tsx +66 -0
  139. package/src/components/primitives/IconButton.stories.tsx +36 -0
  140. package/src/components/primitives/IconButton.tsx +62 -0
  141. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  142. package/src/components/primitives/StatusBar.tsx +23 -0
  143. package/src/components/primitives/Tabs.stories.tsx +29 -0
  144. package/src/components/primitives/Tabs.tsx +102 -0
  145. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  146. package/src/components/primitives/Toolbar.tsx +14 -0
  147. package/src/components/primitives/Tree.stories.tsx +124 -0
  148. package/src/components/primitives/Tree.tsx +96 -0
  149. package/src/components/primitives/frame.test.ts +12 -0
  150. package/src/components/primitives/frame.ts +14 -0
  151. package/src/components/primitives/icons.ts +61 -0
  152. package/src/components/primitives/tree-model.test.ts +99 -0
  153. package/src/components/primitives/tree-model.ts +137 -0
  154. package/src/components/quick-actions-editor.stories.tsx +54 -0
  155. package/src/components/quick-actions-editor.test.tsx +49 -0
  156. package/src/components/quick-actions-editor.tsx +150 -0
  157. package/src/components/session-tree.test.ts +134 -0
  158. package/src/components/session-tree.ts +108 -0
  159. package/src/components/stories-app.tsx +184 -0
  160. package/src/components/stories.test.tsx +66 -0
  161. package/src/components/story-fixtures.ts +102 -0
  162. package/src/components/story-tree.test.ts +100 -0
  163. package/src/components/story.ts +142 -0
  164. package/src/components/syntax-highlight.ts +110 -0
  165. package/src/components/terminal-pane.test.tsx +40 -0
  166. package/src/components/terminal-pane.ts +199 -0
  167. package/src/components/theme-context.stories.tsx +46 -0
  168. package/src/components/theme-context.tsx +37 -0
  169. package/src/components/truncate-text.ts +7 -0
  170. package/src/config.test.ts +461 -31
  171. package/src/config.ts +389 -57
  172. package/src/diff-fold.test.ts +78 -0
  173. package/src/diff-fold.ts +126 -0
  174. package/src/diff-hunk-curate.test.ts +315 -0
  175. package/src/diff-hunk-curate.ts +153 -0
  176. package/src/diff-intraline.test.ts +165 -0
  177. package/src/diff-intraline.ts +146 -0
  178. package/src/diff-syntax.test.ts +112 -0
  179. package/src/diff-syntax.ts +179 -0
  180. package/src/discussions.ts +79 -0
  181. package/src/editor.test.ts +149 -0
  182. package/src/editor.ts +139 -9
  183. package/src/ghostty-terminal.test.ts +85 -0
  184. package/src/ghostty-terminal.ts +199 -0
  185. package/src/herdr.test.ts +9 -34
  186. package/src/herdr.ts +25 -21
  187. package/src/index.ts +11 -0
  188. package/src/integrations.test.ts +41 -0
  189. package/src/integrations.ts +32 -0
  190. package/src/intent-dispatch.test.ts +698 -0
  191. package/src/intent-dispatch.ts +700 -0
  192. package/src/key-bindings.test.ts +156 -0
  193. package/src/key-bindings.ts +465 -0
  194. package/src/keymap.test.ts +611 -0
  195. package/src/keymap.ts +410 -0
  196. package/src/kitty-image.test.ts +66 -0
  197. package/src/kitty-image.ts +138 -0
  198. package/src/mark-runs.ts +120 -0
  199. package/src/prototype-browser.test.ts +56 -0
  200. package/src/prototype-browser.ts +307 -0
  201. package/src/prototype-pixels.tsx +35 -0
  202. package/src/pty.test.ts +81 -0
  203. package/src/pty.ts +245 -0
  204. package/src/ready-signal.test.ts +85 -0
  205. package/src/ready-signal.ts +52 -0
  206. package/src/run.ts +30 -9
  207. package/src/serve.test.ts +24 -12
  208. package/src/serve.ts +20 -9
  209. package/src/share.ts +215 -0
  210. package/src/skills.test.ts +74 -0
  211. package/src/skills.ts +68 -0
  212. package/src/slash-palette.test.ts +54 -0
  213. package/src/slash-palette.ts +104 -0
  214. package/src/split-diff.test.ts +110 -0
  215. package/src/split-diff.ts +149 -0
  216. package/src/test-support.test.ts +264 -0
  217. package/src/test-support.ts +355 -0
  218. package/src/theme-presets.test.ts +51 -0
  219. package/src/theme-presets.ts +214 -0
  220. package/src/theme.ts +84 -19
  221. package/src/thread-chords.test.ts +170 -0
  222. package/src/thread-chords.ts +169 -0
  223. package/src/thread-controller.curate.test.ts +438 -0
  224. package/src/thread-controller.live-diff.test.ts +199 -0
  225. package/src/thread-controller.share.test.ts +472 -0
  226. package/src/thread-controller.tree.test.ts +255 -0
  227. package/src/thread-controller.ts +1509 -0
  228. package/src/thread-selection.test.ts +210 -0
  229. package/src/thread-selection.ts +243 -0
  230. package/src/tree-view.test.ts +128 -0
  231. package/src/tree-view.ts +148 -0
  232. package/src/use-annotation-surface.tsx +851 -0
  233. package/src/use-changes-workbench.ts +153 -0
  234. package/src/use-frame-measure.ts +38 -0
  235. package/src/use-settings-dialog.tsx +274 -0
  236. package/src/use-terminal-virtualizer.ts +233 -0
  237. package/src/version.ts +5 -0
  238. package/src/view-diff.test.ts +66 -9
  239. package/src/view-diff.ts +231 -23
  240. package/src/view-plan.regression.test.ts +60 -0
  241. package/src/view-plan.test.ts +406 -0
  242. package/src/view-plan.ts +570 -0
  243. package/src/walk.test.ts +121 -0
  244. package/src/walk.ts +74 -0
  245. package/src/syntax.test.ts +0 -41
  246. package/src/syntax.ts +0 -88
  247. package/src/view.test.ts +0 -157
  248. package/src/view.ts +0 -312
package/src/theme.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Semantic theme tokens (#22). Values carry over from the design-system
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
- bg: string;
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
- cursorBg: string;
22
- markCommentBg: string;
23
- markSuggestionBg: string;
24
- insFg: string;
25
- delFg: string;
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
- bg: "#1f1f29",
30
- panel: "#1a1a23",
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: "#f5a3a3",
37
- accentInk: "#2a1416",
67
+ accent: "#cba6f7",
68
+ accentInk: "#17171a",
38
69
  green: "#62d96b",
39
- red: "#ff6b6b",
40
- blue: "#84a6e8",
41
- cursorBg: "#33262a",
42
- markCommentBg: "#3d2a2e",
43
- markSuggestionBg: "#20351f",
44
- insFg: "#62d96b",
45
- delFg: "#ff6b6b",
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
+ }