pi-subagents-j0k3r 1.0.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.
@@ -0,0 +1,477 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { writeSubagentsDebugLog } from './debug.js';
5
+
6
+ import type {
7
+ SubagentAssistantItem,
8
+ SubagentBashItem,
9
+ SubagentCustomItem,
10
+ SubagentErrorItem,
11
+ SubagentStatusItem,
12
+ SubagentThreadItem,
13
+ SubagentThreadRenderContext,
14
+ SubagentThreadSnapshot,
15
+ SubagentToolItem,
16
+ SubagentToolResultItem,
17
+ SubagentToolResultPayload,
18
+ SubagentUserItem,
19
+ } from './types.js';
20
+
21
+ type BoundOptions = { textLimit?: number; maxItems?: number };
22
+
23
+ const DEFAULT_TEXT_LIMIT = 4000;
24
+ const DEFAULT_MAX_ITEMS = 200;
25
+ const DEFAULT_RENDER_WIDTH = 100;
26
+ const require = createRequire(import.meta.url);
27
+ let piComponents: Record<string, any> | undefined;
28
+
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ }
32
+
33
+ function isOptionalString(value: unknown): boolean {
34
+ return value === undefined || typeof value === 'string';
35
+ }
36
+
37
+ function isPayload(value: unknown): value is SubagentToolResultPayload {
38
+ if (!isRecord(value) || typeof value.isError !== 'boolean' || !Array.isArray(value.content)) return false;
39
+ return value.content.every((part) => isRecord(part) && typeof part.type === 'string' && isOptionalString(part.text) && isOptionalString(part.data) && isOptionalString(part.mimeType));
40
+ }
41
+
42
+ function isAssistantItem(item: Record<string, unknown>): item is SubagentAssistantItem {
43
+ if (!isRecord(item.message) || item.message.role !== 'assistant' || !Array.isArray(item.message.content)) return false;
44
+ return item.message.content.every((part) => {
45
+ if (!isRecord(part) || typeof part.type !== 'string') return false;
46
+ if (part.type === 'text') return typeof part.text === 'string';
47
+ if (part.type === 'thinking') return isOptionalString(part.text) && isOptionalString(part.thinking);
48
+ if (part.type === 'toolCall') return typeof part.id === 'string' && typeof part.name === 'string' && 'arguments' in part;
49
+ return false;
50
+ });
51
+ }
52
+
53
+ function isToolItem(item: Record<string, unknown>): item is SubagentToolItem {
54
+ return typeof item.name === 'string'
55
+ && ['pending', 'running', 'completed', 'failed', 'partial'].includes(String(item.status))
56
+ && (item.result === undefined || isPayload(item.result));
57
+ }
58
+
59
+ function isToolResultItem(item: Record<string, unknown>): item is SubagentToolResultItem {
60
+ return isPayload(item.result) && isOptionalString(item.name);
61
+ }
62
+
63
+ function isBashItem(item: Record<string, unknown>): item is SubagentBashItem {
64
+ return typeof item.command === 'string'
65
+ && isOptionalString(item.output)
66
+ && (item.exitCode === undefined || typeof item.exitCode === 'number')
67
+ && (item.status === undefined || ['running', 'completed', 'failed', 'cancelled'].includes(String(item.status)));
68
+ }
69
+
70
+ function isCustomItem(item: Record<string, unknown>): item is SubagentCustomItem {
71
+ return typeof item.customType === 'string' && isOptionalString(item.fallbackText);
72
+ }
73
+
74
+ function isThreadItem(value: unknown): value is SubagentThreadItem {
75
+ if (!isRecord(value) || typeof value.type !== 'string') return false;
76
+ if (!isOptionalString(value.id)) return false;
77
+ switch (value.type) {
78
+ case 'assistant': return isAssistantItem(value);
79
+ case 'user': return typeof value.text === 'string' && (value.label === undefined || ['delegated_task', 'context', 'prompt', 'user'].includes(String(value.label)));
80
+ case 'tool': return isToolItem(value);
81
+ case 'tool_result': return isToolResultItem(value);
82
+ case 'bash': return isBashItem(value);
83
+ case 'custom': return isCustomItem(value);
84
+ case 'status': return typeof value.text === 'string' && (value.severity === undefined || ['info', 'success', 'warning'].includes(String(value.severity)));
85
+ case 'error': return typeof value.text === 'string';
86
+ default: return false;
87
+ }
88
+ }
89
+
90
+ export function isValidThreadSnapshot(value: unknown): value is SubagentThreadSnapshot {
91
+ return isRecord(value)
92
+ && value.version === 1
93
+ && ['events', 'session_messages', 'mixed'].includes(String(value.source))
94
+ && isOptionalString(value.created_at)
95
+ && isOptionalString(value.updated_at)
96
+ && Array.isArray(value.items)
97
+ && value.items.every(isThreadItem);
98
+ }
99
+
100
+ function boundText(text: string | undefined, limit: number): string | undefined {
101
+ if (text === undefined) return undefined;
102
+ if (text.length <= limit) return text;
103
+ return `${text.slice(0, Math.max(0, limit - 1))}…`;
104
+ }
105
+
106
+ function boundPayload(payload: SubagentToolResultPayload, limit: number): SubagentToolResultPayload {
107
+ return {
108
+ ...payload,
109
+ preview: boundText(payload.preview, limit),
110
+ content: payload.content.map((part) => ({ ...part, text: boundText(part.text, limit), data: boundText(part.data, limit) })),
111
+ };
112
+ }
113
+
114
+ function boundItem(item: SubagentThreadItem, limit: number): SubagentThreadItem {
115
+ switch (item.type) {
116
+ case 'assistant':
117
+ return {
118
+ ...item,
119
+ message: {
120
+ ...item.message,
121
+ content: item.message.content.map((part) => {
122
+ if (part.type === 'text') return { ...part, text: boundText(part.text, limit) ?? '' };
123
+ if (part.type === 'thinking') return { ...part, text: boundText(part.text, limit), thinking: boundText(part.thinking ?? part.text, limit) };
124
+ return part;
125
+ }),
126
+ },
127
+ };
128
+ case 'user': return { ...item, text: boundText(item.text, limit) ?? '' };
129
+ case 'tool': return { ...item, result: item.result ? boundPayload(item.result, limit) : undefined };
130
+ case 'tool_result': return { ...item, result: boundPayload(item.result, limit) };
131
+ case 'bash': return { ...item, command: boundText(item.command, limit) ?? '', output: boundText(item.output, limit) };
132
+ case 'custom': return { ...item, fallbackText: boundText(item.fallbackText, limit) };
133
+ case 'status': return { ...item, text: boundText(item.text, limit) ?? '' };
134
+ case 'error': return { ...item, text: boundText(item.text, limit) ?? '' };
135
+ }
136
+ }
137
+
138
+ export function boundThreadSnapshot(value: unknown, options: BoundOptions = {}): SubagentThreadSnapshot | undefined {
139
+ if (!isValidThreadSnapshot(value)) return undefined;
140
+ const limit = Math.max(1, options.textLimit ?? DEFAULT_TEXT_LIMIT);
141
+ const maxItems = Math.max(0, options.maxItems ?? DEFAULT_MAX_ITEMS);
142
+ return { ...value, items: value.items.slice(0, maxItems).map((item) => boundItem(item, limit)) };
143
+ }
144
+
145
+ function payloadText(payload: SubagentToolResultPayload): string {
146
+ return payload.preview || payload.content.map((part) => part.text || part.data || '').filter(Boolean).join('\n');
147
+ }
148
+
149
+ function jsonPreview(value: unknown, limit = 240): string {
150
+ if (value === undefined) return '';
151
+ let text: string;
152
+ try { text = JSON.stringify(value); } catch { text = String(value); }
153
+ return boundText(text, limit) ?? '';
154
+ }
155
+
156
+ function runningPiEntrypoint(): string | undefined {
157
+ if (!process.argv[1]) return undefined;
158
+ const resolved = path.resolve(process.argv[1]);
159
+ try { return fs.realpathSync(resolved); } catch { return resolved; }
160
+ }
161
+
162
+ function findRunningPiPackageRoot(): string | undefined {
163
+ let current = runningPiEntrypoint();
164
+ if (!current) return undefined;
165
+ if (!fs.existsSync(current)) return undefined;
166
+ current = fs.statSync(current).isDirectory() ? current : path.dirname(current);
167
+ while (true) {
168
+ const packageJson = path.join(current, 'package.json');
169
+ if (fs.existsSync(packageJson)) {
170
+ try {
171
+ const parsed = JSON.parse(fs.readFileSync(packageJson, 'utf8')) as { name?: string };
172
+ if (parsed.name === '@earendil-works/pi-coding-agent') return current;
173
+ } catch {}
174
+ }
175
+ const parent = path.dirname(current);
176
+ if (parent === current) return undefined;
177
+ current = parent;
178
+ }
179
+ }
180
+
181
+ export function resetPiComponentCacheForTests(): void {
182
+ piComponents = undefined;
183
+ builtInToolDefinitionCache.clear();
184
+ runtimeToolDefinitionsByTask.clear();
185
+ }
186
+
187
+ function loadPiComponents(): Record<string, any> | undefined {
188
+ if (piComponents !== undefined) return piComponents;
189
+ const candidates = [
190
+ () => require('@earendil-works/pi-coding-agent') as Record<string, any>,
191
+ () => {
192
+ const entrypoint = runningPiEntrypoint();
193
+ if (!entrypoint) return undefined;
194
+ return createRequire(entrypoint)('@earendil-works/pi-coding-agent') as Record<string, any>;
195
+ },
196
+ () => {
197
+ const packageRoot = findRunningPiPackageRoot();
198
+ return packageRoot ? require(packageRoot) as Record<string, any> : undefined;
199
+ },
200
+ ];
201
+ for (const candidate of candidates) {
202
+ try {
203
+ const loaded = candidate();
204
+ if (loaded) {
205
+ piComponents = loaded;
206
+ return piComponents;
207
+ }
208
+ } catch {}
209
+ }
210
+ return undefined;
211
+ }
212
+
213
+ function debugLog(context: Pick<SubagentThreadRenderContext, 'cwd'> | undefined, scope: string, data: unknown): void {
214
+ writeSubagentsDebugLog(context?.cwd, scope, data);
215
+ }
216
+
217
+ function renderComponent(component: unknown, width: number): string[] | undefined {
218
+ if (!component || typeof (component as any).render !== 'function') return undefined;
219
+ const lines = (component as any).render(width);
220
+ return Array.isArray(lines) ? lines.filter((line): line is string => typeof line === 'string') : undefined;
221
+ }
222
+
223
+ const TERMINAL_ESCAPE_RE = /\u001b\][^\u001b\u0007]*(?:\u001b\\|\u0007)|\u001b\[[0-?]*[ -/]*[@-~]/g;
224
+
225
+ function terminalVisibleWidth(text: string): number {
226
+ return [...text.replace(TERMINAL_ESCAPE_RE, '')].length;
227
+ }
228
+
229
+ function fitsWidth(context: SubagentThreadRenderContext, text: string, width: number): boolean {
230
+ try {
231
+ if (context.visibleWidth(text) <= width) return true;
232
+ } catch {}
233
+ return terminalVisibleWidth(text) <= width;
234
+ }
235
+
236
+ function safeTruncate(context: SubagentThreadRenderContext, text: string, width = DEFAULT_RENDER_WIDTH): string {
237
+ if (fitsWidth(context, text, width)) return text;
238
+ try {
239
+ return context.truncateToWidth(text, width);
240
+ } catch {
241
+ return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text;
242
+ }
243
+ }
244
+
245
+ function truncateLines(context: SubagentThreadRenderContext, lines: string[], width = DEFAULT_RENDER_WIDTH): string[] {
246
+ const out: string[] = [];
247
+ for (const line of lines) out.push(fitsWidth(context, line, width) ? line : context.truncateToWidth(line, width));
248
+ return out;
249
+ }
250
+
251
+ function assistantText(item: SubagentAssistantItem): string[] {
252
+ const lines: string[] = [];
253
+ for (const part of item.message.content) {
254
+ if (part.type === 'text' && part.text.trim()) lines.push(part.text);
255
+ else if (part.type === 'thinking' && (part.thinking ?? part.text)?.trim()) lines.push(`thinking: ${part.thinking ?? part.text}`);
256
+ // Tool calls are rendered as separate tool rows, matching Pi's main thread composition.
257
+ }
258
+ if (item.message.errorMessage) lines.push(`error: ${item.message.errorMessage}`);
259
+ return lines;
260
+ }
261
+
262
+ function renderAssistantItem(item: SubagentAssistantItem, context: SubagentThreadRenderContext, width: number): string[] {
263
+ const visibleContent = item.message.content.filter((part) => part.type !== 'toolCall');
264
+ if (!visibleContent.length && !item.message.errorMessage) return [];
265
+ const displayItem: SubagentAssistantItem = visibleContent.length === item.message.content.length
266
+ ? item
267
+ : { ...item, message: { ...item.message, content: visibleContent } };
268
+ const componentCtor = loadPiComponents()?.AssistantMessageComponent;
269
+ if (typeof componentCtor === 'function') {
270
+ try {
271
+ const markdownTheme = loadPiComponents()?.getMarkdownTheme?.() ?? context.theme;
272
+ const rendered = renderComponent(new componentCtor(displayItem.message, false, markdownTheme, 'Thinking...'), width);
273
+ if (rendered?.some((line) => line.trim())) return rendered;
274
+ } catch (error) { debugLog(context, 'assistant_component_error', { error }); }
275
+ }
276
+ return assistantText(displayItem);
277
+ }
278
+
279
+ function renderUserItem(item: SubagentUserItem, context: SubagentThreadRenderContext, width: number): string[] {
280
+ const componentCtor = loadPiComponents()?.UserMessageComponent;
281
+ if (typeof componentCtor === 'function') {
282
+ try {
283
+ const markdownTheme = loadPiComponents()?.getMarkdownTheme?.() ?? context.theme;
284
+ const rendered = renderComponent(new componentCtor(item.text, markdownTheme), width);
285
+ if (rendered?.some((line) => line.trim())) return rendered;
286
+ } catch (error) { debugLog(context, 'user_component_error', { error, label: item.label }); }
287
+ }
288
+ return [`${item.label ?? 'user'}: ${item.text}`];
289
+ }
290
+
291
+ const builtInToolDefinitionCache = new Map<string, unknown>();
292
+ const runtimeToolDefinitionsByTask = new Map<string, Map<string, unknown>>();
293
+
294
+ export function registerSubagentRuntimeToolDefinition(taskId: string | undefined, name: string | undefined, definition: unknown): void {
295
+ if (!taskId || !name || !definition) return;
296
+ let byName = runtimeToolDefinitionsByTask.get(taskId);
297
+ if (!byName) {
298
+ byName = new Map<string, unknown>();
299
+ runtimeToolDefinitionsByTask.set(taskId, byName);
300
+ }
301
+ byName.set(name, definition);
302
+ }
303
+
304
+ export function runtimeToolDefinition(taskId: string | undefined, name: string): unknown {
305
+ return taskId ? runtimeToolDefinitionsByTask.get(taskId)?.get(name) : undefined;
306
+ }
307
+
308
+ function builtInToolDefinition(name: string, cwd: string): unknown {
309
+ const key = `${cwd}\0${name}`;
310
+ if (builtInToolDefinitionCache.has(key)) return builtInToolDefinitionCache.get(key);
311
+ const pi = loadPiComponents();
312
+ const factoryByName: Record<string, string> = {
313
+ read: 'createReadToolDefinition',
314
+ bash: 'createBashToolDefinition',
315
+ edit: 'createEditToolDefinition',
316
+ write: 'createWriteToolDefinition',
317
+ grep: 'createGrepToolDefinition',
318
+ find: 'createFindToolDefinition',
319
+ ls: 'createLsToolDefinition',
320
+ };
321
+ const factoryName = factoryByName[name];
322
+ if (!factoryName) return undefined;
323
+ const createSpecificToolDefinition = pi?.[factoryName];
324
+ if (typeof createSpecificToolDefinition === 'function') {
325
+ try {
326
+ const definition = createSpecificToolDefinition(cwd);
327
+ builtInToolDefinitionCache.set(key, definition);
328
+ return definition;
329
+ } catch {}
330
+ }
331
+ const createToolDefinition = pi?.createToolDefinition;
332
+ if (typeof createToolDefinition !== 'function') return undefined;
333
+ try {
334
+ const definition = createToolDefinition(name, cwd);
335
+ builtInToolDefinitionCache.set(key, definition);
336
+ return definition;
337
+ } catch { return undefined; }
338
+ }
339
+
340
+ function argString(value: unknown): string {
341
+ return typeof value === 'string' ? value.trim() : value === undefined || value === null ? '' : String(value).trim();
342
+ }
343
+
344
+ function pathRangeSummary(input: Record<string, unknown>): string {
345
+ const file = argString(input.path ?? input.file_path ?? input.file);
346
+ if (!file) return '';
347
+ const offset = Number(input.offset ?? 1);
348
+ const limit = Number(input.limit);
349
+ if (Number.isFinite(limit) && limit > 0) return `${file}:${Number.isFinite(offset) && offset > 0 ? offset : 1}-${(Number.isFinite(offset) && offset > 0 ? offset : 1) + limit - 1}`;
350
+ if (Number.isFinite(offset) && offset > 1) return `${file}:${offset}`;
351
+ return file;
352
+ }
353
+
354
+ function memoryArgumentSummary(name: string, input: Record<string, unknown>): string {
355
+ if (name === 'memory_search') return argString(input.query);
356
+ if (name === 'memory_recall') return [argString(input.context), argString(input.query)].filter(Boolean).join(' · ');
357
+ if (name === 'memory_get' || name === 'memory_update' || name === 'memory_archive') return argString(input.id);
358
+ if (name === 'memory_list') return [argString(input.scope), argString(input.kind), argString(input.project_name)].filter(Boolean).join(' · ');
359
+ if (name === 'memory_add') return argString(input.title ?? input.summary ?? input.kind);
360
+ if (name === 'memory_project_profile') return argString(input.action);
361
+ if (name === 'memory_session_start') return argString(input.title);
362
+ if (name === 'memory_session_prompt_add' || name === 'memory_session_finish') return argString(input.session_id);
363
+ if (name === 'memory_migrate_project') return input.dry_run === false ? 'apply' : 'dry run';
364
+ if (name === 'memory_export' || name === 'memory_import') return argString(input.path);
365
+ return argString(input.query ?? input.id ?? input.action ?? input.kind ?? input.title ?? input.summary);
366
+ }
367
+
368
+ function toolArgumentSummary(name: string, args: unknown): string {
369
+ const input = isRecord(args) ? args : {};
370
+ if (name === 'read') return pathRangeSummary(input);
371
+ if (name === 'edit' || name === 'write') return argString(input.path ?? input.file_path ?? input.file);
372
+ if (name === 'bash') return argString(input.command).split('\n')[0] ?? '';
373
+ if (name.startsWith('memory_')) return memoryArgumentSummary(name, input);
374
+ if (['grep', 'find', 'ls'].includes(name)) return [argString(input.pattern ?? input.query ?? input.name), argString(input.path ?? input.cwd)].filter(Boolean).join(' · ');
375
+ return jsonPreview(args);
376
+ }
377
+
378
+ function renderToolItem(item: SubagentToolItem, context: SubagentThreadRenderContext, width: number): string[] {
379
+ const toolDefinition = context.getToolDefinition?.(item.name) ?? runtimeToolDefinition(context.taskId, item.name) ?? builtInToolDefinition(item.name, context.cwd);
380
+ const componentCtor = loadPiComponents()?.ToolExecutionComponent;
381
+ if (typeof componentCtor === 'function' && context.tui && toolDefinition) {
382
+ try {
383
+ const component = new componentCtor(item.name, item.tool_call_id ?? item.id ?? item.name, item.arguments, { showImages: context.showImages, imageWidthCells: context.imageWidthCells }, toolDefinition, context.tui, context.cwd);
384
+ component.markExecutionStarted?.();
385
+ component.setArgsComplete?.();
386
+ if (item.result) component.updateResult?.(item.result, item.status === 'partial');
387
+ component.setExpanded?.(context.toolOutputExpanded ?? false);
388
+ const rendered = renderComponent(component, width);
389
+ if (rendered?.some((line) => line.trim())) return rendered;
390
+ debugLog(context, 'tool_component_empty_fallback', { name: item.name, status: item.status, width, hasToolDefinition: Boolean(toolDefinition), hasTui: Boolean(context.tui) });
391
+ } catch (error) { debugLog(context, 'tool_component_error_fallback', { error, name: item.name, status: item.status, width, hasToolDefinition: Boolean(toolDefinition), hasTui: Boolean(context.tui) }); }
392
+ } else {
393
+ debugLog(context, 'tool_component_unavailable_fallback', { name: item.name, status: item.status, hasComponentCtor: typeof componentCtor === 'function', hasToolDefinition: Boolean(toolDefinition), hasTui: Boolean(context.tui) });
394
+ }
395
+ const result = item.result ? payloadText(item.result) : '';
396
+ const args = toolArgumentSummary(item.name, item.arguments);
397
+ const state = item.result?.isError ? 'failed' : item.status;
398
+ const fallback = [`${item.name} ${state}${args ? ` · ${args}` : ''}`, ...(result ? [result] : [])];
399
+ debugLog(context, 'tool_fallback_rendered', { name: item.name, status: item.status, fallbackPreview: fallback.join('\n').slice(0, 1000) });
400
+ return fallback;
401
+ }
402
+
403
+ function renderBashItem(item: SubagentBashItem, context: SubagentThreadRenderContext, width: number): string[] {
404
+ const componentCtor = loadPiComponents()?.BashExecutionComponent;
405
+ if (typeof componentCtor === 'function' && context.tui) {
406
+ try {
407
+ const component = new componentCtor(item.command, context.tui, true);
408
+ if (item.output) component.appendOutput?.(item.output);
409
+ if (item.status !== 'running') component.setComplete?.(item.exitCode, item.cancelled ?? item.status === 'cancelled', item.truncated ? { truncated: true } : undefined, item.fullOutputPath);
410
+ component.setExpanded?.(context.toolOutputExpanded ?? false);
411
+ const rendered = renderComponent(component, width);
412
+ if (rendered?.some((line) => line.trim())) return rendered;
413
+ } catch (error) { debugLog(context, 'bash_component_error', { error, command: item.command.slice(0, 200), status: item.status, hasTui: Boolean(context.tui) }); }
414
+ }
415
+ const status = item.cancelled ? 'cancelled' : item.status ?? (item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed');
416
+ const exit = item.exitCode === undefined ? '' : ` exit:${item.exitCode}`;
417
+ return [`bash ${status}${exit}: ${item.command}`, item.output ?? ''].filter(Boolean);
418
+ }
419
+
420
+ function renderCustomItem(item: SubagentCustomItem, context: SubagentThreadRenderContext, width: number): string[] {
421
+ const renderer = context.getMessageRenderer?.(item.customType);
422
+ const componentCtor = loadPiComponents()?.CustomMessageComponent;
423
+ if (typeof componentCtor === 'function' && renderer) {
424
+ try {
425
+ const markdownTheme = loadPiComponents()?.getMarkdownTheme?.() ?? context.theme;
426
+ const component = new componentCtor({ type: item.customType, content: item.content, display: item.display }, renderer, markdownTheme);
427
+ component.setExpanded?.(context.toolOutputExpanded ?? false);
428
+ const rendered = renderComponent(component, width);
429
+ if (rendered?.some((line) => line.trim())) return rendered;
430
+ } catch (error) { debugLog(context, 'custom_component_error', { error, customType: item.customType, hasRenderer: Boolean(renderer) }); }
431
+ }
432
+ return [`custom ${item.customType}${item.fallbackText ? `: ${item.fallbackText}` : ''}`];
433
+ }
434
+
435
+ function renderItem(item: SubagentThreadItem, context: SubagentThreadRenderContext, width: number): string[] {
436
+ switch (item.type) {
437
+ case 'assistant': return renderAssistantItem(item, context, width);
438
+ case 'user': return renderUserItem(item, context, width);
439
+ case 'tool': return renderToolItem(item, context, width);
440
+ case 'tool_result': return [`tool result${item.name ? ` ${item.name}` : ''}${item.result.isError ? ' failed' : ''}`, payloadText(item.result)].filter(Boolean);
441
+ case 'bash': return renderBashItem(item, context, width);
442
+ case 'custom': return renderCustomItem(item, context, width);
443
+ case 'status': return [`${item.severity ?? 'info'}: ${item.text}`];
444
+ case 'error': return [`error: ${item.text}`];
445
+ }
446
+ }
447
+
448
+ function isRenderableSnapshotRoot(value: unknown): value is { items: unknown[] } {
449
+ return isRecord(value)
450
+ && value.version === 1
451
+ && ['events', 'session_messages', 'mixed'].includes(String(value.source))
452
+ && Array.isArray(value.items);
453
+ }
454
+
455
+ function malformedItemText(item: unknown): string {
456
+ if (isRecord(item) && typeof item.type === 'string') return `malformed thread item: ${item.type}`;
457
+ return 'malformed thread item';
458
+ }
459
+
460
+ export function renderThreadBody(snapshot: unknown, context: SubagentThreadRenderContext): string[] {
461
+ if (!isRenderableSnapshotRoot(snapshot)) return [];
462
+ const width = Math.max(1, Math.floor(context.renderWidth ?? DEFAULT_RENDER_WIDTH));
463
+ const lines: string[] = [];
464
+ for (const rawItem of snapshot.items.slice(0, DEFAULT_MAX_ITEMS)) {
465
+ try {
466
+ if (!isThreadItem(rawItem)) {
467
+ lines.push(safeTruncate(context, malformedItemText(rawItem), width));
468
+ continue;
469
+ }
470
+ const item = boundItem(rawItem, DEFAULT_TEXT_LIMIT);
471
+ lines.push(...truncateLines(context, renderItem(item, context, width), width));
472
+ } catch {
473
+ lines.push(safeTruncate(context, 'thread item unavailable', width));
474
+ }
475
+ }
476
+ return lines;
477
+ }