fix(settings): preserve Linear freshness and dialog translations

This commit is contained in:
Brennan Benson
2026-07-17 14:20:03 -07:00
parent f0fec5dedf
commit df7bda7f2f
9 changed files with 113 additions and 36 deletions
@@ -16,7 +16,8 @@ const UPDATE_COMMAND = 'npx skills update orca-linear --global'
const mocks = vi.hoisted(() => ({
panelProps: [] as Record<string, unknown>[],
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')
})
})
@@ -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
}
/>
@@ -24,10 +24,10 @@ const updateCapableCallers = new Map<string, readonly string[]>([
['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',
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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",
+5 -5
View File
@@ -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",
@@ -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
})
})
})
@@ -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
}