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.
This commit is contained in:
Brennan Benson
2026-07-16 16:51:46 -07:00
parent e192b2a774
commit 65b3f2c873
6 changed files with 125 additions and 27 deletions
+9 -9
View File
@@ -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",
@@ -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<string, SettingsNavInstallStatus>([
[
'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(
@@ -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')
})
@@ -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 (
<IntegrationStatusPill tone="attention">
{translate(
@@ -18,14 +20,7 @@ export function SkillFreshnessStatusPill({ skillName }: { skillName: string }):
</IntegrationStatusPill>
)
}
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 (
<IntegrationStatusPill tone="connected">
{translate('auto.components.skills.SkillFreshnessStatusPill.upToDate', 'Up to date')}
@@ -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')
})
})
@@ -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'
}