revspec 0.2.1 → 0.3.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/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/cli/reply.ts +4 -1
- package/src/cli/watch.ts +6 -0
- package/src/protocol/live-events.ts +5 -3
- package/src/tui/app.ts +64 -114
- package/src/tui/comment-input.ts +5 -82
- package/src/tui/help.ts +5 -5
- package/src/tui/pager.ts +394 -81
- package/src/tui/status-bar.ts +85 -33
package/src/tui/status-bar.ts
CHANGED
|
@@ -1,87 +1,139 @@
|
|
|
1
|
-
import { TextRenderable, type CliRenderer } from "@opentui/core";
|
|
1
|
+
import { BoxRenderable, TextRenderable, TextNodeRenderable, TextAttributes, type CliRenderer } from "@opentui/core";
|
|
2
2
|
import type { ReviewState } from "../state/review-state";
|
|
3
3
|
import { basename } from "path";
|
|
4
4
|
import { theme } from "./theme";
|
|
5
5
|
|
|
6
6
|
export interface TopBarComponents {
|
|
7
|
-
|
|
7
|
+
box: BoxRenderable;
|
|
8
|
+
text: TextRenderable;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface BottomBarComponents {
|
|
11
|
-
|
|
12
|
+
box: BoxRenderable;
|
|
13
|
+
text: TextRenderable;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Build the top bar
|
|
17
|
+
* Build the top bar with styled TextNodes.
|
|
16
18
|
*/
|
|
17
|
-
export function
|
|
19
|
+
export function buildTopBar(
|
|
20
|
+
bar: TopBarComponents,
|
|
18
21
|
specFile: string,
|
|
19
22
|
state: ReviewState,
|
|
20
23
|
unreadCount?: number,
|
|
21
24
|
specChanged?: boolean,
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
): void {
|
|
26
|
+
const t = bar.text;
|
|
27
|
+
t.clear();
|
|
24
28
|
const name = basename(specFile);
|
|
25
|
-
const modeLabel = mode === "markdown" ? "[md]" : mode === "line" ? "[line]" : "";
|
|
26
29
|
const { open, pending } = state.activeThreadCount();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
|
|
31
|
+
// Filename — bold
|
|
32
|
+
t.add(TextNodeRenderable.fromString(` ${name}`, { fg: theme.text, attributes: TextAttributes.BOLD }));
|
|
33
|
+
|
|
34
|
+
t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
|
|
35
|
+
|
|
36
|
+
// Thread summary
|
|
37
|
+
if (open > 0 || pending > 0) {
|
|
38
|
+
const parts: string[] = [];
|
|
39
|
+
if (open > 0) parts.push(`${open} open`);
|
|
40
|
+
if (pending > 0) parts.push(`${pending} pending`);
|
|
41
|
+
t.add(TextNodeRenderable.fromString(parts.join(", "), { fg: theme.yellow }));
|
|
42
|
+
} else {
|
|
43
|
+
t.add(TextNodeRenderable.fromString("No active threads", { fg: theme.subtext }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Unread replies
|
|
33
47
|
if (unreadCount && unreadCount > 0) {
|
|
34
|
-
|
|
48
|
+
t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
|
|
49
|
+
t.add(TextNodeRenderable.fromString(
|
|
50
|
+
`${unreadCount} new repl${unreadCount === 1 ? "y" : "ies"}`,
|
|
51
|
+
{ fg: theme.green, attributes: TextAttributes.BOLD }
|
|
52
|
+
));
|
|
35
53
|
}
|
|
54
|
+
|
|
55
|
+
// Spec changed warning
|
|
36
56
|
if (specChanged) {
|
|
37
|
-
|
|
57
|
+
t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
|
|
58
|
+
t.add(TextNodeRenderable.fromString("!! Spec changed externally", { fg: theme.red, attributes: TextAttributes.BOLD }));
|
|
38
59
|
}
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
|
|
61
|
+
// Cursor position
|
|
62
|
+
t.add(TextNodeRenderable.fromString(" | ", { fg: theme.overlay }));
|
|
63
|
+
t.add(TextNodeRenderable.fromString(`L${state.cursorLine}/${state.lineCount}`, { fg: theme.subtext }));
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
* Build the bottom bar
|
|
45
|
-
* Contextually shows command buffer when in command mode.
|
|
46
|
-
* Prepends mode indicator when provided.
|
|
67
|
+
* Build the bottom bar with styled TextNodes.
|
|
47
68
|
*/
|
|
48
|
-
export function
|
|
69
|
+
export function buildBottomBar(bar: BottomBarComponents, commandBuffer: string | null): void {
|
|
70
|
+
const t = bar.text;
|
|
71
|
+
t.clear();
|
|
49
72
|
if (commandBuffer !== null) {
|
|
50
|
-
|
|
73
|
+
t.add(TextNodeRenderable.fromString(` :${commandBuffer}`, { fg: theme.text }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const hints = [
|
|
77
|
+
{ key: "j/k", action: "move" },
|
|
78
|
+
{ key: "c", action: "comment" },
|
|
79
|
+
{ key: "r", action: "resolve" },
|
|
80
|
+
{ key: "/", action: "search" },
|
|
81
|
+
{ key: "?", action: "help" },
|
|
82
|
+
];
|
|
83
|
+
t.add(TextNodeRenderable.fromString(" ", {}));
|
|
84
|
+
for (let i = 0; i < hints.length; i++) {
|
|
85
|
+
const h = hints[i];
|
|
86
|
+
t.add(TextNodeRenderable.fromString(`[${h.key}]`, { fg: theme.blue }));
|
|
87
|
+
t.add(TextNodeRenderable.fromString(` ${h.action}`, { fg: theme.subtext }));
|
|
88
|
+
if (i < hints.length - 1) {
|
|
89
|
+
t.add(TextNodeRenderable.fromString(" ", {}));
|
|
90
|
+
}
|
|
51
91
|
}
|
|
52
|
-
return ` [j/k] move [c] comment [r] resolve [/] search [?] help`;
|
|
53
92
|
}
|
|
54
93
|
|
|
55
94
|
/**
|
|
56
|
-
* Create the top status bar.
|
|
95
|
+
* Create the top status bar (BoxRenderable with backgroundColor for full-width fill).
|
|
57
96
|
*/
|
|
58
97
|
export function createTopBar(renderer: CliRenderer): TopBarComponents {
|
|
59
|
-
const
|
|
60
|
-
content: "",
|
|
98
|
+
const box = new BoxRenderable(renderer, {
|
|
61
99
|
width: "100%",
|
|
62
100
|
height: 1,
|
|
63
|
-
|
|
101
|
+
backgroundColor: theme.base,
|
|
102
|
+
border: ["bottom"],
|
|
103
|
+
borderColor: theme.surface1,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const text = new TextRenderable(renderer, {
|
|
107
|
+
content: "",
|
|
108
|
+
width: "100%",
|
|
64
109
|
fg: theme.text,
|
|
65
110
|
wrapMode: "none",
|
|
66
111
|
truncate: true,
|
|
67
112
|
});
|
|
68
113
|
|
|
69
|
-
|
|
114
|
+
box.add(text);
|
|
115
|
+
return { box, text };
|
|
70
116
|
}
|
|
71
117
|
|
|
72
118
|
/**
|
|
73
119
|
* Create the bottom status bar.
|
|
74
120
|
*/
|
|
75
121
|
export function createBottomBar(renderer: CliRenderer): BottomBarComponents {
|
|
76
|
-
const
|
|
77
|
-
content: "",
|
|
122
|
+
const box = new BoxRenderable(renderer, {
|
|
78
123
|
width: "100%",
|
|
79
124
|
height: 1,
|
|
80
|
-
|
|
125
|
+
flexShrink: 0,
|
|
126
|
+
backgroundColor: theme.surface0,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const text = new TextRenderable(renderer, {
|
|
130
|
+
content: "",
|
|
131
|
+
width: "100%",
|
|
81
132
|
fg: theme.text,
|
|
82
133
|
wrapMode: "none",
|
|
83
134
|
truncate: true,
|
|
84
135
|
});
|
|
85
136
|
|
|
86
|
-
|
|
137
|
+
box.add(text);
|
|
138
|
+
return { box, text };
|
|
87
139
|
}
|