@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
@@ -0,0 +1,675 @@
1
+ import { clipCells } from "./text-width.js";
2
+ import { paintRaw } from "./theme.js";
3
+
4
+ function accent(text) {
5
+ return paintRaw.accent(text);
6
+ }
7
+
8
+ function green(text) {
9
+ return paintRaw.success(text);
10
+ }
11
+
12
+ function red(text) {
13
+ return paintRaw.danger(text);
14
+ }
15
+
16
+ function dim(text) {
17
+ return paintRaw.dim(text);
18
+ }
19
+
20
+ function bold(text) {
21
+ return paintRaw.bold(text);
22
+ }
23
+
24
+ const COLLAPSED_BODY_CAPS = {
25
+ bash: 5,
26
+ bash_output: 5,
27
+ edit_file: 20,
28
+ write_file: 20,
29
+ grep: 0,
30
+ glob: 0,
31
+ delegate: 1,
32
+ };
33
+
34
+ const EXPANDED_HARD_CAPS = {
35
+ bash: 400,
36
+ bash_output: 400,
37
+ edit_file: 400,
38
+ write_file: 400,
39
+ grep: 200,
40
+ glob: 200,
41
+ read_file: 40,
42
+ fetch_web_page: 24,
43
+ delegate: 24,
44
+ search_web: 16,
45
+ };
46
+
47
+ const ELAPSED_TITLE_TOOLS = new Set(["bash", "bash_output", "delegate", "search_web", "fetch_web_page"]);
48
+ const DURATION_TITLE_TOOLS = new Set(["bash", "bash_output", "delegate"]);
49
+
50
+ export function parseToolArguments(event) {
51
+ if (event && typeof event.arguments === "object" && event.arguments !== null) {
52
+ return event.arguments;
53
+ }
54
+ try {
55
+ const parsed = JSON.parse(String(event?.args_preview || ""));
56
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+
62
+ export function parseToolResult(result) {
63
+ try {
64
+ const parsed = JSON.parse(String(result || ""));
65
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
66
+ return {};
67
+ }
68
+ return parsed;
69
+ } catch {
70
+ return {};
71
+ }
72
+ }
73
+
74
+ // Blocks created from argless events (bare announce, session replay) can
75
+ // recover their key arguments from the result payload at finish time.
76
+ export function argsFromResult(name, result) {
77
+ const { data = {}, meta = {} } = resultDataFromRaw(result);
78
+ const derived = {};
79
+ const put = (key, value) => {
80
+ if (value !== undefined && value !== null && value !== "" && derived[key] === undefined) {
81
+ derived[key] = value;
82
+ }
83
+ };
84
+ switch (name) {
85
+ case "read_file":
86
+ put("path", meta.path);
87
+ break;
88
+ case "write_file":
89
+ case "edit_file":
90
+ put("file_path", Array.isArray(meta.files) ? meta.files[0]?.path : undefined);
91
+ break;
92
+ case "glob":
93
+ put("pattern", meta.pattern);
94
+ put("path", meta.path);
95
+ break;
96
+ case "grep":
97
+ put("pattern", meta.pattern);
98
+ put("path", meta.path);
99
+ put("glob", meta.glob);
100
+ break;
101
+ case "search_web":
102
+ put("query", meta.query);
103
+ break;
104
+ case "fetch_web_page":
105
+ put("url", meta.url);
106
+ break;
107
+ case "bash_output":
108
+ put("bg_id", data.bg_id);
109
+ break;
110
+ case "delegate":
111
+ put("agent_id", data.agent_id);
112
+ break;
113
+ case "skill":
114
+ case "skill_create":
115
+ put("name", data.name);
116
+ break;
117
+ case "agent_create":
118
+ put("name", data.name);
119
+ put("agent_id", data.agent_id);
120
+ break;
121
+ default:
122
+ break;
123
+ }
124
+ return derived;
125
+ }
126
+
127
+ function resultDataFromRaw(result) {
128
+ const payload = parseToolResult(result);
129
+ return {
130
+ payload,
131
+ data: payload.data && typeof payload.data === "object" ? payload.data : {},
132
+ meta: payload.meta && typeof payload.meta === "object" ? payload.meta : {},
133
+ };
134
+ }
135
+
136
+ export function renderToolRunning(context, width) {
137
+ const renderer = TOOL_RENDERERS[context.name] || GENERIC_RENDERER;
138
+ const elapsedSeconds = Math.floor((context.elapsedMs || 0) / 1000);
139
+ const elapsed = ELAPSED_TITLE_TOOLS.has(context.name) ? dim(` · ${elapsedSeconds}s`) : "";
140
+ const lines = [
141
+ clipCells(`${runningGlyph()} ${renderer.runningMain(context, width)}${elapsed}`, Math.max(1, width)),
142
+ ];
143
+ if (context.progressMessage) {
144
+ lines.push(dim(` ↳ ${clipText(context.progressMessage, width, 8)}`));
145
+ }
146
+ return lines;
147
+ }
148
+
149
+ export function renderToolFinished(context, width) {
150
+ const renderer = TOOL_RENDERERS[context.name] || GENERIC_RENDERER;
151
+ const state = finishState(context.event);
152
+ const lines = [clipCells(renderer.finished(context, width, state), Math.max(1, width))];
153
+ if (state.kind === "error") {
154
+ const detail = errorDetail(context.event, width);
155
+ if (detail) {
156
+ lines.push(detail);
157
+ }
158
+ return lines;
159
+ }
160
+ const bodyLimit = context.expanded ? EXPANDED_HARD_CAPS[context.name] ?? 200 : COLLAPSED_BODY_CAPS[context.name] ?? 0;
161
+ const produced = renderer.body ? renderer.body(context, width, bodyLimit) : [];
162
+ const normalized = normalizeBody(produced);
163
+ if (!normalized.lines.length) {
164
+ if (normalized.total > 0) {
165
+ lines.push(bodyFooter(normalized.total, context.expanded));
166
+ }
167
+ return lines;
168
+ }
169
+ lines.push(...normalized.lines);
170
+ const hidden = Math.max(0, normalized.total - normalized.lines.length);
171
+ if (hidden > 0) {
172
+ lines.push(bodyFooter(hidden, context.expanded));
173
+ }
174
+ return lines;
175
+ }
176
+
177
+ function normalizeBody(produced) {
178
+ if (Array.isArray(produced)) {
179
+ return { lines: produced, total: produced.length };
180
+ }
181
+ return {
182
+ lines: Array.isArray(produced?.lines) ? produced.lines : [],
183
+ total: Number.isFinite(produced?.total) ? produced.total : (Array.isArray(produced?.lines) ? produced.lines.length : 0),
184
+ };
185
+ }
186
+
187
+ function finishState(event) {
188
+ const status = String(event?.status || "completed");
189
+ if (status === "completed") {
190
+ return { kind: "ok", status };
191
+ }
192
+ if (status === "cancelled") {
193
+ return { kind: "cancelled", status };
194
+ }
195
+ return { kind: "error", status };
196
+ }
197
+
198
+ function runningGlyph() {
199
+ return accent("◌");
200
+ }
201
+
202
+ function okGlyph() {
203
+ return green("◉");
204
+ }
205
+
206
+ function errorGlyph() {
207
+ return red("⊘");
208
+ }
209
+
210
+ function cancelledGlyph() {
211
+ return dim("◌");
212
+ }
213
+
214
+ function titleFor(state, main) {
215
+ if (state.kind === "error") {
216
+ return `${errorGlyph()} ${main}`;
217
+ }
218
+ if (state.kind === "cancelled") {
219
+ return `${cancelledGlyph()} ${main} ${dim("(cancelled)")}`;
220
+ }
221
+ return `${okGlyph()} ${main}`;
222
+ }
223
+
224
+ function durationPart(name, event) {
225
+ if (!DURATION_TITLE_TOOLS.has(name)) {
226
+ return "";
227
+ }
228
+ return dim(` · ${formatDuration(event?.duration_ms)}`);
229
+ }
230
+
231
+ function formatDuration(durationMs) {
232
+ const value = Number(durationMs || 0);
233
+ if (!Number.isFinite(value) || value <= 0) {
234
+ return "0ms";
235
+ }
236
+ if (value < 1000) {
237
+ return `${Math.trunc(value)}ms`;
238
+ }
239
+ if (value >= 60000) {
240
+ const totalSeconds = Math.round(value / 1000);
241
+ const minutes = Math.floor(totalSeconds / 60);
242
+ const seconds = String(totalSeconds % 60).padStart(2, "0");
243
+ return `${minutes}m ${seconds}s`;
244
+ }
245
+ return `${(value / 1000).toFixed(2)}s`;
246
+ }
247
+
248
+ function clipText(value, width, reserve = 4) {
249
+ return clipCells(singleLineText(value), Math.max(1, width - reserve));
250
+ }
251
+
252
+ function singleLineText(value) {
253
+ return String(value ?? "").replace(/[\r\n\t]+/g, " ").trim();
254
+ }
255
+
256
+ function bodyFooter(hidden, expanded) {
257
+ const hint = expanded ? "" : " · ctrl+o to expand";
258
+ return dim(` … (${hidden} more lines${hint})`);
259
+ }
260
+
261
+ function errorDetail(event, width) {
262
+ const payload = parseToolResult(event?.result);
263
+ const message = singleLineText(payload.error || event?.error_type || "");
264
+ return message ? dim(` ↳ ${clipText(message, width, 8)}`) : "";
265
+ }
266
+
267
+ function resultData(context) {
268
+ const payload = parseToolResult(context.event?.result);
269
+ return {
270
+ payload,
271
+ data: payload.data && typeof payload.data === "object" ? payload.data : {},
272
+ meta: payload.meta && typeof payload.meta === "object" ? payload.meta : {},
273
+ };
274
+ }
275
+
276
+ function shellOutputLines(context, limit) {
277
+ const { payload, data, meta } = resultData(context);
278
+ const combined = `${data.stdout || ""}\n${data.stderr || ""}`;
279
+ const all = combined.split(/\r\n|\r|\n/).map((line) => line.replace(/\t/g, " "));
280
+ while (all.length && !all.at(-1).trim()) {
281
+ all.pop();
282
+ }
283
+ const lines = all.slice(-limit).map((line) => dim(` ${clipText(line, context.width ?? 80, 6)}`));
284
+ if (meta.truncated && meta.total_bytes) {
285
+ lines.push(dim(` … output truncated (${Number(meta.total_bytes)} bytes total)`));
286
+ }
287
+ return { lines, total: all.length };
288
+ }
289
+
290
+ function diffCountSuffix(context) {
291
+ let added = 0;
292
+ let removed = 0;
293
+ const changes = Array.isArray(context.fileChange?.lines) ? context.fileChange.lines : [];
294
+ if (changes.length) {
295
+ for (const change of changes) {
296
+ if (change?.kind === "added") added += 1;
297
+ else if (change?.kind === "removed") removed += 1;
298
+ }
299
+ } else {
300
+ const { meta } = resultData(context);
301
+ const files = Array.isArray(meta.files) ? meta.files : [];
302
+ for (const file of files) {
303
+ added += Number(file?.added_lines) || 0;
304
+ removed += Number(file?.removed_lines) || 0;
305
+ }
306
+ }
307
+ if (!added && !removed) {
308
+ return "";
309
+ }
310
+ return `${dim(" (")}${green(`+${added}`)} ${red(`-${removed}`)}${dim(")")}`;
311
+ }
312
+
313
+ function diffBodyLines(context, limit) {
314
+ const changes = Array.isArray(context.fileChange?.lines)
315
+ ? context.fileChange.lines.map((change) => ({ kind: change?.kind, text: String(change?.text ?? "") }))
316
+ : unifiedDiffLines(context);
317
+ return {
318
+ lines: changes.slice(0, limit).map((change) => diffLine(change, context.width ?? 80)),
319
+ total: changes.length,
320
+ };
321
+ }
322
+
323
+ function unifiedDiffLines(context) {
324
+ const { meta } = resultData(context);
325
+ const files = Array.isArray(meta.files) ? meta.files : [];
326
+ const lines = [];
327
+ for (const file of files) {
328
+ for (const raw of String(file?.diff || "").split(/\r\n|\r|\n/)) {
329
+ if (!raw || raw.startsWith("---") || raw.startsWith("+++")) {
330
+ continue;
331
+ }
332
+ const kind = raw.startsWith("+") ? "added" : raw.startsWith("-") ? "removed" : "context";
333
+ lines.push({ kind, text: raw.slice(1) });
334
+ }
335
+ }
336
+ return lines;
337
+ }
338
+
339
+ function diffLine(change, width) {
340
+ const added = change.kind === "added";
341
+ const removed = change.kind === "removed";
342
+ const marker = added ? "+" : removed ? "-" : " ";
343
+ const style = added ? green : removed ? red : (text) => dim(text);
344
+ return `${dim(` ${marker} `)}${style(clipText(change.text, width, 8))}`;
345
+ }
346
+
347
+ function matchCount(context, key = "count") {
348
+ const { payload, meta } = resultData(context);
349
+ const explicit = Number(meta[key]);
350
+ if (Number.isFinite(explicit) && explicit > 0) {
351
+ return explicit;
352
+ }
353
+ return Array.isArray(payload.data) ? payload.data.length : 0;
354
+ }
355
+
356
+ const BASH_RENDERER = {
357
+ runningMain(context, width) {
358
+ return `${bold("$")} ${commandArg(context.args.command, width, 8)}`;
359
+ },
360
+ finished(context, width, state) {
361
+ const command = commandArg(context.args.command, width, 12);
362
+ const { data } = resultData(context);
363
+ if (state.kind === "ok" && String(data.status) === "running") {
364
+ return `${runningGlyph()} ${bold("$")} ${command}`;
365
+ }
366
+ let main = `${bold("$")} ${command}`;
367
+ const exitCode = Number(data.exit_code);
368
+ if (state.kind !== "cancelled" && Number.isInteger(exitCode) && exitCode !== 0) {
369
+ main += ` ${red(`exit ${exitCode}`)}`;
370
+ }
371
+ return titleFor(state, main) + durationPart("bash", context.event);
372
+ },
373
+ body(context, width, limit) {
374
+ const { data } = resultData(context);
375
+ if (String(data.status) === "running") {
376
+ const bgId = singleLineText(data.bg_id);
377
+ const line = bgId ? `command running in background (bg ${bgId})` : "command running in background";
378
+ return [dim(` ↳ ${clipText(line, width, 8)}`)];
379
+ }
380
+ return shellOutputLines(context, limit);
381
+ },
382
+ };
383
+
384
+ const BASH_OUTPUT_RENDERER = {
385
+ runningMain(context, width) {
386
+ return `${bold("bg")} ${bgIdArg(context.args.bg_id, width, 10)}`;
387
+ },
388
+ finished(context, width, state) {
389
+ const { data } = resultData(context);
390
+ const id = context.args.bg_id || data.bg_id;
391
+ let main = `${bold("bg")} ${bgIdArg(id, width, 14)}`;
392
+ if (state.kind === "cancelled") {
393
+ main += ` ${dim("(cancelled)")}`;
394
+ }
395
+ const waitMs = Number(data.wait_ms ?? data.elapsed_ms);
396
+ const waited = state.kind === "ok" && Number.isFinite(waitMs) && waitMs > 0
397
+ ? dim(` · waited ${formatDuration(waitMs)}`)
398
+ : "";
399
+ return titleFor(state, main) + waited + durationPart("bash_output", context.event);
400
+ },
401
+ body(context, width, limit) {
402
+ return shellOutputLines(context, limit);
403
+ },
404
+ };
405
+
406
+ const READ_RENDERER = {
407
+ runningMain(context, width) {
408
+ return `read ${accentPath(context.args.path || context.args.file_path, width, 10)}`;
409
+ },
410
+ finished(context, width, state) {
411
+ const range = readRange(context.args);
412
+ const main = `read ${accentPath(context.args.path || context.args.file_path, width, 18)}${range}`;
413
+ if (state.kind !== "ok") {
414
+ return titleFor(state, main);
415
+ }
416
+ const { meta } = resultData(context);
417
+ const morePages = meta.truncated || (meta.next_offset !== undefined && meta.next_offset !== null);
418
+ return titleFor(state, morePages ? `${main} ${dim("(more pages)")}` : main);
419
+ },
420
+ body(context, width, limit) {
421
+ if (limit <= 0) {
422
+ return { lines: [], total: 0 };
423
+ }
424
+ const { payload } = resultData(context);
425
+ const content = typeof payload.data === "string" ? payload.data : "";
426
+ const lines = content.split(/\r\n|\r|\n/).filter((line) => line.trim());
427
+ return {
428
+ lines: lines.slice(0, limit).map((line) => dim(` ${clipText(line, width, 6)}`)),
429
+ total: lines.length,
430
+ };
431
+ },
432
+ };
433
+
434
+ function readRange(args) {
435
+ const offset = Number(args.offset);
436
+ if (!Number.isFinite(offset) || offset <= 0) {
437
+ return "";
438
+ }
439
+ const limit = Number(args.limit);
440
+ return dim(`:${offset}${Number.isFinite(limit) && limit > 0 ? `-${offset + limit}` : "+"}`);
441
+ }
442
+
443
+ function mutationRenderer(verb) {
444
+ return {
445
+ runningMain(context, width) {
446
+ return `${verb} ${accentPath(context.args.file_path, width, 10)}`;
447
+ },
448
+ finished(context, width, state) {
449
+ const main = `${verb} ${accentPath(context.args.file_path, width, 18)}${diffCountSuffix(context)}`;
450
+ return titleFor(state, main);
451
+ },
452
+ body(context, width, limit) {
453
+ return diffBodyLines(context, limit);
454
+ },
455
+ };
456
+ }
457
+
458
+ const GREP_RENDERER = {
459
+ runningMain(context, width) {
460
+ return searchTitle(context, width);
461
+ },
462
+ finished(context, width, state) {
463
+ const base = searchTitle(context, width);
464
+ const count = matchCount(context);
465
+ return titleFor(state, count ? `${base} ${dim(`· ${count} matches`)}` : base);
466
+ },
467
+ body(context, width, limit) {
468
+ const { payload } = resultData(context);
469
+ const rows = Array.isArray(payload.data) ? payload.data : [];
470
+ return {
471
+ lines: rows.slice(0, limit).map((row) => dim(
472
+ ` ${clipText(`${row?.file}:${row?.line}: ${String(row?.text ?? "").replace(/\t/g, " ")}`, width, 6)}`,
473
+ )),
474
+ total: rows.length,
475
+ };
476
+ },
477
+ };
478
+
479
+ function searchTitle(context, width) {
480
+ const pattern = clipText(context.args.pattern, Math.max(12, Math.floor(width / 2)), 4);
481
+ const location = context.args.path ? dim(` in ${clipText(context.args.path, Math.max(8, Math.floor(width / 3)), 4)}`) : "";
482
+ const glob = context.args.glob ? dim(` (${clipText(context.args.glob, 24, 0)})`) : "";
483
+ return `grep ${accent(`/${pattern}/`)}${location}${glob}`;
484
+ }
485
+
486
+ const GLOB_RENDERER = {
487
+ runningMain(context, width) {
488
+ return globTitle(context, width);
489
+ },
490
+ finished(context, width, state) {
491
+ const base = globTitle(context, width);
492
+ const count = matchCount(context);
493
+ return titleFor(state, count ? `${base} ${dim(`· ${count} matches`)}` : base);
494
+ },
495
+ body(context, width, limit) {
496
+ const { payload } = resultData(context);
497
+ const rows = Array.isArray(payload.data) ? payload.data : [];
498
+ return {
499
+ lines: rows.slice(0, limit).map((row) => dim(` ${clipText(row?.path, width, 6)}`)),
500
+ total: rows.length,
501
+ };
502
+ },
503
+ };
504
+
505
+ function globTitle(context, width) {
506
+ const pattern = clipText(context.args.pattern, Math.max(12, Math.floor(width / 2)), 4);
507
+ const location = context.args.path ? dim(` in ${clipText(context.args.path, Math.max(8, Math.floor(width / 3)), 4)}`) : "";
508
+ return `glob ${accent(pattern)}${location}`;
509
+ }
510
+
511
+ const SEARCH_WEB_RENDERER = {
512
+ runningMain(context, width) {
513
+ return `search ${quoteArg(context.args.query, width)}`;
514
+ },
515
+ finished(context, width, state) {
516
+ const base = `search ${quoteArg(context.args.query, width)}`;
517
+ const count = matchCount(context, "matches");
518
+ return titleFor(state, count ? `${base} ${dim(`· ${count} results`)}` : base);
519
+ },
520
+ body(context, width, limit) {
521
+ const { payload } = resultData(context);
522
+ const rows = Array.isArray(payload.data) ? payload.data : [];
523
+ const maxEntries = Math.ceil(limit / 2);
524
+ const lines = [];
525
+ for (const row of rows.slice(0, maxEntries)) {
526
+ lines.push(` ${clipText(row?.title, width, 6)}`);
527
+ lines.push(dim(` ${clipText(row?.url, width, 6)}`));
528
+ if (lines.length >= limit) {
529
+ break;
530
+ }
531
+ }
532
+ return { lines: lines.slice(0, limit), total: rows.length * 2 };
533
+ },
534
+ };
535
+
536
+ const FETCH_WEB_PAGE_RENDERER = {
537
+ runningMain(context, width) {
538
+ return `fetch ${accent(clipText(context.args.url, width, 10))}`;
539
+ },
540
+ finished(context, width, state) {
541
+ const { payload, meta } = resultData(context);
542
+ const size = typeof payload.data === "string" && payload.data ? dim(` · ${payload.data.length} chars`) : "";
543
+ const truncated = meta.truncated ? dim(" · truncated") : "";
544
+ return titleFor(state, `fetch ${accent(clipText(context.args.url, width, 14))}${size}${truncated}`);
545
+ },
546
+ body(context, width, limit) {
547
+ const { payload } = resultData(context);
548
+ const content = typeof payload.data === "string" ? payload.data : "";
549
+ const lines = content.split(/\r\n|\r|\n/).filter((line) => line.trim());
550
+ return {
551
+ lines: lines.slice(0, limit).map((line) => dim(` ${clipText(line, width, 6)}`)),
552
+ total: lines.length,
553
+ };
554
+ },
555
+ };
556
+
557
+ const DELEGATE_RENDERER = {
558
+ runningMain(context, width) {
559
+ return `delegate → ${agentArg(context.args.agent_id, width)}`;
560
+ },
561
+ finished(context, width, state) {
562
+ const { data } = resultData(context);
563
+ const status = singleLineText(data.status) || (state.kind === "ok" ? "completed" : "");
564
+ const agentId = context.args.agent_id || data.agent_id;
565
+ const main = `delegate → ${agentArg(agentId, width)}${status ? dim(` · ${status}`) : ""}`;
566
+ return titleFor(state, main) + durationPart("delegate", context.event);
567
+ },
568
+ body(context, width, limit) {
569
+ const { data } = resultData(context);
570
+ const summaryLines = String(data.summary || "").split(/\r\n|\r|\n/).filter((line) => line.trim());
571
+ const lines = summaryLines.slice(0, limit).map((line) => dim(` ${clipText(line, width, 6)}`));
572
+ const published = Array.isArray(data.published_paths) ? data.published_paths.length : 0;
573
+ if (published && lines.length < limit) {
574
+ lines.push(dim(` published ${published} path${published === 1 ? "" : "s"}`));
575
+ }
576
+ return { lines, total: summaryLines.length + (published ? 1 : 0) };
577
+ },
578
+ };
579
+
580
+ const AGENT_CREATE_RENDERER = {
581
+ runningMain(context, width) {
582
+ return `agent-create ${agentArg(context.args.name || context.args.agent_id, width)}`;
583
+ },
584
+ finished(context, width, state) {
585
+ const { data } = resultData(context);
586
+ const root = singleLineText(data.workspace_root);
587
+ const name = agentArg(context.args.name || data.agent_id, width);
588
+ const rootPart = root ? dim(` · ${clipText(root, Math.max(8, width - 24), 0)}`) : "";
589
+ return titleFor(state, `agent-create ${name}${rootPart}`);
590
+ },
591
+ };
592
+
593
+ function skillRenderer(verb) {
594
+ return {
595
+ runningMain(context, width) {
596
+ return `${verb} ${agentArg(context.args.name, width)}`;
597
+ },
598
+ finished(context, width, state) {
599
+ const { data } = resultData(context);
600
+ const path = singleLineText(data.path);
601
+ const pathPart = path ? dim(` · ${clipText(path, Math.max(8, width - 16), 0)}`) : "";
602
+ const overwritten = data.overwritten ? dim(" · overwritten") : "";
603
+ const name = agentArg(context.args.name || data.name, width);
604
+ return titleFor(state, `${verb} ${name}${pathPart}${overwritten}`);
605
+ },
606
+ };
607
+ }
608
+
609
+ const GENERIC_RENDERER = {
610
+ runningMain(context, width) {
611
+ const label = humanToolName(context.name);
612
+ const arg = firstKeyArg(context.args, width);
613
+ return `${label}${arg ? ` ${arg}` : ""}`;
614
+ },
615
+ finished(context, width, state) {
616
+ const label = humanToolName(context.name);
617
+ const arg = firstKeyArg(context.args, width);
618
+ return titleFor(state, `${label}${arg ? ` ${dim(arg)}` : ""}`);
619
+ },
620
+ };
621
+
622
+ function humanToolName(name) {
623
+ return String(name || "tool").replace(/[_-]+/g, " ").trim() || "tool";
624
+ }
625
+
626
+ function firstKeyArg(args, width) {
627
+ for (const key of ["file_path", "path", "query", "url", "pattern", "command", "bg_id", "name", "agent_id"]) {
628
+ const value = singleLineText(args?.[key]);
629
+ if (value) {
630
+ return clipText(value, width, 8);
631
+ }
632
+ }
633
+ return "";
634
+ }
635
+
636
+ function accentPath(value, width, reserve) {
637
+ const text = singleLineText(value);
638
+ return text ? accent(clipText(text, width, reserve)) : dim("…");
639
+ }
640
+
641
+ function agentArg(value, width) {
642
+ const text = singleLineText(value);
643
+ return text ? accent(clipText(text, Math.min(32, width), 0)) : dim("…");
644
+ }
645
+
646
+ function quoteArg(value, width) {
647
+ const text = singleLineText(value);
648
+ return `"${text ? clipText(text, width, 8) : ""}"`;
649
+ }
650
+
651
+ function commandArg(value, width, reserve) {
652
+ const text = singleLineText(value);
653
+ return text ? clipText(text, width, reserve) : dim("…");
654
+ }
655
+
656
+ function bgIdArg(value, width, reserve) {
657
+ const text = singleLineText(value);
658
+ return text ? clipText(text, width, reserve) : dim("…");
659
+ }
660
+
661
+ export const TOOL_RENDERERS = {
662
+ bash: BASH_RENDERER,
663
+ bash_output: BASH_OUTPUT_RENDERER,
664
+ read_file: READ_RENDERER,
665
+ edit_file: mutationRenderer("edit"),
666
+ write_file: mutationRenderer("write"),
667
+ grep: GREP_RENDERER,
668
+ glob: GLOB_RENDERER,
669
+ search_web: SEARCH_WEB_RENDERER,
670
+ fetch_web_page: FETCH_WEB_PAGE_RENDERER,
671
+ delegate: DELEGATE_RENDERER,
672
+ agent_create: AGENT_CREATE_RENDERER,
673
+ skill: skillRenderer("skill"),
674
+ skill_create: skillRenderer("skill-create"),
675
+ };