diff --git a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx index 672d7130b5b..89605980d79 100644 --- a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx +++ b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx @@ -5,7 +5,10 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import type { Repo } from '../../../../shared/repo-types' -import { useSetupScriptPromptRevalidation } from './useSetupScriptPromptRevalidation' +import { + SETUP_PROMPT_REVISIT_REVALIDATION_INTERVAL_MS, + useSetupScriptPromptRevalidation +} from './useSetupScriptPromptRevalidation' import type { SetupScriptPromptState } from './setup-script-prompt-render-state' import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity' @@ -261,6 +264,35 @@ describe('useSetupScriptPromptRevalidation', () => { expect(requestRevalidation).toHaveBeenCalledTimes(1) }) + it('re-inspects each worktree once on first activation, not on every revisit', async () => { + const now = vi.spyOn(Date, 'now').mockReturnValue(1_000_000) + const requestRevalidation = vi.fn() + await render({ + activeRepo: GIT_REPO, + isDismissed: false, + sidebarOpen: true, + promptState: missingSetup('repo-1'), + requestRevalidation + }) + + // Before: every one of these 20 switches cost a hooksCheck + setupScriptImports pair. + for (let round = 0; round < 10; round += 1) { + await setActiveWorktree('worktree-2') + await setActiveWorktree('worktree-1') + } + expect(requestRevalidation).toHaveBeenCalledTimes(1) + + await setActiveWorktree('worktree-3') + expect(requestRevalidation).toHaveBeenCalledTimes(2) + + now.mockReturnValue(1_000_000 + SETUP_PROMPT_REVISIT_REVALIDATION_INTERVAL_MS) + await setActiveWorktree('worktree-1') + expect(requestRevalidation).toHaveBeenCalledTimes(3) + await setActiveWorktree('worktree-2') + expect(requestRevalidation).toHaveBeenCalledTimes(3) + now.mockRestore() + }) + it('re-inspects when the repo runtime reconnects', async () => { const requestRevalidation = vi.fn() await setRuntimeConnectionGeneration(1) diff --git a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts index 867b21052e7..17aeee81e63 100644 --- a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts +++ b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts @@ -6,6 +6,9 @@ import { getRepoHostIdentity } from '@/store/slices/repo-host-identity' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' import type { SetupScriptPromptState } from './setup-script-prompt-render-state' +/** Revisiting an already-activated worktree re-inspects at most this often. */ +export const SETUP_PROMPT_REVISIT_REVALIDATION_INTERVAL_MS = 30_000 + /** * Re-runs the setup-script prompt inspection when a shared `orca.yaml` setup hook * can have become effective outside SetupScriptPromptCard's reactive inputs, so a @@ -72,6 +75,12 @@ export function useSetupScriptPromptRevalidation(input: { // card nulls it for the whole inspection round trip). Remember them instead of // dropping them, then replay once a revalidatable result exists. const pendingRevalidationRef = useRef(false) + // Why: a worktree's first activation is when its creation-time setup hook can have landed. + // Re-inspecting on every revisit cost two host round trips per click on remote repos (#26785). + const activatedWorktreeIdsRef = useRef( + new Set(activeWorktreeId ? [activeWorktreeId] : []) + ) + const lastActivationRevalidationAtRef = useRef(Number.NEGATIVE_INFINITY) useEffect(() => { const worktreeChanged = previousWorktreeIdRef.current !== activeWorktreeId @@ -85,8 +94,21 @@ export function useSetupScriptPromptRevalidation(input: { if (hostChanged) { // A repo/host switch re-runs the card's own inspection, so no extra pass is owed. pendingRevalidationRef.current = false - } else if (worktreeChanged || reconnected) { + } else if (reconnected) { pendingRevalidationRef.current = true + } else if (worktreeChanged) { + const firstActivation = + activeWorktreeId !== null && !activatedWorktreeIdsRef.current.has(activeWorktreeId) + if (activeWorktreeId !== null) { + activatedWorktreeIdsRef.current.add(activeWorktreeId) + } + if ( + firstActivation || + Date.now() - lastActivationRevalidationAtRef.current >= + SETUP_PROMPT_REVISIT_REVALIDATION_INTERVAL_MS + ) { + pendingRevalidationRef.current = true + } } if ( @@ -100,6 +122,7 @@ export function useSetupScriptPromptRevalidation(input: { return } pendingRevalidationRef.current = false + lastActivationRevalidationAtRef.current = Date.now() requestRevalidation() }, [ activeRepo,