@agentuity/coder 1.0.37

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 (92) hide show
  1. package/README.md +57 -0
  2. package/dist/chain-preview.d.ts +55 -0
  3. package/dist/chain-preview.d.ts.map +1 -0
  4. package/dist/chain-preview.js +472 -0
  5. package/dist/chain-preview.js.map +1 -0
  6. package/dist/client.d.ts +43 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +402 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/commands.d.ts +22 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +99 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/footer.d.ts +34 -0
  15. package/dist/footer.d.ts.map +1 -0
  16. package/dist/footer.js +249 -0
  17. package/dist/footer.js.map +1 -0
  18. package/dist/handlers.d.ts +24 -0
  19. package/dist/handlers.d.ts.map +1 -0
  20. package/dist/handlers.js +83 -0
  21. package/dist/handlers.js.map +1 -0
  22. package/dist/hub-overlay.d.ts +107 -0
  23. package/dist/hub-overlay.d.ts.map +1 -0
  24. package/dist/hub-overlay.js +1794 -0
  25. package/dist/hub-overlay.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +1585 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/output-viewer.d.ts +49 -0
  31. package/dist/output-viewer.d.ts.map +1 -0
  32. package/dist/output-viewer.js +389 -0
  33. package/dist/output-viewer.js.map +1 -0
  34. package/dist/overlay.d.ts +40 -0
  35. package/dist/overlay.d.ts.map +1 -0
  36. package/dist/overlay.js +225 -0
  37. package/dist/overlay.js.map +1 -0
  38. package/dist/protocol.d.ts +118 -0
  39. package/dist/protocol.d.ts.map +1 -0
  40. package/dist/protocol.js +3 -0
  41. package/dist/protocol.js.map +1 -0
  42. package/dist/remote-session.d.ts +113 -0
  43. package/dist/remote-session.d.ts.map +1 -0
  44. package/dist/remote-session.js +645 -0
  45. package/dist/remote-session.js.map +1 -0
  46. package/dist/remote-tui.d.ts +40 -0
  47. package/dist/remote-tui.d.ts.map +1 -0
  48. package/dist/remote-tui.js +606 -0
  49. package/dist/remote-tui.js.map +1 -0
  50. package/dist/renderers.d.ts +34 -0
  51. package/dist/renderers.d.ts.map +1 -0
  52. package/dist/renderers.js +669 -0
  53. package/dist/renderers.js.map +1 -0
  54. package/dist/review.d.ts +15 -0
  55. package/dist/review.d.ts.map +1 -0
  56. package/dist/review.js +154 -0
  57. package/dist/review.js.map +1 -0
  58. package/dist/titlebar.d.ts +3 -0
  59. package/dist/titlebar.d.ts.map +1 -0
  60. package/dist/titlebar.js +59 -0
  61. package/dist/titlebar.js.map +1 -0
  62. package/dist/todo/index.d.ts +3 -0
  63. package/dist/todo/index.d.ts.map +1 -0
  64. package/dist/todo/index.js +3 -0
  65. package/dist/todo/index.js.map +1 -0
  66. package/dist/todo/store.d.ts +6 -0
  67. package/dist/todo/store.d.ts.map +1 -0
  68. package/dist/todo/store.js +43 -0
  69. package/dist/todo/store.js.map +1 -0
  70. package/dist/todo/types.d.ts +13 -0
  71. package/dist/todo/types.d.ts.map +1 -0
  72. package/dist/todo/types.js +2 -0
  73. package/dist/todo/types.js.map +1 -0
  74. package/package.json +44 -0
  75. package/src/chain-preview.ts +621 -0
  76. package/src/client.ts +515 -0
  77. package/src/commands.ts +132 -0
  78. package/src/footer.ts +305 -0
  79. package/src/handlers.ts +113 -0
  80. package/src/hub-overlay.ts +2324 -0
  81. package/src/index.ts +1907 -0
  82. package/src/output-viewer.ts +480 -0
  83. package/src/overlay.ts +294 -0
  84. package/src/protocol.ts +157 -0
  85. package/src/remote-session.ts +800 -0
  86. package/src/remote-tui.ts +707 -0
  87. package/src/renderers.ts +740 -0
  88. package/src/review.ts +201 -0
  89. package/src/titlebar.ts +63 -0
  90. package/src/todo/index.ts +2 -0
  91. package/src/todo/store.ts +49 -0
  92. package/src/todo/types.ts +14 -0
@@ -0,0 +1,669 @@
1
+ /**
2
+ * TUI tool renderers for Hub tools.
3
+ *
4
+ * Each renderer provides a compact renderCall (one-line summary of the invocation)
5
+ * and a renderResult (collapsed / expanded views of the result).
6
+ * Renderers are looked up by tool name and spread into the registerTool() call.
7
+ */
8
+ import { Box, Text, Container } from '@mariozechner/pi-tui';
9
+ // ──────────────────────────────────────────────
10
+ // Line-safety helper — must be declared before SimpleText so
11
+ // render() can reference it without temporal-dead-zone issues.
12
+ // ──────────────────────────────────────────────
13
+ /** Pre-render safety net — truncate lines before they reach render(). Reduced from 200 to 160. */
14
+ const SAFE_LINE_WIDTH = 160;
15
+ function safeLine(line) {
16
+ return line.length > SAFE_LINE_WIDTH ? line.slice(0, SAFE_LINE_WIDTH - 3) + '...' : line;
17
+ }
18
+ /**
19
+ * Truncate a string (possibly containing ANSI escape codes) to fit within
20
+ * a given visible width. Handles escape sequences properly so colours are
21
+ * never broken — a RESET is appended when truncation occurs.
22
+ */
23
+ export function truncateToWidth(line, maxWidth) {
24
+ if (maxWidth <= 0)
25
+ return '';
26
+ const normalized = line.replace(/\t/g, ' ');
27
+ // Fast path: no ANSI codes and short enough
28
+ if (normalized.length <= maxWidth && !normalized.includes('\x1b'))
29
+ return normalized;
30
+ // Strip ANSI to measure visible length
31
+ // eslint-disable-next-line no-control-regex
32
+ const visible = normalized.replace(/\x1b\[[0-9;]*m/g, '');
33
+ if (visible.length <= maxWidth)
34
+ return normalized;
35
+ // Need to truncate — walk through respecting ANSI escape sequences
36
+ let vis = 0;
37
+ let i = 0;
38
+ const target = Math.max(0, maxWidth - 3); // room for "..."
39
+ while (i < normalized.length && vis < target) {
40
+ if (normalized[i] === '\x1b') {
41
+ // Skip entire ANSI escape sequence
42
+ const end = normalized.indexOf('m', i);
43
+ if (end !== -1) {
44
+ i = end + 1;
45
+ }
46
+ else {
47
+ i++;
48
+ }
49
+ }
50
+ else {
51
+ vis++;
52
+ i++;
53
+ }
54
+ }
55
+ return normalized.slice(0, i) + '\x1b[0m...';
56
+ }
57
+ // ──────────────────────────────────────────────
58
+ // Lightweight text component used by simpler renderers.
59
+ // Complex renderers (task, parallel_tasks) use Box/Text/Container from pi-tui.
60
+ // ──────────────────────────────────────────────
61
+ export class SimpleText {
62
+ text;
63
+ constructor(text) {
64
+ this.text = text;
65
+ }
66
+ render(width) {
67
+ return this.text.split('\n').map((line) => truncateToWidth(line, width));
68
+ }
69
+ invalidate() {
70
+ // no-op — we don't cache
71
+ }
72
+ }
73
+ // ──────────────────────────────────────────────
74
+ // Helpers
75
+ // ──────────────────────────────────────────────
76
+ /** Extract plain-text from a tool result's content array. */
77
+ function resultText(result) {
78
+ return result.content
79
+ .filter((c) => 'text' in c && typeof c.text === 'string')
80
+ .map((c) => ('text' in c ? c.text : ''))
81
+ .join('\n');
82
+ }
83
+ /** Attempt to parse result text as JSON, returning undefined on failure. */
84
+ function tryParseJson(text) {
85
+ try {
86
+ return JSON.parse(text);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
92
+ /** Truncate a string to a max length, appending '\u2026' when truncated. */
93
+ function truncate(str, max) {
94
+ if (str.length <= max)
95
+ return str;
96
+ return str.slice(0, max - 1) + '\u2026';
97
+ }
98
+ // ──────────────────────────────────────────────
99
+ // Individual tool renderers
100
+ // ──────────────────────────────────────────────
101
+ function memorySearchRenderers() {
102
+ return {
103
+ renderCall(args, theme) {
104
+ const query = String(args['query'] ?? '');
105
+ const limit = args['limit'];
106
+ let text = theme.fg('toolTitle', theme.bold('memory search '));
107
+ text += theme.fg('accent', truncate(query, 60));
108
+ if (limit)
109
+ text += theme.fg('muted', ` (limit ${limit})`);
110
+ return new SimpleText(text);
111
+ },
112
+ renderResult(result, { expanded, isPartial }, theme) {
113
+ if (isPartial)
114
+ return new SimpleText(theme.fg('warning', 'Searching\u2026'));
115
+ const raw = resultText(result);
116
+ const parsed = tryParseJson(raw);
117
+ const items = Array.isArray(parsed) ? parsed : [];
118
+ let text = theme.fg('success', `${items.length} result${items.length !== 1 ? 's' : ''}`);
119
+ if (expanded && items.length > 0) {
120
+ const lines = items.slice(0, 10).map((item) => {
121
+ const key = truncate(String(item['key'] ?? item['id'] ?? '?'), 120);
122
+ const score = typeof item['score'] === 'number'
123
+ ? ` (${item['score'].toFixed(2)})`
124
+ : '';
125
+ return ` ${theme.fg('accent', key)}${theme.fg('muted', score)}`;
126
+ });
127
+ text += '\n' + lines.join('\n');
128
+ if (items.length > 10)
129
+ text += theme.fg('muted', `\n \u2026and ${items.length - 10} more`);
130
+ }
131
+ return new SimpleText(text);
132
+ },
133
+ };
134
+ }
135
+ function memoryStoreRenderers() {
136
+ return {
137
+ renderCall(args, theme) {
138
+ const key = String(args['key'] ?? '');
139
+ let text = theme.fg('toolTitle', theme.bold('memory store '));
140
+ text += theme.fg('accent', truncate(key, 60));
141
+ return new SimpleText(text);
142
+ },
143
+ renderResult(_result, { isPartial }, theme) {
144
+ if (isPartial)
145
+ return new SimpleText(theme.fg('warning', 'Storing\u2026'));
146
+ return new SimpleText(theme.fg('success', 'Stored'));
147
+ },
148
+ };
149
+ }
150
+ function memoryGetRenderers() {
151
+ return {
152
+ renderCall(args, theme) {
153
+ const key = String(args['key'] ?? '');
154
+ let text = theme.fg('toolTitle', theme.bold('memory get '));
155
+ text += theme.fg('accent', truncate(key, 60));
156
+ return new SimpleText(text);
157
+ },
158
+ renderResult(result, { expanded, isPartial }, theme) {
159
+ if (isPartial)
160
+ return new SimpleText(theme.fg('warning', 'Loading\u2026'));
161
+ const raw = resultText(result);
162
+ const parsed = tryParseJson(raw);
163
+ if (!parsed) {
164
+ return new SimpleText(theme.fg('muted', raw ? 'Retrieved' : 'Not found'));
165
+ }
166
+ let text = theme.fg('success', 'Retrieved');
167
+ if (expanded) {
168
+ const preview = typeof parsed === 'object'
169
+ ? JSON.stringify(parsed, null, 2)
170
+ .split('\n')
171
+ .slice(0, 10)
172
+ .map(safeLine)
173
+ .join('\n')
174
+ : String(parsed);
175
+ text += '\n' + theme.fg('toolOutput', truncate(preview, 500));
176
+ }
177
+ return new SimpleText(text);
178
+ },
179
+ };
180
+ }
181
+ function memoryUpdateRenderers() {
182
+ return {
183
+ renderCall(args, theme) {
184
+ const key = String(args['key'] ?? '');
185
+ let text = theme.fg('toolTitle', theme.bold('memory update '));
186
+ text += theme.fg('accent', truncate(key, 60));
187
+ return new SimpleText(text);
188
+ },
189
+ renderResult(_result, { isPartial }, theme) {
190
+ if (isPartial)
191
+ return new SimpleText(theme.fg('warning', 'Updating\u2026'));
192
+ return new SimpleText(theme.fg('success', 'Updated'));
193
+ },
194
+ };
195
+ }
196
+ function memoryDeleteRenderers() {
197
+ return {
198
+ renderCall(args, theme) {
199
+ const key = String(args['key'] ?? '');
200
+ let text = theme.fg('toolTitle', theme.bold('memory delete '));
201
+ text += theme.fg('accent', truncate(key, 60));
202
+ return new SimpleText(text);
203
+ },
204
+ renderResult(_result, { isPartial }, theme) {
205
+ if (isPartial)
206
+ return new SimpleText(theme.fg('warning', 'Deleting\u2026'));
207
+ return new SimpleText(theme.fg('muted', 'Deleted'));
208
+ },
209
+ };
210
+ }
211
+ function memoryListRenderers() {
212
+ return {
213
+ renderCall(args, theme) {
214
+ const namespace = String(args['namespace'] ?? '');
215
+ const prefix = args['prefix'];
216
+ let text = theme.fg('toolTitle', theme.bold('memory list'));
217
+ if (namespace)
218
+ text += theme.fg('accent', ` ${truncate(namespace, 30)}`);
219
+ if (prefix)
220
+ text += theme.fg('accent', ` ${truncate(prefix, 40)}`);
221
+ return new SimpleText(text);
222
+ },
223
+ renderResult(result, { expanded, isPartial }, theme) {
224
+ if (isPartial)
225
+ return new SimpleText(theme.fg('warning', 'Listing\u2026'));
226
+ const raw = resultText(result);
227
+ const parsed = tryParseJson(raw);
228
+ const keys = Array.isArray(parsed)
229
+ ? parsed
230
+ : parsed &&
231
+ typeof parsed === 'object' &&
232
+ Array.isArray(parsed['keys'])
233
+ ? parsed['keys']
234
+ : [];
235
+ let text = theme.fg('success', `${keys.length} key${keys.length !== 1 ? 's' : ''}`);
236
+ if (expanded && keys.length > 0) {
237
+ const lines = keys
238
+ .slice(0, 15)
239
+ .map((k) => ` ${theme.fg('accent', truncate(String(k), 120))}`);
240
+ text += '\n' + lines.join('\n');
241
+ if (keys.length > 15)
242
+ text += theme.fg('muted', `\n \u2026and ${keys.length - 15} more`);
243
+ }
244
+ return new SimpleText(text);
245
+ },
246
+ };
247
+ }
248
+ function context7SearchRenderers() {
249
+ return {
250
+ renderCall(args, theme) {
251
+ const library = String(args['libraryId'] ?? args['library'] ?? '');
252
+ const query = String(args['query'] ?? '');
253
+ let text = theme.fg('toolTitle', theme.bold('context7 '));
254
+ if (library)
255
+ text += theme.fg('accent', truncate(library, 30) + ' \u2014 ');
256
+ text += theme.fg('text', truncate(query, 50));
257
+ return new SimpleText(text);
258
+ },
259
+ renderResult(result, { expanded, isPartial }, theme) {
260
+ if (isPartial)
261
+ return new SimpleText(theme.fg('warning', 'Searching docs\u2026'));
262
+ const raw = resultText(result);
263
+ const parsed = tryParseJson(raw);
264
+ const snippets = Array.isArray(parsed)
265
+ ? parsed
266
+ : parsed &&
267
+ typeof parsed === 'object' &&
268
+ Array.isArray(parsed['snippets'])
269
+ ? parsed['snippets']
270
+ : [];
271
+ const count = snippets.length || (raw.length > 0 ? '?' : '0');
272
+ let text = theme.fg('success', `${count} snippet${count !== 1 ? 's' : ''}`);
273
+ if (expanded && snippets.length > 0) {
274
+ const lines = snippets.slice(0, 5).map((s) => {
275
+ const snip = s;
276
+ const title = String(snip['title'] ?? snip['name'] ?? '');
277
+ return ` ${theme.fg('accent', truncate(title, 80))}`;
278
+ });
279
+ text += '\n' + lines.join('\n');
280
+ if (snippets.length > 5)
281
+ text += theme.fg('muted', `\n \u2026and ${snippets.length - 5} more`);
282
+ }
283
+ return new SimpleText(text);
284
+ },
285
+ };
286
+ }
287
+ function grepAppSearchRenderers() {
288
+ return {
289
+ renderCall(args, theme) {
290
+ const query = String(args['query'] ?? '');
291
+ const lang = args['language'];
292
+ let text = theme.fg('toolTitle', theme.bold('grep.app '));
293
+ text += theme.fg('accent', truncate(query, 50));
294
+ if (lang) {
295
+ const langStr = Array.isArray(lang) ? lang.join(', ') : String(lang);
296
+ text += theme.fg('muted', ` [${langStr}]`);
297
+ }
298
+ return new SimpleText(text);
299
+ },
300
+ renderResult(result, { expanded, isPartial }, theme) {
301
+ if (isPartial)
302
+ return new SimpleText(theme.fg('warning', 'Searching GitHub\u2026'));
303
+ const raw = resultText(result);
304
+ const parsed = tryParseJson(raw);
305
+ const matches = Array.isArray(parsed)
306
+ ? parsed
307
+ : parsed &&
308
+ typeof parsed === 'object' &&
309
+ Array.isArray(parsed['results'])
310
+ ? parsed['results']
311
+ : [];
312
+ const count = matches.length || (raw.length > 0 ? '?' : '0');
313
+ let text = theme.fg('success', `${count} match${count !== 1 ? 'es' : ''}`);
314
+ if (expanded && matches.length > 0) {
315
+ const lines = matches.slice(0, 8).map((m) => {
316
+ const match = m;
317
+ const path = String(match['path'] ?? match['file'] ?? match['repo'] ?? '');
318
+ return ` ${theme.fg('accent', truncate(path, 80))}`;
319
+ });
320
+ text += '\n' + lines.join('\n');
321
+ if (matches.length > 8)
322
+ text += theme.fg('muted', `\n \u2026and ${matches.length - 8} more`);
323
+ }
324
+ return new SimpleText(text);
325
+ },
326
+ };
327
+ }
328
+ function sessionDashboardRenderers() {
329
+ return {
330
+ renderCall(_args, theme) {
331
+ return new SimpleText(theme.fg('toolTitle', theme.bold('session dashboard')));
332
+ },
333
+ renderResult(result, { isPartial }, theme) {
334
+ if (isPartial)
335
+ return new SimpleText(theme.fg('warning', 'Loading dashboard\u2026'));
336
+ const raw = resultText(result);
337
+ const summary = truncate(raw.replace(/\n/g, ' '), 80);
338
+ return new SimpleText(theme.fg('toolOutput', summary || 'OK'));
339
+ },
340
+ };
341
+ }
342
+ function sessionTodoCreateRenderers() {
343
+ return {
344
+ renderCall(args, theme) {
345
+ const title = String(args['title'] ?? '');
346
+ let text = theme.fg('toolTitle', theme.bold('session todo create '));
347
+ text += theme.fg('accent', truncate(title, 60));
348
+ return new SimpleText(text);
349
+ },
350
+ renderResult(result, { expanded, isPartial }, theme) {
351
+ if (isPartial)
352
+ return new SimpleText(theme.fg('warning', 'Creating todo\u2026'));
353
+ const raw = resultText(result);
354
+ const parsed = tryParseJson(raw);
355
+ const task = parsed && typeof parsed === 'object'
356
+ ? parsed['task']
357
+ : undefined;
358
+ if (!task)
359
+ return new SimpleText(theme.fg('toolOutput', truncate(raw.replace(/\n/g, ' '), 100)));
360
+ const id = String(task['id'] ?? '');
361
+ const status = String(task['status'] ?? 'open');
362
+ const priority = String(task['priority'] ?? 'none');
363
+ const title = String(task['title'] ?? '').trim();
364
+ const display = title.length > 0 ? truncate(title, 72) : truncate(id, 28);
365
+ let text = theme.fg('success', `${status} ${display}`);
366
+ text += theme.fg('dim', ` (${truncate(id, 20)} prio:${priority})`);
367
+ if (expanded && title.length > 0) {
368
+ text += '\n' + theme.fg('accent', truncate(title, 120));
369
+ }
370
+ return new SimpleText(text);
371
+ },
372
+ };
373
+ }
374
+ function sessionTodoUpdateRenderers() {
375
+ return {
376
+ renderCall(args, theme) {
377
+ const id = String(args['id'] ?? '');
378
+ const nextStatus = typeof args['status'] === 'string' ? ` -> ${String(args['status'])}` : '';
379
+ return new SimpleText(theme.fg('toolTitle', theme.bold('session todo update ')) +
380
+ theme.fg('accent', truncate(id, 24)) +
381
+ theme.fg('dim', nextStatus));
382
+ },
383
+ renderResult(result, { isPartial }, theme) {
384
+ if (isPartial)
385
+ return new SimpleText(theme.fg('warning', 'Updating todo\u2026'));
386
+ const raw = resultText(result);
387
+ const parsed = tryParseJson(raw);
388
+ const task = parsed && typeof parsed === 'object'
389
+ ? parsed['task']
390
+ : undefined;
391
+ if (!task)
392
+ return new SimpleText(theme.fg('toolOutput', truncate(raw.replace(/\n/g, ' '), 100)));
393
+ const id = String(task['id'] ?? '');
394
+ const status = String(task['status'] ?? 'open');
395
+ const title = String(task['title'] ?? '').trim();
396
+ const display = title.length > 0 ? truncate(title, 72) : truncate(id, 28);
397
+ return new SimpleText(theme.fg('success', `${status} ${display}`) + theme.fg('dim', ` (${truncate(id, 20)})`));
398
+ },
399
+ };
400
+ }
401
+ function sessionTodoListRenderers() {
402
+ return {
403
+ renderCall(args, theme) {
404
+ const status = args['status'] ? ` status:${String(args['status'])}` : '';
405
+ const assignee = args['assignee'] ? ` owner:${String(args['assignee'])}` : '';
406
+ const scope = args['scope'] ? ` scope:${String(args['scope'])}` : '';
407
+ return new SimpleText(theme.fg('toolTitle', theme.bold('session todos')) +
408
+ theme.fg('dim', `${scope}${status}${assignee}`));
409
+ },
410
+ renderResult(result, { expanded, isPartial }, theme) {
411
+ if (isPartial)
412
+ return new SimpleText(theme.fg('warning', 'Loading todos\u2026'));
413
+ const raw = resultText(result);
414
+ const parsed = tryParseJson(raw);
415
+ if (!parsed || typeof parsed !== 'object') {
416
+ return new SimpleText(theme.fg('toolOutput', truncate(raw.replace(/\n/g, ' '), 120)));
417
+ }
418
+ const count = typeof parsed['count'] === 'number' ? parsed['count'] : 0;
419
+ const summary = parsed['summary'] && typeof parsed['summary'] === 'object'
420
+ ? parsed['summary']
421
+ : {};
422
+ const todos = Array.isArray(parsed['todos'])
423
+ ? parsed['todos']
424
+ : [];
425
+ let text = theme.fg('success', `${count} todo${count === 1 ? '' : 's'}`);
426
+ text += theme.fg('dim', ` o:${Number(summary['open'] ?? 0)} ip:${Number(summary['in_progress'] ?? 0)} d:${Number(summary['done'] ?? 0)} c:${Number(summary['closed'] ?? 0)} x:${Number(summary['cancelled'] ?? 0)}`);
427
+ if (expanded && todos.length > 0) {
428
+ const lines = todos.slice(0, 20).map((todo) => {
429
+ const status = String(todo['status'] ?? 'open');
430
+ const marker = status === 'done'
431
+ ? theme.fg('success', '✓')
432
+ : status === 'in_progress'
433
+ ? theme.fg('accent', '●')
434
+ : status === 'cancelled' || status === 'closed'
435
+ ? theme.fg('error', 'x')
436
+ : theme.fg('warning', '○');
437
+ const id = truncate(String(todo['id'] ?? ''), 18);
438
+ const title = truncate(String(todo['title'] ?? ''), 72);
439
+ const owner = typeof todo['assignee'] === 'string' && todo['assignee'].length > 0
440
+ ? ` @${todo['assignee']}`
441
+ : '';
442
+ return ` ${marker} ${theme.fg('dim', id)} ${title}${theme.fg('muted', owner)}`;
443
+ });
444
+ text += '\n' + lines.join('\n');
445
+ if (todos.length > 20)
446
+ text += theme.fg('muted', `\n \u2026and ${todos.length - 20} more`);
447
+ }
448
+ return new SimpleText(text);
449
+ },
450
+ };
451
+ }
452
+ function sessionTodoCommentRenderers() {
453
+ return {
454
+ renderCall(args, theme) {
455
+ const id = String(args['id'] ?? '');
456
+ return new SimpleText(theme.fg('toolTitle', theme.bold('session todo comment ')) +
457
+ theme.fg('accent', truncate(id, 32)));
458
+ },
459
+ renderResult(result, { isPartial }, theme) {
460
+ if (isPartial)
461
+ return new SimpleText(theme.fg('warning', 'Adding comment\u2026'));
462
+ const raw = resultText(result);
463
+ const parsed = tryParseJson(raw);
464
+ if (parsed && parsed['commented'] === true) {
465
+ const taskId = typeof parsed['taskId'] === 'string' ? truncate(parsed['taskId'], 20) : '';
466
+ return new SimpleText(theme.fg('success', `Comment saved${taskId ? ` (${taskId})` : ''}`));
467
+ }
468
+ return new SimpleText(theme.fg('toolOutput', truncate(raw.replace(/\n/g, ' '), 100)));
469
+ },
470
+ };
471
+ }
472
+ function sessionTodoAttachRenderers() {
473
+ return {
474
+ renderCall(args, theme) {
475
+ const id = String(args['id'] ?? '');
476
+ const name = String(args['name'] ?? '');
477
+ let text = theme.fg('toolTitle', theme.bold('session todo attach '));
478
+ text += theme.fg('accent', truncate(id, 24));
479
+ if (name)
480
+ text += theme.fg('dim', ` ${truncate(name, 40)}`);
481
+ return new SimpleText(text);
482
+ },
483
+ renderResult(result, { isPartial }, theme) {
484
+ if (isPartial)
485
+ return new SimpleText(theme.fg('warning', 'Attaching\u2026'));
486
+ const raw = resultText(result);
487
+ const parsed = tryParseJson(raw);
488
+ if (!parsed || typeof parsed !== 'object') {
489
+ return new SimpleText(theme.fg('toolOutput', truncate(raw.replace(/\n/g, ' '), 100)));
490
+ }
491
+ const count = typeof parsed['attachmentCount'] === 'number' ? parsed['attachmentCount'] : undefined;
492
+ const task = parsed['task'] && typeof parsed['task'] === 'object'
493
+ ? parsed['task']
494
+ : undefined;
495
+ const title = typeof task?.['title'] === 'string' ? truncate(task['title'], 56) : '';
496
+ const id = typeof task?.['id'] === 'string' ? truncate(task['id'], 20) : '';
497
+ const text = count !== undefined ? `Attachment saved (${count} total)` : 'Attachment saved';
498
+ const suffix = title ? ` ${title}${id ? ` (${id})` : ''}` : id ? ` (${id})` : '';
499
+ return new SimpleText(theme.fg('success', `${text}${suffix}`));
500
+ },
501
+ };
502
+ }
503
+ // ──────────────────────────────────────────────
504
+ // Registry
505
+ function taskRenderers() {
506
+ return {
507
+ renderCall(args, theme) {
508
+ const agent = String(args['subagent_type'] ?? '?');
509
+ const desc = String(args['description'] ?? '');
510
+ let text = theme.fg('accent', safeLine(agent));
511
+ if (desc)
512
+ text += theme.fg('dim', ` — ${truncate(desc, 60)}`);
513
+ return new Text(text, 0, 0);
514
+ },
515
+ renderResult(result, { expanded, isPartial }, theme) {
516
+ if (isPartial)
517
+ return new Text(theme.fg('warning', 'running...'), 0, 0);
518
+ const raw = resultText(result);
519
+ const lineCount = raw.split('\n').length;
520
+ // Detect agent failure — result starts with "Agent X failed:"
521
+ const isError = raw.startsWith('Agent ') && raw.includes('failed:');
522
+ if (isError) {
523
+ const bgFn = (t) => theme.bg('toolErrorBg', t);
524
+ const box = new Box(1, 0, bgFn);
525
+ let errorContent = theme.fg('error', 'failed');
526
+ if (expanded) {
527
+ errorContent +=
528
+ '\n' + theme.fg('error', raw.split('\n').slice(0, 10).map(safeLine).join('\n'));
529
+ }
530
+ else {
531
+ // Show first line of error in collapsed view
532
+ const firstLine = raw.split('\n')[0] || '';
533
+ errorContent += theme.fg('dim', ' ' + firstLine.slice(0, 80));
534
+ errorContent += theme.fg('muted', ' ctrl+h ctrl+shift+v|alt+shift+v');
535
+ }
536
+ box.addChild(new Text(errorContent, 0, 0));
537
+ return box;
538
+ }
539
+ // Try to extract token stats from the appended footer
540
+ // Pattern: _agent: Xms | Y in Z out tokens | $cost_
541
+ const statsMatch = raw.match(/_(\w+): (\d+)ms \| (\d+) in (\d+) out tokens \| \$([0-9.]+)_/);
542
+ let text = theme.fg('success', 'done');
543
+ if (statsMatch) {
544
+ const [, , durationMs, tokIn, tokOut, cost] = statsMatch;
545
+ const duration = Number(durationMs) >= 1000
546
+ ? `${(Number(durationMs) / 1000).toFixed(1)}s`
547
+ : `${durationMs}ms`;
548
+ text += theme.fg('dim', ` ${duration} \u2191${tokIn} \u2193${tokOut} $${cost}`);
549
+ }
550
+ else {
551
+ text += theme.fg('dim', ` (${lineCount} lines)`);
552
+ }
553
+ if (!expanded) {
554
+ text += theme.fg('muted', ' ctrl+h ctrl+shift+v|alt+shift+v');
555
+ }
556
+ if (expanded) {
557
+ const preview = raw.split('\n').slice(0, 20).map(safeLine).join('\n');
558
+ text += '\n' + theme.fg('dim', preview);
559
+ if (lineCount > 20)
560
+ text += theme.fg('muted', '\n...more ctrl+shift+v|alt+shift+v');
561
+ }
562
+ return new Text(text, 0, 0);
563
+ },
564
+ };
565
+ }
566
+ function parallelTasksRenderers() {
567
+ return {
568
+ renderCall(args, theme) {
569
+ const tasks = args['tasks'] ?? [];
570
+ const agents = tasks.map((t) => String(t['subagent_type'] ?? '?'));
571
+ const text = theme.fg('accent', agents.join(' + '));
572
+ return new Text(safeLine(text), 0, 0);
573
+ },
574
+ renderResult(result, { expanded, isPartial }, theme) {
575
+ if (isPartial)
576
+ return new Text(theme.fg('warning', 'running...'), 0, 0);
577
+ const raw = resultText(result);
578
+ // Parse agent names and statuses from ### headers in the raw output
579
+ // Format: "### agent_name (Xms)" for success, "### agent_name (FAILED)" for failure
580
+ const agentEntries = [];
581
+ const headerPattern = /^### (\S+) \((?:FAILED|(\d+)ms)\)/gm;
582
+ let match = headerPattern.exec(raw);
583
+ while (match !== null) {
584
+ agentEntries.push({ name: match[1] ?? '?', failed: match[2] === undefined });
585
+ match = headerPattern.exec(raw);
586
+ }
587
+ // Build chain visualization with status icons
588
+ // ✓ = success (green), ✗ = failed (red)
589
+ const chain = agentEntries
590
+ .map(({ name, failed }) => {
591
+ const icon = failed
592
+ ? theme.fg('error', '\u2717') // ✗
593
+ : theme.fg('success', '\u2713'); // ✓
594
+ const nameColor = failed ? 'error' : 'dim';
595
+ return `${icon} ${theme.fg(nameColor, name)}`;
596
+ })
597
+ .join(' ');
598
+ const lineCount = raw.split('\n').length;
599
+ const hasFailures = agentEntries.some((e) => e.failed);
600
+ // Build summary header
601
+ let summaryText = chain;
602
+ summaryText +=
603
+ '\n' +
604
+ theme.fg(hasFailures ? 'error' : 'success', hasFailures ? 'done (with failures)' : 'done');
605
+ summaryText += theme.fg('dim', ` (${lineCount} lines)`);
606
+ if (!expanded) {
607
+ summaryText += theme.fg('muted', ' ctrl+h ctrl+shift+v|alt+shift+v');
608
+ return new Text(summaryText, 0, 0);
609
+ }
610
+ // Expanded view: use Container to group summary + agent sections
611
+ const container = new Container();
612
+ container.addChild(new Text(summaryText, 0, 0));
613
+ // Split by ### headers to show each agent section separately
614
+ const sections = raw.split(/(?=^### )/m);
615
+ for (const section of sections) {
616
+ const trimmed = section.trim();
617
+ if (!trimmed)
618
+ continue;
619
+ const isFailed = trimmed.includes('(FAILED)');
620
+ const lines = trimmed.split('\n');
621
+ const preview = lines.slice(0, 15).map(safeLine).join('\n');
622
+ let sectionContent = preview;
623
+ if (lines.length > 15) {
624
+ sectionContent +=
625
+ '\n' +
626
+ theme.fg('muted', ` ...${lines.length - 15} more lines ctrl+shift+v|alt+shift+v`);
627
+ }
628
+ if (isFailed) {
629
+ // Failed sections get error background via Box
630
+ const box = new Box(1, 0, (t) => theme.bg('toolErrorBg', t));
631
+ box.addChild(new Text(theme.fg('error', sectionContent), 0, 0));
632
+ container.addChild(box);
633
+ }
634
+ else {
635
+ container.addChild(new Text(theme.fg('dim', sectionContent), 0, 0));
636
+ }
637
+ }
638
+ return container;
639
+ },
640
+ };
641
+ }
642
+ // ──────────────────────────────────────────────
643
+ const RENDERERS = {
644
+ memory_service_search: memorySearchRenderers,
645
+ memory_service_store: memoryStoreRenderers,
646
+ memory_service_get: memoryGetRenderers,
647
+ memory_service_update: memoryUpdateRenderers,
648
+ memory_service_delete: memoryDeleteRenderers,
649
+ memory_service_list: memoryListRenderers,
650
+ context7_search: context7SearchRenderers,
651
+ grep_app_search: grepAppSearchRenderers,
652
+ session_dashboard: sessionDashboardRenderers,
653
+ session_todo_create: sessionTodoCreateRenderers,
654
+ session_todo_update: sessionTodoUpdateRenderers,
655
+ session_todo_list: sessionTodoListRenderers,
656
+ session_todo_comment: sessionTodoCommentRenderers,
657
+ session_todo_attach: sessionTodoAttachRenderers,
658
+ task: taskRenderers,
659
+ parallel_tasks: parallelTasksRenderers,
660
+ };
661
+ /**
662
+ * Look up renderCall / renderResult functions for a Hub tool.
663
+ * Returns undefined for tools without custom rendering.
664
+ */
665
+ export function getToolRenderers(toolName) {
666
+ const factory = RENDERERS[toolName];
667
+ return factory?.();
668
+ }
669
+ //# sourceMappingURL=renderers.js.map