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);