@bahulam/code 2.6.13 → 2.6.15

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.
@@ -10,9 +10,8 @@
10
10
  * - architect: Feature architect — designs implementations, file plans
11
11
  */
12
12
 
13
- import { c, spinner, inPlace, hr } from './ansi.mjs';
13
+ import { c } from './ansi.mjs';
14
14
  import { TarangStreamClient } from '../core/stream-client.mjs';
15
- import { resolveBackendUrl } from '../core/backend-url.mjs';
16
15
 
17
16
  // ── Agent Definitions ────────────────────────────────────────
18
17
 
@@ -105,20 +104,172 @@ Rules:
105
104
 
106
105
  // ── Agent Runner ─────────────────────────────────────────────
107
106
 
107
+ const TOOL_ALIASES = new Map([
108
+ ['bash', 'shell'],
109
+ ['shell_command', 'shell'],
110
+ ['read', 'read_file'],
111
+ ['write', 'write_file'],
112
+ ['edit', 'edit_file'],
113
+ ['grep', 'search_code'],
114
+ ]);
115
+
116
+ function canonicalToolName(value) {
117
+ const key = String(value || '').trim().toLowerCase();
118
+ return TOOL_ALIASES.get(key) || key;
119
+ }
120
+
121
+ const RUNTIME_PLACEHOLDER_CWDS = new Set([
122
+ '/workspace',
123
+ '/workspace/kepler-code',
124
+ ]);
125
+
126
+ function normalizeScopedArgs(toolName, args = {}, { projectRoot = null } = {}) {
127
+ if (canonicalToolName(toolName) !== 'shell' || !projectRoot) return args;
128
+ const next = { ...(args || {}) };
129
+ const cwd = String(next.cwd || '').trim();
130
+ if (!cwd || RUNTIME_PLACEHOLDER_CWDS.has(cwd)) {
131
+ next.cwd = projectRoot;
132
+ }
133
+ return next;
134
+ }
135
+
136
+ function createScopedToolExecutor(baseExecutor, agent, { projectRoot = null } = {}) {
137
+ const tools = Array.isArray(agent.tools) ? agent.tools : [];
138
+ const allowed = new Set(tools.map(canonicalToolName).filter(Boolean));
139
+ if (!allowed.size) return baseExecutor;
140
+
141
+ return {
142
+ ...baseExecutor,
143
+ execute: async (toolName, args = {}, options = {}) => {
144
+ const canonical = canonicalToolName(toolName);
145
+ if (!allowed.has(canonical)) {
146
+ return {
147
+ success: false,
148
+ output: `Tool '${toolName}' is not allowed for agent '${agent.name}'. Allowed tools: ${tools.join(', ')}`,
149
+ };
150
+ }
151
+ return baseExecutor.execute.call(
152
+ baseExecutor,
153
+ toolName,
154
+ normalizeScopedArgs(toolName, args, { projectRoot }),
155
+ options,
156
+ );
157
+ },
158
+ };
159
+ }
160
+
161
+ export function findBuiltinAgent(agentName) {
162
+ const target = String(agentName || '').trim().toLowerCase();
163
+ return BUILTIN_AGENTS.find(agent => agent.command === target || agent.name.toLowerCase() === target) || null;
164
+ }
165
+
166
+ export function localAgentMatches(agent, target) {
167
+ const needle = String(target || '').trim().toLowerCase();
168
+ if (!needle) return false;
169
+ return [
170
+ agent.slug,
171
+ agent.id,
172
+ agent.name,
173
+ ].some(value => String(value || '').trim().toLowerCase() === needle);
174
+ }
175
+
176
+ function normalizeRunnableAgent(agent) {
177
+ const spec = agent?.spec || {};
178
+ const config = agent?.config || spec.config || agent?.raw_config || {};
179
+ const configAgent = config.agent || {};
180
+ const tools = Array.isArray(agent?.tools)
181
+ ? agent.tools
182
+ : Array.isArray(spec.tools)
183
+ ? spec.tools
184
+ : Array.isArray(config.tools)
185
+ ? config.tools
186
+ : [];
187
+
188
+ const name = agent?.name || spec.name || agent?.slug || agent?.command || 'agent';
189
+ return {
190
+ command: agent?.command || agent?.slug || spec.slug || name,
191
+ slug: agent?.slug || spec.slug || agent?.command || name,
192
+ name,
193
+ description: agent?.description || spec.description || '',
194
+ role: agent?.role || spec.role || 'specialist',
195
+ icon: agent?.icon || '◇',
196
+ systemPrompt: agent?.systemPrompt || agent?.system_prompt || agent?.prompt || spec.system_prompt || configAgent.system_prompt || '',
197
+ readOnly: Boolean(agent?.readOnly),
198
+ model: agent?.model || spec.model || configAgent.model || null,
199
+ models: agent?.models || spec.models || configAgent.models || null,
200
+ tools,
201
+ source: agent?.source || spec.source || '',
202
+ };
203
+ }
204
+
205
+ function agentInstructionPrefix(agent, execContext = {}) {
206
+ const lines = [];
207
+ if (agent.systemPrompt) {
208
+ lines.push(agent.systemPrompt);
209
+ } else {
210
+ lines.push(`You are ${agent.name}, a Bahulam Code sub-agent.`);
211
+ }
212
+ if (agent.description) lines.push(`\nAgent description: ${agent.description}`);
213
+ if (agent.role) lines.push(`Agent role: ${agent.role}`);
214
+ if (agent.tools.length) {
215
+ lines.push(
216
+ `Allowed tools for this sub-agent: ${agent.tools.join(', ')}. ` +
217
+ 'Do not request tools outside this list.',
218
+ );
219
+ }
220
+ if (execContext.project_root) {
221
+ lines.push(`Runtime project root: ${execContext.project_root}. Use this as the cwd for shell commands unless the task explicitly requires a different registered project root.`);
222
+ }
223
+ return lines.join('\n');
224
+ }
225
+
226
+ function displayEventForDirectAgent(event, agent) {
227
+ if (!event || !event.type) return event;
228
+ if (!['tool_call', 'tool_request', 'tool_result', 'tool_done', 'sub_agent_tool'].includes(event.type)) {
229
+ return event;
230
+ }
231
+ return {
232
+ ...event,
233
+ data: {
234
+ ...(event.data || {}),
235
+ internal: true,
236
+ sub_agent: event.data?.sub_agent || agent.slug || agent.command || agent.name || 'agent',
237
+ },
238
+ };
239
+ }
240
+
108
241
  /**
109
- * Run a built-in agent with the given instruction.
110
- * @param {string} agentName - e.g. 'explore', 'review', 'architect'
242
+ * Run a normalized agent definition with the given instruction.
243
+ * @param {Object} agentDefinition - Built-in or .bahulam/agents definition
111
244
  * @param {string} instruction - User's instruction
112
245
  * @param {Object} ctx - { auth, toolExecutor, approval }
113
246
  * @param {Object} session - Session state
114
247
  * @param {Function} renderEvent - Event renderer function
248
+ * @param {Object} [options]
115
249
  */
116
- export async function runAgent(agentName, instruction, ctx, session, renderEvent) {
117
- const agent = BUILTIN_AGENTS.find(a => a.command === agentName);
118
- if (!agent) {
119
- process.stderr.write(` ${c.red('Unknown agent: ' + agentName)}\n`);
120
- return;
121
- }
250
+ export async function runAgentDefinition(agentDefinition, instruction, ctx, session, renderEvent, options = {}) {
251
+ const agent = normalizeRunnableAgent(agentDefinition);
252
+ const userInstruction = String(instruction || '').trim() || 'Run your assigned task now.';
253
+ const suppliedContext = options.execContext || options.context || {};
254
+ const baseCwd = suppliedContext.cwd || options.cwd || process.cwd();
255
+ const suppliedSubAgent = suppliedContext.sub_agent && typeof suppliedContext.sub_agent === 'object'
256
+ ? suppliedContext.sub_agent
257
+ : {};
258
+ const projectRoot = suppliedContext.project_root || null;
259
+ const execContext = {
260
+ ...suppliedContext,
261
+ cwd: suppliedContext.cwd || baseCwd,
262
+ ...(projectRoot ? { project_root: projectRoot } : {}),
263
+ sub_agent: {
264
+ ...suppliedSubAgent,
265
+ name: agent.name,
266
+ slug: agent.slug,
267
+ role: agent.role,
268
+ description: agent.description,
269
+ tools: agent.tools,
270
+ source: agent.source,
271
+ },
272
+ };
122
273
 
123
274
  const creds = ctx.auth.loadCredentials();
124
275
  if (!creds.token) {
@@ -129,49 +280,74 @@ export async function runAgent(agentName, instruction, ctx, session, renderEvent
129
280
  // Header
130
281
  process.stderr.write(`\n ${agent.icon} ${c.bold(c.brand(agent.name))}\n`);
131
282
  process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
132
- process.stderr.write(` ${c.gray(instruction)}\n\n`);
283
+ if (agent.description) process.stderr.write(` ${c.gray(agent.description)}\n`);
284
+ if (agent.source) process.stderr.write(` ${c.dim(agent.source)}\n`);
285
+ process.stderr.write(` ${c.gray(userInstruction)}\n\n`);
133
286
 
134
287
  // Prepend agent system prompt to instruction
135
- const fullInstruction = `${agent.systemPrompt}\n\n---\n\nUser request: ${instruction}`;
288
+ const fullInstruction = `${agentInstructionPrefix(agent, execContext)}\n\n---\n\nUser request: ${userInstruction}`;
136
289
 
137
290
  // For read-only agents, use a restricted approval manager
138
291
  const { ApprovalManager } = await import('../core/approval.mjs');
139
292
  const agentApproval = agent.readOnly
140
293
  ? new ApprovalManager({ planMode: true }) // planMode blocks all writes
141
294
  : ctx.approval;
295
+ const toolExecutor = createScopedToolExecutor(ctx.toolExecutor, agent, {
296
+ projectRoot: execContext.project_root || null,
297
+ });
142
298
 
143
299
  const client = new TarangStreamClient({
144
300
  baseUrl: creds.backendUrl,
145
301
  token: creds.token,
146
- toolExecutor: ctx.toolExecutor,
302
+ toolExecutor,
147
303
  approvalManager: agentApproval,
148
304
  });
149
305
 
150
306
  session.turns++;
151
307
  session.toolCalls = 0;
152
308
  let assistantContent = '';
309
+ if (agent.model) execContext.model_override = agent.model;
310
+ if (agent.models && typeof agent.models === 'object' && Object.keys(agent.models).length) {
311
+ execContext.model_overrides = agent.models;
312
+ }
153
313
 
154
314
  try {
155
- for await (const event of client.execute(fullInstruction, { cwd: process.cwd() })) {
156
- renderEvent(event);
315
+ for await (const event of client.execute(fullInstruction, execContext)) {
316
+ renderEvent(displayEventForDirectAgent(event, agent));
157
317
 
158
318
  if (event.type === 'content' || event.type === 'content_partial') {
159
319
  const text = event.data?.text || '';
160
- if (text) assistantContent = text;
320
+ if (text) assistantContent += text;
161
321
  }
162
322
  }
163
323
  } catch (err) {
164
- inPlace('');
165
324
  process.stderr.write(` ${c.red('Agent error: ' + err.message)}\n`);
166
325
  }
167
326
 
168
327
  // Save to conversation history
169
328
  if (assistantContent) {
170
329
  session.history.push(
171
- { role: 'user', content: `[${agent.name}] ${instruction}` },
330
+ { role: 'user', content: `[${agent.name}] ${userInstruction}` },
172
331
  { role: 'assistant', content: assistantContent }
173
332
  );
174
333
  }
175
334
 
176
335
  process.stderr.write('\n');
177
336
  }
337
+
338
+ /**
339
+ * Run a built-in agent with the given instruction.
340
+ * @param {string} agentName - e.g. 'explore', 'review', 'architect'
341
+ * @param {string} instruction - User's instruction
342
+ * @param {Object} ctx - { auth, toolExecutor, approval }
343
+ * @param {Object} session - Session state
344
+ * @param {Function} renderEvent - Event renderer function
345
+ */
346
+ export async function runAgent(agentName, instruction, ctx, session, renderEvent) {
347
+ const agent = findBuiltinAgent(agentName);
348
+ if (!agent) {
349
+ process.stderr.write(` ${c.red('Unknown agent: ' + agentName)}\n`);
350
+ return;
351
+ }
352
+ return runAgentDefinition(agent, instruction, ctx, session, renderEvent);
353
+ }
@@ -9,7 +9,7 @@
9
9
  * - Content streaming (SSE partials → debounced markdown flush)
10
10
  * - Spinner (single animated line, shared across concerns)
11
11
  * - Stagnation banner
12
- * - Detail expansion (/last, /expand N, Ctrl+D)
12
+ * - Detail expansion (/last, /expand N, F2)
13
13
  *
14
14
  * All mutable state lives in repl-state.mjs (`runtime`). Consumers of this
15
15
  * module (renderEvent, handleCommand, keypress handlers) import the specific
@@ -22,6 +22,8 @@ import { runtime, session } from './repl-state.mjs';
22
22
  import { fitAnsiLine } from './repl-format.mjs';
23
23
  import { exploreCategory, isExploreTool } from './repl-explore.mjs';
24
24
  import {
25
+ clearPinnedStatus,
26
+ drawPinnedStatus,
25
27
  isInputDockMounted,
26
28
  moveToContent,
27
29
  } from '../ui/input-dock.mjs';
@@ -277,6 +279,7 @@ export function renderToolResult(data, eventType = 'tool_result') {
277
279
  const tool = data.tool || data._tool || '';
278
280
  const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
279
281
  recordReadActivity(tool, data.args || {});
282
+ recordWriteActivity(tool, data.args || {}, data);
280
283
 
281
284
  // Update the card buffer so /last and `d` can find it.
282
285
  if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
@@ -285,7 +288,9 @@ export function renderToolResult(data, eventType = 'tool_result') {
285
288
 
286
289
  const { text, tone: t } = summarizeResult(tool, data);
287
290
  // Em dash reads more like prose than a system arrow.
288
- const arrow = paint.text.dim('—');
291
+ const arrow = shellResultTool(tool)
292
+ ? `${paint.text.dim('result')} ${paint.text.dim('—')}`
293
+ : paint.text.dim('—');
289
294
  const painter = t === 'success' ? paint.state.success
290
295
  : t === 'warn' ? paint.state.warn
291
296
  : t === 'danger' ? paint.state.danger
@@ -320,7 +325,10 @@ export function renderToolResult(data, eventType = 'tool_result') {
320
325
  const combined = `${runtime.pendingHead.head} ${outcome}`;
321
326
  if (stripAnsi(combined).length <= cols) {
322
327
  process.stderr.write(`${combined}\n`);
323
- if (diffPreview) process.stderr.write(`${diffPreview}\n`);
328
+ if (diffPreview) {
329
+ process.stderr.write(`${diffPreview}\n`);
330
+ rememberFileDiffPreview(data);
331
+ }
324
332
  runtime.lastRenderedBlock = 'tool';
325
333
  runtime.pendingHead = null;
326
334
  return;
@@ -328,7 +336,10 @@ export function renderToolResult(data, eventType = 'tool_result') {
328
336
  if (isInlineOutcomeTool(tool)) {
329
337
  const compactHead = compactHeadForOutcome(runtime.pendingHead.head, outcome, cols);
330
338
  process.stderr.write(`${compactHead} ${outcome}\n`);
331
- if (diffPreview) process.stderr.write(`${diffPreview}\n`);
339
+ if (diffPreview) {
340
+ process.stderr.write(`${diffPreview}\n`);
341
+ rememberFileDiffPreview(data);
342
+ }
332
343
  runtime.lastRenderedBlock = 'tool';
333
344
  runtime.pendingHead = null;
334
345
  return;
@@ -343,7 +354,10 @@ export function renderToolResult(data, eventType = 'tool_result') {
343
354
 
344
355
  // Two-line shape: gutter under the (already-printed or just-flushed) head.
345
356
  process.stderr.write(`${gutter}${outcome}\n`);
346
- if (diffPreview) process.stderr.write(`${diffPreview}\n`);
357
+ if (diffPreview) {
358
+ process.stderr.write(`${diffPreview}\n`);
359
+ rememberFileDiffPreview(data);
360
+ }
347
361
  runtime.lastRenderedBlock = 'tool';
348
362
 
349
363
  // Lint warnings stay visible alongside writes.
@@ -352,6 +366,65 @@ export function renderToolResult(data, eventType = 'tool_result') {
352
366
  }
353
367
  }
354
368
 
369
+ function fileDiffKey(data = {}) {
370
+ return fileDiffKeys(data)[0] || '';
371
+ }
372
+
373
+ function fileDiffKeys(data = {}) {
374
+ const keys = [];
375
+ const callId = data.call_id || data._callId || data.request_id || data.id;
376
+ if (callId) keys.push(`call:${callId}`);
377
+ const diff = Array.isArray(data.file_diffs) ? data.file_diffs[0]
378
+ : data.file_diff ? data.file_diff
379
+ : data.type === 'file_diff' ? data
380
+ : null;
381
+ const file = diff?.relative_path || diff?.path || data.relative_path || data.path || '';
382
+ if (file) {
383
+ const added = diff?.lines_added ?? data.lines_added ?? '';
384
+ const removed = diff?.lines_removed ?? data.lines_removed ?? '';
385
+ keys.push(`file:${file}:${added}:${removed}`);
386
+ }
387
+ return keys;
388
+ }
389
+
390
+ function rememberFileDiffPreview(data = {}) {
391
+ for (const key of fileDiffKeys(data)) {
392
+ runtime.renderedFileDiffPreviews.add(key);
393
+ }
394
+ }
395
+
396
+ export function renderFileDiffEvent(data = {}) {
397
+ const keys = fileDiffKeys(data);
398
+ if (keys.some(key => runtime.renderedFileDiffPreviews.has(key))) return false;
399
+
400
+ const indent = subAgentIndent();
401
+ const gutter = `${indent}${paint.text.dim('⎿')} `;
402
+ const diffPreview = formatCompactFileDiff({
403
+ file_diff: data,
404
+ lines_added: data.lines_added,
405
+ lines_removed: data.lines_removed,
406
+ }, {
407
+ indent: gutter,
408
+ columns: process.stderr.columns || 120,
409
+ showFileHeader: true,
410
+ });
411
+ if (!diffPreview) return false;
412
+
413
+ renderBlockBoundary('tool', { compactSame: true });
414
+ process.stderr.write(`${diffPreview}\n`);
415
+ for (const key of keys) runtime.renderedFileDiffPreviews.add(key);
416
+ rememberChangedFile(data.relative_path || data.path);
417
+ runtime.lastRenderedBlock = 'tool';
418
+ return true;
419
+ }
420
+
421
+ function shellResultTool(tool) {
422
+ return [
423
+ 'shell', 'run_tests', 'validate_build', 'lint_check',
424
+ 'validate_file', 'validate_structure',
425
+ ].includes(String(tool || '').toLowerCase());
426
+ }
427
+
355
428
  // ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
356
429
  //
357
430
  // All three call into the same renderer so output is consistent across
@@ -404,6 +477,11 @@ export function rememberReadFile(filePath) {
404
477
  if (file && !session.filesRead.includes(file)) session.filesRead.push(file);
405
478
  }
406
479
 
480
+ export function rememberChangedFile(filePath) {
481
+ const file = shortPath(String(filePath || '').trim());
482
+ if (file && !session.filesChanged.includes(file)) session.filesChanged.push(file);
483
+ }
484
+
407
485
  export function recordReadActivity(tool, args = {}) {
408
486
  const normalized = String(tool || '').toLowerCase();
409
487
  if (normalized === 'read_file' || normalized === 'read') {
@@ -418,6 +496,26 @@ export function recordReadActivity(tool, args = {}) {
418
496
  }
419
497
  }
420
498
 
499
+ export function recordWriteActivity(tool, args = {}, result = {}) {
500
+ const normalized = String(tool || '').toLowerCase();
501
+ if (!['write_file', 'edit_file', 'delete_file', 'write_project'].includes(normalized)) return;
502
+
503
+ if (normalized === 'write_project') {
504
+ const files = Array.isArray(args.files) ? args.files : [];
505
+ for (const file of files) {
506
+ rememberChangedFile(file?.file_path || file?.path);
507
+ }
508
+ }
509
+
510
+ rememberChangedFile(args.file_path || args.path || result.file_path || result.path);
511
+ const diffs = Array.isArray(result.file_diffs)
512
+ ? result.file_diffs
513
+ : result.file_diff ? [result.file_diff] : [];
514
+ for (const diff of diffs) {
515
+ rememberChangedFile(diff.relative_path || diff.path);
516
+ }
517
+ }
518
+
421
519
  export function thinkingKind(text) {
422
520
  return /\b(read|reading|inspect|scan|search|open|trace|look(?:ing)?\s+at)\b/i.test(text)
423
521
  ? 'Reading'
@@ -461,7 +559,10 @@ export function startSpinner(text) {
461
559
  if (isExploreActive && isInputDockMounted()) {
462
560
  return;
463
561
  }
464
- if (isInputDockMounted()) moveToContent();
562
+ if (isInputDockMounted()) {
563
+ drawPinnedStatus(rendered);
564
+ return;
565
+ }
465
566
  inPlace(rendered);
466
567
  }, 80);
467
568
  }
@@ -477,7 +578,11 @@ export function stopSpinner() {
477
578
  if (runtime.exploreRun && runtime.exploreRun.lineActive) return;
478
579
  if (runtime.spinInterval) { clearInterval(runtime.spinInterval); runtime.spinInterval = null; }
479
580
  runtime.spinText = '';
480
- if (isInputDockMounted()) moveToContent();
581
+ if (isInputDockMounted()) {
582
+ clearPinnedStatus();
583
+ moveToContent();
584
+ return;
585
+ }
481
586
  inPlace('');
482
587
  }
483
588
 
@@ -493,6 +598,7 @@ export function startContentStream() {
493
598
  runtime.streamBuffer = '';
494
599
  runtime.streamedPartialText = '';
495
600
  runtime.renderedToolResults.clear();
601
+ runtime.renderedFileDiffPreviews.clear();
496
602
  runtime.exploreRun = { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 };
497
603
  runtime.renderedContentThisTurn = false;
498
604
  runtime.contentHeaderPrinted = false;
@@ -517,27 +623,36 @@ export function flushContent() {
517
623
  if (runtime.streamTimer) { clearTimeout(runtime.streamTimer); runtime.streamTimer = null; }
518
624
  if (!runtime.streamBuffer) return;
519
625
 
626
+ const rendered = renderMarkdown(runtime.streamBuffer);
627
+ const lines = transcriptRenderableLines(rendered);
628
+ runtime.streamBuffer = '';
629
+ if (!lines.length) return;
630
+
520
631
  if (isInputDockMounted()) moveToContent();
521
632
  stopSpinner();
522
633
  // Any buffered tool head needs to land BEFORE this content so the order
523
634
  // is preserved on screen.
524
635
  flushPendingHead();
525
636
  flushCompactReadRun();
526
- renderBlockBoundary('content');
637
+ renderBlockBoundary('content', { compactSame: true });
527
638
  if (!runtime.contentHeaderPrinted) {
528
639
  process.stdout.write(`${transcriptHeader('bahulam', { tone: 'assistant' })}\n`);
529
640
  runtime.contentHeaderPrinted = true;
530
641
  }
531
- const rendered = renderMarkdown(runtime.streamBuffer);
532
- for (const line of rendered.split('\n')) {
642
+ for (const line of lines) {
533
643
  process.stdout.write(`${transcriptLine(line, { tone: 'assistant' })}\n`);
534
644
  }
535
- runtime.streamBuffer = '';
536
645
  runtime.renderedContentThisTurn = true;
537
646
  runtime.lastRenderedBlock = 'content';
538
647
  if (typeof runtime.afterContentFlush === 'function') runtime.afterContentFlush();
539
648
  }
540
649
 
650
+ export function transcriptRenderableLines(rendered) {
651
+ const lines = String(rendered ?? '').replace(/\r\n?/g, '\n').split('\n');
652
+ while (lines.length && lines[lines.length - 1] === '') lines.pop();
653
+ return lines;
654
+ }
655
+
541
656
  export function renderStagnation(data = {}) {
542
657
  const rawMessage = data?.message || '';
543
658
  const reason = data?.reason || rawMessage.replace(/^Stagnation:\s*/i, '').trim();
@@ -39,9 +39,11 @@ export const runtime = {
39
39
  pendingHead: null, // { callId, head, indent } buffered until result arrives
40
40
  lastRenderedBlock: null, // 'tool' | 'content' | 'thinking' | 'status' | 'plan' | null
41
41
  renderedToolResults: new Set(),
42
+ renderedFileDiffPreviews: new Set(),
42
43
 
43
44
  // Explore-run collapse (read/list/search/index bursts as concise progress).
44
45
  exploreRun: { counts: {}, recent: [], lineActive: false, lastPrintedSummary: '', lastPrintedTotal: 0, lastPrintedAt: 0 },
46
+ foldedSubAgentTools: null, // { agentType, entries, startedAt } for default/quiet folded sub-agent tools
45
47
 
46
48
  // Animated spinner state (single shared interval; text/frame drive inPlace).
47
49
  spinInterval: null,