diff --git a/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx b/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx index 159115b79e0..a5ab9fa694b 100644 --- a/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx +++ b/src/renderer/src/components/sidebar/SetupScriptPromptCard.tsx @@ -21,12 +21,11 @@ import { openSetupScriptSettings } from './open-setup-script-settings' import { trackSetupScriptPromptExposure } from './setup-script-prompt-exposure-telemetry' import { findSetupScriptPromptRepo, - getRenderedSetupScriptPromptState, markSetupScriptPromptSaved, - type LastVisibleSetupScriptPrompt, type SetupScriptPromptState } from './setup-script-prompt-render-state' import { useSetupScriptPromptRevalidation } from './useSetupScriptPromptRevalidation' +import { useRenderedSetupScriptPromptState } from './useRenderedSetupScriptPromptState' import { translate } from '@/i18n/i18n' import { getRepoHostIdentity } from '@/store/slices/repo-host-identity' import { getRepoExecutionHostId } from '../../../../shared/execution-host' @@ -60,7 +59,6 @@ function SetupScriptPromptCard(): React.JSX.Element | null { const isDismissed = activeRepoHostIdentity ? isSetupScriptPromptDismissed(activeRepoHostIdentity, dismissedRepoIds) : false - const lastVisiblePromptRef = useRef(null) useEffect(() => { if (!sidebarOpen || !activeRepo || !isGitRepoKind(activeRepo) || isDismissed) { @@ -348,31 +346,25 @@ function SetupScriptPromptCard(): React.JSX.Element | null { }) }, [activeRepo, detectedSetupDraft, promptState, saveSetupCandidate]) - if ( + const promptTargetHidden = !sidebarOpen || !activeRepo || !activeRepoHostIdentity || !isGitRepoKind(activeRepo) || isDismissed - ) { - lastVisiblePromptRef.current = null - return null - } - - const renderedPromptState = getRenderedSetupScriptPromptState({ + const renderedPromptState = useRenderedSetupScriptPromptState({ promptState, - activeRepoId: activeRepo.id, + activeRepoId: activeRepo?.id ?? null, activeRepoHostIdentity, - lastVisiblePrompt: lastVisiblePromptRef.current + promptTargetHidden }) if ( + promptTargetHidden || + !activeRepo || !renderedPromptState || (renderedPromptState.status === 'ok' && renderedPromptState.hasEffectiveSetup) ) { - if (renderedPromptState?.status === 'ok' && renderedPromptState.hasEffectiveSetup) { - lastVisiblePromptRef.current = null - } return null } @@ -380,18 +372,9 @@ function SetupScriptPromptCard(): React.JSX.Element | null { // retry-able card entirely — the global scope-mismatch banner explains it and // a retry would just re-fire repo.hooksCheck on every repo focus. if (renderedPromptState.status === 'forbidden') { - lastVisiblePromptRef.current = null return null } - if ( - renderedPromptState.status === 'ok' && - !renderedPromptState.hasEffectiveSetup && - renderedPromptState.repoHostIdentity === activeRepoHostIdentity - ) { - lastVisiblePromptRef.current = { state: renderedPromptState } - } - const isInspectionError = renderedPromptState.status === 'error' const candidate = renderedPromptState.status === 'ok' ? renderedPromptState.candidate : null const isPackageManagerSuggestion = candidate?.provider === 'package-manager' diff --git a/src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts b/src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts new file mode 100644 index 00000000000..16150b126dc --- /dev/null +++ b/src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts @@ -0,0 +1,48 @@ +// @vitest-environment happy-dom + +import { renderHook } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity' +import type { SetupScriptPromptState } from './setup-script-prompt-render-state' +import { useRenderedSetupScriptPromptState } from './useRenderedSetupScriptPromptState' + +function prompt(repoId: string, hostId: string): SetupScriptPromptState { + return { + status: 'ok', + repoId, + repoHostIdentity: getRepoHostIdentityForParts(repoId, hostId), + hasEffectiveSetup: false, + hasSharedHooks: false, + candidate: null + } +} + +describe('useRenderedSetupScriptPromptState', () => { + it('preserves a committed same-host prompt without leaking it to another host', () => { + const local = prompt('repo-1', 'local') + const { result, rerender } = renderHook( + ({ hostIdentity, promptState }) => + useRenderedSetupScriptPromptState({ + promptState, + activeRepoId: 'repo-1', + activeRepoHostIdentity: hostIdentity, + promptTargetHidden: false + }), + { + initialProps: { + hostIdentity: local.repoHostIdentity, + promptState: local as SetupScriptPromptState | null + } + } + ) + + rerender({ hostIdentity: local.repoHostIdentity, promptState: null }) + expect(result.current).toBe(local) + + rerender({ + hostIdentity: getRepoHostIdentityForParts('repo-1', 'ssh:windows'), + promptState: null + }) + expect(result.current).toBeNull() + }) +}) diff --git a/src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.ts b/src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.ts new file mode 100644 index 00000000000..c245b1c3489 --- /dev/null +++ b/src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.ts @@ -0,0 +1,45 @@ +import { useEffect, useRef } from 'react' +import { + getRenderedSetupScriptPromptState, + type LastVisibleSetupScriptPrompt, + type SetupScriptPromptState +} from './setup-script-prompt-render-state' + +export function useRenderedSetupScriptPromptState(input: { + promptState: SetupScriptPromptState | null + activeRepoId: string | null + activeRepoHostIdentity: string | null + promptTargetHidden: boolean +}): SetupScriptPromptState | null { + const { activeRepoHostIdentity, activeRepoId, promptState, promptTargetHidden } = input + const lastVisiblePromptRef = useRef(null) + const renderedPromptState = + !promptTargetHidden && activeRepoId && activeRepoHostIdentity + ? getRenderedSetupScriptPromptState({ + promptState, + activeRepoId, + activeRepoHostIdentity, + lastVisiblePrompt: lastVisiblePromptRef.current + }) + : null + + useEffect(() => { + if ( + renderedPromptState?.status === 'ok' && + !renderedPromptState.hasEffectiveSetup && + renderedPromptState.repoHostIdentity === activeRepoHostIdentity + ) { + lastVisiblePromptRef.current = { state: renderedPromptState } + return + } + if ( + promptTargetHidden || + renderedPromptState?.status === 'forbidden' || + (renderedPromptState?.status === 'ok' && renderedPromptState.hasEffectiveSetup) + ) { + lastVisiblePromptRef.current = null + } + }, [activeRepoHostIdentity, promptTargetHidden, renderedPromptState]) + + return renderedPromptState +} diff --git a/src/renderer/src/store/slices/repos.test.ts b/src/renderer/src/store/slices/repos.test.ts index f8a5bfc2322..764a5650657 100644 --- a/src/renderer/src/store/slices/repos.test.ts +++ b/src/renderer/src/store/slices/repos.test.ts @@ -141,6 +141,7 @@ describe('repo slice runtime routing', () => { expect(store.getState().repos[0]?.displayName).toBe('SSH Renamed') expect(reposUpdate).toHaveBeenCalledWith({ + hostId: 'ssh:ssh-1', repoId: sshRepo.id, updates: { displayName: 'SSH Renamed' } })