revspec 0.5.0 → 0.7.0

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 (47) hide show
  1. package/README.md +84 -67
  2. package/bin/revspec.ts +4 -38
  3. package/package.json +20 -3
  4. package/skills/revspec/SKILL.md +38 -31
  5. package/src/cli/reply.ts +1 -1
  6. package/src/cli/watch.ts +69 -41
  7. package/src/protocol/live-events.ts +6 -16
  8. package/src/state/review-state.ts +37 -24
  9. package/src/tui/app.ts +168 -107
  10. package/src/tui/comment-input.ts +21 -14
  11. package/src/tui/confirm.ts +4 -6
  12. package/src/tui/help.ts +77 -20
  13. package/src/tui/pager.ts +4 -2
  14. package/src/tui/search.ts +9 -4
  15. package/src/tui/spinner.ts +81 -0
  16. package/src/tui/status-bar.ts +9 -8
  17. package/src/tui/thread-list.ts +62 -22
  18. package/src/tui/ui/keymap.ts +55 -0
  19. package/.github/workflows/ci.yml +0 -18
  20. package/CLAUDE.md +0 -27
  21. package/bun.lock +0 -213
  22. package/docs/superpowers/plans/2026-03-14-live-ai-integration.md +0 -1877
  23. package/docs/superpowers/plans/2026-03-14-spectral-v1-implementation.md +0 -2139
  24. package/docs/superpowers/plans/2026-03-15-ui-refactor.md +0 -1025
  25. package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.md +0 -518
  26. package/docs/superpowers/specs/2026-03-14-live-ai-integration-design.review.json +0 -65
  27. package/docs/superpowers/specs/2026-03-14-spec-review-tool-design.md +0 -331
  28. package/docs/superpowers/specs/2026-03-14-spec-review-tool-design.review.json +0 -141
  29. package/docs/superpowers/specs/claude-code-integration-notes.md +0 -26
  30. package/scripts/install-skill.sh +0 -20
  31. package/scripts/release.sh +0 -52
  32. package/test/cli-reply.test.ts +0 -140
  33. package/test/cli-watch.test.ts +0 -216
  34. package/test/cli.test.ts +0 -160
  35. package/test/e2e-live.test.ts +0 -171
  36. package/test/live-interaction.test.ts +0 -398
  37. package/test/opentui-smoke.test.ts +0 -12
  38. package/test/protocol/live-events.test.ts +0 -509
  39. package/test/protocol/live-merge.test.ts +0 -167
  40. package/test/protocol/merge.test.ts +0 -100
  41. package/test/protocol/read.test.ts +0 -92
  42. package/test/protocol/types.test.ts +0 -95
  43. package/test/protocol/write.test.ts +0 -72
  44. package/test/state/review-state.test.ts +0 -399
  45. package/test/tui/pager.test.ts +0 -159
  46. package/test/tui/ui/keybinds.test.ts +0 -71
  47. package/tsconfig.json +0 -14
@@ -1,399 +0,0 @@
1
- import { describe, expect, it, beforeEach } from "bun:test";
2
- import { ReviewState } from "../../src/state/review-state";
3
- import type { Thread } from "../../src/protocol/types";
4
-
5
- const SPEC = ["line one", "line two", "line three", "line four", "line five"];
6
-
7
- function makeThread(
8
- id: string,
9
- line: number,
10
- status: Thread["status"],
11
- messages: Thread["messages"] = [{ author: "reviewer", text: "comment" }]
12
- ): Thread {
13
- return { id, line, status, messages };
14
- }
15
-
16
- describe("ReviewState", () => {
17
- describe("initializes with spec lines and no threads", () => {
18
- it("sets lineCount from specLines", () => {
19
- const state = new ReviewState(SPEC, []);
20
- expect(state.lineCount).toBe(5);
21
- });
22
-
23
- it("starts cursor at line 1", () => {
24
- const state = new ReviewState(SPEC, []);
25
- expect(state.cursorLine).toBe(1);
26
- });
27
-
28
- it("starts with empty threads array", () => {
29
- const state = new ReviewState(SPEC, []);
30
- expect(state.threads).toHaveLength(0);
31
- });
32
- });
33
-
34
- describe("addComment", () => {
35
- it("adds a new thread with status open", () => {
36
- const state = new ReviewState(SPEC, []);
37
- state.addComment(3, "needs clarification");
38
- expect(state.threads).toHaveLength(1);
39
- expect(state.threads[0].line).toBe(3);
40
- expect(state.threads[0].status).toBe("open");
41
- expect(state.threads[0].messages).toHaveLength(1);
42
- expect(state.threads[0].messages[0].author).toBe("reviewer");
43
- expect(state.threads[0].messages[0].text).toBe("needs clarification");
44
- expect(state.threads[0].messages[0].ts).toBeGreaterThan(0);
45
- });
46
-
47
- it("assigns auto-incremented id", () => {
48
- const state = new ReviewState(SPEC, [makeThread("t3", 1, "open")]);
49
- state.addComment(2, "another comment");
50
- expect(state.threads[1].id).toBe("t4");
51
- });
52
- });
53
-
54
- describe("replyToThread", () => {
55
- it("appends human message to existing thread", () => {
56
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "pending")]);
57
- state.replyToThread("t1", "my reply");
58
- expect(state.threads[0].messages).toHaveLength(2);
59
- expect(state.threads[0].messages[1].author).toBe("reviewer");
60
- expect(state.threads[0].messages[1].text).toBe("my reply");
61
- expect(state.threads[0].messages[1].ts).toBeGreaterThan(0);
62
- });
63
-
64
- it("flips status to open", () => {
65
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "pending")]);
66
- state.replyToThread("t1", "my reply");
67
- expect(state.threads[0].status).toBe("open");
68
- });
69
-
70
- it("does nothing for unknown threadId", () => {
71
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "pending")]);
72
- state.replyToThread("t99", "ghost reply");
73
- expect(state.threads[0].messages).toHaveLength(1);
74
- });
75
- });
76
-
77
- describe("resolveThread", () => {
78
- it("sets thread status to resolved", () => {
79
- const state = new ReviewState(SPEC, [makeThread("t1", 1, "open")]);
80
- state.resolveThread("t1");
81
- expect(state.threads[0].status).toBe("resolved");
82
- });
83
-
84
- it("toggles resolved thread back to open", () => {
85
- const state = new ReviewState(SPEC, [makeThread("t1", 1, "resolved")]);
86
- state.resolveThread("t1");
87
- expect(state.threads[0].status).toBe("open");
88
- });
89
-
90
- it("does nothing for unknown threadId", () => {
91
- const state = new ReviewState(SPEC, [makeThread("t1", 1, "open")]);
92
- state.resolveThread("t99");
93
- expect(state.threads[0].status).toBe("open");
94
- });
95
- });
96
-
97
- describe("resolveAllPending", () => {
98
- it("resolves all pending threads", () => {
99
- const state = new ReviewState(SPEC, [
100
- makeThread("t1", 1, "pending"),
101
- makeThread("t2", 2, "pending"),
102
- makeThread("t3", 3, "open"),
103
- ]);
104
- state.resolveAllPending();
105
- expect(state.threads[0].status).toBe("resolved");
106
- expect(state.threads[1].status).toBe("resolved");
107
- });
108
-
109
- it("leaves open threads unchanged", () => {
110
- const state = new ReviewState(SPEC, [
111
- makeThread("t1", 1, "pending"),
112
- makeThread("t2", 2, "open"),
113
- ]);
114
- state.resolveAllPending();
115
- expect(state.threads[1].status).toBe("open");
116
- });
117
- });
118
-
119
- describe("threadAtLine", () => {
120
- it("returns thread when found", () => {
121
- const thread = makeThread("t1", 3, "open");
122
- const state = new ReviewState(SPEC, [thread]);
123
- expect(state.threadAtLine(3)).toEqual(thread);
124
- });
125
-
126
- it("returns null when not found", () => {
127
- const state = new ReviewState(SPEC, [makeThread("t1", 3, "open")]);
128
- expect(state.threadAtLine(5)).toBeNull();
129
- });
130
- });
131
-
132
- describe("nextActiveThread", () => {
133
- it("returns next open/pending thread line after cursor", () => {
134
- const state = new ReviewState(SPEC, [
135
- makeThread("t1", 2, "open"),
136
- makeThread("t2", 4, "pending"),
137
- ]);
138
- state.cursorLine = 3;
139
- expect(state.nextActiveThread()).toBe(4);
140
- });
141
-
142
- it("wraps around to first active thread when none after cursor", () => {
143
- const state = new ReviewState(SPEC, [
144
- makeThread("t1", 1, "open"),
145
- makeThread("t2", 3, "pending"),
146
- ]);
147
- state.cursorLine = 4;
148
- expect(state.nextActiveThread()).toBe(1);
149
- });
150
-
151
- it("skips resolved threads", () => {
152
- const state = new ReviewState(SPEC, [
153
- makeThread("t1", 2, "resolved"),
154
- makeThread("t2", 4, "open"),
155
- ]);
156
- state.cursorLine = 1;
157
- expect(state.nextActiveThread()).toBe(4);
158
- });
159
-
160
- it("returns null when no active threads", () => {
161
- const state = new ReviewState(SPEC, [
162
- makeThread("t1", 2, "resolved"),
163
- makeThread("t2", 4, "outdated"),
164
- ]);
165
- expect(state.nextActiveThread()).toBeNull();
166
- });
167
- });
168
-
169
- describe("prevActiveThread", () => {
170
- it("returns previous open/pending thread line before cursor", () => {
171
- const state = new ReviewState(SPEC, [
172
- makeThread("t1", 1, "open"),
173
- makeThread("t2", 3, "pending"),
174
- ]);
175
- state.cursorLine = 4;
176
- expect(state.prevActiveThread()).toBe(3);
177
- });
178
-
179
- it("wraps around to last active thread when none before cursor", () => {
180
- const state = new ReviewState(SPEC, [
181
- makeThread("t1", 2, "open"),
182
- makeThread("t2", 4, "pending"),
183
- ]);
184
- state.cursorLine = 1;
185
- expect(state.prevActiveThread()).toBe(4);
186
- });
187
-
188
- it("skips resolved threads", () => {
189
- const state = new ReviewState(SPEC, [
190
- makeThread("t1", 1, "open"),
191
- makeThread("t2", 3, "resolved"),
192
- ]);
193
- state.cursorLine = 5;
194
- expect(state.prevActiveThread()).toBe(1);
195
- });
196
-
197
- it("returns null when no active threads", () => {
198
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "resolved")]);
199
- expect(state.prevActiveThread()).toBeNull();
200
- });
201
- });
202
-
203
- describe("canApprove", () => {
204
- it("returns true when all threads are resolved", () => {
205
- const state = new ReviewState(SPEC, [
206
- makeThread("t1", 1, "resolved"),
207
- makeThread("t2", 2, "resolved"),
208
- ]);
209
- expect(state.canApprove()).toBe(true);
210
- });
211
-
212
- it("returns true when threads are resolved or outdated", () => {
213
- const state = new ReviewState(SPEC, [
214
- makeThread("t1", 1, "resolved"),
215
- makeThread("t2", 2, "outdated"),
216
- ]);
217
- expect(state.canApprove()).toBe(true);
218
- });
219
-
220
- it("returns false when any thread is open", () => {
221
- const state = new ReviewState(SPEC, [
222
- makeThread("t1", 1, "resolved"),
223
- makeThread("t2", 2, "open"),
224
- ]);
225
- expect(state.canApprove()).toBe(false);
226
- });
227
-
228
- it("returns false when any thread is pending", () => {
229
- const state = new ReviewState(SPEC, [
230
- makeThread("t1", 1, "resolved"),
231
- makeThread("t2", 2, "pending"),
232
- ]);
233
- expect(state.canApprove()).toBe(false);
234
- });
235
-
236
- it("returns true when there are no threads (clean approval)", () => {
237
- const state = new ReviewState(SPEC, []);
238
- expect(state.canApprove()).toBe(true);
239
- });
240
- });
241
-
242
- describe("nextThreadId", () => {
243
- it("returns t1 when no threads exist", () => {
244
- const state = new ReviewState(SPEC, []);
245
- expect(state.nextThreadId()).toBe("t1");
246
- });
247
-
248
- it("increments from the highest existing id", () => {
249
- const state = new ReviewState(SPEC, [
250
- makeThread("t3", 1, "open"),
251
- makeThread("t1", 2, "open"),
252
- makeThread("t7", 3, "open"),
253
- ]);
254
- expect(state.nextThreadId()).toBe("t8");
255
- });
256
- });
257
-
258
- describe("deleteLastDraftMessage", () => {
259
- it("removes the last human message from the thread", () => {
260
- const state = new ReviewState(SPEC, [
261
- {
262
- id: "t1",
263
- line: 1,
264
- status: "open",
265
- messages: [
266
- { author: "owner", text: "AI response" },
267
- { author: "reviewer", text: "my draft" },
268
- ],
269
- },
270
- ]);
271
- state.deleteLastDraftMessage("t1");
272
- expect(state.threads[0].messages).toHaveLength(1);
273
- expect(state.threads[0].messages[0].author).toBe("owner");
274
- });
275
-
276
- it("removes the thread entirely when it becomes empty", () => {
277
- const state = new ReviewState(SPEC, [
278
- {
279
- id: "t1",
280
- line: 1,
281
- status: "open",
282
- messages: [{ author: "reviewer", text: "only message" }],
283
- },
284
- ]);
285
- state.deleteLastDraftMessage("t1");
286
- expect(state.threads).toHaveLength(0);
287
- });
288
-
289
- it("does nothing for unknown threadId", () => {
290
- const state = new ReviewState(SPEC, [makeThread("t1", 1, "open")]);
291
- state.deleteLastDraftMessage("t99");
292
- expect(state.threads).toHaveLength(1);
293
- expect(state.threads[0].messages).toHaveLength(1);
294
- });
295
- });
296
-
297
- describe("toDraft", () => {
298
- it("serializes current threads", () => {
299
- const threads = [makeThread("t1", 2, "open")];
300
- const state = new ReviewState(SPEC, threads);
301
- expect(state.toDraft()).toEqual({ threads });
302
- });
303
- });
304
-
305
- describe("activeThreadCount", () => {
306
- it("counts open and pending threads separately", () => {
307
- const state = new ReviewState(SPEC, [
308
- makeThread("t1", 1, "open"),
309
- makeThread("t2", 2, "pending"),
310
- makeThread("t3", 3, "open"),
311
- makeThread("t4", 4, "resolved"),
312
- ]);
313
- expect(state.activeThreadCount()).toEqual({ open: 2, pending: 1 });
314
- });
315
-
316
- it("returns zeros when no active threads", () => {
317
- const state = new ReviewState(SPEC, [
318
- makeThread("t1", 1, "resolved"),
319
- makeThread("t2", 2, "outdated"),
320
- ]);
321
- expect(state.activeThreadCount()).toEqual({ open: 0, pending: 0 });
322
- });
323
- });
324
-
325
- describe("unread tracking", () => {
326
- it("tracks unread owner replies", () => {
327
- const state = new ReviewState(["line1", "line2"], []);
328
- state.addComment(1, "fix this");
329
- state.addOwnerReply("t1", "done", 1001);
330
- expect(state.unreadCount()).toBe(1);
331
- });
332
-
333
- it("markRead clears unread for a thread", () => {
334
- const state = new ReviewState(["line1"], []);
335
- state.addComment(1, "fix");
336
- state.addOwnerReply("t1", "done", 1001);
337
- state.markRead("t1");
338
- expect(state.unreadCount()).toBe(0);
339
- });
340
-
341
- it("nextUnreadThread returns line of next unread thread after cursor", () => {
342
- const state = new ReviewState(["a", "b", "c", "d", "e"], []);
343
- state.addComment(2, "fix");
344
- state.addComment(4, "fix too");
345
- state.addOwnerReply("t1", "done", 1001);
346
- state.addOwnerReply("t2", "done", 1002);
347
- state.cursorLine = 1;
348
- expect(state.nextUnreadThread()).toBe(2);
349
- });
350
-
351
- it("prevUnreadThread returns line of prev unread thread before cursor", () => {
352
- const state = new ReviewState(["a", "b", "c", "d", "e"], []);
353
- state.addComment(2, "fix");
354
- state.addComment(4, "fix too");
355
- state.addOwnerReply("t1", "done", 1001);
356
- state.addOwnerReply("t2", "done", 1002);
357
- state.cursorLine = 5;
358
- expect(state.prevUnreadThread()).toBe(4);
359
- });
360
-
361
- it("nextUnreadThread returns null when no unread", () => {
362
- const state = new ReviewState(["a"], []);
363
- expect(state.nextUnreadThread()).toBeNull();
364
- });
365
-
366
- it("isThreadUnread returns correct state", () => {
367
- const state = new ReviewState(["a"], []);
368
- state.addComment(1, "fix");
369
- expect(state.isThreadUnread("t1")).toBe(false);
370
- state.addOwnerReply("t1", "done", 1001);
371
- expect(state.isThreadUnread("t1")).toBe(true);
372
- state.markRead("t1");
373
- expect(state.isThreadUnread("t1")).toBe(false);
374
- });
375
-
376
- it("nextUnreadThread wraps around", () => {
377
- const state = new ReviewState(["a", "b", "c", "d", "e"], []);
378
- state.addComment(2, "fix");
379
- state.addOwnerReply("t1", "done", 1001);
380
- state.cursorLine = 4;
381
- expect(state.nextUnreadThread()).toBe(2); // wraps to beginning
382
- });
383
-
384
- it("addOwnerReply sets thread status to pending", () => {
385
- const state = new ReviewState(["a"], []);
386
- state.addComment(1, "fix");
387
- expect(state.threads[0].status).toBe("open");
388
- state.addOwnerReply("t1", "done", 1001);
389
- expect(state.threads[0].status).toBe("pending");
390
- });
391
-
392
- it("addOwnerReply preserves timestamp on message", () => {
393
- const state = new ReviewState(["a"], []);
394
- state.addComment(1, "fix");
395
- state.addOwnerReply("t1", "done", 1234567890);
396
- expect(state.threads[0].messages[1].ts).toBe(1234567890);
397
- });
398
- });
399
- });
@@ -1,159 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { buildPagerContent } from "../../src/tui/pager";
3
- import { ReviewState } from "../../src/state/review-state";
4
- import type { Thread } from "../../src/protocol/types";
5
-
6
- const SPEC = ["# Title", "Some text", "More text", "Final line"];
7
-
8
- function makeThread(
9
- id: string,
10
- line: number,
11
- status: Thread["status"],
12
- messages: Thread["messages"] = [{ author: "reviewer", text: "comment" }]
13
- ): Thread {
14
- return { id, line, status, messages };
15
- }
16
-
17
- describe("buildPagerContent", () => {
18
- it("renders lines with line numbers", () => {
19
- const state = new ReviewState(SPEC, []);
20
- const content = buildPagerContent(state);
21
- const lines = content.split("\n");
22
-
23
- expect(lines).toHaveLength(4);
24
- expect(lines[0]).toContain(" 1");
25
- expect(lines[0]).toContain("# Title");
26
- expect(lines[1]).toContain(" 2");
27
- expect(lines[1]).toContain("Some text");
28
- expect(lines[2]).toContain(" 3");
29
- expect(lines[3]).toContain(" 4");
30
- });
31
-
32
- it("marks cursor line with > prefix", () => {
33
- const state = new ReviewState(SPEC, []);
34
- state.cursorLine = 3;
35
- const content = buildPagerContent(state);
36
- const lines = content.split("\n");
37
-
38
- expect(lines[2]).toMatch(/^>/);
39
- expect(lines[0]).toMatch(/^ /);
40
- expect(lines[1]).toMatch(/^ /);
41
- expect(lines[3]).toMatch(/^ /);
42
- });
43
-
44
- it("shows gutter indicator for open threads", () => {
45
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "open")]);
46
- const content = buildPagerContent(state);
47
- const lines = content.split("\n");
48
-
49
- expect(lines[1]).toContain("\u258c");
50
- });
51
-
52
- it("shows gutter indicator for pending threads", () => {
53
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "pending")]);
54
- const content = buildPagerContent(state);
55
- const lines = content.split("\n");
56
-
57
- expect(lines[1]).toContain("\u258c");
58
- });
59
-
60
- it("shows checkmark for resolved threads", () => {
61
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "resolved")]);
62
- const content = buildPagerContent(state);
63
- const lines = content.split("\n");
64
-
65
- expect(lines[1]).toContain("\u2713");
66
- });
67
-
68
- it("shows gutter indicator for outdated threads", () => {
69
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "outdated")]);
70
- const content = buildPagerContent(state);
71
- const lines = content.split("\n");
72
-
73
- expect(lines[1]).toContain("\u258c");
74
- });
75
-
76
- it("shows different icons for different statuses", () => {
77
- const state = new ReviewState(SPEC, [
78
- makeThread("t1", 1, "open"),
79
- makeThread("t2", 2, "pending"),
80
- makeThread("t3", 3, "resolved"),
81
- makeThread("t4", 4, "outdated"),
82
- ]);
83
- const content = buildPagerContent(state);
84
- const lines = content.split("\n");
85
-
86
- expect(lines[0]).toContain("\u258c");
87
- expect(lines[1]).toContain("\u258c");
88
- expect(lines[2]).toContain("\u2713");
89
- expect(lines[3]).toContain("\u258c");
90
- });
91
-
92
- it("does not show inline comment preview (removed)", () => {
93
- const state = new ReviewState(SPEC, [
94
- makeThread("t1", 2, "open", [
95
- { author: "reviewer", text: "My response" },
96
- ]),
97
- ]);
98
- const content = buildPagerContent(state);
99
- const lines = content.split("\n");
100
-
101
- // Only gutter indicator, no inline text preview
102
- expect(lines[1]).toContain("\u258c");
103
- expect(lines[1]).not.toContain("My response");
104
- });
105
-
106
- it("does not show status indicator for lines without threads", () => {
107
- const state = new ReviewState(SPEC, [makeThread("t1", 2, "open")]);
108
- const content = buildPagerContent(state);
109
- const lines = content.split("\n");
110
-
111
- expect(lines[0]).not.toContain("\u258c");
112
- expect(lines[0]).not.toContain("\u2713");
113
- });
114
-
115
- it("pads line numbers to 4 characters", () => {
116
- const state = new ReviewState(SPEC, []);
117
- const content = buildPagerContent(state);
118
- const lines = content.split("\n");
119
-
120
- expect(lines[0]).toContain(" 1");
121
- expect(lines[0]).toContain("# Title");
122
- });
123
-
124
- it("handles empty spec", () => {
125
- const state = new ReviewState([], []);
126
- const content = buildPagerContent(state);
127
- expect(content).toBe("");
128
- });
129
-
130
- it("highlights search matches with >> << markers", () => {
131
- const state = new ReviewState(SPEC, []);
132
- const content = buildPagerContent(state, "text");
133
- const lines = content.split("\n");
134
-
135
- expect(lines[1]).toContain("Some >>text<<");
136
- expect(lines[2]).toContain("More >>text<<");
137
- expect(lines[0]).not.toContain(">>");
138
- expect(lines[3]).not.toContain(">>");
139
- });
140
-
141
- it("highlights search matches case-insensitively", () => {
142
- const state = new ReviewState(["Hello World", "hello again"], []);
143
- const content = buildPagerContent(state, "hello");
144
- const lines = content.split("\n");
145
-
146
- expect(lines[0]).toContain(">>Hello<<");
147
- expect(lines[1]).toContain(">>hello<<");
148
- });
149
-
150
- it("does not highlight when searchQuery is null", () => {
151
- const state = new ReviewState(SPEC, []);
152
- const content = buildPagerContent(state, null);
153
- const lines = content.split("\n");
154
-
155
- expect(lines[1]).toContain("Some text");
156
- expect(lines[1]).not.toContain(">>");
157
- });
158
-
159
- });
@@ -1,71 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { createKeybindRegistry } from "../../../src/tui/ui/keybinds";
3
-
4
- function makeKey(name: string, opts: { ctrl?: boolean; shift?: boolean; sequence?: string } = {}): any {
5
- return { name, ctrl: opts.ctrl ?? false, shift: opts.shift ?? false, sequence: opts.sequence ?? name };
6
- }
7
-
8
- describe("createKeybindRegistry", () => {
9
- it("matches single keys", () => {
10
- const reg = createKeybindRegistry([
11
- { key: "j", action: "down" },
12
- { key: "k", action: "up" },
13
- ]);
14
- expect(reg.match(makeKey("j"))).toBe("down");
15
- expect(reg.match(makeKey("k"))).toBe("up");
16
- expect(reg.match(makeKey("x"))).toBeNull();
17
- reg.destroy();
18
- });
19
-
20
- it("matches ctrl keys", () => {
21
- const reg = createKeybindRegistry([
22
- { key: "C-d", action: "half-page-down" },
23
- ]);
24
- expect(reg.match(makeKey("d", { ctrl: true }))).toBe("half-page-down");
25
- expect(reg.match(makeKey("d"))).toBeNull();
26
- reg.destroy();
27
- });
28
-
29
- it("matches shift keys", () => {
30
- const reg = createKeybindRegistry([
31
- { key: "G", action: "goto-bottom" },
32
- { key: "R", action: "resolve-all" },
33
- ]);
34
- expect(reg.match(makeKey("g", { shift: true }))).toBe("goto-bottom");
35
- expect(reg.match(makeKey("r", { shift: true }))).toBe("resolve-all");
36
- reg.destroy();
37
- });
38
-
39
- it("matches two-key sequences", () => {
40
- const reg = createKeybindRegistry([
41
- { key: "gg", action: "goto-top" },
42
- { key: "dd", action: "delete" },
43
- ]);
44
- expect(reg.match(makeKey("g"))).toBeNull();
45
- expect(reg.pending()).toBe("g...");
46
- expect(reg.match(makeKey("g"))).toBe("goto-top");
47
- expect(reg.pending()).toBeNull();
48
- reg.destroy();
49
- });
50
-
51
- it("clears sequence on invalid second key", () => {
52
- const reg = createKeybindRegistry([
53
- { key: "gg", action: "goto-top" },
54
- { key: "j", action: "down" },
55
- ]);
56
- expect(reg.match(makeKey("g"))).toBeNull();
57
- expect(reg.match(makeKey("x"))).toBeNull();
58
- expect(reg.match(makeKey("j"))).toBe("down");
59
- reg.destroy();
60
- });
61
-
62
- it("handles bracket sequences", () => {
63
- const reg = createKeybindRegistry([
64
- { key: "]t", action: "next-thread" },
65
- { key: "[t", action: "prev-thread" },
66
- ]);
67
- expect(reg.match(makeKey("]", { sequence: "]" }))).toBeNull();
68
- expect(reg.match(makeKey("t"))).toBe("next-thread");
69
- reg.destroy();
70
- });
71
- });
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "lib": ["ESNext"],
4
- "target": "ESNext",
5
- "module": "ESNext",
6
- "moduleResolution": "bundler",
7
- "strict": true,
8
- "skipLibCheck": true,
9
- "outDir": "dist",
10
- "rootDir": ".",
11
- "types": ["bun-types"]
12
- },
13
- "include": ["src/**/*.ts", "bin/**/*.ts", "test/**/*.ts"]
14
- }