diff --git a/src/renderer/src/components/settings/TasksPane.tsx b/src/renderer/src/components/settings/TasksPane.tsx index a9bc0d167b9..325628adbb2 100644 --- a/src/renderer/src/components/settings/TasksPane.tsx +++ b/src/renderer/src/components/settings/TasksPane.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useRef } from 'react' import { Github, Gitlab } from 'lucide-react' import type { GlobalSettings, TaskProvider } from '../../../../shared/types' import { @@ -109,10 +109,12 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J readinessByProvider, previousAutoExpanded: previousAutoExpandedRef.current }) - // Commit-only write: a discarded render must not seed the sticky decision. - useEffect(() => { + // Claim during render, not in an effect: a layout effect elsewhere can force a + // sync re-render before passive effects flush, and that render would see an + // unclaimed slot and collapse the open card. + if (autoExpandedProvider !== null && previousAutoExpandedRef.current === null) { previousAutoExpandedRef.current = autoExpandedProvider - }, [autoExpandedProvider]) + } const toggleProvider = (provider: TaskProvider): void => { const isVisible = visibleProviders.includes(provider) diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx b/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx index 15a9f9bf181..f6b446964ac 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx +++ b/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx @@ -351,6 +351,44 @@ describe('useInstalledAgentSkill', () => { expect(latestState?.installed).toBe(true) }) + it('clears loading when a silent refresh supersedes an in-flight focus rescan', async () => { + const firstScan = deferred() + const focusScan = deferred() + const discover = vi + .fn<(target?: SkillDiscoveryTarget) => Promise>() + .mockReturnValueOnce(firstScan.promise) + .mockReturnValueOnce(focusScan.promise) + Object.defineProperty(window, 'api', { + configurable: true, + value: { skills: { discover } } + }) + + await renderProbe() + firstScan.resolve(discoveryResult([skill({ name: 'orca-linear' })])) + await act(async () => { + await firstScan.promise + }) + + await act(async () => { + window.dispatchEvent(new Event('focus')) + }) + expect(latestState?.loading).toBe(true) + + // The silent refresh takes over the generation, so it owns the spinner too. + await act(async () => { + notifyInstalledAgentSkillsRefreshed() + await Promise.resolve() + }) + await flushMicrotasks() + expect(latestState?.loading).toBe(false) + + focusScan.resolve(discoveryResult([skill({ name: 'orca-linear' })])) + await act(async () => { + await focusScan.promise + }) + expect(latestState?.loading).toBe(false) + }) + it('reports unsettled again when the discovery target changes', async () => { const hostScan = deferred() const wslScan = deferred() diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.ts b/src/renderer/src/hooks/useInstalledAgentSkills.ts index cbd69ded4e2..482ea67386d 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.ts +++ b/src/renderer/src/hooks/useInstalledAgentSkills.ts @@ -185,11 +185,9 @@ export function useInstalledAgentSkillNames( } if (!enabled) { - if (showLoading) { - writeIfCurrent(() => { - setLoading(false) - }) - } + writeIfCurrent(() => { + setLoading(false) + }) return false } if (showLoading) { @@ -221,11 +219,12 @@ export function useInstalledAgentSkillNames( ) }) } finally { - if (showLoading) { - writeIfCurrent(() => { - setLoading(false) - }) - } + // Why: a silent refresh can supersede an in-flight loading one, whose own + // clear is then dropped by the generation guard. Only the winning + // generation clears, so it must clear regardless of its own showLoading. + writeIfCurrent(() => { + setLoading(false) + }) } return installedAfterRefresh }, diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 7de06dff492..7a07bba6ae1 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -7081,7 +7081,7 @@ "incompleteBannerTitle": "표시 중인 일부 제공업체에 설정이 필요합니다", "incompleteBannerBody": "사용하지 않는 제공업체를 숨기거나 카드를 펼쳐 단계를 완료하세요.", "providersDescription": "각 카드에서 연결, 작업 표시 여부, Linear의 경우 스킬 설정까지 진행할 수 있습니다.", - "integrationsHint": "모든 제공업체의 자격 증명은 다음에서도 관리할 수 있습니다.", + "integrationsHint": "모든 제공업체의 자격 증명은 다음에서도 관리할 수 있습니다:", "integrationsLink": "연동", "skillHint": ". Linear를 연결한 후 사용 예시는 설정 → Linear에서 확인할 수 있습니다.", "incompleteBannerBodyWithLinear": "사용하지 않는 제공업체를 숨기거나 카드를 펼쳐 단계를 완료하세요. Linear에는 API 액세스, 에이전트 스킬, 작업에 표시가 필요합니다."