@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/lib/rendering.js
ADDED
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import { clipCells, middleClipCells, textWidth, wrapTextCells } from "./text-width.js";
|
|
2
|
+
|
|
3
|
+
const MAX_STARTUP_BANNER_WIDTH = 80;
|
|
4
|
+
const MAX_COMPOSER_WIDTH = 78;
|
|
5
|
+
const MAX_FILE_CHANGE_LINES = 20;
|
|
6
|
+
|
|
7
|
+
export function startupText(info = {}) {
|
|
8
|
+
const header = startupBannerText(info);
|
|
9
|
+
const goal = goalText(info.goal, true);
|
|
10
|
+
const preview = resumePreviewText(info.resume_preview);
|
|
11
|
+
const sections = [header, goal, preview ? `${accent("•")} ${bold("Recent context")}\n${preview}` : ""];
|
|
12
|
+
return sections.filter(Boolean).join("\n\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function promptText(info = {}, _stats = {}, state = {}) {
|
|
16
|
+
return inputPromptFrame(promptHeaderLine(info), state);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function promptActivityLine(state = {}) {
|
|
20
|
+
if (!state.running) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
const elapsed = formatActivityDuration(state.elapsedMs);
|
|
24
|
+
const label = singleLine(state.label) || "Working";
|
|
25
|
+
return ` ${accent(activityFrame(state.frame))} ${bold(label)} ${dim(`(${elapsed}) ctrl+c interrupt`)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function promptPlaceholderText() {
|
|
29
|
+
return "Ask Rind to do anything";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function userInputText(text) {
|
|
33
|
+
const lines = messageLines(text);
|
|
34
|
+
if (!lines.length) {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
const contentWidth = userInputContentWidth();
|
|
38
|
+
const physicalLines = lines.flatMap((line) => (
|
|
39
|
+
wrapTextCells(line, contentWidth, contentWidth).map((chunk) => ` ${chunk.text}`)
|
|
40
|
+
));
|
|
41
|
+
return `${accent("▷")} ${bold("You")}\n${physicalLines.join("\n")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assistantHeaderText() {
|
|
45
|
+
return `${accent("◁")} ${bold("Assistant")}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function outputBlockText(text, leading = false) {
|
|
49
|
+
const body = String(text || "").trimEnd();
|
|
50
|
+
return body ? `${leading ? "\n" : ""}${body}\n` : "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function helpText(commands = []) {
|
|
54
|
+
const lines = [
|
|
55
|
+
`${accent("•")} Controls`,
|
|
56
|
+
helpRow("enter", "send message", "/", "open commands"),
|
|
57
|
+
helpRow("↑ / ↓", "history", "← / →", "move cursor"),
|
|
58
|
+
helpRow("home / end", "line edges", "del / backspace", "edit text"),
|
|
59
|
+
helpRow("ctrl+c", "interrupt or quit", "?", "show shortcuts"),
|
|
60
|
+
helpRow("ctrl+b", "background tasks", "esc", "close monitor"),
|
|
61
|
+
];
|
|
62
|
+
const commandRows = commandDeckText(commands);
|
|
63
|
+
if (commandRows.length) {
|
|
64
|
+
lines.push("", `${accent("•")} Command deck`, ...commandRows);
|
|
65
|
+
}
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function slashDisplayText(display, commands = []) {
|
|
70
|
+
if (!display || typeof display !== "object") {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
switch (display.type) {
|
|
74
|
+
case "help":
|
|
75
|
+
return slashHelpText(display, commands);
|
|
76
|
+
case "status":
|
|
77
|
+
return slashStatusText(display);
|
|
78
|
+
case "doctor":
|
|
79
|
+
return slashDoctorText(display);
|
|
80
|
+
case "sessions":
|
|
81
|
+
return slashSessionsText(display);
|
|
82
|
+
case "skills":
|
|
83
|
+
return slashSkillsText(display);
|
|
84
|
+
case "config":
|
|
85
|
+
return slashConfigText(display);
|
|
86
|
+
default:
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function slashResultText(result, commands = []) {
|
|
92
|
+
if (!result || typeof result !== "object") {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
return slashDisplayText(result.display, commands) || String(result.text || "");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function answerPromptText() {
|
|
99
|
+
return `\n ${accent("▷")} `;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function answerPlaceholderText() {
|
|
103
|
+
return "Type your answer";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function inputHintText(placeholder) {
|
|
107
|
+
const text = singleLine(placeholder);
|
|
108
|
+
return text ? dim(text) : "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function slashMenuText(items, selectedIndex = 0) {
|
|
112
|
+
const visible = menuWindow(items, selectedIndex);
|
|
113
|
+
if (!visible.items.length) {
|
|
114
|
+
return "";
|
|
115
|
+
}
|
|
116
|
+
const lines = [dim(slashMenuTitle(visible))];
|
|
117
|
+
for (const [index, item] of visible.items.entries()) {
|
|
118
|
+
const active = index === visible.activeIndex;
|
|
119
|
+
const marker = active ? accent("›") : dim("·");
|
|
120
|
+
const name = active ? bold(`/${item.name}`) : dim(`/${item.name}`);
|
|
121
|
+
const description = dim(clipSingleLine(item.description, 46));
|
|
122
|
+
lines.push(` ${marker} ${padRight(name, 14)} ${description}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push(dim(" ↑↓ select · enter run · esc close · backspace edit"));
|
|
125
|
+
return `${lines.join("\n")}\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function modelMenuText(items, selectedIndex = 0) {
|
|
129
|
+
const visible = menuWindow(items, selectedIndex);
|
|
130
|
+
if (!visible.items.length) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const lines = [dim(modelMenuTitle(visible))];
|
|
134
|
+
for (const [index, item] of visible.items.entries()) {
|
|
135
|
+
const active = index === visible.activeIndex;
|
|
136
|
+
const marker = active ? accent("›") : dim("·");
|
|
137
|
+
const name = active ? bold(item.name) : dim(item.name);
|
|
138
|
+
const suffix = item.current ? dim("current") : "";
|
|
139
|
+
lines.push(` ${marker} ${padRight(name, 34)} ${suffix}`.trimEnd());
|
|
140
|
+
}
|
|
141
|
+
lines.push(dim(" ↑↓ select · enter use · esc cancel"));
|
|
142
|
+
return `${lines.join("\n")}\n`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function choiceMenuText(options, selectedIndex = 0, recommended = "") {
|
|
146
|
+
return choiceMenuTextWithTitle(options, selectedIndex, recommended, "Choices");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sessionMenuText(options, selectedIndex = 0) {
|
|
150
|
+
return choiceMenuTextWithTitle(options, selectedIndex, "", "Sessions");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function backgroundMonitorText(tasks = [], selectedIndex = 0, selectedTask = null, width = 76) {
|
|
154
|
+
const items = Array.isArray(tasks) ? tasks : [];
|
|
155
|
+
const lines = [bold("Background tasks"), dim(" ↑↓/j/k select · esc/ctrl+b close")];
|
|
156
|
+
if (!items.length) {
|
|
157
|
+
lines.push(dim(" No background tasks."));
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
for (const [index, task] of items.entries()) {
|
|
161
|
+
const active = index === selectedIndex;
|
|
162
|
+
const marker = active ? accent("›") : dim("·");
|
|
163
|
+
const status = singleLine(task?.status) || "unknown";
|
|
164
|
+
const bgId = singleLine(task?.bg_id) || "unknown";
|
|
165
|
+
const command = clipSingleLine(task?.command, Math.max(12, width - 34));
|
|
166
|
+
lines.push(` ${marker} ${padRight(bgId, 12)} ${padRight(status, 10)} ${dim(command)}`.trimEnd());
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
const task = selectedTask || items[selectedIndex];
|
|
170
|
+
if (!task) {
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
const heading = `${singleLine(task.bg_id) || "unknown"} · ${singleLine(task.status) || "unknown"}`;
|
|
174
|
+
lines.push(dim(` ${heading}`));
|
|
175
|
+
const rawOutput = [task.stdout, task.stderr]
|
|
176
|
+
.filter((value) => String(value || ""))
|
|
177
|
+
.join("\n")
|
|
178
|
+
const visibleOutput = rawOutput ? rawOutput.split(/\r?\n/).slice(-18) : [];
|
|
179
|
+
if (!rawOutput) {
|
|
180
|
+
lines.push(dim(" (no output)"));
|
|
181
|
+
} else {
|
|
182
|
+
lines.push(...visibleOutput.map((line) => ` ${clipSingleLine(line, width)}`));
|
|
183
|
+
}
|
|
184
|
+
if (task.truncated) {
|
|
185
|
+
lines.push(dim(" … output truncated"));
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function choiceMenuTextWithTitle(options, selectedIndex = 0, recommended = "", title = "Choices") {
|
|
191
|
+
const visible = menuWindow(options, selectedIndex);
|
|
192
|
+
if (!visible.items.length) {
|
|
193
|
+
return "";
|
|
194
|
+
}
|
|
195
|
+
const lines = [dim(choiceMenuTitle(visible, title))];
|
|
196
|
+
for (const [index, option] of visible.items.entries()) {
|
|
197
|
+
const active = index === visible.activeIndex;
|
|
198
|
+
const marker = active ? accent("›") : dim("·");
|
|
199
|
+
const label = clipSingleLine(option, 60);
|
|
200
|
+
const name = active ? bold(label) : dim(label);
|
|
201
|
+
const suffix = option === recommended ? dim("recommended") : "";
|
|
202
|
+
lines.push(` ${marker} ${padRight(name, 34)} ${suffix}`.trimEnd());
|
|
203
|
+
}
|
|
204
|
+
lines.push(dim(" ↑↓ select · enter confirm · esc cancel"));
|
|
205
|
+
return `${lines.join("\n")}\n`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function choiceMenuTitle(visible, title = "Choices") {
|
|
209
|
+
if (visible.total <= visible.items.length) {
|
|
210
|
+
return ` ${title}`;
|
|
211
|
+
}
|
|
212
|
+
return ` ${title} ${visible.start + 1}-${visible.start + visible.items.length}/${visible.total}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function sessionSwitchedText(info = {}) {
|
|
216
|
+
const sessionId = singleLine(info.session_id) || "unknown";
|
|
217
|
+
const model = singleLine(info.model);
|
|
218
|
+
const goal = goalText(info.goal, true);
|
|
219
|
+
const preview = resumePreviewText(info.resume_preview);
|
|
220
|
+
const lines = [startupBannerText(info), "", `${green("✓")} ${bold("Session switched")}`, dim(detailLine(sessionId))];
|
|
221
|
+
if (model) {
|
|
222
|
+
lines.push(dim(detailLine(`model ${model}`)));
|
|
223
|
+
}
|
|
224
|
+
if (goal) {
|
|
225
|
+
lines.push("", goal);
|
|
226
|
+
}
|
|
227
|
+
if (preview) {
|
|
228
|
+
lines.push("", `${accent("•")} ${bold("Recent context")}`, preview);
|
|
229
|
+
}
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function goalText(goal, includeHint = false) {
|
|
234
|
+
if (!goal || typeof goal !== "object") {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
const status = singleLine(goal.status) || "unknown";
|
|
238
|
+
const objective = clipSingleLine(goal.objective, 96);
|
|
239
|
+
const lines = [`${accent("•")} ${bold("Goal")} · ${status}`];
|
|
240
|
+
if (objective) {
|
|
241
|
+
lines.push(dim(detailLine(objective)));
|
|
242
|
+
}
|
|
243
|
+
if (includeHint && status === "active") {
|
|
244
|
+
lines.push(dim(detailLine("resume manually with /goal resume")));
|
|
245
|
+
}
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function goalCommandText(goal, action = "get") {
|
|
250
|
+
const labels = {
|
|
251
|
+
get: "Goal status",
|
|
252
|
+
set: "Goal started",
|
|
253
|
+
pause: "Goal paused",
|
|
254
|
+
resume: "Goal resumed",
|
|
255
|
+
clear: "Goal cleared",
|
|
256
|
+
};
|
|
257
|
+
const label = labels[action] || "Goal updated";
|
|
258
|
+
if (!goal) {
|
|
259
|
+
return commandResultText(label, "No active goal");
|
|
260
|
+
}
|
|
261
|
+
return commandResultText(label, `${goal.status} · ${clipSingleLine(goal.objective, 80)}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function modelListErrorText(error, currentModel = "") {
|
|
265
|
+
const lines = [`${accent("•")} ${bold("Model list unavailable")}`];
|
|
266
|
+
const current = clipSingleLine(currentModel, 96);
|
|
267
|
+
if (current) {
|
|
268
|
+
lines.push(dim(detailLine(`current: ${current}`)));
|
|
269
|
+
}
|
|
270
|
+
const detail = clipSingleLine(error, 96);
|
|
271
|
+
if (detail) {
|
|
272
|
+
lines.push(dim(detailLine(detail)));
|
|
273
|
+
}
|
|
274
|
+
lines.push(dim(detailLine("use /model set <name> to switch manually")));
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function queuedInputText(text = "") {
|
|
279
|
+
const preview = clipSingleLine(text, 96);
|
|
280
|
+
const lines = [`${accent("•")} ${bold("Queued follow-up")}`];
|
|
281
|
+
if (preview) {
|
|
282
|
+
lines.push(dim(detailLine(preview)));
|
|
283
|
+
}
|
|
284
|
+
lines.push(dim(detailLine("runs after the current turn")));
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function turnCompletedLine(event, tools = { completed: 0, failed: 0 }) {
|
|
289
|
+
const duration = formatDuration(event.duration_ms);
|
|
290
|
+
const summary = toolSummary(tools);
|
|
291
|
+
return summary
|
|
292
|
+
? `${green("─")} ${bold("Worked for")} ${duration} ${dim(`· ${summary}`)}`
|
|
293
|
+
: `${green("─")} ${bold("Worked for")} ${duration}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function interruptText() {
|
|
297
|
+
return `${accent("•")} ${bold("Interrupt requested")}\n${dim(detailLine("ctrl+c again to quit"))}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function cancelledText() {
|
|
301
|
+
return `${accent("•")} ${bold("Interrupted")}\n${dim(detailLine("session preserved; resume with -c"))}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function commandResultText(text, detail = "") {
|
|
305
|
+
const line = `${green("✓")} ${bold(clipSingleLine(text, 96))}`;
|
|
306
|
+
const extra = clipSingleLine(detail, 96);
|
|
307
|
+
return extra ? `${line}\n${dim(detailLine(extra))}` : line;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function modelUsageText() {
|
|
311
|
+
return `${accent("•")} ${bold("Model command")}\n${dim(detailLine("/model set <name>"))}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function contextBuiltLine(event) {
|
|
315
|
+
const decisions = event.decisions && typeof event.decisions === "object" ? event.decisions : {};
|
|
316
|
+
if (!decisions.rind_docs_truncated) {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
const scopes = Array.isArray(decisions.rind_docs_truncated_scopes)
|
|
320
|
+
? decisions.rind_docs_truncated_scopes.join(", ")
|
|
321
|
+
: "unknown";
|
|
322
|
+
return `${accent("•")} ${bold("Context trimmed")}\n${dim(detailLine(`RIND.md: ${clipSingleLine(scopes, 96)}`))}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function unknownCommandText() {
|
|
326
|
+
return `${accent("•")} ${bold("Unknown command")}\n${dim(detailLine("type / to browse commands or ? for shortcuts"))}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function toolRequestedLine(event) {
|
|
330
|
+
const name = event.tool_name || "unknown";
|
|
331
|
+
const detail = toolDetail(name, parseJsonObject(event.args_preview));
|
|
332
|
+
const label = toolLabel(name);
|
|
333
|
+
const line = `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${label}`;
|
|
334
|
+
return detail ? `${line}\n${dim(toolDetailLine(name, detail))}` : line;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function toolStartedLine(event) {
|
|
338
|
+
const name = event.tool_name || "tool";
|
|
339
|
+
return `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${toolLabel(name)}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function toolResultLine(event, fileChange) {
|
|
343
|
+
const name = event.tool_name || "unknown";
|
|
344
|
+
const label = toolLabel(name);
|
|
345
|
+
const duration = formatDuration(event.duration_ms);
|
|
346
|
+
if (event.status === "failed") {
|
|
347
|
+
const suffix = event.error_type ? ` (${event.error_type})` : "";
|
|
348
|
+
const detail = toolErrorDetail(event.result);
|
|
349
|
+
const line = `${red("⊘")} ${bold("Tool")} ${dim("·")} ${label} failed in ${duration}${suffix}`;
|
|
350
|
+
return detail ? `${line}\n${dim(detailLine(detail))}` : line;
|
|
351
|
+
}
|
|
352
|
+
const result = toolResultSummary(event.result);
|
|
353
|
+
if (result.status === "running" && (name === "bash" || name === "bash_output")) {
|
|
354
|
+
const runningText = name === "bash_output"
|
|
355
|
+
? "command output read; command still running in background"
|
|
356
|
+
: "command running in background";
|
|
357
|
+
const line = `${accent("◌")} ${bold("Tool")} ${dim("·")} ${runningText} in ${duration}`;
|
|
358
|
+
const output = result.output;
|
|
359
|
+
return [line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
|
|
360
|
+
.filter(Boolean)
|
|
361
|
+
.join("\n");
|
|
362
|
+
}
|
|
363
|
+
const line = result.exitCode
|
|
364
|
+
? `${red("⊘")} ${bold("Tool")} ${dim("·")} ${label} exited ${result.exitCode} in ${duration}`
|
|
365
|
+
: `${green("◉")} ${bold("Tool")} ${dim("·")} ${completedToolText(name, label)} in ${duration}`;
|
|
366
|
+
const output = result.output;
|
|
367
|
+
return [line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.join("\n");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function planUpdatedLine(plan) {
|
|
373
|
+
const items = Array.isArray(plan) ? plan : [];
|
|
374
|
+
if (!items.length) {
|
|
375
|
+
return `${green("◉")} ${bold("Plan cleared")}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const lines = [`${green("◉")} ${bold("Plan updated")}`];
|
|
379
|
+
for (const item of items) {
|
|
380
|
+
const step = clipSingleLine(item?.step, detailTextWidth());
|
|
381
|
+
if (step) {
|
|
382
|
+
lines.push(` ${planStatusIcon(item?.status)} ${step}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return lines.join("\n");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function toolProgressLine(event) {
|
|
389
|
+
const name = event.tool_name || "tool";
|
|
390
|
+
const message = progressMessage(event.payload);
|
|
391
|
+
return message ? `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolLabel(name)}\n${dim(` ↳ ${message}`)}` : "";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function errorLine(error) {
|
|
395
|
+
const detail = clipSingleLine(error, 120);
|
|
396
|
+
return detail
|
|
397
|
+
? `${red("⊘")} ${bold("Turn failed")}\n${dim(detailLine(detail))}`
|
|
398
|
+
: `${red("⊘")} ${bold("Turn failed")}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function questionText(event = {}) {
|
|
402
|
+
return [
|
|
403
|
+
`${accent("•")} ${bold("Choice required")}`,
|
|
404
|
+
"",
|
|
405
|
+
` ${clipSingleLine(event.question || "Input required", 76)}`,
|
|
406
|
+
].join("\n");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function toolDetail(name, args) {
|
|
410
|
+
if (name === "bash") {
|
|
411
|
+
return clipSingleLine(args.command, 96);
|
|
412
|
+
}
|
|
413
|
+
if (name === "bash_output") {
|
|
414
|
+
const bgId = clipSingleLine(args.bg_id, 96);
|
|
415
|
+
return bgId ? `bg ${bgId}` : "";
|
|
416
|
+
}
|
|
417
|
+
for (const key of ["file_path", "path", "query", "url"]) {
|
|
418
|
+
const value = clipSingleLine(args[key], 96);
|
|
419
|
+
if (value) {
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return "";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function commandDeckText(commands) {
|
|
427
|
+
const items = Array.isArray(commands) ? commands : [];
|
|
428
|
+
if (!items.length) {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
const names = items.map((command) => `/${clipSingleLine(command?.name, 22)}`);
|
|
432
|
+
const lines = [];
|
|
433
|
+
for (let index = 0; index < names.length; index += 4) {
|
|
434
|
+
const row = names.slice(index, index + 4).map((name) => padRight(name, 14)).join(" ").trimEnd();
|
|
435
|
+
lines.push(dim(` ${row}`));
|
|
436
|
+
}
|
|
437
|
+
return lines;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function slashHelpText(display, commands) {
|
|
441
|
+
const command = display.command && typeof display.command === "object" ? display.command : null;
|
|
442
|
+
if (command) {
|
|
443
|
+
const lines = [`${accent("•")} ${bold(`/${clipSingleLine(command.name, 32)}`)}`];
|
|
444
|
+
const description = clipSingleLine(command.description, slashContentWidth());
|
|
445
|
+
if (description) {
|
|
446
|
+
lines.push(slashDetailLine(description));
|
|
447
|
+
}
|
|
448
|
+
const usage = clipSingleLine(command.usage || `/${command.name}`, slashContentWidth());
|
|
449
|
+
if (usage) {
|
|
450
|
+
lines.push(slashDetailLine(`usage: ${usage}`));
|
|
451
|
+
}
|
|
452
|
+
const aliases = slashAliases(command.aliases);
|
|
453
|
+
if (aliases) {
|
|
454
|
+
lines.push(slashDetailLine(`aliases: ${aliases}`));
|
|
455
|
+
}
|
|
456
|
+
return lines.join("\n");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const items = Array.isArray(display.commands) && display.commands.length ? display.commands : commands;
|
|
460
|
+
const lines = [`${accent("•")} ${bold("Commands")}`];
|
|
461
|
+
for (const item of items) {
|
|
462
|
+
if (!item || typeof item !== "object") {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const name = `/${clipSingleLine(item.name, 22)}`;
|
|
466
|
+
const description = clipSingleLine(item.description, slashDescriptionWidth());
|
|
467
|
+
lines.push(` ${padRight(name, 16)} ${dim(description)}`.trimEnd());
|
|
468
|
+
}
|
|
469
|
+
lines.push(slashDetailLine("use /help <command> for usage"));
|
|
470
|
+
return lines.join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function slashStatusText(display) {
|
|
474
|
+
const lines = [`${accent("•")} ${bold("Status")}`];
|
|
475
|
+
lines.push(slashDetailLine(`session ${clipSingleLine(display.session, 42)}`));
|
|
476
|
+
lines.push(slashDetailLine(`model ${clipSingleLine(display.model, 48)}`));
|
|
477
|
+
lines.push(slashDetailLine(`messages ${singleLine(display.messages) || "unknown"} · debug ${display.debug ? "on" : "off"}`));
|
|
478
|
+
const git = display.git && typeof display.git === "object" ? display.git : null;
|
|
479
|
+
if (git) {
|
|
480
|
+
const state = git.dirty ? "dirty" : "clean";
|
|
481
|
+
lines.push(slashDetailLine(`git ${clipSingleLine(git.branch, 48)} · ${state}`));
|
|
482
|
+
}
|
|
483
|
+
for (const usage of Array.isArray(display.usage) ? display.usage : []) {
|
|
484
|
+
lines.push("");
|
|
485
|
+
lines.push(`${accent("•")} ${bold(clipSingleLine(usage.label || "Usage", 48))}`);
|
|
486
|
+
const input = usage.context_window_tokens > 0
|
|
487
|
+
? `${formatCount(usage.input_tokens)} / ${formatCount(usage.context_window_tokens)}`
|
|
488
|
+
: formatCount(usage.input_tokens);
|
|
489
|
+
lines.push(slashDetailLine(`input ${input} · ${formatPercent(usage.context_usage_percent)}`));
|
|
490
|
+
lines.push(slashDetailLine(`cached ${formatCount(usage.cached_input_tokens)} · ${formatPercent(usage.cache_hit_rate)}`));
|
|
491
|
+
lines.push(slashDetailLine(`output ${formatCount(usage.output_tokens)}`));
|
|
492
|
+
}
|
|
493
|
+
return lines.join("\n");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function slashDoctorText(display) {
|
|
497
|
+
const failures = Number(display.failures || 0);
|
|
498
|
+
const warnings = Number(display.warnings || 0);
|
|
499
|
+
const summary = failures || warnings
|
|
500
|
+
? `${failures} fail · ${warnings} warn`
|
|
501
|
+
: "all checks passed";
|
|
502
|
+
const lines = [`${accent("•")} ${bold("Doctor")} ${dim(`· ${summary}`)}`];
|
|
503
|
+
for (const check of Array.isArray(display.checks) ? display.checks : []) {
|
|
504
|
+
if (!check || typeof check !== "object") {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const status = singleLine(check.status).toLowerCase();
|
|
508
|
+
const marker = doctorMarker(status);
|
|
509
|
+
const name = clipSingleLine(check.name, slashDoctorNameWidth());
|
|
510
|
+
const detail = clipSingleLine(check.detail, slashDoctorDetailWidth(name));
|
|
511
|
+
lines.push(` ${marker} ${padRight(status || "unknown", 5)} ${name}${detail ? dim(` · ${detail}`) : ""}`);
|
|
512
|
+
}
|
|
513
|
+
const nextSteps = Array.isArray(display.next_steps) ? display.next_steps : [];
|
|
514
|
+
if (nextSteps.length) {
|
|
515
|
+
lines.push("");
|
|
516
|
+
lines.push(`${accent("•")} ${bold("Next steps")}`);
|
|
517
|
+
for (const step of nextSteps) {
|
|
518
|
+
lines.push(slashDetailLine(step));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return lines.join("\n");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function slashSessionsText(display) {
|
|
525
|
+
const sessions = Array.isArray(display.sessions) ? display.sessions : [];
|
|
526
|
+
const lines = [`${accent("•")} ${bold("Recent sessions")}`];
|
|
527
|
+
if (!sessions.length) {
|
|
528
|
+
lines.push(slashDetailLine("no recent sessions"));
|
|
529
|
+
}
|
|
530
|
+
for (const session of sessions) {
|
|
531
|
+
if (!session || typeof session !== "object") {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const marker = session.current ? accent("›") : dim("·");
|
|
535
|
+
const current = session.current ? dim(" current") : "";
|
|
536
|
+
const id = middleClip(session.id, 32);
|
|
537
|
+
const updated = clipSingleLine(session.updated_at, 28);
|
|
538
|
+
lines.push(` ${marker} ${id}${current}${updated ? dim(` · ${updated}`) : ""}`);
|
|
539
|
+
const title = clipSingleLine(session.title, slashContentWidth());
|
|
540
|
+
const size = sessionSizeText(session);
|
|
541
|
+
lines.push(slashDetailLine([title, size].filter(Boolean).join(" · ")));
|
|
542
|
+
const preview = clipSingleLine(session.preview, slashContentWidth());
|
|
543
|
+
if (preview) {
|
|
544
|
+
lines.push(slashDetailLine(preview));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const resume = clipSingleLine(display.resume_command, slashContentWidth());
|
|
548
|
+
if (resume) {
|
|
549
|
+
lines.push(slashDetailLine(`resume: ${resume}`));
|
|
550
|
+
}
|
|
551
|
+
return lines.join("\n");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function slashSkillsText(display) {
|
|
555
|
+
const skills = Array.isArray(display.skills) ? display.skills : [];
|
|
556
|
+
const lines = [`${accent("•")} ${bold("Skills")}`];
|
|
557
|
+
if (!skills.length) {
|
|
558
|
+
lines.push(slashDetailLine("no skills found"));
|
|
559
|
+
}
|
|
560
|
+
for (const skill of skills) {
|
|
561
|
+
if (!skill || typeof skill !== "object") {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const name = clipSingleLine(skill.name, 30);
|
|
565
|
+
const scope = clipSingleLine(skill.scope, 18);
|
|
566
|
+
const description = clipSingleLine(skill.description, slashDescriptionWidth());
|
|
567
|
+
lines.push(` ${dim("·")} ${bold(name)}${scope ? dim(` [${scope}]`) : ""}${description ? dim(` ${description}`) : ""}`);
|
|
568
|
+
const path = middleClip(skill.path, slashContentWidth());
|
|
569
|
+
if (path) {
|
|
570
|
+
lines.push(slashDetailLine(path));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return lines.join("\n");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function sessionSizeText(session) {
|
|
577
|
+
const messages = optionalNonnegativeNumber(session.messages);
|
|
578
|
+
const tools = optionalNonnegativeNumber(session.tool_calls);
|
|
579
|
+
if (messages === null || tools === null) {
|
|
580
|
+
return "unknown size";
|
|
581
|
+
}
|
|
582
|
+
return `${formatCount(messages)} msg, ${formatCount(tools)} tool`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function optionalNonnegativeNumber(value) {
|
|
586
|
+
const number = Number(value);
|
|
587
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function slashConfigText(display) {
|
|
591
|
+
const lines = [`${accent("•")} ${bold("Config")}`];
|
|
592
|
+
for (const entry of Array.isArray(display.entries) ? display.entries : []) {
|
|
593
|
+
if (!entry || typeof entry !== "object") {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const label = clipSingleLine(entry.label, 22);
|
|
597
|
+
const rawValue = entry.label === "settings"
|
|
598
|
+
? middleClip(entry.value, Math.max(18, slashContentWidth() - visibleLength(label) - 4))
|
|
599
|
+
: clipSingleLine(entry.value, Math.max(18, slashContentWidth() - visibleLength(label) - 4));
|
|
600
|
+
const state = entry.state ? ` (${clipSingleLine(entry.state, 18)})` : "";
|
|
601
|
+
lines.push(slashDetailLine(`${label}: ${rawValue}${state}`));
|
|
602
|
+
}
|
|
603
|
+
return lines.join("\n");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function slashAliases(value) {
|
|
607
|
+
return Array.isArray(value) ? value.map((alias) => `/${clipSingleLine(alias, 18)}`).join(", ") : "";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function slashDetailLine(value) {
|
|
611
|
+
return dim(detailLine(clipSingleLine(value, slashContentWidth())));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function slashContentWidth() {
|
|
615
|
+
const columns = Number(process.stdout.columns);
|
|
616
|
+
if (!Number.isFinite(columns) || columns <= 0) {
|
|
617
|
+
return 96;
|
|
618
|
+
}
|
|
619
|
+
return Math.max(28, Math.min(96, columns - 6));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function slashDescriptionWidth() {
|
|
623
|
+
return Math.max(18, slashContentWidth() - 20);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function slashDoctorNameWidth() {
|
|
627
|
+
return Math.max(10, Math.min(28, slashContentWidth() - 18));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function slashDoctorDetailWidth(name) {
|
|
631
|
+
return Math.max(12, slashContentWidth() - visibleLength(name) - 12);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function doctorMarker(status) {
|
|
635
|
+
if (status === "ok") {
|
|
636
|
+
return green("✓");
|
|
637
|
+
}
|
|
638
|
+
if (status === "fail") {
|
|
639
|
+
return red("⊘");
|
|
640
|
+
}
|
|
641
|
+
return accent("!");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function menuWindow(items, selectedIndex) {
|
|
645
|
+
const entries = Array.isArray(items) ? items : [];
|
|
646
|
+
const total = entries.length;
|
|
647
|
+
if (!total) {
|
|
648
|
+
return { items: [], activeIndex: 0, start: 0, total: 0 };
|
|
649
|
+
}
|
|
650
|
+
const limit = 8;
|
|
651
|
+
const selected = Math.max(0, Math.min(total - 1, Number(selectedIndex) || 0));
|
|
652
|
+
const start = total <= limit ? 0 : Math.min(Math.max(0, selected - 3), total - limit);
|
|
653
|
+
return {
|
|
654
|
+
items: entries.slice(start, start + limit),
|
|
655
|
+
activeIndex: selected - start,
|
|
656
|
+
start,
|
|
657
|
+
total,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function slashMenuTitle(visible) {
|
|
662
|
+
if (visible.total <= visible.items.length) {
|
|
663
|
+
return " Command deck";
|
|
664
|
+
}
|
|
665
|
+
return ` Command deck ${visible.start + 1}-${visible.start + visible.items.length}/${visible.total}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function modelMenuTitle(visible) {
|
|
669
|
+
if (visible.total <= visible.items.length) {
|
|
670
|
+
return " Model deck";
|
|
671
|
+
}
|
|
672
|
+
return ` Model deck ${visible.start + 1}-${visible.start + visible.items.length}/${visible.total}`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function toolDetailLine(name, detail) {
|
|
676
|
+
return name === "bash" ? ` $ ${detail}` : ` ↳ ${detail}`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function detailLine(text) {
|
|
680
|
+
return ` ↳ ${text}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function toolLabel(name) {
|
|
684
|
+
if (name === "bash") {
|
|
685
|
+
return "command";
|
|
686
|
+
}
|
|
687
|
+
if (name === "bash_output") {
|
|
688
|
+
return "command output";
|
|
689
|
+
}
|
|
690
|
+
const labels = {
|
|
691
|
+
edit_file: "file edit",
|
|
692
|
+
read_file: "file read",
|
|
693
|
+
search_files: "file search",
|
|
694
|
+
view_image: "image",
|
|
695
|
+
web_search: "web search",
|
|
696
|
+
};
|
|
697
|
+
return labels[name] || humanToolName(name);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function toolActiveVerb(name) {
|
|
701
|
+
if (name === "bash") {
|
|
702
|
+
return "Running";
|
|
703
|
+
}
|
|
704
|
+
if (name === "bash_output") {
|
|
705
|
+
return "Reading";
|
|
706
|
+
}
|
|
707
|
+
return "Calling";
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function completedToolText(name, label) {
|
|
711
|
+
if (name === "bash") {
|
|
712
|
+
return `Ran ${label}`;
|
|
713
|
+
}
|
|
714
|
+
if (name === "bash_output") {
|
|
715
|
+
return `Read ${label}`;
|
|
716
|
+
}
|
|
717
|
+
return `Called ${label}`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function humanToolName(name) {
|
|
721
|
+
return singleLine(name).replace(/[_-]+/g, " ") || "tool";
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function parseJsonObject(value) {
|
|
725
|
+
try {
|
|
726
|
+
const parsed = JSON.parse(String(value || ""));
|
|
727
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
728
|
+
} catch {
|
|
729
|
+
return {};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function toolErrorDetail(result) {
|
|
734
|
+
const payload = parseJsonObject(result);
|
|
735
|
+
return clipSingleLine(payload.error, 120);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function toolResultSummary(result) {
|
|
739
|
+
const payload = parseJsonObject(result);
|
|
740
|
+
const data = payload.data && typeof payload.data === "object" ? payload.data : {};
|
|
741
|
+
return {
|
|
742
|
+
status: singleLine(data.status).toLowerCase(),
|
|
743
|
+
exitCode: nonZeroExitCode(data.exit_code),
|
|
744
|
+
output: clipSingleLine(data.stdout || data.stderr || data.message, 120),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function nonZeroExitCode(value) {
|
|
749
|
+
const code = Number(value);
|
|
750
|
+
return Number.isInteger(code) && code !== 0 ? code : 0;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function progressMessage(payload) {
|
|
754
|
+
if (!payload || typeof payload !== "object") {
|
|
755
|
+
return "";
|
|
756
|
+
}
|
|
757
|
+
for (const key of ["message", "status", "text"]) {
|
|
758
|
+
const value = clipSingleLine(payload[key], 120);
|
|
759
|
+
if (value) {
|
|
760
|
+
return value;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return "";
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function fileChangeLine(fileChange) {
|
|
767
|
+
if (!fileChange || typeof fileChange !== "object") {
|
|
768
|
+
return "";
|
|
769
|
+
}
|
|
770
|
+
const changes = Array.isArray(fileChange.lines)
|
|
771
|
+
? fileChange.lines.filter((line) => line?.kind === "added" || line?.kind === "removed")
|
|
772
|
+
: [];
|
|
773
|
+
if (!changes.length) {
|
|
774
|
+
return "";
|
|
775
|
+
}
|
|
776
|
+
const path = middleClip(fileChange.file_path, fileChangePathWidth());
|
|
777
|
+
const shown = changes.slice(0, MAX_FILE_CHANGE_LINES);
|
|
778
|
+
const lines = [`${dim(" ↳")} ${path}`];
|
|
779
|
+
for (const change of shown) {
|
|
780
|
+
lines.push(fileChangeDiffLine(change));
|
|
781
|
+
}
|
|
782
|
+
const hidden = changes.length - shown.length;
|
|
783
|
+
if (hidden > 0) {
|
|
784
|
+
lines.push(dim(` … ${hidden} more changed lines`));
|
|
785
|
+
}
|
|
786
|
+
return lines.join("\n");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function fileChangeDiffLine(change) {
|
|
790
|
+
const added = change.kind === "added";
|
|
791
|
+
const marker = added ? "+" : "-";
|
|
792
|
+
const style = added ? green : red;
|
|
793
|
+
return `${dim(` ${marker} `)}${style(clipCells(change.text, detailTextWidth()))}`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function fileChangePathWidth() {
|
|
797
|
+
const columns = Number(process.stdout.columns);
|
|
798
|
+
if (!Number.isFinite(columns) || columns <= 0) {
|
|
799
|
+
return 48;
|
|
800
|
+
}
|
|
801
|
+
return Math.max(18, Math.min(48, columns - 32));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function detailTextWidth() {
|
|
805
|
+
const columns = Number(process.stdout.columns);
|
|
806
|
+
if (!Number.isFinite(columns) || columns <= 0) {
|
|
807
|
+
return 96;
|
|
808
|
+
}
|
|
809
|
+
return Math.max(12, Math.min(96, columns - 6));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function planStatusIcon(status) {
|
|
813
|
+
switch (status) {
|
|
814
|
+
case "in_progress":
|
|
815
|
+
return accent("◐");
|
|
816
|
+
case "completed":
|
|
817
|
+
return green("●");
|
|
818
|
+
case "cancelled":
|
|
819
|
+
return dim("⊖");
|
|
820
|
+
default:
|
|
821
|
+
return dim("○");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function clipSingleLine(value, maxLength) {
|
|
826
|
+
const text = singleLine(value);
|
|
827
|
+
return clipCells(text, maxLength);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function middleClip(value, maxLength) {
|
|
831
|
+
const text = singleLine(value);
|
|
832
|
+
return middleClipCells(text, maxLength);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function singleLine(value) {
|
|
836
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function messageLines(value) {
|
|
840
|
+
const text = String(value || "").replace(/\r/g, "").trim();
|
|
841
|
+
return text ? text.split("\n").map((line) => line.trimEnd()) : [];
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function userInputContentWidth() {
|
|
845
|
+
const columns = Number(process.stdout.columns);
|
|
846
|
+
return Number.isFinite(columns) && columns > 0 ? Math.max(1, Math.floor(columns - 2)) : 78;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function formatDuration(durationMs) {
|
|
850
|
+
const value = Number(durationMs || 0);
|
|
851
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
852
|
+
return "0ms";
|
|
853
|
+
}
|
|
854
|
+
if (value < 1000) {
|
|
855
|
+
return `${Math.trunc(value)}ms`;
|
|
856
|
+
}
|
|
857
|
+
if (value >= 60000) {
|
|
858
|
+
const totalSeconds = Math.round(value / 1000);
|
|
859
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
860
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
861
|
+
return `${minutes}m ${seconds}s`;
|
|
862
|
+
}
|
|
863
|
+
return `${(value / 1000).toFixed(2)}s`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function formatActivityDuration(durationMs) {
|
|
867
|
+
const value = Math.max(0, Number(durationMs || 0));
|
|
868
|
+
if (!Number.isFinite(value) || value < 60000) {
|
|
869
|
+
return `${Math.floor(value / 1000)}s`;
|
|
870
|
+
}
|
|
871
|
+
const totalSeconds = Math.floor(value / 1000);
|
|
872
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
873
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
874
|
+
return `${minutes}m ${seconds}s`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function toolSummary(tools) {
|
|
878
|
+
const completed = Number(tools.completed || 0);
|
|
879
|
+
const failed = Number(tools.failed || 0);
|
|
880
|
+
const parts = [];
|
|
881
|
+
if (completed > 0) {
|
|
882
|
+
parts.push(`${completed} completed`);
|
|
883
|
+
}
|
|
884
|
+
if (failed > 0) {
|
|
885
|
+
parts.push(`${failed} failed`);
|
|
886
|
+
}
|
|
887
|
+
return parts.join(", ");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function formatCount(value) {
|
|
891
|
+
const number = Number(value || 0);
|
|
892
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
893
|
+
return "0";
|
|
894
|
+
}
|
|
895
|
+
if (Math.abs(number) >= 1000) {
|
|
896
|
+
return `${(number / 1000).toFixed(1)}k`;
|
|
897
|
+
}
|
|
898
|
+
return String(Math.trunc(number));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function formatPercent(value) {
|
|
902
|
+
const number = Number(value);
|
|
903
|
+
if (!Number.isFinite(number)) {
|
|
904
|
+
return "0.0%";
|
|
905
|
+
}
|
|
906
|
+
return `${(number * 100).toFixed(1)}%`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function resumePreviewText(value) {
|
|
910
|
+
const lines = String(value || "").trim().split(/\r?\n/).filter(Boolean);
|
|
911
|
+
return lines.map(resumePreviewLine).join("\n");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function resumePreviewLine(line) {
|
|
915
|
+
const message = line.match(/^-?\s*(user|assistant):\s*(.*)$/i);
|
|
916
|
+
if (message) {
|
|
917
|
+
const role = message[1].toLowerCase();
|
|
918
|
+
const marker = role === "user" ? accent("▷") : dim("◁");
|
|
919
|
+
const label = role === "user" ? "You" : "Assistant";
|
|
920
|
+
return `${marker} ${label} ${dim("·")} ${clipSingleLine(message[2], 72)}`;
|
|
921
|
+
}
|
|
922
|
+
return dim(` ${clipSingleLine(line, 78)}`);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function startupBannerText(info) {
|
|
926
|
+
const width = startupBannerWidth();
|
|
927
|
+
const modelLine = `model ${singleLine(info.model) || "unknown"} · session ${singleLine(info.session_id) || "unknown"}`;
|
|
928
|
+
const cwd = middleClip(info.cwd || process.cwd(), width - 4);
|
|
929
|
+
return [
|
|
930
|
+
startupBannerBorder("┌", "┐", width),
|
|
931
|
+
startupBannerLine(`${bold("Rind")} ${dim("workbench online")}`, width),
|
|
932
|
+
startupBannerLine(modelLine, width),
|
|
933
|
+
startupBannerLine(cwd, width),
|
|
934
|
+
startupBannerBorder("└", "┘", width),
|
|
935
|
+
].join("\n");
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function startupBannerBorder(left, right, width) {
|
|
939
|
+
return dim(`${left}${"─".repeat(width - 2)}${right}`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function startupBannerLine(text, frameWidth) {
|
|
943
|
+
const clean = String(text || "").replace(/[\r\n\t]+/g, " ").trimEnd();
|
|
944
|
+
const width = frameWidth - 4;
|
|
945
|
+
const content =
|
|
946
|
+
visibleLength(clean) <= width
|
|
947
|
+
? clean
|
|
948
|
+
: clipCells(clean, width);
|
|
949
|
+
return `${dim("│")} ${padRight(content, width)} ${dim("│")}`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function startupBannerWidth() {
|
|
953
|
+
const columns = Number(process.stdout.columns);
|
|
954
|
+
if (!Number.isFinite(columns) || columns <= 0) {
|
|
955
|
+
return MAX_STARTUP_BANNER_WIDTH;
|
|
956
|
+
}
|
|
957
|
+
return Math.max(44, Math.min(MAX_STARTUP_BANNER_WIDTH, columns - 2));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function helpRow(leftKey, leftText, rightKey, rightText) {
|
|
961
|
+
const left = `${padRight(leftKey, 12)} ${leftText}`;
|
|
962
|
+
const right = `${padRight(rightKey, 14)} ${rightText}`;
|
|
963
|
+
return dim(` ${padRight(left, 33)} ${right}`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function inputPromptFrame(header = "", state = {}) {
|
|
967
|
+
const lines = [""];
|
|
968
|
+
const activity = promptActivityLine(state);
|
|
969
|
+
if (activity) {
|
|
970
|
+
lines.push(activity);
|
|
971
|
+
}
|
|
972
|
+
if (header) {
|
|
973
|
+
lines.push(header);
|
|
974
|
+
}
|
|
975
|
+
lines.push(inputDivider());
|
|
976
|
+
lines.push(" ▷ ");
|
|
977
|
+
return lines.join("\n");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function inputDivider() {
|
|
981
|
+
return dim(` ${"─".repeat(composerWidth())}`);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function activityFrame(frame) {
|
|
985
|
+
const frames = ["◐", "◓", "◑", "◒"];
|
|
986
|
+
const index = Math.abs(Number(frame) || 0) % frames.length;
|
|
987
|
+
return frames[index];
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function padRight(text, width) {
|
|
991
|
+
return `${text}${" ".repeat(Math.max(0, width - visibleLength(text)))}`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function visibleLength(text) {
|
|
995
|
+
return textWidth(text);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function promptHeaderLine(info) {
|
|
999
|
+
const backgroundCount = Number(info.background_count);
|
|
1000
|
+
const backgroundHint = backgroundCount > 0
|
|
1001
|
+
? " · " + dim("[bg:" + backgroundCount + "] (ctrl+b monitor)")
|
|
1002
|
+
: "";
|
|
1003
|
+
const model = singleLine(info.model);
|
|
1004
|
+
const cwd = middleClip(info.cwd, 56);
|
|
1005
|
+
const width = composerWidth();
|
|
1006
|
+
if (model && cwd) {
|
|
1007
|
+
const separator = " · ";
|
|
1008
|
+
const pathWidth = width - visibleLength(model) - visibleLength(separator) - visibleLength(backgroundHint);
|
|
1009
|
+
if (pathWidth > 0) {
|
|
1010
|
+
return ` ${promptModel(clipSingleLine(model, width))}${dim(separator)}${promptPath(clipSingleLine(cwd, pathWidth))}${backgroundHint}`;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (model) {
|
|
1014
|
+
return ` ${promptModel(clipSingleLine(model, width))}${backgroundHint}`;
|
|
1015
|
+
}
|
|
1016
|
+
return cwd ? ` ${promptPath(clipSingleLine(cwd, width))}${backgroundHint}` : "";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function composerWidth() {
|
|
1020
|
+
const columns = Number(process.stdout.columns);
|
|
1021
|
+
if (!Number.isFinite(columns) || columns <= 0) {
|
|
1022
|
+
return MAX_COMPOSER_WIDTH;
|
|
1023
|
+
}
|
|
1024
|
+
return Math.max(1, Math.min(MAX_COMPOSER_WIDTH, columns - 4));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function styled(text, code) {
|
|
1028
|
+
if (!Boolean(process.stdout.isTTY) || process.env.NO_COLOR) {
|
|
1029
|
+
return text;
|
|
1030
|
+
}
|
|
1031
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function bold(text) {
|
|
1035
|
+
return styled(text, "1");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function dim(text) {
|
|
1039
|
+
return styled(text, "2");
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function accent(text) {
|
|
1043
|
+
return styled(text, "38;5;81");
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function green(text) {
|
|
1047
|
+
return styled(text, "38;5;113");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function red(text) {
|
|
1051
|
+
return styled(text, "38;5;203");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function promptModel(text) {
|
|
1055
|
+
return styled(text, "1;38;5;81");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function promptPath(text) {
|
|
1059
|
+
return styled(text, "38;5;110");
|
|
1060
|
+
}
|