revspec 0.2.2 → 0.4.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 +10 -2
- package/README.md +86 -29
- package/bin/revspec.ts +2 -1
- package/docs/superpowers/plans/2026-03-15-ui-refactor.md +1025 -0
- package/package.json +1 -1
- package/scripts/install-skill.sh +20 -0
- package/scripts/release.sh +5 -6
- package/skills/revspec/SKILL.md +137 -0
- package/src/protocol/live-events.ts +3 -2
- package/src/tui/app.ts +198 -310
- package/src/tui/comment-input.ts +145 -144
- package/src/tui/confirm.ts +29 -43
- package/src/tui/help.ts +33 -57
- package/src/tui/pager.ts +162 -82
- package/src/tui/search.ts +6 -6
- package/src/tui/status-bar.ts +77 -34
- package/src/tui/thread-list.ts +28 -54
- package/src/tui/ui/dialog.ts +106 -0
- package/src/tui/ui/hint-bar.ts +20 -0
- package/src/tui/ui/keybinds.ts +104 -0
- package/src/tui/ui/markdown.ts +251 -0
- package/src/tui/ui/theme.ts +49 -0
- package/test/tui/ui/keybinds.test.ts +71 -0
- package/src/tui/theme.ts +0 -34
package/src/tui/app.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { mergeJsonlIntoReview } from "../protocol/live-merge";
|
|
|
13
13
|
import type { Thread } from "../protocol/types";
|
|
14
14
|
import { ReviewState } from "../state/review-state";
|
|
15
15
|
import { createLiveWatcher, type LiveWatcher } from "./live-watcher";
|
|
16
|
-
import {
|
|
16
|
+
import { buildPagerNodes, createPager, countExtraVisualLines, type PagerComponents } from "./pager";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
buildTopBar,
|
|
19
|
+
buildBottomBar,
|
|
20
20
|
createTopBar,
|
|
21
21
|
createBottomBar,
|
|
22
22
|
type TopBarComponents,
|
|
@@ -28,11 +28,13 @@ import { createSearch } from "./search";
|
|
|
28
28
|
import { createThreadList } from "./thread-list";
|
|
29
29
|
import { createConfirm } from "./confirm";
|
|
30
30
|
import { createHelp } from "./help";
|
|
31
|
+
import { createKeybindRegistry, type KeyBinding } from "./ui/keybinds";
|
|
31
32
|
|
|
32
33
|
export async function runTui(
|
|
33
34
|
specFile: string,
|
|
34
35
|
reviewPath: string,
|
|
35
|
-
draftPath: string
|
|
36
|
+
draftPath: string,
|
|
37
|
+
version?: string
|
|
36
38
|
): Promise<void> {
|
|
37
39
|
// 1. Read spec file into lines
|
|
38
40
|
const specContent = readFileSync(specFile, "utf8");
|
|
@@ -99,21 +101,20 @@ export async function runTui(
|
|
|
99
101
|
useMouse: false,
|
|
100
102
|
});
|
|
101
103
|
|
|
102
|
-
// 6. Build layout:
|
|
104
|
+
// 6. Build layout (opencode pattern): flex column, scrollbox fills middle
|
|
103
105
|
const rootBox = new BoxRenderable(renderer, {
|
|
104
|
-
|
|
105
|
-
height: "100%",
|
|
106
|
+
flexGrow: 1,
|
|
106
107
|
flexDirection: "column",
|
|
108
|
+
width: "100%",
|
|
107
109
|
});
|
|
108
110
|
|
|
109
111
|
const topBar: TopBarComponents = createTopBar(renderer);
|
|
110
112
|
const pager: PagerComponents = createPager(renderer);
|
|
111
113
|
const bottomBar: BottomBarComponents = createBottomBar(renderer);
|
|
112
114
|
|
|
113
|
-
rootBox.add(topBar.
|
|
115
|
+
rootBox.add(topBar.box);
|
|
114
116
|
rootBox.add(pager.scrollBox);
|
|
115
|
-
rootBox.add(bottomBar.
|
|
116
|
-
|
|
117
|
+
rootBox.add(bottomBar.box);
|
|
117
118
|
renderer.root.add(rootBox);
|
|
118
119
|
|
|
119
120
|
// 7. Initial render
|
|
@@ -126,13 +127,9 @@ export async function runTui(
|
|
|
126
127
|
}
|
|
127
128
|
} catch {}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
pager.markdownNode.content = state.specLines.join("\n");
|
|
133
|
-
}
|
|
134
|
-
topBar.bar.content = buildTopBarText(specFile, state, state.unreadCount(), specMtimeChanged, pager.mode);
|
|
135
|
-
bottomBar.bar.content = buildBottomBarText(commandBuffer);
|
|
130
|
+
buildPagerNodes(pager.lineNode, state, searchQuery, state.unreadThreadIds);
|
|
131
|
+
buildTopBar(topBar, specFile, state, state.unreadCount(), specMtimeChanged);
|
|
132
|
+
buildBottomBar(bottomBar, commandBuffer);
|
|
136
133
|
renderer.requestRender();
|
|
137
134
|
}
|
|
138
135
|
|
|
@@ -142,16 +139,6 @@ export async function runTui(
|
|
|
142
139
|
// Command mode state
|
|
143
140
|
let commandBuffer: string | null = null;
|
|
144
141
|
|
|
145
|
-
// Bracket-pending state for ]t / [t / ]r / [r navigation
|
|
146
|
-
let bracketPending: "]" | "[" | null = null;
|
|
147
|
-
let bracketPendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
148
|
-
|
|
149
|
-
// Delete-pending state: first `d` sets timer, second `d` within 500ms executes
|
|
150
|
-
let deletePendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
151
|
-
|
|
152
|
-
// g-pending state: first `g` sets timer, second `g` within 500ms goes to top
|
|
153
|
-
let gPendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
154
|
-
|
|
155
142
|
// Overlay state — when an overlay is active, normal keybindings are blocked.
|
|
156
143
|
// The overlay's own key handlers manage its lifecycle.
|
|
157
144
|
type ActiveOverlay = {
|
|
@@ -203,15 +190,16 @@ export async function runTui(
|
|
|
203
190
|
// Signal to watch process that session has ended
|
|
204
191
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
205
192
|
liveWatcher.stop();
|
|
193
|
+
keybinds.destroy();
|
|
206
194
|
renderer.destroy();
|
|
207
195
|
resolve();
|
|
208
196
|
}
|
|
209
197
|
|
|
210
198
|
// Helper: scroll pager to ensure cursor line is visible
|
|
211
199
|
function ensureCursorVisible(): void {
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
const cursorRow = state.cursorLine - 1;
|
|
200
|
+
// Map spec line to visual row, accounting for table border extra lines
|
|
201
|
+
const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1);
|
|
202
|
+
const cursorRow = state.cursorLine - 1 + extra;
|
|
215
203
|
const viewportHeight = Math.max(1, renderer.height - 2); // minus top + bottom bar
|
|
216
204
|
|
|
217
205
|
const currentScroll = pager.scrollBox.scrollTop;
|
|
@@ -233,7 +221,7 @@ export async function runTui(
|
|
|
233
221
|
if (cmd === "w") {
|
|
234
222
|
// Merge JSONL -> JSON, stay open
|
|
235
223
|
doMerge();
|
|
236
|
-
bottomBar.
|
|
224
|
+
bottomBar.text.content = " \u2714 Merged to review JSON";
|
|
237
225
|
renderer.requestRender();
|
|
238
226
|
setTimeout(() => { refreshPager(); }, 1200);
|
|
239
227
|
return "stay";
|
|
@@ -246,19 +234,21 @@ export async function runTui(
|
|
|
246
234
|
if (cmd === "q") {
|
|
247
235
|
// Exit only if merged (no pending changes)
|
|
248
236
|
if (hasPendingChanges()) {
|
|
249
|
-
bottomBar.
|
|
237
|
+
bottomBar.text.content = " Unmerged changes. Use :w to save or :q! to discard";
|
|
250
238
|
renderer.requestRender();
|
|
251
239
|
setTimeout(() => { refreshPager(); }, 2000);
|
|
252
240
|
return "stay";
|
|
253
241
|
}
|
|
254
242
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
255
243
|
liveWatcher.stop();
|
|
244
|
+
keybinds.destroy();
|
|
256
245
|
return "exit";
|
|
257
246
|
}
|
|
258
247
|
if (cmd === "q!") {
|
|
259
248
|
// Exit without merging
|
|
260
249
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
261
250
|
liveWatcher.stop();
|
|
251
|
+
keybinds.destroy();
|
|
262
252
|
return "exit";
|
|
263
253
|
}
|
|
264
254
|
return "stay"; // unknown command, ignore
|
|
@@ -354,6 +344,7 @@ export async function runTui(
|
|
|
354
344
|
function showHelpOverlay(): void {
|
|
355
345
|
const overlay = createHelp({
|
|
356
346
|
renderer,
|
|
347
|
+
version: version ?? "?",
|
|
357
348
|
onClose: () => {
|
|
358
349
|
dismissOverlay();
|
|
359
350
|
},
|
|
@@ -379,6 +370,35 @@ export async function runTui(
|
|
|
379
370
|
return null;
|
|
380
371
|
}
|
|
381
372
|
|
|
373
|
+
// --- Keybind registry ---
|
|
374
|
+
|
|
375
|
+
const bindings: KeyBinding[] = [
|
|
376
|
+
{ key: "j", action: "cursor-down" },
|
|
377
|
+
{ key: "down", action: "cursor-down" },
|
|
378
|
+
{ key: "k", action: "cursor-up" },
|
|
379
|
+
{ key: "up", action: "cursor-up" },
|
|
380
|
+
{ key: "C-d", action: "half-page-down" },
|
|
381
|
+
{ key: "C-u", action: "half-page-up" },
|
|
382
|
+
{ key: "G", action: "goto-bottom" },
|
|
383
|
+
{ key: "gg", action: "goto-top" },
|
|
384
|
+
{ key: "n", action: "search-next" },
|
|
385
|
+
{ key: "N", action: "search-prev" },
|
|
386
|
+
{ key: "c", action: "comment" },
|
|
387
|
+
{ key: "l", action: "thread-list" },
|
|
388
|
+
{ key: "r", action: "resolve" },
|
|
389
|
+
{ key: "R", action: "resolve-all" },
|
|
390
|
+
{ key: "dd", action: "delete-draft" },
|
|
391
|
+
{ key: "a", action: "approve" },
|
|
392
|
+
{ key: "]t", action: "next-thread" },
|
|
393
|
+
{ key: "[t", action: "prev-thread" },
|
|
394
|
+
{ key: "]r", action: "next-unread" },
|
|
395
|
+
{ key: "[r", action: "prev-unread" },
|
|
396
|
+
{ key: "?", action: "help" },
|
|
397
|
+
{ key: "/", action: "search" },
|
|
398
|
+
{ key: ":", action: "command-mode" },
|
|
399
|
+
];
|
|
400
|
+
const keybinds = createKeybindRegistry(bindings);
|
|
401
|
+
|
|
382
402
|
refreshPager();
|
|
383
403
|
renderer.start();
|
|
384
404
|
|
|
@@ -454,230 +474,140 @@ export async function runTui(
|
|
|
454
474
|
}
|
|
455
475
|
|
|
456
476
|
// Normal mode keybindings
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
state.cursorLine++;
|
|
466
|
-
ensureCursorVisible();
|
|
467
|
-
refreshPager();
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
break;
|
|
477
|
+
const action = keybinds.match(key);
|
|
478
|
+
|
|
479
|
+
// Show pending sequence hint
|
|
480
|
+
if (!action) {
|
|
481
|
+
const p = keybinds.pending();
|
|
482
|
+
if (p) {
|
|
483
|
+
bottomBar.text.content = ` ${p}`;
|
|
484
|
+
renderer.requestRender();
|
|
471
485
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
refreshPager();
|
|
482
|
-
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
switch (action) {
|
|
490
|
+
case "cursor-down":
|
|
491
|
+
if (state.cursorLine < state.lineCount) {
|
|
492
|
+
state.cursorLine++;
|
|
493
|
+
ensureCursorVisible();
|
|
494
|
+
refreshPager();
|
|
483
495
|
}
|
|
484
496
|
break;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (deletePendingTimer) { clearTimeout(deletePendingTimer); deletePendingTimer = null; }
|
|
490
|
-
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
491
|
-
if (pager.mode === "markdown") {
|
|
492
|
-
pager.scrollBox.scrollBy(half);
|
|
493
|
-
renderer.requestRender();
|
|
494
|
-
} else {
|
|
495
|
-
state.cursorLine = Math.min(state.cursorLine + half, state.lineCount);
|
|
496
|
-
ensureCursorVisible();
|
|
497
|
-
refreshPager();
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
// d without ctrl — delete draft comment (dd = double-tap within 500ms)
|
|
501
|
-
ensureLineMode(pager);
|
|
497
|
+
case "cursor-up":
|
|
498
|
+
if (state.cursorLine > 1) {
|
|
499
|
+
state.cursorLine--;
|
|
500
|
+
ensureCursorVisible();
|
|
502
501
|
refreshPager();
|
|
503
|
-
const thread = state.threadAtLine(state.cursorLine);
|
|
504
|
-
if (!thread) break;
|
|
505
|
-
if (deletePendingTimer) {
|
|
506
|
-
// Second d within 500ms — execute delete
|
|
507
|
-
clearTimeout(deletePendingTimer);
|
|
508
|
-
deletePendingTimer = null;
|
|
509
|
-
const hadReviewerMsg = thread.messages.some((m) => m.author === "reviewer");
|
|
510
|
-
if (hadReviewerMsg) {
|
|
511
|
-
state.deleteLastDraftMessage(thread.id);
|
|
512
|
-
appendEvent(jsonlPath, { type: "delete", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
513
|
-
refreshPager();
|
|
514
|
-
bottomBar.bar.content = " \u2714 Deleted draft comment";
|
|
515
|
-
renderer.requestRender();
|
|
516
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
517
|
-
} else {
|
|
518
|
-
bottomBar.bar.content = " No reviewer message to delete";
|
|
519
|
-
renderer.requestRender();
|
|
520
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
521
|
-
}
|
|
522
|
-
} else {
|
|
523
|
-
// First d — show hint and start timer
|
|
524
|
-
bottomBar.bar.content = " Press d again to delete";
|
|
525
|
-
renderer.requestRender();
|
|
526
|
-
deletePendingTimer = setTimeout(() => {
|
|
527
|
-
deletePendingTimer = null;
|
|
528
|
-
refreshPager();
|
|
529
|
-
}, 500);
|
|
530
|
-
}
|
|
531
502
|
}
|
|
532
503
|
break;
|
|
504
|
+
case "half-page-down": {
|
|
505
|
+
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
506
|
+
state.cursorLine = Math.min(state.cursorLine + half, state.lineCount);
|
|
507
|
+
ensureCursorVisible();
|
|
508
|
+
refreshPager();
|
|
509
|
+
break;
|
|
533
510
|
}
|
|
534
|
-
case "
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
pager.scrollBox.scrollBy(-half);
|
|
540
|
-
renderer.requestRender();
|
|
541
|
-
} else {
|
|
542
|
-
state.cursorLine = Math.max(state.cursorLine - half, 1);
|
|
543
|
-
ensureCursorVisible();
|
|
544
|
-
refreshPager();
|
|
545
|
-
}
|
|
546
|
-
}
|
|
511
|
+
case "half-page-up": {
|
|
512
|
+
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
513
|
+
state.cursorLine = Math.max(state.cursorLine - half, 1);
|
|
514
|
+
ensureCursorVisible();
|
|
515
|
+
refreshPager();
|
|
547
516
|
break;
|
|
548
517
|
}
|
|
549
|
-
case "
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
518
|
+
case "goto-bottom":
|
|
519
|
+
state.cursorLine = state.lineCount;
|
|
520
|
+
ensureCursorVisible();
|
|
521
|
+
refreshPager();
|
|
522
|
+
break;
|
|
523
|
+
case "goto-top":
|
|
524
|
+
state.cursorLine = 1;
|
|
525
|
+
ensureCursorVisible();
|
|
526
|
+
refreshPager();
|
|
527
|
+
break;
|
|
528
|
+
case "search-next":
|
|
529
|
+
if (searchQuery) {
|
|
530
|
+
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, 1);
|
|
531
|
+
if (match !== null) {
|
|
532
|
+
state.cursorLine = match;
|
|
533
|
+
ensureCursorVisible();
|
|
561
534
|
}
|
|
562
535
|
} else {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (match !== null) {
|
|
567
|
-
state.cursorLine = match;
|
|
568
|
-
ensureCursorVisible();
|
|
569
|
-
}
|
|
570
|
-
} else {
|
|
571
|
-
bottomBar.bar.content = " No active search \u2014 use / to search";
|
|
572
|
-
renderer.requestRender();
|
|
573
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
574
|
-
}
|
|
536
|
+
bottomBar.text.content = " No active search \u2014 use / to search";
|
|
537
|
+
renderer.requestRender();
|
|
538
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
575
539
|
}
|
|
576
540
|
refreshPager();
|
|
577
541
|
break;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
state.cursorLine = Math.max(1, pager.scrollBox.scrollTop + 1);
|
|
586
|
-
refreshPager();
|
|
587
|
-
ensureCursorVisible();
|
|
542
|
+
case "search-prev":
|
|
543
|
+
if (searchQuery) {
|
|
544
|
+
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, -1);
|
|
545
|
+
if (match !== null) {
|
|
546
|
+
state.cursorLine = match;
|
|
547
|
+
ensureCursorVisible();
|
|
548
|
+
}
|
|
588
549
|
} else {
|
|
589
|
-
|
|
590
|
-
refreshPager();
|
|
591
|
-
pager.scrollBox.scrollTo(state.cursorLine - 1);
|
|
550
|
+
bottomBar.text.content = " No active search \u2014 use / to search";
|
|
592
551
|
renderer.requestRender();
|
|
552
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
593
553
|
}
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
case "c": {
|
|
597
|
-
// Comment: new or reply — auto-switch to line mode
|
|
598
|
-
ensureLineMode(pager);
|
|
599
554
|
refreshPager();
|
|
555
|
+
break;
|
|
556
|
+
case "comment":
|
|
600
557
|
showCommentInput();
|
|
601
558
|
break;
|
|
602
|
-
|
|
603
|
-
case "l": {
|
|
604
|
-
// Thread list
|
|
605
|
-
ensureLineMode(pager);
|
|
606
|
-
refreshPager();
|
|
559
|
+
case "thread-list":
|
|
607
560
|
showThreadListOverlay();
|
|
608
561
|
break;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (thread) {
|
|
617
|
-
const wasResolved = thread.status === "resolved";
|
|
618
|
-
state.resolveThread(thread.id);
|
|
619
|
-
state.markRead(thread.id);
|
|
620
|
-
appendEvent(jsonlPath, { type: wasResolved ? "unresolve" : "resolve", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
621
|
-
refreshPager();
|
|
622
|
-
const msg = wasResolved
|
|
623
|
-
? ` \u21a9 Reopened thread #${thread.id}`
|
|
624
|
-
: ` \u2714 Resolved thread #${thread.id}`;
|
|
625
|
-
bottomBar.bar.content = msg;
|
|
626
|
-
renderer.requestRender();
|
|
627
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
628
|
-
}
|
|
629
|
-
} else {
|
|
630
|
-
// Shift+R = resolve all pending
|
|
631
|
-
const { pending } = state.activeThreadCount();
|
|
632
|
-
const pendingThreads = state.threads.filter(t => t.status === "pending");
|
|
633
|
-
state.resolveAllPending();
|
|
634
|
-
for (const t of pendingThreads) {
|
|
635
|
-
appendEvent(jsonlPath, { type: "resolve", threadId: t.id, author: "reviewer", ts: Date.now() });
|
|
636
|
-
}
|
|
562
|
+
case "resolve": {
|
|
563
|
+
const thread = state.threadAtLine(state.cursorLine);
|
|
564
|
+
if (thread) {
|
|
565
|
+
const wasResolved = thread.status === "resolved";
|
|
566
|
+
state.resolveThread(thread.id);
|
|
567
|
+
state.markRead(thread.id);
|
|
568
|
+
appendEvent(jsonlPath, { type: wasResolved ? "unresolve" : "resolve", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
637
569
|
refreshPager();
|
|
638
|
-
|
|
570
|
+
const msg = wasResolved
|
|
571
|
+
? ` \u21a9 Reopened thread #${thread.id}`
|
|
572
|
+
: ` \u2714 Resolved thread #${thread.id}`;
|
|
573
|
+
bottomBar.text.content = msg;
|
|
639
574
|
renderer.requestRender();
|
|
640
575
|
setTimeout(() => { refreshPager(); }, 1500);
|
|
641
576
|
}
|
|
642
577
|
break;
|
|
643
578
|
}
|
|
644
|
-
case "
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
579
|
+
case "resolve-all": {
|
|
580
|
+
const { pending } = state.activeThreadCount();
|
|
581
|
+
const pendingThreads = state.threads.filter(t => t.status === "pending");
|
|
582
|
+
state.resolveAllPending();
|
|
583
|
+
for (const t of pendingThreads) {
|
|
584
|
+
appendEvent(jsonlPath, { type: "resolve", threadId: t.id, author: "reviewer", ts: Date.now() });
|
|
585
|
+
}
|
|
586
|
+
refreshPager();
|
|
587
|
+
bottomBar.text.content = ` \u2714 Resolved ${pending} pending thread(s)`;
|
|
588
|
+
renderer.requestRender();
|
|
589
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case "delete-draft": {
|
|
593
|
+
const thread = state.threadAtLine(state.cursorLine);
|
|
594
|
+
if (!thread) break;
|
|
595
|
+
const hadReviewerMsg = thread.messages.some((m) => m.author === "reviewer");
|
|
596
|
+
if (hadReviewerMsg) {
|
|
597
|
+
state.deleteLastDraftMessage(thread.id);
|
|
598
|
+
appendEvent(jsonlPath, { type: "delete", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
599
|
+
refreshPager();
|
|
600
|
+
bottomBar.text.content = " \u2714 Deleted draft comment";
|
|
601
|
+
renderer.requestRender();
|
|
602
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
655
603
|
} else {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
clearTimeout(gPendingTimer);
|
|
660
|
-
gPendingTimer = null;
|
|
661
|
-
if (pager.mode === "markdown") {
|
|
662
|
-
pager.scrollBox.scrollTo(0);
|
|
663
|
-
renderer.requestRender();
|
|
664
|
-
} else {
|
|
665
|
-
state.cursorLine = 1;
|
|
666
|
-
ensureCursorVisible();
|
|
667
|
-
refreshPager();
|
|
668
|
-
}
|
|
669
|
-
} else {
|
|
670
|
-
gPendingTimer = setTimeout(() => {
|
|
671
|
-
gPendingTimer = null;
|
|
672
|
-
}, 500);
|
|
673
|
-
}
|
|
604
|
+
bottomBar.text.content = " No reviewer message to delete";
|
|
605
|
+
renderer.requestRender();
|
|
606
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
674
607
|
}
|
|
675
608
|
break;
|
|
676
609
|
}
|
|
677
|
-
case "
|
|
678
|
-
// Approve
|
|
679
|
-
ensureLineMode(pager);
|
|
680
|
-
refreshPager();
|
|
610
|
+
case "approve":
|
|
681
611
|
if (state.canApprove()) {
|
|
682
612
|
const confirmOverlay = createConfirm({
|
|
683
613
|
renderer,
|
|
@@ -692,105 +622,63 @@ export async function runTui(
|
|
|
692
622
|
},
|
|
693
623
|
});
|
|
694
624
|
showOverlay(confirmOverlay);
|
|
695
|
-
return;
|
|
696
625
|
} else {
|
|
697
|
-
// Show why approval is blocked
|
|
698
626
|
const { open, pending } = state.activeThreadCount();
|
|
699
627
|
const total = open + pending;
|
|
700
|
-
const msg =
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
bottomBar.bar.content = ` \u26a0 ${msg}`;
|
|
628
|
+
const msg = total === 0
|
|
629
|
+
? "No threads to approve"
|
|
630
|
+
: `${total} thread${total !== 1 ? "s" : ""} still open/pending`;
|
|
631
|
+
bottomBar.text.content = ` \u26a0 ${msg}`;
|
|
705
632
|
renderer.requestRender();
|
|
706
|
-
setTimeout(() => {
|
|
707
|
-
refreshPager();
|
|
708
|
-
}, 2000);
|
|
633
|
+
setTimeout(() => { refreshPager(); }, 2000);
|
|
709
634
|
}
|
|
710
635
|
break;
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (bracketPendingTimer) { clearTimeout(bracketPendingTimer); bracketPendingTimer = null; }
|
|
718
|
-
if (key.name === "t" || key.sequence === "t") {
|
|
719
|
-
if (pending === "]") {
|
|
720
|
-
const next = state.nextActiveThread();
|
|
721
|
-
if (next !== null) {
|
|
722
|
-
state.cursorLine = next;
|
|
723
|
-
ensureCursorVisible();
|
|
724
|
-
refreshPager();
|
|
725
|
-
}
|
|
726
|
-
} else {
|
|
727
|
-
const prev = state.prevActiveThread();
|
|
728
|
-
if (prev !== null) {
|
|
729
|
-
state.cursorLine = prev;
|
|
730
|
-
ensureCursorVisible();
|
|
731
|
-
refreshPager();
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
} else if (key.name === "r" || key.sequence === "r") {
|
|
735
|
-
if (pending === "]") {
|
|
736
|
-
const nextLine = state.nextUnreadThread();
|
|
737
|
-
if (nextLine !== null) {
|
|
738
|
-
state.cursorLine = nextLine;
|
|
739
|
-
ensureCursorVisible();
|
|
740
|
-
refreshPager();
|
|
741
|
-
}
|
|
742
|
-
} else {
|
|
743
|
-
const prevLine = state.prevUnreadThread();
|
|
744
|
-
if (prevLine !== null) {
|
|
745
|
-
state.cursorLine = prevLine;
|
|
746
|
-
ensureCursorVisible();
|
|
747
|
-
refreshPager();
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
refreshPager(); // clear the bracket hint
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
// Check for "]" or "[" to start bracket sequence
|
|
755
|
-
if (key.sequence === "]") {
|
|
756
|
-
bracketPending = "]";
|
|
757
|
-
bottomBar.bar.content = " ]...";
|
|
758
|
-
renderer.requestRender();
|
|
759
|
-
bracketPendingTimer = setTimeout(() => {
|
|
760
|
-
bracketPending = null;
|
|
761
|
-
bracketPendingTimer = null;
|
|
762
|
-
refreshPager();
|
|
763
|
-
}, 500);
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
if (key.sequence === "[") {
|
|
767
|
-
bracketPending = "[";
|
|
768
|
-
bottomBar.bar.content = " [...";
|
|
769
|
-
renderer.requestRender();
|
|
770
|
-
bracketPendingTimer = setTimeout(() => {
|
|
771
|
-
bracketPending = null;
|
|
772
|
-
bracketPendingTimer = null;
|
|
773
|
-
refreshPager();
|
|
774
|
-
}, 500);
|
|
775
|
-
break;
|
|
636
|
+
case "next-thread": {
|
|
637
|
+
const next = state.nextActiveThread();
|
|
638
|
+
if (next !== null) {
|
|
639
|
+
state.cursorLine = next;
|
|
640
|
+
ensureCursorVisible();
|
|
641
|
+
refreshPager();
|
|
776
642
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case "prev-thread": {
|
|
646
|
+
const prev = state.prevActiveThread();
|
|
647
|
+
if (prev !== null) {
|
|
648
|
+
state.cursorLine = prev;
|
|
649
|
+
ensureCursorVisible();
|
|
650
|
+
refreshPager();
|
|
781
651
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case "next-unread": {
|
|
655
|
+
const nextLine = state.nextUnreadThread();
|
|
656
|
+
if (nextLine !== null) {
|
|
657
|
+
state.cursorLine = nextLine;
|
|
658
|
+
ensureCursorVisible();
|
|
659
|
+
refreshPager();
|
|
786
660
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
case "prev-unread": {
|
|
664
|
+
const prevLine = state.prevUnreadThread();
|
|
665
|
+
if (prevLine !== null) {
|
|
666
|
+
state.cursorLine = prevLine;
|
|
667
|
+
ensureCursorVisible();
|
|
790
668
|
refreshPager();
|
|
791
669
|
}
|
|
792
670
|
break;
|
|
793
671
|
}
|
|
672
|
+
case "help":
|
|
673
|
+
showHelpOverlay();
|
|
674
|
+
break;
|
|
675
|
+
case "search":
|
|
676
|
+
showSearchOverlay();
|
|
677
|
+
break;
|
|
678
|
+
case "command-mode":
|
|
679
|
+
commandBuffer = "";
|
|
680
|
+
refreshPager();
|
|
681
|
+
break;
|
|
794
682
|
}
|
|
795
683
|
});
|
|
796
684
|
});
|