Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 24 additions & 35 deletions apps/desktop/src/app/chat/composer/hooks/use-composer-queue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react'
import { type RefObject, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'

import { useI18n } from '@/i18n'
import { triggerHaptic } from '@/lib/haptics'
import { useSessionSlice } from '@/lib/use-session-slice'
import { queueManager as QueueManager } from '@/lib/queue-manager'
import { type ComposerAttachment } from '@/store/composer'
import { resetBrowseState } from '@/store/composer-input-history'
import {
$queuedPromptsBySession,
enqueueQueuedPrompt,
getQueuedPrompts,
MAX_AUTO_DRAIN_ATTEMPTS,
Expand All @@ -17,6 +16,7 @@
shouldAutoDrain,
updateQueuedPrompt
} from '@/store/composer-queue'

import { notify } from '@/store/notifications'

import { cloneAttachments, type QueueEditState } from '../composer-utils'
Expand Down Expand Up @@ -64,10 +64,12 @@
const { t } = useI18n()
const scope = useComposerScope()

// Per-session slice (edge): re-renders only when THIS session's queue changes,
// not on cross-session queue churn (the plain atom's map ref changes on every
// write; the keyed array does not).
const queuedPrompts = useSessionSlice($queuedPromptsBySession, activeQueueSessionKey)
// Per-session queue slice: re-renders only when THIS session's queue changes.
const emptyQueue = useRef<QueuedPromptEntry[]>([]).current
const queuedPrompts = useSyncExternalStore(
useCallback((cb: () => void) => QueueManager.subscribe(cb), []),
useCallback(() => activeQueueSessionKey ? QueueManager.getAll(activeQueueSessionKey) : emptyQueue, [activeQueueSessionKey])

Check warning on line 71 in apps/desktop/src/app/chat/composer/hooks/use-composer-queue.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / Typecheck & Test (apps/desktop)

React Hook useCallback has a missing dependency: 'emptyQueue'. Either include it or remove the dependency array
)

const [queueEdit, setQueueEdit] = useState<QueueEditState | null>(null)
queueEditRef.current = queueEdit
Expand Down Expand Up @@ -183,47 +185,34 @@
}, [activeQueueSessionKey, attachments, clearDraft, draftRef, scope.attachments])

// All queue drain paths share one lock + send-then-remove sequence.
// `pickEntry` lets each caller choose head, by-id, or skip-edited.
const runDrain = useCallback(
async (pickEntry: (entries: QueuedPromptEntry[]) => QueuedPromptEntry | undefined): Promise<boolean> => {
if (drainingQueueRef.current || !activeQueueSessionKey) {
if (!activeQueueSessionKey) {
return false
}

const drainQueueSessionKey = activeQueueSessionKey
const drainRuntimeSessionId = sessionId ?? null
const entry = pickEntry(getQueuedPrompts(drainQueueSessionKey))
const entry = pickEntry(getQueuedPrompts(activeQueueSessionKey))

if (!entry) {
return false
}

drainingQueueRef.current = true

try {
const accepted = await Promise.resolve(
onSubmit(entry.text, {
attachments: entry.attachments,
fromQueue: true,
sessionId: drainRuntimeSessionId,
storedSessionId: drainQueueSessionKey
})
)

if (accepted === false) {
return false
}

drainFailuresRef.current.delete(entry.id)
removeQueuedPrompt(drainQueueSessionKey, entry.id)
resetBrowseState(drainRuntimeSessionId)
const accepted = await Promise.resolve(
onSubmit(entry.text, {
attachments: entry.attachments,
fromQueue: true,
storedSessionId: activeQueueSessionKey
})
)

return true
} finally {
drainingQueueRef.current = false
if (accepted !== false) {
removeQueuedPrompt(activeQueueSessionKey, entry.id)
resetBrowseState(activeQueueSessionKey)
}

return accepted !== false
},
[activeQueueSessionKey, onSubmit, sessionId]
[activeQueueSessionKey, onSubmit]
)

const pickDrainHead = useCallback(
Expand Down
20 changes: 11 additions & 9 deletions apps/desktop/src/app/contrib/wiring.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import { PersistentTerminal } from '../right-sidebar/terminal/persistent'
import { CRON_ROUTE, routeSessionId, sessionRoute, SETTINGS_ROUTE, syncWorkspaceIsPage } from '../routes'
import { SessionPickerOverlay } from '../session-picker-overlay'
import { SessionSwitcher } from '../session-switcher'
import { useBackgroundQueueDrain } from '../session/hooks/use-background-queue-drain'
import { QueueManager } from '@/lib/queue-manager'
import { useContextSuggestions } from '../session/hooks/use-context-suggestions'
import { useCwdActions } from '../session/hooks/use-cwd-actions'
import { useHermesConfig } from '../session/hooks/use-hermes-config'
Expand Down Expand Up @@ -540,14 +540,16 @@ export function ContribWiring({ children }: { children: ReactNode }) {
updateSessionState
})

// Runs outside the selected ChatBar so queues belonging to background
// sessions continue once those sessions are idle.
useBackgroundQueueDrain({
enabled: gatewayState === 'open',
runtimeIdByStoredSessionIdRef,
selectedStoredSessionId,
submitText
})
// QueueManager handles all auto-drain logic independently of component
// lifecycle. It subscribes to $workingSessionIds and resolves session
// identity through session.resume (#61573).
useEffect(() => {
if (gatewayState === 'open') {
QueueManager.init(requestGateway)
}

return () => QueueManager.destroy()
}, [gatewayState, requestGateway])

// Session-tile delegate (resume/submit/interrupt/slash + the session verbs
// the tile TAB menu needs, without touching the primary view).
Expand Down
142 changes: 0 additions & 142 deletions apps/desktop/src/app/session/hooks/use-background-queue-drain.test.tsx

This file was deleted.

Loading
Loading