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