diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx index cf2d4f731a7..b7d6a9d56bc 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx @@ -16,7 +16,8 @@ const UPDATE_COMMAND = 'npx skills update orca-linear --global' const mocks = vi.hoisted(() => ({ panelProps: [] as Record[], runtime: 'native' as 'native' | 'wsl', - skillInstalled: true + skillInstalled: true, + updateSkillName: 'orca-linear' })) vi.mock('./AgentSkillSetupPanel', () => ({ @@ -40,6 +41,13 @@ vi.mock('./CliSkillRuntimeSetup', () => ({ getWslCliDistroRequest: () => undefined })) +vi.mock('@/lib/linear-agent-skill-update-command', () => ({ + getLinearAgentSkillUpdateTarget: () => ({ + command: UPDATE_COMMAND, + skillName: mocks.updateSkillName + }) +})) + vi.mock('@/hooks/useInstalledAgentSkills', () => ({ GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['home'], useInstalledAgentSkillNames: () => ({ @@ -86,6 +94,7 @@ describe('LinearAgentSkillPane', () => { mocks.panelProps.length = 0 mocks.runtime = 'native' mocks.skillInstalled = true + mocks.updateSkillName = 'orca-linear' }) it('renders the Linear skill card and the usage examples', () => { @@ -121,16 +130,10 @@ describe('LinearAgentSkillPane', () => { expect(mocks.panelProps.at(-1)?.freshnessSkillName).toBeUndefined() }) - it('offers the canonical install when the accepted skill is missing', async () => { - mocks.skillInstalled = false + it('checks freshness under the legacy name when that is the installed update target', async () => { + mocks.updateSkillName = 'linear-tickets' await renderPane() - expect(mocks.panelProps.at(-1)).toEqual( - expect.objectContaining({ - installed: false, - command: ORCA_LINEAR_SKILL_INSTALL_COMMAND, - freshnessSkillName: ORCA_LINEAR_SKILL_NAME - }) - ) + expect(mocks.panelProps.at(-1)?.freshnessSkillName).toBe('linear-tickets') }) }) diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx index 248feda978a..ad477a9ed8d 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx @@ -5,13 +5,13 @@ import { LinearIcon } from '@/components/icons/LinearIcon' import { LINEAR_AGENT_SKILL_NAMES, ORCA_LINEAR_SKILL_INSTALL_COMMAND, - ORCA_LINEAR_SKILL_NAME, - ORCA_LINEAR_SKILL_UPDATE_COMMAND + ORCA_LINEAR_SKILL_NAME } from '@/lib/agent-feature-install-commands' import { AGENT_SKILL_CLI_PREREQUISITE_NOTICE, ensureOrcaCliAvailableForAgentSkillTerminal } from '@/lib/agent-skill-cli-prerequisite' +import { getLinearAgentSkillUpdateTarget } from '@/lib/linear-agent-skill-update-command' import { getLinearUsageExamples } from '@/lib/linear-usage-examples' import type { SkillUsageExample } from '@/lib/skill-usage-example' import { @@ -52,6 +52,7 @@ export function LinearAgentSkillPane(): React.JSX.Element { installed: linearSkillInstalled, loading: linearSkillLoading, error: linearSkillError, + skills: linearSkills, refresh: refreshLinearSkill } = useInstalledAgentSkillNames(LINEAR_AGENT_SKILL_NAMES, { discoveryTarget: activeSkillRuntime.discoveryTarget, @@ -68,14 +69,20 @@ export function LinearAgentSkillPane(): React.JSX.Element { ), [activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason] ) + const updateTarget = useMemo( + () => getLinearAgentSkillUpdateTarget(linearSkills, linearSkillInstalled), + [linearSkillInstalled, linearSkills] + ) const updateCommand = useMemo(() => { + const command = updateTarget.command return activeSkillRuntime.installDisabledReason - ? ORCA_LINEAR_SKILL_UPDATE_COMMAND - : buildSkillCommandForRuntime( - ORCA_LINEAR_SKILL_UPDATE_COMMAND, - activeSkillRuntime.agentRuntime - ) - }, [activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason]) + ? command + : buildSkillCommandForRuntime(command, activeSkillRuntime.agentRuntime) + }, [ + activeSkillRuntime.agentRuntime, + activeSkillRuntime.installDisabledReason, + updateTarget.command + ]) return ( diff --git a/src/renderer/src/components/settings/agent-skill-installed-command-callers.test.ts b/src/renderer/src/components/settings/agent-skill-installed-command-callers.test.ts index 89964ae30dc..70f6da85fe1 100644 --- a/src/renderer/src/components/settings/agent-skill-installed-command-callers.test.ts +++ b/src/renderer/src/components/settings/agent-skill-installed-command-callers.test.ts @@ -24,10 +24,10 @@ const updateCapableCallers = new Map([ ['COMPUTER_USE_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}'] ], [ - // Why: the Linear settings section must update only the canonical skill; - // legacy-only discovery is treated as missing and follows the install path. + // Why: the Linear settings section shares the update-target resolver so + // legacy-only installs keep the command and freshness identity aligned. 'src/renderer/src/components/settings/LinearAgentSkillPane.tsx', - ['ORCA_LINEAR_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}'] + ['getLinearAgentSkillUpdateTarget', 'installedCommand={updateCommand}'] ], [ 'src/renderer/src/components/settings/EphemeralVmsPane.tsx', @@ -50,8 +50,10 @@ const updateCapableCallers = new Map([ ['ORCA_CLI_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}'] ], [ + // Why: the single-skill update command selection moved into + // getLinearAgentSkillUpdateCommand so the settings install CTA shares it. 'src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx', - ['ORCA_LINEAR_SKILL_UPDATE_COMMAND', 'installedCommand={installedCommand}'] + ['getLinearAgentSkillUpdateCommand', 'installedCommand={installedCommand}'] ], [ 'src/renderer/src/components/sidebar/LinearAgentSkillSetupDialog.tsx', diff --git a/src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx b/src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx index e030930795b..a3a4a1db3dd 100644 --- a/src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx +++ b/src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx @@ -146,8 +146,8 @@ describe('LinearAgentSkillInstallCta', () => { expect(rendered.textContent).not.toContain('Not installed') }) - it('migrates a legacy-only install through the canonical install command', async () => { - mocks.skillState.installed = false + it('updates through the legacy skill name when only linear-tickets is installed', async () => { + mocks.skillState.installed = true mocks.skillState.skills = [ discoveredSkill({ name: 'linear-tickets', @@ -158,11 +158,7 @@ describe('LinearAgentSkillInstallCta', () => { const rendered = await renderCta() - expect(rendered.textContent).toContain( - 'npx skills add https://github.com/stablyai/orca --skill orca-linear --global' - ) - expect(rendered.textContent).toContain('Not installed') - expect(rendered.textContent).not.toContain('npx skills update linear-tickets --global') + expect(rendered.textContent).toContain('npx skills update linear-tickets --global') }) it('notes that remote agent environments need their own setup', async () => { diff --git a/src/renderer/src/components/settings/linear-agent-skill-install-cta.tsx b/src/renderer/src/components/settings/linear-agent-skill-install-cta.tsx index 87ec6160183..43e755dcbe2 100644 --- a/src/renderer/src/components/settings/linear-agent-skill-install-cta.tsx +++ b/src/renderer/src/components/settings/linear-agent-skill-install-cta.tsx @@ -11,9 +11,9 @@ import { import { LINEAR_AGENT_SKILL_NAMES, ORCA_LINEAR_SKILL_INSTALL_COMMAND, - ORCA_LINEAR_SKILL_NAME, - ORCA_LINEAR_SKILL_UPDATE_COMMAND + ORCA_LINEAR_SKILL_NAME } from '@/lib/agent-feature-install-commands' +import { getLinearAgentSkillUpdateCommand } from '@/lib/linear-agent-skill-update-command' import { cn } from '@/lib/utils' import { getLinearAgentSkillSetupInlineRuntimeCopy } from '../sidebar/linear-agent-skill-setup-copy' import { @@ -57,10 +57,12 @@ export function LinearAgentSkillInstallCta({ const command = useMemo( () => buildSkillCommandForRuntime( - skill.installed ? ORCA_LINEAR_SKILL_UPDATE_COMMAND : ORCA_LINEAR_SKILL_INSTALL_COMMAND, + skill.installed + ? getLinearAgentSkillUpdateCommand(skill.skills, skill.installed) + : ORCA_LINEAR_SKILL_INSTALL_COMMAND, agentRuntime ), - [agentRuntime, skill.installed] + [agentRuntime, skill.installed, skill.skills] ) const subordinateRowClass = useIntegrationSubordinateRowClass('space-y-1.5') const commandRowClass = useIntegrationCommandRowClass() diff --git a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx index 9760b753968..e6157339c4a 100644 --- a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx +++ b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx @@ -9,9 +9,9 @@ import { } from '@/hooks/useInstalledAgentSkills' import { LINEAR_AGENT_SKILL_NAMES, - ORCA_LINEAR_SKILL_INSTALL_COMMAND, - ORCA_LINEAR_SKILL_UPDATE_COMMAND + ORCA_LINEAR_SKILL_INSTALL_COMMAND } from '@/lib/agent-feature-install-commands' +import { getLinearAgentSkillUpdateCommand } from '@/lib/linear-agent-skill-update-command' import { ensureOrcaCliAvailableForAgentSkillTerminal, isOrcaCliAvailableOnPath @@ -122,8 +122,12 @@ export function LinearAgentSkillSetupPrompt({ [agentRuntime] ) const installedCommand = useMemo( - () => buildSkillCommandForRuntime(ORCA_LINEAR_SKILL_UPDATE_COMMAND, agentRuntime), - [agentRuntime] + () => + buildSkillCommandForRuntime( + getLinearAgentSkillUpdateCommand(skill.skills, skill.installed), + agentRuntime + ), + [agentRuntime, skill.installed, skill.skills] ) const terminalShellOverride = getLinearPromptTerminalShellOverride( currentPlatform, diff --git a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx index 347687a4248..7ff473b49da 100644 --- a/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx +++ b/src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.update-command.test.tsx @@ -160,18 +160,13 @@ describe('LinearAgentSkillSetupPrompt update command', () => { ) }) - it('migrates a legacy-only install through the canonical install command', async () => { - mocks.skillState.installed = false + it('uses the legacy update command when only the legacy Linear skill is installed', async () => { mocks.skillState.skills = [legacyLinearSkillPath()] await renderPrompt() expect(mocks.panelProps.at(-1)).toEqual( - expect.objectContaining({ - installed: false, - command: 'npx skills add https://github.com/stablyai/orca --skill orca-linear --global', - installedCommand: 'npx skills update orca-linear --global' - }) + expect.objectContaining({ installedCommand: 'npx skills update linear-tickets --global' }) ) }) diff --git a/src/renderer/src/lib/linear-agent-skill-update-command.test.ts b/src/renderer/src/lib/linear-agent-skill-update-command.test.ts new file mode 100644 index 00000000000..f4e2c543ced --- /dev/null +++ b/src/renderer/src/lib/linear-agent-skill-update-command.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import type { DiscoveredSkill } from '../../../shared/skills' +import { + LINEAR_TICKETS_SKILL_UPDATE_COMMAND, + ORCA_LINEAR_SKILL_UPDATE_COMMAND +} from './agent-feature-install-commands' +import { getLinearAgentSkillUpdateTarget } from './linear-agent-skill-update-command' + +function skill(name: string): DiscoveredSkill { + return { + id: name, + name, + description: null, + providers: ['agent-skills'], + sourceKind: 'home', + sourceLabel: 'Agent skills home', + rootPath: '/home/test/.agents/skills', + directoryPath: `/home/test/.agents/skills/${name}`, + skillFilePath: `/home/test/.agents/skills/${name}/SKILL.md`, + installed: true, + fileCount: 1, + updatedAt: null + } +} + +describe('getLinearAgentSkillUpdateTarget', () => { + it('targets the canonical skill for missing, canonical, and dual-name installs', () => { + expect(getLinearAgentSkillUpdateTarget([], false)).toEqual({ + skillName: 'orca-linear', + command: ORCA_LINEAR_SKILL_UPDATE_COMMAND + }) + expect(getLinearAgentSkillUpdateTarget([skill('orca-linear')], true)).toEqual({ + skillName: 'orca-linear', + command: ORCA_LINEAR_SKILL_UPDATE_COMMAND + }) + expect( + getLinearAgentSkillUpdateTarget([skill('orca-linear'), skill('linear-tickets')], true) + ).toEqual({ + skillName: 'orca-linear', + command: ORCA_LINEAR_SKILL_UPDATE_COMMAND + }) + }) + + it('keeps the update and freshness identity on a legacy-only install', () => { + expect(getLinearAgentSkillUpdateTarget([skill('linear-tickets')], true)).toEqual({ + skillName: 'linear-tickets', + command: LINEAR_TICKETS_SKILL_UPDATE_COMMAND + }) + }) +}) diff --git a/src/renderer/src/lib/linear-agent-skill-update-command.ts b/src/renderer/src/lib/linear-agent-skill-update-command.ts new file mode 100644 index 00000000000..4dc3661d506 --- /dev/null +++ b/src/renderer/src/lib/linear-agent-skill-update-command.ts @@ -0,0 +1,40 @@ +import type { DiscoveredSkill } from '../../../shared/skills' +import { + GLOBAL_AGENT_SKILL_SOURCE_KINDS, + hasInstalledAgentSkill +} from '@/hooks/useInstalledAgentSkills' +import { + LINEAR_TICKETS_SKILL_NAME, + LINEAR_TICKETS_SKILL_UPDATE_COMMAND, + ORCA_LINEAR_SKILL_NAME, + ORCA_LINEAR_SKILL_UPDATE_COMMAND +} from '@/lib/agent-feature-install-commands' + +export type LinearAgentSkillUpdateTarget = { + skillName: typeof ORCA_LINEAR_SKILL_NAME | typeof LINEAR_TICKETS_SKILL_NAME + command: string +} + +// Why: legacy-only installs must update and report freshness for the installed +// legacy skill, while fresh/canonical/both-name states use the canonical name. +export function getLinearAgentSkillUpdateTarget( + skills: readonly DiscoveredSkill[], + installed: boolean +): LinearAgentSkillUpdateTarget { + const canonicalSkillInstalled = hasInstalledAgentSkill(skills, ORCA_LINEAR_SKILL_NAME, { + sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS + }) + const legacySkillInstalled = hasInstalledAgentSkill(skills, LINEAR_TICKETS_SKILL_NAME, { + sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS + }) + return !installed || canonicalSkillInstalled || !legacySkillInstalled + ? { skillName: ORCA_LINEAR_SKILL_NAME, command: ORCA_LINEAR_SKILL_UPDATE_COMMAND } + : { skillName: LINEAR_TICKETS_SKILL_NAME, command: LINEAR_TICKETS_SKILL_UPDATE_COMMAND } +} + +export function getLinearAgentSkillUpdateCommand( + skills: readonly DiscoveredSkill[], + installed: boolean +): string { + return getLinearAgentSkillUpdateTarget(skills, installed).command +} diff --git a/src/shared/agent-feature-install-commands.test.ts b/src/shared/agent-feature-install-commands.test.ts index 4ae216414ec..e242f9f5fd7 100644 --- a/src/shared/agent-feature-install-commands.test.ts +++ b/src/shared/agent-feature-install-commands.test.ts @@ -4,7 +4,6 @@ import { buildAgentFeatureSkillUpdateCommand, COMPUTER_USE_SKILL_UPDATE_COMMAND, EPHEMERAL_VMS_SKILL_UPDATE_COMMAND, - LINEAR_AGENT_SKILL_NAMES, LINEAR_TICKETS_SKILL_UPDATE_COMMAND, ORCA_LINEAR_SKILL_UPDATE_COMMAND, ORCA_CLI_ORCHESTRATION_SKILL_INSTALL_COMMAND, @@ -39,8 +38,4 @@ describe('agent feature skill commands', () => { buildAgentFeatureSkillInstallCommand(['orca-cli', 'orchestration']) ) }) - - it('requires the canonical Linear skill so legacy installs migrate', () => { - expect(LINEAR_AGENT_SKILL_NAMES).toEqual(['orca-linear']) - }) }) diff --git a/src/shared/agent-feature-install-commands.ts b/src/shared/agent-feature-install-commands.ts index 4712aef705c..60bdc6d6baf 100644 --- a/src/shared/agent-feature-install-commands.ts +++ b/src/shared/agent-feature-install-commands.ts @@ -6,9 +6,7 @@ export const ORCHESTRATION_SKILL_NAME = 'orchestration' export const EPHEMERAL_VMS_SKILL_NAME = 'orca-per-workspace-env' export const ORCA_LINEAR_SKILL_NAME = 'orca-linear' export const LINEAR_TICKETS_SKILL_NAME = 'linear-tickets' -// Why: linear-tickets is retired and must not satisfy setup readiness; legacy-only -// users should be offered the canonical orca-linear install path. -export const LINEAR_AGENT_SKILL_NAMES = [ORCA_LINEAR_SKILL_NAME] as const +export const LINEAR_AGENT_SKILL_NAMES = [ORCA_LINEAR_SKILL_NAME, LINEAR_TICKETS_SKILL_NAME] as const export function buildAgentFeatureSkillInstallCommand(skillNames: readonly string[]): string { if (skillNames.length === 0) {