mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Split managed skill prompt settings
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
<AgentStatusHooksSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
<ManagedAgentSkillBackgroundUpdatesSetting
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
/>
|
||||
<ManagedAgentSkillSetupPromptsSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
<AgentGeneratedTabTitlesSetting settings={settings} updateSettings={updateSettings} />
|
||||
|
||||
@@ -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 (
|
||||
<section className="space-y-3">
|
||||
<SettingsSwitchRow
|
||||
label={getManagedAgentSkillBackgroundUpdatesTitle()}
|
||||
description={getManagedAgentSkillBackgroundUpdatesDescription()}
|
||||
label={getManagedAgentSkillSetupPromptsTitle()}
|
||||
description={getManagedAgentSkillSetupPromptsDescription()}
|
||||
checked={enabled}
|
||||
onChange={() =>
|
||||
updateSettings({
|
||||
managedAgentSkillBackgroundUpdatesEnabled: !enabled
|
||||
managedAgentSkillSetupPromptsEnabled: !enabled
|
||||
})
|
||||
}
|
||||
ariaLabel={getManagedAgentSkillBackgroundUpdatesTitle()}
|
||||
ariaLabel={getManagedAgentSkillSetupPromptsTitle()}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
<ExperimentalPane settings={settings} updateSettings={vi.fn()} />
|
||||
)
|
||||
|
||||
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<HTMLButtonElement>(
|
||||
'#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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
|
||||
{showManagedSkillBackgroundUpdates ? (
|
||||
<SearchableSetting
|
||||
title={getManagedAgentSkillBackgroundUpdatesTitle()}
|
||||
description={getManagedAgentSkillBackgroundUpdatesDescription()}
|
||||
keywords={getExperimentalSearchEntry().managedSkillBackgroundUpdates.keywords}
|
||||
className="space-y-3 py-2"
|
||||
id="experimental-managed-skill-background-updates"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 shrink space-y-0.5">
|
||||
<Label>{getManagedAgentSkillBackgroundUpdatesTitle()}</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getManagedAgentSkillBackgroundUpdatesDescription()}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsSwitch
|
||||
checked={managedSkillBackgroundUpdatesEnabled}
|
||||
ariaLabel={getManagedAgentSkillBackgroundUpdatesTitle()}
|
||||
onChange={() =>
|
||||
updateSettings({
|
||||
managedAgentSkillBackgroundUpdatesEnabled: !managedSkillBackgroundUpdatesEnabled
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
) : null}
|
||||
|
||||
{showWorktreeSymlinks ? (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
getAgentStatusHooksTitle
|
||||
} from './agent-status-hooks-copy'
|
||||
import {
|
||||
getManagedAgentSkillBackgroundUpdatesDescription,
|
||||
getManagedAgentSkillBackgroundUpdatesSearchKeywords,
|
||||
getManagedAgentSkillBackgroundUpdatesTitle
|
||||
} from './managed-agent-skill-background-updates-copy'
|
||||
getManagedAgentSkillSetupPromptsDescription,
|
||||
getManagedAgentSkillSetupPromptsSearchKeywords,
|
||||
getManagedAgentSkillSetupPromptsTitle
|
||||
} from './managed-agent-skill-setup-prompts-copy'
|
||||
import { getAgentCacheTimerSearchEntries } from './agent-cache-timer-search'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { searchKeywords, translateSearchKeyword, uniqueKeywords } from './settings-search-keywords'
|
||||
@@ -79,9 +79,9 @@ export const getAgentsPaneSearchEntries = createLocalizedCatalog(() => [
|
||||
keywords: getAgentStatusHooksSearchKeywords()
|
||||
},
|
||||
{
|
||||
title: getManagedAgentSkillBackgroundUpdatesTitle(),
|
||||
description: getManagedAgentSkillBackgroundUpdatesDescription(),
|
||||
keywords: getManagedAgentSkillBackgroundUpdatesSearchKeywords()
|
||||
title: getManagedAgentSkillSetupPromptsTitle(),
|
||||
description: getManagedAgentSkillSetupPromptsDescription(),
|
||||
keywords: getManagedAgentSkillSetupPromptsSearchKeywords()
|
||||
},
|
||||
{
|
||||
title: getAgentGeneratedTabTitlesTitle(),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
])
|
||||
}
|
||||
@@ -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<typeof getDefaultSettings> | null,
|
||||
updateSettings: vi.fn<(updates: Partial<GlobalSettings>) => Promise<void>>(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<ManagedAgentSkillFallback> & Pick<ManagedAgentSkillFallback, 'context'>
|
||||
): 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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<ManagedAgentSkillDialogState>({
|
||||
active: null,
|
||||
@@ -46,6 +46,12 @@ export function ManagedAgentSkillSetupDialogHost(): React.JSX.Element | null {
|
||||
const queuedKeysRef = useRef(new Set<string>())
|
||||
const snoozedKeysRef = useRef(new Set<string>())
|
||||
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 {
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "更新"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "업데이트"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "更新"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user