fix(sidebar): revalidate setup-script prompt when the hook becomes effective (#8752) (#8893)

The "Add a setup script" card cached its inspection result and only re-ran
on activeRepo/settings/sidebarOpen/dismiss/retry changes. A shared orca.yaml
setup hook that became effective on disk — edited externally, or run during
worktree creation — left the stale prompt visible until a full sidebar reopen.

Extract the revalidation into useSetupScriptPromptRevalidation, which
re-inspects on window focus (external edits / terminal hook runs) and when a
worktree of the repo activates while the prompt still shows no effective setup.

Claude-Session: https://claude.ai/code/session_01C8RPZ1mhUCMWcojgD6jLaN

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
This commit is contained in:
Kaynan Sampaio de Camargo 2026-07-24 00:25:12 -07:00 committed by GitHub
parent 4e670d3e4c
commit deb2b50e71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 236 additions and 0 deletions

View File

@ -26,6 +26,7 @@ import {
type LastVisibleSetupScriptPrompt,
useSetupScriptPromptProjectContext
} from './setup-script-prompt-render-state'
import { useSetupScriptPromptRevalidation } from './useSetupScriptPromptRevalidation'
import { translate } from '@/i18n/i18n'
type PromptState = SetupScriptPromptInspection
@ -113,6 +114,14 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
setInspectionRetryKey((value) => value + 1)
}, [])
useSetupScriptPromptRevalidation({
activeRepo,
isDismissed,
sidebarOpen,
promptState,
requestRevalidation: handleRetryInspection
})
useEffect(() => {
if (
!sidebarOpen ||

View File

@ -0,0 +1,166 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt'
import type { Repo } from '../../../../shared/types'
import { useSetupScriptPromptRevalidation } from './useSetupScriptPromptRevalidation'
const GIT_REPO = { id: 'repo-1', kind: 'git' } as unknown as Repo
function missingSetup(repoId: string): SetupScriptPromptInspection {
return { status: 'ok', repoId, hasEffectiveSetup: false, hasSharedHooks: true, candidate: null }
}
function effectiveSetup(repoId: string): SetupScriptPromptInspection {
return { status: 'ok', repoId, hasEffectiveSetup: true, hasSharedHooks: true, candidate: null }
}
type HarnessProps = {
activeRepo: Repo | null
isDismissed: boolean
sidebarOpen: boolean
promptState: SetupScriptPromptInspection | null
requestRevalidation: () => void
}
function Harness(props: HarnessProps): null {
useSetupScriptPromptRevalidation(props)
return null
}
const roots: Root[] = []
async function render(props: HarnessProps): Promise<(next: HarnessProps) => Promise<void>> {
const container = document.createElement('div')
const root = createRoot(container)
roots.push(root)
await act(async () => {
root.render(<Harness {...props} />)
})
return async (next: HarnessProps) => {
await act(async () => {
root.render(<Harness {...next} />)
})
}
}
async function dispatchWindowFocus(): Promise<void> {
await act(async () => {
window.dispatchEvent(new Event('focus'))
})
}
async function setActiveWorktree(worktreeId: string | null): Promise<void> {
await act(async () => {
useAppStore.setState({ activeWorktreeId: worktreeId })
})
}
describe('useSetupScriptPromptRevalidation', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
useAppStore.setState({ activeWorktreeId: 'worktree-1' })
})
afterEach(() => {
roots.splice(0).forEach((root) => act(() => root.unmount()))
document.body.replaceChildren()
useAppStore.setState({ activeWorktreeId: null })
vi.clearAllMocks()
})
it('re-inspects on window focus while the prompt shows no effective setup', async () => {
const requestRevalidation = vi.fn()
await render({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: true,
promptState: missingSetup('repo-1'),
requestRevalidation
})
await dispatchWindowFocus()
expect(requestRevalidation).toHaveBeenCalledTimes(1)
})
it('does not re-inspect on window focus once setup is effective', async () => {
const requestRevalidation = vi.fn()
await render({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: true,
promptState: effectiveSetup('repo-1'),
requestRevalidation
})
await dispatchWindowFocus()
expect(requestRevalidation).not.toHaveBeenCalled()
})
it('does not listen for focus while the sidebar is closed', async () => {
const requestRevalidation = vi.fn()
await render({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: false,
promptState: missingSetup('repo-1'),
requestRevalidation
})
await dispatchWindowFocus()
expect(requestRevalidation).not.toHaveBeenCalled()
})
it('re-inspects when a worktree activates while the prompt shows no effective setup', async () => {
const requestRevalidation = vi.fn()
// Mirror the card's real lifecycle: promptState is null on mount, so the
// activation effect does not fire until a negative result has been cached.
const rerender = await render({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: true,
promptState: null,
requestRevalidation
})
await rerender({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: true,
promptState: missingSetup('repo-1'),
requestRevalidation
})
expect(requestRevalidation).not.toHaveBeenCalled()
await setActiveWorktree('worktree-2')
expect(requestRevalidation).toHaveBeenCalledTimes(1)
})
it('does not re-inspect on worktree activation once setup is effective', async () => {
const requestRevalidation = vi.fn()
const rerender = await render({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: true,
promptState: null,
requestRevalidation
})
await rerender({
activeRepo: GIT_REPO,
isDismissed: false,
sidebarOpen: true,
promptState: effectiveSetup('repo-1'),
requestRevalidation
})
await setActiveWorktree('worktree-2')
expect(requestRevalidation).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,61 @@
import { useEffect, useRef } from 'react'
import { useAppStore } from '@/store'
import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt'
import { isGitRepoKind } from '../../../../shared/repo-kind'
import type { Repo } from '../../../../shared/types'
/**
* 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
* stale "Add a setup script" prompt clears without a full sidebar reopen.
*/
export function useSetupScriptPromptRevalidation(input: {
activeRepo: Repo | null
isDismissed: boolean
sidebarOpen: boolean
promptState: SetupScriptPromptInspection | null
requestRevalidation: () => void
}): void {
const { activeRepo, isDismissed, sidebarOpen, promptState, requestRevalidation } = input
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
// Why: only revalidate while the prompt still shows no effective setup — there is
// nothing to clear (and no RPC worth spending, notably over SSH) once it is
// configured.
const showsMissingSetup =
promptState?.status === 'ok' &&
promptState.repoId === activeRepo?.id &&
!promptState.hasEffectiveSetup
// Why: orca.yaml is edited on disk or the hook runs in a terminal outside React
// state. Re-inspect on window focus so returning to Orca detects it (mirrors
// useInstalledAgentSkills' focus revalidation).
useEffect(() => {
if (
!sidebarOpen ||
!activeRepo ||
!isGitRepoKind(activeRepo) ||
isDismissed ||
!showsMissingSetup
) {
return
}
window.addEventListener('focus', requestRevalidation)
return () => {
window.removeEventListener('focus', requestRevalidation)
}
}, [activeRepo, isDismissed, requestRevalidation, showsMissingSetup, sidebarOpen])
// Why: the setup hook runs during worktree creation, so activating a worktree in
// this repo can make the setup effective after a negative result was cached. Fire
// only on an actual activation change, not on mount/remount with a seeded id —
// the initial inspection already covers the mounted worktree.
const previousWorktreeIdRef = useRef(activeWorktreeId)
useEffect(() => {
const changed = previousWorktreeIdRef.current !== activeWorktreeId
previousWorktreeIdRef.current = activeWorktreeId
if (changed && showsMissingSetup) {
requestRevalidation()
}
}, [activeWorktreeId, requestRevalidation, showsMissingSetup])
}