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:
parent
4e670d3e4c
commit
deb2b50e71
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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])
|
||||
}
|
||||
Loading…
Reference in New Issue