@rind-ai/cli 0.4.1
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/bin/rind.js +5 -0
- package/lib/assistant-renderer.js +265 -0
- package/lib/assistant-stream-buffer.js +25 -0
- package/lib/background-controller.js +307 -0
- package/lib/choice-menu-state.js +46 -0
- package/lib/command-controller.js +126 -0
- package/lib/compact-context-state.js +22 -0
- package/lib/composer-terminal.js +203 -0
- package/lib/event-controller.js +242 -0
- package/lib/frontend-cli-implementation.js +1111 -0
- package/lib/frontend-cli.js +5 -0
- package/lib/input-controller.js +94 -0
- package/lib/input-errors.js +3 -0
- package/lib/interrupt-state.js +9 -0
- package/lib/line-editor.js +541 -0
- package/lib/model-menu-state.js +50 -0
- package/lib/rendering.js +1060 -0
- package/lib/runtime-client.js +201 -0
- package/lib/runtime-env.js +21 -0
- package/lib/runtime-protocol.js +15 -0
- package/lib/slash-command-mode.js +27 -0
- package/lib/slash-menu-state.js +59 -0
- package/lib/terminal-key.js +97 -0
- package/lib/terminal-ui.js +581 -0
- package/lib/text-width.js +151 -0
- package/lib/turn-controller.js +78 -0
- package/package.json +28 -0
package/bin/rind.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { graphemes, textWidth } from "./text-width.js";
|
|
2
|
+
|
|
3
|
+
const INLINE_TOKEN_RE = /(\[[^\]]+\]\([^)]+\)|`[^`]+`|\*\*[^*]+\*\*)/g;
|
|
4
|
+
const PLAIN_TEXT_RE = /[`*#>|\[]/;
|
|
5
|
+
const CONTENT_PREFIX = " ";
|
|
6
|
+
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
7
|
+
|
|
8
|
+
export class AssistantRenderer {
|
|
9
|
+
constructor(write, options = {}) {
|
|
10
|
+
this.write = write;
|
|
11
|
+
this.color = options.color ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
|
|
12
|
+
this.pending = "";
|
|
13
|
+
this.inCodeBlock = false;
|
|
14
|
+
this.lineOpen = false;
|
|
15
|
+
this.atLineStart = true;
|
|
16
|
+
this.visibleColumn = 0;
|
|
17
|
+
this.columns = options.columns;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
append(text) {
|
|
21
|
+
this.pending += String(text || "");
|
|
22
|
+
while (true) {
|
|
23
|
+
const newlineIndex = this.pending.indexOf("\n");
|
|
24
|
+
if (newlineIndex === -1) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const line = this.pending.slice(0, newlineIndex);
|
|
28
|
+
this.pending = this.pending.slice(newlineIndex + 1);
|
|
29
|
+
this.renderLine(line, true);
|
|
30
|
+
}
|
|
31
|
+
this.flushPlainPending();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
finish() {
|
|
35
|
+
if (this.pending) {
|
|
36
|
+
this.renderLine(this.pending, false);
|
|
37
|
+
this.pending = "";
|
|
38
|
+
}
|
|
39
|
+
if (this.lineOpen) {
|
|
40
|
+
this.writeText("\n");
|
|
41
|
+
this.lineOpen = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
flushPlainPending() {
|
|
46
|
+
if (!this.pending || this.inCodeBlock || !isPlainLine(this.pending)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.writePlain(this.pending, false);
|
|
50
|
+
this.pending = "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
renderLine(line, newline) {
|
|
54
|
+
if (isTableLine(line, this.inCodeBlock)) {
|
|
55
|
+
this.renderTableLine(line, newline);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (line.trim().startsWith("```")) {
|
|
59
|
+
this.renderCodeFence(line, newline);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.inCodeBlock) {
|
|
63
|
+
this.writeStyled(styled(line, this.color, "codeBlock"), newline);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (isPlainLine(line)) {
|
|
67
|
+
this.writePlain(line, newline);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.writeStyled(renderMarkdownishLine(line, this.color), newline);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
renderTableLine(line, newline) {
|
|
74
|
+
const cells = parseTableRow(line);
|
|
75
|
+
if (!cells.length || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const rendered = cells.map((cell, index) =>
|
|
79
|
+
renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
|
|
80
|
+
);
|
|
81
|
+
this.writeStyled(rendered.join(dim(" | ", this.color)), newline);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
renderCodeFence(line, newline) {
|
|
85
|
+
const opening = !this.inCodeBlock;
|
|
86
|
+
this.inCodeBlock = opening;
|
|
87
|
+
const label = opening ? line.trim().slice(3).trim().slice(0, 32) : "";
|
|
88
|
+
this.writeStyled(dim(opening ? codeOpenLabel(label) : "└ end", this.color), newline);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
writePlain(text, newline) {
|
|
92
|
+
this.writeText(text + (newline ? "\n" : ""));
|
|
93
|
+
this.lineOpen = Boolean(text) && !newline;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
writeStyled(text, newline) {
|
|
97
|
+
this.writeText(text + (newline ? "\n" : ""));
|
|
98
|
+
this.lineOpen = Boolean(text) && !newline;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
writeText(text) {
|
|
102
|
+
const parts = String(text || "").split(/(\r\n|\r|\n)/);
|
|
103
|
+
const maxWidth = Math.max(1, Math.floor(Number(this.columns ?? process.stdout.columns ?? 80) || 80));
|
|
104
|
+
const prefixWidth = textWidth(CONTENT_PREFIX);
|
|
105
|
+
let output = "";
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
if (!part) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (part === "\r\n" || part === "\r" || part === "\n") {
|
|
111
|
+
if (this.atLineStart) {
|
|
112
|
+
output += CONTENT_PREFIX;
|
|
113
|
+
}
|
|
114
|
+
output += part;
|
|
115
|
+
this.atLineStart = true;
|
|
116
|
+
this.visibleColumn = 0;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const segment of ansiSegments(part)) {
|
|
120
|
+
if (segment.ansi) {
|
|
121
|
+
if (this.atLineStart) {
|
|
122
|
+
output += CONTENT_PREFIX;
|
|
123
|
+
this.atLineStart = false;
|
|
124
|
+
this.visibleColumn = prefixWidth;
|
|
125
|
+
}
|
|
126
|
+
output += segment.text;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const grapheme of graphemes(segment.text)) {
|
|
130
|
+
const segmentWidth = textWidth(grapheme);
|
|
131
|
+
if (this.atLineStart) {
|
|
132
|
+
output += CONTENT_PREFIX;
|
|
133
|
+
this.atLineStart = false;
|
|
134
|
+
this.visibleColumn = prefixWidth;
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
segmentWidth > 0
|
|
138
|
+
&& this.visibleColumn > prefixWidth
|
|
139
|
+
&& this.visibleColumn + segmentWidth > maxWidth
|
|
140
|
+
) {
|
|
141
|
+
output += `\n${CONTENT_PREFIX}`;
|
|
142
|
+
this.visibleColumn = prefixWidth;
|
|
143
|
+
}
|
|
144
|
+
output += grapheme;
|
|
145
|
+
this.visibleColumn += segmentWidth;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (output) {
|
|
150
|
+
this.write(output);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ansiSegments(value) {
|
|
156
|
+
const segments = [];
|
|
157
|
+
let position = 0;
|
|
158
|
+
for (const match of value.matchAll(ANSI_SEQUENCE)) {
|
|
159
|
+
if (match.index > position) {
|
|
160
|
+
segments.push({ text: value.slice(position, match.index), ansi: false });
|
|
161
|
+
}
|
|
162
|
+
segments.push({ text: match[0], ansi: true });
|
|
163
|
+
position = match.index + match[0].length;
|
|
164
|
+
}
|
|
165
|
+
if (position < value.length) {
|
|
166
|
+
segments.push({ text: value.slice(position), ansi: false });
|
|
167
|
+
}
|
|
168
|
+
return segments;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderMarkdownishLine(line, color) {
|
|
172
|
+
const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
173
|
+
if (heading) {
|
|
174
|
+
return renderInline(heading[2], color, "heading");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const quote = line.match(/^(\s*)>\s?(.*)$/);
|
|
178
|
+
if (quote) {
|
|
179
|
+
return `${quote[1]}${dim("│ ", color)}${renderInline(quote[2], color)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const list = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
|
|
183
|
+
if (list) {
|
|
184
|
+
const marker = /^\d+\.$/.test(list[2]) ? list[2] : "•";
|
|
185
|
+
return `${list[1]}${dim(`${marker} `, color)}${renderInline(list[3], color)}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return renderInline(line, color);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderInline(text, color, baseStyle = "") {
|
|
192
|
+
const source = String(text || "");
|
|
193
|
+
let output = "";
|
|
194
|
+
let index = 0;
|
|
195
|
+
for (const match of source.matchAll(INLINE_TOKEN_RE)) {
|
|
196
|
+
output += styled(source.slice(index, match.index), color, baseStyle);
|
|
197
|
+
output += renderInlineToken(match[0], color, baseStyle);
|
|
198
|
+
index = match.index + match[0].length;
|
|
199
|
+
}
|
|
200
|
+
return output + styled(source.slice(index), color, baseStyle);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderInlineToken(token, color, baseStyle) {
|
|
204
|
+
const link = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
205
|
+
if (link) {
|
|
206
|
+
return `${renderInline(link[1], color, baseStyle)} ${dim(`(${link[2]})`, color)}`;
|
|
207
|
+
}
|
|
208
|
+
if (token.startsWith("`") && token.endsWith("`")) {
|
|
209
|
+
return styled(token.slice(1, -1), color, "inlineCode");
|
|
210
|
+
}
|
|
211
|
+
if (token.startsWith("**") && token.endsWith("**")) {
|
|
212
|
+
return styled(token.slice(2, -2), color, baseStyle || "emphasis");
|
|
213
|
+
}
|
|
214
|
+
return token;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isPlainLine(line) {
|
|
218
|
+
if (!line) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (PLAIN_TEXT_RE.test(line)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const stripped = line.trimStart();
|
|
225
|
+
return !stripped.match(/^([-*+]|\d+\.)\s+/);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isTableLine(line, inCodeBlock) {
|
|
229
|
+
const stripped = line.trim();
|
|
230
|
+
return !inCodeBlock && stripped.includes("|") && stripped.split("|").length > 2;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseTableRow(line) {
|
|
234
|
+
let stripped = line.trim();
|
|
235
|
+
if (stripped.startsWith("|")) {
|
|
236
|
+
stripped = stripped.slice(1);
|
|
237
|
+
}
|
|
238
|
+
if (stripped.endsWith("|")) {
|
|
239
|
+
stripped = stripped.slice(0, -1);
|
|
240
|
+
}
|
|
241
|
+
return stripped.split("|").map((cell) => cell.trim());
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function codeOpenLabel(label) {
|
|
245
|
+
return label ? `┌ code ${label}` : "┌ code";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function styled(text, color, style) {
|
|
249
|
+
if (!text || !color || !style) {
|
|
250
|
+
return text;
|
|
251
|
+
}
|
|
252
|
+
const codes = {
|
|
253
|
+
codeBlock: "38;5;110",
|
|
254
|
+
emphasis: "1;38;5;221",
|
|
255
|
+
heading: "1;38;5;81",
|
|
256
|
+
inlineCode: "38;5;215",
|
|
257
|
+
tableHeader: "1;38;5;81",
|
|
258
|
+
};
|
|
259
|
+
const code = codes[style] || codes.emphasis;
|
|
260
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function dim(text, color) {
|
|
264
|
+
return color ? `\x1b[2m${text}\x1b[0m` : text;
|
|
265
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function createAssistantStreamBuffer() {
|
|
2
|
+
let pending = "";
|
|
3
|
+
|
|
4
|
+
return {
|
|
5
|
+
push(text, holdPartialLine = false) {
|
|
6
|
+
const output = pending + String(text || "");
|
|
7
|
+
if (!holdPartialLine || output.endsWith("\n")) {
|
|
8
|
+
pending = "";
|
|
9
|
+
return output;
|
|
10
|
+
}
|
|
11
|
+
const splitAt = output.lastIndexOf("\n");
|
|
12
|
+
if (splitAt === -1) {
|
|
13
|
+
pending = output;
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
pending = output.slice(splitAt + 1);
|
|
17
|
+
return output.slice(0, splitAt + 1);
|
|
18
|
+
},
|
|
19
|
+
flush() {
|
|
20
|
+
const output = pending;
|
|
21
|
+
pending = "";
|
|
22
|
+
return output;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { backgroundMonitorText } from "./rendering.js";
|
|
2
|
+
|
|
3
|
+
export function createBackgroundController({
|
|
4
|
+
request,
|
|
5
|
+
state,
|
|
6
|
+
redraw = () => {},
|
|
7
|
+
log = () => {},
|
|
8
|
+
terminalUi = false,
|
|
9
|
+
}) {
|
|
10
|
+
const tasks = new Map();
|
|
11
|
+
const pendingCommands = new Map();
|
|
12
|
+
let refreshTimer = null;
|
|
13
|
+
let listInFlight = null;
|
|
14
|
+
let monitorTimer = null;
|
|
15
|
+
let monitorPollInFlight = false;
|
|
16
|
+
let monitor = null;
|
|
17
|
+
let monitorInputWasActive = false;
|
|
18
|
+
|
|
19
|
+
function refresh() {
|
|
20
|
+
if (!terminalUi || state.runtimeClosing) {
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
}
|
|
23
|
+
if (listInFlight) {
|
|
24
|
+
return listInFlight;
|
|
25
|
+
}
|
|
26
|
+
listInFlight = request("background.list")
|
|
27
|
+
.then((result) => {
|
|
28
|
+
const listed = Array.isArray(result?.tasks) ? result.tasks : [];
|
|
29
|
+
const ids = new Set();
|
|
30
|
+
for (const task of listed) {
|
|
31
|
+
const bgId = String(task?.bg_id || "").trim();
|
|
32
|
+
if (!bgId) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
ids.add(bgId);
|
|
36
|
+
tasks.set(bgId, { ...(tasks.get(bgId) || {}), ...task, bg_id: bgId });
|
|
37
|
+
}
|
|
38
|
+
for (const bgId of tasks.keys()) {
|
|
39
|
+
if (!ids.has(bgId) && tasks.get(bgId)?.status === "running") {
|
|
40
|
+
tasks.delete(bgId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
updateCount();
|
|
44
|
+
if (monitor) {
|
|
45
|
+
monitor.selectedIndex = clampIndex(monitor.selectedIndex);
|
|
46
|
+
redraw(true);
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.finally(() => {
|
|
50
|
+
listInFlight = null;
|
|
51
|
+
});
|
|
52
|
+
return listInFlight;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function updateCount() {
|
|
56
|
+
const count = [...tasks.values()].filter((task) => task.status === "running").length;
|
|
57
|
+
if (Number(state.sessionInfo?.background_count) !== count) {
|
|
58
|
+
state.sessionInfo = { ...state.sessionInfo, background_count: count };
|
|
59
|
+
redraw();
|
|
60
|
+
}
|
|
61
|
+
if (count > 0) {
|
|
62
|
+
startRefresh();
|
|
63
|
+
} else {
|
|
64
|
+
stopRefresh();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function startRefresh() {
|
|
69
|
+
if (refreshTimer || state.runtimeClosing) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
refreshTimer = setInterval(() => {
|
|
73
|
+
void refresh().catch(() => {});
|
|
74
|
+
}, 1000);
|
|
75
|
+
refreshTimer.unref?.();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stopRefresh() {
|
|
79
|
+
if (!refreshTimer) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
clearInterval(refreshTimer);
|
|
83
|
+
refreshTimer = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clear() {
|
|
87
|
+
tasks.clear();
|
|
88
|
+
pendingCommands.clear();
|
|
89
|
+
stopRefresh();
|
|
90
|
+
updateCount();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function recordCommand(event) {
|
|
94
|
+
if (event?.tool_name !== "bash" || !event.tool_call_id) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const args = parseObject(event.args_preview);
|
|
98
|
+
if (args.command) {
|
|
99
|
+
pendingCommands.set(event.tool_call_id, String(args.command));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function recordResult(event) {
|
|
104
|
+
const parsed = parseObject(event?.result);
|
|
105
|
+
const data = parsed.data && typeof parsed.data === "object" ? parsed.data : parsed;
|
|
106
|
+
const bgId = String(data?.bg_id || "").trim();
|
|
107
|
+
if (!bgId) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const previous = tasks.get(bgId) || {};
|
|
111
|
+
const command = pendingCommands.get(event.tool_call_id) || previous.command || "";
|
|
112
|
+
tasks.set(bgId, { ...previous, ...data, bg_id: bgId, command });
|
|
113
|
+
if (event.tool_call_id) {
|
|
114
|
+
pendingCommands.delete(event.tool_call_id);
|
|
115
|
+
}
|
|
116
|
+
updateCount();
|
|
117
|
+
void pollMonitor();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function enterMonitor() {
|
|
121
|
+
if (monitor || state.runtimeClosing) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
monitorInputWasActive = state.inputActive;
|
|
125
|
+
monitor = { selectedIndex: 0 };
|
|
126
|
+
state.inputActive = true;
|
|
127
|
+
redraw(true);
|
|
128
|
+
void refresh()
|
|
129
|
+
.then(() => {
|
|
130
|
+
if (!monitor) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!tasks.size) {
|
|
134
|
+
exitMonitor();
|
|
135
|
+
log("No background tasks.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
monitor.selectedIndex = clampIndex(monitor.selectedIndex);
|
|
139
|
+
startMonitorPolling();
|
|
140
|
+
void pollMonitor();
|
|
141
|
+
redraw(true);
|
|
142
|
+
})
|
|
143
|
+
.catch((error) => {
|
|
144
|
+
if (!monitor || state.runtimeClosing) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
exitMonitor();
|
|
148
|
+
log(`Background monitor failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function exitMonitor() {
|
|
153
|
+
stopMonitorPolling();
|
|
154
|
+
monitor = null;
|
|
155
|
+
state.inputActive = monitorInputWasActive;
|
|
156
|
+
redraw(true);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function startMonitorPolling() {
|
|
160
|
+
if (monitorTimer) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
monitorTimer = setInterval(() => {
|
|
164
|
+
void pollMonitor();
|
|
165
|
+
}, 500);
|
|
166
|
+
monitorTimer.unref?.();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stopMonitorPolling() {
|
|
170
|
+
if (!monitorTimer) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
clearInterval(monitorTimer);
|
|
174
|
+
monitorTimer = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function pollMonitor() {
|
|
178
|
+
if (!monitor || monitorPollInFlight || state.runtimeClosing) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const available = [...tasks.values()];
|
|
182
|
+
if (!available.length) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
monitor.selectedIndex = clampIndex(monitor.selectedIndex);
|
|
186
|
+
const selected = available[monitor.selectedIndex];
|
|
187
|
+
if (!selected?.bg_id) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
monitorPollInFlight = true;
|
|
191
|
+
try {
|
|
192
|
+
const result = await request("background.output", {
|
|
193
|
+
bg_id: selected.bg_id,
|
|
194
|
+
max_output_chars: 20000,
|
|
195
|
+
});
|
|
196
|
+
if (result?.task && typeof result.task === "object") {
|
|
197
|
+
tasks.set(selected.bg_id, {
|
|
198
|
+
...selected,
|
|
199
|
+
...result.task,
|
|
200
|
+
command: selected.command || "",
|
|
201
|
+
});
|
|
202
|
+
updateCount();
|
|
203
|
+
redraw();
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Periodic list refresh reconciles expired tasks without interrupting input.
|
|
207
|
+
} finally {
|
|
208
|
+
monitorPollInFlight = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function moveSelection(delta) {
|
|
213
|
+
const count = tasks.size;
|
|
214
|
+
if (!monitor || !count) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const current = clampIndex(monitor.selectedIndex);
|
|
218
|
+
monitor.selectedIndex = (current + delta + count) % count;
|
|
219
|
+
void pollMonitor();
|
|
220
|
+
redraw();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleInput(key) {
|
|
224
|
+
const modified = key.ctrl || key.alt || key.shift;
|
|
225
|
+
if (!modified && key.name === "escape") {
|
|
226
|
+
exitMonitor();
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (key.ctrl && key.name === "b") {
|
|
230
|
+
exitMonitor();
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (modified) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
if (key.name === "up" || key.text === "k") {
|
|
237
|
+
moveSelection(-1);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (key.name === "down" || key.text === "j") {
|
|
241
|
+
moveSelection(1);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function frame(width) {
|
|
248
|
+
const list = [...tasks.values()];
|
|
249
|
+
const selectedIndex = clampIndex(monitor?.selectedIndex);
|
|
250
|
+
const text = backgroundMonitorText(
|
|
251
|
+
list,
|
|
252
|
+
selectedIndex,
|
|
253
|
+
list[selectedIndex],
|
|
254
|
+
Math.max(20, Number(width) - 4),
|
|
255
|
+
);
|
|
256
|
+
const lines = text.split("\n");
|
|
257
|
+
const selectedRow = list.length ? 2 + selectedIndex : Math.max(0, lines.length - 1);
|
|
258
|
+
return {
|
|
259
|
+
lines,
|
|
260
|
+
cursorRow: Math.min(selectedRow, Math.max(0, lines.length - 1)),
|
|
261
|
+
cursorColumn: 0,
|
|
262
|
+
focusRow: Math.min(selectedRow, Math.max(0, lines.length - 1)),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function stop() {
|
|
267
|
+
stopRefresh();
|
|
268
|
+
stopMonitorPolling();
|
|
269
|
+
monitor = null;
|
|
270
|
+
pendingCommands.clear();
|
|
271
|
+
tasks.clear();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
refresh,
|
|
276
|
+
recordCommand,
|
|
277
|
+
recordResult,
|
|
278
|
+
enterMonitor,
|
|
279
|
+
exitMonitor,
|
|
280
|
+
handleInput,
|
|
281
|
+
clear,
|
|
282
|
+
stop,
|
|
283
|
+
frame,
|
|
284
|
+
isMonitoring: () => Boolean(monitor),
|
|
285
|
+
moveSelection,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
function clampIndex(index) {
|
|
289
|
+
const count = tasks.size;
|
|
290
|
+
if (!count) {
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
return Math.min(count - 1, Math.max(0, Number(index) || 0));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseObject(value) {
|
|
298
|
+
if (value && typeof value === "object") {
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(String(value || ""));
|
|
303
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
304
|
+
} catch {
|
|
305
|
+
return {};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createChoiceMenuState(options, recommended = "") {
|
|
2
|
+
const items = normalizeOptions(options);
|
|
3
|
+
let selected = items.indexOf(recommended);
|
|
4
|
+
if (selected < 0) {
|
|
5
|
+
selected = 0;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
options() {
|
|
9
|
+
return items;
|
|
10
|
+
},
|
|
11
|
+
selectedIndex() {
|
|
12
|
+
return selected;
|
|
13
|
+
},
|
|
14
|
+
selectedOption() {
|
|
15
|
+
return items[selected] || "";
|
|
16
|
+
},
|
|
17
|
+
handleKey(key = {}) {
|
|
18
|
+
if (!items.length) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (key.name === "up" || key.text === "k") {
|
|
22
|
+
selected = selected <= 0 ? items.length - 1 : selected - 1;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (key.name === "down" || key.text === "j") {
|
|
26
|
+
selected = selected >= items.length - 1 ? 0 : selected + 1;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeOptions(options) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const items = [];
|
|
37
|
+
for (const option of Array.isArray(options) ? options : []) {
|
|
38
|
+
const value = String(option || "").trim();
|
|
39
|
+
if (!value || seen.has(value)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(value);
|
|
43
|
+
items.push(value);
|
|
44
|
+
}
|
|
45
|
+
return items;
|
|
46
|
+
}
|