diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 4109cb2b2ab..00df8d69f51 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -493,6 +493,8 @@ describe('Store', () => { expect(settings.experimentalActivityDefaultedOffForAllUsers).toBe(true) expect(settings.experimentalTerminalAttention).toBe(false) expect(settings.experimentalNewWorktreeCardStyle).toBe(true) + expect(settings.managedAgentSkillSetupPromptsEnabled).toBe(true) + expect(settings.managedAgentSkillBackgroundUpdatesEnabled).toBe(false) expect(settings.floatingTerminalEnabled).toBe(true) expect(settings.floatingTerminalDefaultedForAllUsers).toBe(true) expect(settings.notifications.customSoundPath).toBeNull() diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx index 70d35346fe1..e4d1b474863 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -10,7 +10,7 @@ import { useAppStore } from '../../store' import { getAgentGeneratedTabTitlesTitle } from './agent-generated-tab-title-copy' import { getAgentStatusHooksTitle } from './agent-status-hooks-copy' import { getAgentAwakeDescription, getAgentAwakeTitle } from './agent-awake-copy' -import { getManagedAgentSkillBackgroundUpdatesTitle } from './managed-agent-skill-background-updates-copy' +import { getManagedAgentSkillSetupPromptsTitle } from './managed-agent-skill-setup-prompts-copy' import { AgentAwakeSetting } from './AgentAwakeSetting' import { AgentAvailabilityControl, @@ -18,7 +18,7 @@ import { AgentGeneratedTabTitlesSetting, AgentStatusHooksSetting, AgentsPane, - ManagedAgentSkillBackgroundUpdatesSetting, + ManagedAgentSkillSetupPromptsSetting, getAgentsPaneSearchEntries, buildAgentAvailabilitySettingsUpdate, createAgentAvailabilityUpdateQueue @@ -146,11 +146,12 @@ describe('AgentsPane', () => { expect(markup).toContain( 'Keeps this computer and display awake while agents are working. Orca also asks this device to stay awake when the lid is closed, subject to its power policy.' ) - expect(markup).toContain('Automatically update verified Orca skills') + expect(markup).toContain('Show agent skill setup prompts') expect(markup).toContain( - 'Experimental. When enabled, Orca can update verified Orca-managed global skills in the background when a workflow needs them. When off, Orca asks you to review updates manually.' + 'When an Orca workflow needs a missing or outdated managed skill, show a prompt to install or update it.' ) - expect(markup).toContain('aria-checked="false"') + expect(markup).toContain('aria-checked="true"') + expect(markup).not.toContain('Automatically update verified Orca skills') }) it('does not render the legacy agent location control on Windows', () => { @@ -229,21 +230,21 @@ describe('AgentsPane', () => { }) }) - it('enables experimental managed skill background updates from the default off state', () => { + it('disables managed skill setup prompts from the default on state', () => { const updateSettings = vi.fn() - const element = ManagedAgentSkillBackgroundUpdatesSetting({ + const element = ManagedAgentSkillSetupPromptsSetting({ settings: getDefaultSettings('/tmp'), updateSettings }) - const updatesSwitch = findSwitchRow(element, getManagedAgentSkillBackgroundUpdatesTitle()) - expect(updatesSwitch.props.checked).toBe(false) + const updatesSwitch = findSwitchRow(element, getManagedAgentSkillSetupPromptsTitle()) + expect(updatesSwitch.props.checked).toBe(true) const onChange = updatesSwitch.props.onChange as () => void onChange() expect(updateSettings).toHaveBeenCalledWith({ - managedAgentSkillBackgroundUpdatesEnabled: true + managedAgentSkillSetupPromptsEnabled: false }) }) @@ -280,10 +281,10 @@ describe('AgentsPane', () => { expect(matchesSettingsSearch('codex', getAgentsPaneSearchEntries())).toBe(true) }) - it('includes managed skill update search metadata', () => { - expect(matchesSettingsSearch('experimental', getAgentsPaneSearchEntries())).toBe(true) - expect(matchesSettingsSearch('automatic', getAgentsPaneSearchEntries())).toBe(true) - expect(matchesSettingsSearch('manual', getAgentsPaneSearchEntries())).toBe(true) + it('includes managed skill prompt search metadata', () => { + expect(matchesSettingsSearch('prompt', getAgentsPaneSearchEntries())).toBe(true) + expect(matchesSettingsSearch('setup', getAgentsPaneSearchEntries())).toBe(true) + expect(matchesSettingsSearch('install', getAgentsPaneSearchEntries())).toBe(true) expect(matchesSettingsSearch('skills', getAgentsPaneSearchEntries())).toBe(true) }) diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index f6092117881..9593f907a29 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -18,9 +18,9 @@ import { } from './agent-generated-tab-title-copy' import { getAgentStatusHooksDescription, getAgentStatusHooksTitle } from './agent-status-hooks-copy' import { - getManagedAgentSkillBackgroundUpdatesDescription, - getManagedAgentSkillBackgroundUpdatesTitle -} from './managed-agent-skill-background-updates-copy' + getManagedAgentSkillSetupPromptsDescription, + getManagedAgentSkillSetupPromptsTitle +} from './managed-agent-skill-setup-prompts-copy' import { SettingsBadge, SettingsSegmentedControl, @@ -827,10 +827,7 @@ export function AgentsPane({ settings, updateSettings }: AgentsPaneProps): React - + @@ -978,23 +975,23 @@ export function AgentStatusHooksSetting({ ) } -export function ManagedAgentSkillBackgroundUpdatesSetting({ +export function ManagedAgentSkillSetupPromptsSetting({ settings, updateSettings }: AgentsPaneProps): React.JSX.Element { - const enabled = settings.managedAgentSkillBackgroundUpdatesEnabled === true + const enabled = settings.managedAgentSkillSetupPromptsEnabled !== false return (
updateSettings({ - managedAgentSkillBackgroundUpdatesEnabled: !enabled + managedAgentSkillSetupPromptsEnabled: !enabled }) } - ariaLabel={getManagedAgentSkillBackgroundUpdatesTitle()} + ariaLabel={getManagedAgentSkillSetupPromptsTitle()} />
) diff --git a/src/renderer/src/components/settings/ExperimentalPane.test.tsx b/src/renderer/src/components/settings/ExperimentalPane.test.tsx index 10c5cd76cd1..0fb119a3b01 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.test.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.test.tsx @@ -8,6 +8,7 @@ import type { GlobalSettings } from '../../../../shared/types' import { getDefaultSettings } from '../../../../shared/constants' import { ExperimentalPane } from './ExperimentalPane' import { getExperimentalPaneSearchEntries } from './experimental-search' +import { getManagedAgentSkillBackgroundUpdatesTitle } from './managed-agent-skill-background-updates-copy' vi.mock('../../store', () => ({ useAppStore: (selector: (state: { settingsSearchQuery: string }) => unknown) => @@ -77,6 +78,21 @@ describe('ExperimentalPane', () => { ) }) + it('renders managed skill background updates as an off-by-default experimental switch', () => { + const settings = getDefaultSettings('/tmp') + const markup = renderToStaticMarkup( + + ) + + expect(settings.managedAgentSkillBackgroundUpdatesEnabled).toBe(false) + expect(markup).toContain('Automatically update verified Orca skills') + expect(markup).toContain('Experimental. When enabled') + expect(markup).toContain('aria-checked="false"') + expect(getExperimentalPaneSearchEntries().map((entry) => entry.title)).toContain( + getManagedAgentSkillBackgroundUpdatesTitle() + ) + }) + it('renders the agent sleep idle duration as configurable minutes', async () => { const updateSettings = vi.fn() const settings = { @@ -138,4 +154,25 @@ describe('ExperimentalPane', () => { expect(updateSettings).toHaveBeenCalledWith({ experimentalNewWorktreeCardStyle: true }) root.unmount() }) + + it('enables managed skill background updates through the experimental switch', async () => { + const updateSettings = vi.fn() + const { root, container } = await renderExperimentalPane({ updateSettings }) + + const switchButton = container.querySelector( + '#experimental-managed-skill-background-updates button[role="switch"]' + ) + if (!switchButton) { + throw new Error('Managed skill background updates switch was not rendered') + } + + await act(async () => { + switchButton.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(updateSettings).toHaveBeenCalledWith({ + managedAgentSkillBackgroundUpdatesEnabled: true + }) + root.unmount() + }) }) diff --git a/src/renderer/src/components/settings/ExperimentalPane.tsx b/src/renderer/src/components/settings/ExperimentalPane.tsx index 5786d409aba..932d46ef296 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.tsx @@ -6,6 +6,10 @@ import { matchesSettingsSearch } from './settings-search' import { getExperimentalPaneSearchEntries, getExperimentalSearchEntry } from './experimental-search' import { HiddenExperimentalGroup } from './HiddenExperimentalGroup' import { NumberField, SettingsSwitch } from './SettingsFormControls' +import { + getManagedAgentSkillBackgroundUpdatesDescription, + getManagedAgentSkillBackgroundUpdatesTitle +} from './managed-agent-skill-background-updates-copy' import { translate } from '@/i18n/i18n' import { MAX_AGENT_HIBERNATION_IDLE_MS, @@ -47,8 +51,13 @@ export function ExperimentalPane({ const showNewWorktreeCardStyle = matchesSettingsSearch(searchQuery, [ getExperimentalSearchEntry().newWorktreeCardStyle ]) + const showManagedSkillBackgroundUpdates = matchesSettingsSearch(searchQuery, [ + getExperimentalSearchEntry().managedSkillBackgroundUpdates + ]) const agentHibernationEnabled = settings.experimentalAgentHibernation === true const newWorktreeCardStyleEnabled = settings.experimentalNewWorktreeCardStyle === true + const managedSkillBackgroundUpdatesEnabled = + settings.managedAgentSkillBackgroundUpdatesEnabled === true // Why: the planner owns ms-based bounds/defaults; the UI edits minutes // while displaying the same effective clamped value the planner will use. const agentHibernationIdleMinutes = Math.round( @@ -313,6 +322,34 @@ export function ExperimentalPane({ ) : null} + {showManagedSkillBackgroundUpdates ? ( + +
+
+ +

+ {getManagedAgentSkillBackgroundUpdatesDescription()} +

+
+ + updateSettings({ + managedAgentSkillBackgroundUpdatesEnabled: !managedSkillBackgroundUpdatesEnabled + }) + } + /> +
+
+ ) : null} + {showWorktreeSymlinks ? ( [ keywords: getAgentStatusHooksSearchKeywords() }, { - title: getManagedAgentSkillBackgroundUpdatesTitle(), - description: getManagedAgentSkillBackgroundUpdatesDescription(), - keywords: getManagedAgentSkillBackgroundUpdatesSearchKeywords() + title: getManagedAgentSkillSetupPromptsTitle(), + description: getManagedAgentSkillSetupPromptsDescription(), + keywords: getManagedAgentSkillSetupPromptsSearchKeywords() }, { title: getAgentGeneratedTabTitlesTitle(), diff --git a/src/renderer/src/components/settings/experimental-search.ts b/src/renderer/src/components/settings/experimental-search.ts index 8ce3096ebdd..c2287724cb0 100644 --- a/src/renderer/src/components/settings/experimental-search.ts +++ b/src/renderer/src/components/settings/experimental-search.ts @@ -3,6 +3,11 @@ import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' import { getNewWorktreeCardStyleSearchEntry } from './new-worktree-card-style-search-entry' +import { + getManagedAgentSkillBackgroundUpdatesDescription, + getManagedAgentSkillBackgroundUpdatesSearchKeywords, + getManagedAgentSkillBackgroundUpdatesTitle +} from './managed-agent-skill-background-updates-copy' export const getExperimentalPaneSearchEntries = createLocalizedCatalog( (): SettingsSearchEntry[] => [ @@ -182,6 +187,11 @@ export const getExperimentalPaneSearchEntries = createLocalizedCatalog( ] }, getNewWorktreeCardStyleSearchEntry(), + { + title: getManagedAgentSkillBackgroundUpdatesTitle(), + description: getManagedAgentSkillBackgroundUpdatesDescription(), + keywords: getManagedAgentSkillBackgroundUpdatesSearchKeywords() + }, { title: translate( 'auto.components.settings.experimental.search.78c2a8dc74', @@ -266,6 +276,7 @@ export function getExperimentalSearchEntry() { 'New card style' ) ), + managedSkillBackgroundUpdates: findEntry(getManagedAgentSkillBackgroundUpdatesTitle()), symlinksOnWorktrees: findEntry( translate( 'auto.components.settings.experimental.search.78c2a8dc74', diff --git a/src/renderer/src/components/settings/managed-agent-skill-setup-prompts-copy.ts b/src/renderer/src/components/settings/managed-agent-skill-setup-prompts-copy.ts new file mode 100644 index 00000000000..6d0bb44cb6f --- /dev/null +++ b/src/renderer/src/components/settings/managed-agent-skill-setup-prompts-copy.ts @@ -0,0 +1,45 @@ +import { translate } from '@/i18n/i18n' +import { searchKeywords } from './settings-search-keywords' + +export function getManagedAgentSkillSetupPromptsTitle(): string { + return translate( + 'auto.components.settings.managed.agent.skill.setup.prompts.title', + 'Show agent skill setup prompts' + ) +} + +export function getManagedAgentSkillSetupPromptsDescription(): string { + return translate( + 'auto.components.settings.managed.agent.skill.setup.prompts.description', + 'When an Orca workflow needs a missing or outdated managed skill, show a prompt to install or update it.' + ) +} + +export function getManagedAgentSkillSetupPromptsSearchKeywords(): string[] { + return searchKeywords([ + { + key: 'auto.components.settings.managed.agent.skill.setup.prompts.search.agent', + fallback: 'agent' + }, + { + key: 'auto.components.settings.managed.agent.skill.setup.prompts.search.skills', + fallback: 'skills' + }, + { + key: 'auto.components.settings.managed.agent.skill.setup.prompts.search.setup', + fallback: 'setup' + }, + { + key: 'auto.components.settings.managed.agent.skill.setup.prompts.search.prompt', + fallback: 'prompt' + }, + { + key: 'auto.components.settings.managed.agent.skill.setup.prompts.search.install', + fallback: 'install' + }, + { + key: 'auto.components.settings.managed.agent.skill.setup.prompts.search.update', + fallback: 'update' + } + ]) +} diff --git a/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.test.ts b/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.test.ts index 7114968c247..7a8227d56c1 100644 --- a/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.test.ts +++ b/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.test.ts @@ -1,5 +1,12 @@ +// @vitest-environment happy-dom + +import React from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getDefaultSettings } from '../../../../shared/constants' import type { ManagedAgentSkillFallback } from '../../../../shared/skills' +import type { GlobalSettings } from '../../../../shared/types' import { getManagedSkillFallbackDisplayMessage, getManagedSkillContextCopy, @@ -12,11 +19,37 @@ import { prepareManagedAgentSkillSetupTerminal, replaceActiveAfterManagedAgentSkillRecheck } from './managed-agent-skill-dialog-state' +import { ManagedAgentSkillSetupDialogHost } from './ManagedAgentSkillSetupDialogHost' const cliPrerequisiteMocks = vi.hoisted(() => ({ ensureOrcaCliAvailableForAgentSkillTerminal: vi.fn() })) +const hookMocks = vi.hoisted(() => ({ + notifyInstalledAgentSkillsChanged: vi.fn(), + useInstalledAgentSkill: vi.fn(() => ({ + installed: false, + loading: false, + error: null + })) +})) + +const storeMocks = vi.hoisted(() => ({ + state: { + settings: null as ReturnType | null, + updateSettings: vi.fn<(updates: Partial) => Promise>(async () => { + return undefined + }), + openSettingsPage: vi.fn(), + openSettingsTarget: vi.fn() + } +})) + +const toastMocks = vi.hoisted(() => ({ + error: vi.fn(), + message: vi.fn() +})) + vi.mock('@/lib/agent-skill-cli-prerequisite', () => ({ AGENT_SKILL_CLI_PREREQUISITE_NOTICE: 'CLI registration notice', ensureOrcaCliAvailableForAgentSkillTerminal: @@ -24,6 +57,26 @@ vi.mock('@/lib/agent-skill-cli-prerequisite', () => ({ isOrcaCliAvailableOnPath: vi.fn() })) +vi.mock('@/hooks/useInstalledAgentSkills', () => ({ + notifyInstalledAgentSkillsChanged: hookMocks.notifyInstalledAgentSkillsChanged, + useInstalledAgentSkill: hookMocks.useInstalledAgentSkill +})) + +vi.mock('@/store', () => { + const useAppStore = Object.assign( + (selector: (state: typeof storeMocks.state) => unknown) => selector(storeMocks.state), + { getState: () => storeMocks.state } + ) + return { useAppStore } +}) + +vi.mock('sonner', () => ({ + toast: { + error: toastMocks.error, + message: toastMocks.message + } +})) + function fallback( patch: Partial & Pick ): ManagedAgentSkillFallback { @@ -50,6 +103,20 @@ function fallback( beforeEach(() => { cliPrerequisiteMocks.ensureOrcaCliAvailableForAgentSkillTerminal.mockReset() + hookMocks.notifyInstalledAgentSkillsChanged.mockReset() + hookMocks.useInstalledAgentSkill.mockClear() + storeMocks.state.settings = getDefaultSettings('/tmp') + storeMocks.state.updateSettings.mockReset() + storeMocks.state.updateSettings.mockImplementation(async (updates) => { + storeMocks.state.settings = storeMocks.state.settings + ? { ...storeMocks.state.settings, ...updates } + : null + }) + storeMocks.state.openSettingsPage.mockReset() + storeMocks.state.openSettingsTarget.mockReset() + toastMocks.error.mockReset() + toastMocks.message.mockReset() + document.body.innerHTML = '' }) describe('ManagedAgentSkillSetupDialogHost copy', () => { @@ -184,3 +251,118 @@ describe('ManagedAgentSkillSetupDialogHost queue state', () => { }) }) }) + +describe('ManagedAgentSkillSetupDialogHost rendering', () => { + async function renderHost(): Promise<{ + root: Root + emitFallback: (event: ManagedAgentSkillFallback) => void + }> { + let fallbackListener: ((event: ManagedAgentSkillFallback) => void) | null = null + window.api = { + skills: { + onManagedFallback: vi.fn((listener) => { + fallbackListener = listener + return vi.fn() + }), + onManagedUpdated: vi.fn(() => vi.fn()) + } + } as unknown as typeof window.api + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + await act(async () => { + root.render(React.createElement(ManagedAgentSkillSetupDialogHost)) + }) + return { + root, + emitFallback: (event) => { + if (!fallbackListener) { + throw new Error('fallback listener was not registered') + } + fallbackListener(event) + } + } + } + + it('turns off managed skill setup prompts when Don’t show again is clicked', async () => { + const { root, emitFallback } = await renderHost() + await act(async () => { + emitFallback(fallback({ context: 'agent-orchestration' })) + }) + + const button = Array.from(document.querySelectorAll('button')).find( + (entry) => entry.textContent === "Don't show again" + ) + if (!button) { + throw new Error("Don't show again button was not rendered") + } + + await act(async () => { + button.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(storeMocks.state.updateSettings).toHaveBeenCalledWith({ + managedAgentSkillSetupPromptsEnabled: false + }) + expect(toastMocks.message).toHaveBeenCalledWith( + 'Agent skill setup prompts are off', + expect.objectContaining({ + description: 'Turn them back on in Settings → Agents.', + action: expect.objectContaining({ label: 'Open Settings' }) + }) + ) + const toastOptions = toastMocks.message.mock.calls[0]?.[1] + toastOptions?.action?.onClick() + expect(storeMocks.state.openSettingsTarget).toHaveBeenCalledWith({ + pane: 'agents', + repoId: null + }) + expect(storeMocks.state.openSettingsPage).toHaveBeenCalledOnce() + root.unmount() + }) + + it('keeps the modal open when Don’t show again cannot persist', async () => { + storeMocks.state.updateSettings.mockResolvedValue(undefined) + const { root, emitFallback } = await renderHost() + await act(async () => { + emitFallback(fallback({ context: 'agent-orchestration' })) + }) + + const button = Array.from(document.querySelectorAll('button')).find( + (entry) => entry.textContent === "Don't show again" + ) + if (!button) { + throw new Error("Don't show again button was not rendered") + } + + await act(async () => { + button.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(toastMocks.error).toHaveBeenCalledWith( + "Couldn't turn off agent skill setup prompts", + expect.objectContaining({ + description: 'Try again from Settings → Agents.', + action: expect.objectContaining({ label: 'Open Settings' }) + }) + ) + expect(toastMocks.message).not.toHaveBeenCalled() + expect(document.body.textContent).toContain('Agent skill setup needed') + root.unmount() + }) + + it('does not show managed skill setup prompts when the setting is off', async () => { + storeMocks.state.settings = { + ...getDefaultSettings('/tmp'), + managedAgentSkillSetupPromptsEnabled: false + } + const { root, emitFallback } = await renderHost() + + await act(async () => { + emitFallback(fallback({ context: 'agent-orchestration' })) + }) + + expect(document.body.textContent).not.toContain('Agent skill setup needed') + root.unmount() + }) +}) diff --git a/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.tsx b/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.tsx index 8027e5c80f4..ed1f6be2596 100644 --- a/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.tsx +++ b/src/renderer/src/components/skills/ManagedAgentSkillSetupDialogHost.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Info, RefreshCw } from 'lucide-react' +import { toast } from 'sonner' import type { ManagedAgentSkillFallback } from '../../../../shared/skills' import { buildAgentFeatureSkillUpdateCommand } from '@/lib/agent-feature-install-commands' import { @@ -34,10 +35,9 @@ import { isOrcaCliAvailableOnPath } from '@/lib/agent-skill-cli-prerequisite' import { basename } from '@/lib/path' +import { useAppStore } from '@/store' import { translate } from '@/i18n/i18n' -const DISMISS_STORAGE_PREFIX = 'orca:managed-agent-skill-setup-dismissed:' - export function ManagedAgentSkillSetupDialogHost(): React.JSX.Element | null { const [dialogState, setDialogState] = useState({ active: null, @@ -46,6 +46,12 @@ export function ManagedAgentSkillSetupDialogHost(): React.JSX.Element | null { const queuedKeysRef = useRef(new Set()) const snoozedKeysRef = useRef(new Set()) const [rechecking, setRechecking] = useState(false) + const setupPromptsEnabled = useAppStore( + (state) => state.settings?.managedAgentSkillSetupPromptsEnabled !== false + ) + const updateSettings = useAppStore((state) => state.updateSettings) + const openSettingsPage = useAppStore((state) => state.openSettingsPage) + const openSettingsTarget = useAppStore((state) => state.openSettingsTarget) const active = dialogState.active const installedState = useInstalledAgentSkill(active?.skillName ?? 'linear-tickets', { enabled: active !== null, @@ -54,16 +60,29 @@ export function ManagedAgentSkillSetupDialogHost(): React.JSX.Element | null { sourceKinds: active ? getInstalledStateSourceKinds(active.scope) : undefined }) - const enqueueFallback = useCallback((event: ManagedAgentSkillFallback): void => { - if (isDismissed(event.uiKey) || snoozedKeysRef.current.has(event.uiKey)) { + const enqueueFallback = useCallback( + (event: ManagedAgentSkillFallback): void => { + if (!setupPromptsEnabled || snoozedKeysRef.current.has(event.uiKey)) { + return + } + if (queuedKeysRef.current.has(event.uiKey)) { + return + } + queuedKeysRef.current.add(event.uiKey) + setDialogState((current) => enqueueManagedAgentSkillFallback(current, event)) + }, + [setupPromptsEnabled] + ) + + useEffect(() => { + if (setupPromptsEnabled) { return } - if (queuedKeysRef.current.has(event.uiKey)) { - return - } - queuedKeysRef.current.add(event.uiKey) - setDialogState((current) => enqueueManagedAgentSkillFallback(current, event)) - }, []) + queuedKeysRef.current.clear() + snoozedKeysRef.current.clear() + setDialogState({ active: null, queue: [] }) + setRechecking(false) + }, [setupPromptsEnabled]) const advanceQueue = useCallback((): void => { setDialogState((current) => { @@ -244,8 +263,59 @@ export function ManagedAgentSkillSetupDialogHost(): React.JSX.Element | null { variant="ghost" size="sm" onClick={() => { - setDismissed(active.uiKey) - advanceQueue() + void (async () => { + await updateSettings({ managedAgentSkillSetupPromptsEnabled: false }) + if ( + useAppStore.getState().settings?.managedAgentSkillSetupPromptsEnabled !== false + ) { + toast.error( + translate( + 'auto.components.skills.ManagedAgentSkillSetupDialogHost.promptsDisableFailedToast', + "Couldn't turn off agent skill setup prompts" + ), + { + description: translate( + 'auto.components.skills.ManagedAgentSkillSetupDialogHost.promptsDisableFailedToastDescription', + 'Try again from Settings → Agents.' + ), + action: { + label: translate( + 'auto.components.skills.ManagedAgentSkillSetupDialogHost.openSettings', + 'Open Settings' + ), + onClick: () => { + openSettingsTarget({ pane: 'agents', repoId: null }) + openSettingsPage() + } + } + } + ) + return + } + toast.message( + translate( + 'auto.components.skills.ManagedAgentSkillSetupDialogHost.promptsDisabledToast', + 'Agent skill setup prompts are off' + ), + { + description: translate( + 'auto.components.skills.ManagedAgentSkillSetupDialogHost.promptsDisabledToastDescription', + 'Turn them back on in Settings → Agents.' + ), + action: { + label: translate( + 'auto.components.skills.ManagedAgentSkillSetupDialogHost.openSettings', + 'Open Settings' + ), + onClick: () => { + openSettingsTarget({ pane: 'agents', repoId: null }) + openSettingsPage() + } + } + } + ) + advanceQueue() + })() }} > {translate( @@ -269,19 +339,3 @@ export function ManagedAgentSkillSetupDialogHost(): React.JSX.Element | null { ) } - -function isDismissed(uiKey: string): boolean { - try { - return window.localStorage.getItem(`${DISMISS_STORAGE_PREFIX}${uiKey}`) === 'true' - } catch { - return false - } -} - -function setDismissed(uiKey: string): void { - try { - window.localStorage.setItem(`${DISMISS_STORAGE_PREFIX}${uiKey}`, 'true') - } catch { - // Local storage can be unavailable in constrained test/browser contexts. - } -} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 73a1f9f6262..b29e9702665 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -3316,6 +3316,11 @@ "recheck": "Re-check", "dontShowAgain": "Don't show again", "notNow": "Not now", + "promptsDisabledToast": "Agent skill setup prompts are off", + "promptsDisabledToastDescription": "Turn them back on in Settings → Agents.", + "promptsDisableFailedToast": "Couldn't turn off agent skill setup prompts", + "promptsDisableFailedToastDescription": "Try again from Settings → Agents.", + "openSettings": "Open Settings", "linearWorktreeContext": "A worktree was started from a Linear task. {{actionLabel}} the Linear agent skill to enable agents to read and update Linear issues.", "agentOrchestrationContext": "Orca Orchestration was used. {{actionLabel}} the orchestration skill to enable agents to coordinate reliably.", "agentComputerUseContext": "Computer Use was used. {{actionLabel}} the Computer Use skill to enable agents to control apps reliably.", @@ -4390,6 +4395,20 @@ "experimental": "experimental" } } + }, + "setup": { + "prompts": { + "title": "Show agent skill setup prompts", + "description": "When an Orca workflow needs a missing or outdated managed skill, show a prompt to install or update it.", + "search": { + "agent": "agent", + "skills": "skills", + "setup": "setup", + "prompt": "prompt", + "install": "install", + "update": "update" + } + } } } } diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index de53a87fea9..8cf2c35674b 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -3316,6 +3316,11 @@ "recheck": "Volver a comprobar", "dontShowAgain": "No volver a mostrar", "notNow": "Ahora no", + "promptsDisabledToast": "Los avisos de configuración de habilidades de agente están desactivados", + "promptsDisabledToastDescription": "Puedes volver a activarlos en Configuración → Agentes.", + "promptsDisableFailedToast": "No se pudieron desactivar los avisos de configuración de habilidades de agente", + "promptsDisableFailedToastDescription": "Inténtalo de nuevo desde Configuración → Agentes.", + "openSettings": "Abrir Configuración", "linearWorktreeContext": "A worktree was started from a Linear task. {{actionLabel}} the Linear agent skill to enable agents to read and update Linear issues.", "agentOrchestrationContext": "Orca Orchestration was used. {{actionLabel}} the orchestration skill to enable agents to coordinate reliably.", "agentComputerUseContext": "Computer Use was used. {{actionLabel}} the Computer Use skill to enable agents to control apps reliably.", @@ -4390,6 +4395,20 @@ "experimental": "experimental" } } + }, + "setup": { + "prompts": { + "title": "Mostrar avisos de configuración de habilidades de agente", + "description": "Cuando un flujo de trabajo de Orca necesite una habilidad administrada faltante o desactualizada, muestra un aviso para instalarla o actualizarla.", + "search": { + "agent": "agente", + "skills": "habilidades", + "setup": "configuración", + "prompt": "aviso", + "install": "instalar", + "update": "actualizar" + } + } } } } diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index e5b5c49802d..76aec0f9d6d 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -3316,6 +3316,11 @@ "recheck": "再チェック", "dontShowAgain": "今後表示しない", "notNow": "今はしない", + "promptsDisabledToast": "エージェントスキル設定プロンプトはオフです", + "promptsDisabledToastDescription": "Settings → Agents で再度オンにできます。", + "promptsDisableFailedToast": "エージェントスキル設定プロンプトをオフにできませんでした", + "promptsDisableFailedToastDescription": "Settings → Agents からもう一度お試しください。", + "openSettings": "設定を開く", "linearWorktreeContext": "A worktree was started from a Linear task. {{actionLabel}} the Linear agent skill to enable agents to read and update Linear issues.", "agentOrchestrationContext": "Orca Orchestration was used. {{actionLabel}} the orchestration skill to enable agents to coordinate reliably.", "agentComputerUseContext": "Computer Use was used. {{actionLabel}} the Computer Use skill to enable agents to control apps reliably.", @@ -4375,6 +4380,20 @@ "experimental": "experimental" } } + }, + "setup": { + "prompts": { + "title": "エージェントスキル設定プロンプトを表示", + "description": "Orca のワークフローで未インストールまたは古い管理対象スキルが必要になったとき、インストールまたは更新のプロンプトを表示します。", + "search": { + "agent": "エージェント", + "skills": "スキル", + "setup": "設定", + "prompt": "プロンプト", + "install": "インストール", + "update": "更新" + } + } } } } diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 08e598a2362..c2b214fcf5e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -3316,6 +3316,11 @@ "recheck": "다시 확인", "dontShowAgain": "다시 표시하지 않기", "notNow": "나중에", + "promptsDisabledToast": "에이전트 스킬 설정 프롬프트가 꺼져 있습니다", + "promptsDisabledToastDescription": "Settings → Agents에서 다시 켤 수 있습니다.", + "promptsDisableFailedToast": "에이전트 스킬 설정 프롬프트를 끌 수 없습니다", + "promptsDisableFailedToastDescription": "Settings → Agents에서 다시 시도하세요.", + "openSettings": "설정 열기", "linearWorktreeContext": "A worktree was started from a Linear task. {{actionLabel}} the Linear agent skill to enable agents to read and update Linear issues.", "agentOrchestrationContext": "Orca Orchestration was used. {{actionLabel}} the orchestration skill to enable agents to coordinate reliably.", "agentComputerUseContext": "Computer Use was used. {{actionLabel}} the Computer Use skill to enable agents to control apps reliably.", @@ -4375,6 +4380,20 @@ "experimental": "experimental" } } + }, + "setup": { + "prompts": { + "title": "에이전트 스킬 설정 프롬프트 표시", + "description": "Orca 워크플로에 누락되었거나 오래된 관리형 스킬이 필요하면 설치 또는 업데이트 프롬프트를 표시합니다.", + "search": { + "agent": "에이전트", + "skills": "스킬", + "setup": "설정", + "prompt": "프롬프트", + "install": "설치", + "update": "업데이트" + } + } } } } diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 3ba576024ab..3bba1102bef 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -3316,6 +3316,11 @@ "recheck": "重新检查", "dontShowAgain": "不再显示", "notNow": "稍后", + "promptsDisabledToast": "代理技能设置提示已关闭", + "promptsDisabledToastDescription": "可在 Settings → Agents 中重新开启。", + "promptsDisableFailedToast": "无法关闭代理技能设置提示", + "promptsDisableFailedToastDescription": "请从 Settings → Agents 重试。", + "openSettings": "打开设置", "linearWorktreeContext": "A worktree was started from a Linear task. {{actionLabel}} the Linear agent skill to enable agents to read and update Linear issues.", "agentOrchestrationContext": "Orca Orchestration was used. {{actionLabel}} the orchestration skill to enable agents to coordinate reliably.", "agentComputerUseContext": "Computer Use was used. {{actionLabel}} the Computer Use skill to enable agents to control apps reliably.", @@ -4375,6 +4380,20 @@ "experimental": "experimental" } } + }, + "setup": { + "prompts": { + "title": "显示代理技能设置提示", + "description": "当 Orca 工作流需要缺失或过期的托管技能时,显示安装或更新提示。", + "search": { + "agent": "代理", + "skills": "技能", + "setup": "设置", + "prompt": "提示", + "install": "安装", + "update": "更新" + } + } } } } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 6b96b8f4370..36a5b46910c 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -316,6 +316,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings { agentDefaultEnv: { ...DEFAULT_TUI_AGENT_ENV }, agentYoloDefaultsMigrated: true, agentStatusHooksEnabled: true, + managedAgentSkillSetupPromptsEnabled: true, // Why: keep silent skill mutation opt-in while the verified updater is experimental. managedAgentSkillBackgroundUpdatesEnabled: false, tabAutoGenerateTitle: false, diff --git a/src/shared/types.ts b/src/shared/types.ts index 4f8c35d6491..1c22f21161a 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2648,6 +2648,9 @@ export type GlobalSettings = { /** Why: disabling must persist so startup does not reinstall global agent * hook entries right after the user removes them from Settings or CLI. */ agentStatusHooksEnabled: boolean + /** Why: users can silence managed-skill setup prompts from the modal, but + * the prompt should be easy to restore from normal Agents settings. */ + managedAgentSkillSetupPromptsEnabled?: boolean /** Why: verified background updates mutate user skill files, so this remains * opt-in while experimental; off makes updates manual. */ managedAgentSkillBackgroundUpdatesEnabled?: boolean