@cueloop/client 0.1.0-alpha.16 → 0.1.0-alpha.18

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.
@@ -30,11 +30,11 @@ function key(name: string, shift = false): { name: string; shift: boolean } {
30
30
  }
31
31
 
32
32
  describe("compose overlay", () => {
33
- const s = state({ overlay: "compose" });
33
+ const keyState = state({ overlay: "compose" });
34
34
  const table: [string, Intent[]][] = [
35
- ["escape", [{ t: "closeOverlay" }]],
36
- ["return", [{ t: "saveCompose" }]],
37
- ["enter", [{ t: "saveCompose" }]],
35
+ ["escape", [{ type: "closeOverlay" }]],
36
+ ["return", [{ type: "saveCompose" }]],
37
+ ["enter", [{ type: "saveCompose" }]],
38
38
  ["left", []],
39
39
  ["right", []],
40
40
  ["j", []],
@@ -42,25 +42,25 @@ describe("compose overlay", () => {
42
42
  ];
43
43
  for (const [name, expected] of table) {
44
44
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
45
- expect(reduceKey(s, key(name))).toEqual(expected);
45
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
46
46
  });
47
47
  }
48
48
  });
49
49
 
50
50
  describe("submit overlay", () => {
51
- const s = state({ overlay: "submit" });
51
+ const keyState = state({ overlay: "submit" });
52
52
  const table: [string, Intent[]][] = [
53
- ["escape", [{ t: "closeOverlay" }]],
54
- ["return", [{ t: "submitVerdict" }]],
55
- ["enter", [{ t: "submitVerdict" }]],
56
- ["left", [{ t: "cycleVerdict", dir: -1 }]],
57
- ["right", [{ t: "cycleVerdict", dir: 1 }]],
53
+ ["escape", [{ type: "closeOverlay" }]],
54
+ ["return", [{ type: "submitVerdict" }]],
55
+ ["enter", [{ type: "submitVerdict" }]],
56
+ ["left", [{ type: "cycleVerdict", dir: -1 }]],
57
+ ["right", [{ type: "cycleVerdict", dir: 1 }]],
58
58
  ["j", []],
59
59
  ["q", []],
60
60
  ];
61
61
  for (const [name, expected] of table) {
62
62
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
63
- expect(reduceKey(s, key(name))).toEqual(expected);
63
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
64
64
  });
65
65
  }
66
66
  });
@@ -69,40 +69,40 @@ describe("completion overlay", () => {
69
69
  const prompt = state({ overlay: "completion-prompt" });
70
70
  const counting = state({ overlay: "completion-counting" });
71
71
  const table: [KeyState, string, Intent[]][] = [
72
- [prompt, "return", [{ t: "finishReview" }]],
73
- [prompt, "enter", [{ t: "finishReview" }]],
74
- [prompt, "q", [{ t: "finishReview" }]],
75
- [prompt, "a", [{ t: "optInAutoClose" }]],
76
- [prompt, "escape", [{ t: "dismissCompletion" }]],
72
+ [prompt, "return", [{ type: "finishReview" }]],
73
+ [prompt, "enter", [{ type: "finishReview" }]],
74
+ [prompt, "q", [{ type: "finishReview" }]],
75
+ [prompt, "a", [{ type: "optInAutoClose" }]],
76
+ [prompt, "escape", [{ type: "dismissCompletion" }]],
77
77
  [prompt, "j", []],
78
- [counting, "return", [{ t: "finishReview" }]],
79
- [counting, "q", [{ t: "finishReview" }]],
80
- [counting, "a", [{ t: "optInAutoClose" }]],
81
- [counting, "escape", [{ t: "dismissCompletion" }]],
78
+ [counting, "return", [{ type: "finishReview" }]],
79
+ [counting, "q", [{ type: "finishReview" }]],
80
+ [counting, "a", [{ type: "optInAutoClose" }]],
81
+ [counting, "escape", [{ type: "dismissCompletion" }]],
82
82
  ];
83
- for (const [s, name, expected] of table) {
84
- test(`${s.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
85
- expect(reduceKey(s, key(name))).toEqual(expected);
83
+ for (const [overlayState, name, expected] of table) {
84
+ test(`${overlayState.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
85
+ expect(reduceKey(overlayState, key(name))).toEqual(expected);
86
86
  });
87
87
  }
88
88
  });
89
89
 
90
90
  describe("inbox mode", () => {
91
- const s = state({ view: "inbox" });
91
+ const keyState = state({ view: "inbox" });
92
92
  const table: [string, Intent[]][] = [
93
- ["j", [{ t: "inboxMove", to: "down" }]],
94
- ["down", [{ t: "inboxMove", to: "down" }]],
95
- ["k", [{ t: "inboxMove", to: "up" }]],
96
- ["up", [{ t: "inboxMove", to: "up" }]],
97
- ["return", [{ t: "openSession" }]],
98
- ["enter", [{ t: "openSession" }]],
99
- ["q", [{ t: "exit" }]],
93
+ ["j", [{ type: "inboxMove", to: "down" }]],
94
+ ["down", [{ type: "inboxMove", to: "down" }]],
95
+ ["k", [{ type: "inboxMove", to: "up" }]],
96
+ ["up", [{ type: "inboxMove", to: "up" }]],
97
+ ["return", [{ type: "openSession" }]],
98
+ ["enter", [{ type: "openSession" }]],
99
+ ["q", [{ type: "exit" }]],
100
100
  ["c", []],
101
101
  ["escape", []],
102
102
  ];
103
103
  for (const [name, expected] of table) {
104
104
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
105
- expect(reduceKey(s, key(name))).toEqual(expected);
105
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
106
106
  });
107
107
  }
108
108
 
@@ -111,142 +111,142 @@ describe("inbox mode", () => {
111
111
  for (const name of ["j", "k", "down", "up", "return", "enter"]) {
112
112
  expect(reduceKey(empty, key(name))).toEqual([]);
113
113
  }
114
- expect(reduceKey(empty, key("q"))).toEqual([{ t: "exit" }]);
114
+ expect(reduceKey(empty, key("q"))).toEqual([{ type: "exit" }]);
115
115
  });
116
116
  });
117
117
 
118
118
  describe("plan normal mode", () => {
119
- const s = state();
119
+ const keyState = state();
120
120
  const table: [string, boolean, Intent[]][] = [
121
- ["j", false, [{ t: "move", to: "down" }]],
122
- ["down", false, [{ t: "move", to: "down" }]],
123
- ["k", false, [{ t: "move", to: "up" }]],
124
- ["up", false, [{ t: "move", to: "up" }]],
125
- ["g", false, [{ t: "move", to: "top" }]],
126
- ["g", true, [{ t: "move", to: "bottom" }]],
127
- ["v", false, [{ t: "startSpan" }]],
128
- ["c", false, [{ t: "openCompose", kind: "comment", from: "cursor" }]],
129
- ["s", false, [{ t: "openCompose", kind: "suggestion", from: "cursor" }]],
121
+ ["j", false, [{ type: "move", to: "down" }]],
122
+ ["down", false, [{ type: "move", to: "down" }]],
123
+ ["k", false, [{ type: "move", to: "up" }]],
124
+ ["up", false, [{ type: "move", to: "up" }]],
125
+ ["g", false, [{ type: "move", to: "top" }]],
126
+ ["g", true, [{ type: "move", to: "bottom" }]],
127
+ ["v", false, [{ type: "startSpan" }]],
128
+ ["c", false, [{ type: "openCompose", kind: "comment", from: "cursor" }]],
129
+ ["s", false, [{ type: "openCompose", kind: "suggestion", from: "cursor" }]],
130
130
  // the default state has a selected card, so Cut and edit act on the card
131
- ["x", false, [{ t: "removeAnnotation" }]],
132
- ["e", false, [{ t: "editCard" }]],
133
- ["n", false, [{ t: "nextAnn" }]],
134
- ["p", false, [{ t: "prevAnn" }]],
135
- ["backspace", false, [{ t: "removeAnnotation" }]],
136
- ["return", false, [{ t: "openSubmit" }]],
137
- ["q", false, [{ t: "exit" }]],
138
- ["escape", false, [{ t: "deselect" }]],
131
+ ["x", false, [{ type: "removeAnnotation" }]],
132
+ ["e", false, [{ type: "editCard" }]],
133
+ ["n", false, [{ type: "nextAnnotation" }]],
134
+ ["p", false, [{ type: "prevAnnotation" }]],
135
+ ["backspace", false, [{ type: "removeAnnotation" }]],
136
+ ["return", false, [{ type: "openSubmit" }]],
137
+ ["q", false, [{ type: "exit" }]],
138
+ ["escape", false, [{ type: "deselect" }]],
139
139
  ["z", false, []],
140
140
  ];
141
141
  for (const [name, shift, expected] of table) {
142
142
  test(`${shift ? "shift+" : ""}${name} -> ${JSON.stringify(expected)}`, () => {
143
- expect(reduceKey(s, key(name, shift))).toEqual(expected);
143
+ expect(reduceKey(keyState, key(name, shift))).toEqual(expected);
144
144
  });
145
145
  }
146
146
 
147
147
  test("resolved sessions guard the mutating verbs", () => {
148
- const r = state({ resolved: true });
148
+ const resolvedState = state({ resolved: true });
149
149
  for (const name of ["c", "s", "x", "e"]) {
150
- expect(reduceKey(r, key(name))).toEqual([{ t: "status", msg: "review submitted - read-only" }]);
150
+ expect(reduceKey(resolvedState, key(name))).toEqual([{ type: "status", message: "review submitted - read-only" }]);
151
151
  }
152
- expect(reduceKey(r, key("return"))).toEqual([]);
153
- expect(reduceKey(r, key("backspace"))).toEqual([]);
152
+ expect(reduceKey(resolvedState, key("return"))).toEqual([]);
153
+ expect(reduceKey(resolvedState, key("backspace"))).toEqual([]);
154
154
  });
155
155
 
156
156
  test("cut text cannot host a comment or a span", () => {
157
157
  const cut = state({ cursorAnnotatable: false });
158
- expect(reduceKey(cut, key("c"))).toEqual([{ t: "status", msg: "text is cut - restore it first" }]);
159
- expect(reduceKey(cut, key("s"))).toEqual([{ t: "status", msg: "text is cut - restore it first" }]);
158
+ expect(reduceKey(cut, key("c"))).toEqual([{ type: "status", message: "text is cut - restore it first" }]);
159
+ expect(reduceKey(cut, key("s"))).toEqual([{ type: "status", message: "text is cut - restore it first" }]);
160
160
  expect(reduceKey(cut, key("v"))).toEqual([]);
161
161
  });
162
162
 
163
163
  test("annotation navigation without annotations reports, delete without focus is silent", () => {
164
164
  const none = state({ annotationCount: 0, hasFocusedAnnotation: false });
165
- expect(reduceKey(none, key("n"))).toEqual([{ t: "status", msg: "no annotations" }]);
166
- expect(reduceKey(none, key("p"))).toEqual([{ t: "status", msg: "no annotations" }]);
165
+ expect(reduceKey(none, key("n"))).toEqual([{ type: "status", message: "no annotations" }]);
166
+ expect(reduceKey(none, key("p"))).toEqual([{ type: "status", message: "no annotations" }]);
167
167
  expect(reduceKey(none, key("backspace"))).toEqual([]);
168
168
  });
169
169
 
170
170
  test("without a selected card x cuts the block and e opens the editor", () => {
171
171
  const unfocused = state({ hasFocusedAnnotation: false });
172
- expect(reduceKey(unfocused, key("x"))).toEqual([{ t: "cut" }]);
173
- expect(reduceKey(unfocused, key("e"))).toEqual([{ t: "edit" }]);
172
+ expect(reduceKey(unfocused, key("x"))).toEqual([{ type: "cut" }]);
173
+ expect(reduceKey(unfocused, key("e"))).toEqual([{ type: "edit" }]);
174
174
  });
175
175
  });
176
176
 
177
177
  describe("span mode", () => {
178
- const s = state({ spanMode: true });
178
+ const keyState = state({ spanMode: true });
179
179
  const table: [string, Intent[]][] = [
180
- ["l", [{ t: "spanKey", name: "l" }]],
181
- ["h", [{ t: "spanKey", name: "h" }]],
182
- ["w", [{ t: "spanKey", name: "w" }]],
183
- ["b", [{ t: "spanKey", name: "b" }]],
184
- ["$", [{ t: "spanKey", name: "$" }]],
185
- ["0", [{ t: "spanKey", name: "0" }]],
186
- ["c", [{ t: "openCompose", kind: "comment", from: "span" }]],
187
- ["s", [{ t: "openCompose", kind: "suggestion", from: "span" }]],
188
- ["escape", [{ t: "closeOverlay" }]],
189
- ["q", [{ t: "exit" }]],
180
+ ["l", [{ type: "spanKey", name: "l" }]],
181
+ ["h", [{ type: "spanKey", name: "h" }]],
182
+ ["w", [{ type: "spanKey", name: "w" }]],
183
+ ["b", [{ type: "spanKey", name: "b" }]],
184
+ ["$", [{ type: "spanKey", name: "$" }]],
185
+ ["0", [{ type: "spanKey", name: "0" }]],
186
+ ["c", [{ type: "openCompose", kind: "comment", from: "span" }]],
187
+ ["s", [{ type: "openCompose", kind: "suggestion", from: "span" }]],
188
+ ["escape", [{ type: "closeOverlay" }]],
189
+ ["q", [{ type: "exit" }]],
190
190
  ["j", []],
191
191
  ];
192
192
  for (const [name, expected] of table) {
193
193
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
194
- expect(reduceKey(s, key(name))).toEqual(expected);
194
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
195
195
  });
196
196
  }
197
197
  });
198
198
 
199
199
  describe("diff mode", () => {
200
- const s = state({ view: "diff" });
200
+ const keyState = state({ view: "diff" });
201
201
  const table: [string, Intent[]][] = [
202
- ["j", [{ t: "move", to: "down" }]],
203
- ["k", [{ t: "move", to: "up" }]],
204
- ["g", [{ t: "move", to: "top" }]],
205
- ["c", [{ t: "openCompose", kind: "comment", from: "cursor" }]],
206
- ["n", [{ t: "nextAnn" }]],
207
- ["p", [{ t: "prevAnn" }]],
208
- ["backspace", [{ t: "removeAnnotation" }]],
209
- ["return", [{ t: "openSubmit" }]],
210
- ["v", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
211
- ["x", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
212
- ["e", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
213
- ["s", [{ t: "status", msg: "plan-only verb - diff review uses c on a line" }]],
214
- ["q", [{ t: "exit" }]],
202
+ ["j", [{ type: "move", to: "down" }]],
203
+ ["k", [{ type: "move", to: "up" }]],
204
+ ["g", [{ type: "move", to: "top" }]],
205
+ ["c", [{ type: "openCompose", kind: "comment", from: "cursor" }]],
206
+ ["n", [{ type: "nextAnnotation" }]],
207
+ ["p", [{ type: "prevAnnotation" }]],
208
+ ["backspace", [{ type: "removeAnnotation" }]],
209
+ ["return", [{ type: "openSubmit" }]],
210
+ ["v", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
211
+ ["x", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
212
+ ["e", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
213
+ ["s", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
214
+ ["q", [{ type: "exit" }]],
215
215
  ];
216
216
  for (const [name, expected] of table) {
217
217
  test(`${name} -> ${JSON.stringify(expected)}`, () => {
218
- expect(reduceKey(s, key(name))).toEqual(expected);
218
+ expect(reduceKey(keyState, key(name))).toEqual(expected);
219
219
  });
220
220
  }
221
221
 
222
222
  test("comment guards: resolved first, then non-code rows", () => {
223
223
  expect(reduceKey(state({ view: "diff", resolved: true }), key("c"))).toEqual([
224
- { t: "status", msg: "review submitted - read-only" },
224
+ { type: "status", message: "review submitted - read-only" },
225
225
  ]);
226
226
  expect(reduceKey(state({ view: "diff", cursorAnnotatable: false }), key("c"))).toEqual([
227
- { t: "status", msg: "move to a code line to comment" },
227
+ { type: "status", message: "move to a code line to comment" },
228
228
  ]);
229
229
  });
230
230
  });
231
231
 
232
232
  describe("guided walk", () => {
233
233
  test("w enters the walk in diff view; resolved and read-only sessions answer", () => {
234
- expect(reduceKey(state({ view: "diff" }), key("w"))).toEqual([{ t: "walkStart" }]);
234
+ expect(reduceKey(state({ view: "diff" }), key("w"))).toEqual([{ type: "walkStart" }]);
235
235
  expect(reduceKey(state({ view: "diff", resolved: true }), key("w"))).toEqual([
236
- { t: "status", msg: "review submitted - read-only" },
236
+ { type: "status", message: "review submitted - read-only" },
237
237
  ]);
238
238
  expect(reduceKey(state({ view: "diff", readOnly: true }), key("w"))).toEqual([
239
- { t: "status", msg: "observer - read-only" },
239
+ { type: "status", message: "observer - read-only" },
240
240
  ]);
241
- expect(reduceKey(state(), key("w"))).toEqual([{ t: "status", msg: "the guided walk is a diff-review mode" }]);
241
+ expect(reduceKey(state(), key("w"))).toEqual([{ type: "status", message: "the guided walk is a diff-review mode" }]);
242
242
  });
243
243
 
244
244
  const walking = state({ view: "diff", overlay: "walk" });
245
245
  const table: [string, Intent[]][] = [
246
- ["]", [{ t: "walkForward" }]],
247
- ["[", [{ t: "walkBack" }]],
248
- ["escape", [{ t: "walkLeave" }]],
249
- ["q", [{ t: "exit" }]],
246
+ ["]", [{ type: "walkForward" }]],
247
+ ["[", [{ type: "walkBack" }]],
248
+ ["escape", [{ type: "walkLeave" }]],
249
+ ["q", [{ type: "exit" }]],
250
250
  // mid-walk return does nothing: submit lives on the end card only
251
251
  ["return", []],
252
252
  ["j", []],
@@ -260,8 +260,8 @@ describe("guided walk", () => {
260
260
 
261
261
  test("the end card's return leaves the walk and opens the submit confirm", () => {
262
262
  const atEnd = state({ view: "diff", overlay: "walk", walkAtEnd: true });
263
- expect(reduceKey(atEnd, key("return"))).toEqual([{ t: "walkLeave" }, { t: "openSubmit" }]);
264
- expect(reduceKey(atEnd, key("enter"))).toEqual([{ t: "walkLeave" }, { t: "openSubmit" }]);
263
+ expect(reduceKey(atEnd, key("return"))).toEqual([{ type: "walkLeave" }, { type: "openSubmit" }]);
264
+ expect(reduceKey(atEnd, key("enter"))).toEqual([{ type: "walkLeave" }, { type: "openSubmit" }]);
265
265
  });
266
266
  });
267
267
 
@@ -284,24 +284,24 @@ describe("duplicated-branch collapse", () => {
284
284
  describe("read-only filter", () => {
285
285
  test("every mutating key answers observer - read-only, in plan and diff", () => {
286
286
  for (const view of ["plan", "diff"] as const) {
287
- const s = state({ view, readOnly: true });
287
+ const keyState = state({ view, readOnly: true });
288
288
  for (const name of ["c", "s", "x", "e", "backspace", "return", "enter"]) {
289
- expect(reduceKey(s, key(name))).toEqual([{ t: "status", msg: "observer - read-only" }]);
289
+ expect(reduceKey(keyState, key(name))).toEqual([{ type: "status", message: "observer - read-only" }]);
290
290
  }
291
291
  }
292
292
  });
293
293
 
294
294
  test("navigation and annotation focus still work for observers", () => {
295
- const s = state({ readOnly: true });
296
- expect(reduceKey(s, key("j"))).toEqual([{ t: "move", to: "down" }]);
297
- expect(reduceKey(s, key("n"))).toEqual([{ t: "nextAnn" }]);
298
- expect(reduceKey(s, key("q"))).toEqual([{ t: "exit" }]);
295
+ const keyState = state({ readOnly: true });
296
+ expect(reduceKey(keyState, key("j"))).toEqual([{ type: "move", to: "down" }]);
297
+ expect(reduceKey(keyState, key("n"))).toEqual([{ type: "nextAnnotation" }]);
298
+ expect(reduceKey(keyState, key("q"))).toEqual([{ type: "exit" }]);
299
299
  });
300
300
 
301
301
  test("span-mode c/s are gated by key name even under a rebound keymap", () => {
302
302
  const rebound = { ...DEFAULT_KEYS, comment: ["m"], suggest: ["t"] };
303
- const s = state({ spanMode: true, readOnly: true, keys: rebound });
304
- expect(reduceKey(s, key("c"))).toEqual([{ t: "status", msg: "observer - read-only" }]);
305
- expect(reduceKey(s, key("s"))).toEqual([{ t: "status", msg: "observer - read-only" }]);
303
+ const keyState = state({ spanMode: true, readOnly: true, keys: rebound });
304
+ expect(reduceKey(keyState, key("c"))).toEqual([{ type: "status", message: "observer - read-only" }]);
305
+ expect(reduceKey(keyState, key("s"))).toEqual([{ type: "status", message: "observer - read-only" }]);
306
306
  });
307
307
  });
package/src/keymap.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The keyboard grammar as a pure reducer (#70): view state in, intents out.
2
+ * The keyboard grammar as a pure reducer: view state in, intents out.
3
3
  * App builds a KeyState per key event, calls reduceKey, and dispatches the
4
4
  * intents to controller verbs and view-state setters - no branch bodies live
5
5
  * in the key handler. Diff and plan reviews share one path for annotation
@@ -9,33 +9,33 @@
9
9
  import { actionFor } from "./config";
10
10
 
11
11
  export type Intent =
12
- | { t: "exit" }
13
- | { t: "status"; msg: string }
14
- | { t: "move"; to: "down" | "up" | "top" | "bottom" }
15
- | { t: "inboxMove"; to: "down" | "up" }
16
- | { t: "openSession" }
17
- | { t: "startSpan" }
18
- | { t: "spanKey"; name: string }
19
- | { t: "openCompose"; kind: "comment" | "suggestion"; from: "cursor" | "span" }
20
- | { t: "openSubmit" }
21
- | { t: "cut" }
22
- | { t: "edit" }
23
- | { t: "editCard" }
24
- | { t: "nextAnn" }
25
- | { t: "prevAnn" }
26
- | { t: "walkStart" }
27
- | { t: "walkForward" }
28
- | { t: "walkBack" }
29
- | { t: "walkLeave" }
30
- | { t: "removeAnnotation" }
31
- | { t: "deselect" }
32
- | { t: "closeOverlay" }
33
- | { t: "saveCompose" }
34
- | { t: "submitVerdict" }
35
- | { t: "cycleVerdict"; dir: -1 | 1 }
36
- | { t: "finishReview" }
37
- | { t: "optInAutoClose" }
38
- | { t: "dismissCompletion" };
12
+ | { type: "exit" }
13
+ | { type: "status"; message: string }
14
+ | { type: "move"; to: "down" | "up" | "top" | "bottom" }
15
+ | { type: "inboxMove"; to: "down" | "up" }
16
+ | { type: "openSession" }
17
+ | { type: "startSpan" }
18
+ | { type: "spanKey"; name: string }
19
+ | { type: "openCompose"; kind: "comment" | "suggestion"; from: "cursor" | "span" }
20
+ | { type: "openSubmit" }
21
+ | { type: "cut" }
22
+ | { type: "edit" }
23
+ | { type: "editCard" }
24
+ | { type: "nextAnnotation" }
25
+ | { type: "prevAnnotation" }
26
+ | { type: "walkStart" }
27
+ | { type: "walkForward" }
28
+ | { type: "walkBack" }
29
+ | { type: "walkLeave" }
30
+ | { type: "removeAnnotation" }
31
+ | { type: "deselect" }
32
+ | { type: "closeOverlay" }
33
+ | { type: "saveCompose" }
34
+ | { type: "submitVerdict" }
35
+ | { type: "cycleVerdict"; dir: -1 | 1 }
36
+ | { type: "finishReview" }
37
+ | { type: "optInAutoClose" }
38
+ | { type: "dismissCompletion" };
39
39
 
40
40
  export interface KeyInput {
41
41
  name: string;
@@ -66,8 +66,8 @@ const MUTATING_ACTIONS = new Set(["comment", "suggest", "cut", "edit", "delete_a
66
66
 
67
67
  const SPAN_KEYS = ["l", "h", "w", "b", "$", "0"];
68
68
 
69
- function status(msg: string): Intent[] {
70
- return [{ t: "status", msg }];
69
+ function status(message: string): Intent[] {
70
+ return [{ type: "status", message }];
71
71
  }
72
72
 
73
73
  /**
@@ -81,36 +81,36 @@ export function reduceKey(state: KeyState, key: KeyInput, resolvedAction?: strin
81
81
  // compose/submit overlays own the keys via focused inputs; only escape,
82
82
  // return, and verdict arrows route through the grammar
83
83
  if (state.overlay === "compose" || state.overlay === "submit") {
84
- if (name === "escape") return [{ t: "closeOverlay" }];
84
+ if (name === "escape") return [{ type: "closeOverlay" }];
85
85
  if (name === "return" || name === "enter") {
86
- return [state.overlay === "compose" ? { t: "saveCompose" } : { t: "submitVerdict" }];
86
+ return [state.overlay === "compose" ? { type: "saveCompose" } : { type: "submitVerdict" }];
87
87
  }
88
88
  if (state.overlay === "submit" && (name === "left" || name === "right")) {
89
- return [{ t: "cycleVerdict", dir: name === "left" ? -1 : 1 }];
89
+ return [{ type: "cycleVerdict", dir: name === "left" ? -1 : 1 }];
90
90
  }
91
91
  return [];
92
92
  }
93
93
  if (state.overlay === "completion-prompt" || state.overlay === "completion-counting") {
94
- if (name === "return" || name === "enter" || name === "q") return [{ t: "finishReview" }];
95
- if (name === "a") return [{ t: "optInAutoClose" }];
96
- if (name === "escape") return [{ t: "dismissCompletion" }];
94
+ if (name === "return" || name === "enter" || name === "q") return [{ type: "finishReview" }];
95
+ if (name === "a") return [{ type: "optInAutoClose" }];
96
+ if (name === "escape") return [{ type: "dismissCompletion" }];
97
97
  return [];
98
98
  }
99
99
  // the walk wizard owns its keys while active: ] advances (marking the
100
100
  // current file viewed), [ steps back, escape leaves keeping progress, and
101
101
  // return on the end card hands over to the submit confirm
102
102
  if (state.overlay === "walk") {
103
- if (name === "]") return [{ t: "walkForward" }];
104
- if (name === "[") return [{ t: "walkBack" }];
105
- if (name === "escape") return [{ t: "walkLeave" }];
103
+ if (name === "]") return [{ type: "walkForward" }];
104
+ if (name === "[") return [{ type: "walkBack" }];
105
+ if (name === "escape") return [{ type: "walkLeave" }];
106
106
  if ((name === "return" || name === "enter") && state.walkAtEnd) {
107
- return [{ t: "walkLeave" }, { t: "openSubmit" }];
107
+ return [{ type: "walkLeave" }, { type: "openSubmit" }];
108
108
  }
109
- if (name === "q") return [{ t: "exit" }];
109
+ if (name === "q") return [{ type: "exit" }];
110
110
  return [];
111
111
  }
112
112
  const action = resolvedAction ?? actionFor(state.keys, name, key.shift);
113
- if (action === "quit") return [{ t: "exit" }];
113
+ if (action === "quit") return [{ type: "exit" }];
114
114
  // the ONE read-only rule: any mutating attempt answers instead of acting
115
115
  // (span-mode c/s are hardwired keys, so they gate by name as well)
116
116
  const mutating = MUTATING_ACTIONS.has(action ?? "") || (state.spanMode && (name === "c" || name === "s"));
@@ -124,24 +124,24 @@ export function reduceKey(state: KeyState, key: KeyInput, resolvedAction?: strin
124
124
 
125
125
  function inboxGrammar(state: KeyState, name: string): Intent[] {
126
126
  if (!state.hasInboxItems) return [];
127
- if (name === "j" || name === "down") return [{ t: "inboxMove", to: "down" }];
128
- if (name === "k" || name === "up") return [{ t: "inboxMove", to: "up" }];
129
- if (name === "return" || name === "enter") return [{ t: "openSession" }];
127
+ if (name === "j" || name === "down") return [{ type: "inboxMove", to: "down" }];
128
+ if (name === "k" || name === "up") return [{ type: "inboxMove", to: "up" }];
129
+ if (name === "return" || name === "enter") return [{ type: "openSession" }];
130
130
  return [];
131
131
  }
132
132
 
133
133
  function diffGrammar(state: KeyState, action: string | undefined): Intent[] {
134
- const nav = navIntent(action);
135
- if (nav) return nav;
134
+ const navigation = navigationIntent(action);
135
+ if (navigation) return navigation;
136
136
  if (action === "walk") {
137
137
  // marking viewed writes the session record, so a resolved review answers
138
138
  if (state.resolved) return status("review submitted - read-only");
139
- return [{ t: "walkStart" }];
139
+ return [{ type: "walkStart" }];
140
140
  }
141
141
  if (action === "comment") {
142
142
  if (state.resolved) return status("review submitted - read-only");
143
143
  if (!state.cursorAnnotatable) return status("move to a code line to comment");
144
- return [{ t: "openCompose", kind: "comment", from: "cursor" }];
144
+ return [{ type: "openCompose", kind: "comment", from: "cursor" }];
145
145
  }
146
146
  const shared = annotationCluster(state, action);
147
147
  if (shared) return shared;
@@ -152,39 +152,39 @@ function diffGrammar(state: KeyState, action: string | undefined): Intent[] {
152
152
  }
153
153
 
154
154
  function spanGrammar(name: string): Intent[] {
155
- if (name === "escape") return [{ t: "closeOverlay" }];
156
- if (SPAN_KEYS.includes(name)) return [{ t: "spanKey", name }];
155
+ if (name === "escape") return [{ type: "closeOverlay" }];
156
+ if (SPAN_KEYS.includes(name)) return [{ type: "spanKey", name }];
157
157
  if (name === "c" || name === "s") {
158
- return [{ t: "openCompose", kind: name === "s" ? "suggestion" : "comment", from: "span" }];
158
+ return [{ type: "openCompose", kind: name === "s" ? "suggestion" : "comment", from: "span" }];
159
159
  }
160
160
  return [];
161
161
  }
162
162
 
163
163
  function planGrammar(state: KeyState, action: string | undefined, name: string): Intent[] {
164
- const nav = navIntent(action);
165
- if (nav) return nav;
164
+ const navigation = navigationIntent(action);
165
+ if (navigation) return navigation;
166
166
  if (action === "walk") return status("the guided walk is a diff-review mode");
167
- if (name === "escape") return [{ t: "deselect" }];
168
- if (action === "span") return state.cursorAnnotatable ? [{ t: "startSpan" }] : [];
167
+ if (name === "escape") return [{ type: "deselect" }];
168
+ if (action === "span") return state.cursorAnnotatable ? [{ type: "startSpan" }] : [];
169
169
  if (action === "comment" || action === "suggest") {
170
170
  if (state.resolved) return status("review submitted - read-only");
171
171
  if (!state.cursorAnnotatable) return status("text is cut - restore it first");
172
- return [{ t: "openCompose", kind: action === "suggest" ? "suggestion" : "comment", from: "cursor" }];
172
+ return [{ type: "openCompose", kind: action === "suggest" ? "suggestion" : "comment", from: "cursor" }];
173
173
  }
174
174
  if (action === "cut" || action === "edit") {
175
175
  if (state.resolved) return status("review submitted - read-only");
176
176
  // the document selects, the rail edits: with a card selected, Cut deletes
177
177
  // the annotation and edit rewrites the card body in place
178
- if (state.hasFocusedAnnotation) return [action === "cut" ? { t: "removeAnnotation" } : { t: "editCard" }];
179
- return [{ t: action }];
178
+ if (state.hasFocusedAnnotation) return [action === "cut" ? { type: "removeAnnotation" } : { type: "editCard" }];
179
+ return [{ type: action }];
180
180
  }
181
181
  return annotationCluster(state, action) ?? [];
182
182
  }
183
183
 
184
184
  /** Cursor movement is the same intent everywhere; views clamp their own bounds. */
185
- function navIntent(action: string | undefined): Intent[] | null {
185
+ function navigationIntent(action: string | undefined): Intent[] | null {
186
186
  if (action === "down" || action === "up" || action === "top" || action === "bottom") {
187
- return [{ t: "move", to: action }];
187
+ return [{ type: "move", to: action }];
188
188
  }
189
189
  return null;
190
190
  }
@@ -193,12 +193,12 @@ function navIntent(action: string | undefined): Intent[] | null {
193
193
  function annotationCluster(state: KeyState, action: string | undefined): Intent[] | null {
194
194
  if (action === "next_annotation" || action === "prev_annotation") {
195
195
  if (!state.annotationCount) return status("no annotations");
196
- return [{ t: action === "next_annotation" ? "nextAnn" : "prevAnn" }];
196
+ return [{ type: action === "next_annotation" ? "nextAnnotation" : "prevAnnotation" }];
197
197
  }
198
198
  if (action === "delete_annotation") {
199
199
  if (state.resolved || !state.hasFocusedAnnotation) return [];
200
- return [{ t: "removeAnnotation" }];
200
+ return [{ type: "removeAnnotation" }];
201
201
  }
202
- if (action === "submit") return state.resolved ? [] : [{ t: "openSubmit" }];
202
+ if (action === "submit") return state.resolved ? [] : [{ type: "openSubmit" }];
203
203
  return null;
204
204
  }
package/src/run.ts CHANGED
@@ -10,15 +10,15 @@ export interface RunClientOptions {
10
10
  home?: string;
11
11
  }
12
12
 
13
- export async function runClient(opts: RunClientOptions): Promise<number> {
13
+ export async function runClient(options: RunClientOptions): Promise<number> {
14
14
  // mouse movement reporting makes multiplexers forward drags to the app,
15
15
  // so the renderer's native selection is the drag driver
16
16
  const renderer = await createCliRenderer({ enableMouseMovement: true });
17
17
  return new Promise<number>((resolve) => {
18
18
  createRoot(renderer).render(
19
19
  React.createElement(App, {
20
- home: opts.home,
21
- sessionId: opts.sessionId,
20
+ home: options.home,
21
+ sessionId: options.sessionId,
22
22
  onExit: (code: number) => {
23
23
  renderer.destroy();
24
24
  resolve(code);