From 6abd1ce53b69a96e0d5a706ef6fe2ea068bee7fe Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 19 Sep 2026 05:12:42 -0700 Subject: [PATCH] fix(agents): refresh remote detection on new launch surfaces (#21659) --- .../src/hooks/useDetectedAgents.test.tsx | 78 +++++++++++++++++-- src/renderer/src/hooks/useDetectedAgents.ts | 18 +++-- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/hooks/useDetectedAgents.test.tsx b/src/renderer/src/hooks/useDetectedAgents.test.tsx index cc1c49b5b90..fbc1ffe6a82 100644 --- a/src/renderer/src/hooks/useDetectedAgents.test.tsx +++ b/src/renderer/src/hooks/useDetectedAgents.test.tsx @@ -237,6 +237,22 @@ describe('useDetectedAgents (ssh call site)', () => { expect(detectRemoteAgents).toHaveBeenCalledTimes(2) expect(useAppStore.getState().remoteDetectedAgentIds['ssh-1']).toEqual(['kilo']) }) + + it('refreshes a non-empty SSH cache when a new launch surface opens', async () => { + useAppStore.setState({ remoteDetectedAgentIds: { 'ssh-1': ['claude'] } }) + detectRemoteAgents.mockResolvedValueOnce(['claude', 'devin']) + + const root = await renderProbe({ kind: 'ssh', connectionId: 'ssh-1' }) + + expect(detectRemoteAgents).toHaveBeenCalledTimes(1) + expect(useAppStore.getState().remoteDetectedAgentIds['ssh-1']).toEqual(['claude', 'devin']) + + await act(async () => { + root.render(createElement(HookProbe, { target: { kind: 'ssh', connectionId: 'ssh-1' } })) + }) + await flushEffects() + expect(detectRemoteAgents).toHaveBeenCalledTimes(1) + }) }) describe('useDetectedAgents (unresolved target)', () => { @@ -283,6 +299,56 @@ describe('useDetectedAgents (runtime call site)', () => { ).toHaveLength(2) }) + it('refreshes a non-empty runtime cache when a new launch surface opens', async () => { + useAppStore.setState({ runtimeDetectedAgentIds: { 'env-1': ['claude'] } }) + runtimeEnvironmentCall.mockImplementation(({ method }: { method: string }) => { + const result = + method === 'status.get' + ? { + runtimeId: 'remote-runtime', + rendererGraphEpoch: 1, + graphStatus: 'ready', + authoritativeWindowId: null, + liveTabCount: 0, + liveLeafCount: 0, + runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION, + minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION + } + : { + agents: ['claude', 'devin'], + addedPathSegments: [], + shellHydrationOk: true, + pathSource: 'shell_hydrate', + pathFailureReason: 'none' + } + return Promise.resolve({ + id: method, + ok: true, + result, + _meta: { runtimeId: 'remote-runtime' } + }) + }) + + const root = await renderProbe({ kind: 'runtime', environmentId: 'env-1' }) + + expect( + runtimeEnvironmentCall.mock.calls.filter( + ([{ method }]) => method === 'preflight.refreshAgents' + ) + ).toHaveLength(1) + expect(useAppStore.getState().runtimeDetectedAgentIds['env-1']).toEqual(['claude', 'devin']) + + await act(async () => { + root.render(createElement(HookProbe, { target: { kind: 'runtime', environmentId: 'env-1' } })) + }) + await flushEffects() + expect( + runtimeEnvironmentCall.mock.calls.filter( + ([{ method }]) => method === 'preflight.refreshAgents' + ) + ).toHaveLength(1) + }) + it('does not re-probe after an explicit refresh finds no agents', async () => { useAppStore.setState({ runtimeDetectedAgentIds: { 'env-1': ['claude'] }, @@ -324,16 +390,18 @@ describe('useDetectedAgents (runtime call site)', () => { }) }) - await renderProbe({ kind: 'runtime', environmentId: 'env-1' }) - await renderProbe({ kind: 'runtime', environmentId: 'env-1' }) - await act(async () => { - await latestHookResult?.refresh() - }) + const root = await renderProbe({ kind: 'runtime', environmentId: 'env-1' }) await flushEffects() expect(refreshCalls).toBe(1) expect(detectCalls).toBe(0) expect(useAppStore.getState().runtimeDetectedAgentIds['env-1']).toEqual([]) + + await act(async () => { + root.render(createElement(HookProbe, { target: { kind: 'runtime', environmentId: 'env-1' } })) + }) + await flushEffects() + expect(refreshCalls).toBe(1) }) it('retries a cached empty runtime result when the launch surface is reopened', async () => { diff --git a/src/renderer/src/hooks/useDetectedAgents.ts b/src/renderer/src/hooks/useDetectedAgents.ts index 284083b287d..8ed723a5ad5 100644 --- a/src/renderer/src/hooks/useDetectedAgents.ts +++ b/src/renderer/src/hooks/useDetectedAgents.ts @@ -155,17 +155,23 @@ export function useDetectedAgents( if (targetKind === 'ssh' && targetId) { if (detectedIds === null) { void state.ensureRemoteDetectedAgents(targetId) - } else if (detectedIds.length === 0 && isNewRemoteTarget) { - // Why: a newly opened remote launch surface should get one fresh probe - // after a prior empty result, but must not spin while the host has no agents. + } else if (isNewRemoteTarget && detectedIds.length > 0) { + // Why: a host can install an agent after its cached list was populated; + // refresh once when a new launch surface first observes that host. + void state.refreshRemoteDetectedAgents(targetId) + } else if (isNewRemoteTarget) { + // Empty results are intentionally retryable through the normal probe. void state.ensureRemoteDetectedAgents(targetId) } } else if (targetKind === 'runtime' && targetId) { if (detectedIds === null) { void state.ensureRuntimeDetectedAgents(targetId) - } else if (detectedIds.length === 0 && isNewRemoteTarget) { - // Why: remote `orca serve` users can install/fix PATH without reconnecting; - // retry once per mounted surface so the menu can pick that up. + } else if (isNewRemoteTarget && detectedIds.length > 0) { + // Why: a host can install an agent after its cached list was populated; + // refresh once when a new launch surface first observes that host. + void state.refreshRuntimeDetectedAgents(targetId) + } else if (isNewRemoteTarget) { + // Empty results are intentionally retryable through the normal probe. void state.ensureRuntimeDetectedAgents(targetId) } } else {