revspec 0.3.0 → 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/docs/superpowers/plans/2026-03-15-ui-refactor.md +1025 -0
- package/package.json +1 -1
- package/src/tui/app.ts +173 -225
- package/src/tui/comment-input.ts +145 -144
- package/src/tui/confirm.ts +29 -43
- package/src/tui/help.ts +28 -52
- package/src/tui/pager.ts +24 -257
- package/src/tui/search.ts +6 -6
- package/src/tui/status-bar.ts +11 -20
- 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/package.json
CHANGED
package/src/tui/app.ts
CHANGED
|
@@ -28,6 +28,7 @@ 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,
|
|
@@ -138,16 +139,6 @@ export async function runTui(
|
|
|
138
139
|
// Command mode state
|
|
139
140
|
let commandBuffer: string | null = null;
|
|
140
141
|
|
|
141
|
-
// Bracket-pending state for ]t / [t / ]r / [r navigation
|
|
142
|
-
let bracketPending: "]" | "[" | null = null;
|
|
143
|
-
let bracketPendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
144
|
-
|
|
145
|
-
// Delete-pending state: first `d` sets timer, second `d` within 500ms executes
|
|
146
|
-
let deletePendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
147
|
-
|
|
148
|
-
// g-pending state: first `g` sets timer, second `g` within 500ms goes to top
|
|
149
|
-
let gPendingTimer: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
-
|
|
151
142
|
// Overlay state — when an overlay is active, normal keybindings are blocked.
|
|
152
143
|
// The overlay's own key handlers manage its lifecycle.
|
|
153
144
|
type ActiveOverlay = {
|
|
@@ -199,6 +190,7 @@ export async function runTui(
|
|
|
199
190
|
// Signal to watch process that session has ended
|
|
200
191
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
201
192
|
liveWatcher.stop();
|
|
193
|
+
keybinds.destroy();
|
|
202
194
|
renderer.destroy();
|
|
203
195
|
resolve();
|
|
204
196
|
}
|
|
@@ -249,12 +241,14 @@ export async function runTui(
|
|
|
249
241
|
}
|
|
250
242
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
251
243
|
liveWatcher.stop();
|
|
244
|
+
keybinds.destroy();
|
|
252
245
|
return "exit";
|
|
253
246
|
}
|
|
254
247
|
if (cmd === "q!") {
|
|
255
248
|
// Exit without merging
|
|
256
249
|
appendEvent(jsonlPath, { type: "session-end", author: "reviewer", ts: Date.now() });
|
|
257
250
|
liveWatcher.stop();
|
|
251
|
+
keybinds.destroy();
|
|
258
252
|
return "exit";
|
|
259
253
|
}
|
|
260
254
|
return "stay"; // unknown command, ignore
|
|
@@ -376,6 +370,35 @@ export async function runTui(
|
|
|
376
370
|
return null;
|
|
377
371
|
}
|
|
378
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
|
+
|
|
379
402
|
refreshPager();
|
|
380
403
|
renderer.start();
|
|
381
404
|
|
|
@@ -451,173 +474,140 @@ export async function runTui(
|
|
|
451
474
|
}
|
|
452
475
|
|
|
453
476
|
// Normal mode keybindings
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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();
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
switch (action) {
|
|
490
|
+
case "cursor-down":
|
|
457
491
|
if (state.cursorLine < state.lineCount) {
|
|
458
492
|
state.cursorLine++;
|
|
459
493
|
ensureCursorVisible();
|
|
460
494
|
refreshPager();
|
|
461
495
|
}
|
|
462
496
|
break;
|
|
463
|
-
|
|
464
|
-
case "k":
|
|
465
|
-
case "up": {
|
|
497
|
+
case "cursor-up":
|
|
466
498
|
if (state.cursorLine > 1) {
|
|
467
499
|
state.cursorLine--;
|
|
468
500
|
ensureCursorVisible();
|
|
469
501
|
refreshPager();
|
|
470
502
|
}
|
|
471
503
|
break;
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const half = Math.max(1, Math.floor(pageSize() / 2));
|
|
478
|
-
state.cursorLine = Math.min(state.cursorLine + half, state.lineCount);
|
|
479
|
-
ensureCursorVisible();
|
|
480
|
-
refreshPager();
|
|
481
|
-
} else {
|
|
482
|
-
// d without ctrl — delete draft comment (dd = double-tap within 500ms)
|
|
483
|
-
refreshPager();
|
|
484
|
-
const thread = state.threadAtLine(state.cursorLine);
|
|
485
|
-
if (!thread) break;
|
|
486
|
-
if (deletePendingTimer) {
|
|
487
|
-
// Second d within 500ms — execute delete
|
|
488
|
-
clearTimeout(deletePendingTimer);
|
|
489
|
-
deletePendingTimer = null;
|
|
490
|
-
const hadReviewerMsg = thread.messages.some((m) => m.author === "reviewer");
|
|
491
|
-
if (hadReviewerMsg) {
|
|
492
|
-
state.deleteLastDraftMessage(thread.id);
|
|
493
|
-
appendEvent(jsonlPath, { type: "delete", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
494
|
-
refreshPager();
|
|
495
|
-
bottomBar.text.content = " \u2714 Deleted draft comment";
|
|
496
|
-
renderer.requestRender();
|
|
497
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
498
|
-
} else {
|
|
499
|
-
bottomBar.text.content = " No reviewer message to delete";
|
|
500
|
-
renderer.requestRender();
|
|
501
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
502
|
-
}
|
|
503
|
-
} else {
|
|
504
|
-
// First d — show hint and start timer
|
|
505
|
-
bottomBar.text.content = " Press d again to delete";
|
|
506
|
-
renderer.requestRender();
|
|
507
|
-
deletePendingTimer = setTimeout(() => {
|
|
508
|
-
deletePendingTimer = null;
|
|
509
|
-
refreshPager();
|
|
510
|
-
}, 500);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
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();
|
|
513
509
|
break;
|
|
514
510
|
}
|
|
515
|
-
case "
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
ensureCursorVisible();
|
|
521
|
-
refreshPager();
|
|
522
|
-
}
|
|
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();
|
|
523
516
|
break;
|
|
524
517
|
}
|
|
525
|
-
case "
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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();
|
|
537
534
|
}
|
|
538
535
|
} else {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
536
|
+
bottomBar.text.content = " No active search \u2014 use / to search";
|
|
537
|
+
renderer.requestRender();
|
|
538
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
539
|
+
}
|
|
540
|
+
refreshPager();
|
|
541
|
+
break;
|
|
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();
|
|
550
548
|
}
|
|
549
|
+
} else {
|
|
550
|
+
bottomBar.text.content = " No active search \u2014 use / to search";
|
|
551
|
+
renderer.requestRender();
|
|
552
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
551
553
|
}
|
|
552
554
|
refreshPager();
|
|
553
555
|
break;
|
|
554
|
-
|
|
555
|
-
case "c": {
|
|
556
|
+
case "comment":
|
|
556
557
|
showCommentInput();
|
|
557
558
|
break;
|
|
558
|
-
|
|
559
|
-
case "l": {
|
|
560
|
-
// Thread list
|
|
559
|
+
case "thread-list":
|
|
561
560
|
showThreadListOverlay();
|
|
562
561
|
break;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
state.resolveThread(thread.id);
|
|
571
|
-
state.markRead(thread.id);
|
|
572
|
-
appendEvent(jsonlPath, { type: wasResolved ? "unresolve" : "resolve", threadId: thread.id, author: "reviewer", ts: Date.now() });
|
|
573
|
-
refreshPager();
|
|
574
|
-
const msg = wasResolved
|
|
575
|
-
? ` \u21a9 Reopened thread #${thread.id}`
|
|
576
|
-
: ` \u2714 Resolved thread #${thread.id}`;
|
|
577
|
-
bottomBar.text.content = msg;
|
|
578
|
-
renderer.requestRender();
|
|
579
|
-
setTimeout(() => { refreshPager(); }, 1500);
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
// Shift+R = resolve all pending
|
|
583
|
-
const { pending } = state.activeThreadCount();
|
|
584
|
-
const pendingThreads = state.threads.filter(t => t.status === "pending");
|
|
585
|
-
state.resolveAllPending();
|
|
586
|
-
for (const t of pendingThreads) {
|
|
587
|
-
appendEvent(jsonlPath, { type: "resolve", threadId: t.id, author: "reviewer", ts: Date.now() });
|
|
588
|
-
}
|
|
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() });
|
|
589
569
|
refreshPager();
|
|
590
|
-
|
|
570
|
+
const msg = wasResolved
|
|
571
|
+
? ` \u21a9 Reopened thread #${thread.id}`
|
|
572
|
+
: ` \u2714 Resolved thread #${thread.id}`;
|
|
573
|
+
bottomBar.text.content = msg;
|
|
591
574
|
renderer.requestRender();
|
|
592
575
|
setTimeout(() => { refreshPager(); }, 1500);
|
|
593
576
|
}
|
|
594
577
|
break;
|
|
595
578
|
}
|
|
596
|
-
case "
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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() });
|
|
601
599
|
refreshPager();
|
|
600
|
+
bottomBar.text.content = " \u2714 Deleted draft comment";
|
|
601
|
+
renderer.requestRender();
|
|
602
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
602
603
|
} else {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
clearTimeout(gPendingTimer);
|
|
607
|
-
gPendingTimer = null;
|
|
608
|
-
state.cursorLine = 1;
|
|
609
|
-
ensureCursorVisible();
|
|
610
|
-
refreshPager();
|
|
611
|
-
} else {
|
|
612
|
-
gPendingTimer = setTimeout(() => {
|
|
613
|
-
gPendingTimer = null;
|
|
614
|
-
}, 500);
|
|
615
|
-
}
|
|
604
|
+
bottomBar.text.content = " No reviewer message to delete";
|
|
605
|
+
renderer.requestRender();
|
|
606
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
616
607
|
}
|
|
617
608
|
break;
|
|
618
609
|
}
|
|
619
|
-
case "
|
|
620
|
-
// Approve
|
|
610
|
+
case "approve":
|
|
621
611
|
if (state.canApprove()) {
|
|
622
612
|
const confirmOverlay = createConfirm({
|
|
623
613
|
renderer,
|
|
@@ -632,105 +622,63 @@ export async function runTui(
|
|
|
632
622
|
},
|
|
633
623
|
});
|
|
634
624
|
showOverlay(confirmOverlay);
|
|
635
|
-
return;
|
|
636
625
|
} else {
|
|
637
|
-
// Show why approval is blocked
|
|
638
626
|
const { open, pending } = state.activeThreadCount();
|
|
639
627
|
const total = open + pending;
|
|
640
|
-
const msg =
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
: `${total} thread${total !== 1 ? "s" : ""} still open/pending`;
|
|
628
|
+
const msg = total === 0
|
|
629
|
+
? "No threads to approve"
|
|
630
|
+
: `${total} thread${total !== 1 ? "s" : ""} still open/pending`;
|
|
644
631
|
bottomBar.text.content = ` \u26a0 ${msg}`;
|
|
645
632
|
renderer.requestRender();
|
|
646
|
-
setTimeout(() => {
|
|
647
|
-
refreshPager();
|
|
648
|
-
}, 2000);
|
|
633
|
+
setTimeout(() => { refreshPager(); }, 2000);
|
|
649
634
|
}
|
|
650
635
|
break;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (bracketPendingTimer) { clearTimeout(bracketPendingTimer); bracketPendingTimer = null; }
|
|
658
|
-
if (key.name === "t" || key.sequence === "t") {
|
|
659
|
-
if (pending === "]") {
|
|
660
|
-
const next = state.nextActiveThread();
|
|
661
|
-
if (next !== null) {
|
|
662
|
-
state.cursorLine = next;
|
|
663
|
-
ensureCursorVisible();
|
|
664
|
-
refreshPager();
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
const prev = state.prevActiveThread();
|
|
668
|
-
if (prev !== null) {
|
|
669
|
-
state.cursorLine = prev;
|
|
670
|
-
ensureCursorVisible();
|
|
671
|
-
refreshPager();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} else if (key.name === "r" || key.sequence === "r") {
|
|
675
|
-
if (pending === "]") {
|
|
676
|
-
const nextLine = state.nextUnreadThread();
|
|
677
|
-
if (nextLine !== null) {
|
|
678
|
-
state.cursorLine = nextLine;
|
|
679
|
-
ensureCursorVisible();
|
|
680
|
-
refreshPager();
|
|
681
|
-
}
|
|
682
|
-
} else {
|
|
683
|
-
const prevLine = state.prevUnreadThread();
|
|
684
|
-
if (prevLine !== null) {
|
|
685
|
-
state.cursorLine = prevLine;
|
|
686
|
-
ensureCursorVisible();
|
|
687
|
-
refreshPager();
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
refreshPager(); // clear the bracket hint
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
// Check for "]" or "[" to start bracket sequence
|
|
695
|
-
if (key.sequence === "]") {
|
|
696
|
-
bracketPending = "]";
|
|
697
|
-
bottomBar.text.content = " ]...";
|
|
698
|
-
renderer.requestRender();
|
|
699
|
-
bracketPendingTimer = setTimeout(() => {
|
|
700
|
-
bracketPending = null;
|
|
701
|
-
bracketPendingTimer = null;
|
|
702
|
-
refreshPager();
|
|
703
|
-
}, 500);
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
706
|
-
if (key.sequence === "[") {
|
|
707
|
-
bracketPending = "[";
|
|
708
|
-
bottomBar.text.content = " [...";
|
|
709
|
-
renderer.requestRender();
|
|
710
|
-
bracketPendingTimer = setTimeout(() => {
|
|
711
|
-
bracketPending = null;
|
|
712
|
-
bracketPendingTimer = null;
|
|
713
|
-
refreshPager();
|
|
714
|
-
}, 500);
|
|
715
|
-
break;
|
|
636
|
+
case "next-thread": {
|
|
637
|
+
const next = state.nextActiveThread();
|
|
638
|
+
if (next !== null) {
|
|
639
|
+
state.cursorLine = next;
|
|
640
|
+
ensureCursorVisible();
|
|
641
|
+
refreshPager();
|
|
716
642
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case "prev-thread": {
|
|
646
|
+
const prev = state.prevActiveThread();
|
|
647
|
+
if (prev !== null) {
|
|
648
|
+
state.cursorLine = prev;
|
|
649
|
+
ensureCursorVisible();
|
|
650
|
+
refreshPager();
|
|
721
651
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case "next-unread": {
|
|
655
|
+
const nextLine = state.nextUnreadThread();
|
|
656
|
+
if (nextLine !== null) {
|
|
657
|
+
state.cursorLine = nextLine;
|
|
658
|
+
ensureCursorVisible();
|
|
659
|
+
refreshPager();
|
|
726
660
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
case "prev-unread": {
|
|
664
|
+
const prevLine = state.prevUnreadThread();
|
|
665
|
+
if (prevLine !== null) {
|
|
666
|
+
state.cursorLine = prevLine;
|
|
667
|
+
ensureCursorVisible();
|
|
730
668
|
refreshPager();
|
|
731
669
|
}
|
|
732
670
|
break;
|
|
733
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;
|
|
734
682
|
}
|
|
735
683
|
});
|
|
736
684
|
});
|