feat(desktop): concurrent multi-profile gateway sockets
Keep one persistent socket per profile with live work instead of closing the single socket on every profile swap, so background sessions across profiles keep streaming at once. A gateway registry owns the primary (window) socket plus lazy secondaries (own backoff/reconnect); all feed the same session-keyed event handler. Secondaries are pruned to profiles with a working/needs-input session, the keepalive pings every open backend, and LRU eviction spares freshly-touched backends so the soft cap can't abort a running agent. Approval/sudo/secret prompts are parked per-session (surfaced via the needs-input badge) so a background turn can block without hijacking the foreground. Single-profile users only ever have the primary, so their path is unchanged.
This commit is contained in:
@@ -2,7 +2,8 @@ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime }
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { $approvalRequest } from '@/store/prompts'
|
||||
import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
|
||||
import { $activeSessionId } from '@/store/session'
|
||||
import { $toolDisclosureStates } from '@/store/tool-view'
|
||||
|
||||
import { Thread } from './thread'
|
||||
@@ -120,13 +121,15 @@ function GroupHarness({ message }: { message: ThreadMessage }) {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
$approvalRequest.set(null)
|
||||
clearAllPrompts()
|
||||
$activeSessionId.set('sess-1')
|
||||
$toolDisclosureStates.set({})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
$approvalRequest.set(null)
|
||||
clearAllPrompts()
|
||||
$activeSessionId.set(null)
|
||||
})
|
||||
|
||||
describe('ToolGroupSlot approval surfacing', () => {
|
||||
@@ -143,7 +146,7 @@ describe('ToolGroupSlot approval surfacing', () => {
|
||||
})
|
||||
|
||||
it('force-opens the group body so the approval surfaces without expanding', async () => {
|
||||
$approvalRequest.set({ command: 'rm -rf /tmp/x', description: 'dangerous command', sessionId: 'sess-1' })
|
||||
setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'dangerous command', sessionId: 'sess-1' })
|
||||
|
||||
const { container } = render(<GroupHarness message={groupedPendingMessage()} />)
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { HermesGateway } from '@/hermes'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
import { $approvalRequest } from '@/store/prompts'
|
||||
import { $approvalRequest, clearAllPrompts, setApprovalRequest } from '@/store/prompts'
|
||||
import { $activeSessionId } from '@/store/session'
|
||||
|
||||
import { PendingToolApproval } from './tool-approval'
|
||||
import type { ToolPart } from './tool-fallback-model'
|
||||
@@ -13,7 +14,8 @@ function part(toolName: string): ToolPart {
|
||||
}
|
||||
|
||||
function setRequest(command = 'rm -rf /tmp/x') {
|
||||
$approvalRequest.set({ command, description: 'dangerous command', sessionId: 'sess-1' })
|
||||
$activeSessionId.set('sess-1')
|
||||
setApprovalRequest({ command, description: 'dangerous command', sessionId: 'sess-1' })
|
||||
}
|
||||
|
||||
function mockGateway() {
|
||||
@@ -25,7 +27,8 @@ function mockGateway() {
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
$approvalRequest.set(null)
|
||||
clearAllPrompts()
|
||||
$activeSessionId.set(null)
|
||||
$gateway.set(null)
|
||||
})
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => {
|
||||
session_id: request.sessionId ?? undefined
|
||||
})
|
||||
triggerHaptic(choice === 'deny' ? 'cancel' : 'submit')
|
||||
clearApprovalRequest()
|
||||
clearApprovalRequest(request.sessionId)
|
||||
} catch (error) {
|
||||
notifyError(error, 'Could not send approval response')
|
||||
setSubmitting(null)
|
||||
|
||||
@@ -64,7 +64,7 @@ function SudoDialog() {
|
||||
request_id: request.requestId
|
||||
})
|
||||
triggerHaptic('submit')
|
||||
clearSudoRequest(request.requestId)
|
||||
clearSudoRequest(request.sessionId, request.requestId)
|
||||
} catch (error) {
|
||||
notifyError(error, 'Could not send sudo password')
|
||||
setSubmitting(false)
|
||||
@@ -163,7 +163,7 @@ function SecretDialog() {
|
||||
value: secret
|
||||
})
|
||||
triggerHaptic('submit')
|
||||
clearSecretRequest(request.requestId)
|
||||
clearSecretRequest(request.sessionId, request.requestId)
|
||||
} catch (error) {
|
||||
notifyError(error, 'Could not send secret')
|
||||
setSubmitting(false)
|
||||
|
||||
Reference in New Issue
Block a user