Update installed Orca skills without reinstalling (#5755)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-06-18 22:23:27 -07:00 committed by GitHub
parent f80704cc60
commit f1384597f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 812 additions and 123 deletions

View File

@ -1,5 +1,8 @@
import type { JSX } from 'react'
import { ORCA_CLI_SKILL_INSTALL_COMMAND } from '@/lib/agent-feature-install-commands'
import {
ORCA_CLI_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
@ -9,7 +12,7 @@ import type { InstalledAgentSkillState } from '@/hooks/useInstalledAgentSkills'
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
import { AgentSkillSetupPanel } from '@/components/settings/AgentSkillSetupPanel'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from '@/components/settings/CliSkillRuntimeSetup'
@ -25,11 +28,12 @@ export function BrowserUseSkillSetupCard(props: {
const activeSkillRuntime = useActiveProjectSkillRuntime()
const installCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillInstallCommandForRuntime(
ORCA_CLI_SKILL_INSTALL_COMMAND,
activeSkillRuntime.agentRuntime
)
? buildSkillCommandForRuntime(ORCA_CLI_SKILL_INSTALL_COMMAND, activeSkillRuntime.agentRuntime)
: ORCA_CLI_SKILL_INSTALL_COMMAND
const updateCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillCommandForRuntime(ORCA_CLI_SKILL_UPDATE_COMMAND, activeSkillRuntime.agentRuntime)
: ORCA_CLI_SKILL_UPDATE_COMMAND
const handleBeforeOpenTerminal = async (): Promise<void> => {
useAppStore.getState().recordFeatureInteraction('agent-browser-setup')
@ -51,6 +55,7 @@ export function BrowserUseSkillSetupCard(props: {
"Enables agents to navigate and verify pages in Orca's browser."
)}
command={installCommand}
installedCommand={updateCommand}
terminalTitle="Browser Use setup"
terminalAriaLabel="Browser Use skill install terminal"
terminalWorktreeId="feature-wall-browser-use-skill-terminal"

View File

@ -13,7 +13,10 @@ import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
} from '@/lib/agent-skill-cli-prerequisite'
import { ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/orchestration-install-command'
import {
ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from '@/lib/orchestration-install-command'
import {
GLOBAL_AGENT_SKILL_SOURCE_KINDS,
useInstalledAgentSkill
@ -21,7 +24,7 @@ import {
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
import { useAppStore } from '@/store'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from '@/components/settings/CliSkillRuntimeSetup'
@ -41,11 +44,18 @@ export function FloatingTerminalOrchestrationDialog({
const activeSkillRuntime = useActiveProjectSkillRuntime()
const installCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillInstallCommandForRuntime(
? buildSkillCommandForRuntime(
ORCHESTRATION_SKILL_INSTALL_COMMAND,
activeSkillRuntime.agentRuntime
)
: ORCHESTRATION_SKILL_INSTALL_COMMAND
const updateCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillCommandForRuntime(
ORCHESTRATION_SKILL_UPDATE_COMMAND,
activeSkillRuntime.agentRuntime
)
: ORCHESTRATION_SKILL_UPDATE_COMMAND
const {
installed: orchestrationSkillDetected,
loading: orchestrationSkillLoading,
@ -119,6 +129,7 @@ export function FloatingTerminalOrchestrationDialog({
'Enables agents to hand off context and coordinate work through Orca.'
)}
command={installCommand}
installedCommand={updateCommand}
terminalTitle="Orchestration setup"
terminalAriaLabel="Orchestration skill install terminal"
terminalWorktreeId="floating-terminal-orchestration-skill-terminal"

View File

@ -1,24 +1,64 @@
// @vitest-environment happy-dom
import { act, type ComponentProps } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import type { ComponentProps } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import { TooltipProvider } from '../ui/tooltip'
const INSTALL_COMMAND = 'npx skills add https://github.com/stablyai/orca --skill orca-cli --global'
const UPDATE_COMMAND = 'npx skills update orca-cli --global'
const mocks = vi.hoisted(() => ({
clipboardWrite: vi.fn(),
terminalProps: [] as { command: string; description: string }[],
toastError: vi.fn(),
toastSuccess: vi.fn()
}))
vi.mock('sonner', () => ({
toast: {
error: mocks.toastError,
success: mocks.toastSuccess
}
}))
vi.mock('../onboarding/OnboardingInlineCommandTerminal', () => ({
OnboardingInlineCommandTerminal: (props: { command: string; description: string }) => {
mocks.terminalProps.push(props)
return (
<div
data-testid="inline-command-terminal"
data-command={props.command}
data-description={props.description}
>
{props.command}
</div>
)
}
}))
function panelProps(
overrides: Partial<ComponentProps<typeof AgentSkillSetupPanel>> = {}
): ComponentProps<typeof AgentSkillSetupPanel> {
return {
title: 'CLI skill',
description: 'Enables agents to use Orca workflows.',
command: INSTALL_COMMAND,
terminalTitle: 'CLI skill setup',
terminalAriaLabel: 'CLI skill install terminal',
terminalWorktreeId: 'settings-cli-skill-terminal',
installed: false,
loading: false,
error: null,
onRecheck: vi.fn(),
...overrides
}
}
function renderPanel(overrides: Partial<ComponentProps<typeof AgentSkillSetupPanel>> = {}): string {
return renderToStaticMarkup(
<AgentSkillSetupPanel
title="CLI skill"
description="Enables agents to use Orca workflows."
command="npx skills add https://github.com/stablyai/orca --skill orca-cli --global"
terminalTitle="CLI skill setup"
terminalAriaLabel="CLI skill install terminal"
terminalWorktreeId="settings-cli-skill-terminal"
installed={false}
loading={false}
error={null}
onRecheck={vi.fn()}
{...overrides}
/>
)
return renderToStaticMarkup(<AgentSkillSetupPanel {...panelProps(overrides)} />)
}
function buttonLabels(html: string): string[] {
@ -36,7 +76,79 @@ function buttonMarkupByLabel(html: string, label: string): string | undefined {
)
}
let root: Root | null = null
let container: HTMLDivElement | null = null
async function renderInteractivePanel(
overrides: Partial<ComponentProps<typeof AgentSkillSetupPanel>> = {}
): Promise<HTMLDivElement> {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
await rerenderInteractivePanel(overrides)
return container
}
async function rerenderInteractivePanel(
overrides: Partial<ComponentProps<typeof AgentSkillSetupPanel>> = {}
): Promise<void> {
await act(async () => {
root?.render(
<TooltipProvider>
<AgentSkillSetupPanel {...panelProps(overrides)} />
</TooltipProvider>
)
})
await act(async () => {})
}
function findButton(label: string): HTMLButtonElement {
const button = Array.from((container ?? document.body).querySelectorAll('button')).find(
(candidate) => candidate.textContent?.trim() === label
)
expect(button).toBeDefined()
return button as HTMLButtonElement
}
async function clickButton(label: string): Promise<void> {
await act(async () => {
findButton(label).dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
await act(async () => {})
}
describe('AgentSkillSetupPanel', () => {
beforeEach(() => {
mocks.clipboardWrite.mockReset()
mocks.clipboardWrite.mockResolvedValue(undefined)
mocks.terminalProps.length = 0
mocks.toastError.mockReset()
mocks.toastSuccess.mockReset()
Object.defineProperty(window, 'api', {
configurable: true,
value: {
cli: {
getInstallStatus: vi.fn()
},
ui: {
writeClipboardText: mocks.clipboardWrite
}
}
})
})
afterEach(async () => {
if (root) {
await act(async () => {
root?.unmount()
})
}
root = null
container?.remove()
container = null
Reflect.deleteProperty(window, 'api')
})
it('keeps the install action visible after the skill is detected', () => {
const html = renderPanel({ installed: true })
@ -65,6 +177,25 @@ describe('AgentSkillSetupPanel', () => {
expect(buttonLabels(html)).not.toContain('Install CLI &amp; Skill')
})
it('keeps the installed action label when CLI prerequisites are missing', async () => {
await renderInteractivePanel({
installed: true,
installedCommand: UPDATE_COMMAND,
installLabel: 'Install CLI & Skill',
preInstallNotice: 'Install the Orca CLI before running agent skill setup.',
getPrerequisiteStatus: vi.fn(
async () =>
({
state: 'not_installed'
}) as Awaited<ReturnType<typeof window.api.cli.getInstallStatus>>
),
isPrerequisiteAvailable: () => false
})
expect(findButton('Update').disabled).toBe(false)
expect(container?.textContent).not.toContain('Install CLI & Skill')
})
it('can hide install after the skill is detected', () => {
const html = renderPanel({ installed: true, showInstallWhenInstalled: false })
@ -85,4 +216,62 @@ describe('AgentSkillSetupPanel', () => {
expect(buttonMarkupByLabel(html, 'Install')).toContain('disabled=""')
})
it('opens not-installed setup with the install command for preview, copy, and terminal', async () => {
await renderInteractivePanel({ installedCommand: UPDATE_COMMAND })
await clickButton('Install')
expect(container?.textContent).toContain(INSTALL_COMMAND)
expect(mocks.terminalProps.at(-1)).toMatchObject({
command: INSTALL_COMMAND,
description: 'Press Enter to run the command.'
})
await act(async () => {
container
?.querySelector<HTMLButtonElement>('button[aria-label="Copy command"]')
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(mocks.clipboardWrite).toHaveBeenCalledWith(INSTALL_COMMAND)
expect(mocks.toastSuccess).toHaveBeenCalledWith('Copied command.')
})
it('opens installed setup with the installed command for preview, copy, and terminal', async () => {
await renderInteractivePanel({ installed: true, installedCommand: UPDATE_COMMAND })
await clickButton('Update')
expect(container?.textContent).toContain(UPDATE_COMMAND)
expect(mocks.terminalProps.at(-1)).toMatchObject({ command: UPDATE_COMMAND })
await act(async () => {
container
?.querySelector<HTMLButtonElement>('button[aria-label="Copy command"]')
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(mocks.clipboardWrite).toHaveBeenCalledWith(UPDATE_COMMAND)
})
it('keeps an open terminal on the command captured when it opened', async () => {
await renderInteractivePanel({ installed: false, installedCommand: UPDATE_COMMAND })
await clickButton('Install')
await rerenderInteractivePanel({ installed: true, installedCommand: UPDATE_COMMAND })
expect(container?.textContent).toContain(INSTALL_COMMAND)
expect(container?.textContent).not.toContain(UPDATE_COMMAND)
expect(mocks.terminalProps.at(-1)).toMatchObject({ command: INSTALL_COMMAND })
})
it('falls back to the install command for installed callers without installedCommand', async () => {
await renderInteractivePanel({ installed: true })
await clickButton('Update')
expect(container?.textContent).toContain(INSTALL_COMMAND)
expect(mocks.terminalProps.at(-1)).toMatchObject({ command: INSTALL_COMMAND })
})
})

View File

@ -18,6 +18,7 @@ type AgentSkillSetupPanelProps = {
title: string
description: ReactNode
command: string
installedCommand?: string
terminalTitle: string
terminalAriaLabel: string
terminalWorktreeId: string
@ -51,6 +52,7 @@ export function AgentSkillSetupPanel({
title,
description,
command,
installedCommand,
terminalTitle,
terminalAriaLabel,
terminalWorktreeId,
@ -78,6 +80,7 @@ export function AgentSkillSetupPanel({
onRecheck
}: AgentSkillSetupPanelProps): React.JSX.Element {
const [terminalOpen, setTerminalOpen] = useState(false)
const [terminalCommand, setTerminalCommand] = useState<string | null>(null)
const [preInstallNoticeVisible, setPreInstallNoticeVisible] = useState(
Boolean(preInstallNotice && !installed)
)
@ -86,7 +89,10 @@ export function AgentSkillSetupPanel({
() => (getPrerequisiteStatus ?? window.api.cli.getInstallStatus)(),
[getPrerequisiteStatus]
)
const actionLabel = installed && preInstallNoticeVisible ? installLabel : installedInstallLabel
const activeCommand = installed ? (installedCommand ?? command) : command
// Why: the inline terminal auto-inserts when its command changes, so keep an
// already-open terminal pinned to the command selected by the user's click.
const openTerminalCommand = terminalCommand ?? activeCommand
useEffect(() => {
if (!preInstallNotice) {
@ -132,22 +138,19 @@ export function AgentSkillSetupPanel({
}
}
const copyInstallCommand = async (): Promise<void> => {
const copyActiveCommand = async (): Promise<void> => {
try {
await window.api.ui.writeClipboardText(command)
await window.api.ui.writeClipboardText(openTerminalCommand)
toast.success(
translate(
'auto.components.settings.AgentSkillSetupPanel.378ad26865',
'Copied install command.'
)
translate('auto.components.settings.AgentSkillSetupPanel.copiedCommand', 'Copied command.')
)
} catch (error) {
toast.error(
error instanceof Error
? error.message
: translate(
'auto.components.settings.AgentSkillSetupPanel.a31e2aa302',
'Failed to copy install command.'
'auto.components.settings.AgentSkillSetupPanel.failedToCopyCommand',
'Failed to copy command.'
)
)
}
@ -161,12 +164,14 @@ export function AgentSkillSetupPanel({
variant="outline"
size="sm"
onClick={() => {
const nextCommand = activeCommand
void (async () => {
try {
await onBeforeOpenTerminal?.()
await refreshPreInstallNotice()
} finally {
if (mountedRef.current) {
setTerminalCommand(nextCommand)
setTerminalOpen(true)
}
}
@ -175,7 +180,7 @@ export function AgentSkillSetupPanel({
disabled={terminalOpen || installDisabled}
>
<Terminal className="size-3.5" />
{installed ? actionLabel : installLabel}
{installed ? installedInstallLabel : installLabel}
</Button>
) : null}
{!installed || showRecheckWhenInstalled ? (
@ -274,7 +279,7 @@ export function AgentSkillSetupPanel({
>
<div className="flex min-w-0 max-w-full items-center gap-2 overflow-hidden rounded-md border border-border bg-muted/35 px-3 py-2">
<code className="scrollbar-sleek min-w-0 flex-1 overflow-x-auto whitespace-nowrap font-mono text-xs text-muted-foreground">
{command}
{openTerminalCommand}
</code>
<Tooltip>
<TooltipTrigger asChild>
@ -284,10 +289,10 @@ export function AgentSkillSetupPanel({
size="icon-sm"
className="shrink-0"
aria-label={translate(
'auto.components.settings.AgentSkillSetupPanel.817d3f9f18',
'Copy install command'
'auto.components.settings.AgentSkillSetupPanel.copyCommandAria',
'Copy command'
)}
onClick={() => void copyInstallCommand()}
onClick={() => void copyActiveCommand()}
>
<Copy className="size-4" />
</Button>
@ -302,11 +307,11 @@ export function AgentSkillSetupPanel({
</div>
<OnboardingInlineCommandTerminal
worktreeId={terminalWorktreeId}
command={command}
command={openTerminalCommand}
title={terminalTitle}
description={translate(
'auto.components.settings.AgentSkillSetupPanel.0b810ec59f',
'Press Enter to run the install command.'
'auto.components.settings.AgentSkillSetupPanel.runCommandDescription',
'Press Enter to run the command.'
)}
ariaLabel={terminalAriaLabel}
terminalHeightPx={terminalHeightPx}

View File

@ -3,7 +3,8 @@ import { toast } from 'sonner'
import type { CliInstallStatus } from '../../../../shared/cli-install-types'
import {
ORCA_CLI_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_NAME
ORCA_CLI_SKILL_NAME,
ORCA_CLI_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
@ -30,7 +31,7 @@ import { BrowserUseSkillStep } from './BrowserUseSkillStep'
import { BrowserUseCliStep } from './BrowserUseCliStep'
import { BrowserUseCookieImportStep } from './BrowserUseCookieImportStep'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from './CliSkillRuntimeSetup'
@ -57,11 +58,12 @@ export function BrowserUseSetup({
const activeSkillRuntime = useActiveProjectSkillRuntime()
const browserUseInstallCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillInstallCommandForRuntime(
ORCA_CLI_SKILL_INSTALL_COMMAND,
activeSkillRuntime.agentRuntime
)
? buildSkillCommandForRuntime(ORCA_CLI_SKILL_INSTALL_COMMAND, activeSkillRuntime.agentRuntime)
: ORCA_CLI_SKILL_INSTALL_COMMAND
const browserUseUpdateCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillCommandForRuntime(ORCA_CLI_SKILL_UPDATE_COMMAND, activeSkillRuntime.agentRuntime)
: ORCA_CLI_SKILL_UPDATE_COMMAND
const handleCliStatusChange = useCallback(
(nextStatus: CliInstallStatus | null): void => {
@ -275,6 +277,7 @@ export function BrowserUseSetup({
>
<BrowserUseSkillStep
command={browserUseInstallCommand}
installedCommand={browserUseUpdateCommand}
skillDetected={skillDetected}
skillLoading={skillLoading}
skillError={activeSkillRuntime.installDisabledReason ?? skillError}

View File

@ -0,0 +1,41 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { BrowserUseSkillStep } from './BrowserUseSkillStep'
const capturedPanel = vi.hoisted(() => ({
props: null as null | Record<string, unknown>
}))
vi.mock('./AgentSkillSetupPanel', () => ({
AgentSkillSetupPanel: (props: Record<string, unknown>) => {
capturedPanel.props = props
return <div data-testid="browser-use-skill-step" />
}
}))
describe('BrowserUseSkillStep', () => {
it('forwards a single-skill installed command even when setup installs a bundle', () => {
const bundleInstallCommand =
'npx skills add https://github.com/stablyai/orca --skill orca-cli orchestration --global'
const updateCommand = 'npx skills update orca-cli --global'
renderToStaticMarkup(
<BrowserUseSkillStep
command={bundleInstallCommand}
installedCommand={updateCommand}
skillDetected
skillLoading={false}
skillError={null}
onRecheck={vi.fn()}
/>
)
expect(capturedPanel.props).toEqual(
expect.objectContaining({
command: bundleInstallCommand,
installedCommand: updateCommand,
installed: true
})
)
})
})

View File

@ -5,6 +5,7 @@ import { translate } from '@/i18n/i18n'
type Props = {
command: string
installedCommand: string
skillDetected: boolean
skillLoading: boolean
skillError: string | null
@ -18,6 +19,7 @@ type Props = {
export function BrowserUseSkillStep({
command,
installedCommand,
skillDetected,
skillLoading,
skillError,
@ -40,6 +42,7 @@ export function BrowserUseSkillStep({
"Enables agents to navigate and verify pages in Orca's browser."
)}
command={command}
installedCommand={installedCommand}
terminalTitle="Browser Use setup"
terminalAriaLabel="Browser Use skill install terminal"
terminalWorktreeId="settings-browser-use-skill-terminal"

View File

@ -1,10 +1,10 @@
import { useCallback, useMemo } from 'react'
import type { CliInstallStatus } from '../../../../shared/cli-install-types'
import type { SkillDiscoveryTarget } from '../../../../shared/skills'
import type { GlobalSettings } from '../../../../shared/types'
import {
ORCA_CLI_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_NAME
ORCA_CLI_SKILL_NAME,
ORCA_CLI_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
@ -17,10 +17,11 @@ import {
} from '@/hooks/useInstalledAgentSkills'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getAgentSkillTerminalShellOverride,
getSelectedAgentRuntime,
getSkillDiscoveryTargetForRuntime,
getWslCliDistroRequest
} from './CliSkillRuntimeSetup'
import { Label } from '../ui/label'
@ -48,12 +49,9 @@ export function CliAgentSkillSetup({
getSelectedAgentRuntime(settings, wslSupportedPlatform, wslAvailable, wslCapabilitiesLoading),
[settings, wslAvailable, wslCapabilitiesLoading, wslSupportedPlatform]
)
const cliSkillDiscoveryTarget = useMemo<SkillDiscoveryTarget | undefined>(
() =>
agentRuntime.runtime === 'wsl'
? { runtime: 'wsl', wslDistro: agentRuntime.wslDistro }
: undefined,
[agentRuntime.runtime, agentRuntime.wslDistro]
const cliSkillDiscoveryTarget = useMemo(
() => getSkillDiscoveryTargetForRuntime(agentRuntime),
[agentRuntime]
)
const {
installed: cliSkillDetected,
@ -64,10 +62,14 @@ export function CliAgentSkillSetup({
discoveryTarget: cliSkillDiscoveryTarget,
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
const cliSkillInstallCommand = buildSkillInstallCommandForRuntime(
const cliSkillInstallCommand = buildSkillCommandForRuntime(
ORCA_CLI_SKILL_INSTALL_COMMAND,
agentRuntime
)
const cliSkillUpdateCommand = buildSkillCommandForRuntime(
ORCA_CLI_SKILL_UPDATE_COMMAND,
agentRuntime
)
const cliSkillTerminalShellOverride = getAgentSkillTerminalShellOverride(
currentPlatform,
settings,
@ -102,6 +104,7 @@ export function CliAgentSkillSetup({
'Enables agents to use Orca workspace, terminal, and progress commands.'
)}
command={cliSkillInstallCommand}
installedCommand={cliSkillUpdateCommand}
terminalTitle={translate(
'auto.components.settings.CliSection.cliSkillTerminalTitle',
'CLI skill setup'

View File

@ -5,23 +5,30 @@ import { CliSection } from './CliSection'
const capturedPanel = vi.hoisted(() => ({
props: null as null | {
command: string
installedCommand: string
getPrerequisiteStatus: () => Promise<unknown>
onBeforeOpenTerminal: () => Promise<void>
}
},
useInstalledAgentSkill: vi.fn()
}))
vi.mock('@/hooks/useInstalledAgentSkills', () => ({
GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['global'],
useInstalledAgentSkill: () => ({
installed: false,
loading: false,
error: null,
refresh: vi.fn()
})
useInstalledAgentSkill: capturedPanel.useInstalledAgentSkill
}))
capturedPanel.useInstalledAgentSkill.mockReturnValue({
installed: false,
loading: false,
error: null,
refresh: vi.fn()
})
vi.mock('./AgentSkillSetupPanel', () => ({
AgentSkillSetupPanel: function AgentSkillSetupPanel(props: {
command: string
installedCommand: string
getPrerequisiteStatus: () => Promise<unknown>
onBeforeOpenTerminal: () => Promise<void>
}) {
@ -75,6 +82,17 @@ describe('CliSection project runtime defaults', () => {
await capturedPanel.props?.getPrerequisiteStatus()
await capturedPanel.props?.onBeforeOpenTerminal()
expect(capturedPanel.useInstalledAgentSkill).toHaveBeenCalledWith(
'orca-cli',
expect.objectContaining({
discoveryTarget: { runtime: 'wsl', wslDistro: 'Ubuntu' },
sourceKinds: ['global']
})
)
expect(capturedPanel.props?.command).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(capturedPanel.props?.command).toContain('npx skills add')
expect(capturedPanel.props?.installedCommand).toContain("wsl.exe -d 'Ubuntu' -- sh -c")
expect(capturedPanel.props?.installedCommand).toContain('npx skills update orca-cli --global')
expect(getWslInstallStatus).toHaveBeenCalledWith({ distro: 'Ubuntu' })
expect(getWslInstallStatus).toHaveBeenCalledTimes(2)
})

View File

@ -2,11 +2,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { FolderOpen, RefreshCw } from 'lucide-react'
import { toast } from 'sonner'
import type { CliInstallStatus } from '../../../../shared/cli-install-types'
import type { SkillDiscoveryTarget } from '../../../../shared/skills'
import type { GlobalSettings } from '../../../../shared/types'
import {
ORCA_CLI_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_NAME
ORCA_CLI_SKILL_NAME,
ORCA_CLI_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
@ -24,10 +24,11 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import { CliRegistrationDialog } from './CliRegistrationDialog'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getAgentSkillTerminalShellOverride,
getSelectedAgentRuntime,
getSkillDiscoveryTargetForRuntime,
getWslCliDistroRequest
} from './CliSkillRuntimeSetup'
import { WslCliRegistration } from './WslCliRegistration'
@ -85,9 +86,9 @@ export function CliSection({
getSelectedAgentRuntime(settings, wslSupportedPlatform, wslAvailable, wslCapabilitiesLoading),
[settings, wslAvailable, wslCapabilitiesLoading, wslSupportedPlatform]
)
const cliSkillDiscoveryTarget = useMemo<SkillDiscoveryTarget | undefined>(
() => (agentRuntime.runtime === 'wsl' ? { runtime: 'wsl' } : undefined),
[agentRuntime.runtime]
const cliSkillDiscoveryTarget = useMemo(
() => getSkillDiscoveryTargetForRuntime(agentRuntime),
[agentRuntime]
)
const {
installed: cliSkillDetected,
@ -98,10 +99,14 @@ export function CliSection({
discoveryTarget: cliSkillDiscoveryTarget,
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
const cliSkillInstallCommand = buildSkillInstallCommandForRuntime(
const cliSkillInstallCommand = buildSkillCommandForRuntime(
ORCA_CLI_SKILL_INSTALL_COMMAND,
agentRuntime
)
const cliSkillUpdateCommand = buildSkillCommandForRuntime(
ORCA_CLI_SKILL_UPDATE_COMMAND,
agentRuntime
)
const cliSkillTerminalShellOverride = getAgentSkillTerminalShellOverride(
currentPlatform,
settings,
@ -366,6 +371,7 @@ export function CliSection({
'Enables agents to use Orca workspace, terminal, and progress commands.'
)}
command={cliSkillInstallCommand}
installedCommand={cliSkillUpdateCommand}
terminalTitle="CLI skill setup"
terminalAriaLabel="CLI skill install terminal"
terminalWorktreeId={`settings-cli-skill-terminal-${agentRuntime.runtime}`}

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import {
buildSkillCommandForRuntime,
buildSkillInstallCommandForRuntime,
getSelectedAgentRuntime,
getSkillDiscoveryTargetForRuntime
@ -19,6 +20,18 @@ describe('CliSkillRuntimeSetup runtime helpers', () => {
expect(command).toContain('npx skills add orchestration --global')
})
it('wraps WSL skill updates with the same selected distro login shell', () => {
const command = buildSkillCommandForRuntime('npx skills update orchestration --global', {
runtime: 'wsl',
wslDistro: 'Fedora Remix',
label: 'WSL Fedora Remix'
})
expect(command).toContain("wsl.exe -d 'Fedora Remix' -- sh -c")
expect(command).toContain('getent passwd')
expect(command).toContain('npx skills update orchestration --global')
})
it('preserves the selected WSL distro for skill discovery', () => {
expect(
getSkillDiscoveryTargetForRuntime({

View File

@ -62,10 +62,7 @@ export function getWslCliDistroRequest(
: undefined
}
export function buildSkillInstallCommandForRuntime(
command: string,
runtime: LocalAgentRuntime
): string {
export function buildSkillCommandForRuntime(command: string, runtime: LocalAgentRuntime): string {
if (runtime.runtime !== 'wsl') {
return command
}
@ -76,6 +73,13 @@ export function buildSkillInstallCommandForRuntime(
return `wsl.exe${distroArg} -- sh -c ${quotePowerShellSingle(wslCommand)}`
}
export function buildSkillInstallCommandForRuntime(
command: string,
runtime: LocalAgentRuntime
): string {
return buildSkillCommandForRuntime(command, runtime)
}
export function getSkillDiscoveryTargetForRuntime(
runtime: LocalAgentRuntime
): { runtime: 'wsl'; wslDistro?: string | null } | undefined {

View File

@ -1,7 +1,8 @@
import { MonitorCog } from 'lucide-react'
import {
COMPUTER_USE_SKILL_INSTALL_COMMAND,
COMPUTER_USE_SKILL_NAME
COMPUTER_USE_SKILL_NAME,
COMPUTER_USE_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
@ -15,7 +16,7 @@ import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRunti
import { useAppStore } from '@/store'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from './CliSkillRuntimeSetup'
@ -25,11 +26,18 @@ export function ComputerUseSkillSetupPanel(): React.JSX.Element {
const activeSkillRuntime = useActiveProjectSkillRuntime()
const installCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillInstallCommandForRuntime(
? buildSkillCommandForRuntime(
COMPUTER_USE_SKILL_INSTALL_COMMAND,
activeSkillRuntime.agentRuntime
)
: COMPUTER_USE_SKILL_INSTALL_COMMAND
const updateCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillCommandForRuntime(
COMPUTER_USE_SKILL_UPDATE_COMMAND,
activeSkillRuntime.agentRuntime
)
: COMPUTER_USE_SKILL_UPDATE_COMMAND
const {
installed: computerUseSkillDetected,
loading: computerUseSkillLoading,
@ -48,6 +56,7 @@ export function ComputerUseSkillSetupPanel(): React.JSX.Element {
'Enables agents to inspect and operate local desktop apps.'
)}
command={installCommand}
installedCommand={updateCommand}
terminalTitle="Computer Use setup"
terminalAriaLabel="Computer Use skill install terminal"
terminalWorktreeId="settings-computer-use-skill-terminal"

View File

@ -1,5 +1,8 @@
import { Import, Loader2 } from 'lucide-react'
import { ORCA_CLI_SKILL_INSTALL_COMMAND } from '@/lib/agent-feature-install-commands'
import {
ORCA_CLI_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
@ -128,6 +131,7 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element {
'Enables agents to use Orca CLI commands, including mobile emulator control.'
)}
command={ORCA_CLI_SKILL_INSTALL_COMMAND}
installedCommand={ORCA_CLI_SKILL_UPDATE_COMMAND}
terminalTitle="Orca CLI skill setup"
terminalAriaLabel="Orca CLI skill install terminal"
terminalWorktreeId="settings-mobile-emulator-orca-cli-skill-terminal"

View File

@ -1,12 +1,57 @@
// @vitest-environment happy-dom
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getOrchestrationUsageExamples } from '@/lib/orchestration-usage-examples'
import { OrchestrationPane } from './OrchestrationPane'
const INSTALL_COMMAND =
'npx skills add https://github.com/stablyai/orca --skill orchestration --global'
const UPDATE_COMMAND = 'npx skills update orchestration --global'
const mocks = vi.hoisted(() => ({
dialogProps: [] as Record<string, unknown>[],
panelProps: [] as Record<string, unknown>[],
skillInstalled: true
}))
vi.mock('./AgentSkillSetupPanel', () => ({
AgentSkillSetupPanel: (
props: Record<string, unknown> & { actionHint?: ReactNode; footer?: ReactNode }
) => {
mocks.panelProps.push(props)
return (
<section>
<h3>{String(props.title)}</h3>
<span>{props.installed ? 'Installed' : 'Not installed'}</span>
<code>{String(props.command)}</code>
<code>{String(props.installedCommand)}</code>
<button type="button">{props.installed ? 'Update' : 'Install'}</button>
<button type="button">Re-check</button>
{props.actionHint}
{props.footer}
</section>
)
}
}))
vi.mock('./OrchestrationSkillPromptDialog', () => ({
OrchestrationSkillPromptDialog: (props: Record<string, unknown>) => {
mocks.dialogProps.push(props)
return props.open ? (
<div data-testid="orchestration-skill-prompt-dialog">
<code>{String(props.command)}</code>
</div>
) : null
}
}))
vi.mock('@/hooks/useInstalledAgentSkills', () => ({
GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['home'],
useInstalledAgentSkill: () => ({
installed: true,
installed: mocks.skillInstalled,
loading: false,
error: null,
skills: [
@ -38,14 +83,42 @@ vi.mock('@/hooks/useDetectedAgents', () => ({
})
}))
let root: Root | null = null
let container: HTMLDivElement | null = null
async function renderPane(): Promise<HTMLDivElement> {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
await act(async () => {
root?.render(<OrchestrationPane />)
})
return container
}
describe('OrchestrationPane', () => {
afterEach(async () => {
if (root) {
await act(async () => {
root?.unmount()
})
}
root = null
container?.remove()
container = null
mocks.dialogProps.length = 0
mocks.panelProps.length = 0
mocks.skillInstalled = true
})
it('keeps skill setup visible after install and shows agent coverage plus examples', () => {
const markup = renderToStaticMarkup(<OrchestrationPane />)
expect(markup).toContain('Orchestration skill')
expect(markup).toContain('Installed')
expect(markup).toContain('Agent coverage')
expect(markup).toContain('Copy install command')
expect(markup).not.toContain('Prefer your own terminal?')
expect(markup).not.toContain('Copy update command')
expect(markup).toContain('detected agents')
expect(markup).toContain('Gemini')
expect(markup).toContain('Ready')
@ -59,4 +132,52 @@ describe('OrchestrationPane', () => {
expect(markup).toMatch(/<button\b[^>]*>[\s\S]*?Update[\s\S]*?<\/button>/)
expect(markup).toContain('Re-check')
})
it('passes update commands to the main panel without an installed manual-copy path', async () => {
const rendered = await renderPane()
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({
command: INSTALL_COMMAND,
installedCommand: UPDATE_COMMAND
})
)
expect(rendered.textContent).not.toContain('Prefer your own terminal?')
expect(rendered.textContent).not.toContain('Copy update command')
expect(rendered.textContent).not.toContain('Copy install command')
expect(mocks.dialogProps).not.toContainEqual(expect.objectContaining({ mode: 'update' }))
expect(mocks.dialogProps).not.toContainEqual(
expect.objectContaining({
command: UPDATE_COMMAND,
open: true
})
)
})
it('keeps first-time manual copy on the install command', async () => {
mocks.skillInstalled = false
const rendered = await renderPane()
expect(rendered.textContent).toContain('Prefer your own terminal?')
expect(rendered.textContent).toContain('Copy install command')
expect(rendered.textContent).not.toContain('Copy update command')
const copyButton = Array.from(rendered.querySelectorAll('button')).find(
(button) => button.textContent === 'Copy install command'
)
expect(copyButton).toBeDefined()
await act(async () => {
copyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(mocks.dialogProps.at(-1)).toEqual(
expect.objectContaining({
command: INSTALL_COMMAND,
open: true
})
)
expect(rendered.textContent).toContain(INSTALL_COMMAND)
})
})

View File

@ -5,7 +5,10 @@ import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
} from '@/lib/agent-skill-cli-prerequisite'
import { ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/orchestration-install-command'
import {
ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from '@/lib/orchestration-install-command'
import { getOrchestrationUsageExamples } from '@/lib/orchestration-usage-examples'
import {
GLOBAL_AGENT_SKILL_SOURCE_KINDS,
@ -18,7 +21,7 @@ import { useAppStore } from '../../store'
import { getOrchestrationPaneSearchEntries } from './orchestration-search'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from './CliSkillRuntimeSetup'
@ -43,11 +46,18 @@ export function OrchestrationPane(): React.JSX.Element {
const activeSkillRuntime = useActiveProjectSkillRuntime()
const orchestrationInstallCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillInstallCommandForRuntime(
? buildSkillCommandForRuntime(
ORCHESTRATION_SKILL_INSTALL_COMMAND,
activeSkillRuntime.agentRuntime
)
: ORCHESTRATION_SKILL_INSTALL_COMMAND
const orchestrationUpdateCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillCommandForRuntime(
ORCHESTRATION_SKILL_UPDATE_COMMAND,
activeSkillRuntime.agentRuntime
)
: ORCHESTRATION_SKILL_UPDATE_COMMAND
const {
installed: orchestrationSkillDetected,
@ -87,6 +97,7 @@ export function OrchestrationPane(): React.JSX.Element {
'Enables agents to hand off context and coordinate work through Orca.'
)}
command={orchestrationInstallCommand}
installedCommand={orchestrationUpdateCommand}
terminalTitle="Orchestration setup"
terminalAriaLabel="Orchestration skill install terminal"
terminalWorktreeId="settings-orchestration-skill-terminal"
@ -111,7 +122,8 @@ export function OrchestrationPane(): React.JSX.Element {
: ensureOrcaCliAvailableForAgentSkillTerminal())
}}
actionHint={
activeSkillRuntime.installDisabledReason ? null : (
// Installed updates stay on the primary panel so there is only one update path.
activeSkillRuntime.installDisabledReason || orchestrationSkillDetected ? null : (
<p className="text-[12px] leading-snug text-muted-foreground">
{translate(
'auto.components.settings.OrchestrationPane.832f1f3ee6',
@ -120,7 +132,9 @@ export function OrchestrationPane(): React.JSX.Element {
<button
type="button"
className="font-medium text-foreground underline-offset-2 hover:underline"
onClick={() => setSkillPromptOpen(true)}
onClick={() => {
setSkillPromptOpen(true)
}}
>
{translate(
'auto.components.settings.OrchestrationPane.7bc082f4de',

View File

@ -3,12 +3,15 @@ import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
} from '@/lib/agent-skill-cli-prerequisite'
import { ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/orchestration-install-command'
import {
ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from '@/lib/orchestration-install-command'
import type { InstalledAgentSkillState } from '@/hooks/useInstalledAgentSkills'
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from './CliSkillRuntimeSetup'
@ -24,11 +27,18 @@ export function OrchestrationSetupCard(props: {
const activeSkillRuntime = useActiveProjectSkillRuntime()
const installCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillInstallCommandForRuntime(
? buildSkillCommandForRuntime(
ORCHESTRATION_SKILL_INSTALL_COMMAND,
activeSkillRuntime.agentRuntime
)
: ORCHESTRATION_SKILL_INSTALL_COMMAND
const updateCommand =
activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason
? buildSkillCommandForRuntime(
ORCHESTRATION_SKILL_UPDATE_COMMAND,
activeSkillRuntime.agentRuntime
)
: ORCHESTRATION_SKILL_UPDATE_COMMAND
const setupPanel = (
<AgentSkillSetupPanel
@ -42,6 +52,7 @@ export function OrchestrationSetupCard(props: {
'Enables agents to hand off context and coordinate work through Orca.'
)}
command={installCommand}
installedCommand={updateCommand}
terminalTitle="Orchestration setup"
terminalAriaLabel="Orchestration skill install terminal"
terminalWorktreeId="feature-wall-orchestration-skill-terminal"

View File

@ -0,0 +1,139 @@
import { readdirSync, readFileSync, statSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const repoRoot = path.resolve(fileURLToPath(new URL('../../../../../', import.meta.url)))
const componentsRoot = path.join(repoRoot, 'src/renderer/src/components')
const updateCapableCallers = new Map<string, readonly string[]>([
[
'src/renderer/src/components/settings/OrchestrationPane.tsx',
['ORCHESTRATION_SKILL_UPDATE_COMMAND', 'installedCommand={orchestrationUpdateCommand}']
],
[
'src/renderer/src/components/settings/OrchestrationSetupCard.tsx',
['ORCHESTRATION_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
],
[
'src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx',
['ORCHESTRATION_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
],
[
'src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx',
['COMPUTER_USE_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
],
[
'src/renderer/src/components/settings/CliAgentSkillSetup.tsx',
['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={cliSkillUpdateCommand}']
],
[
'src/renderer/src/components/settings/CliSection.tsx',
['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={cliSkillUpdateCommand}']
],
[
'src/renderer/src/components/settings/BrowserUsePane.tsx',
['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={browserUseUpdateCommand}']
],
[
'src/renderer/src/components/settings/BrowserUseSkillStep.tsx',
['installedCommand={installedCommand}']
],
[
'src/renderer/src/components/feature-wall/BrowserUseSkillSetupCard.tsx',
['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}']
],
[
'src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx',
['LINEAR_TICKETS_SKILL_UPDATE_COMMAND', 'installedCommand={installedCommand}']
],
[
'src/renderer/src/components/sidebar/LinearAgentSkillSetupDialog.tsx',
['installedCommand={installedCommand}']
],
[
'src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx',
['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={ORCA_CLI_SKILL_UPDATE_COMMAND}']
]
])
const installOnlyCallers = new Map<string, readonly string[]>([
[
'src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx',
['showInstallWhenInstalled={!setup.cliSkillInstalled}']
]
])
const directPanelCallers = new Set([
// BrowserUsePane and LinearAgentSkillSetupPrompt delegate through child setup
// components that forward installedCommand and are validated separately above.
...[...updateCapableCallers.keys()].filter(
(relativePath) =>
relativePath !== 'src/renderer/src/components/settings/BrowserUsePane.tsx' &&
relativePath !== 'src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx'
),
...installOnlyCallers.keys()
])
function relativeRepoPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join('/')
}
function readRepoFile(relativePath: string): string {
return readFileSync(path.join(repoRoot, relativePath), 'utf8')
}
function findProductionPanelCallers(dir: string): string[] {
const found: string[] = []
for (const entry of readdirSync(dir)) {
const entryPath = path.join(dir, entry)
const stat = statSync(entryPath)
if (stat.isDirectory()) {
found.push(...findProductionPanelCallers(entryPath))
continue
}
if (!entryPath.endsWith('.tsx') || entryPath.includes('.test.')) {
continue
}
const source = readFileSync(entryPath, 'utf8')
if (source.includes('<AgentSkillSetupPanel')) {
found.push(relativeRepoPath(entryPath))
}
}
return found.sort()
}
describe('AgentSkillSetupPanel installed-command call sites', () => {
it('keeps every update-capable production caller on an explicit single-skill update command', () => {
for (const [relativePath, expectedSnippets] of updateCapableCallers) {
const source = readRepoFile(relativePath)
for (const snippet of expectedSnippets) {
expect(source, `${relativePath} should include ${snippet}`).toContain(snippet)
}
}
})
it('keeps orchestration installed updates on the primary panel only', () => {
const source = readRepoFile('src/renderer/src/components/settings/OrchestrationPane.tsx')
expect(source).toContain('installedCommand={orchestrationUpdateCommand}')
expect(source).not.toContain('Copy update command')
expect(source).not.toContain('copyUpdateCommand')
})
it('fails when a production caller can show the default Update action without installedCommand', () => {
const productionCallers = findProductionPanelCallers(componentsRoot)
expect(productionCallers).toEqual([...directPanelCallers].sort())
for (const [relativePath, expectedSnippets] of installOnlyCallers) {
const source = readRepoFile(relativePath)
expect(source, `${relativePath} intentionally hides the installed action`).not.toContain(
'installedCommand='
)
for (const snippet of expectedSnippets) {
expect(source, `${relativePath} should include ${snippet}`).toContain(snippet)
}
}
})
})

View File

@ -25,6 +25,7 @@ type LinearAgentSkillSetupDialogProps = {
successDescription: string
missingLabel: string
command: string
installedCommand: string
terminalShellOverride?: string
installed: boolean
loading: boolean
@ -44,6 +45,7 @@ export function LinearAgentSkillSetupDialog({
successDescription,
missingLabel,
command,
installedCommand,
terminalShellOverride,
installed,
loading,
@ -124,6 +126,7 @@ export function LinearAgentSkillSetupDialog({
)}
description={missingLabel}
command={command}
installedCommand={installedCommand}
terminalTitle={translate(
'auto.components.sidebar.LinearAgentSkillSetupPrompt.terminalTitle',
'Install Linear agent skill'

View File

@ -49,7 +49,7 @@ vi.mock('@/lib/agent-skill-cli-prerequisite', () => ({
}))
vi.mock('../settings/CliSkillRuntimeSetup', () => ({
buildSkillInstallCommandForRuntime: (
buildSkillCommandForRuntime: (
command: string,
runtime: { runtime: string; wslDistro?: string | null }
) =>

View File

@ -64,7 +64,7 @@ vi.mock('@/lib/agent-skill-cli-prerequisite', () => ({
}))
vi.mock('../settings/CliSkillRuntimeSetup', () => ({
buildSkillInstallCommandForRuntime: (
buildSkillCommandForRuntime: (
command: string,
runtime: { runtime: string; wslDistro?: string | null }
) =>
@ -348,6 +348,8 @@ describe('LinearAgentSkillSetupPrompt', () => {
expect(document.body.textContent).toContain("wsl.exe -d 'Fedora' -- bash -lc 'npx skills add")
expect(mocks.panelProps.at(-1)).toEqual(
expect.objectContaining({
installedCommand:
"wsl.exe -d 'Fedora' -- bash -lc 'npx skills update linear-tickets --global'",
terminalShellOverride: 'powershell.exe',
getPrerequisiteStatus: expect.any(Function)
})

View File

@ -8,8 +8,9 @@ import {
useInstalledAgentSkill
} from '@/hooks/useInstalledAgentSkills'
import {
LINEAR_TICKETS_SKILL_INSTALL_COMMAND,
LINEAR_TICKETS_SKILL_NAME,
buildAgentFeatureSkillInstallCommand
LINEAR_TICKETS_SKILL_UPDATE_COMMAND
} from '@/lib/agent-feature-install-commands'
import {
ensureOrcaCliAvailableForAgentSkillTerminal,
@ -17,7 +18,7 @@ import {
} from '@/lib/agent-skill-cli-prerequisite'
import { cn } from '@/lib/utils'
import {
buildSkillInstallCommandForRuntime,
buildSkillCommandForRuntime,
ensureWslCliAvailableForAgentSkillTerminal,
getWslCliDistroRequest
} from '../settings/CliSkillRuntimeSetup'
@ -110,11 +111,11 @@ export function LinearAgentSkillSetupPrompt({
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
})
const command = useMemo(
() =>
buildSkillInstallCommandForRuntime(
buildAgentFeatureSkillInstallCommand([LINEAR_TICKETS_SKILL_NAME]),
agentRuntime
),
() => buildSkillCommandForRuntime(LINEAR_TICKETS_SKILL_INSTALL_COMMAND, agentRuntime),
[agentRuntime]
)
const installedCommand = useMemo(
() => buildSkillCommandForRuntime(LINEAR_TICKETS_SKILL_UPDATE_COMMAND, agentRuntime),
[agentRuntime]
)
const terminalShellOverride = getLinearPromptTerminalShellOverride(
@ -277,6 +278,7 @@ export function LinearAgentSkillSetupPrompt({
successDescription={successDescription}
missingLabel={missingLabel}
command={command}
installedCommand={installedCommand}
terminalShellOverride={terminalShellOverride}
installed={skill.installed}
loading={showCheckingModal || cliLoading || skill.loading}

View File

@ -4322,15 +4322,19 @@
"f97b986b7f": "wsl"
},
"AgentSkillSetupPanel": {
"0b810ec59f": "Press Enter to run the install command.",
"0b810ec59f": "Press Enter to run the command.",
"ed197f59a2": "Copy command",
"817d3f9f18": "Copy install command",
"817d3f9f18": "Copy command",
"5289300939": "Not installed",
"9fcebceb2a": "Installed",
"68a468752e": "Checking...",
"c689392435": "Re-check",
"a31e2aa302": "Failed to copy install command.",
"378ad26865": "Copied install command."
"a31e2aa302": "Failed to copy command.",
"378ad26865": "Copied command.",
"copiedCommand": "Copied command.",
"failedToCopyCommand": "Failed to copy command.",
"copyCommandAria": "Copy command",
"runCommandDescription": "Press Enter to run the command."
},
"AgentsPane": {
"d83834f5e6": "Detecting installed agents…",

View File

@ -4322,15 +4322,19 @@
"f97b986b7f": "wsl"
},
"AgentSkillSetupPanel": {
"0b810ec59f": "Presione Entrar para ejecutar el comando de instalación.",
"0b810ec59f": "Press Enter to run the command.",
"ed197f59a2": "comando copiar",
"817d3f9f18": "Copiar comando de instalación",
"817d3f9f18": "Copy command",
"5289300939": "No instalado",
"9fcebceb2a": "Instalado",
"68a468752e": "De cheques...",
"c689392435": "Vuelva a comprobar",
"a31e2aa302": "No se pudo copiar el comando de instalación.",
"378ad26865": "Comando de instalación copiado."
"a31e2aa302": "Failed to copy command.",
"378ad26865": "Copied command.",
"copiedCommand": "Copied command.",
"failedToCopyCommand": "Failed to copy command.",
"copyCommandAria": "Copy command",
"runCommandDescription": "Press Enter to run the command."
},
"AgentsPane": {
"d83834f5e6": "Detectando agents instalados…",

View File

@ -4307,15 +4307,19 @@
"f97b986b7f": "wsl"
},
"AgentSkillSetupPanel": {
"0b810ec59f": "Enter を押してインストール コマンドを実行します。",
"0b810ec59f": "Press Enter to run the command.",
"ed197f59a2": "コピーコマンド",
"817d3f9f18": "インストールコマンドのコピー",
"817d3f9f18": "Copy command",
"5289300939": "未インストール",
"9fcebceb2a": "インストール済み",
"68a468752e": "確認中...",
"c689392435": "再確認",
"a31e2aa302": "インストールコマンドのコピーに失敗しました。",
"378ad26865": "インストールコマンドをコピーしました。"
"a31e2aa302": "Failed to copy command.",
"378ad26865": "Copied command.",
"copiedCommand": "Copied command.",
"failedToCopyCommand": "Failed to copy command.",
"copyCommandAria": "Copy command",
"runCommandDescription": "Press Enter to run the command."
},
"AgentsPane": {
"d83834f5e6": "インストールされている agents を検出しています…",

View File

@ -4307,15 +4307,19 @@
"f97b986b7f": "wsl"
},
"AgentSkillSetupPanel": {
"0b810ec59f": "Enter를 눌러 설치 명령을 실행하십시오.",
"0b810ec59f": "Press Enter to run the command.",
"ed197f59a2": "명령 복사",
"817d3f9f18": "설치 명령 복사",
"817d3f9f18": "Copy command",
"5289300939": "설치되지 않음",
"9fcebceb2a": "설치됨",
"68a468752e": "확인 중...",
"c689392435": "재확인",
"a31e2aa302": "설치 명령을 복사하지 못했습니다.",
"378ad26865": "설치 명령을 복사했습니다."
"a31e2aa302": "Failed to copy command.",
"378ad26865": "Copied command.",
"copiedCommand": "Copied command.",
"failedToCopyCommand": "Failed to copy command.",
"copyCommandAria": "Copy command",
"runCommandDescription": "Press Enter to run the command."
},
"AgentsPane": {
"d83834f5e6": "설치된 agents 감지 중…",

View File

@ -4307,15 +4307,19 @@
"f97b986b7f": "wsl"
},
"AgentSkillSetupPanel": {
"0b810ec59f": "按 Enter 运行安装命令。",
"0b810ec59f": "Press Enter to run the command.",
"ed197f59a2": "复制命令",
"817d3f9f18": "复制安装命令",
"817d3f9f18": "Copy command",
"5289300939": "未安装",
"9fcebceb2a": "已安装",
"68a468752e": "检查中...",
"c689392435": "重新检查",
"a31e2aa302": "无法复制安装命令。",
"378ad26865": "复制安装命令。"
"a31e2aa302": "Failed to copy command.",
"378ad26865": "Copied command.",
"copiedCommand": "Copied command.",
"failedToCopyCommand": "Failed to copy command.",
"copyCommandAria": "Copy command",
"runCommandDescription": "Press Enter to run the command."
},
"AgentsPane": {
"d83834f5e6": "检测已安装的 Agent...",

View File

@ -1,11 +1,16 @@
export {
buildAgentFeatureSkillInstallCommand,
buildAgentFeatureSkillUpdateCommand,
COMPUTER_USE_SKILL_INSTALL_COMMAND,
COMPUTER_USE_SKILL_NAME,
COMPUTER_USE_SKILL_UPDATE_COMMAND,
LINEAR_TICKETS_SKILL_INSTALL_COMMAND,
LINEAR_TICKETS_SKILL_NAME,
LINEAR_TICKETS_SKILL_UPDATE_COMMAND,
ORCA_CLI_SKILL_INSTALL_COMMAND,
ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_NAME,
ORCHESTRATION_SKILL_NAME
ORCA_CLI_SKILL_UPDATE_COMMAND,
ORCHESTRATION_SKILL_NAME,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from '../../../shared/agent-feature-install-commands'

View File

@ -1 +1,4 @@
export { ORCHESTRATION_SKILL_INSTALL_COMMAND } from '../../../shared/agent-feature-install-commands'
export {
ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from '../../../shared/agent-feature-install-commands'

View File

@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import {
buildAgentFeatureSkillInstallCommand,
buildAgentFeatureSkillUpdateCommand,
COMPUTER_USE_SKILL_UPDATE_COMMAND,
LINEAR_TICKETS_SKILL_UPDATE_COMMAND,
ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCA_CLI_SKILL_UPDATE_COMMAND,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from './agent-feature-install-commands'
describe('agent feature skill commands', () => {
it('builds single-skill update commands', () => {
expect(buildAgentFeatureSkillUpdateCommand('orchestration')).toBe(
'npx skills update orchestration --global'
)
})
it('trims and rejects blank update skill names', () => {
expect(buildAgentFeatureSkillUpdateCommand(' orca-cli ')).toBe(
'npx skills update orca-cli --global'
)
expect(() => buildAgentFeatureSkillUpdateCommand(' ')).toThrow('A skill name is required.')
})
it('exports single-skill update constants without changing install bundles', () => {
expect(ORCA_CLI_SKILL_UPDATE_COMMAND).toBe('npx skills update orca-cli --global')
expect(COMPUTER_USE_SKILL_UPDATE_COMMAND).toBe('npx skills update computer-use --global')
expect(ORCHESTRATION_SKILL_UPDATE_COMMAND).toBe('npx skills update orchestration --global')
expect(LINEAR_TICKETS_SKILL_UPDATE_COMMAND).toBe('npx skills update linear-tickets --global')
expect(ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND).toBe(
buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration'])
)
})
})

View File

@ -12,18 +12,35 @@ export function buildAgentFeatureSkillInstallCommand(skillNames: readonly string
return `npx skills add ${ORCA_SKILLS_REPOSITORY_URL} --skill ${skillNames.join(' ')} --global`
}
export function buildAgentFeatureSkillUpdateCommand(skillName: string): string {
const trimmedSkillName = skillName.trim()
if (!trimmedSkillName) {
throw new Error('A skill name is required.')
}
return `npx skills update ${trimmedSkillName} --global`
}
export const ORCA_CLI_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
ORCA_CLI_SKILL_NAME
])
export const ORCA_CLI_SKILL_UPDATE_COMMAND =
buildAgentFeatureSkillUpdateCommand(ORCA_CLI_SKILL_NAME)
export const COMPUTER_USE_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
COMPUTER_USE_SKILL_NAME
])
export const COMPUTER_USE_SKILL_UPDATE_COMMAND =
buildAgentFeatureSkillUpdateCommand(COMPUTER_USE_SKILL_NAME)
export const ORCHESTRATION_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
ORCHESTRATION_SKILL_NAME
])
export const ORCHESTRATION_SKILL_UPDATE_COMMAND =
buildAgentFeatureSkillUpdateCommand(ORCHESTRATION_SKILL_NAME)
export const ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
ORCA_CLI_SKILL_NAME,
ORCHESTRATION_SKILL_NAME
@ -32,3 +49,6 @@ export const ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND = buildAgentFeatureSki
export const LINEAR_TICKETS_SKILL_INSTALL_COMMAND = buildAgentFeatureSkillInstallCommand([
LINEAR_TICKETS_SKILL_NAME
])
export const LINEAR_TICKETS_SKILL_UPDATE_COMMAND =
buildAgentFeatureSkillUpdateCommand(LINEAR_TICKETS_SKILL_NAME)