diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx index fac6db03bba..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', () => ({ @@ -41,7 +42,10 @@ vi.mock('./CliSkillRuntimeSetup', () => ({ })) vi.mock('@/lib/linear-agent-skill-update-command', () => ({ - getLinearAgentSkillUpdateCommand: () => UPDATE_COMMAND + getLinearAgentSkillUpdateTarget: () => ({ + command: UPDATE_COMMAND, + skillName: mocks.updateSkillName + }) })) vi.mock('@/hooks/useInstalledAgentSkills', () => ({ @@ -90,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', () => { @@ -124,4 +129,11 @@ describe('LinearAgentSkillPane', () => { expect(mocks.panelProps.at(-1)?.freshnessSkillName).toBeUndefined() }) + + 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)?.freshnessSkillName).toBe('linear-tickets') + }) }) diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx index 38dd9dd62de..ad477a9ed8d 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx @@ -11,7 +11,7 @@ import { AGENT_SKILL_CLI_PREREQUISITE_NOTICE, ensureOrcaCliAvailableForAgentSkillTerminal } from '@/lib/agent-skill-cli-prerequisite' -import { getLinearAgentSkillUpdateCommand } from '@/lib/linear-agent-skill-update-command' +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 { @@ -69,16 +69,19 @@ export function LinearAgentSkillPane(): React.JSX.Element { ), [activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason] ) + const updateTarget = useMemo( + () => getLinearAgentSkillUpdateTarget(linearSkills, linearSkillInstalled), + [linearSkillInstalled, linearSkills] + ) const updateCommand = useMemo(() => { - const command = getLinearAgentSkillUpdateCommand(linearSkills, linearSkillInstalled) + const command = updateTarget.command return activeSkillRuntime.installDisabledReason ? command : buildSkillCommandForRuntime(command, activeSkillRuntime.agentRuntime) }, [ activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason, - linearSkillInstalled, - linearSkills + updateTarget.command ]) return ( @@ -134,7 +137,7 @@ export function LinearAgentSkillPane(): React.JSX.Element { // Why: the local-host-only freshness scan cannot vouch for a WSL runtime, // so fall back to the presence-only pill there (mirrors the other skills). freshnessSkillName={ - activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : ORCA_LINEAR_SKILL_NAME + activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : updateTarget.skillName } /> 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 c2f876bb0f4..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 shares getLinearAgentSkillUpdateCommand so - // legacy-only installs still update through the installed skill name. + // 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', - ['getLinearAgentSkillUpdateCommand', 'installedCommand={updateCommand}'] + ['getLinearAgentSkillUpdateTarget', 'installedCommand={updateCommand}'] ], [ 'src/renderer/src/components/settings/EphemeralVmsPane.tsx', diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index e533ee41e64..0b1b050b797 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -9192,11 +9192,11 @@ "androidApk": "APK de Android" }, "SkillUsageExampleDialog": { - "copiedPrompt": "Copied example prompt.", - "copyFailed": "Failed to copy prompt.", - "copyExampleAria": "Copy {{value0}} example prompt", - "done": "Done", - "copyPrompt": "Copy prompt" + "copiedPrompt": "Prompt de ejemplo copiado.", + "copyFailed": "No se pudo copiar el prompt.", + "copyExampleAria": "Copiar prompt de ejemplo {{value0}}", + "done": "Hecho", + "copyPrompt": "Copiar prompt" }, "LinearAgentSkillPane": { "title": "Linear", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index d322e01b8e4..34deb5584d9 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -9192,11 +9192,11 @@ "androidApk": "Android APK" }, "SkillUsageExampleDialog": { - "copiedPrompt": "Copied example prompt.", - "copyFailed": "Failed to copy prompt.", - "copyExampleAria": "Copy {{value0}} example prompt", - "done": "Done", - "copyPrompt": "Copy prompt" + "copiedPrompt": "プロンプトの例をコピーしました。", + "copyFailed": "プロンプトのコピーに失敗しました。", + "copyExampleAria": "{{value0}} プロンプトの例をコピーします", + "done": "完了", + "copyPrompt": "プロンプトのコピー" }, "LinearAgentSkillPane": { "title": "Linear", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 2f6cda47452..e8bbd80c159 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -9192,11 +9192,11 @@ "androidApk": "Android APK" }, "SkillUsageExampleDialog": { - "copiedPrompt": "Copied example prompt.", - "copyFailed": "Failed to copy prompt.", - "copyExampleAria": "Copy {{value0}} example prompt", - "done": "Done", - "copyPrompt": "Copy prompt" + "copiedPrompt": "예시 프롬프트를 복사했습니다.", + "copyFailed": "프롬프트를 복사하지 못했습니다.", + "copyExampleAria": "{{value0}} 예시 프롬프트 복사", + "done": "완료", + "copyPrompt": "프롬프트 복사" }, "LinearAgentSkillPane": { "title": "Linear", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 1de024b5958..37e4378c5d1 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -9192,11 +9192,11 @@ "androidApk": "Android APK" }, "SkillUsageExampleDialog": { - "copiedPrompt": "Copied example prompt.", - "copyFailed": "Failed to copy prompt.", - "copyExampleAria": "Copy {{value0}} example prompt", - "done": "Done", - "copyPrompt": "Copy prompt" + "copiedPrompt": "复制示例提示。", + "copyFailed": "复制提示失败。", + "copyExampleAria": "复制 {{value0}} 示例提示", + "done": "完成", + "copyPrompt": "复制提示" }, "LinearAgentSkillPane": { "title": "Linear", 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 index 69dbcce84d7..4dc3661d506 100644 --- a/src/renderer/src/lib/linear-agent-skill-update-command.ts +++ b/src/renderer/src/lib/linear-agent-skill-update-command.ts @@ -10,12 +10,17 @@ import { ORCA_LINEAR_SKILL_UPDATE_COMMAND } from '@/lib/agent-feature-install-commands' -// Why: legacy-only installs must update the installed legacy skill, while -// fresh/canonical/both-name states should move through the canonical name. -export function getLinearAgentSkillUpdateCommand( +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 -): string { +): LinearAgentSkillUpdateTarget { const canonicalSkillInstalled = hasInstalledAgentSkill(skills, ORCA_LINEAR_SKILL_NAME, { sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS }) @@ -23,6 +28,13 @@ export function getLinearAgentSkillUpdateCommand( sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS }) return !installed || canonicalSkillInstalled || !legacySkillInstalled - ? ORCA_LINEAR_SKILL_UPDATE_COMMAND - : LINEAR_TICKETS_SKILL_UPDATE_COMMAND + ? { 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 }