diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts index c493604febf..19f0d47ce82 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts @@ -755,4 +755,49 @@ describe('useAiVaultSessionRefresh in-app agent session behavior', () => { await setAgentStatuses({ 'pane-2': makeAgentEntry('sess-2', 'working') }) expect(listSessionsMock).toHaveBeenCalledTimes(3) }) + + // Reported against an adhoc build: a workspace whose editor was loading files fine still showed + // "SSH relay is not ready" with "0 shown ยท 0 recent" in this panel. That error is what the relay + // throws before it is ready, which is ordinary at startup and for the window a reconnect leaves it + // not-ready โ€” but nothing here retried on the relay simply becoming ready. The remaining triggers + // are mount, window refocus and a new agent session id, so the error stuck while the rest of the + // workspace worked. The file explorer already recovers off this same signal. + it('retries after a not-ready failure once the SSH connection lands', async () => { + listSessionsMock.mockRejectedValueOnce(new Error('SSH relay is not ready')) + await renderHook(['/home/neil/projects/orca']) + await flushMicrotasks() + + expect(latest?.error).toBe('SSH relay is not ready') + const callsWhileBroken = listSessionsMock.mock.calls.length + + listSessionsMock.mockResolvedValue(EMPTY_RESULT) + await act(async () => { + useAppStore.setState({ sshConnectedGeneration: 1 }) + }) + await flushMicrotasks() + + expect( + listSessionsMock.mock.calls.length, + 'the panel never retried after SSH became ready' + ).toBeGreaterThan(callsWhileBroken) + expect(latest?.error).toBeNull() + }) + + it('does not rescan on a connection bump when the last listing succeeded', async () => { + // Gated on a prior error so a local workspace, or one that already listed fine, does not rescan + // every time some unrelated host connects. + listSessionsMock.mockResolvedValue(EMPTY_RESULT) + await renderHook(['/home/neil/projects/orca']) + await flushMicrotasks() + + expect(latest?.error).toBeNull() + const callsWhileHealthy = listSessionsMock.mock.calls.length + + await act(async () => { + useAppStore.setState({ sshConnectedGeneration: 1 }) + }) + await flushMicrotasks() + + expect(listSessionsMock.mock.calls.length).toBe(callsWhileHealthy) + }) }) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts index d6bf0bb120c..0c64607853e 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts @@ -267,6 +267,27 @@ export function useAiVaultSessionRefresh( void refresh({ force: false, reuseLoadedDepth: true }) }, [executionHostScope, refresh, scanScopeKey]) + // Why: this panel can query the relay before it is ready โ€” at startup, and again for the window + // in which a reconnect leaves the session not-ready โ€” and the query throws 'SSH relay is not + // ready'. Nothing else here retries: the remaining triggers are mount, window refocus and a new + // agent session id, so a user whose workspace is otherwise working sits on that error + // indefinitely. The file explorer already recovers this way for the same reason + // (use-file-explorer-tree-load-effects.ts); this panel simply never did. + // + // Gated on a prior error so a local workspace, or one that already listed fine, does not rescan + // every time some other host connects. + const sshConnectedGeneration = useAppStore((s) => s.sshConnectedGeneration) + const sshGenerationRef = useRef(sshConnectedGeneration) + useEffect(() => { + if (sshConnectedGeneration <= sshGenerationRef.current) { + return + } + sshGenerationRef.current = sshConnectedGeneration + if (error !== null) { + void refresh({ background: true, force: false }) + } + }, [sshConnectedGeneration, error, refresh]) + // Refocus checks the shared host cache without forcing another transcript scan. useEffect(() => { const onRefocus = (): void => {