@cueloop/client 0.1.0-alpha.16 → 0.1.0-alpha.17
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/package.json +5 -5
- package/src/App.plan-review.test.tsx +1 -1
- package/src/App.readonly.test.tsx +3 -3
- package/src/App.rendering.test.tsx +16 -16
- package/src/App.test.tsx +1 -1
- package/src/App.tsx +137 -138
- package/src/App.walk.test.tsx +1 -1
- package/src/components/AnnotationCard.tsx +4 -4
- package/src/components/ComposeBar.tsx +2 -2
- package/src/components/DiffSheet.tsx +8 -8
- package/src/components/PlanSheet.stories.tsx +3 -3
- package/src/components/PlanSheet.tsx +31 -31
- package/src/components/WalkWizard.tsx +1 -1
- package/src/components/stories.test.tsx +1 -1
- package/src/components/story-fixtures.ts +1 -1
- package/src/components/story.ts +1 -1
- package/src/components/syntax-highlight.ts +1 -1
- package/src/components/theme-context.tsx +1 -1
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +29 -29
- package/src/config.ts +12 -12
- package/src/editor.ts +1 -1
- package/src/herdr.ts +1 -1
- package/src/key-bindings.ts +1 -1
- package/src/keymap.test.ts +111 -111
- package/src/keymap.ts +64 -64
- package/src/run.ts +3 -3
- package/src/serve.test.ts +6 -6
- package/src/serve.ts +7 -7
- package/src/session-controller.ts +71 -71
- package/src/theme.ts +1 -1
- package/src/view-diff.test.ts +8 -8
- package/src/view-diff.ts +22 -22
- package/src/{view.test.ts → view-plan.test.ts} +48 -48
- package/src/view-plan.ts +362 -0
- package/src/walk.ts +4 -4
- package/src/components/format.ts +0 -6
- package/src/view.ts +0 -341
package/src/keymap.test.ts
CHANGED
|
@@ -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
|
|
33
|
+
const keyState = state({ overlay: "compose" });
|
|
34
34
|
const table: [string, Intent[]][] = [
|
|
35
|
-
["escape", [{
|
|
36
|
-
["return", [{
|
|
37
|
-
["enter", [{
|
|
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(
|
|
45
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
describe("submit overlay", () => {
|
|
51
|
-
const
|
|
51
|
+
const keyState = state({ overlay: "submit" });
|
|
52
52
|
const table: [string, Intent[]][] = [
|
|
53
|
-
["escape", [{
|
|
54
|
-
["return", [{
|
|
55
|
-
["enter", [{
|
|
56
|
-
["left", [{
|
|
57
|
-
["right", [{
|
|
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(
|
|
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", [{
|
|
73
|
-
[prompt, "enter", [{
|
|
74
|
-
[prompt, "q", [{
|
|
75
|
-
[prompt, "a", [{
|
|
76
|
-
[prompt, "escape", [{
|
|
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", [{
|
|
79
|
-
[counting, "q", [{
|
|
80
|
-
[counting, "a", [{
|
|
81
|
-
[counting, "escape", [{
|
|
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 [
|
|
84
|
-
test(`${
|
|
85
|
-
expect(reduceKey(
|
|
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
|
|
91
|
+
const keyState = state({ view: "inbox" });
|
|
92
92
|
const table: [string, Intent[]][] = [
|
|
93
|
-
["j", [{
|
|
94
|
-
["down", [{
|
|
95
|
-
["k", [{
|
|
96
|
-
["up", [{
|
|
97
|
-
["return", [{
|
|
98
|
-
["enter", [{
|
|
99
|
-
["q", [{
|
|
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(
|
|
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([{
|
|
114
|
+
expect(reduceKey(empty, key("q"))).toEqual([{ type: "exit" }]);
|
|
115
115
|
});
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
describe("plan normal mode", () => {
|
|
119
|
-
const
|
|
119
|
+
const keyState = state();
|
|
120
120
|
const table: [string, boolean, Intent[]][] = [
|
|
121
|
-
["j", false, [{
|
|
122
|
-
["down", false, [{
|
|
123
|
-
["k", false, [{
|
|
124
|
-
["up", false, [{
|
|
125
|
-
["g", false, [{
|
|
126
|
-
["g", true, [{
|
|
127
|
-
["v", false, [{
|
|
128
|
-
["c", false, [{
|
|
129
|
-
["s", false, [{
|
|
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, [{
|
|
132
|
-
["e", false, [{
|
|
133
|
-
["n", false, [{
|
|
134
|
-
["p", false, [{
|
|
135
|
-
["backspace", false, [{
|
|
136
|
-
["return", false, [{
|
|
137
|
-
["q", false, [{
|
|
138
|
-
["escape", false, [{
|
|
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(
|
|
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
|
|
148
|
+
const resolvedState = state({ resolved: true });
|
|
149
149
|
for (const name of ["c", "s", "x", "e"]) {
|
|
150
|
-
expect(reduceKey(
|
|
150
|
+
expect(reduceKey(resolvedState, key(name))).toEqual([{ type: "status", message: "review submitted - read-only" }]);
|
|
151
151
|
}
|
|
152
|
-
expect(reduceKey(
|
|
153
|
-
expect(reduceKey(
|
|
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([{
|
|
159
|
-
expect(reduceKey(cut, key("s"))).toEqual([{
|
|
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([{
|
|
166
|
-
expect(reduceKey(none, key("p"))).toEqual([{
|
|
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([{
|
|
173
|
-
expect(reduceKey(unfocused, key("e"))).toEqual([{
|
|
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
|
|
178
|
+
const keyState = state({ spanMode: true });
|
|
179
179
|
const table: [string, Intent[]][] = [
|
|
180
|
-
["l", [{
|
|
181
|
-
["h", [{
|
|
182
|
-
["w", [{
|
|
183
|
-
["b", [{
|
|
184
|
-
["$", [{
|
|
185
|
-
["0", [{
|
|
186
|
-
["c", [{
|
|
187
|
-
["s", [{
|
|
188
|
-
["escape", [{
|
|
189
|
-
["q", [{
|
|
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(
|
|
194
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
describe("diff mode", () => {
|
|
200
|
-
const
|
|
200
|
+
const keyState = state({ view: "diff" });
|
|
201
201
|
const table: [string, Intent[]][] = [
|
|
202
|
-
["j", [{
|
|
203
|
-
["k", [{
|
|
204
|
-
["g", [{
|
|
205
|
-
["c", [{
|
|
206
|
-
["n", [{
|
|
207
|
-
["p", [{
|
|
208
|
-
["backspace", [{
|
|
209
|
-
["return", [{
|
|
210
|
-
["v", [{
|
|
211
|
-
["x", [{
|
|
212
|
-
["e", [{
|
|
213
|
-
["s", [{
|
|
214
|
-
["q", [{
|
|
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(
|
|
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
|
-
{
|
|
224
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
225
225
|
]);
|
|
226
226
|
expect(reduceKey(state({ view: "diff", cursorAnnotatable: false }), key("c"))).toEqual([
|
|
227
|
-
{
|
|
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([{
|
|
234
|
+
expect(reduceKey(state({ view: "diff" }), key("w"))).toEqual([{ type: "walkStart" }]);
|
|
235
235
|
expect(reduceKey(state({ view: "diff", resolved: true }), key("w"))).toEqual([
|
|
236
|
-
{
|
|
236
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
237
237
|
]);
|
|
238
238
|
expect(reduceKey(state({ view: "diff", readOnly: true }), key("w"))).toEqual([
|
|
239
|
-
{
|
|
239
|
+
{ type: "status", message: "observer - read-only" },
|
|
240
240
|
]);
|
|
241
|
-
expect(reduceKey(state(), key("w"))).toEqual([{
|
|
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
|
-
["]", [{
|
|
247
|
-
["[", [{
|
|
248
|
-
["escape", [{
|
|
249
|
-
["q", [{
|
|
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([{
|
|
264
|
-
expect(reduceKey(atEnd, key("enter"))).toEqual([{
|
|
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
|
|
287
|
+
const keyState = state({ view, readOnly: true });
|
|
288
288
|
for (const name of ["c", "s", "x", "e", "backspace", "return", "enter"]) {
|
|
289
|
-
expect(reduceKey(
|
|
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
|
|
296
|
-
expect(reduceKey(
|
|
297
|
-
expect(reduceKey(
|
|
298
|
-
expect(reduceKey(
|
|
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
|
|
304
|
-
expect(reduceKey(
|
|
305
|
-
expect(reduceKey(
|
|
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
|
|
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
|
-
| {
|
|
13
|
-
| {
|
|
14
|
-
| {
|
|
15
|
-
| {
|
|
16
|
-
| {
|
|
17
|
-
| {
|
|
18
|
-
| {
|
|
19
|
-
| {
|
|
20
|
-
| {
|
|
21
|
-
| {
|
|
22
|
-
| {
|
|
23
|
-
| {
|
|
24
|
-
| {
|
|
25
|
-
| {
|
|
26
|
-
| {
|
|
27
|
-
| {
|
|
28
|
-
| {
|
|
29
|
-
| {
|
|
30
|
-
| {
|
|
31
|
-
| {
|
|
32
|
-
| {
|
|
33
|
-
| {
|
|
34
|
-
| {
|
|
35
|
-
| {
|
|
36
|
-
| {
|
|
37
|
-
| {
|
|
38
|
-
| {
|
|
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(
|
|
70
|
-
return [{
|
|
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 [{
|
|
84
|
+
if (name === "escape") return [{ type: "closeOverlay" }];
|
|
85
85
|
if (name === "return" || name === "enter") {
|
|
86
|
-
return [state.overlay === "compose" ? {
|
|
86
|
+
return [state.overlay === "compose" ? { type: "saveCompose" } : { type: "submitVerdict" }];
|
|
87
87
|
}
|
|
88
88
|
if (state.overlay === "submit" && (name === "left" || name === "right")) {
|
|
89
|
-
return [{
|
|
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 [{
|
|
95
|
-
if (name === "a") return [{
|
|
96
|
-
if (name === "escape") return [{
|
|
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 [{
|
|
104
|
-
if (name === "[") return [{
|
|
105
|
-
if (name === "escape") return [{
|
|
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 [{
|
|
107
|
+
return [{ type: "walkLeave" }, { type: "openSubmit" }];
|
|
108
108
|
}
|
|
109
|
-
if (name === "q") return [{
|
|
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 [{
|
|
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 [{
|
|
128
|
-
if (name === "k" || name === "up") return [{
|
|
129
|
-
if (name === "return" || name === "enter") return [{
|
|
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
|
|
135
|
-
if (
|
|
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 [{
|
|
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 [{
|
|
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 [{
|
|
156
|
-
if (SPAN_KEYS.includes(name)) return [{
|
|
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 [{
|
|
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
|
|
165
|
-
if (
|
|
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 [{
|
|
168
|
-
if (action === "span") return state.cursorAnnotatable ? [{
|
|
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 [{
|
|
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" ? {
|
|
179
|
-
return [{
|
|
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
|
|
185
|
+
function navigationIntent(action: string | undefined): Intent[] | null {
|
|
186
186
|
if (action === "down" || action === "up" || action === "top" || action === "bottom") {
|
|
187
|
-
return [{
|
|
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 [{
|
|
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 [{
|
|
200
|
+
return [{ type: "removeAnnotation" }];
|
|
201
201
|
}
|
|
202
|
-
if (action === "submit") return state.resolved ? [] : [{
|
|
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(
|
|
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:
|
|
21
|
-
sessionId:
|
|
20
|
+
home: options.home,
|
|
21
|
+
sessionId: options.sessionId,
|
|
22
22
|
onExit: (code: number) => {
|
|
23
23
|
renderer.destroy();
|
|
24
24
|
resolve(code);
|