revspec 0.5.0 → 0.6.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.
- package/CLAUDE.md +3 -1
- package/README.md +36 -11
- package/bun.lock +3 -0
- package/package.json +6 -3
- package/src/tui/app.ts +30 -6
- package/src/tui/comment-input.ts +14 -3
- package/src/tui/help.ts +76 -16
- package/src/tui/pager.ts +4 -2
- package/src/tui/search.ts +9 -4
- package/src/tui/status-bar.ts +1 -3
- package/test/e2e/__snapshots__/snapshot.test.ts.snap +31 -0
- package/test/e2e/fixtures/spec.md +36 -0
- package/test/e2e/harness.ts +80 -0
- package/test/e2e/snapshot.test.ts +182 -0
- package/test/{cli-reply.test.ts → integration/cli-reply.test.ts} +2 -2
- package/test/{cli-watch.test.ts → integration/cli-watch.test.ts} +2 -2
- package/test/{cli.test.ts → integration/cli.test.ts} +3 -3
- package/test/{e2e-live.test.ts → integration/e2e-live.test.ts} +4 -4
- package/test/{live-interaction.test.ts → integration/live-interaction.test.ts} +4 -4
- package/test/{protocol → unit/protocol}/live-events.test.ts +1 -1
- package/test/{protocol → unit/protocol}/live-merge.test.ts +3 -3
- package/test/{protocol → unit/protocol}/merge.test.ts +2 -2
- package/test/{protocol → unit/protocol}/read.test.ts +2 -2
- package/test/{protocol → unit/protocol}/types.test.ts +1 -1
- package/test/{protocol → unit/protocol}/write.test.ts +2 -2
- package/test/{state → unit/state}/review-state.test.ts +2 -2
- package/test/{tui → unit/tui}/pager.test.ts +3 -3
- package/test/{tui → unit/tui}/ui/keybinds.test.ts +1 -1
- /package/test/{opentui-smoke.test.ts → integration/opentui-smoke.test.ts} +0 -0
package/CLAUDE.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
- Tech: Bun + TypeScript + @opentui/core
|
|
4
4
|
- npm: `revspec` | GitHub: icyrainz/revspec
|
|
5
5
|
- Run: `bun run bin/revspec.ts <file.md>`
|
|
6
|
-
- Test: `bun test`
|
|
6
|
+
- Test: `bun run test` (~3s, excludes E2E)
|
|
7
|
+
- E2E: `bun run test:e2e` (~25s, bun-pty snapshots — only run before release, update with `--update-snapshots`)
|
|
8
|
+
- All: `bun run test:all` (everything)
|
|
7
9
|
- Release: `./scripts/release.sh` (version is set manually in package.json)
|
|
8
10
|
- Dev: `bun link` to symlink local build to global `revspec` command
|
|
9
11
|
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Revspec renders markdown in-place (toggle with `m`):
|
|
|
35
35
|
|
|
36
36
|
- **Headings** — colored and bold, `#`–`######`
|
|
37
37
|
- **Inline** — bold (`**`/`__`), italic (`*`/`_`), bold-italic (`***`), strikethrough (`~~`), `code`, [links](url)
|
|
38
|
-
- **Fenced code blocks** —
|
|
38
|
+
- **Fenced code blocks** — ` ``` ` markers dimmed, body in green
|
|
39
39
|
- **Tables** — box-drawing borders, header row bolded, auto-column-widths
|
|
40
40
|
- **Lists** — unordered (`•`), ordered, task lists (`☐`/`☑`)
|
|
41
41
|
- **Blockquotes** — bar gutter, italicized text
|
|
@@ -44,26 +44,41 @@ Revspec renders markdown in-place (toggle with `m`):
|
|
|
44
44
|
|
|
45
45
|
### Keybindings
|
|
46
46
|
|
|
47
|
+
**Navigation**
|
|
48
|
+
|
|
47
49
|
| Key | Action |
|
|
48
50
|
|-----|--------|
|
|
49
51
|
| `j/k` | Move cursor down/up |
|
|
50
52
|
| `gg` / `G` | Go to top / bottom |
|
|
51
53
|
| `Ctrl+D/U` | Half page down/up |
|
|
52
|
-
| `
|
|
53
|
-
|
|
|
54
|
-
| `r` | Resolve thread (toggle) |
|
|
55
|
-
| `R` | Resolve all pending |
|
|
56
|
-
| `dd` | Delete draft comment (double-tap) |
|
|
57
|
-
| `/` | Search |
|
|
54
|
+
| `zz` | Center cursor line in viewport |
|
|
55
|
+
| `/` | Search (smartcase: lowercase = case-insensitive, any uppercase = case-sensitive) |
|
|
58
56
|
| `n/N` | Next/prev search match |
|
|
57
|
+
| `Esc` | Clear search highlights |
|
|
59
58
|
| `]t/[t` | Next/prev thread |
|
|
60
59
|
| `]r/[r` | Next/prev unread AI reply |
|
|
61
|
-
|
|
60
|
+
|
|
61
|
+
**Review**
|
|
62
|
+
|
|
63
|
+
| Key | Action |
|
|
64
|
+
|-----|--------|
|
|
65
|
+
| `c` | Open thread / comment on line |
|
|
66
|
+
| `r` | Resolve thread (toggle) |
|
|
67
|
+
| `R` | Resolve all pending |
|
|
68
|
+
| `dd` | Delete thread (with confirm) |
|
|
69
|
+
| `T` | List threads |
|
|
62
70
|
| `a` | Approve spec |
|
|
71
|
+
|
|
72
|
+
**Commands**
|
|
73
|
+
|
|
74
|
+
| Key | Action |
|
|
75
|
+
|-----|--------|
|
|
63
76
|
| `:w` | Merge changes to review JSON |
|
|
64
|
-
| `:wq` | Merge and quit |
|
|
65
|
-
| `:q` | Quit (
|
|
77
|
+
| `:wq` / `:qw` | Merge and quit |
|
|
78
|
+
| `:q` | Quit (blocks if unsaved) |
|
|
66
79
|
| `:q!` | Quit without merging |
|
|
80
|
+
| `:{N}` | Jump to line N (e.g. `:42`) |
|
|
81
|
+
| `Ctrl+C` | Quit without merging |
|
|
67
82
|
| `?` | Help |
|
|
68
83
|
|
|
69
84
|
### Thread popup
|
|
@@ -71,7 +86,7 @@ Revspec renders markdown in-place (toggle with `m`):
|
|
|
71
86
|
The thread popup has two modes:
|
|
72
87
|
|
|
73
88
|
- **Insert mode** — type your comment, `Tab` sends, `Esc` switches to normal mode
|
|
74
|
-
- **Normal mode** — `j/k` and `Ctrl+D/U` scroll the conversation
|
|
89
|
+
- **Normal mode** — `j/k` and `Ctrl+D/U` scroll the conversation, `gg/G` top/bottom, `c` to reply, `r` to resolve, `Esc` to close
|
|
75
90
|
|
|
76
91
|
## Live AI Integration
|
|
77
92
|
|
|
@@ -125,6 +140,16 @@ Install the `/revspec` skill for Claude Code:
|
|
|
125
140
|
|
|
126
141
|
Then use `/revspec` in Claude Code after generating a spec.
|
|
127
142
|
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
bun test # Run all tests (~70s)
|
|
147
|
+
bun test test/e2e # E2E snapshot tests only (~66s)
|
|
148
|
+
bun test --update-snapshots # Regenerate snapshots after UI changes
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
E2E tests use `bun-pty` to spawn revspec in a pseudo-terminal (80x24), send keystrokes, capture plain-text screen output, and compare against saved snapshots. Covers: navigation, search, overlays (help, comment, thread list, confirm), thread creation/resolve/delete, command mode, and context-sensitive hints.
|
|
152
|
+
|
|
128
153
|
## Protocol
|
|
129
154
|
|
|
130
155
|
Communication happens through a JSONL file (`spec.review.live.jsonl`) — append-only, both sides write to it. On session end, events are merged into `spec.review.json`.
|
package/bun.lock
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@types/bun": "latest",
|
|
12
|
+
"bun-pty": "^0.4.8",
|
|
12
13
|
},
|
|
13
14
|
"peerDependencies": {
|
|
14
15
|
"typescript": "^5",
|
|
@@ -110,6 +111,8 @@
|
|
|
110
111
|
|
|
111
112
|
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
|
|
112
113
|
|
|
114
|
+
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
|
115
|
+
|
|
113
116
|
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
114
117
|
|
|
115
118
|
"bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="],
|
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "revspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"revspec": "./bin/revspec.ts"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "bun run bin/revspec.ts",
|
|
10
|
-
"test": "bun test"
|
|
10
|
+
"test": "bun test test/unit test/integration",
|
|
11
|
+
"test:e2e": "bun test test/e2e",
|
|
12
|
+
"test:all": "bun test"
|
|
11
13
|
},
|
|
12
14
|
"devDependencies": {
|
|
13
|
-
"@types/bun": "latest"
|
|
15
|
+
"@types/bun": "latest",
|
|
16
|
+
"bun-pty": "^0.4.8"
|
|
14
17
|
},
|
|
15
18
|
"peerDependencies": {
|
|
16
19
|
"typescript": "^5"
|
package/src/tui/app.ts
CHANGED
|
@@ -253,6 +253,14 @@ export async function runTui(
|
|
|
253
253
|
keybinds.destroy();
|
|
254
254
|
return "exit";
|
|
255
255
|
}
|
|
256
|
+
// :{N} — jump to line number
|
|
257
|
+
const lineNum = parseInt(cmd, 10);
|
|
258
|
+
if (!isNaN(lineNum) && lineNum > 0) {
|
|
259
|
+
state.cursorLine = Math.min(lineNum, state.lineCount);
|
|
260
|
+
ensureCursorVisible();
|
|
261
|
+
refreshPager();
|
|
262
|
+
return "stay";
|
|
263
|
+
}
|
|
256
264
|
return "stay"; // unknown command, ignore
|
|
257
265
|
}
|
|
258
266
|
|
|
@@ -361,11 +369,14 @@ export async function runTui(
|
|
|
361
369
|
currentLine: number,
|
|
362
370
|
direction: 1 | -1
|
|
363
371
|
): number | null {
|
|
364
|
-
|
|
372
|
+
// Smartcase: if query has any uppercase, case-sensitive
|
|
373
|
+
const caseSensitive = query !== query.toLowerCase();
|
|
374
|
+
const q = caseSensitive ? query : query.toLowerCase();
|
|
365
375
|
const total = lines.length;
|
|
366
376
|
for (let offset = 1; offset <= total; offset++) {
|
|
367
377
|
const i = ((currentLine - 1) + offset * direction + total) % total;
|
|
368
|
-
|
|
378
|
+
const line = caseSensitive ? lines[i] : lines[i].toLowerCase();
|
|
379
|
+
if (line.includes(q)) {
|
|
369
380
|
return i + 1; // 1-based
|
|
370
381
|
}
|
|
371
382
|
}
|
|
@@ -386,7 +397,7 @@ export async function runTui(
|
|
|
386
397
|
{ key: "n", action: "search-next" },
|
|
387
398
|
{ key: "N", action: "search-prev" },
|
|
388
399
|
{ key: "c", action: "comment" },
|
|
389
|
-
{ key: "
|
|
400
|
+
{ key: "T", action: "thread-list" },
|
|
390
401
|
{ key: "r", action: "resolve" },
|
|
391
402
|
{ key: "R", action: "resolve-all" },
|
|
392
403
|
{ key: "dd", action: "delete-draft" },
|
|
@@ -395,11 +406,12 @@ export async function runTui(
|
|
|
395
406
|
{ key: "[t", action: "prev-thread" },
|
|
396
407
|
{ key: "]r", action: "next-unread" },
|
|
397
408
|
{ key: "[r", action: "prev-unread" },
|
|
409
|
+
{ key: "zz", action: "center-cursor" },
|
|
398
410
|
{ key: "?", action: "help" },
|
|
399
411
|
{ key: "/", action: "search" },
|
|
400
412
|
{ key: ":", action: "command-mode" },
|
|
401
413
|
];
|
|
402
|
-
const keybinds = createKeybindRegistry(bindings);
|
|
414
|
+
const keybinds = createKeybindRegistry(bindings, 300);
|
|
403
415
|
|
|
404
416
|
refreshPager();
|
|
405
417
|
renderer.start();
|
|
@@ -460,9 +472,13 @@ export async function runTui(
|
|
|
460
472
|
return;
|
|
461
473
|
}
|
|
462
474
|
|
|
463
|
-
// Ctrl+C to exit —
|
|
475
|
+
// Ctrl+C to exit — quit without merging (same as :q!)
|
|
464
476
|
if (key.ctrl && key.name === "c") {
|
|
465
|
-
|
|
477
|
+
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
478
|
+
liveWatcher.stop();
|
|
479
|
+
keybinds.destroy();
|
|
480
|
+
renderer.destroy();
|
|
481
|
+
resolve();
|
|
466
482
|
return;
|
|
467
483
|
}
|
|
468
484
|
|
|
@@ -527,6 +543,14 @@ export async function runTui(
|
|
|
527
543
|
ensureCursorVisible();
|
|
528
544
|
refreshPager();
|
|
529
545
|
break;
|
|
546
|
+
case "center-cursor": {
|
|
547
|
+
const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1);
|
|
548
|
+
const cursorRow = state.cursorLine - 1 + extra;
|
|
549
|
+
const halfView = Math.floor(pageSize() / 2);
|
|
550
|
+
pager.scrollBox.scrollTo(Math.max(0, cursorRow - halfView));
|
|
551
|
+
refreshPager();
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
530
554
|
case "search-next":
|
|
531
555
|
if (searchQuery) {
|
|
532
556
|
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, 1);
|
package/src/tui/comment-input.ts
CHANGED
|
@@ -50,16 +50,17 @@ function createThreadView(
|
|
|
50
50
|
{ key: "NORMAL", action: "" },
|
|
51
51
|
{ key: "c", action: "reply" },
|
|
52
52
|
{ key: "r", action: "resolve" },
|
|
53
|
-
{ key: "
|
|
53
|
+
{ key: "q", action: "close" },
|
|
54
54
|
];
|
|
55
55
|
const insertHints = [
|
|
56
56
|
{ key: "INSERT", action: "" },
|
|
57
57
|
{ key: "Tab", action: "send" },
|
|
58
|
-
{ key: "Esc", action: "
|
|
58
|
+
{ key: "Esc", action: "normal" },
|
|
59
59
|
];
|
|
60
60
|
|
|
61
61
|
// --- State ---
|
|
62
62
|
let mode: "normal" | "insert" = "insert";
|
|
63
|
+
let pendingG: ReturnType<typeof setTimeout> | null = null;
|
|
63
64
|
|
|
64
65
|
// Build the textarea now (we need it in the key handler closure)
|
|
65
66
|
const textarea = new TextareaRenderable(renderer, {
|
|
@@ -151,10 +152,19 @@ function createThreadView(
|
|
|
151
152
|
case "g":
|
|
152
153
|
if (key.shift) {
|
|
153
154
|
// G = go to bottom
|
|
155
|
+
if (pendingG) { clearTimeout(pendingG); pendingG = null; }
|
|
154
156
|
scrollBox.scrollTo(scrollBox.scrollHeight);
|
|
155
157
|
renderer.requestRender();
|
|
158
|
+
} else if (pendingG) {
|
|
159
|
+
// gg = go to top
|
|
160
|
+
clearTimeout(pendingG);
|
|
161
|
+
pendingG = null;
|
|
162
|
+
scrollBox.scrollTo(0);
|
|
163
|
+
renderer.requestRender();
|
|
164
|
+
} else {
|
|
165
|
+
// First g — wait for second
|
|
166
|
+
pendingG = setTimeout(() => { pendingG = null; }, 300);
|
|
156
167
|
}
|
|
157
|
-
// TODO: gg = go to top (needs double-tap tracking)
|
|
158
168
|
return;
|
|
159
169
|
}
|
|
160
170
|
};
|
|
@@ -273,6 +283,7 @@ function createThreadView(
|
|
|
273
283
|
return {
|
|
274
284
|
container: dialog.container,
|
|
275
285
|
cleanup() {
|
|
286
|
+
if (pendingG) clearTimeout(pendingG);
|
|
276
287
|
renderer.keyInput.off("keypress", keyHandler);
|
|
277
288
|
dialog.cleanup();
|
|
278
289
|
textarea.destroy();
|
package/src/tui/help.ts
CHANGED
|
@@ -50,14 +50,14 @@ export function createHelp(opts: {
|
|
|
50
50
|
renderer,
|
|
51
51
|
title: "Help",
|
|
52
52
|
width: "60%",
|
|
53
|
-
height: Math.min(
|
|
53
|
+
height: Math.min(34, renderer.height - 2),
|
|
54
54
|
top: "10%",
|
|
55
55
|
left: "20%",
|
|
56
56
|
borderColor: theme.info,
|
|
57
57
|
onDismiss: onClose,
|
|
58
58
|
hints: [
|
|
59
|
+
{ key: "j/k", action: "navigate" },
|
|
59
60
|
{ key: "q/?/Esc", action: "close" },
|
|
60
|
-
{ key: "j/k", action: "scroll" },
|
|
61
61
|
],
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -71,41 +71,59 @@ export function createHelp(opts: {
|
|
|
71
71
|
wrapMode: "none",
|
|
72
72
|
}));
|
|
73
73
|
|
|
74
|
+
addHelpSection(dialog.content, renderer, "Quick Start", [
|
|
75
|
+
" Navigate to a line and press c to comment.",
|
|
76
|
+
" The AI replies in real-time via the thread popup.",
|
|
77
|
+
" Press r to resolve threads, a to approve the spec.",
|
|
78
|
+
" Use :wq to save and quit when done reviewing.",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
addHelpSection(dialog.content, renderer, "Thread Popup", [
|
|
82
|
+
" Opens in INSERT mode — type and press Tab to send.",
|
|
83
|
+
" Press Esc for NORMAL mode — scroll with j/k/gg/G,",
|
|
84
|
+
" c to reply, r to resolve, q to close.",
|
|
85
|
+
]);
|
|
86
|
+
|
|
74
87
|
addHelpSection(dialog.content, renderer, "Navigation", [
|
|
75
88
|
" j/k Down/up",
|
|
76
|
-
" gg
|
|
77
|
-
" G Go to last line / scroll to bottom",
|
|
89
|
+
" gg/G Top/bottom",
|
|
78
90
|
" Ctrl+d/u Half page down/up",
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
91
|
+
" zz Center cursor line",
|
|
92
|
+
" / Search (smartcase)",
|
|
93
|
+
" n/N Next/prev match",
|
|
94
|
+
" Esc Clear search",
|
|
82
95
|
" ]t/[t Next/prev thread",
|
|
83
|
-
" ]r/[r Next/prev unread
|
|
96
|
+
" ]r/[r Next/prev unread",
|
|
84
97
|
]);
|
|
85
98
|
|
|
86
99
|
addHelpSection(dialog.content, renderer, "Review", [
|
|
87
|
-
" c Comment / view thread
|
|
88
|
-
" r Resolve thread",
|
|
100
|
+
" c Comment / view thread",
|
|
101
|
+
" r Resolve thread (toggle)",
|
|
89
102
|
" R Resolve all pending",
|
|
90
|
-
" dd Delete thread
|
|
91
|
-
"
|
|
103
|
+
" dd Delete thread",
|
|
104
|
+
" T List threads",
|
|
92
105
|
" a Approve spec",
|
|
93
106
|
]);
|
|
94
107
|
|
|
95
108
|
addHelpSection(dialog.content, renderer, "Commands", [
|
|
96
|
-
" :w
|
|
97
|
-
" :
|
|
98
|
-
" :
|
|
99
|
-
" :q! Quit without
|
|
109
|
+
" :w Merge to review JSON",
|
|
110
|
+
" :wq Merge and quit",
|
|
111
|
+
" :q Quit (blocks if unmerged)",
|
|
112
|
+
" :q! Quit without merging",
|
|
113
|
+
" :{N} Jump to line N",
|
|
114
|
+
" Ctrl+C Quit without merging",
|
|
100
115
|
]);
|
|
101
116
|
|
|
102
117
|
// Trailing blank line
|
|
103
118
|
dialog.content.add(new TextRenderable(renderer, { content: "", width: "100%", height: 1, wrapMode: "none" }));
|
|
104
119
|
|
|
120
|
+
let pendingG: ReturnType<typeof setTimeout> | null = null;
|
|
121
|
+
|
|
105
122
|
const extraKeyHandler = (key: KeyEvent) => {
|
|
106
123
|
if (key.name === "q" || key.sequence === "?") {
|
|
107
124
|
key.preventDefault();
|
|
108
125
|
key.stopPropagation();
|
|
126
|
+
if (pendingG) { clearTimeout(pendingG); pendingG = null; }
|
|
109
127
|
onClose();
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
@@ -123,6 +141,47 @@ export function createHelp(opts: {
|
|
|
123
141
|
renderer.requestRender();
|
|
124
142
|
return;
|
|
125
143
|
}
|
|
144
|
+
// G = goto bottom
|
|
145
|
+
if (key.name === "g" && key.shift) {
|
|
146
|
+
key.preventDefault();
|
|
147
|
+
key.stopPropagation();
|
|
148
|
+
if (pendingG) { clearTimeout(pendingG); pendingG = null; }
|
|
149
|
+
dialog.content.scrollTo(dialog.content.scrollHeight);
|
|
150
|
+
renderer.requestRender();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// gg = goto top
|
|
154
|
+
if (key.name === "g" && !key.shift && !key.ctrl) {
|
|
155
|
+
key.preventDefault();
|
|
156
|
+
key.stopPropagation();
|
|
157
|
+
if (pendingG) {
|
|
158
|
+
clearTimeout(pendingG);
|
|
159
|
+
pendingG = null;
|
|
160
|
+
dialog.content.scrollTo(0);
|
|
161
|
+
renderer.requestRender();
|
|
162
|
+
} else {
|
|
163
|
+
pendingG = setTimeout(() => { pendingG = null; }, 300);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Ctrl+D = half page down
|
|
168
|
+
if (key.ctrl && key.name === "d") {
|
|
169
|
+
key.preventDefault();
|
|
170
|
+
key.stopPropagation();
|
|
171
|
+
const half = Math.max(1, Math.floor((renderer.height - 4) / 2));
|
|
172
|
+
dialog.content.scrollTo(Math.min(dialog.content.scrollTop + half, dialog.content.scrollHeight));
|
|
173
|
+
renderer.requestRender();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Ctrl+U = half page up
|
|
177
|
+
if (key.ctrl && key.name === "u") {
|
|
178
|
+
key.preventDefault();
|
|
179
|
+
key.stopPropagation();
|
|
180
|
+
const half = Math.max(1, Math.floor((renderer.height - 4) / 2));
|
|
181
|
+
dialog.content.scrollTo(Math.max(dialog.content.scrollTop - half, 0));
|
|
182
|
+
renderer.requestRender();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
126
185
|
};
|
|
127
186
|
|
|
128
187
|
renderer.keyInput.on("keypress", extraKeyHandler);
|
|
@@ -130,6 +189,7 @@ export function createHelp(opts: {
|
|
|
130
189
|
return {
|
|
131
190
|
container: dialog.container,
|
|
132
191
|
cleanup() {
|
|
192
|
+
if (pendingG) clearTimeout(pendingG);
|
|
133
193
|
dialog.cleanup();
|
|
134
194
|
renderer.keyInput.off("keypress", extraKeyHandler);
|
|
135
195
|
},
|
package/src/tui/pager.ts
CHANGED
|
@@ -35,7 +35,8 @@ export function buildPagerContent(state: ReviewState, searchQuery?: string | nul
|
|
|
35
35
|
const prefix = isCursor ? ">" : " ";
|
|
36
36
|
let specText = state.specLines[i];
|
|
37
37
|
if (searchQuery) {
|
|
38
|
-
const
|
|
38
|
+
const csSensitive = searchQuery !== searchQuery.toLowerCase();
|
|
39
|
+
const regex = new RegExp(searchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), csSensitive ? "g" : "gi");
|
|
39
40
|
specText = specText.replace(regex, (match) => `>>${match}<<`);
|
|
40
41
|
}
|
|
41
42
|
let indicator = " ";
|
|
@@ -173,7 +174,8 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
|
|
|
173
174
|
} else if (searchQuery) {
|
|
174
175
|
// When searching, show colored match segments (no markdown styling)
|
|
175
176
|
const escaped = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
176
|
-
const
|
|
177
|
+
const caseSensitive = searchQuery !== searchQuery.toLowerCase();
|
|
178
|
+
const searchRegex = new RegExp(`(${escaped})`, caseSensitive ? "g" : "gi");
|
|
177
179
|
const parts = specText.split(searchRegex);
|
|
178
180
|
for (let p = 0; p < parts.length; p++) {
|
|
179
181
|
const part = parts[p];
|
package/src/tui/search.ts
CHANGED
|
@@ -83,18 +83,23 @@ export function createSearch(opts: SearchOptions): SearchOverlay {
|
|
|
83
83
|
if (key.name === "return") {
|
|
84
84
|
key.preventDefault();
|
|
85
85
|
key.stopPropagation();
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
86
|
+
const raw = input.value.trim();
|
|
87
|
+
if (raw.length === 0) {
|
|
88
88
|
onCancel();
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Smartcase: if query has any uppercase, case-sensitive; otherwise case-insensitive
|
|
93
|
+
const caseSensitive = raw !== raw.toLowerCase();
|
|
94
|
+
const query = caseSensitive ? raw : raw.toLowerCase();
|
|
95
|
+
|
|
92
96
|
// Search forward from cursor, wrapping around
|
|
93
97
|
const total = specLines.length;
|
|
94
98
|
for (let offset = 1; offset <= total; offset++) {
|
|
95
99
|
const i = (cursorLine - 1 + offset) % total;
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const line = caseSensitive ? specLines[i] : specLines[i].toLowerCase();
|
|
101
|
+
if (line.includes(query)) {
|
|
102
|
+
onResult(i + 1, raw); // 1-based line number + original query (preserves case for n/N)
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
100
105
|
}
|
package/src/tui/status-bar.ts
CHANGED
|
@@ -84,14 +84,12 @@ export function buildBottomBar(bar: BottomBarComponents, commandBuffer: string |
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
const hints: Hint[] = [
|
|
87
|
-
{ key: "j/k", action: "
|
|
87
|
+
{ key: "j/k", action: "navigate" },
|
|
88
88
|
{ key: "c", action: "comment" },
|
|
89
89
|
];
|
|
90
90
|
if (hasThread) {
|
|
91
91
|
hints.push({ key: "r", action: "resolve" });
|
|
92
|
-
hints.push({ key: "dd", action: "delete thread" });
|
|
93
92
|
}
|
|
94
|
-
hints.push({ key: "/", action: "search" });
|
|
95
93
|
hints.push({ key: "?", action: "help" });
|
|
96
94
|
buildHints(t, hints);
|
|
97
95
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
|
+
|
|
3
|
+
exports[`revspec E2E snapshots initial render 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20"`;
|
|
4
|
+
|
|
5
|
+
exports[`revspec E2E snapshots G scrolls to bottom 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 37/37 17 │ Key │ Format │ TTL │ 18├──────────────┼───────────┼─────┤ 19│ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ 20 21Tasks 22 23☑ Implement login 24☐ Add rate limitg 25 26│ Note: tokens are not encrypted at rest. 27█28Code Example █29█30\`\`\`typescript █31const token: string = await auth.login();█ 32 \`\`\` █33 █34────────────────────────────────────────█35 █ 36 End of spec. █> 37 █"`;
|
|
6
|
+
|
|
7
|
+
exports[`revspec E2E snapshots j/k moves cursor 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 6 1 Auth System Design> 6"`;
|
|
8
|
+
|
|
9
|
+
exports[`revspec E2E snapshots Ctrl+D half page down 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 2/37 1 Auth System Design> 12"`;
|
|
10
|
+
|
|
11
|
+
exports[`revspec E2E snapshots / search highlights matches 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 / Search..."`;
|
|
12
|
+
|
|
13
|
+
exports[`revspec E2E snapshots n jumps to next match 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 / Search... n"`;
|
|
14
|
+
|
|
15
|
+
exports[`revspec E2E snapshots ? opens help 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 ┌─ Help ───────────────────────────────────────┐│ ││ ▀ ││ revspec vX.Y.Z ││ ││ Quick Start ││ Navigate to a line and press c to comment ││ The AI replies in real-time via the threa ││ Press r to resolve threads, a to approve ││ Use :wq to save and quit when done review ││ ││ Thread Popup ││ Opens in INSERT mode — type and press Tab ││ Press Esc for NORMAL mode — scroll with j ││ c to reply, r to resolve, q to close. ││ ││ Navigation ││ j/k Down/up ││ gg/G Top/bottom ││ Ctrl+d/u Half page down/up ││ [j/k] navigate [q/?/Esc] close │└──────────────────────────────────────────────┘"`;
|
|
16
|
+
|
|
17
|
+
exports[`revspec E2E snapshots c opens comment input 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘"`;
|
|
18
|
+
|
|
19
|
+
exports[`revspec E2E snapshots Tab submits comment 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │This is a test commentNORMALc] reply [r] resolve [q] closerrsolve [?] help"`;
|
|
20
|
+
|
|
21
|
+
exports[`revspec E2E snapshots Esc switches to normal mode 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ NORMALc] reply [r] resolve [q] close"`;
|
|
22
|
+
|
|
23
|
+
exports[`revspec E2E snapshots thread gutter indicator 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │Test threadNORMALc] reply [r] resolve [q] closerrsolve [?] helputh System Design verview « Test thread sers authenticate via OAuth2 with Google and GitHub providers. ndpoints POST /auth/login — initiates OAuth flow GET /auth/callback — handles provider callback POST /auth/refresh — refreshes expired tokens oken Storage okens are stored in Redis as JSON blobs. See Redis docs. ──────────────┬───────────┬─────┐ Key │ Format │ TTL │ ──────────────┼───────────┼─────┤ session:{id} │ JSON blob │ 24h │ ──────────────┴───────────┴─────┘"`;
|
|
24
|
+
|
|
25
|
+
exports[`revspec E2E snapshots T opens thread list 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 ┌─ Threads (0 active) ─────────────────────────────────┐│ ││ No active threads. Press [Esc] to close. █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [j/k] navigate [Enter] jump [Esc] close │└──────────────────────────────────────────────────────┘"`;
|
|
26
|
+
|
|
27
|
+
exports[`revspec E2E snapshots r resolves thread 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │Comment to resolveNORMALc] reply [r] resolve [q] closerrsolve [?] helputh System Design verview « Comment to resolve sers authenticate via OAuth2 with Google and GitHub providers. ndpoints POST /auth/login — initiates OAuth flow GET /auth/callback — handles provider callback POST /auth/refresh — refreshes expired tokens oken Storage okens are stored in Redis as JSON blobs. See Redis docs. ──────────────┬───────────┬─────┐ Key │ Format │ TTL │ ──────────────┼───────────┼─────┤ session:{id} │ JSON blob │ 24h │ ──────────────┴───────────┴─────┘ No active threads · L4/37✓✔ Resolved thread #t1"`;
|
|
28
|
+
|
|
29
|
+
exports[`revspec E2E snapshots dd y deletes thread 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 4 1 Auth System Design> 4 ┌─ New comment on line 4 ──────────────────────────────────────┐│ ││ █ ││ █ ││ █ ││ █ ││ █ ││ █ ││ ││ ││ ││ ││ ││ [INSERT] [Tab] send [Esc] normal ││ ──────────────────────────────────────── ││ Press c to reply... ││ ││ ││ ││ │└──────────────────────────────────────────────────────────────┘ 1 open · L4/37 Thread #t1 (line 4)──│You YYYY-MM-DD HH:MM:SS▌ │Comment to deleteNORMALc] reply [r] resolve [q] closerrsolve [?] helputh System Design verview « Comment to delete sers authenticate via OAuth2 with Google and GitHub providers. ndpoints POST /auth/login — initiates OAuth flow GET /auth/callback — handles provider callback POST /auth/refresh — refreshes expired tokens oken Storage okens are stored in Redis as JSON blobs. See Redis docs. ──────────────┬───────────┬─────┐ Key │ Format │ TTL │ ──────────────┼───────────┼─────┤ session:{id} │ JSON blob │ 24h │ ──────────────┴───────────┴─────┘ ┌─ Delete Thread ──────────────────────┐│ ││ Delete thread #t1 on line 4? ││ ││ ││ ││ ││ [y] yes [n/Esc] no │└──────────────────────────────────────┘d... No active threads · L4/37 login — initiates OAuth flow allback — handles provider callback refresh — refreshes expired tokens ored in Redis as JSON blobs. See Redis d ✔ Deleted thread #t1"`;
|
|
30
|
+
|
|
31
|
+
exports[`revspec E2E snapshots :w merges review 1`] = `" spec.md · No active threads · L1/37 > 1 Auth System Design █ 2 3 Overview 4 5 Users authenticate via OAuth2 with Google and GitHub providers. 6 7 Endpoints 8 9 • POST /auth/login — initiates OAuth flow 10 • GET /auth/callback — handles provider callback 11 • POST /auth/refresh — refreshes expired tokens 12 13 Token Storage 14 15 Tokens are stored in Redis as JSON blobs. See Redis docs. 16 ┌──────────────┬───────────┬─────┐ 17 │ Key │ Format │ TTL │ 18 ├──────────────┼───────────┼─────┤ 19 │ session:{id} │ JSON blob │ 24h │ └──────────────┴───────────┴─────┘ [j/k] navigate [c] comment [?] help ██████████ 20 :w"`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Auth System Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Users authenticate via **OAuth2** with `Google` and *GitHub* providers.
|
|
6
|
+
|
|
7
|
+
## Endpoints
|
|
8
|
+
|
|
9
|
+
- POST /auth/login — initiates OAuth flow
|
|
10
|
+
- GET /auth/callback — handles provider callback
|
|
11
|
+
- POST /auth/refresh — refreshes ~~expired~~ tokens
|
|
12
|
+
|
|
13
|
+
## Token Storage
|
|
14
|
+
|
|
15
|
+
Tokens are stored in Redis as JSON blobs. See [Redis docs](https://redis.io).
|
|
16
|
+
|
|
17
|
+
| Key | Format | TTL |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `session:{id}` | JSON blob | 24h |
|
|
20
|
+
|
|
21
|
+
## Tasks
|
|
22
|
+
|
|
23
|
+
- [x] Implement login
|
|
24
|
+
- [ ] Add rate limiting
|
|
25
|
+
|
|
26
|
+
> Note: tokens are **not encrypted** at rest.
|
|
27
|
+
|
|
28
|
+
### Code Example
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const token: string = await auth.login();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
End of spec.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawn, type IPty } from "bun-pty";
|
|
2
|
+
import { resolve, dirname, basename, join } from "path";
|
|
3
|
+
import { unlinkSync, existsSync } from "fs";
|
|
4
|
+
|
|
5
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
6
|
+
|
|
7
|
+
function stripAnsi(str: string): string {
|
|
8
|
+
return str
|
|
9
|
+
.replace(/\x1bP(?:[^\x1b]|\x1b[^\\])*\x1b\\/g, "")
|
|
10
|
+
.replace(/\x1b\[[0-9;?>=!]*[ -/]*[A-Za-z@`~]/g, "")
|
|
11
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
12
|
+
.replace(/\x1b[()][0-9A-Z]/g, "")
|
|
13
|
+
.replace(/\x1b[>=<]/g, "")
|
|
14
|
+
.replace(/\x1b./g, "")
|
|
15
|
+
.replace(/\r/g, "")
|
|
16
|
+
.replace(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g, "YYYY-MM-DD HH:MM:SS")
|
|
17
|
+
.replace(/revspec v\d+\.\d+\.\d+/g, "revspec vX.Y.Z");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TuiHarness {
|
|
21
|
+
sendKeys: (keys: string) => void;
|
|
22
|
+
wait: (ms?: number) => Promise<void>;
|
|
23
|
+
capture: () => string;
|
|
24
|
+
contains: (text: string) => boolean;
|
|
25
|
+
quit: () => Promise<void>;
|
|
26
|
+
cleanReviewFiles: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function createHarness(specFile: string, opts?: { cols?: number; rows?: number }): Promise<TuiHarness> {
|
|
30
|
+
const cols = opts?.cols ?? 80;
|
|
31
|
+
const rows = opts?.rows ?? 24;
|
|
32
|
+
const specPath = resolve(specFile);
|
|
33
|
+
let buffer = "";
|
|
34
|
+
|
|
35
|
+
const pty = spawn("bun", ["run", CLI, specPath], {
|
|
36
|
+
name: "xterm-256color",
|
|
37
|
+
cols,
|
|
38
|
+
rows,
|
|
39
|
+
env: { ...process.env, TERM: "xterm-256color", NO_COLOR: undefined, FORCE_COLOR: undefined },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
pty.onData((data: string) => { buffer += data; });
|
|
43
|
+
|
|
44
|
+
function sendKeys(keys: string): void { pty.write(keys); }
|
|
45
|
+
|
|
46
|
+
function wait(ms = 50): Promise<void> {
|
|
47
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function capture(): string {
|
|
51
|
+
const clean = stripAnsi(buffer);
|
|
52
|
+
const lines = clean.split("\n");
|
|
53
|
+
return lines.slice(Math.max(0, lines.length - rows), lines.length).join("\n").trimEnd();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function contains(text: string): boolean { return capture().includes(text); }
|
|
57
|
+
|
|
58
|
+
function cleanReviewFiles(): void {
|
|
59
|
+
const dir = dirname(specPath);
|
|
60
|
+
const base = basename(specPath, ".md");
|
|
61
|
+
for (const ext of [".review.json", ".review.live.jsonl", ".review.live.offset", ".review.live.lock", ".review.draft.json"]) {
|
|
62
|
+
const f = join(dir, `${base}${ext}`);
|
|
63
|
+
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function quit(): Promise<void> {
|
|
68
|
+
sendKeys("\x1b");
|
|
69
|
+
await wait(30);
|
|
70
|
+
sendKeys(":q!\n");
|
|
71
|
+
await wait(100);
|
|
72
|
+
try { pty.kill(); } catch {}
|
|
73
|
+
cleanReviewFiles();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wait for initial render
|
|
77
|
+
await wait(150);
|
|
78
|
+
|
|
79
|
+
return { sendKeys, wait, capture, contains, quit, cleanReviewFiles };
|
|
80
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
+
import { createHarness, type TuiHarness } from "./harness";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
|
|
5
|
+
const SPEC = resolve(import.meta.dir, "fixtures/spec.md");
|
|
6
|
+
const S = 350; // sequence wait (gg/dd need 300ms timeout)
|
|
7
|
+
|
|
8
|
+
describe("revspec E2E snapshots", () => {
|
|
9
|
+
let harness: TuiHarness | null = null;
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (harness) { await harness.quit(); harness = null; }
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("initial render", async () => {
|
|
16
|
+
harness = await createHarness(SPEC);
|
|
17
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("G scrolls to bottom", async () => {
|
|
21
|
+
harness = await createHarness(SPEC);
|
|
22
|
+
harness.sendKeys("G");
|
|
23
|
+
await harness.wait();
|
|
24
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("j/k moves cursor", async () => {
|
|
28
|
+
harness = await createHarness(SPEC);
|
|
29
|
+
harness.sendKeys("jjjjj");
|
|
30
|
+
await harness.wait();
|
|
31
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("Ctrl+D half page down", async () => {
|
|
35
|
+
harness = await createHarness(SPEC);
|
|
36
|
+
harness.sendKeys("\x04");
|
|
37
|
+
await harness.wait();
|
|
38
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("/ search highlights matches", async () => {
|
|
42
|
+
harness = await createHarness(SPEC);
|
|
43
|
+
harness.sendKeys("/token\n");
|
|
44
|
+
await harness.wait();
|
|
45
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("n jumps to next match", async () => {
|
|
49
|
+
harness = await createHarness(SPEC);
|
|
50
|
+
harness.sendKeys("/token\n");
|
|
51
|
+
await harness.wait();
|
|
52
|
+
harness.sendKeys("n");
|
|
53
|
+
await harness.wait();
|
|
54
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("? opens help", async () => {
|
|
58
|
+
harness = await createHarness(SPEC);
|
|
59
|
+
harness.sendKeys("?");
|
|
60
|
+
await harness.wait();
|
|
61
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("c opens comment input", async () => {
|
|
65
|
+
harness = await createHarness(SPEC);
|
|
66
|
+
harness.sendKeys("jjj");
|
|
67
|
+
await harness.wait();
|
|
68
|
+
harness.sendKeys("c");
|
|
69
|
+
await harness.wait();
|
|
70
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("Tab submits comment", async () => {
|
|
74
|
+
harness = await createHarness(SPEC);
|
|
75
|
+
harness.sendKeys("jjj");
|
|
76
|
+
await harness.wait();
|
|
77
|
+
harness.sendKeys("c");
|
|
78
|
+
await harness.wait();
|
|
79
|
+
harness.sendKeys("This is a test comment\t");
|
|
80
|
+
await harness.wait();
|
|
81
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Esc switches to normal mode", async () => {
|
|
85
|
+
harness = await createHarness(SPEC);
|
|
86
|
+
harness.sendKeys("jjj");
|
|
87
|
+
await harness.wait();
|
|
88
|
+
harness.sendKeys("c");
|
|
89
|
+
await harness.wait();
|
|
90
|
+
harness.sendKeys("\x1b");
|
|
91
|
+
await harness.wait();
|
|
92
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("thread gutter indicator", async () => {
|
|
96
|
+
harness = await createHarness(SPEC);
|
|
97
|
+
harness.sendKeys("jjj");
|
|
98
|
+
await harness.wait();
|
|
99
|
+
harness.sendKeys("c");
|
|
100
|
+
await harness.wait();
|
|
101
|
+
harness.sendKeys("Test thread\t");
|
|
102
|
+
await harness.wait();
|
|
103
|
+
harness.sendKeys("\x1b");
|
|
104
|
+
await harness.wait();
|
|
105
|
+
harness.sendKeys("q");
|
|
106
|
+
await harness.wait();
|
|
107
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("T opens thread list", async () => {
|
|
111
|
+
harness = await createHarness(SPEC);
|
|
112
|
+
harness.sendKeys("T");
|
|
113
|
+
await harness.wait();
|
|
114
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("r resolves thread", async () => {
|
|
118
|
+
harness = await createHarness(SPEC);
|
|
119
|
+
harness.sendKeys("jjj");
|
|
120
|
+
await harness.wait();
|
|
121
|
+
harness.sendKeys("c");
|
|
122
|
+
await harness.wait();
|
|
123
|
+
harness.sendKeys("Comment to resolve\t");
|
|
124
|
+
await harness.wait();
|
|
125
|
+
harness.sendKeys("\x1b");
|
|
126
|
+
await harness.wait();
|
|
127
|
+
harness.sendKeys("q");
|
|
128
|
+
await harness.wait();
|
|
129
|
+
harness.sendKeys("r");
|
|
130
|
+
await harness.wait();
|
|
131
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("dd y deletes thread", async () => {
|
|
135
|
+
harness = await createHarness(SPEC);
|
|
136
|
+
harness.sendKeys("jjj");
|
|
137
|
+
await harness.wait();
|
|
138
|
+
harness.sendKeys("c");
|
|
139
|
+
await harness.wait();
|
|
140
|
+
harness.sendKeys("Comment to delete\t");
|
|
141
|
+
await harness.wait();
|
|
142
|
+
harness.sendKeys("\x1b");
|
|
143
|
+
await harness.wait();
|
|
144
|
+
harness.sendKeys("q");
|
|
145
|
+
await harness.wait();
|
|
146
|
+
harness.sendKeys("dd");
|
|
147
|
+
await harness.wait(S);
|
|
148
|
+
harness.sendKeys("y");
|
|
149
|
+
await harness.wait();
|
|
150
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test(":w merges review", async () => {
|
|
154
|
+
harness = await createHarness(SPEC);
|
|
155
|
+
harness.sendKeys(":w\n");
|
|
156
|
+
await harness.wait();
|
|
157
|
+
expect(harness.capture()).toMatchSnapshot();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("bottom bar shows resolve on thread line", async () => {
|
|
161
|
+
harness = await createHarness(SPEC);
|
|
162
|
+
harness.sendKeys("jjj");
|
|
163
|
+
await harness.wait();
|
|
164
|
+
harness.sendKeys("c");
|
|
165
|
+
await harness.wait();
|
|
166
|
+
harness.sendKeys("hint test\t");
|
|
167
|
+
await harness.wait();
|
|
168
|
+
harness.sendKeys("\x1b");
|
|
169
|
+
await harness.wait();
|
|
170
|
+
harness.sendKeys("q");
|
|
171
|
+
await harness.wait();
|
|
172
|
+
expect(harness.contains("resolve")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("bottom bar hides resolve on non-thread line", async () => {
|
|
176
|
+
harness = await createHarness(SPEC);
|
|
177
|
+
const lines = harness.capture().split("\n");
|
|
178
|
+
const bottom = lines[lines.length - 1] || "";
|
|
179
|
+
expect(bottom).toContain("navigate");
|
|
180
|
+
expect(bottom).not.toContain("resolve");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { appendEvent, readEventsFromOffset } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset } from "../../src/protocol/live-events";
|
|
6
6
|
|
|
7
|
-
const CLI = resolve(import.meta.dir, "
|
|
7
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
8
8
|
|
|
9
9
|
interface SpawnResult {
|
|
10
10
|
exitCode: number;
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { appendEvent, readEventsFromOffset } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset } from "../../src/protocol/live-events";
|
|
6
6
|
|
|
7
|
-
const CLI = resolve(import.meta.dir, "
|
|
7
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
8
8
|
|
|
9
9
|
interface SpawnResult {
|
|
10
10
|
exitCode: number;
|
|
@@ -2,10 +2,10 @@ import { describe, it, expect, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { appendEvent } from "
|
|
6
|
-
import { writeReviewFile } from "
|
|
5
|
+
import { appendEvent } from "../../src/protocol/live-events";
|
|
6
|
+
import { writeReviewFile } from "../../src/protocol/write";
|
|
7
7
|
|
|
8
|
-
const CLI = resolve(import.meta.dir, "
|
|
8
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts");
|
|
9
9
|
|
|
10
10
|
interface SpawnResult {
|
|
11
11
|
exitCode: number;
|
|
@@ -2,11 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync, existsSync } from "fs"
|
|
3
3
|
import { join } from "path"
|
|
4
4
|
import { tmpdir } from "os"
|
|
5
|
-
import { appendEvent, readEventsFromOffset } from "
|
|
6
|
-
import { mergeJsonlIntoReview } from "
|
|
7
|
-
import { writeReviewFile } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset } from "../../src/protocol/live-events"
|
|
6
|
+
import { mergeJsonlIntoReview } from "../../src/protocol/live-merge"
|
|
7
|
+
import { writeReviewFile } from "../../src/protocol/write"
|
|
8
8
|
|
|
9
|
-
const CLI = join(import.meta.dir, "..", "bin", "revspec.ts")
|
|
9
|
+
const CLI = join(import.meta.dir, "..", "..", "bin", "revspec.ts")
|
|
10
10
|
|
|
11
11
|
describe("E2E: live review loop", () => {
|
|
12
12
|
let dir: string
|
|
@@ -2,11 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs"
|
|
3
3
|
import { join, resolve } from "path"
|
|
4
4
|
import { tmpdir } from "os"
|
|
5
|
-
import { appendEvent, readEventsFromOffset, replayEventsToThreads } from "
|
|
6
|
-
import { mergeJsonlIntoReview } from "
|
|
7
|
-
import { ReviewState } from "
|
|
5
|
+
import { appendEvent, readEventsFromOffset, replayEventsToThreads } from "../../src/protocol/live-events"
|
|
6
|
+
import { mergeJsonlIntoReview } from "../../src/protocol/live-merge"
|
|
7
|
+
import { ReviewState } from "../../src/state/review-state"
|
|
8
8
|
|
|
9
|
-
const CLI = resolve(import.meta.dir, "
|
|
9
|
+
const CLI = resolve(import.meta.dir, "../../bin/revspec.ts")
|
|
10
10
|
|
|
11
11
|
interface SpawnResult {
|
|
12
12
|
exitCode: number
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import { mergeJsonlIntoReview } from "
|
|
6
|
-
import { appendEvent } from "
|
|
7
|
-
import type { ReviewFile } from "
|
|
5
|
+
import { mergeJsonlIntoReview } from "../../../src/protocol/live-merge";
|
|
6
|
+
import { appendEvent } from "../../../src/protocol/live-events";
|
|
7
|
+
import type { ReviewFile } from "../../../src/protocol/types";
|
|
8
8
|
|
|
9
9
|
function tmpDir() {
|
|
10
10
|
return mkdtempSync(join(tmpdir(), "revspec-test-"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { mergeDraftIntoReview } from "
|
|
3
|
-
import type { ReviewFile, DraftFile, Thread } from "
|
|
2
|
+
import { mergeDraftIntoReview } from "../../../src/protocol/merge";
|
|
3
|
+
import type { ReviewFile, DraftFile, Thread } from "../../../src/protocol/types";
|
|
4
4
|
|
|
5
5
|
const baseReview: ReviewFile = {
|
|
6
6
|
file: "spec.md",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, afterEach } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { readReviewFile, readDraftFile } from "
|
|
5
|
-
import type { ReviewFile, DraftFile } from "
|
|
4
|
+
import { readReviewFile, readDraftFile } from "../../../src/protocol/read";
|
|
5
|
+
import type { ReviewFile, DraftFile } from "../../../src/protocol/types";
|
|
6
6
|
|
|
7
7
|
function tmpDir() {
|
|
8
8
|
return mkdtempSync("/tmp/revspec-test-");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync, readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { writeReviewFile, writeDraftFile } from "
|
|
5
|
-
import type { ReviewFile, DraftFile } from "
|
|
4
|
+
import { writeReviewFile, writeDraftFile } from "../../../src/protocol/write";
|
|
5
|
+
import type { ReviewFile, DraftFile } from "../../../src/protocol/types";
|
|
6
6
|
|
|
7
7
|
function tmpDir() {
|
|
8
8
|
return mkdtempSync("/tmp/revspec-test-");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach } from "bun:test";
|
|
2
|
-
import { ReviewState } from "
|
|
3
|
-
import type { Thread } from "
|
|
2
|
+
import { ReviewState } from "../../../src/state/review-state";
|
|
3
|
+
import type { Thread } from "../../../src/protocol/types";
|
|
4
4
|
|
|
5
5
|
const SPEC = ["line one", "line two", "line three", "line four", "line five"];
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { buildPagerContent } from "
|
|
3
|
-
import { ReviewState } from "
|
|
4
|
-
import type { Thread } from "
|
|
2
|
+
import { buildPagerContent } from "../../../src/tui/pager";
|
|
3
|
+
import { ReviewState } from "../../../src/state/review-state";
|
|
4
|
+
import type { Thread } from "../../../src/protocol/types";
|
|
5
5
|
|
|
6
6
|
const SPEC = ["# Title", "Some text", "More text", "Final line"];
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { createKeybindRegistry } from "
|
|
2
|
+
import { createKeybindRegistry } from "../../../../src/tui/ui/keybinds";
|
|
3
3
|
|
|
4
4
|
function makeKey(name: string, opts: { ctrl?: boolean; shift?: boolean; sequence?: string } = {}): any {
|
|
5
5
|
return { name, ctrl: opts.ctrl ?? false, shift: opts.shift ?? false, sequence: opts.sequence ?? name };
|
|
File without changes
|