diff --git a/src/agent/approval-execution.ts b/src/agent/approval-execution.ts index 994e61d..5fdaa7c 100644 --- a/src/agent/approval-execution.ts +++ b/src/agent/approval-execution.ts @@ -56,6 +56,9 @@ const PARALLEL_SAFE_TOOLS = new Set([ "BashOutput", // Task spawns independent subagents "Task", + // Plan mode tools (no parameters, no file operations) + "EnterPlanMode", + "ExitPlanMode", ]); function isParallelSafe(toolName: string): boolean { @@ -194,10 +197,18 @@ async function executeSingleDecision( // Execute the approved tool try { - const parsedArgs = - typeof decision.approval.toolArgs === "string" - ? JSON.parse(decision.approval.toolArgs) - : decision.approval.toolArgs || {}; + // Safe parse - toolArgs should be "{}" but handle edge cases + let parsedArgs: Record = {}; + if (typeof decision.approval.toolArgs === "string") { + try { + parsedArgs = JSON.parse(decision.approval.toolArgs); + } catch { + // Empty or malformed args - use empty object + parsedArgs = {}; + } + } else { + parsedArgs = decision.approval.toolArgs || {}; + } const toolResult = await executeTool( decision.approval.toolName, @@ -328,10 +339,18 @@ export async function executeApprovalBatch( parallelIndices.push(i); } else { // Get resource key for write tools - const args = - typeof decision.approval.toolArgs === "string" - ? JSON.parse(decision.approval.toolArgs) - : decision.approval.toolArgs || {}; + // Safe parse - handle empty or malformed toolArgs + let args: Record = {}; + if (typeof decision.approval.toolArgs === "string") { + try { + args = JSON.parse(decision.approval.toolArgs); + } catch { + // Empty or malformed args - use empty object (will use global lock) + args = {}; + } + } else { + args = decision.approval.toolArgs || {}; + } const resourceKey = getResourceKey(toolName, args); const indices = writeToolsByResource.get(resourceKey) || []; diff --git a/src/cli/helpers/stream.ts b/src/cli/helpers/stream.ts index 53d07fa..f94ee70 100644 --- a/src/cli/helpers/stream.ts +++ b/src/cli/helpers/stream.ts @@ -215,10 +215,12 @@ export async function drainStream( // Include ALL tool_call_ids - don't filter out incomplete entries // Missing name/args will be handled by denial logic in App.tsx + // Default empty toolArgs to "{}" - empty string causes JSON.parse("") to fail + // This happens for tools with no parameters (e.g., EnterPlanMode, ExitPlanMode) approvals = allPending.map((a) => ({ toolCallId: a.toolCallId, toolName: a.toolName || "", - toolArgs: a.toolArgs || "", + toolArgs: a.toolArgs || "{}", })); if (approvals.length === 0) {