fix(desktop): keep live gateway across profile switches
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { act, cleanup, render, screen } from '@testing-library/react'
|
||||
import { atom } from 'nanostores'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { HermesGateway } from '@/hermes'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
|
||||
import { ChatRoutesSurface } from './surfaces'
|
||||
import type { WiringActions } from './types'
|
||||
|
||||
vi.mock('@/contrib/react/use-contributions', () => ({ useContributions: vi.fn() }))
|
||||
vi.mock('@/store/gateway', () => ({ $gateway: atom<unknown>(null) }))
|
||||
vi.mock('@/store/profile', () => ({ $activeGatewayProfile: atom('default') }))
|
||||
vi.mock('@/store/session', () => ({
|
||||
$freshDraftReady: atom(false),
|
||||
$gatewayState: atom('open')
|
||||
}))
|
||||
vi.mock('../chat', () => ({
|
||||
ChatView: ({ gateway }: { gateway: { id?: string } | null }) => <div data-testid="gateway">{gateway?.id}</div>
|
||||
}))
|
||||
vi.mock('../chat/sidebar', () => ({ ChatSidebar: () => null }))
|
||||
vi.mock('../right-sidebar/terminal/chrome', () => ({ TerminalPaneChrome: () => null }))
|
||||
vi.mock('../shell/hooks/use-status-snapshot', () => ({ useStatusSnapshot: () => ({}) }))
|
||||
vi.mock('../shell/hooks/use-statusbar-items', () => ({ useStatusbarItems: () => ({ leftStatusbarItems: [], statusbarItems: [] }) }))
|
||||
vi.mock('../shell/statusbar-controls', () => ({ StatusbarControls: () => null }))
|
||||
vi.mock('../routes', () => ({
|
||||
contributedRoutes: () => [],
|
||||
NEW_CHAT_ROUTE: '/new',
|
||||
ROUTES_AREA: 'routes',
|
||||
sessionRoute: (id: string) => `/${id}`
|
||||
}))
|
||||
vi.mock('./latest-actions', () => ({ latestChatActions: () => ({}), latestSidebarActions: () => ({}) }))
|
||||
vi.mock('./panes', () => ({ setStatusbarItemGroup: vi.fn(), useStatusbarContributions: () => [] }))
|
||||
vi.mock('../shell/model-menu-panel', () => ({ ModelMenuPanel: () => null }))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
$gateway.set(null)
|
||||
$activeGatewayProfile.set('default')
|
||||
})
|
||||
|
||||
describe('ChatRoutesSurface', () => {
|
||||
it('passes the live gateway after an open-to-open profile switch', () => {
|
||||
const gatewayA = { id: 'a' } as unknown as HermesGateway
|
||||
const gatewayB = { id: 'b' } as unknown as HermesGateway
|
||||
|
||||
$gateway.set(gatewayA)
|
||||
const actions = { getGateway: () => $gateway.get() } as unknown as WiringActions
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<ChatRoutesSurface actions={actions} />
|
||||
</MemoryRouter>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('gateway').textContent).toBe('a')
|
||||
|
||||
act(() => {
|
||||
$gateway.set(gatewayB)
|
||||
$activeGatewayProfile.set('other')
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('gateway').textContent).toBe('b')
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import { Navigate, Route, Routes, useParams } from 'react-router'
|
||||
|
||||
import { ContribBoundary, ContribRender } from '@/contrib/react/boundary'
|
||||
import { useContributions } from '@/contrib/react/use-contributions'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import { $freshDraftReady, $gatewayState } from '@/store/session'
|
||||
|
||||
@@ -102,10 +103,9 @@ export const StatusbarSurface = memo(function StatusbarSurface({
|
||||
})
|
||||
|
||||
/** The workspace pane: the real route table (chat + full-page views + plugin
|
||||
* routes). Subscribes to `$gatewayState` and ROUTES_AREA itself; the gateway
|
||||
* instance + voice cap arrive as props so a reconnect/config load re-renders
|
||||
* only this surface. ChatView subscribes to its own session atoms, so
|
||||
* streaming never round-trips through the controller. */
|
||||
* routes). Subscribes to the gateway instance/state and ROUTES_AREA itself;
|
||||
* the voice cap arrives as a prop. ChatView subscribes to its own session
|
||||
* atoms, so streaming never round-trips through the controller. */
|
||||
export const ChatRoutesSurface = memo(function ChatRoutesSurface({
|
||||
actions,
|
||||
maxVoiceRecordingSeconds
|
||||
@@ -114,19 +114,11 @@ export const ChatRoutesSurface = memo(function ChatRoutesSurface({
|
||||
maxVoiceRecordingSeconds?: number
|
||||
}) {
|
||||
const activeGatewayProfile = useStore($activeGatewayProfile)
|
||||
const gateway = useStore($gateway)
|
||||
const gatewayState = useStore($gatewayState)
|
||||
useContributions(ROUTES_AREA)
|
||||
const routeContributions = contributedRoutes()
|
||||
|
||||
// Recapture the live gateway instance whenever the connection state flips.
|
||||
// getGateway reads a controller ref, so gatewayState is the intentional
|
||||
// re-eval trigger (not a value the computation itself reads).
|
||||
const gateway = useMemo(
|
||||
() => actions.getGateway(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[actions, gatewayState]
|
||||
)
|
||||
|
||||
const modelMenuContent = useMemo(
|
||||
() =>
|
||||
gatewayState === 'open' ? (
|
||||
|
||||
Reference in New Issue
Block a user