@rind-ai/cli 0.4.1 → 0.6.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.
Files changed (49) hide show
  1. package/bin/rind.js +5 -5
  2. package/lib/assistant-renderer.js +179 -265
  3. package/lib/choice-menu-state.js +46 -46
  4. package/lib/cli-input-actions.js +548 -0
  5. package/lib/cli-output-controller.js +460 -0
  6. package/lib/cli-runtime-controller.js +350 -0
  7. package/lib/cli-state-store.js +32 -0
  8. package/lib/cli-state.js +41 -0
  9. package/lib/command-controller.js +159 -126
  10. package/lib/compact-context-state.js +22 -22
  11. package/lib/components/assistant-message.js +169 -0
  12. package/lib/components/composer-area.js +25 -0
  13. package/lib/components/dynamic-block.js +20 -0
  14. package/lib/components/monitor-stack.js +35 -0
  15. package/lib/components/text-block.js +47 -0
  16. package/lib/components/tool-block.js +122 -0
  17. package/lib/composer-terminal.js +224 -203
  18. package/lib/event-controller.js +243 -242
  19. package/lib/frontend-cli-implementation.js +656 -1111
  20. package/lib/input-controller.js +75 -94
  21. package/lib/input-errors.js +3 -3
  22. package/lib/interrupt-state.js +9 -9
  23. package/lib/line-editor.js +541 -541
  24. package/lib/local-slash-commands.js +217 -0
  25. package/lib/markdown-lines.js +103 -0
  26. package/lib/model-menu-state.js +50 -50
  27. package/lib/one-shot-progress.js +145 -0
  28. package/lib/one-shot.js +228 -0
  29. package/lib/question-menu-state.js +61 -0
  30. package/lib/rendering.js +1309 -1060
  31. package/lib/runtime-client.js +241 -193
  32. package/lib/runtime-env.js +21 -21
  33. package/lib/runtime-protocol.js +122 -15
  34. package/lib/slash-command-mode.js +0 -11
  35. package/lib/slash-menu-state.js +59 -59
  36. package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
  37. package/lib/terminal-key.js +97 -97
  38. package/lib/text-width.js +335 -151
  39. package/lib/theme-menu-state.js +31 -0
  40. package/lib/theme.js +134 -0
  41. package/lib/tool-display.js +675 -0
  42. package/lib/tui/component.js +55 -0
  43. package/lib/tui/cursor.js +29 -0
  44. package/lib/tui/input-buffer.js +172 -0
  45. package/lib/tui/tui.js +591 -0
  46. package/lib/turn-controller.js +68 -78
  47. package/package.json +28 -28
  48. package/lib/assistant-stream-buffer.js +0 -25
  49. package/lib/terminal-ui.js +0 -581
package/lib/rendering.js CHANGED
@@ -1,1060 +1,1309 @@
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
- }
1
+ import { clipCells, middleClipCells, textWidth, wrapTextCells } from "./text-width.js";
2
+ import { paint, flavorSwatch } from "./theme.js";
3
+ import { homedir } from "node:os";
4
+
5
+ const MAX_STARTUP_BANNER_WIDTH = 80;
6
+ const MAX_COMPOSER_WIDTH = 78;
7
+ const MAX_FILE_CHANGE_LINES = 20;
8
+
9
+ export function startupText(info = {}, width) {
10
+ const header = startupBannerText(info, width);
11
+ const goal = goalText(info.goal, true);
12
+ const preview = resumePreviewText(info.resume_preview);
13
+ const sections = [header, goal, preview ? `${accent("◆")} ${bold("Recent context")}\n${preview}` : ""];
14
+ return sections.filter(Boolean).join("\n\n");
15
+ }
16
+
17
+ export function promptText(info = {}, _stats = {}, state = {}, frameWidth) {
18
+ return inputPromptFrame(promptHeaderLine(info, frameWidth), state, frameWidth);
19
+ }
20
+
21
+ export function promptActivityLine(state = {}) {
22
+ if (!state.running) {
23
+ return "";
24
+ }
25
+ const elapsed = formatActivityDuration(state.elapsedMs);
26
+ const label = singleLine(state.label) || "Working";
27
+ return ` ${accent(activityFrame(state.frame))} ${bold(label)} ${dim(`(${elapsed}) ctrl+c interrupt`)}`;
28
+ }
29
+
30
+ export function promptPlaceholderText() {
31
+ return "Ask Rind to do anything";
32
+ }
33
+
34
+ export function userInputText(text, width) {
35
+ const lines = messageLines(text);
36
+ if (!lines.length) {
37
+ return "";
38
+ }
39
+ const contentWidth = userInputContentWidth(width);
40
+ const physicalLines = lines.flatMap((line) => (
41
+ wrapTextCells(line, contentWidth, contentWidth).map((chunk) => ` ${chunk.text}`)
42
+ ));
43
+ return `${accent("▷")} ${bold("You")}\n${physicalLines.join("\n")}`;
44
+ }
45
+
46
+ export function assistantHeaderText() {
47
+ return `${accent("◁")} ${bold("Assistant")}`;
48
+ }
49
+
50
+ export function outputBlockText(text, leading = false) {
51
+ const body = String(text || "").trimEnd();
52
+ return body ? `${leading ? "\n" : ""}${body}\n` : "";
53
+ }
54
+
55
+ export function helpText(commands = []) {
56
+ const lines = [
57
+ sectionRule("Controls"),
58
+ helpRow("enter", "send / steer", "tab", "queue follow-up"),
59
+ helpRow("↑ / ↓", "history", "← / →", "move cursor"),
60
+ helpRow("home / end", "line edges", "del / backspace", "edit text"),
61
+ helpRow("ctrl+c", "interrupt or quit", "?", "show shortcuts"),
62
+ helpRow("ctrl+b", "task monitor", "esc", "close monitor"),
63
+ helpRow("ctrl+o", "toggle tool detail", "", ""),
64
+ ];
65
+ const deckItems = Array.isArray(commands)
66
+ ? commands.filter((item) => item && typeof item === "object")
67
+ : [];
68
+ if (deckItems.length) {
69
+ lines.push("", sectionRule("Commands", `${deckItems.length} available`), ...commandDeckText(deckItems));
70
+ }
71
+ return lines.join("\n");
72
+ }
73
+
74
+ export function slashDisplayText(display, commands = []) {
75
+ if (!display || typeof display !== "object") {
76
+ return "";
77
+ }
78
+ switch (display.type) {
79
+ case "help":
80
+ return slashHelpText(display, commands);
81
+ case "status":
82
+ return slashStatusText(display);
83
+ case "doctor":
84
+ return slashDoctorText(display);
85
+ case "sessions":
86
+ return slashSessionsText(display);
87
+ case "skills":
88
+ return slashSkillsText(display);
89
+ case "config":
90
+ return slashConfigText(display);
91
+ case "theme":
92
+ return slashThemeText(display);
93
+ default:
94
+ return "";
95
+ }
96
+ }
97
+
98
+ export function slashResultText(result, commands = []) {
99
+ if (!result || typeof result !== "object") {
100
+ return "";
101
+ }
102
+ return slashDisplayText(result.display, commands) || String(result.text || "");
103
+ }
104
+
105
+ export function answerPromptText() {
106
+ return `\n ${accent("▷")} `;
107
+ }
108
+
109
+ export function answerPlaceholderText() {
110
+ return "Type your answer";
111
+ }
112
+
113
+ export function inputHintText(placeholder) {
114
+ const text = singleLine(placeholder);
115
+ return text ? dim(text) : "";
116
+ }
117
+
118
+ export function slashMenuText(items, selectedIndex = 0) {
119
+ const visible = menuWindow(items, selectedIndex);
120
+ if (!visible.items.length) {
121
+ return "";
122
+ }
123
+ const lines = [dim(slashMenuTitle(visible))];
124
+ for (const [index, item] of visible.items.entries()) {
125
+ const active = index === visible.activeIndex;
126
+ const marker = active ? accent("›") : dim("·");
127
+ const name = active ? bold(`/${item.name}`) : dim(`/${item.name}`);
128
+ const description = dim(clipSingleLine(item.description, 46));
129
+ lines.push(` ${marker} ${padRight(name, 14)} ${description}`);
130
+ }
131
+ lines.push(dim(" ↑↓ select · enter run · esc close · backspace edit"));
132
+ return `${lines.join("\n")}\n`;
133
+ }
134
+
135
+ export function modelMenuText(items, selectedIndex = 0) {
136
+ const visible = menuWindow(items, selectedIndex);
137
+ if (!visible.items.length) {
138
+ return "";
139
+ }
140
+ const lines = [dim(modelMenuTitle(visible))];
141
+ for (const [index, item] of visible.items.entries()) {
142
+ const active = index === visible.activeIndex;
143
+ const marker = active ? accent("›") : dim("·");
144
+ const name = active ? bold(item.name) : dim(item.name);
145
+ const suffix = item.current ? dim("current") : "";
146
+ lines.push(` ${marker} ${padRight(name, 34)} ${suffix}`.trimEnd());
147
+ }
148
+ lines.push(dim(" ↑↓ select · enter use · esc cancel"));
149
+ return `${lines.join("\n")}\n`;
150
+ }
151
+
152
+ export function themeMenuText(items, selectedIndex = 0) {
153
+ const visible = menuWindow(items, selectedIndex);
154
+ if (!visible.items.length) {
155
+ return "";
156
+ }
157
+ const lines = [dim(" Theme deck")];
158
+ for (const [index, item] of visible.items.entries()) {
159
+ const active = index === visible.activeIndex;
160
+ const marker = active ? accent("›") : dim("·");
161
+ const label = padRight(clipSingleLine(item?.label || item?.name, 16), 12);
162
+ const name = active ? bold(label) : dim(label);
163
+ const suffix = item?.current ? dim("current") : "";
164
+ lines.push(` ${marker} ${name} ${flavorSwatch(item?.name)}${suffix ? ` ${suffix}` : ""}`);
165
+ }
166
+ lines.push(dim(" ↑↓ select · enter use · esc cancel"));
167
+ return `${lines.join("\n")}\n`;
168
+ }
169
+
170
+ export function taskMonitorTabs(page = "background", backgroundCount = 0, delegateCount = 0, width = 76) {
171
+ const background = Math.max(0, Math.floor(Number(backgroundCount) || 0));
172
+ const delegates = Math.max(0, Math.floor(Number(delegateCount) || 0));
173
+ const tabs = [
174
+ { page: "background", label: `Background [${background}]` },
175
+ { page: "delegates", label: `Delegates [${delegates}]` },
176
+ ].map((tab) => tab.page === page
177
+ ? bold(accent(`› ${tab.label}`))
178
+ : dim(` ${tab.label}`));
179
+ const inline = tabs.join(" ");
180
+ return textWidth(inline) <= Math.max(1, Number(width) || 76)
181
+ ? inline
182
+ : tabs.join("\n");
183
+ }
184
+
185
+ export function choiceMenuText(options, selectedIndex = 0) {
186
+ return choiceMenuTextWithTitle(options, selectedIndex, "Choices");
187
+ }
188
+
189
+ export function sessionMenuText(options, selectedIndex = 0) {
190
+ return choiceMenuTextWithTitle(options, selectedIndex, "Sessions");
191
+ }
192
+
193
+ export function questionMenuFrame(
194
+ options,
195
+ selectedIndex = 0,
196
+ customInput = "",
197
+ editing = false,
198
+ customLabel = "Type your own answer",
199
+ width = 76,
200
+ ) {
201
+ const entries = [
202
+ ...(Array.isArray(options) ? options : []),
203
+ { label: customLabel, description: "" },
204
+ ];
205
+ const visible = menuWindow(entries, selectedIndex);
206
+ if (!visible.items.length) {
207
+ return { text: "", cursor: null };
208
+ }
209
+ const lines = [dim(choiceMenuTitle(visible, "Answers"))];
210
+ let cursor = null;
211
+ for (const [index, option] of visible.items.entries()) {
212
+ const active = index === visible.activeIndex;
213
+ const marker = active ? accent("›") : dim("·");
214
+ const isCustom = option.label === customLabel;
215
+ const customText = String(customInput || "");
216
+ const labelLines = isCustom && editing
217
+ ? wrapQuestionLines(customText || `${customLabel}:`, Math.max(1, width - 4))
218
+ : wrapQuestionLines(option.label, Math.max(1, width - 4));
219
+ const labelStyle = isCustom && editing && !customText ? dim : active ? bold : dim;
220
+ let firstPushedLine = -1;
221
+ let lastPushedLine = -1;
222
+ let firstPrefixWidth = 0;
223
+ for (const [lineIndex, labelLine] of labelLines.entries()) {
224
+ const prefix = lineIndex === 0 ? ` ${marker} ` : " ";
225
+ lines.push(`${prefix}${labelStyle(labelLine)}`);
226
+ if (firstPushedLine === -1) {
227
+ firstPushedLine = lines.length - 1;
228
+ firstPrefixWidth = textWidth(prefix);
229
+ }
230
+ lastPushedLine = lines.length - 1;
231
+ }
232
+ if (active && editing && isCustom) {
233
+ const cursorLine = customText ? Math.max(0, lastPushedLine) : Math.max(0, firstPushedLine);
234
+ const cursorColumn = customText
235
+ ? textWidth(lines[cursorLine])
236
+ : firstPrefixWidth;
237
+ cursor = {
238
+ line: cursorLine,
239
+ column: Math.max(0, cursorColumn),
240
+ };
241
+ }
242
+ if (option.description) {
243
+ const descriptionLines = wrapQuestionLines(option.description, Math.max(1, width - 6));
244
+ for (const [lineIndex, descriptionLine] of descriptionLines.entries()) {
245
+ lines.push(dim(`${lineIndex === 0 ? " ↳ " : " "}${descriptionLine}`));
246
+ }
247
+ } else if (option.label === customLabel && !editing) {
248
+ lines.push(dim(" ↳ press Tab to type"));
249
+ }
250
+ }
251
+ lines.push(dim(" ↑↓ select · enter confirm · esc cancel"));
252
+ return { text: `${lines.join("\n")}\n`, cursor };
253
+ }
254
+
255
+ function wrapQuestionLines(value, width) {
256
+ const text = String(value || "").replace(/\r?\n/g, " ").trim();
257
+ return wrapTextCells(text, Math.max(1, width), Math.max(1, width)).map((chunk) => chunk.text);
258
+ }
259
+
260
+ export function backgroundMonitorText(tasks = [], selectedIndex = 0, selectedTask = null, width = 76) {
261
+ const items = Array.isArray(tasks) ? tasks : [];
262
+ const lines = [dim(" ←→ page · ↑↓/j/k select · esc/ctrl+b close")];
263
+ if (!items.length) {
264
+ lines.push(dim(" No background tasks."));
265
+ return lines.join("\n");
266
+ }
267
+ for (const [index, task] of items.entries()) {
268
+ const active = index === selectedIndex;
269
+ const marker = active ? accent("›") : dim("·");
270
+ const status = singleLine(task?.status) || "unknown";
271
+ const bgId = singleLine(task?.bg_id) || "unknown";
272
+ const command = clipSingleLine(task?.command, Math.max(12, width - 34));
273
+ lines.push(` ${marker} ${padRight(bgId, 12)} ${padRight(status, 10)} ${dim(command)}`.trimEnd());
274
+ }
275
+ lines.push("");
276
+ const task = selectedTask || items[selectedIndex];
277
+ if (!task) {
278
+ return lines.join("\n");
279
+ }
280
+ const heading = `${singleLine(task.bg_id) || "unknown"} · ${singleLine(task.status) || "unknown"}`;
281
+ lines.push(dim(` ${heading}`));
282
+ const rawOutput = [task.stdout, task.stderr]
283
+ .filter((value) => String(value || ""))
284
+ .join("\n")
285
+ const visibleOutput = rawOutput ? rawOutput.split(/\r?\n/).slice(-18) : [];
286
+ if (!rawOutput) {
287
+ lines.push(dim(" (no output)"));
288
+ } else {
289
+ lines.push(...visibleOutput.map((line) => ` ${clipSingleLine(line, width)}`));
290
+ }
291
+ if (task.truncated) {
292
+ lines.push(dim(" output truncated"));
293
+ }
294
+ return lines.join("\n");
295
+ }
296
+
297
+ export function delegateMonitorText(delegates = [], selectedIndex = 0, selectedDelegate = null, width = 76) {
298
+ const items = Array.isArray(delegates) ? delegates : [];
299
+ const lines = [dim(" ←→ page · ↑↓/j/k select · esc/ctrl+b close")];
300
+ if (!items.length) {
301
+ lines.push(dim(" No delegates."));
302
+ return lines.join("\n");
303
+ }
304
+ for (const [index, delegate] of items.entries()) {
305
+ const active = index === selectedIndex;
306
+ const marker = active ? accent("›") : dim("·");
307
+ const agent = padRight(clipSingleLine(delegate?.agent_id, 28), 28);
308
+ const status = padRight(clipSingleLine(delegate?.status, 10), 10);
309
+ const task = clipSingleLine(delegate?.task, Math.max(12, width - 44));
310
+ lines.push(` ${marker} ${agent} ${status} ${dim(task)}`.trimEnd());
311
+ }
312
+ lines.push("");
313
+ const delegate = selectedDelegate || items[selectedIndex];
314
+ if (!delegate) {
315
+ return lines.join("\n");
316
+ }
317
+ const heading = `${singleLine(delegate.agent_id) || "unknown"} · ${singleLine(delegate.status) || "unknown"}`;
318
+ lines.push(dim(` ${heading}`));
319
+ const task = clipSingleLine(delegate.task, width);
320
+ if (task) {
321
+ lines.push(dim(` task: ${task}`));
322
+ }
323
+ const summary = clipSingleLine(delegate.summary, width);
324
+ if (summary) {
325
+ lines.push(dim(` ↳ ${summary}`));
326
+ }
327
+ return lines.join("\n");
328
+ }
329
+
330
+ function choiceMenuTextWithTitle(options, selectedIndex = 0, title = "Choices") {
331
+ const visible = menuWindow(options, selectedIndex);
332
+ if (!visible.items.length) {
333
+ return "";
334
+ }
335
+ const lines = [dim(choiceMenuTitle(visible, title))];
336
+ for (const [index, option] of visible.items.entries()) {
337
+ const active = index === visible.activeIndex;
338
+ const marker = active ? accent("") : dim("·");
339
+ const label = clipSingleLine(option, 60);
340
+ const name = active ? bold(label) : dim(label);
341
+ lines.push(` ${marker} ${name}`);
342
+ }
343
+ lines.push(dim(" ↑↓ select · enter confirm · esc cancel"));
344
+ return `${lines.join("\n")}\n`;
345
+ }
346
+
347
+ function choiceMenuTitle(visible, title = "Choices") {
348
+ if (visible.total <= visible.items.length) {
349
+ return ` ${title}`;
350
+ }
351
+ return ` ${title} ${visible.start + 1}-${visible.start + visible.items.length}/${visible.total}`;
352
+ }
353
+
354
+ export function sessionSwitchedText(info = {}) {
355
+ const sessionId = singleLine(info.session_id) || "unknown";
356
+ const model = singleLine(info.model);
357
+ const goal = goalText(info.goal, true);
358
+ const preview = resumePreviewText(info.resume_preview);
359
+ const lines = [startupBannerText(info), "", `${green("✓")} ${bold("Session switched")}`];
360
+ lines.push(dim(` session ${sessionId}`));
361
+ if (model) {
362
+ lines.push(dim(` model ${model}`));
363
+ }
364
+ if (goal) {
365
+ lines.push("", goal);
366
+ }
367
+ if (preview) {
368
+ lines.push("", `${accent("◆")} ${bold("Recent context")}`, preview);
369
+ }
370
+ return lines.join("\n");
371
+ }
372
+
373
+ export function goalText(goal, includeHint = false) {
374
+ if (!goal || typeof goal !== "object") {
375
+ return "";
376
+ }
377
+ const status = singleLine(goal.status) || "unknown";
378
+ const objective = clipSingleLine(goal.objective, 96);
379
+ const lines = [`${accent("◆")} ${bold("Goal")} ${dim(`· ${status}`)}`];
380
+ if (objective) {
381
+ lines.push(dim(` ${objective}`));
382
+ }
383
+ if (includeHint && status === "active") {
384
+ lines.push(dim(" resume manually with /goal resume"));
385
+ }
386
+ return lines.join("\n");
387
+ }
388
+
389
+ export function goalCommandText(goal, action = "get") {
390
+ const labels = {
391
+ get: "Goal status",
392
+ set: "Goal started",
393
+ pause: "Goal paused",
394
+ resume: "Goal resumed",
395
+ clear: "Goal cleared",
396
+ };
397
+ const label = labels[action] || "Goal updated";
398
+ if (!goal) {
399
+ return commandResultText(label, "No active goal");
400
+ }
401
+ return commandResultText(label, `${goal.status} · ${clipSingleLine(goal.objective, 80)}`);
402
+ }
403
+
404
+ export function modelListErrorText(error, currentModel = "") {
405
+ const current = clipSingleLine(currentModel, 96);
406
+ const detail = clipSingleLine(error, 96);
407
+ return notice(
408
+ "Model list unavailable",
409
+ current ? `current: ${current}` : "",
410
+ detail,
411
+ "use /model set <name> to switch manually",
412
+ );
413
+ }
414
+
415
+ export function turnCompletedLine(event, tools = { completed: 0, failed: 0 }) {
416
+ const duration = formatDuration(event.duration_ms);
417
+ const summary = toolSummary(tools);
418
+ return summary
419
+ ? `${green("─")} ${bold("Worked for")} ${duration} ${dim(`· ${summary}`)}`
420
+ : `${green("─")} ${bold("Worked for")} ${duration}`;
421
+ }
422
+
423
+ export function interruptText() {
424
+ return notice("Interrupt requested", "ctrl+c again to quit");
425
+ }
426
+
427
+ export function cancelledText() {
428
+ return notice("Interrupted", "session preserved; resume with -c");
429
+ }
430
+
431
+ export function commandResultText(text, detail = "") {
432
+ const extra = clipSingleLine(detail, 96);
433
+ return `${green("✓")} ${bold(clipSingleLine(text, 96))}${extra ? dim(` ${extra}`) : ""}`;
434
+ }
435
+
436
+ export function modelUsageText() {
437
+ return notice("Model command", "/model set <name>");
438
+ }
439
+
440
+ export function contextBuiltLine(event) {
441
+ const decisions = event.decisions && typeof event.decisions === "object" ? event.decisions : {};
442
+ if (!decisions.rind_docs_truncated) {
443
+ return "";
444
+ }
445
+ const scopes = Array.isArray(decisions.rind_docs_truncated_scopes)
446
+ ? decisions.rind_docs_truncated_scopes.join(", ")
447
+ : "unknown";
448
+ return notice("Context trimmed", `RIND.md: ${clipSingleLine(scopes, 96)}`);
449
+ }
450
+
451
+ export function unknownCommandText() {
452
+ return notice("Unknown command", "type / to browse commands or ? for shortcuts");
453
+ }
454
+
455
+ function notice(label, ...details) {
456
+ const lines = [`${accent("")} ${bold(label)}`];
457
+ for (const detail of details.flat()) {
458
+ if (detail) {
459
+ lines.push(dim(` ${detail}`));
460
+ }
461
+ }
462
+ return lines.join("\n");
463
+ }
464
+
465
+ export function toolRequestedLine(event) {
466
+ const name = event.tool_name || "unknown";
467
+ const detail = toolDetail(name, parseJsonObject(event.args_preview));
468
+ const label = toolLabel(name);
469
+ const line = `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${label}`;
470
+ return detail ? `${line}\n${dim(toolDetailLine(name, detail))}` : line;
471
+ }
472
+
473
+ export function toolStartedLine(event) {
474
+ const name = event.tool_name || "tool";
475
+ return `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${toolLabel(name)}`;
476
+ }
477
+
478
+ export function toolResultLine(event, fileChange) {
479
+ const name = event.tool_name || "unknown";
480
+ const label = toolLabel(name);
481
+ const duration = formatDuration(event.duration_ms);
482
+ if (event.status === "failed") {
483
+ const suffix = event.error_type ? ` (${event.error_type})` : "";
484
+ const detail = toolErrorDetail(event.result);
485
+ const line = `${red("")} ${bold("Tool")} ${dim("·")} ${label} failed in ${duration}${suffix}`;
486
+ return detail ? `${line}\n${dim(detailLine(detail))}` : line;
487
+ }
488
+ const result = toolResultSummary(event.result);
489
+ if (result.status === "running" && (name === "bash" || name === "bash_output")) {
490
+ const runningText = name === "bash_output"
491
+ ? "command output read; command still running in background"
492
+ : "command running in background";
493
+ const line = `${accent("")} ${bold("Tool")} ${dim("·")} ${runningText} in ${duration}`;
494
+ const output = result.output;
495
+ return [line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
496
+ .filter(Boolean)
497
+ .join("\n");
498
+ }
499
+ const line = result.exitCode
500
+ ? `${red("⊘")} ${bold("Tool")} ${dim("·")} ${label} exited ${result.exitCode} in ${duration}`
501
+ : `${green("◉")} ${bold("Tool")} ${dim("·")} ${completedToolText(name, label)} in ${duration}`;
502
+ const output = result.output;
503
+ return [line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
504
+ .filter(Boolean)
505
+ .join("\n");
506
+ }
507
+
508
+ export function planUpdatedLine(plan) {
509
+ const items = Array.isArray(plan) ? plan : [];
510
+ if (!items.length) {
511
+ return `${green("")} ${bold("Plan cleared")}`;
512
+ }
513
+
514
+ const lines = [`${green("◉")} ${bold("Plan updated")}`];
515
+ for (const item of items) {
516
+ const step = clipSingleLine(item?.step, detailTextWidth());
517
+ if (step) {
518
+ lines.push(` ${planStatusIcon(item?.status)} ${step}`);
519
+ }
520
+ }
521
+ return lines.join("\n");
522
+ }
523
+
524
+ export function goalContinuedLine(round) {
525
+ return `${accent("◌")} ${bold("Goal continued")} ${dim(`· round ${Number(round) || 0}`)}`;
526
+ }
527
+
528
+ export function toolProgressLine(event) {
529
+ const name = event.tool_name || "tool";
530
+ const message = progressMessage(event.payload);
531
+ return message ? `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolLabel(name)}\n${dim(` ↳ ${message}`)}` : "";
532
+ }
533
+
534
+ export function errorLine(error) {
535
+ const detail = clipSingleLine(error, 120);
536
+ return detail
537
+ ? `${red("⊘")} ${bold("Turn failed")}\n${dim(detailLine(detail))}`
538
+ : `${red("⊘")} ${bold("Turn failed")}`;
539
+ }
540
+
541
+ export function questionText(event = {}) {
542
+ return ` Q: ${clipSingleLine(event.question || "Input required", 76)}`;
543
+ }
544
+
545
+ export function questionAnswerText(event = {}, answer = "") {
546
+ const question = clipSingleLine(event.question || "Input required", 76);
547
+ const value = clipSingleLine(String(answer || "").trim() || "(no answer)", 76);
548
+ return [` ${dim("Q:")} ${question}`, ` ${green("A:")} ${value}`].join("\n");
549
+ }
550
+
551
+ function toolDetail(name, args) {
552
+ if (name === "bash") {
553
+ return clipSingleLine(args.command, 96);
554
+ }
555
+ if (name === "bash_output") {
556
+ const bgId = clipSingleLine(args.bg_id, 96);
557
+ return bgId ? `bg ${bgId}` : "";
558
+ }
559
+ if (name === "delegate") {
560
+ return clipSingleLine(args.agent_id, 96);
561
+ }
562
+ for (const key of ["file_path", "path", "query", "url"]) {
563
+ const value = clipSingleLine(args[key], 96);
564
+ if (value) {
565
+ return value;
566
+ }
567
+ }
568
+ return "";
569
+ }
570
+
571
+ function commandDeckText(commands) {
572
+ const items = Array.isArray(commands) ? commands : [];
573
+ const rows = [];
574
+ for (const item of items) {
575
+ if (!item || typeof item !== "object") {
576
+ continue;
577
+ }
578
+ const name = padRight(`/${clipSingleLine(item.name, 22)}`, 16);
579
+ const description = clipSingleLine(item.description, slashContentWidth() - 20);
580
+ rows.push(` ${accent(name)}${dim(description)}`.trimEnd());
581
+ }
582
+ return rows;
583
+ }
584
+
585
+ function slashHelpText(display, commands) {
586
+ const command = display.command && typeof display.command === "object" ? display.command : null;
587
+ if (command) {
588
+ const lines = [sectionRule(`/${clipSingleLine(command.name, 32)}`)];
589
+ const description = clipSingleLine(command.description, slashContentWidth());
590
+ if (description) {
591
+ lines.push(` ${description}`);
592
+ }
593
+ lines.push("");
594
+ lines.push(kvRow("usage", clipSingleLine(command.usage || `/${command.name}`, slashContentWidth() - 14), 10));
595
+ const aliases = slashAliases(command.aliases);
596
+ if (aliases) {
597
+ lines.push(kvRow("aliases", aliases, 10));
598
+ }
599
+ return lines.join("\n");
600
+ }
601
+
602
+ const items = (Array.isArray(display.commands) && display.commands.length ? display.commands : commands)
603
+ .filter((item) => item && typeof item === "object");
604
+ const lines = [sectionRule("Commands", `${items.length} available`)];
605
+ for (const item of items) {
606
+ const name = padRight(`/${clipSingleLine(item.name, 22)}`, 16);
607
+ const description = clipSingleLine(item.description, slashContentWidth() - 20);
608
+ lines.push(` ${accent(name)}${dim(description)}`.trimEnd());
609
+ }
610
+ lines.push("");
611
+ lines.push(dim(" use /help <command> for usage"));
612
+ return lines.join("\n");
613
+ }
614
+
615
+ function slashStatusText(display) {
616
+ const lines = [sectionRule("Status")];
617
+ lines.push(kvRow("session", clipSingleLine(display.session, 48)));
618
+ lines.push(kvRow("model", clipSingleLine(display.model, 52)));
619
+ lines.push(kvRow("messages", `${singleLine(display.messages) || "unknown"} · debug ${display.debug ? "on" : "off"}`));
620
+ const git = display.git && typeof display.git === "object" ? display.git : null;
621
+ if (git) {
622
+ const state = git.dirty ? "dirty" : "clean";
623
+ lines.push(kvRow("git", `${clipSingleLine(git.branch, 48)} · ${state}`));
624
+ }
625
+ for (const usage of Array.isArray(display.usage) ? display.usage : []) {
626
+ lines.push("", sectionRule(sectionLabel(usage.label) || "Sampling"));
627
+ const windowTokens = Number(usage.context_window_tokens) || 0;
628
+ if (windowTokens > 0) {
629
+ lines.push(kvRow("context", `${usageMeter(usage.context_usage_percent)} ${dim(formatPercent(usage.context_usage_percent))}`));
630
+ lines.push(kvRow("input", `${formatCount(usage.input_tokens)} ${dim(`/ ${formatCount(windowTokens)} tokens`)}`));
631
+ } else {
632
+ lines.push(kvRow("input", formatCount(usage.input_tokens)));
633
+ }
634
+ lines.push(kvRow("cached", `${formatCount(usage.cached_input_tokens)} ${dim(`· ${formatPercent(usage.cache_hit_rate)} hit`)}`));
635
+ lines.push(kvRow("output", formatCount(usage.output_tokens)));
636
+ }
637
+ return lines.join("\n");
638
+ }
639
+
640
+ function slashDoctorText(display) {
641
+ const failures = Number(display.failures || 0);
642
+ const warnings = Number(display.warnings || 0);
643
+ const summary = failures || warnings
644
+ ? `${failures} fail · ${warnings} warn`
645
+ : "all checks passed";
646
+ const checks = (Array.isArray(display.checks) ? display.checks : [])
647
+ .filter((check) => check && typeof check === "object");
648
+ const widths = checks.map((check) => visibleLength(clipSingleLine(check.name, 28)));
649
+ const nameWidth = Math.min(24, Math.max(10, ...(widths.length ? widths : [10])));
650
+ const lines = [sectionRule("Doctor", summary)];
651
+ for (const check of checks) {
652
+ const status = singleLine(check.status).toLowerCase();
653
+ const marker = doctorMarker(status);
654
+ const name = padRight(clipSingleLine(check.name, 28), nameWidth);
655
+ const detail = clipSingleLine(check.detail, Math.max(12, slashContentWidth() - nameWidth - 8));
656
+ lines.push(` ${marker} ${name} ${dim(detail)}`.trimEnd());
657
+ }
658
+ const nextSteps = Array.isArray(display.next_steps) ? display.next_steps : [];
659
+ if (nextSteps.length) {
660
+ lines.push("", sectionRule("Next steps"));
661
+ for (const step of nextSteps) {
662
+ lines.push(` ${dim(clipSingleLine(step, slashContentWidth()))}`);
663
+ }
664
+ }
665
+ return lines.join("\n");
666
+ }
667
+
668
+ function slashSessionsText(display) {
669
+ const sessions = Array.isArray(display.sessions) ? display.sessions : [];
670
+ const lines = [sectionRule("Sessions", sessions.length ? `${sessions.length} recent` : "")];
671
+ if (!sessions.length) {
672
+ lines.push(dim(" no recent sessions"));
673
+ }
674
+ for (const session of sessions) {
675
+ if (!session || typeof session !== "object") {
676
+ continue;
677
+ }
678
+ const marker = session.current ? accent("›") : dim("·");
679
+ const current = session.current ? dim(" · current") : "";
680
+ const id = middleClip(session.id, 32);
681
+ const updated = clipSingleLine(session.updated_at, 28);
682
+ lines.push(` ${marker} ${id}${current}${updated ? dim(` · ${updated}`) : ""}`);
683
+ const title = clipSingleLine(session.title, slashContentWidth());
684
+ const size = sessionSizeText(session);
685
+ const summary = [title, size].filter(Boolean).join(" · ");
686
+ if (summary) {
687
+ lines.push(dim(` ${summary}`));
688
+ }
689
+ const preview = clipSingleLine(session.preview, slashContentWidth());
690
+ if (preview) {
691
+ lines.push(dim(` ${preview}`));
692
+ }
693
+ }
694
+ const resume = clipSingleLine(display.resume_command, slashContentWidth());
695
+ if (resume) {
696
+ lines.push("", dim(` resume: ${resume}`));
697
+ }
698
+ return lines.join("\n");
699
+ }
700
+
701
+ function slashSkillsText(display) {
702
+ const skills = (Array.isArray(display.skills) ? display.skills : [])
703
+ .filter((skill) => skill && typeof skill === "object");
704
+ const widths = skills.map((skill) => visibleLength(clipSingleLine(skill.name, 30)));
705
+ const nameWidth = Math.min(24, Math.max(12, ...(widths.length ? widths : [12])));
706
+ const scopeWidths = skills.map((skill) => visibleLength(clipSingleLine(skill.scope, 18)));
707
+ const scopeWidth = Math.max(0, ...(scopeWidths.length ? scopeWidths : [0]));
708
+ const lines = [sectionRule("Skills", skills.length ? `${skills.length} available` : "")];
709
+ if (!skills.length) {
710
+ lines.push(dim(" no skills found"));
711
+ return lines.join("\n");
712
+ }
713
+ for (const skill of skills) {
714
+ const name = padRight(clipSingleLine(skill.name, 30), nameWidth);
715
+ const scope = clipSingleLine(skill.scope, 18);
716
+ const tag = scope ? padRight(`[${scope}]`, scopeWidth) : "";
717
+ const description = clipSingleLine(
718
+ skill.description,
719
+ Math.max(18, slashContentWidth() - nameWidth - (scopeWidth ? scopeWidth + 4 : 0)),
720
+ );
721
+ lines.push(` ${bold(name)} ${tag ? dim(tag) : ""} ${dim(description)}`.trimEnd());
722
+ const location = prettySkillLocation(skill.path);
723
+ if (location) {
724
+ lines.push(dim(` ${clipSingleLine(location, Math.max(18, slashContentWidth() - 6))}`));
725
+ }
726
+ }
727
+ return lines.join("\n");
728
+ }
729
+
730
+ // Paths read best compressed: collapse the home directory to "~" and drop the
731
+ // redundant SKILL.md filename that every entry shares.
732
+ function prettySkillLocation(value) {
733
+ let location = String(value || "").trim();
734
+ if (!location) {
735
+ return "";
736
+ }
737
+ location = location.replace(/[\\/]+SKILL\.md$/i, "");
738
+ const home = homedir();
739
+ if (home && (location === home || location.startsWith(home))) {
740
+ const rest = location.slice(home.length);
741
+ location = rest ? `~${rest}` : "~";
742
+ }
743
+ return location;
744
+ }
745
+
746
+ function sessionSizeText(session) {
747
+ const messages = optionalNonnegativeNumber(session.messages);
748
+ const tools = optionalNonnegativeNumber(session.tool_calls);
749
+ if (messages === null || tools === null) {
750
+ return "unknown size";
751
+ }
752
+ return `${formatCount(messages)} msg, ${formatCount(tools)} tool`;
753
+ }
754
+
755
+ function optionalNonnegativeNumber(value) {
756
+ const number = Number(value);
757
+ return Number.isFinite(number) && number >= 0 ? number : null;
758
+ }
759
+
760
+ function slashConfigText(display) {
761
+ const entries = (Array.isArray(display.entries) ? display.entries : [])
762
+ .filter((entry) => entry && typeof entry === "object");
763
+ const lines = [sectionRule("Config", entries.length ? `${entries.length} ${entries.length === 1 ? "key" : "keys"}` : "")];
764
+ for (const entry of entries) {
765
+ const label = clipSingleLine(entry.label, 22);
766
+ const rawValue = entry.label === "settings"
767
+ ? middleClip(entry.value, Math.max(18, slashContentWidth() - visibleLength(label) - 6))
768
+ : clipSingleLine(entry.value, Math.max(18, slashContentWidth() - visibleLength(label) - 6));
769
+ const state = entry.state ? dim(` (${clipSingleLine(entry.state, 18)})`) : "";
770
+ lines.push(` ${dim(padRight(label, 18))}${rawValue}${state}`.trimEnd());
771
+ }
772
+ return lines.join("\n");
773
+ }
774
+
775
+ function slashThemeText(display) {
776
+ const flavors = Array.isArray(display.flavors) ? display.flavors : [];
777
+ const current = singleLine(display.current) || "mocha";
778
+ const meta = display.changed && display.previous
779
+ ? `${singleLine(display.previous)} ${current}`
780
+ : current;
781
+ const lines = [sectionRule("Theme", meta)];
782
+ if (!flavors.length) {
783
+ lines.push(dim(` active: ${current}`));
784
+ return lines.join("\n");
785
+ }
786
+ const labelWidth = Math.min(
787
+ 16,
788
+ Math.max(8, ...flavors.map((flavor) => visibleLength(clipSingleLine(flavor?.label, 16)))),
789
+ );
790
+ for (const flavor of flavors) {
791
+ if (!flavor || typeof flavor !== "object") {
792
+ continue;
793
+ }
794
+ const isCurrent = Boolean(flavor.current);
795
+ const marker = isCurrent ? accent("›") : dim("·");
796
+ const label = padRight(clipSingleLine(flavor.label || flavor.name, 16), labelWidth);
797
+ const tag = isCurrent ? dim(" · current") : "";
798
+ lines.push(` ${marker} ${isCurrent ? bold(label) : dim(label)} ${flavorSwatch(flavor.name)}${tag}`);
799
+ }
800
+ lines.push("");
801
+ lines.push(dim(" /theme <latte | frappe | macchiato | mocha>"));
802
+ return lines.join("\n");
803
+ }
804
+
805
+ function sectionRule(title, meta = "") {
806
+ const titleText = clipSingleLine(title, 48);
807
+ const metaText = meta ? clipSingleLine(meta, 48) : "";
808
+ const head = metaText ? `${titleText} ${dim(`· ${metaText}`)}` : titleText;
809
+ const fill = Math.max(3, slashContentWidth() - visibleLength(head) - 6);
810
+ return ` ${dim("──")} ${head} ${dim("─".repeat(fill))}`;
811
+ }
812
+
813
+ function kvRow(label, value, labelWidth = 12) {
814
+ return ` ${dim(padRight(label, labelWidth))}${value}`;
815
+ }
816
+
817
+ function usageMeter(ratio) {
818
+ const cells = 10;
819
+ const clamped = Math.max(0, Math.min(1, Number(ratio) || 0));
820
+ const filled = Math.min(cells, Math.round(clamped * cells));
821
+ const tone = clamped >= 0.85 ? red : clamped >= 0.6 ? accent : (text) => text;
822
+ return `${tone("▮".repeat(filled))}${dim("▯".repeat(cells - filled))}`;
823
+ }
824
+
825
+ function sectionLabel(value) {
826
+ return singleLine(value).replace(/:\s*$/, "");
827
+ }
828
+
829
+ function slashAliases(value) {
830
+ return Array.isArray(value) ? value.map((alias) => `/${clipSingleLine(alias, 18)}`).join(", ") : "";
831
+ }
832
+
833
+ function slashContentWidth() {
834
+ const columns = Number(process.stdout.columns);
835
+ if (!Number.isFinite(columns) || columns <= 0) {
836
+ return 96;
837
+ }
838
+ return Math.max(28, Math.min(96, columns - 6));
839
+ }
840
+
841
+ function doctorMarker(status) {
842
+ if (status === "ok") {
843
+ return green("✓");
844
+ }
845
+ if (status === "fail") {
846
+ return red("⊘");
847
+ }
848
+ return paint.warning("!");
849
+ }
850
+
851
+ function menuWindow(items, selectedIndex) {
852
+ const entries = Array.isArray(items) ? items : [];
853
+ const total = entries.length;
854
+ if (!total) {
855
+ return { items: [], activeIndex: 0, start: 0, total: 0 };
856
+ }
857
+ const limit = 8;
858
+ const selected = Math.max(0, Math.min(total - 1, Number(selectedIndex) || 0));
859
+ const start = total <= limit ? 0 : Math.min(Math.max(0, selected - 3), total - limit);
860
+ return {
861
+ items: entries.slice(start, start + limit),
862
+ activeIndex: selected - start,
863
+ start,
864
+ total,
865
+ };
866
+ }
867
+
868
+ function slashMenuTitle(visible) {
869
+ if (visible.total <= visible.items.length) {
870
+ return " Command deck";
871
+ }
872
+ return ` Command deck ${visible.start + 1}-${visible.start + visible.items.length}/${visible.total}`;
873
+ }
874
+
875
+ function modelMenuTitle(visible) {
876
+ if (visible.total <= visible.items.length) {
877
+ return " Model deck";
878
+ }
879
+ return ` Model deck ${visible.start + 1}-${visible.start + visible.items.length}/${visible.total}`;
880
+ }
881
+
882
+ function toolDetailLine(name, detail) {
883
+ if (name === "delegate") {
884
+ return ` agent: ${detail}`;
885
+ }
886
+ return name === "bash" ? ` $ ${detail}` : ` ↳ ${detail}`;
887
+ }
888
+
889
+ function detailLine(text) {
890
+ return ` ↳ ${text}`;
891
+ }
892
+
893
+ function toolLabel(name) {
894
+ if (name === "bash") {
895
+ return "command";
896
+ }
897
+ if (name === "bash_output") {
898
+ return "command output";
899
+ }
900
+ const labels = {
901
+ edit_file: "file edit",
902
+ read_file: "file read",
903
+ };
904
+ return labels[name] || humanToolName(name);
905
+ }
906
+
907
+ function toolActiveVerb(name) {
908
+ if (name === "bash") {
909
+ return "Running";
910
+ }
911
+ if (name === "bash_output") {
912
+ return "Reading";
913
+ }
914
+ return "Calling";
915
+ }
916
+
917
+ function completedToolText(name, label) {
918
+ if (name === "bash") {
919
+ return `Ran ${label}`;
920
+ }
921
+ if (name === "bash_output") {
922
+ return `Read ${label}`;
923
+ }
924
+ return `Called ${label}`;
925
+ }
926
+
927
+ function humanToolName(name) {
928
+ return singleLine(name).replace(/[_-]+/g, " ") || "tool";
929
+ }
930
+
931
+ function parseJsonObject(value) {
932
+ try {
933
+ const parsed = JSON.parse(String(value || ""));
934
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
935
+ } catch {
936
+ return {};
937
+ }
938
+ }
939
+
940
+ function toolErrorDetail(result) {
941
+ const payload = parseJsonObject(result);
942
+ return clipSingleLine(payload.error, 120);
943
+ }
944
+
945
+ function toolResultSummary(result) {
946
+ const payload = parseJsonObject(result);
947
+ const data = payload.data && typeof payload.data === "object" ? payload.data : {};
948
+ return {
949
+ status: singleLine(data.status).toLowerCase(),
950
+ exitCode: nonZeroExitCode(data.exit_code),
951
+ output: clipSingleLine(data.stdout || data.stderr || data.message, 120),
952
+ };
953
+ }
954
+
955
+ function nonZeroExitCode(value) {
956
+ const code = Number(value);
957
+ return Number.isInteger(code) && code !== 0 ? code : 0;
958
+ }
959
+
960
+ function progressMessage(payload) {
961
+ if (!payload || typeof payload !== "object") {
962
+ return "";
963
+ }
964
+ for (const key of ["message", "status", "text"]) {
965
+ const value = clipSingleLine(payload[key], 120);
966
+ if (value) {
967
+ return value;
968
+ }
969
+ }
970
+ return "";
971
+ }
972
+
973
+ function fileChangeLine(fileChange) {
974
+ if (!fileChange || typeof fileChange !== "object") {
975
+ return "";
976
+ }
977
+ const changes = Array.isArray(fileChange.lines)
978
+ ? fileChange.lines.filter((line) => line?.kind === "added" || line?.kind === "removed")
979
+ : [];
980
+ if (!changes.length) {
981
+ return "";
982
+ }
983
+ const path = middleClip(fileChange.file_path, fileChangePathWidth());
984
+ const shown = changes.slice(0, MAX_FILE_CHANGE_LINES);
985
+ const lines = [`${dim("")} ${path}`];
986
+ for (const change of shown) {
987
+ lines.push(fileChangeDiffLine(change));
988
+ }
989
+ const hidden = changes.length - shown.length;
990
+ if (hidden > 0) {
991
+ lines.push(dim(` … ${hidden} more changed lines`));
992
+ }
993
+ return lines.join("\n");
994
+ }
995
+
996
+ function fileChangeDiffLine(change) {
997
+ const added = change.kind === "added";
998
+ const marker = added ? "+" : "-";
999
+ const style = added ? green : red;
1000
+ return `${dim(` ${marker} `)}${style(clipCells(change.text, detailTextWidth()))}`;
1001
+ }
1002
+
1003
+ function fileChangePathWidth() {
1004
+ const columns = Number(process.stdout.columns);
1005
+ if (!Number.isFinite(columns) || columns <= 0) {
1006
+ return 48;
1007
+ }
1008
+ return Math.max(18, Math.min(48, columns - 32));
1009
+ }
1010
+
1011
+ function detailTextWidth() {
1012
+ const columns = Number(process.stdout.columns);
1013
+ if (!Number.isFinite(columns) || columns <= 0) {
1014
+ return 96;
1015
+ }
1016
+ return Math.max(12, Math.min(96, columns - 6));
1017
+ }
1018
+
1019
+ function planStatusIcon(status) {
1020
+ switch (status) {
1021
+ case "in_progress":
1022
+ return accent("◐");
1023
+ case "completed":
1024
+ return green("●");
1025
+ case "cancelled":
1026
+ return dim("⊖");
1027
+ default:
1028
+ return dim("○");
1029
+ }
1030
+ }
1031
+
1032
+ function clipSingleLine(value, maxLength) {
1033
+ const text = singleLine(value);
1034
+ return clipCells(text, maxLength);
1035
+ }
1036
+
1037
+ function middleClip(value, maxLength) {
1038
+ const text = singleLine(value);
1039
+ return middleClipCells(text, maxLength);
1040
+ }
1041
+
1042
+ function singleLine(value) {
1043
+ return String(value || "").replace(/\s+/g, " ").trim();
1044
+ }
1045
+
1046
+ function messageLines(value) {
1047
+ const text = String(value || "").replace(/\r/g, "").trim();
1048
+ return text ? text.split("\n").map((line) => line.trimEnd()) : [];
1049
+ }
1050
+
1051
+ function userInputContentWidth(width) {
1052
+ const columns = Number(width ?? process.stdout.columns);
1053
+ return Number.isFinite(columns) && columns > 0 ? Math.max(1, Math.floor(columns - 2)) : 78;
1054
+ }
1055
+
1056
+ function formatDuration(durationMs) {
1057
+ const value = Number(durationMs || 0);
1058
+ if (!Number.isFinite(value) || value <= 0) {
1059
+ return "0ms";
1060
+ }
1061
+ if (value < 1000) {
1062
+ return `${Math.trunc(value)}ms`;
1063
+ }
1064
+ if (value >= 60000) {
1065
+ const totalSeconds = Math.round(value / 1000);
1066
+ const minutes = Math.floor(totalSeconds / 60);
1067
+ const seconds = String(totalSeconds % 60).padStart(2, "0");
1068
+ return `${minutes}m ${seconds}s`;
1069
+ }
1070
+ return `${(value / 1000).toFixed(2)}s`;
1071
+ }
1072
+
1073
+ function formatActivityDuration(durationMs) {
1074
+ const value = Math.max(0, Number(durationMs || 0));
1075
+ if (!Number.isFinite(value) || value < 60000) {
1076
+ return `${Math.floor(value / 1000)}s`;
1077
+ }
1078
+ const totalSeconds = Math.floor(value / 1000);
1079
+ const minutes = Math.floor(totalSeconds / 60);
1080
+ const seconds = String(totalSeconds % 60).padStart(2, "0");
1081
+ return `${minutes}m ${seconds}s`;
1082
+ }
1083
+
1084
+ function toolSummary(tools) {
1085
+ const completed = Number(tools.completed || 0);
1086
+ const failed = Number(tools.failed || 0);
1087
+ const parts = [];
1088
+ if (completed > 0) {
1089
+ parts.push(`${completed} completed`);
1090
+ }
1091
+ if (failed > 0) {
1092
+ parts.push(`${failed} failed`);
1093
+ }
1094
+ return parts.join(", ");
1095
+ }
1096
+
1097
+ function formatCount(value) {
1098
+ const number = Number(value || 0);
1099
+ if (!Number.isFinite(number) || number <= 0) {
1100
+ return "0";
1101
+ }
1102
+ if (Math.abs(number) >= 1000) {
1103
+ return `${(number / 1000).toFixed(1)}k`;
1104
+ }
1105
+ return String(Math.trunc(number));
1106
+ }
1107
+
1108
+ function formatPercent(value) {
1109
+ const number = Number(value);
1110
+ if (!Number.isFinite(number)) {
1111
+ return "0.0%";
1112
+ }
1113
+ return `${(number * 100).toFixed(1)}%`;
1114
+ }
1115
+
1116
+ function resumePreviewText(value) {
1117
+ const lines = String(value || "").trim().split(/\r?\n/).filter(Boolean);
1118
+ return lines.map(resumePreviewLine).join("\n");
1119
+ }
1120
+
1121
+ function resumePreviewLine(line) {
1122
+ const message = line.match(/^-?\s*(user|assistant):\s*(.*)$/i);
1123
+ if (message) {
1124
+ const role = message[1].toLowerCase();
1125
+ const marker = role === "user" ? accent("▷") : dim("◁");
1126
+ const label = role === "user" ? "You" : "Assistant";
1127
+ return `${marker} ${label} ${dim("·")} ${clipSingleLine(message[2], 72)}`;
1128
+ }
1129
+ return dim(` ${clipSingleLine(line, 78)}`);
1130
+ }
1131
+
1132
+ function startupBannerText(info, frameWidth) {
1133
+ const width = startupBannerWidth(frameWidth);
1134
+ const modelLine = `model ${singleLine(info.model) || "unknown"} · session ${singleLine(info.session_id) || "unknown"}`;
1135
+ const version = singleLine(info.version) || "unknown";
1136
+ const cwd = middleClip(info.cwd || process.cwd(), width - 4);
1137
+ return [
1138
+ startupBannerBorder("┌", "┐", width),
1139
+ startupBannerLine(`${bold("Rind")} ${dim(`v${version}`)}`, width),
1140
+ startupBannerLine(modelLine, width),
1141
+ startupBannerLine(cwd, width),
1142
+ startupBannerBorder("└", "┘", width),
1143
+ ].join("\n");
1144
+ }
1145
+
1146
+ function startupBannerBorder(left, right, width) {
1147
+ return dim(`${left}${"─".repeat(width - 2)}${right}`);
1148
+ }
1149
+
1150
+ function startupBannerLine(text, frameWidth) {
1151
+ const clean = String(text || "").replace(/[\r\n\t]+/g, " ").trimEnd();
1152
+ const width = frameWidth - 4;
1153
+ const content =
1154
+ visibleLength(clean) <= width
1155
+ ? clean
1156
+ : clipCells(clean, width);
1157
+ return `${dim("│")} ${padRight(content, width)} ${dim("│")}`;
1158
+ }
1159
+
1160
+ function startupBannerWidth(frameWidth) {
1161
+ const columns = Number(frameWidth ?? process.stdout.columns);
1162
+ if (!Number.isFinite(columns) || columns <= 0) {
1163
+ return MAX_STARTUP_BANNER_WIDTH;
1164
+ }
1165
+ return Math.max(44, Math.min(MAX_STARTUP_BANNER_WIDTH, columns - 2));
1166
+ }
1167
+
1168
+ function helpRow(leftKey, leftText, rightKey = "", rightText = "") {
1169
+ const left = `${padRight(leftKey, 12)} ${leftText}`;
1170
+ if (!rightKey && !rightText) {
1171
+ return dim(` ${left}`);
1172
+ }
1173
+ const right = `${padRight(rightKey, 14)} ${rightText}`;
1174
+ return dim(` ${padRight(left, 33)} ${right}`);
1175
+ }
1176
+
1177
+ function inputPromptFrame(header = "", state = {}, frameWidth) {
1178
+ const lines = [""];
1179
+ const activity = promptActivityLine(state);
1180
+ if (activity) {
1181
+ lines.push(activity);
1182
+ }
1183
+ lines.push(...pendingInputLines(state.pendingInputs, frameWidth));
1184
+ if (header) {
1185
+ lines.push(header);
1186
+ }
1187
+ lines.push(inputDivider(frameWidth));
1188
+ lines.push(" ▷ ");
1189
+ return lines.join("\n");
1190
+ }
1191
+
1192
+ function inputDivider(frameWidth) {
1193
+ return dim(` ${"─".repeat(dividerWidth(frameWidth))}`);
1194
+ }
1195
+
1196
+ function dividerWidth(frameWidth) {
1197
+ const columns = Number(frameWidth ?? process.stdout.columns);
1198
+ if (!Number.isFinite(columns) || columns <= 0) {
1199
+ return MAX_COMPOSER_WIDTH;
1200
+ }
1201
+ return Math.max(1, Math.floor(columns) - 2);
1202
+ }
1203
+
1204
+ function pendingInputLines(entries, frameWidth) {
1205
+ if (!Array.isArray(entries)) {
1206
+ return [];
1207
+ }
1208
+ const width = composerWidth(frameWidth);
1209
+ const lines = entries.flatMap((entry) => {
1210
+ const input = singleLine(entry?.input);
1211
+ if (!input) {
1212
+ return [];
1213
+ }
1214
+ const label = entry.mode === "steering" ? "Steering" : "Queue";
1215
+ return dim(` ${label}: ${clipSingleLine(input, Math.max(1, width - visibleLength(label) - 4))}`);
1216
+ });
1217
+ const hints = [];
1218
+ if (entries.some((entry) => entry?.mode === "follow_up")) {
1219
+ hints.push("alt+up recall queue");
1220
+ }
1221
+ if (entries.some((entry) => entry?.mode === "steering")) {
1222
+ hints.push("alt+down recall steer");
1223
+ }
1224
+ if (hints.length) {
1225
+ lines.push(dim(` ${hints.join(" · ")}`));
1226
+ }
1227
+ return lines;
1228
+ }
1229
+
1230
+ function activityFrame(frame) {
1231
+ const frames = ["◐", "◓", "◑", "◒"];
1232
+ const index = Math.abs(Number(frame) || 0) % frames.length;
1233
+ return frames[index];
1234
+ }
1235
+
1236
+ function padRight(text, width) {
1237
+ return `${text}${" ".repeat(Math.max(0, width - visibleLength(text)))}`;
1238
+ }
1239
+
1240
+ function visibleLength(text) {
1241
+ return textWidth(text);
1242
+ }
1243
+
1244
+ function promptHeaderLine(info, frameWidth) {
1245
+ const backgroundCount = Number(info.background_count);
1246
+ const delegateCount = Number(info.delegate_count);
1247
+ const taskHints = [];
1248
+ if (backgroundCount > 0) {
1249
+ taskHints.push(`[bg:${backgroundCount}]`);
1250
+ }
1251
+ if (delegateCount > 0) {
1252
+ taskHints.push(`[delegate:${delegateCount}]`);
1253
+ }
1254
+ const taskHint = taskHints.length
1255
+ ? dim(` · ${taskHints.join(" ")} (ctrl+b monitor)`)
1256
+ : "";
1257
+ const model = singleLine(info.model);
1258
+ const effort = singleLine(info.reasoning_effort);
1259
+ const cwd = middleClip(info.cwd, 56);
1260
+ const width = composerWidth(frameWidth);
1261
+ const effortSegment = effort ? `${dim(" · ")}${promptModel(effort)}` : "";
1262
+ if (model && cwd) {
1263
+ const separator = " · ";
1264
+ const pathWidth = width - visibleLength(model) - visibleLength(separator) - visibleLength(taskHint) - visibleLength(effortSegment);
1265
+ if (pathWidth > 0) {
1266
+ return ` ${promptModel(clipSingleLine(model, width))}${effortSegment}${dim(separator)}${promptPath(clipSingleLine(cwd, pathWidth))}${taskHint}`;
1267
+ }
1268
+ }
1269
+ if (model) {
1270
+ return ` ${promptModel(clipSingleLine(model, width))}${taskHint}`;
1271
+ }
1272
+ return cwd ? ` ${promptPath(clipSingleLine(cwd, width))}${taskHint}` : "";
1273
+ }
1274
+
1275
+ function composerWidth(frameWidth) {
1276
+ const columns = Number(frameWidth ?? process.stdout.columns);
1277
+ if (!Number.isFinite(columns) || columns <= 0) {
1278
+ return MAX_COMPOSER_WIDTH;
1279
+ }
1280
+ return Math.max(1, Math.min(MAX_COMPOSER_WIDTH, columns - 4));
1281
+ }
1282
+
1283
+ function bold(text) {
1284
+ return paint.bold(text);
1285
+ }
1286
+
1287
+ function dim(text) {
1288
+ return paint.dim(text);
1289
+ }
1290
+
1291
+ function accent(text) {
1292
+ return paint.accent(text);
1293
+ }
1294
+
1295
+ function green(text) {
1296
+ return paint.success(text);
1297
+ }
1298
+
1299
+ function red(text) {
1300
+ return paint.danger(text);
1301
+ }
1302
+
1303
+ function promptModel(text) {
1304
+ return paint.bold(paint.accent(text));
1305
+ }
1306
+
1307
+ function promptPath(text) {
1308
+ return paint.path(text);
1309
+ }