revspec 0.7.3 → 0.8.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/README.md +3 -3
- package/package.json +1 -1
- package/src/state/review-state.ts +30 -0
- package/src/tui/app.ts +99 -0
- package/src/tui/comment-input.ts +24 -11
- package/src/tui/confirm.ts +3 -3
- package/src/tui/help.ts +8 -3
- package/src/tui/thread-list.ts +4 -4
- package/src/tui/ui/dialog.ts +2 -1
package/README.md
CHANGED
|
@@ -30,11 +30,11 @@ Install the `/revspec` skill for Claude Code:
|
|
|
30
30
|
/plugin install revspec
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Or manually (symlink, stays updated with
|
|
33
|
+
Or manually (clone + symlink, stays updated with `git pull`):
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
37
|
-
ln -sfn /
|
|
36
|
+
git clone https://github.com/icyrainz/revspec.git ~/.local/share/revspec
|
|
37
|
+
ln -sfn ~/.local/share/revspec/skills/revspec ~/.claude/skills/revspec
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Usage
|
package/package.json
CHANGED
|
@@ -113,6 +113,36 @@ export class ReviewState {
|
|
|
113
113
|
return this.threads.reduce((max, t) => (t.line > max.line ? t : max)).line;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
nextHeading(level: number): number | null {
|
|
117
|
+
const prefix = "#".repeat(level) + " ";
|
|
118
|
+
const guard = "#".repeat(level + 1);
|
|
119
|
+
for (let i = this.cursorLine; i < this.specLines.length; i++) {
|
|
120
|
+
const line = this.specLines[i];
|
|
121
|
+
if (line.startsWith(prefix) && !line.startsWith(guard)) return i + 1;
|
|
122
|
+
}
|
|
123
|
+
// Wrap: search from top
|
|
124
|
+
for (let i = 0; i < this.cursorLine - 1; i++) {
|
|
125
|
+
const line = this.specLines[i];
|
|
126
|
+
if (line.startsWith(prefix) && !line.startsWith(guard)) return i + 1;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
prevHeading(level: number): number | null {
|
|
132
|
+
const prefix = "#".repeat(level) + " ";
|
|
133
|
+
const guard = "#".repeat(level + 1);
|
|
134
|
+
for (let i = this.cursorLine - 2; i >= 0; i--) {
|
|
135
|
+
const line = this.specLines[i];
|
|
136
|
+
if (line.startsWith(prefix) && !line.startsWith(guard)) return i + 1;
|
|
137
|
+
}
|
|
138
|
+
// Wrap: search from bottom
|
|
139
|
+
for (let i = this.specLines.length - 1; i >= this.cursorLine; i--) {
|
|
140
|
+
const line = this.specLines[i];
|
|
141
|
+
if (line.startsWith(prefix) && !line.startsWith(guard)) return i + 1;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
canApprove(): boolean {
|
|
117
147
|
// No threads = clean approval (spec is good as-is)
|
|
118
148
|
if (this.threads.length === 0) return true;
|
package/src/tui/app.ts
CHANGED
|
@@ -123,6 +123,21 @@ export async function runTui(
|
|
|
123
123
|
// Command mode state
|
|
124
124
|
let commandBuffer: string | null = null;
|
|
125
125
|
|
|
126
|
+
// Previous position for '' jump-back
|
|
127
|
+
let prevCursorLine: number = 1;
|
|
128
|
+
function savePrevPosition(): void {
|
|
129
|
+
prevCursorLine = state.cursorLine;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Map visual row back to spec line number (for H/M/L)
|
|
133
|
+
function visualRowToSpecLine(targetRow: number): number {
|
|
134
|
+
for (let i = 0; i < state.specLines.length; i++) {
|
|
135
|
+
const row = i + countExtraVisualLines(state.specLines, i);
|
|
136
|
+
if (row >= targetRow) return i + 1;
|
|
137
|
+
}
|
|
138
|
+
return state.lineCount;
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
// Active spec poll interval (for submit spinner leak prevention)
|
|
127
142
|
let activeSpecPoll: ReturnType<typeof setInterval> | null = null;
|
|
128
143
|
|
|
@@ -203,6 +218,7 @@ export async function runTui(
|
|
|
203
218
|
// :{N} — jump to line number
|
|
204
219
|
const lineNum = parseInt(cmd, 10);
|
|
205
220
|
if (!isNaN(lineNum) && lineNum > 0) {
|
|
221
|
+
savePrevPosition();
|
|
206
222
|
state.cursorLine = Math.min(lineNum, state.lineCount);
|
|
207
223
|
ensureCursorVisible();
|
|
208
224
|
refreshPager();
|
|
@@ -271,6 +287,7 @@ export async function runTui(
|
|
|
271
287
|
cursorLine: state.cursorLine,
|
|
272
288
|
onResult: (lineNumber: number, query: string) => {
|
|
273
289
|
searchQuery = query;
|
|
290
|
+
savePrevPosition();
|
|
274
291
|
state.cursorLine = lineNumber;
|
|
275
292
|
dismissOverlay();
|
|
276
293
|
ensureCursorVisible();
|
|
@@ -289,6 +306,7 @@ export async function runTui(
|
|
|
289
306
|
renderer,
|
|
290
307
|
threads: state.threads,
|
|
291
308
|
onSelect: (lineNumber: number) => {
|
|
309
|
+
savePrevPosition();
|
|
292
310
|
state.cursorLine = lineNumber;
|
|
293
311
|
dismissOverlay();
|
|
294
312
|
ensureCursorVisible();
|
|
@@ -390,6 +408,16 @@ export async function runTui(
|
|
|
390
408
|
{ key: "[t", action: "prev-thread" },
|
|
391
409
|
{ key: "]r", action: "next-unread" },
|
|
392
410
|
{ key: "[r", action: "prev-unread" },
|
|
411
|
+
{ key: "]1", action: "next-h1" },
|
|
412
|
+
{ key: "[1", action: "prev-h1" },
|
|
413
|
+
{ key: "]2", action: "next-h2" },
|
|
414
|
+
{ key: "[2", action: "prev-h2" },
|
|
415
|
+
{ key: "]3", action: "next-h3" },
|
|
416
|
+
{ key: "[3", action: "prev-h3" },
|
|
417
|
+
{ key: "''", action: "jump-back" },
|
|
418
|
+
{ key: "H", action: "screen-top" },
|
|
419
|
+
{ key: "M", action: "screen-middle" },
|
|
420
|
+
{ key: "L", action: "screen-bottom" },
|
|
393
421
|
{ key: "zz", action: "center-cursor" },
|
|
394
422
|
{ key: "?", action: "help" },
|
|
395
423
|
{ key: "/", action: "search" },
|
|
@@ -512,11 +540,13 @@ export async function runTui(
|
|
|
512
540
|
break;
|
|
513
541
|
}
|
|
514
542
|
case "goto-bottom":
|
|
543
|
+
savePrevPosition();
|
|
515
544
|
state.cursorLine = state.lineCount;
|
|
516
545
|
ensureCursorVisible();
|
|
517
546
|
refreshPager();
|
|
518
547
|
break;
|
|
519
548
|
case "goto-top":
|
|
549
|
+
savePrevPosition();
|
|
520
550
|
state.cursorLine = 1;
|
|
521
551
|
ensureCursorVisible();
|
|
522
552
|
refreshPager();
|
|
@@ -533,6 +563,7 @@ export async function runTui(
|
|
|
533
563
|
if (searchQuery) {
|
|
534
564
|
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, 1);
|
|
535
565
|
if (match !== null) {
|
|
566
|
+
savePrevPosition();
|
|
536
567
|
state.cursorLine = match;
|
|
537
568
|
ensureCursorVisible();
|
|
538
569
|
}
|
|
@@ -547,6 +578,7 @@ export async function runTui(
|
|
|
547
578
|
if (searchQuery) {
|
|
548
579
|
const match = findNextMatch(state.specLines, searchQuery, state.cursorLine, -1);
|
|
549
580
|
if (match !== null) {
|
|
581
|
+
savePrevPosition();
|
|
550
582
|
state.cursorLine = match;
|
|
551
583
|
ensureCursorVisible();
|
|
552
584
|
}
|
|
@@ -693,6 +725,7 @@ export async function runTui(
|
|
|
693
725
|
case "next-thread": {
|
|
694
726
|
const next = state.nextThread();
|
|
695
727
|
if (next !== null) {
|
|
728
|
+
savePrevPosition();
|
|
696
729
|
state.cursorLine = next;
|
|
697
730
|
ensureCursorVisible();
|
|
698
731
|
refreshPager();
|
|
@@ -706,6 +739,7 @@ export async function runTui(
|
|
|
706
739
|
case "prev-thread": {
|
|
707
740
|
const prev = state.prevThread();
|
|
708
741
|
if (prev !== null) {
|
|
742
|
+
savePrevPosition();
|
|
709
743
|
state.cursorLine = prev;
|
|
710
744
|
ensureCursorVisible();
|
|
711
745
|
refreshPager();
|
|
@@ -719,6 +753,7 @@ export async function runTui(
|
|
|
719
753
|
case "next-unread": {
|
|
720
754
|
const nextLine = state.nextUnreadThread();
|
|
721
755
|
if (nextLine !== null) {
|
|
756
|
+
savePrevPosition();
|
|
722
757
|
state.cursorLine = nextLine;
|
|
723
758
|
ensureCursorVisible();
|
|
724
759
|
refreshPager();
|
|
@@ -732,6 +767,7 @@ export async function runTui(
|
|
|
732
767
|
case "prev-unread": {
|
|
733
768
|
const prevLine = state.prevUnreadThread();
|
|
734
769
|
if (prevLine !== null) {
|
|
770
|
+
savePrevPosition();
|
|
735
771
|
state.cursorLine = prevLine;
|
|
736
772
|
ensureCursorVisible();
|
|
737
773
|
refreshPager();
|
|
@@ -742,6 +778,69 @@ export async function runTui(
|
|
|
742
778
|
}
|
|
743
779
|
break;
|
|
744
780
|
}
|
|
781
|
+
case "next-h1":
|
|
782
|
+
case "next-h2":
|
|
783
|
+
case "next-h3": {
|
|
784
|
+
const level = parseInt(action.slice(-1));
|
|
785
|
+
const next = state.nextHeading(level);
|
|
786
|
+
if (next !== null) {
|
|
787
|
+
savePrevPosition();
|
|
788
|
+
state.cursorLine = next;
|
|
789
|
+
ensureCursorVisible();
|
|
790
|
+
refreshPager();
|
|
791
|
+
} else {
|
|
792
|
+
setBottomBarMessage(bottomBar, ` No h${level} headings`);
|
|
793
|
+
renderer.requestRender();
|
|
794
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case "prev-h1":
|
|
799
|
+
case "prev-h2":
|
|
800
|
+
case "prev-h3": {
|
|
801
|
+
const level = parseInt(action.slice(-1));
|
|
802
|
+
const prev = state.prevHeading(level);
|
|
803
|
+
if (prev !== null) {
|
|
804
|
+
savePrevPosition();
|
|
805
|
+
state.cursorLine = prev;
|
|
806
|
+
ensureCursorVisible();
|
|
807
|
+
refreshPager();
|
|
808
|
+
} else {
|
|
809
|
+
setBottomBarMessage(bottomBar, ` No h${level} headings`);
|
|
810
|
+
renderer.requestRender();
|
|
811
|
+
setTimeout(() => { refreshPager(); }, 1500);
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
case "jump-back": {
|
|
816
|
+
const tmp = state.cursorLine;
|
|
817
|
+
state.cursorLine = prevCursorLine;
|
|
818
|
+
prevCursorLine = tmp;
|
|
819
|
+
ensureCursorVisible();
|
|
820
|
+
refreshPager();
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case "screen-top": {
|
|
824
|
+
const topRow = pager.scrollBox.scrollTop;
|
|
825
|
+
savePrevPosition();
|
|
826
|
+
state.cursorLine = visualRowToSpecLine(topRow);
|
|
827
|
+
refreshPager();
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
case "screen-middle": {
|
|
831
|
+
const midRow = pager.scrollBox.scrollTop + Math.floor(pageSize() / 2);
|
|
832
|
+
savePrevPosition();
|
|
833
|
+
state.cursorLine = visualRowToSpecLine(midRow);
|
|
834
|
+
refreshPager();
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
case "screen-bottom": {
|
|
838
|
+
const botRow = pager.scrollBox.scrollTop + pageSize() - 1;
|
|
839
|
+
savePrevPosition();
|
|
840
|
+
state.cursorLine = visualRowToSpecLine(botRow);
|
|
841
|
+
refreshPager();
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
745
844
|
case "help":
|
|
746
845
|
showHelpOverlay();
|
|
747
846
|
break;
|
package/src/tui/comment-input.ts
CHANGED
|
@@ -168,8 +168,10 @@ function createThreadView(
|
|
|
168
168
|
const dialog = createDialog({
|
|
169
169
|
renderer,
|
|
170
170
|
title,
|
|
171
|
-
width: "
|
|
172
|
-
height: "
|
|
171
|
+
width: "70%",
|
|
172
|
+
height: "80%",
|
|
173
|
+
top: "8%",
|
|
174
|
+
left: "15%",
|
|
173
175
|
borderColor: theme.blue,
|
|
174
176
|
onDismiss: onCancel,
|
|
175
177
|
hints: insertHints,
|
|
@@ -220,18 +222,29 @@ function createThreadView(
|
|
|
220
222
|
scrollBox.add(renderMessage(msg));
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
// ---
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
// --- Layout: pack bottom elements into a single container ---
|
|
226
|
+
// Outside tmux, opentui's ScrollBox expands over intermediate siblings.
|
|
227
|
+
// Keeping exactly 2 direct children (ScrollBox + bottomPanel) avoids the issue,
|
|
228
|
+
// matching the pattern that works in help/thread-list dialogs.
|
|
229
|
+
dialog.container.remove(dialog.hintBox.id);
|
|
230
|
+
const bottomPanel = new BoxRenderable(renderer, {
|
|
231
|
+
width: "100%",
|
|
232
|
+
height: 6, // separator(1) + textarea(4) + hints(1)
|
|
233
|
+
flexShrink: 0,
|
|
234
|
+
flexGrow: 0,
|
|
235
|
+
flexDirection: "column",
|
|
236
|
+
});
|
|
237
|
+
const sep = new BoxRenderable(renderer, {
|
|
226
238
|
width: "100%",
|
|
227
239
|
height: 1,
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
flexShrink: 0,
|
|
241
|
+
border: ["top"],
|
|
242
|
+
borderColor: theme.border,
|
|
230
243
|
});
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
dialog.container.add(
|
|
244
|
+
bottomPanel.add(sep);
|
|
245
|
+
bottomPanel.add(textarea);
|
|
246
|
+
bottomPanel.add(dialog.hintBox);
|
|
247
|
+
dialog.container.add(bottomPanel);
|
|
235
248
|
|
|
236
249
|
// --- Mode helpers (need dialog.setHints available) ---
|
|
237
250
|
function enterInsert(): void {
|
package/src/tui/confirm.ts
CHANGED
|
@@ -31,10 +31,10 @@ export function createConfirm(opts: ConfirmOptions): ConfirmOverlay {
|
|
|
31
31
|
const dialog = createDialog({
|
|
32
32
|
renderer,
|
|
33
33
|
title,
|
|
34
|
-
width: "
|
|
34
|
+
width: "44%",
|
|
35
35
|
height: 9,
|
|
36
|
-
top: "
|
|
37
|
-
left: "
|
|
36
|
+
top: "30%",
|
|
37
|
+
left: "28%",
|
|
38
38
|
borderColor: theme.warning,
|
|
39
39
|
onDismiss: onCancel,
|
|
40
40
|
hints: CONFIRM_HINTS,
|
package/src/tui/help.ts
CHANGED
|
@@ -50,10 +50,10 @@ export function createHelp(opts: {
|
|
|
50
50
|
const dialog = createDialog({
|
|
51
51
|
renderer,
|
|
52
52
|
title: "Help",
|
|
53
|
-
width: "
|
|
54
|
-
height: Math.min(
|
|
53
|
+
width: "64%",
|
|
54
|
+
height: Math.min(32, renderer.height - 4),
|
|
55
55
|
top: "10%",
|
|
56
|
-
left: "
|
|
56
|
+
left: "18%",
|
|
57
57
|
borderColor: theme.info,
|
|
58
58
|
onDismiss: onClose,
|
|
59
59
|
hints: HELP_HINTS,
|
|
@@ -92,6 +92,11 @@ export function createHelp(opts: {
|
|
|
92
92
|
" Esc Clear search",
|
|
93
93
|
" ]t/[t Next/prev thread",
|
|
94
94
|
" ]r/[r Next/prev unread",
|
|
95
|
+
" ]1/[1 Next/prev h1 heading",
|
|
96
|
+
" ]2/[2 Next/prev h2 heading",
|
|
97
|
+
" ]3/[3 Next/prev h3 heading",
|
|
98
|
+
" '' Jump to previous position",
|
|
99
|
+
" H/M/L Screen top/middle/bottom",
|
|
95
100
|
]);
|
|
96
101
|
|
|
97
102
|
addHelpSection(dialog.content, renderer, "Review", [
|
package/src/tui/thread-list.ts
CHANGED
|
@@ -50,10 +50,10 @@ export function createThreadList(opts: ThreadListOptions): ThreadListOverlay {
|
|
|
50
50
|
const dialog = createDialog({
|
|
51
51
|
renderer,
|
|
52
52
|
title: `Threads (${activeCount} active, ${allThreads.length} total)`,
|
|
53
|
-
width: "
|
|
54
|
-
height: "
|
|
55
|
-
top: "
|
|
56
|
-
left: "
|
|
53
|
+
width: "56%",
|
|
54
|
+
height: "50%",
|
|
55
|
+
top: "20%",
|
|
56
|
+
left: "22%",
|
|
57
57
|
borderColor: theme.mauve,
|
|
58
58
|
onDismiss: onCancel,
|
|
59
59
|
hints: THREAD_LIST_HINTS,
|
package/src/tui/ui/dialog.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface DialogOptions {
|
|
|
23
23
|
export interface DialogComponents {
|
|
24
24
|
container: BoxRenderable;
|
|
25
25
|
content: ScrollBoxRenderable;
|
|
26
|
+
hintBox: BoxRenderable;
|
|
26
27
|
hintText: TextRenderable;
|
|
27
28
|
setHints: (hints: Hint[]) => void;
|
|
28
29
|
cleanup: () => void;
|
|
@@ -102,5 +103,5 @@ export function createDialog(opts: DialogOptions): DialogComponents {
|
|
|
102
103
|
renderer.keyInput.off("keypress", keyHandler);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
return { container, content, hintText, setHints, cleanup };
|
|
106
|
+
return { container, content, hintBox, hintText, setHints, cleanup };
|
|
106
107
|
}
|