@rind-ai/cli 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/rind.js +2 -2
- package/lib/assistant-renderer.js +42 -15
- package/lib/cli-input-actions.js +133 -19
- package/lib/cli-output-controller.js +82 -94
- package/lib/cli-runtime-controller.js +155 -8
- package/lib/cli-state.js +2 -2
- package/lib/command-controller.js +34 -0
- package/lib/components/assistant-message.js +54 -21
- package/lib/composer-terminal.js +1 -1
- package/lib/event-controller.js +9 -11
- package/lib/frontend-cli-implementation.js +108 -18
- package/lib/ipc.js +186 -0
- package/lib/line-editor.js +29 -4
- package/lib/local-slash-commands.js +2 -0
- package/lib/markdown-lines.js +142 -0
- package/lib/one-shot-progress.js +145 -145
- package/lib/one-shot.js +3 -3
- package/lib/prompt-history-store.js +54 -0
- package/lib/question-menu-state.js +10 -0
- package/lib/rendering.js +466 -102
- package/lib/runtime-client.js +12 -13
- package/lib/runtime-protocol.js +5 -0
- package/lib/send.js +53 -0
- package/lib/task-monitor-controller.js +45 -45
- package/lib/terminal-key.js +47 -3
- package/lib/text-width.js +2 -19
- package/lib/tool-display.js +18 -23
- package/lib/tui/cursor.js +17 -8
- package/lib/tui/input-buffer.js +69 -28
- package/lib/tui/tui.js +128 -30
- package/lib/turn-controller.js +13 -4
- package/package.json +5 -5
- package/lib/frontend-cli.js +0 -5
package/lib/rendering.js
CHANGED
|
@@ -1,10 +1,363 @@
|
|
|
1
|
-
import { clipCells, middleClipCells, textWidth, wrapTextCells } from "./text-width.js";
|
|
1
|
+
import { clipCells, graphemes, middleClipCells, stripAnsi, textWidth, wrapTextCells } from "./text-width.js";
|
|
2
|
+
import { formatDuration } from "./tool-display.js";
|
|
2
3
|
import { paint, flavorSwatch } from "./theme.js";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
|
|
5
6
|
const MAX_STARTUP_BANNER_WIDTH = 80;
|
|
6
7
|
const MAX_COMPOSER_WIDTH = 78;
|
|
7
8
|
const MAX_FILE_CHANGE_LINES = 20;
|
|
9
|
+
const BOARD_MIN_WIDTH = 60;
|
|
10
|
+
const BOARD_MAX_WIDTH = 100;
|
|
11
|
+
const BOARD_PALETTE_ROLES = ["accent", "success", "warning", "danger", "notice", "path", "code", "fence"];
|
|
12
|
+
const BOARD_SECTION_SHORT_LABELS = {
|
|
13
|
+
chat_user: "user",
|
|
14
|
+
chat_assistant: "chat",
|
|
15
|
+
reasoning: "reasoning",
|
|
16
|
+
system_prompt: "system",
|
|
17
|
+
skill_catalog: "skills",
|
|
18
|
+
rind_docs: "RIND",
|
|
19
|
+
rind_docs_user: "RIND",
|
|
20
|
+
rind_docs_project: "RIND",
|
|
21
|
+
compaction_handoff: "compact",
|
|
22
|
+
goal_policy: "goal",
|
|
23
|
+
delegate: "delegate",
|
|
24
|
+
team_agent_catalog: "team",
|
|
25
|
+
"tool:other": "other",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function boardWidth(width) {
|
|
29
|
+
const columns = Number(width ?? process.stdout.columns);
|
|
30
|
+
if (!Number.isFinite(columns) || columns <= 0) {
|
|
31
|
+
return BOARD_MAX_WIDTH;
|
|
32
|
+
}
|
|
33
|
+
return Math.max(BOARD_MIN_WIDTH, Math.min(BOARD_MAX_WIDTH, Math.floor(columns)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function occupancyTone(percent) {
|
|
37
|
+
const value = Number(percent);
|
|
38
|
+
if (!Number.isFinite(value)) {
|
|
39
|
+
return "neutral";
|
|
40
|
+
}
|
|
41
|
+
if (value > 0.85) {
|
|
42
|
+
return "err";
|
|
43
|
+
}
|
|
44
|
+
if (value >= 0.6) {
|
|
45
|
+
return "warn";
|
|
46
|
+
}
|
|
47
|
+
return "neutral";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BOARD_FOOTER = "Tab switch page · Esc exit";
|
|
51
|
+
|
|
52
|
+
export function contextBoardText(page = {}, width) {
|
|
53
|
+
const frameWidth = boardWidth(width);
|
|
54
|
+
const breakdown = isBoardRecord(page.breakdown) ? page.breakdown : null;
|
|
55
|
+
const usage = isBoardRecord(page.latest_usage) ? page.latest_usage : null;
|
|
56
|
+
const plain = page.plain === true;
|
|
57
|
+
const title = "Context · last sampling";
|
|
58
|
+
if (!breakdown || !Array.isArray(breakdown.sections) || !breakdown.sections.length) {
|
|
59
|
+
return boardPanel({
|
|
60
|
+
title,
|
|
61
|
+
lines: boardEmptyLines(frameWidth, "No context sampled yet"),
|
|
62
|
+
footer: BOARD_FOOTER,
|
|
63
|
+
page,
|
|
64
|
+
frameWidth,
|
|
65
|
+
plain,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const windowTokens = boardNumber(breakdown.context_window_tokens);
|
|
69
|
+
const estimated = boardNumber(breakdown.estimated_total);
|
|
70
|
+
const measured = Math.max(0, boardNumber(usage?.input_tokens));
|
|
71
|
+
const usedPercent = windowTokens > 0 ? estimated / windowTokens : 0;
|
|
72
|
+
const lines = contextMetaLines(windowTokens, usedPercent, measured, estimated, frameWidth - 4);
|
|
73
|
+
const innerWidth = frameWidth - 4;
|
|
74
|
+
if (windowTokens > 0) {
|
|
75
|
+
lines.push(stackedShareBar(breakdown.sections, windowTokens, innerWidth));
|
|
76
|
+
lines.push(barUnderline(breakdown.sections, windowTokens, innerWidth));
|
|
77
|
+
}
|
|
78
|
+
lines.push(...contextSectionRows(breakdown.sections, estimated, innerWidth));
|
|
79
|
+
return boardPanel({
|
|
80
|
+
title: contextTitle(breakdown, title),
|
|
81
|
+
lines,
|
|
82
|
+
footer: BOARD_FOOTER,
|
|
83
|
+
page,
|
|
84
|
+
frameWidth,
|
|
85
|
+
plain,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function usageBoardText(page = {}, width) {
|
|
90
|
+
const frameWidth = boardWidth(width);
|
|
91
|
+
const summary = isBoardRecord(page.summary) ? page.summary : {};
|
|
92
|
+
const totals = isBoardRecord(summary.totals) ? summary.totals : {};
|
|
93
|
+
const plain = page.plain === true;
|
|
94
|
+
const days = boardNumber(summary.days) || 7;
|
|
95
|
+
const title = `Token usage · last ${days} day${days === 1 ? "" : "s"}`;
|
|
96
|
+
if (!boardNumber(totals.samples)) {
|
|
97
|
+
return boardPanel({
|
|
98
|
+
title,
|
|
99
|
+
lines: boardEmptyLines(frameWidth, "No usage recorded yet"),
|
|
100
|
+
footer: BOARD_FOOTER,
|
|
101
|
+
page,
|
|
102
|
+
frameWidth,
|
|
103
|
+
plain,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const lines = usageHeroLines(totals, frameWidth - 4);
|
|
107
|
+
lines.push(dim(`${boardNumber(totals.samples)} samples`));
|
|
108
|
+
const byDay = Array.isArray(summary.by_day) ? summary.by_day : [];
|
|
109
|
+
if (byDay.length) {
|
|
110
|
+
lines.push("", bold("By day"), ...byDayRows(byDay, frameWidth - 4));
|
|
111
|
+
}
|
|
112
|
+
const byModel = Array.isArray(summary.by_model) ? summary.by_model : [];
|
|
113
|
+
if (byModel.length) {
|
|
114
|
+
lines.push("", bold("By model"), ...rightRows(byModel.map((row) => [boardText(row?.model), boardCompact(row?.tokens)]), frameWidth - 4));
|
|
115
|
+
}
|
|
116
|
+
const sessions = Array.isArray(summary.recent_sessions) ? summary.recent_sessions : [];
|
|
117
|
+
if (sessions.length) {
|
|
118
|
+
lines.push(
|
|
119
|
+
"",
|
|
120
|
+
bold(`By session (latest ${sessions.length})`),
|
|
121
|
+
...rightRows(sessions.map((row) => [sessionRowLabel(row), boardCompact(row?.tokens)]), frameWidth - 4),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const compactions = boardNumber(totals.compactions);
|
|
125
|
+
const footer = compactions > 0
|
|
126
|
+
? `${formatBoardNumber(compactions)} compaction call${compactions === 1 ? "" : "s"} · ${BOARD_FOOTER}`
|
|
127
|
+
: BOARD_FOOTER;
|
|
128
|
+
return boardPanel({ title, lines, footer, page, frameWidth, plain });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function contextTitle(breakdown, fallback) {
|
|
132
|
+
const turn = boardText(breakdown.turn_id);
|
|
133
|
+
const time = boardText(breakdown.captured_at).slice(11, 19);
|
|
134
|
+
const parts = [fallback];
|
|
135
|
+
if (turn) {
|
|
136
|
+
parts.push(`turn ${turn.slice(0, 4)}`);
|
|
137
|
+
}
|
|
138
|
+
if (/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
|
139
|
+
parts.push(time);
|
|
140
|
+
}
|
|
141
|
+
return parts.join(" · ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function contextMetaLines(windowTokens, usedPercent, measured, estimated, inner) {
|
|
145
|
+
const tone = occupancyTone(usedPercent);
|
|
146
|
+
const paintTone = tone === "err" ? red : tone === "warn" ? paint.warning : (text) => text;
|
|
147
|
+
const windowPart = `Window ${formatBoardNumber(windowTokens)} · ${paintTone(`used ${Math.round(usedPercent * 100)}%`)}`;
|
|
148
|
+
const measuredPart = measured > 0
|
|
149
|
+
? `measured ${formatBoardNumber(measured)} · estimated ~${formatBoardNumber(estimated)} ${estimatedDeviation(measured, estimated)}`
|
|
150
|
+
: "";
|
|
151
|
+
if (measuredPart && textWidth(`${windowPart} ${measuredPart}`) > inner) {
|
|
152
|
+
return [windowPart, measuredPart];
|
|
153
|
+
}
|
|
154
|
+
return [measuredPart ? `${windowPart} ${measuredPart}` : windowPart];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function estimatedDeviation(measured, estimated) {
|
|
158
|
+
if (estimated <= 0) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
const percent = Math.round(((measured - estimated) / estimated) * 100);
|
|
162
|
+
return `(${percent >= 0 ? "+" : ""}${percent}%)`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stackedShareBar(sections, windowTokens, cells) {
|
|
166
|
+
const spans = boardSpans(sections, windowTokens, cells);
|
|
167
|
+
const remaining = Math.max(0, cells - spans.reduce((sum, span) => sum + span.cells, 0));
|
|
168
|
+
const bar = spans.map((span, index) => (
|
|
169
|
+
boardPalette(index)(BOARD_BAR_CELL.repeat(span.cells))
|
|
170
|
+
)).join("");
|
|
171
|
+
return `${bar}${dim(BOARD_REMAINDER_CELL.repeat(remaining))}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function barUnderline(sections, windowTokens, cells) {
|
|
175
|
+
const spans = boardSpans(sections, windowTokens, cells);
|
|
176
|
+
const pieces = [];
|
|
177
|
+
for (const [index, span] of spans.entries()) {
|
|
178
|
+
const label = boardShortLabel(span);
|
|
179
|
+
const labelWidth = textWidth(label);
|
|
180
|
+
if (label && span.cells >= labelWidth + 1) {
|
|
181
|
+
pieces.push(boardPalette(index)(label));
|
|
182
|
+
pieces.push(dim(BOARD_UNDERLINE_CELL.repeat(span.cells - labelWidth)));
|
|
183
|
+
} else {
|
|
184
|
+
pieces.push(dim(BOARD_UNDERLINE_CELL.repeat(span.cells)));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
pieces.push(dim(BOARD_UNDERLINE_CELL.repeat(Math.max(0, cells - spans.reduce((sum, span) => sum + span.cells, 0)))));
|
|
188
|
+
return pieces.join("");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function boardSpans(sections, windowTokens, cells) {
|
|
192
|
+
return sections.map((section, index) => ({
|
|
193
|
+
...section,
|
|
194
|
+
paletteIndex: index,
|
|
195
|
+
cells: Math.max(0, Math.round((boardNumber(section.tokens) / windowTokens) * cells)),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function contextSectionRows(sections, total, innerWidth) {
|
|
200
|
+
const nameWidth = Math.min(
|
|
201
|
+
Math.max(12, ...sections.map((section) => textWidth(boardText(section.label)))),
|
|
202
|
+
Math.max(12, innerWidth - 24),
|
|
203
|
+
);
|
|
204
|
+
const tokenTexts = sections.map((section) => formatBoardNumber(section.tokens));
|
|
205
|
+
const tokenWidth = Math.max(6, ...tokenTexts.map(textWidth));
|
|
206
|
+
const messageTexts = sections.map((section) => boardMessagesLabel(section.messages));
|
|
207
|
+
const messageWidth = Math.max(6, ...messageTexts.map(textWidth));
|
|
208
|
+
return sections.map((section, index) => {
|
|
209
|
+
const percent = total > 0 ? Math.round((boardNumber(section.tokens) / total) * 100) : 0;
|
|
210
|
+
return [
|
|
211
|
+
` ${padRight(clipSingleLine(section.label, nameWidth), nameWidth)}`,
|
|
212
|
+
padLeft(tokenTexts[index], tokenWidth),
|
|
213
|
+
padLeft(`${percent}%`, 4),
|
|
214
|
+
padLeft(messageTexts[index], messageWidth),
|
|
215
|
+
].join(" ");
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function boardMessagesLabel(count) {
|
|
220
|
+
const value = Math.max(0, boardNumber(count));
|
|
221
|
+
return `${formatBoardNumber(value)} ${value === 1 ? "msg" : "msgs"}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function usageHeroLine(totals) {
|
|
225
|
+
const input = Math.max(0, boardNumber(totals.input));
|
|
226
|
+
const cached = Math.max(0, boardNumber(totals.cached));
|
|
227
|
+
const hit = input > 0 ? Math.round((cached / input) * 100) : 0;
|
|
228
|
+
return [
|
|
229
|
+
`Input ${boardCompact(totals.input)}`,
|
|
230
|
+
`Cache hit ${boardCompact(cached)}·${hit}%`,
|
|
231
|
+
`Output ${boardCompact(totals.output)}`,
|
|
232
|
+
`Reasoning ${boardCompact(totals.reasoning)}`,
|
|
233
|
+
].join(" ");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function usageHeroLines(totals, inner) {
|
|
237
|
+
const line = usageHeroLine(totals);
|
|
238
|
+
if (textWidth(stripAnsi(line)) <= inner) {
|
|
239
|
+
return [line];
|
|
240
|
+
}
|
|
241
|
+
return [
|
|
242
|
+
`Input ${boardCompact(totals.input)} Cache hit ${boardCompact(totals.cached)}`,
|
|
243
|
+
`Output ${boardCompact(totals.output)} Reasoning ${boardCompact(totals.reasoning)}`,
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function byDayRows(byDay, innerWidth) {
|
|
248
|
+
const values = byDay.map((row) => boardCompact(row?.tokens));
|
|
249
|
+
const valueWidth = Math.max(5, ...values.map(textWidth));
|
|
250
|
+
const peak = Math.max(1, ...byDay.map((row) => Math.max(0, boardNumber(row?.tokens))));
|
|
251
|
+
const barCells = Math.max(3, innerWidth - 11 - valueWidth);
|
|
252
|
+
return byDay.map((row, index) => {
|
|
253
|
+
const filled = Math.max(row && boardNumber(row.tokens) > 0 ? 1 : 0, Math.round((Math.max(0, boardNumber(row?.tokens)) / peak) * barCells));
|
|
254
|
+
const bar = accent(BOARD_BAR_CELL.repeat(Math.min(barCells, filled)));
|
|
255
|
+
const region = bar + " ".repeat(Math.max(0, barCells - Math.min(barCells, filled)));
|
|
256
|
+
return ` ${boardDayLabel(row?.day)} ${region} ${padLeft(values[index], valueWidth)}`;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function boardDayLabel(day) {
|
|
261
|
+
const text = boardText(day);
|
|
262
|
+
return /^\d{2}-\d{2}$/.test(text) ? text : " ".repeat(5);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function rightRows(rows, innerWidth) {
|
|
266
|
+
const valueWidth = Math.max(5, ...rows.map((row) => textWidth(row[1])));
|
|
267
|
+
const labelWidth = Math.max(1, innerWidth - 2 - valueWidth - 2);
|
|
268
|
+
const clipped = rows.map((row) => clipSingleLine(row[0], labelWidth));
|
|
269
|
+
const columnWidth = Math.max(1, ...clipped.map(textWidth));
|
|
270
|
+
return rows.map((row, index) => ` ${padRight(clipped[index], columnWidth)} ${padLeft(row[1], valueWidth)}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function sessionRowLabel(row) {
|
|
274
|
+
const updated = boardText(row?.updated_at);
|
|
275
|
+
const day = /^\d{4}-\d{2}-\d{2}/.test(updated) ? updated.slice(5, 10) : "";
|
|
276
|
+
return [day, boardText(row?.session_id)].filter(Boolean).join(" ");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function boardShortLabel(section) {
|
|
280
|
+
if (BOARD_SECTION_SHORT_LABELS[section.key]) {
|
|
281
|
+
return BOARD_SECTION_SHORT_LABELS[section.key];
|
|
282
|
+
}
|
|
283
|
+
if (String(section.key || "").startsWith("tool:")) {
|
|
284
|
+
return String(section.key).slice(5);
|
|
285
|
+
}
|
|
286
|
+
const label = boardText(section.label);
|
|
287
|
+
return label.split("·")[0].trim().slice(0, 12);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function boardPalette(index) {
|
|
291
|
+
const painter = paint[BOARD_PALETTE_ROLES[index % BOARD_PALETTE_ROLES.length]];
|
|
292
|
+
return typeof painter === "function" ? painter : (text) => text;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function boardPanel({ title, lines, footer, page, frameWidth, plain }) {
|
|
296
|
+
const index = Math.max(1, boardNumber(page?.index) || 1);
|
|
297
|
+
const count = Math.max(index, boardNumber(page?.count) || 1);
|
|
298
|
+
if (plain) {
|
|
299
|
+
// Interaction hints are meaningless in frame-less pipe output.
|
|
300
|
+
return [bold(clipSingleLine(title, frameWidth)), ...lines].join("\n");
|
|
301
|
+
}
|
|
302
|
+
const inner = frameWidth - 4;
|
|
303
|
+
const pageLabel = count > 1 ? `${index}/${count}` : "";
|
|
304
|
+
const titlePart = ` ${clipSingleLine(title, inner - pageLabel.length - 4)} `;
|
|
305
|
+
const dashes = Math.max(1, frameWidth - 2 - textWidth(titlePart) - (pageLabel ? pageLabel.length + 2 : 0));
|
|
306
|
+
const top = `┌${accent(titlePart)}${dim("─".repeat(dashes))}${pageLabel ? ` ${dim(pageLabel)} ` : ""}┐`;
|
|
307
|
+
const footerLines = footer ? [dim(footer)] : [];
|
|
308
|
+
const bodyRows = [...lines, ...footerLines].map((line) => `${dim("│")} ${padRight(line, inner)} ${dim("│")}`);
|
|
309
|
+
const bottom = `${dim("└")}${dim("─".repeat(frameWidth - 2))}${dim("┘")}`;
|
|
310
|
+
return [top, ...bodyRows, bottom].join("\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function boardEmptyLines(frameWidth, label) {
|
|
314
|
+
const inner = frameWidth - 4;
|
|
315
|
+
return [
|
|
316
|
+
boardCentered(bold(label || "Nothing sampled yet"), inner),
|
|
317
|
+
boardCentered(dim("Send a message first, then try again."), inner),
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function boardCentered(text, inner) {
|
|
322
|
+
return `${" ".repeat(Math.max(0, Math.floor((inner - textWidth(text)) / 2)))}${text}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isBoardRecord(value) {
|
|
326
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function boardText(value) {
|
|
330
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function boardNumber(value) {
|
|
334
|
+
const number = Number(value);
|
|
335
|
+
return Number.isFinite(number) ? number : 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatBoardNumber(value) {
|
|
339
|
+
const number = Math.max(0, Math.round(boardNumber(value)));
|
|
340
|
+
return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function boardCompact(value) {
|
|
344
|
+
const number = Math.max(0, boardNumber(value));
|
|
345
|
+
if (number >= 1e6) {
|
|
346
|
+
return `${(number / 1e6).toFixed(2)}M`;
|
|
347
|
+
}
|
|
348
|
+
if (number >= 1e5) {
|
|
349
|
+
return `${Math.round(number / 1e3)}K`;
|
|
350
|
+
}
|
|
351
|
+
if (number >= 1e3) {
|
|
352
|
+
return `${(number / 1e3).toFixed(1)}K`.replace(/\.0K$/, "K");
|
|
353
|
+
}
|
|
354
|
+
return String(Math.round(number));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const BOARD_BAR_CELL = "█";
|
|
358
|
+
const BOARD_REMAINDER_CELL = "░";
|
|
359
|
+
const BOARD_UNDERLINE_CELL = "▔";
|
|
360
|
+
|
|
8
361
|
|
|
9
362
|
export function startupText(info = {}, width) {
|
|
10
363
|
const header = startupBannerText(info, width);
|
|
@@ -14,8 +367,8 @@ export function startupText(info = {}, width) {
|
|
|
14
367
|
return sections.filter(Boolean).join("\n\n");
|
|
15
368
|
}
|
|
16
369
|
|
|
17
|
-
export function promptText(info = {},
|
|
18
|
-
return inputPromptFrame(promptHeaderLine(info, frameWidth), state, frameWidth);
|
|
370
|
+
export function promptText(info = {}, stats = {}, state = {}, frameWidth) {
|
|
371
|
+
return inputPromptFrame(promptHeaderLine(info, stats, frameWidth), state, frameWidth);
|
|
19
372
|
}
|
|
20
373
|
|
|
21
374
|
export function promptActivityLine(state = {}) {
|
|
@@ -27,11 +380,23 @@ export function promptActivityLine(state = {}) {
|
|
|
27
380
|
return ` ${accent(activityFrame(state.frame))} ${bold(label)} ${dim(`(${elapsed}) ctrl+c interrupt`)}`;
|
|
28
381
|
}
|
|
29
382
|
|
|
383
|
+
export function promptHintLine(state = {}) {
|
|
384
|
+
if (state.menuOpen) {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
const text = state.inputMode === "question"
|
|
388
|
+
? " ↑↓ choose · enter confirm · esc cancel"
|
|
389
|
+
: state.running
|
|
390
|
+
? " enter steer · tab queue · ctrl+c stop · ctrl+b tasks"
|
|
391
|
+
: " enter send · ↑↓ history · / commands · ? help";
|
|
392
|
+
return dim(clipCells(text, composerWidth(state.frameWidth)));
|
|
393
|
+
}
|
|
394
|
+
|
|
30
395
|
export function promptPlaceholderText() {
|
|
31
396
|
return "Ask Rind to do anything";
|
|
32
397
|
}
|
|
33
398
|
|
|
34
|
-
export function userInputText(text, width) {
|
|
399
|
+
export function userInputText(text, width, source = "") {
|
|
35
400
|
const lines = messageLines(text);
|
|
36
401
|
if (!lines.length) {
|
|
37
402
|
return "";
|
|
@@ -40,7 +405,8 @@ export function userInputText(text, width) {
|
|
|
40
405
|
const physicalLines = lines.flatMap((line) => (
|
|
41
406
|
wrapTextCells(line, contentWidth, contentWidth).map((chunk) => ` ${chunk.text}`)
|
|
42
407
|
));
|
|
43
|
-
|
|
408
|
+
const origin = source ? dim(` · via ${source}`) : "";
|
|
409
|
+
return `${accent("▷")} ${bold("You")}${origin}\n${physicalLines.join("\n")}`;
|
|
44
410
|
}
|
|
45
411
|
|
|
46
412
|
export function assistantHeaderText() {
|
|
@@ -197,6 +563,7 @@ export function questionMenuFrame(
|
|
|
197
563
|
editing = false,
|
|
198
564
|
customLabel = "Type your own answer",
|
|
199
565
|
width = 76,
|
|
566
|
+
editorCursor = null,
|
|
200
567
|
) {
|
|
201
568
|
const entries = [
|
|
202
569
|
...(Array.isArray(options) ? options : []),
|
|
@@ -230,14 +597,24 @@ export function questionMenuFrame(
|
|
|
230
597
|
lastPushedLine = lines.length - 1;
|
|
231
598
|
}
|
|
232
599
|
if (active && editing && isCustom) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
600
|
+
if (editorCursor) {
|
|
601
|
+
cursor = customAnswerCursor(
|
|
602
|
+
customText,
|
|
603
|
+
editorCursor,
|
|
604
|
+
Math.max(0, firstPushedLine),
|
|
605
|
+
firstPrefixWidth,
|
|
606
|
+
Math.max(1, width - 4),
|
|
607
|
+
);
|
|
608
|
+
} else {
|
|
609
|
+
const cursorLine = customText ? Math.max(0, lastPushedLine) : Math.max(0, firstPushedLine);
|
|
610
|
+
const cursorColumn = customText
|
|
611
|
+
? textWidth(lines[cursorLine])
|
|
612
|
+
: firstPrefixWidth;
|
|
613
|
+
cursor = {
|
|
614
|
+
line: cursorLine,
|
|
615
|
+
column: Math.max(0, cursorColumn),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
241
618
|
}
|
|
242
619
|
if (option.description) {
|
|
243
620
|
const descriptionLines = wrapQuestionLines(option.description, Math.max(1, width - 6));
|
|
@@ -257,6 +634,23 @@ function wrapQuestionLines(value, width) {
|
|
|
257
634
|
return wrapTextCells(text, Math.max(1, width), Math.max(1, width)).map((chunk) => chunk.text);
|
|
258
635
|
}
|
|
259
636
|
|
|
637
|
+
function customAnswerCursor(customText, editorCursor, firstRow, prefixWidth, labelWidth) {
|
|
638
|
+
const rawLines = String(customText || "").split("\n");
|
|
639
|
+
const line = Math.min(rawLines.length - 1, Math.max(0, Math.floor(Number(editorCursor.line) || 0)));
|
|
640
|
+
const column = Math.min(
|
|
641
|
+
graphemes(rawLines[line]).length,
|
|
642
|
+
Math.max(0, Math.floor(Number(editorCursor.column) || 0)),
|
|
643
|
+
);
|
|
644
|
+
const before = [...rawLines.slice(0, line), graphemes(rawLines[line]).slice(0, column).join("")]
|
|
645
|
+
.join(" ")
|
|
646
|
+
.trim();
|
|
647
|
+
const chunks = wrapTextCells(before, Math.max(1, labelWidth), Math.max(1, labelWidth));
|
|
648
|
+
return {
|
|
649
|
+
line: firstRow + chunks.length - 1,
|
|
650
|
+
column: prefixWidth + textWidth(chunks[chunks.length - 1].text),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
260
654
|
export function backgroundMonitorText(tasks = [], selectedIndex = 0, selectedTask = null, width = 76) {
|
|
261
655
|
const items = Array.isArray(tasks) ? tasks : [];
|
|
262
656
|
const lines = [dim(" ←→ page · ↑↓/j/k select · esc/ctrl+b close")];
|
|
@@ -433,10 +827,6 @@ export function commandResultText(text, detail = "") {
|
|
|
433
827
|
return `${green("✓")} ${bold(clipSingleLine(text, 96))}${extra ? dim(` — ${extra}`) : ""}`;
|
|
434
828
|
}
|
|
435
829
|
|
|
436
|
-
export function modelUsageText() {
|
|
437
|
-
return notice("Model command", "/model set <name>");
|
|
438
|
-
}
|
|
439
|
-
|
|
440
830
|
export function contextBuiltLine(event) {
|
|
441
831
|
const decisions = event.decisions && typeof event.decisions === "object" ? event.decisions : {};
|
|
442
832
|
if (!decisions.rind_docs_truncated) {
|
|
@@ -448,10 +838,6 @@ export function contextBuiltLine(event) {
|
|
|
448
838
|
return notice("Context trimmed", `RIND.md: ${clipSingleLine(scopes, 96)}`);
|
|
449
839
|
}
|
|
450
840
|
|
|
451
|
-
export function unknownCommandText() {
|
|
452
|
-
return notice("Unknown command", "type / to browse commands or ? for shortcuts");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
841
|
function notice(label, ...details) {
|
|
456
842
|
const lines = [`${accent("◆")} ${bold(label)}`];
|
|
457
843
|
for (const detail of details.flat()) {
|
|
@@ -462,18 +848,18 @@ function notice(label, ...details) {
|
|
|
462
848
|
return lines.join("\n");
|
|
463
849
|
}
|
|
464
850
|
|
|
465
|
-
export function toolRequestedLine(event) {
|
|
851
|
+
export function toolRequestedLine(event) {
|
|
466
852
|
const name = event.tool_name || "unknown";
|
|
467
853
|
const detail = toolDetail(name, parseJsonObject(event.args_preview));
|
|
468
854
|
const label = toolLabel(name);
|
|
469
855
|
const line = `${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${label}`;
|
|
470
|
-
return indentToolText(detail ? `${line}\n${dim(toolDetailLine(name, detail))}` : line);
|
|
471
|
-
}
|
|
856
|
+
return indentToolText(detail ? `${line}\n${dim(toolDetailLine(name, detail))}` : line);
|
|
857
|
+
}
|
|
472
858
|
|
|
473
|
-
export function toolStartedLine(event) {
|
|
474
|
-
const name = event.tool_name || "tool";
|
|
475
|
-
return indentToolText(`${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${toolLabel(name)}`);
|
|
476
|
-
}
|
|
859
|
+
export function toolStartedLine(event) {
|
|
860
|
+
const name = event.tool_name || "tool";
|
|
861
|
+
return indentToolText(`${accent("◌")} ${bold("Tool")} ${dim("·")} ${toolActiveVerb(name)} ${toolLabel(name)}`);
|
|
862
|
+
}
|
|
477
863
|
|
|
478
864
|
export function toolResultLine(event, fileChange) {
|
|
479
865
|
const name = event.tool_name || "unknown";
|
|
@@ -483,7 +869,7 @@ export function toolResultLine(event, fileChange) {
|
|
|
483
869
|
const suffix = event.error_type ? ` (${event.error_type})` : "";
|
|
484
870
|
const detail = toolErrorDetail(event.result);
|
|
485
871
|
const line = `${red("⊘")} ${bold("Tool")} ${dim("·")} ${label} failed in ${duration}${suffix}`;
|
|
486
|
-
return indentToolText(detail ? `${line}\n${dim(detailLine(detail))}` : line);
|
|
872
|
+
return indentToolText(detail ? `${line}\n${dim(detailLine(detail))}` : line);
|
|
487
873
|
}
|
|
488
874
|
const result = toolResultSummary(event.result);
|
|
489
875
|
if (result.status === "running" && (name === "bash" || name === "bash_output")) {
|
|
@@ -492,53 +878,41 @@ export function toolResultLine(event, fileChange) {
|
|
|
492
878
|
: "command running in background";
|
|
493
879
|
const line = `${accent("◌")} ${bold("Tool")} ${dim("·")} ${runningText} in ${duration}`;
|
|
494
880
|
const output = result.output;
|
|
495
|
-
return indentToolText([line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
|
|
496
|
-
.filter(Boolean)
|
|
497
|
-
.join("\n"));
|
|
881
|
+
return indentToolText([line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
|
|
882
|
+
.filter(Boolean)
|
|
883
|
+
.join("\n"));
|
|
498
884
|
}
|
|
499
885
|
const line = result.exitCode
|
|
500
886
|
? `${red("⊘")} ${bold("Tool")} ${dim("·")} ${label} exited ${result.exitCode} in ${duration}`
|
|
501
887
|
: `${green("◉")} ${bold("Tool")} ${dim("·")} ${completedToolText(name, label)} in ${duration}`;
|
|
502
888
|
const output = result.output;
|
|
503
|
-
return indentToolText([line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
|
|
504
|
-
.filter(Boolean)
|
|
505
|
-
.join("\n"));
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function indentToolText(value) {
|
|
509
|
-
return String(value || "")
|
|
510
|
-
.split("\n")
|
|
511
|
-
.map((line) => ` ${line}`)
|
|
512
|
-
.join("\n");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
export function planUpdatedLine(plan) {
|
|
516
|
-
const items = Array.isArray(plan) ? plan : [];
|
|
517
|
-
if (!items.length) {
|
|
518
|
-
return ` ${green("◉")} ${bold("Plan cleared")}`;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const lines = [` ${green("◉")} ${bold("Plan updated")}`];
|
|
522
|
-
for (const item of items) {
|
|
523
|
-
const step = clipSingleLine(item?.step, detailTextWidth());
|
|
524
|
-
if (step) {
|
|
525
|
-
lines.push(` ${planStatusIcon(item?.status)} ${step}`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return lines.join("\n");
|
|
889
|
+
return indentToolText([line, output ? dim(detailLine(output)) : "", fileChangeLine(fileChange)]
|
|
890
|
+
.filter(Boolean)
|
|
891
|
+
.join("\n"));
|
|
529
892
|
}
|
|
530
893
|
|
|
531
|
-
|
|
532
|
-
return
|
|
894
|
+
function indentToolText(value) {
|
|
895
|
+
return String(value || "")
|
|
896
|
+
.split("\n")
|
|
897
|
+
.map((line) => ` ${line}`)
|
|
898
|
+
.join("\n");
|
|
533
899
|
}
|
|
534
900
|
|
|
535
|
-
export function
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
901
|
+
export function planUpdatedLine(plan) {
|
|
902
|
+
const items = Array.isArray(plan) ? plan : [];
|
|
903
|
+
if (!items.length) {
|
|
904
|
+
return ` ${green("◉")} ${bold("Plan cleared")}`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const lines = [` ${green("◉")} ${bold("Plan updated")}`];
|
|
908
|
+
for (const item of items) {
|
|
909
|
+
const step = clipSingleLine(item?.step, detailTextWidth());
|
|
910
|
+
if (step) {
|
|
911
|
+
lines.push(` ${planStatusIcon(item?.status)} ${step}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return lines.join("\n");
|
|
915
|
+
}
|
|
542
916
|
|
|
543
917
|
export function errorLine(error) {
|
|
544
918
|
const detail = clipSingleLine(error, 120);
|
|
@@ -966,19 +1340,6 @@ function nonZeroExitCode(value) {
|
|
|
966
1340
|
return Number.isInteger(code) && code !== 0 ? code : 0;
|
|
967
1341
|
}
|
|
968
1342
|
|
|
969
|
-
function progressMessage(payload) {
|
|
970
|
-
if (!payload || typeof payload !== "object") {
|
|
971
|
-
return "";
|
|
972
|
-
}
|
|
973
|
-
for (const key of ["message", "status", "text"]) {
|
|
974
|
-
const value = clipSingleLine(payload[key], 120);
|
|
975
|
-
if (value) {
|
|
976
|
-
return value;
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return "";
|
|
980
|
-
}
|
|
981
|
-
|
|
982
1343
|
function fileChangeLine(fileChange) {
|
|
983
1344
|
if (!fileChange || typeof fileChange !== "object") {
|
|
984
1345
|
return "";
|
|
@@ -1062,23 +1423,6 @@ function userInputContentWidth(width) {
|
|
|
1062
1423
|
return Number.isFinite(columns) && columns > 0 ? Math.max(1, Math.floor(columns - 2)) : 78;
|
|
1063
1424
|
}
|
|
1064
1425
|
|
|
1065
|
-
function formatDuration(durationMs) {
|
|
1066
|
-
const value = Number(durationMs || 0);
|
|
1067
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
1068
|
-
return "0ms";
|
|
1069
|
-
}
|
|
1070
|
-
if (value < 1000) {
|
|
1071
|
-
return `${Math.trunc(value)}ms`;
|
|
1072
|
-
}
|
|
1073
|
-
if (value >= 60000) {
|
|
1074
|
-
const totalSeconds = Math.round(value / 1000);
|
|
1075
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1076
|
-
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
1077
|
-
return `${minutes}m ${seconds}s`;
|
|
1078
|
-
}
|
|
1079
|
-
return `${(value / 1000).toFixed(2)}s`;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
1426
|
function formatActivityDuration(durationMs) {
|
|
1083
1427
|
const value = Math.max(0, Number(durationMs || 0));
|
|
1084
1428
|
if (!Number.isFinite(value) || value < 60000) {
|
|
@@ -1193,6 +1537,10 @@ function inputPromptFrame(header = "", state = {}, frameWidth) {
|
|
|
1193
1537
|
if (header) {
|
|
1194
1538
|
lines.push(header);
|
|
1195
1539
|
}
|
|
1540
|
+
const hint = promptHintLine({ ...state, frameWidth });
|
|
1541
|
+
if (hint) {
|
|
1542
|
+
lines.push(hint);
|
|
1543
|
+
}
|
|
1196
1544
|
lines.push(inputDivider(frameWidth));
|
|
1197
1545
|
lines.push(" ▷ ");
|
|
1198
1546
|
return lines.join("\n");
|
|
@@ -1226,6 +1574,7 @@ function pendingInputLines(entries, frameWidth) {
|
|
|
1226
1574
|
const hints = [];
|
|
1227
1575
|
if (entries.some((entry) => entry?.mode === "follow_up")) {
|
|
1228
1576
|
hints.push("alt+up recall queue");
|
|
1577
|
+
hints.push("alt+right steer now");
|
|
1229
1578
|
}
|
|
1230
1579
|
if (entries.some((entry) => entry?.mode === "steering")) {
|
|
1231
1580
|
hints.push("alt+down recall steer");
|
|
@@ -1246,11 +1595,15 @@ function padRight(text, width) {
|
|
|
1246
1595
|
return `${text}${" ".repeat(Math.max(0, width - visibleLength(text)))}`;
|
|
1247
1596
|
}
|
|
1248
1597
|
|
|
1598
|
+
function padLeft(text, width) {
|
|
1599
|
+
return `${" ".repeat(Math.max(0, width - visibleLength(text)))}${text}`;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1249
1602
|
function visibleLength(text) {
|
|
1250
1603
|
return textWidth(text);
|
|
1251
1604
|
}
|
|
1252
1605
|
|
|
1253
|
-
function promptHeaderLine(info, frameWidth) {
|
|
1606
|
+
function promptHeaderLine(info, stats = {}, frameWidth) {
|
|
1254
1607
|
const backgroundCount = Number(info.background_count);
|
|
1255
1608
|
const delegateCount = Number(info.delegate_count);
|
|
1256
1609
|
const taskHints = [];
|
|
@@ -1263,6 +1616,7 @@ function promptHeaderLine(info, frameWidth) {
|
|
|
1263
1616
|
const taskHint = taskHints.length
|
|
1264
1617
|
? dim(` · ${taskHints.join(" ")} (ctrl+b monitor)`)
|
|
1265
1618
|
: "";
|
|
1619
|
+
const ctxSegment = contextUsageSegment(stats);
|
|
1266
1620
|
const model = singleLine(info.model);
|
|
1267
1621
|
const effort = singleLine(info.reasoning_effort);
|
|
1268
1622
|
const cwd = middleClip(info.cwd, 56);
|
|
@@ -1270,15 +1624,25 @@ function promptHeaderLine(info, frameWidth) {
|
|
|
1270
1624
|
const effortSegment = effort ? `${dim(" · ")}${promptModel(effort)}` : "";
|
|
1271
1625
|
if (model && cwd) {
|
|
1272
1626
|
const separator = " · ";
|
|
1273
|
-
const pathWidth = width - visibleLength(model) - visibleLength(separator) - visibleLength(taskHint) - visibleLength(effortSegment);
|
|
1627
|
+
const pathWidth = width - visibleLength(model) - visibleLength(separator) - visibleLength(taskHint) - visibleLength(effortSegment) - visibleLength(ctxSegment);
|
|
1274
1628
|
if (pathWidth > 0) {
|
|
1275
|
-
return ` ${promptModel(clipSingleLine(model, width))}${effortSegment}${dim(separator)}${promptPath(clipSingleLine(cwd, pathWidth))}${taskHint}`;
|
|
1629
|
+
return ` ${promptModel(clipSingleLine(model, width))}${effortSegment}${dim(separator)}${promptPath(clipSingleLine(cwd, pathWidth))}${taskHint}${ctxSegment}`;
|
|
1276
1630
|
}
|
|
1277
1631
|
}
|
|
1278
1632
|
if (model) {
|
|
1279
|
-
return ` ${promptModel(clipSingleLine(model, width))}${taskHint}`;
|
|
1633
|
+
return ` ${promptModel(clipSingleLine(model, width))}${taskHint}${ctxSegment}`;
|
|
1634
|
+
}
|
|
1635
|
+
return cwd ? ` ${promptPath(clipSingleLine(cwd, width))}${taskHint}${ctxSegment}` : "";
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function contextUsageSegment(stats) {
|
|
1639
|
+
const ratio = Number(stats.context_usage_percent);
|
|
1640
|
+
if (!Number.isFinite(ratio) || ratio <= 0) {
|
|
1641
|
+
return "";
|
|
1280
1642
|
}
|
|
1281
|
-
|
|
1643
|
+
const percent = `ctx ${Math.round(ratio * 100)}%`;
|
|
1644
|
+
const tone = occupancyTone(ratio);
|
|
1645
|
+
return `${dim(" · ")}${tone === "err" ? red(percent) : tone === "warn" ? paint.warning(percent) : percent}`;
|
|
1282
1646
|
}
|
|
1283
1647
|
|
|
1284
1648
|
function composerWidth(frameWidth) {
|