Keep rejected managed approval pending in UI

This commit is contained in:
mateaix 2026-09-15 04:47:06 +08:00
parent 90c9256dc6
commit 3e9377458d
2 changed files with 45 additions and 17 deletions

View File

@ -824,13 +824,18 @@ export function useChat(options: UseChatOptions): UseChatReturn {
currentAssistantId.value = null
}
streamPhase.value = data.status === 'awaiting_approval' ? 'awaiting_approval'
streamPhase.value = errorFired ? 'idle'
: data.status === 'awaiting_approval' ? 'awaiting_approval'
: data.status === 'stopped' ? 'stopped' : 'completed'
if (data.status !== 'awaiting_approval') {
phaseInfo.value = null
compactStatus.value = null
lifecycleStage.value = null
expirePendingApprovals(data.status === 'stopped' ? 'stopped' : 'completed')
// An error may be followed by a protocol-level done event. Its status
// does not resolve an approval or turn the failed request into success.
if (!errorFired) {
expirePendingApprovals(data.status === 'stopped' ? 'stopped' : 'completed')
}
}
// Safety cleanup for queue state (no-op if queued_input_started already handled it)
@ -846,13 +851,15 @@ export function useChat(options: UseChatOptions): UseChatReturn {
: data.status === 'interrupted' ? 'interrupted'
: data.status === 'awaiting_approval' ? 'awaiting_approval'
: 'completed'
onStreamEnd?.({
conversationId: data.conversationId || streamConversationId,
reason,
assistantMessageId: data.assistantMessageId,
persisted: data.persisted,
messageCount: data.messageCount,
})
if (!errorFired) {
onStreamEnd?.({
conversationId: data.conversationId || streamConversationId,
reason,
assistantMessageId: data.assistantMessageId,
persisted: data.persisted,
messageCount: data.messageCount,
})
}
// Re-attach SSE if any generative task is still in flight, so the eventual
// async_task_completed event reaches us live (otherwise the user has to
@ -861,7 +868,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
const reconnectableStatus = !data.status
|| data.status === 'completed'
|| data.status === 'idle'
if (reconnectableStatus
if (!errorFired && reconnectableStatus
&& !reconnectingForAsyncTasks
&& pendingAsyncTaskIds.size > 0
&& streamConversationId) {
@ -918,7 +925,8 @@ export function useChat(options: UseChatOptions): UseChatReturn {
lifecycleStage.value = null
// Clear queue on error to avoid stale state
messageQueue.clear()
expirePendingApprovals('failed')
// The approval may still be pending after a rejected request. The view
// reconciles it against the server's pending list in onStreamEnd.
if (errorFired) return
errorFired = true

View File

@ -808,6 +808,32 @@ const {
// lastActiveTime /
await loadConversations()
if (meta.conversationId && meta.conversationId === currentConversationId.value) {
if (meta.reason === 'error') {
// Keep the failed turn visible, but take approval state from the
// server. A rejected approval SSE error leaves its pending row open.
try {
const approvalRes: any = await chatApi.getPendingApprovals(meta.conversationId)
if (meta.conversationId !== currentConversationId.value) return
const serverIds = new Set<string>((approvalRes.data || []).map((p: any) => p.pendingId))
messages.value = messages.value.map((m) => {
const pending = (m as any).metadata?.pendingApproval
if (!pending?.pendingId || pending.status !== 'pending_approval') return m
const active = serverIds.has(pending.pendingId)
return {
...m,
status: active ? 'awaiting_approval'
: m.status === 'awaiting_approval' ? 'failed' : m.status,
metadata: {
...(m as any).metadata,
currentPhase: active ? 'awaiting_approval' : undefined,
pendingApproval: { ...pending, status: active ? 'pending_approval' : 'expired' },
},
}
})
} catch {
// Keep the local pending card if the authoritative read is unavailable.
}
}
// Skip DB refresh for awaiting_approval / interrupted / error:
// - awaiting_approval / interrupted: avoids overwriting local-only state
// or breaking message ordering.
@ -2065,10 +2091,6 @@ async function handleSendMessage(content: string, pendingApprovalId?: string) {
return
}
//
const decision = trimmed === '/approve' ? 'approved' : 'denied'
;(pendingMsg as any).metadata.pendingApproval.status = decision
inputText.value = ''
chatInputRef.value?.clear?.()
@ -2082,8 +2104,6 @@ async function handleSendMessage(content: string, pendingApprovalId?: string) {
})
} catch (e: any) {
console.error('Approval stream failed:', e)
//
;(pendingMsg as any).metadata.pendingApproval.status = 'pending_approval'
mcToast.error(e?.message || 'Approval failed')
}
return