@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.
- package/README.md +57 -0
- package/dist/chain-preview.d.ts +55 -0
- package/dist/chain-preview.d.ts.map +1 -0
- package/dist/chain-preview.js +472 -0
- package/dist/chain-preview.js.map +1 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +402 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +99 -0
- package/dist/commands.js.map +1 -0
- package/dist/footer.d.ts +34 -0
- package/dist/footer.d.ts.map +1 -0
- package/dist/footer.js +249 -0
- package/dist/footer.js.map +1 -0
- package/dist/handlers.d.ts +24 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +83 -0
- package/dist/handlers.js.map +1 -0
- package/dist/hub-overlay.d.ts +107 -0
- package/dist/hub-overlay.d.ts.map +1 -0
- package/dist/hub-overlay.js +1794 -0
- package/dist/hub-overlay.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1585 -0
- package/dist/index.js.map +1 -0
- package/dist/output-viewer.d.ts +49 -0
- package/dist/output-viewer.d.ts.map +1 -0
- package/dist/output-viewer.js +389 -0
- package/dist/output-viewer.js.map +1 -0
- package/dist/overlay.d.ts +40 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +225 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocol.d.ts +118 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -0
- package/dist/remote-session.d.ts +113 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +645 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/remote-tui.d.ts +40 -0
- package/dist/remote-tui.d.ts.map +1 -0
- package/dist/remote-tui.js +606 -0
- package/dist/remote-tui.js.map +1 -0
- package/dist/renderers.d.ts +34 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +669 -0
- package/dist/renderers.js.map +1 -0
- package/dist/review.d.ts +15 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +154 -0
- package/dist/review.js.map +1 -0
- package/dist/titlebar.d.ts +3 -0
- package/dist/titlebar.d.ts.map +1 -0
- package/dist/titlebar.js +59 -0
- package/dist/titlebar.js.map +1 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.d.ts.map +1 -0
- package/dist/todo/index.js +3 -0
- package/dist/todo/index.js.map +1 -0
- package/dist/todo/store.d.ts +6 -0
- package/dist/todo/store.d.ts.map +1 -0
- package/dist/todo/store.js +43 -0
- package/dist/todo/store.js.map +1 -0
- package/dist/todo/types.d.ts +13 -0
- package/dist/todo/types.d.ts.map +1 -0
- package/dist/todo/types.js +2 -0
- package/dist/todo/types.js.map +1 -0
- package/package.json +44 -0
- package/src/chain-preview.ts +621 -0
- package/src/client.ts +515 -0
- package/src/commands.ts +132 -0
- package/src/footer.ts +305 -0
- package/src/handlers.ts +113 -0
- package/src/hub-overlay.ts +2324 -0
- package/src/index.ts +1907 -0
- package/src/output-viewer.ts +480 -0
- package/src/overlay.ts +294 -0
- package/src/protocol.ts +157 -0
- package/src/remote-session.ts +800 -0
- package/src/remote-tui.ts +707 -0
- package/src/renderers.ts +740 -0
- package/src/review.ts +201 -0
- package/src/titlebar.ts +63 -0
- package/src/todo/index.ts +2 -0
- package/src/todo/store.ts +49 -0
- 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
|