@cueloop/client 0.1.0-alpha.33 → 0.1.0-alpha.34
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 +27 -26
- package/src/App.autoclose.test.tsx +7 -4
- package/src/App.diff.test.tsx +130 -4
- package/src/App.export.test.tsx +22 -4
- package/src/App.plan-review.test.tsx +127 -27
- package/src/App.readonly.test.tsx +31 -8
- package/src/App.rendering.test.tsx +12 -3
- package/src/App.review-panel.test.tsx +11 -3
- package/src/App.submit-confirm.test.tsx +12 -7
- package/src/App.test.tsx +46 -9
- package/src/App.toast.test.tsx +10 -3
- package/src/App.tsx +400 -100
- package/src/App.walk.test.tsx +30 -10
- package/src/attribution.ts +8 -2
- package/src/clipboard.ts +5 -1
- package/src/components/AnnotationCard.stories.tsx +28 -9
- package/src/components/AnnotationCard.tsx +53 -38
- package/src/components/Breadcrumb.tsx +7 -1
- package/src/components/CodeBlock.stories.tsx +19 -2
- package/src/components/CodeBlock.tsx +24 -9
- package/src/components/CompletionOverlay.tsx +13 -3
- package/src/components/ConfirmCard.stories.tsx +22 -3
- package/src/components/ConfirmCard.tsx +22 -5
- package/src/components/ConfirmDialog.tsx +9 -1
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +18 -2
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +184 -98
- package/src/components/InboxList.stories.tsx +16 -3
- package/src/components/InboxList.tsx +17 -10
- package/src/components/KeybindsDialog.tsx +19 -3
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +5 -1
- package/src/components/MenuBar.tsx +18 -2
- package/src/components/PlanSheet.stories.tsx +34 -2
- package/src/components/PlanSheet.tsx +141 -32
- package/src/components/PromptDialog.tsx +18 -2
- package/src/components/ReviewPanel.stories.tsx +13 -3
- package/src/components/ReviewPanel.tsx +8 -4
- package/src/components/ReviewRail.stories.tsx +145 -72
- package/src/components/ReviewRail.tsx +177 -38
- package/src/components/SettingsDialog.stories.tsx +51 -3
- package/src/components/SettingsDialog.tsx +8 -3
- package/src/components/SettingsRows.tsx +19 -3
- package/src/components/Toast.tsx +17 -2
- package/src/components/WalkWizard.stories.tsx +13 -2
- package/src/components/WalkWizard.tsx +9 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +281 -83
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.tsx +5 -1
- package/src/components/primitives/Dialog.tsx +9 -1
- package/src/components/primitives/Tabs.tsx +24 -4
- package/src/components/stories-app.tsx +34 -6
- package/src/components/story-fixtures.ts +11 -3
- package/src/components/story.ts +3 -1
- package/src/components/syntax-highlight.ts +47 -12
- package/src/components/theme-context.stories.tsx +6 -1
- package/src/components/theme-context.tsx +7 -1
- package/src/config.test.ts +178 -2
- package/src/config.ts +121 -23
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +13 -4
- package/src/editor.ts +31 -5
- package/src/herdr.ts +8 -2
- package/src/index.ts +9 -1
- package/src/integrations.ts +3 -1
- package/src/intent-dispatch.test.ts +193 -9
- package/src/intent-dispatch.ts +151 -18
- package/src/key-bindings.test.ts +19 -9
- package/src/key-bindings.ts +36 -14
- package/src/keymap.test.ts +171 -26
- package/src/keymap.ts +83 -16
- package/src/review-panel.test.ts +3 -1
- package/src/review-panel.ts +9 -2
- package/src/serve.test.ts +11 -3
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +65 -17
- package/src/session-controller.ts +301 -26
- package/src/share.ts +41 -8
- package/src/test-support.ts +16 -6
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +1 -3
- package/src/view-diff.ts +15 -4
- package/src/view-plan.test.ts +40 -6
- package/src/view-plan.ts +71 -28
- package/src/walk.test.ts +7 -2
- package/src/walk.ts +7 -2
package/src/key-bindings.test.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { describe, expect, test } from "bun:test";
|
|
|
4
4
|
import { DEFAULT_KEYS } from "./config";
|
|
5
5
|
import { KeyBindings } from "./key-bindings";
|
|
6
6
|
|
|
7
|
-
function bindings(
|
|
7
|
+
function bindings(
|
|
8
|
+
context: { overlay?: "none" | "walk" | "compose" | "submit"; spanMode?: boolean } = {},
|
|
9
|
+
): KeyBindings {
|
|
8
10
|
const resolver = new KeyBindings(DEFAULT_KEYS);
|
|
9
11
|
resolver.setContext({ overlay: context.overlay ?? "none", spanMode: context.spanMode ?? false });
|
|
10
12
|
return resolver;
|
|
@@ -19,7 +21,7 @@ describe("grammar-layer resolution", () => {
|
|
|
19
21
|
["g", true, "bottom"],
|
|
20
22
|
["v", false, "span"],
|
|
21
23
|
["c", false, "comment"],
|
|
22
|
-
["s", false,
|
|
24
|
+
["s", false, undefined],
|
|
23
25
|
["x", false, "cut"],
|
|
24
26
|
["e", false, "edit"],
|
|
25
27
|
["n", false, "next_annotation"],
|
|
@@ -86,26 +88,32 @@ describe("mode layers own their keys", () => {
|
|
|
86
88
|
describe("getActiveKeys-generated status hints", () => {
|
|
87
89
|
test("normal mode hint matches the locked string", () => {
|
|
88
90
|
expect(bindings().statusHint("normal")).toBe(
|
|
89
|
-
"j/k move · v span · drag selects · c comment ·
|
|
91
|
+
"j/k move · v span · drag selects · c comment · x cut · e edit · n/p annotations · enter submit · q quit",
|
|
90
92
|
);
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
test("card-selected hint keeps the locked Cut label", () => {
|
|
94
|
-
expect(bindings().statusHint("card")).toBe(
|
|
96
|
+
expect(bindings().statusHint("card")).toBe(
|
|
97
|
+
"card · e edit · x Cut · n/p cards · esc deselect · enter submit",
|
|
98
|
+
);
|
|
95
99
|
});
|
|
96
100
|
|
|
97
101
|
test("span hint", () => {
|
|
98
102
|
expect(bindings({ spanMode: true }).statusHint("span")).toBe(
|
|
99
|
-
"span · l/h grow/shrink · w/b slide · $ end · c comment ·
|
|
103
|
+
"span · l/h grow/shrink · w/b slide · $ end · c comment · esc",
|
|
100
104
|
);
|
|
101
105
|
});
|
|
102
106
|
|
|
103
107
|
test("compose hint", () => {
|
|
104
|
-
expect(bindings({ overlay: "compose" }).statusHint("compose")).toBe(
|
|
108
|
+
expect(bindings({ overlay: "compose" }).statusHint("compose")).toBe(
|
|
109
|
+
"typing · enter save · esc cancel",
|
|
110
|
+
);
|
|
105
111
|
});
|
|
106
112
|
|
|
107
113
|
test("submit hint", () => {
|
|
108
|
-
expect(bindings({ overlay: "submit" }).statusHint("submit")).toBe(
|
|
114
|
+
expect(bindings({ overlay: "submit" }).statusHint("submit")).toBe(
|
|
115
|
+
"verdict ←/→ · enter submit · esc cancel",
|
|
116
|
+
);
|
|
109
117
|
});
|
|
110
118
|
|
|
111
119
|
test("walk hint", () => {
|
|
@@ -115,7 +123,9 @@ describe("getActiveKeys-generated status hints", () => {
|
|
|
115
123
|
});
|
|
116
124
|
|
|
117
125
|
test("read-only hint", () => {
|
|
118
|
-
expect(bindings().statusHint("read-only")).toBe(
|
|
126
|
+
expect(bindings().statusHint("read-only")).toBe(
|
|
127
|
+
"observer - read-only · j/k move · n/p annotations · q quit",
|
|
128
|
+
);
|
|
119
129
|
});
|
|
120
130
|
|
|
121
131
|
test("collaborator hint annotates but shows no plan edit or submit", () => {
|
|
@@ -123,7 +133,7 @@ describe("getActiveKeys-generated status hints", () => {
|
|
|
123
133
|
const hint = bindings().statusHint("collaborator");
|
|
124
134
|
|
|
125
135
|
// Assert
|
|
126
|
-
expect(hint).toBe("j/k move · v span · drag selects · c comment ·
|
|
136
|
+
expect(hint).toBe("j/k move · v span · drag selects · c comment · n/p annotations · q quit");
|
|
127
137
|
expect(hint).not.toContain("cut");
|
|
128
138
|
expect(hint).not.toContain("edit");
|
|
129
139
|
expect(hint).not.toContain("submit");
|
package/src/key-bindings.ts
CHANGED
|
@@ -24,7 +24,15 @@ export interface KeyLayerContext {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Hint templates keyed by the same view states the status line shows. */
|
|
27
|
-
export type HintMode =
|
|
27
|
+
export type HintMode =
|
|
28
|
+
| "normal"
|
|
29
|
+
| "collaborator"
|
|
30
|
+
| "card"
|
|
31
|
+
| "span"
|
|
32
|
+
| "compose"
|
|
33
|
+
| "submit"
|
|
34
|
+
| "walk"
|
|
35
|
+
| "read-only";
|
|
28
36
|
|
|
29
37
|
export interface CheatsheetEntry {
|
|
30
38
|
keys: string;
|
|
@@ -35,9 +43,7 @@ export interface CheatsheetSection {
|
|
|
35
43
|
entries: CheatsheetEntry[];
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
type HintEntry =
|
|
39
|
-
| { text: string }
|
|
40
|
-
| { commands: string[]; label: string; labelFirst?: boolean };
|
|
46
|
+
type HintEntry = { text: string } | { commands: string[]; label: string; labelFirst?: boolean };
|
|
41
47
|
|
|
42
48
|
const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
|
|
43
49
|
normal: [
|
|
@@ -45,7 +51,6 @@ const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
|
|
|
45
51
|
{ commands: ["span"], label: "span" },
|
|
46
52
|
{ text: "drag selects" },
|
|
47
53
|
{ commands: ["comment"], label: "comment" },
|
|
48
|
-
{ commands: ["suggest"], label: "suggest" },
|
|
49
54
|
{ commands: ["cut"], label: "cut" },
|
|
50
55
|
{ commands: ["edit"], label: "edit" },
|
|
51
56
|
{ commands: ["next_annotation", "prev_annotation"], label: "annotations" },
|
|
@@ -58,7 +63,6 @@ const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
|
|
|
58
63
|
{ commands: ["span"], label: "span" },
|
|
59
64
|
{ text: "drag selects" },
|
|
60
65
|
{ commands: ["comment"], label: "comment" },
|
|
61
|
-
{ commands: ["suggest"], label: "suggest" },
|
|
62
66
|
{ commands: ["next_annotation", "prev_annotation"], label: "annotations" },
|
|
63
67
|
{ commands: ["quit"], label: "quit" },
|
|
64
68
|
],
|
|
@@ -76,7 +80,6 @@ const HINT_TEMPLATES: Record<HintMode, HintEntry[]> = {
|
|
|
76
80
|
{ commands: ["span_slide_forward", "span_slide_back"], label: "slide" },
|
|
77
81
|
{ commands: ["span_to_end"], label: "end" },
|
|
78
82
|
{ commands: ["span_comment"], label: "comment" },
|
|
79
|
-
{ commands: ["span_suggest"], label: "suggest" },
|
|
80
83
|
{ commands: ["span_cancel"], label: "" },
|
|
81
84
|
],
|
|
82
85
|
compose: [
|
|
@@ -182,7 +185,10 @@ class HeadlessKeymapHost implements KeymapHost<object, KeymapEvent> {
|
|
|
182
185
|
function normalizedEvent(key: ResolvableKey): { name: string; shift: boolean } {
|
|
183
186
|
if (key.name.length === 1) {
|
|
184
187
|
if (/[a-zA-Z]/.test(key.name)) {
|
|
185
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
name: key.name.toLowerCase(),
|
|
190
|
+
shift: key.shift || key.name !== key.name.toLowerCase(),
|
|
191
|
+
};
|
|
186
192
|
}
|
|
187
193
|
return { name: key.name, shift: false };
|
|
188
194
|
}
|
|
@@ -204,7 +210,6 @@ const SPAN_COMMANDS: [key: string, command: string][] = [
|
|
|
204
210
|
["$", "span_to_end"],
|
|
205
211
|
["0", "span_to_start"],
|
|
206
212
|
["c", "span_comment"],
|
|
207
|
-
["s", "span_suggest"],
|
|
208
213
|
["escape", "span_cancel"],
|
|
209
214
|
];
|
|
210
215
|
|
|
@@ -281,7 +286,13 @@ export class KeyBindings {
|
|
|
281
286
|
{ key: "enter", cmd: "walk_submit" },
|
|
282
287
|
{ key: "q", cmd: "quit" },
|
|
283
288
|
],
|
|
284
|
-
commands: [
|
|
289
|
+
commands: [
|
|
290
|
+
record("walk_next"),
|
|
291
|
+
record("walk_prev"),
|
|
292
|
+
record("walk_leave"),
|
|
293
|
+
record("walk_submit"),
|
|
294
|
+
record("quit"),
|
|
295
|
+
],
|
|
285
296
|
});
|
|
286
297
|
this.keymap.registerLayer({
|
|
287
298
|
priority: 20,
|
|
@@ -312,7 +323,9 @@ export class KeyBindings {
|
|
|
312
323
|
});
|
|
313
324
|
this.keymap.registerLayer({
|
|
314
325
|
priority: 20,
|
|
315
|
-
when: () =>
|
|
326
|
+
when: () =>
|
|
327
|
+
this.context.overlay === "completion-prompt" ||
|
|
328
|
+
this.context.overlay === "completion-counting",
|
|
316
329
|
bindings: [
|
|
317
330
|
{ key: "return", cmd: "finish_review" },
|
|
318
331
|
{ key: "enter", cmd: "finish_review" },
|
|
@@ -320,7 +333,11 @@ export class KeyBindings {
|
|
|
320
333
|
{ key: "a", cmd: "opt_in_auto_close" },
|
|
321
334
|
{ key: "escape", cmd: "dismiss_completion" },
|
|
322
335
|
],
|
|
323
|
-
commands: [
|
|
336
|
+
commands: [
|
|
337
|
+
record("finish_review"),
|
|
338
|
+
record("opt_in_auto_close"),
|
|
339
|
+
record("dismiss_completion"),
|
|
340
|
+
],
|
|
324
341
|
});
|
|
325
342
|
}
|
|
326
343
|
|
|
@@ -344,7 +361,9 @@ export class KeyBindings {
|
|
|
344
361
|
private keyDisplayFor(command: string): string | null {
|
|
345
362
|
for (const activeKey of this.keymap.getActiveKeys({ includeBindings: true })) {
|
|
346
363
|
const bindingCommand =
|
|
347
|
-
typeof activeKey.command === "string"
|
|
364
|
+
typeof activeKey.command === "string"
|
|
365
|
+
? activeKey.command
|
|
366
|
+
: activeKey.bindings?.[0]?.command;
|
|
348
367
|
if (bindingCommand !== command) continue;
|
|
349
368
|
return KEY_GLYPHS[activeKey.stroke.name] ?? activeKey.display;
|
|
350
369
|
}
|
|
@@ -365,7 +384,10 @@ export class KeyBindings {
|
|
|
365
384
|
if (!displays.length) continue;
|
|
366
385
|
const keyPart = displays.join("/");
|
|
367
386
|
if (!entry.label) fragments.push(keyPart);
|
|
368
|
-
else
|
|
387
|
+
else
|
|
388
|
+
fragments.push(
|
|
389
|
+
entry.labelFirst ? `${entry.label} ${keyPart}` : `${keyPart} ${entry.label}`,
|
|
390
|
+
);
|
|
369
391
|
}
|
|
370
392
|
return fragments.join(" · ");
|
|
371
393
|
}
|
package/src/keymap.test.ts
CHANGED
|
@@ -21,7 +21,11 @@ function state(patch: Partial<KeyState> = {}): KeyState {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function key(
|
|
24
|
+
function key(
|
|
25
|
+
name: string,
|
|
26
|
+
shift = false,
|
|
27
|
+
meta = false,
|
|
28
|
+
): { name: string; shift: boolean; meta: boolean } {
|
|
25
29
|
return { name, shift, meta };
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -133,7 +137,9 @@ describe("collaborator (share) capabilities", () => {
|
|
|
133
137
|
const collab = state({ canEditPlan: false, canSubmitVerdict: false });
|
|
134
138
|
|
|
135
139
|
// Act / Assert
|
|
136
|
-
expect(reduceKey(collab, key("c"), "comment")).toEqual([
|
|
140
|
+
expect(reduceKey(collab, key("c"), "comment")).toEqual([
|
|
141
|
+
{ type: "openCompose", kind: "comment", from: "cursor" },
|
|
142
|
+
]);
|
|
137
143
|
});
|
|
138
144
|
|
|
139
145
|
test("can edit their own card: edit with a focused annotation rewrites it", () => {
|
|
@@ -158,7 +164,9 @@ describe("collaborator (share) capabilities", () => {
|
|
|
158
164
|
const collab = state({ canSubmitVerdict: false });
|
|
159
165
|
|
|
160
166
|
// Act / Assert
|
|
161
|
-
expect(reduceKey(collab, key("x"), "submit")).toEqual([
|
|
167
|
+
expect(reduceKey(collab, key("x"), "submit")).toEqual([
|
|
168
|
+
{ type: "status", message: "shared view - your notes save as you go; q to leave" },
|
|
169
|
+
]);
|
|
162
170
|
});
|
|
163
171
|
|
|
164
172
|
test("cannot re-share: only the owner shares", () => {
|
|
@@ -166,7 +174,9 @@ describe("collaborator (share) capabilities", () => {
|
|
|
166
174
|
const collab = state({ canShare: false });
|
|
167
175
|
|
|
168
176
|
// Act / Assert
|
|
169
|
-
expect(reduceKey(collab, key("S", true), "share")).toEqual([
|
|
177
|
+
expect(reduceKey(collab, key("S", true), "share")).toEqual([
|
|
178
|
+
{ type: "status", message: "only the plan owner can share" },
|
|
179
|
+
]);
|
|
170
180
|
});
|
|
171
181
|
});
|
|
172
182
|
|
|
@@ -178,7 +188,9 @@ describe("share", () => {
|
|
|
178
188
|
|
|
179
189
|
test("an observer cannot share", () => {
|
|
180
190
|
// Act / Assert
|
|
181
|
-
expect(reduceKey(state({ readOnly: true }), key("S", true), "share")).toEqual([
|
|
191
|
+
expect(reduceKey(state({ readOnly: true }), key("S", true), "share")).toEqual([
|
|
192
|
+
{ type: "status", message: "observer - read-only" },
|
|
193
|
+
]);
|
|
182
194
|
});
|
|
183
195
|
});
|
|
184
196
|
|
|
@@ -194,7 +206,6 @@ describe("plan normal mode", () => {
|
|
|
194
206
|
["g", true, [{ type: "move", to: "bottom" }]],
|
|
195
207
|
["v", false, [{ type: "startSpan" }]],
|
|
196
208
|
["c", false, [{ type: "openCompose", kind: "comment", from: "cursor" }]],
|
|
197
|
-
["s", false, [{ type: "openCompose", kind: "suggestion", from: "cursor" }]],
|
|
198
209
|
// the default state has a selected card, so Cut and edit act on the card
|
|
199
210
|
["x", false, [{ type: "removeAnnotation" }]],
|
|
200
211
|
["e", false, [{ type: "editCard" }]],
|
|
@@ -217,20 +228,35 @@ describe("plan normal mode", () => {
|
|
|
217
228
|
const resolvedState = state({ resolved: true });
|
|
218
229
|
|
|
219
230
|
// Assert
|
|
220
|
-
for (const name of ["c", "
|
|
221
|
-
expect(reduceKey(resolvedState, key(name))).toEqual([
|
|
231
|
+
for (const name of ["c", "x", "e"]) {
|
|
232
|
+
expect(reduceKey(resolvedState, key(name))).toEqual([
|
|
233
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
234
|
+
]);
|
|
222
235
|
}
|
|
223
236
|
expect(reduceKey(resolvedState, key("return"))).toEqual([]);
|
|
224
237
|
expect(reduceKey(resolvedState, key("backspace"))).toEqual([]);
|
|
225
238
|
});
|
|
226
239
|
|
|
240
|
+
test("u restores a rail removal (a cut block); a share viewer's is silent", () => {
|
|
241
|
+
// Assert - the undo intent leaves the target to the dispatcher
|
|
242
|
+
expect(reduceKey(state(), key("u"))).toEqual([{ type: "restoreCuration" }]);
|
|
243
|
+
expect(reduceKey(state({ resolved: true }), key("u"))).toEqual([
|
|
244
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
245
|
+
]);
|
|
246
|
+
expect(reduceKey(state({ canEditPlan: false }), key("u"))).toEqual([]);
|
|
247
|
+
expect(reduceKey(state({ readOnly: true }), key("u"))).toEqual([
|
|
248
|
+
{ type: "status", message: "observer - read-only" },
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
227
252
|
test("cut text cannot host a comment or a span", () => {
|
|
228
253
|
// Arrange
|
|
229
254
|
const cut = state({ cursorAnnotatable: false });
|
|
230
255
|
|
|
231
256
|
// Assert
|
|
232
|
-
expect(reduceKey(cut, key("c"))).toEqual([
|
|
233
|
-
|
|
257
|
+
expect(reduceKey(cut, key("c"))).toEqual([
|
|
258
|
+
{ type: "status", message: "text is cut - restore it first" },
|
|
259
|
+
]);
|
|
234
260
|
expect(reduceKey(cut, key("v"))).toEqual([]);
|
|
235
261
|
});
|
|
236
262
|
|
|
@@ -265,7 +291,8 @@ describe("span mode", () => {
|
|
|
265
291
|
["$", [{ type: "spanKey", name: "$" }]],
|
|
266
292
|
["0", [{ type: "spanKey", name: "0" }]],
|
|
267
293
|
["c", [{ type: "openCompose", kind: "comment", from: "span" }]],
|
|
268
|
-
["
|
|
294
|
+
["x", [{ type: "spanCut" }]],
|
|
295
|
+
["a", [{ type: "openSpanActions" }]],
|
|
269
296
|
["escape", [{ type: "closeOverlay" }]],
|
|
270
297
|
["q", [{ type: "exit" }]],
|
|
271
298
|
["j", []],
|
|
@@ -275,6 +302,73 @@ describe("span mode", () => {
|
|
|
275
302
|
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
276
303
|
});
|
|
277
304
|
}
|
|
305
|
+
|
|
306
|
+
test("an observer's span-mode mutations answer read-only", () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
const observer = state({ spanMode: true, readOnly: true });
|
|
309
|
+
|
|
310
|
+
// Assert
|
|
311
|
+
expect(reduceKey(observer, key("x"))).toEqual([
|
|
312
|
+
{ type: "status", message: "observer - read-only" },
|
|
313
|
+
]);
|
|
314
|
+
expect(reduceKey(observer, key("a"))).toEqual([
|
|
315
|
+
{ type: "status", message: "observer - read-only" },
|
|
316
|
+
]);
|
|
317
|
+
expect(reduceKey(observer, key("c"))).toEqual([
|
|
318
|
+
{ type: "status", message: "observer - read-only" },
|
|
319
|
+
]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("span-mode cut is owner-only, like the normal plan cut", () => {
|
|
323
|
+
// Arrange
|
|
324
|
+
const collaborator = state({ spanMode: true, canEditPlan: false });
|
|
325
|
+
|
|
326
|
+
// Assert
|
|
327
|
+
expect(reduceKey(collaborator, key("x"))).toEqual([]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("all span-mode mutations are blocked once the review is resolved", () => {
|
|
331
|
+
// Arrange
|
|
332
|
+
const resolved = state({ spanMode: true, resolved: true });
|
|
333
|
+
const readOnlyStatus: Intent[] = [{ type: "status", message: "review submitted - read-only" }];
|
|
334
|
+
|
|
335
|
+
// Assert - comment, cut, and quick-actions all answer read-only
|
|
336
|
+
expect(reduceKey(resolved, key("x"))).toEqual(readOnlyStatus);
|
|
337
|
+
expect(reduceKey(resolved, key("c"))).toEqual(readOnlyStatus);
|
|
338
|
+
expect(reduceKey(resolved, key("a"))).toEqual(readOnlyStatus);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("span-mode cut gates for an observer even when cut is rebound off x", () => {
|
|
342
|
+
// Arrange - the span sub-grammar hardwires x, so the read-only gate must catch it by name
|
|
343
|
+
const rebound = { ...DEFAULT_KEYS, cut: ["d"] };
|
|
344
|
+
const observer = state({ spanMode: true, readOnly: true, keys: rebound });
|
|
345
|
+
|
|
346
|
+
// Assert
|
|
347
|
+
expect(reduceKey(observer, key("x"))).toEqual([
|
|
348
|
+
{ type: "status", message: "observer - read-only" },
|
|
349
|
+
]);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("span actions overlay", () => {
|
|
354
|
+
// Arrange
|
|
355
|
+
const keyState = state({ overlay: "spanActions" });
|
|
356
|
+
const table: [string, Intent[]][] = [
|
|
357
|
+
["j", [{ type: "moveSpanAction", direction: 1 }]],
|
|
358
|
+
["down", [{ type: "moveSpanAction", direction: 1 }]],
|
|
359
|
+
["k", [{ type: "moveSpanAction", direction: -1 }]],
|
|
360
|
+
["up", [{ type: "moveSpanAction", direction: -1 }]],
|
|
361
|
+
["return", [{ type: "pickSpanAction" }]],
|
|
362
|
+
["enter", [{ type: "pickSpanAction" }]],
|
|
363
|
+
["escape", [{ type: "closeSpanActions" }]],
|
|
364
|
+
["c", []],
|
|
365
|
+
["q", []],
|
|
366
|
+
];
|
|
367
|
+
for (const [name, expected] of table) {
|
|
368
|
+
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
369
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
278
372
|
});
|
|
279
373
|
|
|
280
374
|
describe("diff mode", () => {
|
|
@@ -290,9 +384,8 @@ describe("diff mode", () => {
|
|
|
290
384
|
["backspace", [{ type: "removeAnnotation" }]],
|
|
291
385
|
["return", [{ type: "openSubmit" }]],
|
|
292
386
|
["v", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
|
|
293
|
-
["x", [{ type: "
|
|
387
|
+
["x", [{ type: "rejectChange" }]],
|
|
294
388
|
["e", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
|
|
295
|
-
["s", [{ type: "status", message: "plan-only verb - diff review uses c on a line" }]],
|
|
296
389
|
["q", [{ type: "exit" }]],
|
|
297
390
|
];
|
|
298
391
|
for (const [name, expected] of table) {
|
|
@@ -301,6 +394,40 @@ describe("diff mode", () => {
|
|
|
301
394
|
});
|
|
302
395
|
}
|
|
303
396
|
|
|
397
|
+
test("shift+x rejects the whole hunk under the cursor", () => {
|
|
398
|
+
// Assert
|
|
399
|
+
expect(reduceKey(keyState, key("x", true))).toEqual([{ type: "rejectHunk" }]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("curation is owner-only and locked once resolved", () => {
|
|
403
|
+
// Assert - a resolved diff answers before curating
|
|
404
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("x"))).toEqual([
|
|
405
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
406
|
+
]);
|
|
407
|
+
// a collaborator (canEditPlan false) cannot curate hunks
|
|
408
|
+
expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("x"))).toEqual([
|
|
409
|
+
{ type: "status", message: "only the diff owner can curate hunks" },
|
|
410
|
+
]);
|
|
411
|
+
// an observer's mutating attempt is stopped by the read-only gate
|
|
412
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("x"))).toEqual([
|
|
413
|
+
{ type: "status", message: "observer - read-only" },
|
|
414
|
+
]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("u restores a curated-out item, owner-only and locked once resolved", () => {
|
|
418
|
+
// Assert - the undo intent leaves the target to the dispatcher
|
|
419
|
+
expect(reduceKey(keyState, key("u"))).toEqual([{ type: "restoreCuration" }]);
|
|
420
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("u"))).toEqual([
|
|
421
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
422
|
+
]);
|
|
423
|
+
expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("u"))).toEqual([
|
|
424
|
+
{ type: "status", message: "only the diff owner can curate hunks" },
|
|
425
|
+
]);
|
|
426
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("u"))).toEqual([
|
|
427
|
+
{ type: "status", message: "observer - read-only" },
|
|
428
|
+
]);
|
|
429
|
+
});
|
|
430
|
+
|
|
304
431
|
test("comment guards: resolved first, then non-code rows", () => {
|
|
305
432
|
// Assert
|
|
306
433
|
expect(reduceKey(state({ view: "diff", resolved: true }), key("c"))).toEqual([
|
|
@@ -322,7 +449,9 @@ describe("guided walk", () => {
|
|
|
322
449
|
expect(reduceKey(state({ view: "diff", readOnly: true }), key("w"))).toEqual([
|
|
323
450
|
{ type: "status", message: "observer - read-only" },
|
|
324
451
|
]);
|
|
325
|
-
expect(reduceKey(state(), key("w"))).toEqual([
|
|
452
|
+
expect(reduceKey(state(), key("w"))).toEqual([
|
|
453
|
+
{ type: "status", message: "the guided walk is a diff-review mode" },
|
|
454
|
+
]);
|
|
326
455
|
});
|
|
327
456
|
|
|
328
457
|
// Arrange
|
|
@@ -348,7 +477,10 @@ describe("guided walk", () => {
|
|
|
348
477
|
const atEnd = state({ view: "diff", overlay: "walk", walkAtEnd: true });
|
|
349
478
|
|
|
350
479
|
// Assert
|
|
351
|
-
expect(reduceKey(atEnd, key("return"))).toEqual([
|
|
480
|
+
expect(reduceKey(atEnd, key("return"))).toEqual([
|
|
481
|
+
{ type: "walkLeave" },
|
|
482
|
+
{ type: "openSubmit" },
|
|
483
|
+
]);
|
|
352
484
|
expect(reduceKey(atEnd, key("enter"))).toEqual([{ type: "walkLeave" }, { type: "openSubmit" }]);
|
|
353
485
|
});
|
|
354
486
|
});
|
|
@@ -378,7 +510,9 @@ describe("review panel controls", () => {
|
|
|
378
510
|
|
|
379
511
|
test("span mode still owns b as a span verb, not a panel cycle", () => {
|
|
380
512
|
// Assert
|
|
381
|
-
expect(reduceKey(state({ spanMode: true }), key("b"))).toEqual([
|
|
513
|
+
expect(reduceKey(state({ spanMode: true }), key("b"))).toEqual([
|
|
514
|
+
{ type: "spanKey", name: "b" },
|
|
515
|
+
]);
|
|
382
516
|
});
|
|
383
517
|
|
|
384
518
|
test("the walk overlay keeps the brackets for stepping", () => {
|
|
@@ -411,8 +545,10 @@ describe("read-only filter", () => {
|
|
|
411
545
|
test("every mutating key answers observer - read-only, in plan and diff", () => {
|
|
412
546
|
for (const view of ["plan", "diff"] as const) {
|
|
413
547
|
const keyState = state({ view, readOnly: true });
|
|
414
|
-
for (const name of ["c", "
|
|
415
|
-
expect(reduceKey(keyState, key(name))).toEqual([
|
|
548
|
+
for (const name of ["c", "x", "e", "backspace", "return", "enter"]) {
|
|
549
|
+
expect(reduceKey(keyState, key(name))).toEqual([
|
|
550
|
+
{ type: "status", message: "observer - read-only" },
|
|
551
|
+
]);
|
|
416
552
|
}
|
|
417
553
|
}
|
|
418
554
|
});
|
|
@@ -427,21 +563,24 @@ describe("read-only filter", () => {
|
|
|
427
563
|
expect(reduceKey(keyState, key("q"))).toEqual([{ type: "exit" }]);
|
|
428
564
|
});
|
|
429
565
|
|
|
430
|
-
test("span-mode c
|
|
566
|
+
test("span-mode c is gated by key name even under a rebound keymap", () => {
|
|
431
567
|
// Arrange
|
|
432
|
-
const rebound = { ...DEFAULT_KEYS, comment: ["m"]
|
|
568
|
+
const rebound = { ...DEFAULT_KEYS, comment: ["m"] };
|
|
433
569
|
const keyState = state({ spanMode: true, readOnly: true, keys: rebound });
|
|
434
570
|
|
|
435
571
|
// Assert
|
|
436
|
-
expect(reduceKey(keyState, key("c"))).toEqual([
|
|
437
|
-
|
|
572
|
+
expect(reduceKey(keyState, key("c"))).toEqual([
|
|
573
|
+
{ type: "status", message: "observer - read-only" },
|
|
574
|
+
]);
|
|
438
575
|
});
|
|
439
576
|
});
|
|
440
577
|
|
|
441
578
|
describe("inbox delete", () => {
|
|
442
579
|
test("d on a session requests delete", () => {
|
|
443
580
|
// Arrange / Act / Assert
|
|
444
|
-
expect(reduceKey(state({ view: "inbox" }), key("d"))).toEqual([
|
|
581
|
+
expect(reduceKey(state({ view: "inbox" }), key("d"))).toEqual([
|
|
582
|
+
{ type: "requestDeleteSession" },
|
|
583
|
+
]);
|
|
445
584
|
});
|
|
446
585
|
});
|
|
447
586
|
|
|
@@ -465,13 +604,19 @@ describe("confirm overlay", () => {
|
|
|
465
604
|
describe("rename grammar", () => {
|
|
466
605
|
test("r on a focused note opens rename", () => {
|
|
467
606
|
// Arrange / Act / Assert
|
|
468
|
-
expect(reduceKey(state({ hasFocusedAnnotation: true }), key("r"))).toEqual([
|
|
607
|
+
expect(reduceKey(state({ hasFocusedAnnotation: true }), key("r"))).toEqual([
|
|
608
|
+
{ type: "openRename" },
|
|
609
|
+
]);
|
|
469
610
|
});
|
|
470
611
|
|
|
471
612
|
test("prompt overlay routes ⏎ to confirm and esc to cancel", () => {
|
|
472
613
|
// Arrange / Act / Assert
|
|
473
|
-
expect(reduceKey(state({ overlay: "prompt" }), key("return"))).toEqual([
|
|
474
|
-
|
|
614
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("return"))).toEqual([
|
|
615
|
+
{ type: "confirmDialog" },
|
|
616
|
+
]);
|
|
617
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("escape"))).toEqual([
|
|
618
|
+
{ type: "closeOverlay" },
|
|
619
|
+
]);
|
|
475
620
|
expect(reduceKey(state({ overlay: "prompt" }), key("a"))).toEqual([]);
|
|
476
621
|
});
|
|
477
622
|
});
|