diff --git a/src/agent/check-approval.ts b/src/agent/check-approval.ts index 49ff35f..6e04bbe 100644 --- a/src/agent/check-approval.ts +++ b/src/agent/check-approval.ts @@ -43,13 +43,25 @@ export async function getResumeData( try { // Fetch messages from conversation or agent depending on what's provided let messages: Message[]; + // The source of truth for in-context message IDs: + // - For conversations: conversation.in_context_message_ids + // - For legacy agent-only: agent.message_ids + let inContextMessageIds: string[] | null | undefined; + if (conversationId) { // Use conversations API for conversation-specific history - messages = await client.conversations.messages.list(conversationId); + // Fetch both messages and conversation state in parallel + const [messagesResult, conversation] = await Promise.all([ + client.conversations.messages.list(conversationId), + client.conversations.retrieve(conversationId), + ]); + messages = messagesResult; + inContextMessageIds = conversation.in_context_message_ids; } else { // Fall back to agent messages (legacy behavior) const messagesPage = await client.agents.messages.list(agent.id); messages = messagesPage.items; + inContextMessageIds = agent.message_ids; } if (!messages || messages.length === 0) { @@ -61,8 +73,7 @@ export async function getResumeData( } // Compare cursor last message with in-context last message ID - // The backend uses in-context messages for CONFLICT validation, so if they're - // desynced, we need to check the in-context message for pending approvals + // The source of truth is the conversation's (or agent's) in_context_message_ids const cursorLastMessage = messages[messages.length - 1]; if (!cursorLastMessage) { return { @@ -73,8 +84,8 @@ export async function getResumeData( } const inContextLastMessageId = - agent.message_ids && agent.message_ids.length > 0 - ? agent.message_ids[agent.message_ids.length - 1] + inContextMessageIds && inContextMessageIds.length > 0 + ? inContextMessageIds[inContextMessageIds.length - 1] : null; // If there are no in-context messages, there can be no pending approval diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 87db7b1..c53837c 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -7113,12 +7113,15 @@ Plan file path: ${planFilePath}`; // Skip Task tools that don't have a pending approval // They render as empty Boxes (ToolCallMessage returns null for non-finished Task tools) // which causes N blank lines when N Task tools are called in parallel + // Note: pendingIds doesn't include the ACTIVE approval (currentApproval), + // so we must also check if this is the active approval if ( ln.kind === "tool_call" && ln.name && isTaskTool(ln.name) && ln.toolCallId && - !pendingIds.has(ln.toolCallId) + !pendingIds.has(ln.toolCallId) && + ln.toolCallId !== currentApproval?.toolCallId ) { return null; } @@ -7462,21 +7465,68 @@ Plan file path: ${planFilePath}`; {/* Fallback approval UI when backfill is disabled (no liveItems) */} {liveItems.length === 0 && currentApproval && ( - handleApproveCurrent()} - onApproveAlways={(scope) => handleApproveAlways(scope)} - onDeny={(reason) => handleDenyCurrent(reason)} - onCancel={handleCancelApprovals} - isFocused={true} - approveAlwaysText={ - currentApprovalContext?.approveAlwaysText - } - allowPersistence={ - currentApprovalContext?.allowPersistence ?? true - } - /> + {isTaskTool(currentApproval.toolName) ? ( + { + try { + const args = JSON.parse( + currentApproval.toolArgs || "{}", + ); + return { + subagentType: + typeof args.subagent_type === "string" + ? args.subagent_type + : "unknown", + description: + typeof args.description === "string" + ? args.description + : "(no description)", + prompt: + typeof args.prompt === "string" + ? args.prompt + : "(no prompt)", + model: + typeof args.model === "string" + ? args.model + : undefined, + }; + } catch { + return { + subagentType: "unknown", + description: "(parse error)", + prompt: "(parse error)", + }; + } + })()} + onApprove={() => handleApproveCurrent()} + onApproveAlways={(scope) => handleApproveAlways(scope)} + onDeny={(reason) => handleDenyCurrent(reason)} + onCancel={handleCancelApprovals} + isFocused={true} + approveAlwaysText={ + currentApprovalContext?.approveAlwaysText + } + allowPersistence={ + currentApprovalContext?.allowPersistence ?? true + } + /> + ) : ( + handleApproveCurrent()} + onApproveAlways={(scope) => handleApproveAlways(scope)} + onDeny={(reason) => handleDenyCurrent(reason)} + onCancel={handleCancelApprovals} + isFocused={true} + approveAlwaysText={ + currentApprovalContext?.approveAlwaysText + } + allowPersistence={ + currentApprovalContext?.allowPersistence ?? true + } + /> + )} )} diff --git a/src/index.ts b/src/index.ts index 2c0b796..0f0d574 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1292,10 +1292,12 @@ async function main(): Promise { setResumedExistingConversation(true); // Load message history and pending approvals from the conversation + // Re-fetch agent to get fresh message_ids for accurate pending approval detection setLoadingState("checking"); + const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, - agent, + freshAgent, specifiedConversationId, ); setResumeData(data); @@ -1316,10 +1318,12 @@ async function main(): Promise { setResumedExistingConversation(true); // Load message history and pending approvals from the conversation + // Re-fetch agent to get fresh message_ids for accurate pending approval detection setLoadingState("checking"); + const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, - agent, + freshAgent, lastSession.conversationId, ); setResumeData(data);