fix: use queue for permission requests to prevent blocking

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 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-01-15 21:19:49 +08:00
parent 5d112e99cd
commit 205d0e73f3
5 changed files with 49 additions and 32 deletions

View File

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

View File

@@ -23,7 +23,7 @@ interface ChatViewProps {
export function ChatView({ updates, isProcessing, hasSession, isInitializing, currentSessionId, onSelectFolder }: ChatViewProps) {
const bottomRef = useRef<HTMLDivElement>(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

View File

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

View File

@@ -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 () => {

View File

@@ -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<string, RespondedRequest>
@@ -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<PermissionStore>((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<PermissionStore>((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<PermissionStore>((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<PermissionStore>((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<PermissionStore>((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<PermissionStore>((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 = {