From 65b3f2c873a2da56a4d755a7bda49bde2d14a95b Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:51:46 -0700 Subject: [PATCH] fix(settings): require freshness proof for current badge Only label a skill Up to date when every discovered placement is current. Keep Installed for unloaded, blocked, inaccessible, or unrecognized inventories, and share the classifier with the existing freshness pill. Regenerate the current skill manifest so its app version matches the package release and the required verification gate passes. --- resources/skills/current-manifest.json | 18 ++--- .../src/components/settings/Settings.tsx | 20 +++--- .../settings/SettingsSidebar.test.tsx | 7 ++ .../skills/SkillFreshnessStatusPill.tsx | 13 ++-- .../skill-freshness-display-status.test.ts | 69 +++++++++++++++++++ .../src/lib/skill-freshness-display-status.ts | 25 +++++++ 6 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 src/renderer/src/lib/skill-freshness-display-status.test.ts create mode 100644 src/renderer/src/lib/skill-freshness-display-status.ts diff --git a/resources/skills/current-manifest.json b/resources/skills/current-manifest.json index fca1162fa5e..51cbb6af5cc 100644 --- a/resources/skills/current-manifest.json +++ b/resources/skills/current-manifest.json @@ -1,11 +1,11 @@ { "schemaVersion": 1, - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "skills": [ { "name": "computer-use", "sourcePath": "skills/computer-use", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 5, "packageDigest": "cd2809474d57fd7277adb277448e6fa446810d3cbad71ac0b473b9e8ff1bad68", "gitTreeSha": "306c0f8cb63bcac265a5b7975dc2f855be4f1344", @@ -24,7 +24,7 @@ { "name": "linear-tickets", "sourcePath": "skills/linear-tickets", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 4, "packageDigest": "f198d7b22e5ee1673dac403f9cca0553b124e0a90e4fdd05d2c23b7344e32d2b", "gitTreeSha": "de9fc106bbb4e313a90ff9a9513a720909bbd176", @@ -43,7 +43,7 @@ { "name": "orca-cli", "sourcePath": "skills/orca-cli", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 32, "packageDigest": "51740ff13f379ac5743d3fd28a14b17168dcef40f7048c20182dce166098c45f", "gitTreeSha": "ded93000a5f654e2b4f324501282459bd56afe19", @@ -62,7 +62,7 @@ { "name": "orca-emulator", "sourcePath": "skills/orca-emulator", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 4, "packageDigest": "453b1d9aa20b51b8a4d32c7b6def6a93f7ef9c730de32abbcbc1788ad1b1820b", "gitTreeSha": "66be6abe99f1807da85934aee0e22daefc8f7656", @@ -81,7 +81,7 @@ { "name": "orca-emulator-android", "sourcePath": "skills/orca-emulator-android", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 2, "packageDigest": "12272cf82e0731f11e424822b961882457034e730358cc65ea28e4eb9c8ff7f5", "gitTreeSha": "f7b0fc8cbf5cd78ca5156f6bbe3a20f1462d8f83", @@ -100,7 +100,7 @@ { "name": "orca-linear", "sourcePath": "skills/orca-linear", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 2, "packageDigest": "d44d09e6ecb6a64da177083aad26a95f031cd1cf26ba059fdc888c2628aef64f", "gitTreeSha": "c34f42030f43e5a85737996fa375bbd79cb5bea8", @@ -119,7 +119,7 @@ { "name": "orca-per-workspace-env", "sourcePath": "skills/orca-per-workspace-env", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 2, "packageDigest": "fa3b65a1a107fca3f0375c696852477b62f58c154b9eb5c0663c41edc4bcd30d", "gitTreeSha": "354e775b79ea6952ec63acac4d3ee8a9ae07a650", @@ -138,7 +138,7 @@ { "name": "orchestration", "sourcePath": "skills/orchestration", - "appVersion": "1.4.144-rc.1", + "appVersion": "1.4.144-rc.2", "releaseRevision": 24, "packageDigest": "9fbfa2ae3f3f99441563a4b8b1c6302107944480db8718ddb326a862a51f7ab9", "gitTreeSha": "086c41e0b353b4908d2963694b4a7c791d4b3982", diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index f2bdf9b74cd..ef50a504a3a 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -12,6 +12,7 @@ import { import { toast } from 'sonner' import type { GlobalSettings, OrcaHooks, ProjectHostSetup, Repo } from '../../../../shared/types' import type { SpeechModelState } from '../../../../shared/speech-types' +import type { SkillFreshnessInventory } from '../../../../shared/skill-freshness' import type { SourceControlAiSettings, SourceControlAiSettingsPatch @@ -101,6 +102,7 @@ import { } from '@/hooks/useInstalledAgentSkills' import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime' import { useSkillFreshness } from '@/hooks/useSkillFreshness' +import { getSkillFreshnessDisplayStatus } from '@/lib/skill-freshness-display-status' import { deriveNeededSectionIds, getInitialMountedSectionIds } from './settings-load-performance' import { translate } from '@/i18n/i18n' import { getProjectHostSetupProjectionFromState } from '../../store/selectors' @@ -206,9 +208,10 @@ function getSettingsNavGroupDefinitionsForSearch( } function getSkillNavInstallStatus(skill: { + name: string installed: boolean loading: boolean - updateAvailable?: boolean + inventory: SkillFreshnessInventory | null }): SettingsNavInstallStatus { if (skill.loading) { return 'checking' @@ -216,9 +219,7 @@ function getSkillNavInstallStatus(skill: { if (!skill.installed) { return 'install' } - // A freshness-tracked skill that's present with no eligible update is current, so it - // reads "Up to date" (pairs with "Update available"). Voice sets 'installed' directly. - return skill.updateAvailable ? 'update-available' : 'up-to-date' + return getSkillFreshnessDisplayStatus(skill.inventory, skill.name) } function hasReadyVoiceModel( @@ -735,16 +736,16 @@ function Settings(): React.JSX.Element { orchestrationSkill const { installed: computerUseSkillInstalled, loading: computerUseSkillLoading } = computerUseSkill - const eligibleUpdateSkillNames = skillFreshnessInventory?.eligibleUpdateNames const capabilityInstallStatusBySectionId = useMemo(() => { - const eligibleUpdates = new Set(skillFreshnessApplies ? (eligibleUpdateSkillNames ?? []) : []) + const applicableFreshnessInventory = skillFreshnessApplies ? skillFreshnessInventory : null const next = new Map([ [ 'orchestration', getSkillNavInstallStatus({ + name: ORCHESTRATION_SKILL_NAME, installed: orchestrationSkillInstalled, loading: orchestrationSkillLoading, - updateAvailable: eligibleUpdates.has(ORCHESTRATION_SKILL_NAME) + inventory: applicableFreshnessInventory }) ] ]) @@ -752,9 +753,10 @@ function Settings(): React.JSX.Element { next.set( 'computer-use', getSkillNavInstallStatus({ + name: COMPUTER_USE_SKILL_NAME, installed: computerUseSkillInstalled, loading: computerUseSkillLoading, - updateAvailable: eligibleUpdates.has(COMPUTER_USE_SKILL_NAME) + inventory: applicableFreshnessInventory }) ) if (settings) { @@ -772,13 +774,13 @@ function Settings(): React.JSX.Element { }, [ computerUseSkillInstalled, computerUseSkillLoading, - eligibleUpdateSkillNames, modelStates, orchestrationSkillInstalled, orchestrationSkillLoading, settings, showDesktopOnlySettings, skillFreshnessApplies, + skillFreshnessInventory, voiceModelStatesLoading ]) const navSections = useMemo( diff --git a/src/renderer/src/components/settings/SettingsSidebar.test.tsx b/src/renderer/src/components/settings/SettingsSidebar.test.tsx index 11eb49163a0..6fec899efea 100644 --- a/src/renderer/src/components/settings/SettingsSidebar.test.tsx +++ b/src/renderer/src/components/settings/SettingsSidebar.test.tsx @@ -62,6 +62,12 @@ function renderSidebar( title: 'Voice', icon: Mic, installStatus: 'installed' + }, + { + id: 'computer-use', + title: 'Computer Use', + icon: Bot, + installStatus: 'up-to-date' } ] }, @@ -114,6 +120,7 @@ describe('SettingsSidebar', () => { expect(markup).toContain('Not installed') expect(markup).toContain('Installed') + expect(markup).toContain('Up to date') expect(markup).toContain('Optional') }) diff --git a/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx b/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx index 3c858686591..d71bf0d8ba7 100644 --- a/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx +++ b/src/renderer/src/components/skills/SkillFreshnessStatusPill.tsx @@ -1,6 +1,7 @@ import { useSkillFreshness } from '@/hooks/useSkillFreshness' import { translate } from '@/i18n/i18n' import { IntegrationStatusPill } from '@/components/integration-status-pill' +import { getSkillFreshnessDisplayStatus } from '@/lib/skill-freshness-display-status' // 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. @@ -8,7 +9,8 @@ import { IntegrationStatusPill } from '@/components/integration-status-pill' // placement is never advertised as updatable here. export function SkillFreshnessStatusPill({ skillName }: { skillName: string }): React.JSX.Element { const { inventory } = useSkillFreshness() - if (inventory?.eligibleUpdateNames.includes(skillName)) { + const status = getSkillFreshnessDisplayStatus(inventory, skillName) + if (status === 'update-available') { return ( {translate( @@ -18,14 +20,7 @@ export function SkillFreshnessStatusPill({ skillName }: { skillName: string }): ) } - const placements = inventory?.installations.filter( - (installation) => installation.name === skillName - ) - if ( - placements && - placements.length > 0 && - placements.every((installation) => installation.status === 'current') - ) { + if (status === 'up-to-date') { return ( {translate('auto.components.skills.SkillFreshnessStatusPill.upToDate', 'Up to date')} diff --git a/src/renderer/src/lib/skill-freshness-display-status.test.ts b/src/renderer/src/lib/skill-freshness-display-status.test.ts new file mode 100644 index 00000000000..9d244007d0e --- /dev/null +++ b/src/renderer/src/lib/skill-freshness-display-status.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import type { + SkillFreshnessInstallation, + SkillFreshnessInventory, + SkillFreshnessStatus +} from '../../../shared/skill-freshness' +import { getSkillFreshnessDisplayStatus } from './skill-freshness-display-status' + +const SKILL_NAME = 'orca-cli' + +function placement(status: SkillFreshnessStatus, index = 0): SkillFreshnessInstallation { + return { + id: `${SKILL_NAME}-${index}`, + name: SKILL_NAME, + rootId: 'home-agents', + providers: ['agent-skills'], + sourceKind: 'home', + sourceLabel: 'Agent skills home', + unresolvedPath: `/home/.agents/skills/${SKILL_NAME}-${index}`, + resolvedPath: `/home/.agents/skills/${SKILL_NAME}-${index}`, + physicalIdentity: `physical-${index}`, + topology: 'canonical-copy', + status, + installedReleaseRevision: 1, + installedAppVersion: '1.0.0', + currentReleaseRevision: 2, + currentPackageDigest: 'current', + currentAppVersion: '2.0.0', + observedPackageDigest: status === 'current' ? 'current' : 'other', + errorCategory: null + } +} + +function inventory( + installations: SkillFreshnessInstallation[], + eligibleUpdateNames: string[] = [] +): SkillFreshnessInventory { + return { schemaVersion: 1, installations, eligibleUpdateNames, scannedAt: 1 } +} + +describe('getSkillFreshnessDisplayStatus', () => { + it('shows update available when the inventory authorizes an update', () => { + expect( + getSkillFreshnessDisplayStatus(inventory([placement('outdated')], [SKILL_NAME]), SKILL_NAME) + ).toBe('update-available') + }) + + it('shows up to date only when every discovered placement is current', () => { + expect( + getSkillFreshnessDisplayStatus( + inventory([placement('current'), placement('current', 1)]), + SKILL_NAME + ) + ).toBe('up-to-date') + }) + + it.each([ + ['before the inventory loads', null], + ['when the inventory has no matching placement', inventory([])], + [ + 'when any placement is unrecognized', + inventory([placement('current'), placement('unrecognized', 1)]) + ], + ['when a placement is inaccessible', inventory([placement('inaccessible')])], + ['when an outdated placement is not eligible', inventory([placement('outdated')])] + ])('falls back to installed %s', (_scenario, value) => { + expect(getSkillFreshnessDisplayStatus(value, SKILL_NAME)).toBe('installed') + }) +}) diff --git a/src/renderer/src/lib/skill-freshness-display-status.ts b/src/renderer/src/lib/skill-freshness-display-status.ts new file mode 100644 index 00000000000..25264022659 --- /dev/null +++ b/src/renderer/src/lib/skill-freshness-display-status.ts @@ -0,0 +1,25 @@ +import type { SkillFreshnessInventory } from '../../../shared/skill-freshness' + +export type SkillFreshnessDisplayStatus = 'installed' | 'up-to-date' | 'update-available' + +export function getSkillFreshnessDisplayStatus( + inventory: SkillFreshnessInventory | null, + skillName: string +): SkillFreshnessDisplayStatus { + if (inventory?.eligibleUpdateNames.includes(skillName)) { + return 'update-available' + } + + let hasPlacement = false + for (const installation of inventory?.installations ?? []) { + if (installation.name !== skillName) { + continue + } + hasPlacement = true + // No eligible update is not proof that a blocked or unrecognized copy is current. + if (installation.status !== 'current') { + return 'installed' + } + } + return hasPlacement ? 'up-to-date' : 'installed' +}