diff --git a/src/renderer/src/components/feature-wall/BrowserUseSkillSetupCard.tsx b/src/renderer/src/components/feature-wall/BrowserUseSkillSetupCard.tsx index dd46fb2bf..b8b1cd583 100644 --- a/src/renderer/src/components/feature-wall/BrowserUseSkillSetupCard.tsx +++ b/src/renderer/src/components/feature-wall/BrowserUseSkillSetupCard.tsx @@ -26,14 +26,12 @@ export function BrowserUseSkillSetupCard(props: { }): JSX.Element { const { compact, terminalHeightPx, skill } = props const activeSkillRuntime = useActiveProjectSkillRuntime() - const installCommand = - activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason - ? 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 installCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime(ORCA_CLI_SKILL_INSTALL_COMMAND, activeSkillRuntime.agentRuntime) + : ORCA_CLI_SKILL_INSTALL_COMMAND + const updateCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime(ORCA_CLI_SKILL_UPDATE_COMMAND, activeSkillRuntime.agentRuntime) + : ORCA_CLI_SKILL_UPDATE_COMMAND const handleBeforeOpenTerminal = async (): Promise => { useAppStore.getState().recordFeatureInteraction('agent-browser-setup') diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx index 6c39591d0..abfc314d6 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.tsx @@ -42,20 +42,18 @@ export function FloatingTerminalOrchestrationDialog({ onSetupStateChange }: FloatingTerminalOrchestrationDialogProps): React.JSX.Element { const activeSkillRuntime = useActiveProjectSkillRuntime() - const installCommand = - activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason - ? 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 installCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + ORCHESTRATION_SKILL_INSTALL_COMMAND, + activeSkillRuntime.agentRuntime + ) + : ORCHESTRATION_SKILL_INSTALL_COMMAND + const updateCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + ORCHESTRATION_SKILL_UPDATE_COMMAND, + activeSkillRuntime.agentRuntime + ) + : ORCHESTRATION_SKILL_UPDATE_COMMAND const { installed: orchestrationSkillDetected, loading: orchestrationSkillLoading, diff --git a/src/renderer/src/components/settings/BrowserUsePane.tsx b/src/renderer/src/components/settings/BrowserUsePane.tsx index 760375489..8f2dea14e 100644 --- a/src/renderer/src/components/settings/BrowserUsePane.tsx +++ b/src/renderer/src/components/settings/BrowserUsePane.tsx @@ -56,14 +56,12 @@ export function BrowserUseSetup({ const [cliBusy, setCliBusy] = useState(false) const mountedRef = useMountedRef() const activeSkillRuntime = useActiveProjectSkillRuntime() - const browserUseInstallCommand = - activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason - ? 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 browserUseInstallCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime(ORCA_CLI_SKILL_INSTALL_COMMAND, activeSkillRuntime.agentRuntime) + : ORCA_CLI_SKILL_INSTALL_COMMAND + const browserUseUpdateCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime(ORCA_CLI_SKILL_UPDATE_COMMAND, activeSkillRuntime.agentRuntime) + : ORCA_CLI_SKILL_UPDATE_COMMAND const handleCliStatusChange = useCallback( (nextStatus: CliInstallStatus | null): void => { diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx index 02c1fa659..64b7bb3dd 100644 --- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx +++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' +import { buildAgentFeatureSkillInstallCommand } from '../../../../shared/agent-feature-install-commands' import { buildSkillCommandForRuntime, buildSkillInstallCommandForRuntime, @@ -32,6 +33,38 @@ describe('CliSkillRuntimeSetup runtime helpers', () => { expect(command).toContain('npx skills update orchestration --global') }) + it('reinstalls Windows-host skill updates through the add path', () => { + expect( + buildSkillCommandForRuntime( + 'npx skills update orchestration --global', + { + runtime: 'host', + label: 'Windows' + }, + 'win32' + ) + ).toBe(buildAgentFeatureSkillInstallCommand(['orchestration'])) + }) + + it('treats missing runtime as a Windows host fallback for skill updates', () => { + expect( + buildSkillCommandForRuntime('npx skills update orca-cli --global', undefined, 'win32') + ).toBe(buildAgentFeatureSkillInstallCommand(['orca-cli'])) + }) + + it('keeps non-Windows host skill updates on the update path', () => { + expect( + buildSkillCommandForRuntime( + 'npx skills update orchestration --global', + { + runtime: 'host', + label: 'This device' + }, + 'linux' + ) + ).toBe('npx skills update orchestration --global') + }) + it('preserves the selected WSL distro for skill discovery', () => { expect( getSkillDiscoveryTargetForRuntime({ diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx index 686115955..895caa1b8 100644 --- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx +++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx @@ -7,6 +7,7 @@ import { buildWslLoginShellCommand, escapeWslShCommandForWindows } from '../../../../shared/wsl-login-shell-command' +import { buildAgentFeatureSkillInstallCommand } from '../../../../shared/agent-feature-install-commands' import { toast } from 'sonner' import type { CliInstallStatus } from '../../../../shared/cli-install-types' import { @@ -21,6 +22,11 @@ export type LocalAgentRuntime = { label: string } +const LOCAL_HOST_AGENT_RUNTIME: LocalAgentRuntime = { + runtime: 'host', + label: '' +} + export function getHostRuntimeLabel(): string { return navigator.userAgent.includes('Windows') ? 'Windows' : 'This device' } @@ -62,15 +68,64 @@ export function getWslCliDistroRequest( : undefined } -export function buildSkillCommandForRuntime(command: string, runtime: LocalAgentRuntime): string { - if (runtime.runtime !== 'wsl') { +export function buildSkillCommandForRuntime( + command: string, + runtime?: LocalAgentRuntime, + currentPlatform = getSkillCommandPlatform() +): string { + const resolvedRuntime = runtime ?? LOCAL_HOST_AGENT_RUNTIME + const normalizedCommand = normalizeWindowsSkillUpdateCommand( + command, + resolvedRuntime, + currentPlatform + ) + if (resolvedRuntime.runtime !== 'wsl') { + return normalizedCommand + } + + const distroArg = resolvedRuntime.wslDistro?.trim() + ? ` -d ${quotePowerShellSingle(resolvedRuntime.wslDistro.trim())}` + : '' + const wslCommand = escapeWslShCommandForWindows(buildWslLoginShellCommand(normalizedCommand)) + return `wsl.exe${distroArg} -- sh -c ${quotePowerShellSingle(wslCommand)}` +} + +function normalizeWindowsSkillUpdateCommand( + command: string, + runtime: LocalAgentRuntime, + currentPlatform: NodeJS.Platform +): string { + if (runtime.runtime === 'wsl' || currentPlatform !== 'win32') { return command } - const distroArg = runtime.wslDistro?.trim() - ? ` -d ${quotePowerShellSingle(runtime.wslDistro.trim())}` - : '' - const wslCommand = escapeWslShCommandForWindows(buildWslLoginShellCommand(command)) - return `wsl.exe${distroArg} -- sh -c ${quotePowerShellSingle(wslCommand)}` + + const trimmedCommand = command.trim() + const updateMatch = /^npx\s+skills\s+update\s+([A-Za-z0-9_-]+)\s+--global$/i.exec(trimmedCommand) + if (!updateMatch) { + return command + } + + // Why: the `skills update` subcommand is currently unreliable on native + // Windows, while reinstalling from the same repo source is idempotent and + // keeps the setup affordance working. + return buildAgentFeatureSkillInstallCommand([updateMatch[1]]) +} + +function getSkillCommandPlatform(): NodeJS.Platform { + const platform = + typeof window === 'undefined' ? undefined : window.api?.platform?.get?.()?.platform + if (platform) { + return platform + } + + const userAgent = typeof navigator === 'undefined' ? '' : navigator.userAgent + if (userAgent.includes('Windows')) { + return 'win32' + } + if (userAgent.includes('Mac')) { + return 'darwin' + } + return 'linux' } export function buildSkillInstallCommandForRuntime( diff --git a/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx b/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx index 46107b735..270e1785a 100644 --- a/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx +++ b/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx @@ -24,20 +24,18 @@ import { translate } from '@/i18n/i18n' export function ComputerUseSkillSetupPanel(): React.JSX.Element { const activeSkillRuntime = useActiveProjectSkillRuntime() - const installCommand = - activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason - ? 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 installCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + COMPUTER_USE_SKILL_INSTALL_COMMAND, + activeSkillRuntime.agentRuntime + ) + : COMPUTER_USE_SKILL_INSTALL_COMMAND + const updateCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + COMPUTER_USE_SKILL_UPDATE_COMMAND, + activeSkillRuntime.agentRuntime + ) + : COMPUTER_USE_SKILL_UPDATE_COMMAND const { installed: computerUseSkillDetected, loading: computerUseSkillLoading, diff --git a/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx b/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx index 17622fa18..e6c870952 100644 --- a/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx +++ b/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx @@ -10,6 +10,7 @@ import { import { cn } from '@/lib/utils' import { useMobileEmulatorAgentSetupState } from '../emulator-pane/use-mobile-emulator-agent-setup-state' import { AgentSkillSetupPanel } from './AgentSkillSetupPanel' +import { buildSkillCommandForRuntime } from './CliSkillRuntimeSetup' import { StepBadge } from './BrowserUseStepBadge' import { MobileEmulatorExamples } from './MobileEmulatorExamples' import { Button } from '../ui/button' @@ -25,6 +26,8 @@ const EMULATOR_CLI_COMMANDS = [ export function MobileEmulatorAgentControlRow(): React.JSX.Element { const setup = useMobileEmulatorAgentSetupState(true) + const cliSkillInstallCommand = buildSkillCommandForRuntime(ORCA_CLI_SKILL_INSTALL_COMMAND) + const cliSkillUpdateCommand = buildSkillCommandForRuntime(ORCA_CLI_SKILL_UPDATE_COMMAND) const handleEnableCli = async (): Promise => { await setup.handleEnableCli() @@ -130,8 +133,8 @@ export function MobileEmulatorAgentControlRow(): React.JSX.Element { 'auto.components.settings.MobileEmulatorAgentControlRow.d94ca6a623', 'Enables agents to use Orca CLI commands, including mobile emulator control.' )} - command={ORCA_CLI_SKILL_INSTALL_COMMAND} - installedCommand={ORCA_CLI_SKILL_UPDATE_COMMAND} + command={cliSkillInstallCommand} + installedCommand={cliSkillUpdateCommand} terminalTitle="Orca CLI skill setup" terminalAriaLabel="Orca CLI skill install terminal" terminalWorktreeId="settings-mobile-emulator-orca-cli-skill-terminal" diff --git a/src/renderer/src/components/settings/OrchestrationPane.test.tsx b/src/renderer/src/components/settings/OrchestrationPane.test.tsx index ed0f2f074..02da99134 100644 --- a/src/renderer/src/components/settings/OrchestrationPane.test.tsx +++ b/src/renderer/src/components/settings/OrchestrationPane.test.tsx @@ -3,13 +3,13 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, 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 UPDATE_COMMAND = INSTALL_COMMAND const mocks = vi.hoisted(() => ({ dialogProps: [] as Record[], @@ -97,6 +97,31 @@ async function renderPane(): Promise { } describe('OrchestrationPane', () => { + beforeEach(() => { + Object.defineProperty(window, 'api', { + configurable: true, + value: { + ...window.api, + platform: { + get: () => ({ platform: 'win32', osRelease: 'test' }) + }, + wsl: { + isAvailable: vi.fn().mockResolvedValue(false), + listDistros: vi.fn().mockResolvedValue([]) + }, + pwsh: { + isAvailable: vi.fn().mockResolvedValue(false) + }, + gitBash: { + isAvailable: vi.fn().mockResolvedValue(false) + }, + runtime: { + getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) + } + } + }) + }) + afterEach(async () => { if (root) { await act(async () => { diff --git a/src/renderer/src/components/settings/OrchestrationPane.tsx b/src/renderer/src/components/settings/OrchestrationPane.tsx index 460ccf3f2..59aea3347 100644 --- a/src/renderer/src/components/settings/OrchestrationPane.tsx +++ b/src/renderer/src/components/settings/OrchestrationPane.tsx @@ -44,20 +44,18 @@ export function OrchestrationPane(): React.JSX.Element { const [selectedExampleId, setSelectedExampleId] = useState(null) const [skillPromptOpen, setSkillPromptOpen] = useState(false) const activeSkillRuntime = useActiveProjectSkillRuntime() - const orchestrationInstallCommand = - activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason - ? 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 orchestrationInstallCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + ORCHESTRATION_SKILL_INSTALL_COMMAND, + activeSkillRuntime.agentRuntime + ) + : ORCHESTRATION_SKILL_INSTALL_COMMAND + const orchestrationUpdateCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + ORCHESTRATION_SKILL_UPDATE_COMMAND, + activeSkillRuntime.agentRuntime + ) + : ORCHESTRATION_SKILL_UPDATE_COMMAND const { installed: orchestrationSkillDetected, diff --git a/src/renderer/src/components/settings/OrchestrationSetupCard.tsx b/src/renderer/src/components/settings/OrchestrationSetupCard.tsx index fe005c7c0..b757261bb 100644 --- a/src/renderer/src/components/settings/OrchestrationSetupCard.tsx +++ b/src/renderer/src/components/settings/OrchestrationSetupCard.tsx @@ -25,20 +25,18 @@ export function OrchestrationSetupCard(props: { }): JSX.Element { const { compact, terminalHeightPx, skill } = props const activeSkillRuntime = useActiveProjectSkillRuntime() - const installCommand = - activeSkillRuntime.agentRuntime && !activeSkillRuntime.installDisabledReason - ? 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 installCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + ORCHESTRATION_SKILL_INSTALL_COMMAND, + activeSkillRuntime.agentRuntime + ) + : ORCHESTRATION_SKILL_INSTALL_COMMAND + const updateCommand = !activeSkillRuntime.installDisabledReason + ? buildSkillCommandForRuntime( + ORCHESTRATION_SKILL_UPDATE_COMMAND, + activeSkillRuntime.agentRuntime + ) + : ORCHESTRATION_SKILL_UPDATE_COMMAND const setupPanel = ( ([ ], [ 'src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx', - ['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={ORCA_CLI_SKILL_UPDATE_COMMAND}'] + ['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={cliSkillUpdateCommand}'] ] ])