fix: task tool rendering issues (#534)

This commit is contained in:
Charles Packer
2026-01-13 19:21:42 -08:00
committed by GitHub
parent 6ccb8b9605
commit 480a270f53
3 changed files with 88 additions and 23 deletions

View File

@@ -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

View File

@@ -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 && (
<Box flexDirection="column">
<InlineGenericApproval
toolName={currentApproval.toolName}
toolArgs={currentApproval.toolArgs}
onApprove={() => handleApproveCurrent()}
onApproveAlways={(scope) => handleApproveAlways(scope)}
onDeny={(reason) => handleDenyCurrent(reason)}
onCancel={handleCancelApprovals}
isFocused={true}
approveAlwaysText={
currentApprovalContext?.approveAlwaysText
}
allowPersistence={
currentApprovalContext?.allowPersistence ?? true
}
/>
{isTaskTool(currentApproval.toolName) ? (
<InlineTaskApproval
taskInfo={(() => {
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
}
/>
) : (
<InlineGenericApproval
toolName={currentApproval.toolName}
toolArgs={currentApproval.toolArgs}
onApprove={() => handleApproveCurrent()}
onApproveAlways={(scope) => handleApproveAlways(scope)}
onDeny={(reason) => handleDenyCurrent(reason)}
onCancel={handleCancelApprovals}
isFocused={true}
approveAlwaysText={
currentApprovalContext?.approveAlwaysText
}
allowPersistence={
currentApprovalContext?.allowPersistence ?? true
}
/>
)}
</Box>
)}

View File

@@ -1292,10 +1292,12 @@ async function main(): Promise<void> {
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<void> {
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);