From 205d0e73f3e48cf978facf87c3f873570e6473dc Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:19:49 +0800 Subject: [PATCH] fix: use queue for permission requests to prevent blocking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed pendingRequest from single value to pendingRequests queue. This prevents consecutive permission requests from overwriting each other, which caused the first request's Promise to never resolve. - permissionStore: pendingRequest → pendingRequests[] - Added addPendingRequest() and getCurrentRequest() - respondToRequest() now removes from queue head - Updated all consumers to use queue[0] Co-Authored-By: Claude Opus 4.5 --- src/renderer/src/App.tsx | 2 +- src/renderer/src/components/ChatView.tsx | 2 +- .../permission/PermissionRequestItem.tsx | 4 +- src/renderer/src/hooks/useApp.ts | 4 +- src/renderer/src/stores/permissionStore.ts | 69 ++++++++++++------- 5 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 39fbd804e..1c519aee4 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -45,7 +45,7 @@ function AppContent(): React.JSX.Element { const setSidebarOpen = useUIStore((s) => s.setSidebarOpen) // Permission state - get the session ID that has a pending permission request - const pendingPermission = usePermissionStore((s) => s.pendingRequest) + const pendingPermission = usePermissionStore((s) => s.pendingRequests[0] ?? null) const permissionPendingSessionId = pendingPermission?.multicaSessionId ?? null // Modal actions diff --git a/src/renderer/src/components/ChatView.tsx b/src/renderer/src/components/ChatView.tsx index 6c588afdb..bdea6be89 100644 --- a/src/renderer/src/components/ChatView.tsx +++ b/src/renderer/src/components/ChatView.tsx @@ -23,7 +23,7 @@ interface ChatViewProps { export function ChatView({ updates, isProcessing, hasSession, isInitializing, currentSessionId, onSelectFolder }: ChatViewProps) { const bottomRef = useRef(null) - const pendingPermission = usePermissionStore((s) => s.pendingRequest) + const pendingPermission = usePermissionStore((s) => s.pendingRequests[0] ?? null) // Only show permission request if it belongs to the current session const currentPermission = pendingPermission?.multicaSessionId === currentSessionId diff --git a/src/renderer/src/components/permission/PermissionRequestItem.tsx b/src/renderer/src/components/permission/PermissionRequestItem.tsx index 006bd9f2e..e22615ef2 100644 --- a/src/renderer/src/components/permission/PermissionRequestItem.tsx +++ b/src/renderer/src/components/permission/PermissionRequestItem.tsx @@ -12,12 +12,12 @@ import { StandardPermissionUI } from './StandardPermissionUI' import type { PermissionRequestItemProps, AskUserQuestionInput } from './types' export function PermissionRequestItem({ request }: PermissionRequestItemProps) { - const pendingRequest = usePermissionStore((s) => s.pendingRequest) + const currentRequest = usePermissionStore((s) => s.pendingRequests[0] ?? null) const currentQuestionIndex = usePermissionStore((s) => s.currentQuestionIndex) const getRespondedRequest = usePermissionStore((s) => s.getRespondedRequest) const { toolCall } = request - const isPending = pendingRequest?.requestId === request.requestId + const isPending = currentRequest?.requestId === request.requestId // Get responded data for completed requests const respondedData = getRespondedRequest(request.requestId) diff --git a/src/renderer/src/hooks/useApp.ts b/src/renderer/src/hooks/useApp.ts index 79164d642..5fe840999 100644 --- a/src/renderer/src/hooks/useApp.ts +++ b/src/renderer/src/hooks/useApp.ts @@ -158,10 +158,10 @@ export function useApp(): AppState & AppActions { }) // Subscribe to permission requests - const setPendingRequest = usePermissionStore.getState().setPendingRequest + const addPendingRequest = usePermissionStore.getState().addPendingRequest const unsubPermission = window.electronAPI.onPermissionRequest((request) => { console.log('[useApp] Permission request received:', request) - setPendingRequest(request) + addPendingRequest(request) }) return () => { diff --git a/src/renderer/src/stores/permissionStore.ts b/src/renderer/src/stores/permissionStore.ts index a1527ebf1..293c87e2f 100644 --- a/src/renderer/src/stores/permissionStore.ts +++ b/src/renderer/src/stores/permissionStore.ts @@ -44,8 +44,8 @@ export interface RespondedRequest { } interface PermissionStore { - // Current pending permission request - pendingRequest: PermissionRequest | null + // Pending permission requests queue (supports concurrent requests) + pendingRequests: PermissionRequest[] // Responded requests (requestId -> responded info) respondedRequests: Map @@ -60,8 +60,11 @@ interface PermissionStore { // Collected answers for multi-question AskUserQuestion collectedAnswers: QuestionAnswer[] - // Set pending request - setPendingRequest: (request: PermissionRequest | null) => void + // Get current request (first in queue) + getCurrentRequest: () => PermissionRequest | null + + // Add pending request to queue + addPendingRequest: (request: PermissionRequest) => void // Respond to permission request (with optional data for AskUserQuestion) respondToRequest: (optionId: string, data?: PermissionResponseData) => void @@ -80,25 +83,36 @@ interface PermissionStore { } export const usePermissionStore = create((set, get) => ({ - pendingRequest: null, + pendingRequests: [], respondedRequests: new Map(), lastRespondedRequest: null, currentQuestionIndex: 0, collectedAnswers: [], - setPendingRequest: (request) => { - // Clear last responded and reset multi-question state when a new request comes in - set({ - pendingRequest: request, - lastRespondedRequest: null, - currentQuestionIndex: 0, - collectedAnswers: [], + getCurrentRequest: () => { + return get().pendingRequests[0] || null + }, + + addPendingRequest: (request) => { + set((state) => { + const isQueueEmpty = state.pendingRequests.length === 0 + return { + pendingRequests: [...state.pendingRequests, request], + // Only reset multi-question state when queue was empty + ...(isQueueEmpty ? { + lastRespondedRequest: null, + currentQuestionIndex: 0, + collectedAnswers: [], + } : {}), + } }) }, respondToRequest: (optionId, data) => { - const { pendingRequest, respondedRequests } = get() - if (!pendingRequest) { + const { pendingRequests, respondedRequests } = get() + const currentRequest = pendingRequests[0] + + if (!currentRequest) { console.warn('[PermissionStore] No pending request to respond to') return } @@ -110,11 +124,12 @@ export const usePermissionStore = create((set, get) => ({ selectedOptionsLength: data?.selectedOptions?.length, hasAnswers: !!data?.answers, answersLength: data?.answers?.length, + queueLength: pendingRequests.length, }) // Create responded request object const respondedRequest: RespondedRequest = { - request: pendingRequest, + request: currentRequest, response: { optionId, selectedOption: data?.selectedOption, @@ -127,10 +142,10 @@ export const usePermissionStore = create((set, get) => ({ // Store the responded request for later reference const newResponded = new Map(respondedRequests) - newResponded.set(pendingRequest.requestId, respondedRequest) + newResponded.set(currentRequest.requestId, respondedRequest) const response: PermissionResponse = { - requestId: pendingRequest.requestId, + requestId: currentRequest.requestId, optionId, data, } @@ -138,10 +153,10 @@ export const usePermissionStore = create((set, get) => ({ // Send response to main process window.electronAPI.respondToPermission(response) - // Clear pending request, store responded, and set as last responded - // Also reset multi-question state + // Remove current request from queue (shift), store responded + // Reset multi-question state for the next request set({ - pendingRequest: null, + pendingRequests: pendingRequests.slice(1), respondedRequests: newResponded, lastRespondedRequest: respondedRequest, currentQuestionIndex: 0, @@ -150,14 +165,16 @@ export const usePermissionStore = create((set, get) => ({ }, answerCurrentQuestion: (answer, isCustom = false) => { - const { pendingRequest, currentQuestionIndex, collectedAnswers } = get() - if (!pendingRequest) { + const { pendingRequests, currentQuestionIndex, collectedAnswers } = get() + const currentRequest = pendingRequests[0] + + if (!currentRequest) { console.warn('[PermissionStore] No pending request for answerCurrentQuestion') return } // Get questions from rawInput - const rawInput = pendingRequest.toolCall.rawInput as AskUserQuestionInput | undefined + const rawInput = currentRequest.toolCall.rawInput as AskUserQuestionInput | undefined const questions = rawInput?.questions || [] const currentQuestion = questions[currentQuestionIndex] @@ -190,9 +207,9 @@ export const usePermissionStore = create((set, get) => ({ console.log(`[PermissionStore] All ${questions.length} questions answered, sending response`) // Find allow option - const allowOption = pendingRequest.options.find((o) => o.kind === 'allow_once') || - pendingRequest.options.find((o) => o.kind === 'allow') || - pendingRequest.options[0] + const allowOption = currentRequest.options.find((o) => o.kind === 'allow_once') || + currentRequest.options.find((o) => o.kind === 'allow') || + currentRequest.options[0] // Build response data with all answers const responseData: PermissionResponseData = {