@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.
- package/package.json +4 -4
- package/src/agents/scaffold.mjs +1 -0
- package/src/commands/agent.mjs +3 -2
- package/src/core/approval-log.mjs +45 -4
- package/src/core/approval.mjs +265 -41
- package/src/core/file-diff.mjs +1 -1
- package/src/core/headless.mjs +14 -3
- package/src/core/local-agent.mjs +3 -2
- package/src/core/risk-tier.mjs +53 -2
- package/src/core/safety.mjs +61 -4
- package/src/core/tool-executor.mjs +38 -16
- package/src/core/trust.mjs +5 -3
- package/src/index.mjs +1 -1
- package/src/terminal/agents.mjs +194 -18
- package/src/terminal/repl-render.mjs +126 -11
- package/src/terminal/repl-state.mjs +2 -0
- package/src/terminal/repl.mjs +553 -88
- package/src/terminal/tool-display.mjs +154 -2
- package/src/ui/approval.mjs +211 -14
- package/src/ui/icons.mjs +11 -5
- package/src/ui/input-dock.mjs +214 -29
- package/src/ui/slash-commands.mjs +10 -0
- package/src/ui/tool-card.mjs +261 -30
- package/src/ui/tool-details.mjs +206 -14
- package/src/ui/transcript-block.mjs +2 -3
package/src/terminal/agents.mjs
CHANGED
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* - architect: Feature architect — designs implementations, file plans
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { c
|
|
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
|
|
110
|
-
* @param {
|
|
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
|
|
117
|
-
const agent =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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}] ${
|
|
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,
|
|
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 =
|
|
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)
|
|
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)
|
|
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)
|
|
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())
|
|
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())
|
|
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
|
|
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,
|