mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
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:
@@ -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'
|
||||
}
|
||||
Reference in New Issue
Block a user