From 55ba8d0969b4c9e34e47fe621ea4812528441365 Mon Sep 17 00:00:00 2001 From: DexterFromLab Date: Thu, 12 Mar 2026 16:24:20 +0100 Subject: [PATCH] Add incoming message visibility and shell command execution to Aider runner - Emit 'input' events so agents show received prompts in their console - Execute detected shell commands (btmsg, bttask, etc.) from LLM output - Feed command results back to aider for iterative autonomous work - Detect commands in code blocks, bare btmsg/bttask lines, and $ prefixes - More robust THINKING/ANSWER marker detection (multiple unicode variants) - Adapter handles new 'input' and 'tool_result' event types Co-Authored-By: Claude Opus 4.6 --- v2/sidecar/aider-runner.ts | 128 ++++++++++++++++++++++++-- v2/src/lib/adapters/aider-messages.ts | 22 +++++ 2 files changed, 141 insertions(+), 9 deletions(-) diff --git a/v2/sidecar/aider-runner.ts b/v2/sidecar/aider-runner.ts index 313ece4..57e719e 100644 --- a/v2/sidecar/aider-runner.ts +++ b/v2/sidecar/aider-runner.ts @@ -145,6 +145,19 @@ function shouldSuppress(line: string): boolean { return t === '' || SUPPRESS_RE.some(p => p.test(t)); } +// --- Shell command execution --- +// Runs a shell command and returns {stdout, stderr, exitCode} + +function execShell(cmd: string, env: Record, cwd: string): { stdout: string; exitCode: number } { + try { + const result = execSync(cmd, { env, cwd, timeout: 30000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); + return { stdout: result.trim(), exitCode: 0 }; + } catch (e: unknown) { + const err = e as { stdout?: string; stderr?: string; status?: number }; + return { stdout: (err.stdout ?? err.stderr ?? String(e)).trim(), exitCode: err.status ?? 1 }; + } +} + // --- Turn output parsing --- // Parses complete turn output into structured blocks: // thinking, answer text, shell commands, cost info @@ -154,6 +167,9 @@ interface TurnBlock { content: string; } +// Known shell command patterns — commands from btmsg/bttask/common tools +const SHELL_CMD_RE = /^(btmsg |bttask |cat |ls |find |grep |mkdir |cd |cp |mv |rm |pip |npm |git |curl |wget |python |node |bash |sh )/; + function parseTurnOutput(buffer: string): TurnBlock[] { const blocks: TurnBlock[] = []; const lines = buffer.split('\n'); @@ -162,23 +178,26 @@ function parseTurnOutput(buffer: string): TurnBlock[] { let answerLines: string[] = []; let inThinking = false; let inAnswer = false; + let inCodeBlock = false; + let codeBlockLang = ''; + let codeBlockLines: string[] = []; for (const line of lines) { const t = line.trim(); // Skip suppressed lines - if (shouldSuppress(line)) continue; + if (shouldSuppress(line) && !inCodeBlock) continue; // Prompt markers — skip if (PROMPT_RE.test(t)) continue; - // Thinking block markers - if (t === '► THINKING' || t === '► THINKING') { + // Thinking block markers (handle various unicode arrows and spacing) + if (/^[►▶⯈❯>]\s*THINKING$/i.test(t)) { inThinking = true; inAnswer = false; continue; } - if (t === '► ANSWER' || t === '► ANSWER') { + if (/^[►▶⯈❯>]\s*ANSWER$/i.test(t)) { if (thinkingLines.length > 0) { blocks.push({ type: 'thinking', content: thinkingLines.join('\n') }); thinkingLines = []; @@ -188,15 +207,44 @@ function parseTurnOutput(buffer: string): TurnBlock[] { continue; } + // Code block detection (```bash, ```shell, ```) + if (t.startsWith('```') && !inCodeBlock) { + inCodeBlock = true; + codeBlockLang = t.slice(3).trim().toLowerCase(); + codeBlockLines = []; + continue; + } + if (t === '```' && inCodeBlock) { + inCodeBlock = false; + // If this was a bash/shell code block, extract commands + if (['bash', 'shell', 'sh', ''].includes(codeBlockLang)) { + for (const cmdLine of codeBlockLines) { + const cmd = cmdLine.trim().replace(/^\$ /, ''); + if (cmd && SHELL_CMD_RE.test(cmd)) { + if (answerLines.length > 0) { + blocks.push({ type: 'text', content: answerLines.join('\n') }); + answerLines = []; + } + blocks.push({ type: 'shell', content: cmd }); + } + } + } + codeBlockLines = []; + continue; + } + if (inCodeBlock) { + codeBlockLines.push(line); + continue; + } + // Cost line if (/^Tokens: .+Cost:/.test(t)) { blocks.push({ type: 'cost', content: t }); continue; } - // Shell command + // Shell command ($ prefix or Running prefix) if (t.startsWith('$ ') || t.startsWith('Running ')) { - // Flush accumulated answer text first if (answerLines.length > 0) { blocks.push({ type: 'text', content: answerLines.join('\n') }); answerLines = []; @@ -205,6 +253,19 @@ function parseTurnOutput(buffer: string): TurnBlock[] { continue; } + // Detect bare btmsg/bttask commands in answer text + if (inAnswer && SHELL_CMD_RE.test(t) && !t.includes('`') && !t.startsWith('#')) { + if (answerLines.length > 0) { + blocks.push({ type: 'text', content: answerLines.join('\n') }); + answerLines = []; + } + blocks.push({ type: 'shell', content: t }); + continue; + } + + // Aider's "Applied edit" / flake8 output — suppress from answer text + if (/^Applied edit to |^Fix any errors|^Running: /.test(t)) continue; + // Accumulate into thinking or answer if (inThinking) { thinkingLines.push(line); @@ -249,6 +310,13 @@ async function handleQuery(msg: QueryMessage) { send({ type: 'agent_started', sessionId }); + // Show the incoming prompt in the console + send({ + type: 'agent_event', + sessionId, + event: { type: 'input', prompt }, + }); + // Pre-fetch fresh context for follow-up turns too const ctx = prefetchContext(existing.env, existing.cwd); const fullPrompt = `${ctx}\n\nNow act on the above. Your current task:\n${prompt}`; @@ -294,6 +362,13 @@ async function handleQuery(msg: QueryMessage) { event: { type: 'system', subtype: 'init', session_id: sessionId, model: aiderModel, cwd }, }); + // Show the incoming prompt in the console + send({ + type: 'agent_event', + sessionId, + event: { type: 'input', prompt }, + }); + const child = spawn(aiderPath, args, { cwd, env, @@ -355,7 +430,9 @@ async function handleQuery(msg: QueryMessage) { const duration = Date.now() - session.turnStartTime; const blocks = parseTurnOutput(session.turnBuffer); - // Emit structured blocks + // Emit structured blocks and execute shell commands + const shellResults: string[] = []; + for (const block of blocks) { switch (block.type) { case 'thinking': @@ -376,18 +453,40 @@ async function handleQuery(msg: QueryMessage) { } break; - case 'shell': + case 'shell': { + const cmdId = `shell-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + + // Emit tool_use (command being run) send({ type: 'agent_event', sessionId, event: { type: 'tool_use', - id: `shell-${Date.now()}`, + id: cmdId, name: 'Bash', input: { command: block.content }, }, }); + + // Actually execute the command + log(`[exec] Running: ${block.content}`); + const result = execShell(block.content, session.env, session.cwd); + const output = result.stdout || '(no output)'; + + // Emit tool_result (command output) + send({ + type: 'agent_event', + sessionId, + event: { + type: 'tool_result', + tool_use_id: cmdId, + content: output, + }, + }); + + shellResults.push(`$ ${block.content}\n${output}`); break; + } case 'cost': // Parsed below for the result event @@ -416,6 +515,17 @@ async function handleQuery(msg: QueryMessage) { send({ type: 'agent_stopped', sessionId, exitCode: 0, signal: null }); session.turnBuffer = ''; + + // If commands were executed, feed results back to aider for next turn + if (shellResults.length > 0 && child.exitCode === null) { + const feedback = `The following commands were executed and here are the results:\n\n${shellResults.join('\n\n')}\n\nBased on these results, continue your work. If the task is complete, say "DONE".`; + log(`[exec] Feeding ${shellResults.length} command results back to aider`); + session.turnBuffer = ''; + session.turnStartTime = Date.now(); + session.turns++; + send({ type: 'agent_started', sessionId }); + child.stdin?.write(feedback + '\n'); + } }); child.stderr?.on('data', (data: Buffer) => { diff --git a/v2/src/lib/adapters/aider-messages.ts b/v2/src/lib/adapters/aider-messages.ts index 71e8ab7..efc203d 100644 --- a/v2/src/lib/adapters/aider-messages.ts +++ b/v2/src/lib/adapters/aider-messages.ts @@ -7,6 +7,7 @@ import type { TextContent, ThinkingContent, ToolCallContent, + ToolResultContent, CostContent, ErrorContent, } from './claude-messages'; @@ -20,7 +21,9 @@ import { str, num } from '../utils/type-guards'; * - {type:'system', subtype:'init', model, session_id, cwd} * - {type:'assistant', message:{role:'assistant', content:'...'}} — batched text block * - {type:'thinking', content:'...'} — thinking/reasoning block + * - {type:'input', prompt:'...'} — incoming prompt/message (shown in console) * - {type:'tool_use', id, name, input} — shell command execution + * - {type:'tool_result', tool_use_id, content} — shell command output * - {type:'result', subtype:'result', cost_usd, duration_ms, is_error} * - {type:'error', message:'...'} */ @@ -50,6 +53,14 @@ export function adaptAiderMessage(raw: Record): AgentMessage[] timestamp, }]; + case 'input': + return [{ + id: uuid, + type: 'text', + content: { text: `📨 **Received:**\n${str(raw.prompt)}` } satisfies TextContent, + timestamp, + }]; + case 'thinking': return [{ id: uuid, @@ -84,6 +95,17 @@ export function adaptAiderMessage(raw: Record): AgentMessage[] timestamp, }]; + case 'tool_result': + return [{ + id: uuid, + type: 'tool_result', + content: { + toolUseId: str(raw.tool_use_id), + output: raw.content, + } satisfies ToolResultContent, + timestamp, + }]; + case 'result': return [{ id: uuid,