From bfc8e3b00db4ec87ef05e8aefff338bb6a42ccb4 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 25 Jul 2026 20:09:58 -0500 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20give=20=E2=8C=98T=20session=20t?= =?UTF-8?q?abs=20a=20live=20gateway=20so=20slash=20completions=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useGatewayRequest` only exposed the gateway through a ref that its own subscription effect fills in, so a component reading it during render saw null on mount. Session tiles — what ⌘T and the tab-strip "+" open — did exactly that and passed `gateway={null}` down to ChatBar. The slash adapter is disabled without a gateway, so a new tab had no completions at all while ⌘N, which resolves its gateway through a state-driven memo, worked fine. Nothing re-rendered the tile afterwards unless the connection state happened to flip, so it never recovered. Return the `$gateway` atom as a reactive value alongside the ref and use it wherever the gateway is needed as a render-time value. The ref stays for `requestGateway`, which wants a stable identity. The model picker, model visibility, and settings overlays read the same ref during render, so they're moved over too. --- apps/desktop/src/app/chat/session-tile.tsx | 8 ++-- apps/desktop/src/app/contrib/wiring.tsx | 8 ++-- .../gateway/hooks/use-gateway-request.test.ts | 39 +++++++++++++++++++ .../app/gateway/hooks/use-gateway-request.ts | 10 ++++- 4 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 apps/desktop/src/app/gateway/hooks/use-gateway-request.test.ts diff --git a/apps/desktop/src/app/chat/session-tile.tsx b/apps/desktop/src/app/chat/session-tile.tsx index 410cf87e2e42..6ca29e70ade9 100644 --- a/apps/desktop/src/app/chat/session-tile.tsx +++ b/apps/desktop/src/app/chat/session-tile.tsx @@ -113,7 +113,7 @@ function TileChat({ storedSessionId: string view: SessionView }) { - const { gatewayRef, requestGateway } = useGatewayRequest() + const { gateway, requestGateway } = useGatewayRequest() const queryClient = useQueryClient() const { selectModel } = useModelControls({ queryClient, requestGateway }) const activeGatewayProfile = useStore($activeGatewayProfile) @@ -151,20 +151,20 @@ function TileChat({ () => gatewayOpen ? ( ) : null, - [activeGatewayProfile, gatewayOpen, gatewayRef, requestGateway, selectModel] + [activeGatewayProfile, gateway, gatewayOpen, requestGateway, selectModel] ) return ( composer.addContextRefAttachment(`@url:${formatRefValue(url)}`, url)} diff --git a/apps/desktop/src/app/contrib/wiring.tsx b/apps/desktop/src/app/contrib/wiring.tsx index e8716bde9262..963fc67c3d0f 100644 --- a/apps/desktop/src/app/contrib/wiring.tsx +++ b/apps/desktop/src/app/contrib/wiring.tsx @@ -222,7 +222,7 @@ export function ContribWiring({ children }: { children: ReactNode }) { setMessages }) - const { connectionRef, gatewayRef, requestGateway } = useGatewayRequest() + const { connectionRef, gateway, gatewayRef, requestGateway } = useGatewayRequest() const { loadMoreMessagingForPlatform, @@ -943,13 +943,13 @@ export function ContribWiring({ children }: { children: ReactNode }) { /> )} @@ -965,7 +965,7 @@ export function ContribWiring({ children }: { children: ReactNode }) { {settingsOpen && ( { void refreshHermesConfig() diff --git a/apps/desktop/src/app/gateway/hooks/use-gateway-request.test.ts b/apps/desktop/src/app/gateway/hooks/use-gateway-request.test.ts new file mode 100644 index 000000000000..72ad6a2a2687 --- /dev/null +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-request.test.ts @@ -0,0 +1,39 @@ +import { act, renderHook } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' + +import type { HermesGateway } from '@/hermes' +import { $gateway } from '@/store/gateway' + +import { useGatewayRequest } from './use-gateway-request' + +const fakeGateway = { connectionState: 'open' } as unknown as HermesGateway + +afterEach(() => { + $gateway.set(null) +}) + +describe('useGatewayRequest', () => { + // The composer's `/` completions only exist when ChatBar receives a non-null + // gateway PROP. `gatewayRef` is populated by a subscription effect, so it is + // still null on the first render — a surface that read the ref while + // rendering (session tiles / ⌘T tabs) shipped `gateway={null}` and silently + // lost slash completions. The returned `gateway` value must be live + // immediately so that never happens again. + it('exposes the live gateway on the first render, before effects run', () => { + $gateway.set(fakeGateway) + + const { result } = renderHook(() => useGatewayRequest()) + + expect(result.current.gateway).toBe(fakeGateway) + }) + + it('tracks the gateway when the active socket changes', () => { + const { result } = renderHook(() => useGatewayRequest()) + + expect(result.current.gateway).toBeNull() + + act(() => $gateway.set(fakeGateway)) + + expect(result.current.gateway).toBe(fakeGateway) + }) +}) diff --git a/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts b/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts index 76c98c17a230..04f53f785b48 100644 --- a/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts @@ -9,6 +9,14 @@ import { $gatewayState, setConnection } from '@/store/session' export function useGatewayRequest() { const gatewayState = useStore($gatewayState) + // Reactive companion to `gatewayRef`. The ref exists so `requestGateway` + // keeps a stable identity and always reaches the live socket, but it is only + // populated by the subscription effect below — i.e. AFTER the first render. + // A component that reads `gatewayRef.current` while rendering therefore sees + // null on mount, and if the connection state doesn't happen to flip + // afterwards it never re-renders to pick the instance up. Anything that needs + // the gateway as a render-time VALUE (props, memo deps) must use this. + const gateway = useStore($gateway) as HermesGateway | null const gatewayRef = useRef(null) const connectionRef = useRef['getConnection']>> | null>( @@ -136,5 +144,5 @@ export function useGatewayRequest() { [ensureGatewayOpen] ) - return { connectionRef, gatewayRef, requestGateway } + return { connectionRef, gateway, gatewayRef, requestGateway } }