{translate(
diff --git a/src/renderer/src/components/settings/CliSection.tsx b/src/renderer/src/components/settings/CliSection.tsx
index bb830eb63fa..eae2ce2a76b 100644
--- a/src/renderer/src/components/settings/CliSection.tsx
+++ b/src/renderer/src/components/settings/CliSection.tsx
@@ -22,7 +22,6 @@ import { Button } from '../ui/button'
import { Label } from '../ui/label'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
-import { SkillFreshnessCheckButton } from '../skills/SkillFreshnessCheckButton'
import { CliRegistrationDialog } from './CliRegistrationDialog'
import {
buildSkillCommandForRuntime,
@@ -391,9 +390,8 @@ export function CliSection({
}))
}}
onRecheck={refreshCliSkill}
+ freshnessSkillName={agentRuntime.runtime === 'host' ? ORCA_CLI_SKILL_NAME : undefined}
/>
-
-
) : null}
diff --git a/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx b/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx
index 270e1785aef..0ffc0a2b526 100644
--- a/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx
+++ b/src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx
@@ -79,6 +79,9 @@ export function ComputerUseSkillSetupPanel(): React.JSX.Element {
: ensureOrcaCliAvailableForAgentSkillTerminal())
}}
onRecheck={refreshComputerUseSkill}
+ freshnessSkillName={
+ activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : COMPUTER_USE_SKILL_NAME
+ }
/>
)
}
diff --git a/src/renderer/src/components/settings/EphemeralVmsPane.tsx b/src/renderer/src/components/settings/EphemeralVmsPane.tsx
index 85b27f12ed7..f1a34a0396f 100644
--- a/src/renderer/src/components/settings/EphemeralVmsPane.tsx
+++ b/src/renderer/src/components/settings/EphemeralVmsPane.tsx
@@ -163,6 +163,9 @@ export function EphemeralVmsPane(): React.JSX.Element {
: ensureOrcaCliAvailableForAgentSkillTerminal())
}}
onRecheck={refreshSkill}
+ freshnessSkillName={
+ activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : EPHEMERAL_VMS_SKILL_NAME
+ }
/>
diff --git a/src/renderer/src/components/settings/OrchestrationSetupCard.tsx b/src/renderer/src/components/settings/OrchestrationSetupCard.tsx
index b757261bba6..d597895d580 100644
--- a/src/renderer/src/components/settings/OrchestrationSetupCard.tsx
+++ b/src/renderer/src/components/settings/OrchestrationSetupCard.tsx
@@ -7,6 +7,7 @@ import {
ORCHESTRATION_SKILL_INSTALL_COMMAND,
ORCHESTRATION_SKILL_UPDATE_COMMAND
} from '@/lib/orchestration-install-command'
+import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands'
import type { InstalledAgentSkillState } from '@/hooks/useInstalledAgentSkills'
import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
@@ -75,6 +76,9 @@ export function OrchestrationSetupCard(props: {
: ensureOrcaCliAvailableForAgentSkillTerminal())
}}
onRecheck={skill.refresh}
+ freshnessSkillName={
+ activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : ORCHESTRATION_SKILL_NAME
+ }
/>
)
diff --git a/src/renderer/src/components/skills/SkillFreshnessCheckButton.test.tsx b/src/renderer/src/components/skills/SkillFreshnessCheckButton.test.tsx
deleted file mode 100644
index 5d93e600d2a..00000000000
--- a/src/renderer/src/components/skills/SkillFreshnessCheckButton.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-// @vitest-environment happy-dom
-
-import { act } from 'react'
-import { createRoot, type Root } from 'react-dom/client'
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { SkillFreshnessCheckButton } from './SkillFreshnessCheckButton'
-import {
- consumeSkillFreshnessUpdateDialogRequest,
- subscribeSkillFreshnessUpdateDialog
-} from './skill-freshness-update-dialog'
-
-let root: Root | null = null
-let container: HTMLDivElement | null = null
-
-async function renderButton(): Promise {
- container = document.createElement('div')
- document.body.appendChild(container)
- root = createRoot(container)
- await act(async () => {
- root?.render()
- })
-}
-
-describe('SkillFreshnessCheckButton', () => {
- beforeEach(() => {
- consumeSkillFreshnessUpdateDialogRequest()
- })
-
- afterEach(async () => {
- if (root) {
- await act(async () => root?.unmount())
- }
- root = null
- container?.remove()
- container = null
- })
-
- it('requests the update dialog when clicked', async () => {
- const listener = vi.fn()
- const unsubscribe = subscribeSkillFreshnessUpdateDialog(listener)
- await renderButton()
-
- const button = container?.querySelector('button')
- expect(button?.textContent).toContain('Check for skill updates')
- await act(async () => {
- button?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
- })
-
- expect(listener).toHaveBeenCalledTimes(1)
- expect(consumeSkillFreshnessUpdateDialogRequest()).toBe(true)
- unsubscribe()
- })
-})
diff --git a/src/renderer/src/components/skills/SkillFreshnessCheckButton.tsx b/src/renderer/src/components/skills/SkillFreshnessCheckButton.tsx
deleted file mode 100644
index e0f53a10e96..00000000000
--- a/src/renderer/src/components/skills/SkillFreshnessCheckButton.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { RefreshCw } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { translate } from '@/i18n/i18n'
-import { requestSkillFreshnessUpdateDialog } from './skill-freshness-update-dialog'
-
-// Compact re-entry point for the freshness update dialog, mounted at App root.
-// Works even when nothing is outdated: the dialog then shows the up-to-date or
-// diagnostic state.
-export function SkillFreshnessCheckButton({
- className
-}: {
- className?: string
-}): React.JSX.Element {
- return (
-
- )
-}
diff --git a/src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx b/src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx
new file mode 100644
index 00000000000..3a26115e00f
--- /dev/null
+++ b/src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx
@@ -0,0 +1,108 @@
+// @vitest-environment happy-dom
+
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { SkillFreshnessInventory } from '../../../../shared/skill-freshness'
+import { SkillFreshnessStatusPill } from './SkillFreshnessStatusPill'
+
+const mocks = vi.hoisted(() => ({
+ inventory: null as SkillFreshnessInventory | null
+}))
+
+vi.mock('@/hooks/useSkillFreshness', () => ({
+ useSkillFreshness: () => ({
+ inventory: mocks.inventory,
+ loading: false,
+ error: null,
+ refresh: vi.fn()
+ })
+}))
+
+function inventory(
+ entries: { name: string; status: 'current' | 'outdated' | 'unrecognized' }[],
+ eligibleUpdateNames: string[]
+): SkillFreshnessInventory {
+ return {
+ schemaVersion: 1,
+ installations: entries.map((entry, index) => ({
+ id: `${entry.name}-${index}`,
+ name: entry.name,
+ description: null,
+ rootId: 'home-agents',
+ providers: ['agent-skills'],
+ sourceKind: 'home',
+ sourceLabel: 'Agent skills home',
+ unresolvedPath: `/home/.agents/skills/${entry.name}`,
+ resolvedPath: `/home/.agents/skills/${entry.name}`,
+ physicalIdentity: `physical-${entry.name}-${index}`,
+ topology: 'canonical-copy',
+ status: entry.status,
+ installedReleaseRevision: 1,
+ installedAppVersion: '1.0.0',
+ currentReleaseRevision: 2,
+ currentPackageDigest: 'current',
+ currentAppVersion: '2.0.0',
+ observedPackageDigest: 'old',
+ errorCategory: null
+ })),
+ eligibleUpdateNames,
+ scannedAt: 1
+ }
+}
+
+let root: Root | null = null
+let container: HTMLDivElement | null = null
+
+async function renderPill(skillName: string): Promise {
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+ await act(async () => {
+ root?.render()
+ })
+ return container
+}
+
+describe('SkillFreshnessStatusPill', () => {
+ beforeEach(() => {
+ mocks.inventory = null
+ })
+
+ afterEach(async () => {
+ if (root) {
+ await act(async () => root?.unmount())
+ }
+ root = null
+ container?.remove()
+ container = null
+ })
+
+ it('shows Update available for an eligible outdated skill', async () => {
+ mocks.inventory = inventory([{ name: 'orca-cli', status: 'outdated' }], ['orca-cli'])
+
+ expect((await renderPill('orca-cli')).textContent).toBe('Update available')
+ })
+
+ it('shows Up to date when every placement is current', async () => {
+ mocks.inventory = inventory([{ name: 'orca-cli', status: 'current' }], [])
+
+ expect((await renderPill('orca-cli')).textContent).toBe('Up to date')
+ })
+
+ it('falls back to Installed for a blocked outdated placement', async () => {
+ mocks.inventory = inventory(
+ [
+ { name: 'orca-cli', status: 'outdated' },
+ { name: 'orca-cli', status: 'unrecognized' }
+ ],
+ []
+ )
+
+ expect((await renderPill('orca-cli')).textContent).toBe('Installed')
+ })
+
+ it('falls back to Installed before the inventory loads', async () => {
+ expect((await renderPill('orca-cli')).textContent).toBe('Installed')
+ })
+})
diff --git a/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx b/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx
new file mode 100644
index 00000000000..3c858686591
--- /dev/null
+++ b/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx
@@ -0,0 +1,40 @@
+import { useSkillFreshness } from '@/hooks/useSkillFreshness'
+import { translate } from '@/i18n/i18n'
+import { IntegrationStatusPill } from '@/components/integration-status-pill'
+
+// Why: the setup rails' Installed pill is presence-only; when freshness knows a
+// safe update exists (or that every copy is current) the pill should say so.
+// Falls back to plain Installed for blocked/unrecognized copies so an unsafe
+// placement is never advertised as updatable here.
+export function SkillFreshnessStatusPill({ skillName }: { skillName: string }): React.JSX.Element {
+ const { inventory } = useSkillFreshness()
+ if (inventory?.eligibleUpdateNames.includes(skillName)) {
+ return (
+
+ {translate(
+ 'auto.components.skills.SkillFreshnessStatusPill.updateAvailable',
+ 'Update available'
+ )}
+
+ )
+ }
+ const placements = inventory?.installations.filter(
+ (installation) => installation.name === skillName
+ )
+ if (
+ placements &&
+ placements.length > 0 &&
+ placements.every((installation) => installation.status === 'current')
+ ) {
+ return (
+
+ {translate('auto.components.skills.SkillFreshnessStatusPill.upToDate', 'Up to date')}
+
+ )
+ }
+ return (
+
+ {translate('auto.components.skills.SkillFreshnessStatusPill.installed', 'Installed')}
+
+ )
+}
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index a6b5cf9a99a..43c17063511 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -3641,8 +3641,10 @@
"checkNow": "Check now",
"close": "Close"
},
- "SkillFreshnessCheckButton": {
- "label": "Check for skill updates"
+ "SkillFreshnessStatusPill": {
+ "updateAvailable": "Update available",
+ "upToDate": "Up to date",
+ "installed": "Installed"
}
},
"sidebar": {
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 208c9e8733a..292dcb560e3 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -3641,8 +3641,10 @@
"checkNow": "Comprobar ahora",
"close": "Cerrar"
},
- "SkillFreshnessCheckButton": {
- "label": "Buscar actualizaciones de skills"
+ "SkillFreshnessStatusPill": {
+ "updateAvailable": "Actualización disponible",
+ "upToDate": "Actualizado",
+ "installed": "Instalado"
}
},
"sidebar": {
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 21bc3ca2318..029fc3f934a 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -3641,8 +3641,10 @@
"checkNow": "今すぐ確認",
"close": "閉じる"
},
- "SkillFreshnessCheckButton": {
- "label": "スキルの更新を確認"
+ "SkillFreshnessStatusPill": {
+ "updateAvailable": "更新があります",
+ "upToDate": "最新です",
+ "installed": "インストール済み"
}
},
"sidebar": {
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 6a9060b8254..973db5dbbce 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -3641,8 +3641,10 @@
"checkNow": "지금 확인",
"close": "닫기"
},
- "SkillFreshnessCheckButton": {
- "label": "스킬 업데이트 확인"
+ "SkillFreshnessStatusPill": {
+ "updateAvailable": "업데이트 가능",
+ "upToDate": "최신 상태",
+ "installed": "설치됨"
}
},
"sidebar": {
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 60133b0b16e..fd986fb5efc 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -3641,8 +3641,10 @@
"checkNow": "立即检查",
"close": "关闭"
},
- "SkillFreshnessCheckButton": {
- "label": "检查技能更新"
+ "SkillFreshnessStatusPill": {
+ "updateAvailable": "有可用更新",
+ "upToDate": "已是最新",
+ "installed": "已安装"
}
},
"sidebar": {