diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx new file mode 100644 index 00000000000..57395b7b28d --- /dev/null +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx @@ -0,0 +1,125 @@ +// @vitest-environment happy-dom + +import { act, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { renderToStaticMarkup } from 'react-dom/server' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + ORCA_LINEAR_SKILL_INSTALL_COMMAND, + ORCA_LINEAR_SKILL_NAME +} from '@/lib/agent-feature-install-commands' +import { getLinearUsageExamples } from '@/lib/linear-usage-examples' +import { LinearAgentSkillPane } from './LinearAgentSkillPane' + +const UPDATE_COMMAND = 'npx skills update orca-linear --global' + +const mocks = vi.hoisted(() => ({ + panelProps: [] as Record[], + runtime: 'native' as 'native' | 'wsl', + skillInstalled: true +})) + +vi.mock('./AgentSkillSetupPanel', () => ({ + AgentSkillSetupPanel: (props: Record & { footer?: ReactNode }) => { + mocks.panelProps.push(props) + return ( +
+

{String(props.title)}

+ {props.installed ? 'Installed' : 'Not installed'} + {String(props.command)} + {String(props.installedCommand)} + {String(props.freshnessSkillName)} +
+ ) + } +})) + +vi.mock('./CliSkillRuntimeSetup', () => ({ + buildSkillCommandForRuntime: (command: string) => command, + ensureWslCliAvailableForAgentSkillTerminal: vi.fn(), + getWslCliDistroRequest: () => undefined +})) + +vi.mock('@/lib/linear-agent-skill-update-command', () => ({ + getLinearAgentSkillUpdateCommand: () => UPDATE_COMMAND +})) + +vi.mock('@/hooks/useInstalledAgentSkills', () => ({ + GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['home'], + useInstalledAgentSkillNames: () => ({ + installed: mocks.skillInstalled, + loading: false, + error: null, + skills: [], + refresh: vi.fn() + }) +})) + +vi.mock('@/hooks/useActiveProjectSkillRuntime', () => ({ + useActiveProjectSkillRuntime: () => ({ + discoveryTarget: undefined, + agentRuntime: { runtime: mocks.runtime }, + terminalShellOverride: undefined, + installDisabledReason: null + }) +})) + +let root: Root | null = null +let container: HTMLDivElement | null = null + +async function renderPane(): Promise { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + await act(async () => { + root?.render() + }) + return container +} + +describe('LinearAgentSkillPane', () => { + afterEach(async () => { + if (root) { + await act(async () => { + root?.unmount() + }) + } + root = null + container?.remove() + container = null + mocks.panelProps.length = 0 + mocks.runtime = 'native' + mocks.skillInstalled = true + }) + + it('renders the Linear skill card and the usage examples', () => { + const markup = renderToStaticMarkup() + + expect(markup).toContain('Linear skill') + expect(markup).toContain('How to use it') + const examples = getLinearUsageExamples() + expect(examples).toHaveLength(5) + for (const example of examples) { + expect(markup).toContain(example.title) + } + }) + + it('passes the orca-linear install/update commands and freshness on a local runtime', async () => { + await renderPane() + + expect(mocks.panelProps.at(-1)).toEqual( + expect.objectContaining({ + command: ORCA_LINEAR_SKILL_INSTALL_COMMAND, + installedCommand: UPDATE_COMMAND, + freshnessSkillName: ORCA_LINEAR_SKILL_NAME + }) + ) + }) + + it('drops freshness on a WSL runtime the local scan cannot vouch for', async () => { + mocks.runtime = 'wsl' + await renderPane() + + expect(mocks.panelProps.at(-1)?.freshnessSkillName).toBeUndefined() + }) +}) diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx new file mode 100644 index 00000000000..c7146370c88 --- /dev/null +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx @@ -0,0 +1,150 @@ +import { useMemo } from 'react' +import { ArrowRightCircle, BookOpen, Link2, ListTodo, MessageSquarePlus } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' +import { LinearIcon } from '@/components/icons/LinearIcon' +import { + LINEAR_AGENT_SKILL_NAMES, + ORCA_LINEAR_SKILL_INSTALL_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 { getLinearAgentSkillUpdateCommand } from '@/lib/linear-agent-skill-update-command' +import { getLinearUsageExamples } from '@/lib/linear-usage-examples' +import type { SkillUsageExample } from '@/lib/skill-usage-example' +import { + GLOBAL_AGENT_SKILL_SOURCE_KINDS, + useInstalledAgentSkillNames +} from '@/hooks/useInstalledAgentSkills' +import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime' +import { AgentSkillSetupPanel } from './AgentSkillSetupPanel' +import { + buildSkillCommandForRuntime, + ensureWslCliAvailableForAgentSkillTerminal, + getWslCliDistroRequest +} from './CliSkillRuntimeSetup' +import { getLinearAgentSkillPaneSearchEntries } from './linear-agent-skill-search' +import { SearchableSetting } from './SearchableSetting' +import { SkillUsageExamplesSection } from './SkillUsageExamplesSection' +import { translate } from '@/i18n/i18n' +export { getLinearAgentSkillPaneSearchEntries } from './linear-agent-skill-search' + +const LINEAR_EXAMPLE_ICONS: Record = { + 'read-ticket': BookOpen, + 'post-update': MessageSquarePlus, + 'move-state': ArrowRightCircle, + 'attach-pr': Link2, + 'triage-followups': ListTodo +} + +function resolveLinearExampleIcon(example: SkillUsageExample): LucideIcon { + return LINEAR_EXAMPLE_ICONS[example.id] ?? LinearIcon +} + +// Why: this section is rendered as a Settings section only when the Linear +// provider is connected, so the orca-linear agent skill sits beside the +// connection that makes it useful. +export function LinearAgentSkillPane(): React.JSX.Element { + const activeSkillRuntime = useActiveProjectSkillRuntime() + const { + installed: linearSkillInstalled, + loading: linearSkillLoading, + error: linearSkillError, + skills: linearSkills, + refresh: refreshLinearSkill + } = useInstalledAgentSkillNames(LINEAR_AGENT_SKILL_NAMES, { + discoveryTarget: activeSkillRuntime.discoveryTarget, + sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS + }) + + const installCommand = useMemo( + () => + activeSkillRuntime.installDisabledReason + ? ORCA_LINEAR_SKILL_INSTALL_COMMAND + : buildSkillCommandForRuntime( + ORCA_LINEAR_SKILL_INSTALL_COMMAND, + activeSkillRuntime.agentRuntime + ), + [activeSkillRuntime.agentRuntime, activeSkillRuntime.installDisabledReason] + ) + const updateCommand = useMemo(() => { + const command = getLinearAgentSkillUpdateCommand(linearSkills, linearSkillInstalled) + return activeSkillRuntime.installDisabledReason + ? command + : buildSkillCommandForRuntime(command, activeSkillRuntime.agentRuntime) + }, [ + activeSkillRuntime.agentRuntime, + activeSkillRuntime.installDisabledReason, + linearSkillInstalled, + linearSkills + ]) + + return ( + + } + preInstallNotice={AGENT_SKILL_CLI_PREREQUISITE_NOTICE} + getPrerequisiteStatus={() => + activeSkillRuntime.agentRuntime?.runtime === 'wsl' + ? window.api.cli.getWslInstallStatus( + getWslCliDistroRequest(activeSkillRuntime.agentRuntime) + ) + : window.api.cli.getInstallStatus() + } + onBeforeOpenTerminal={async () => { + await (activeSkillRuntime.agentRuntime?.runtime === 'wsl' + ? ensureWslCliAvailableForAgentSkillTerminal(activeSkillRuntime.agentRuntime) + : ensureOrcaCliAvailableForAgentSkillTerminal()) + }} + onRecheck={refreshLinearSkill} + // 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 + } + /> + + + + ) +} diff --git a/src/renderer/src/components/settings/OrchestrationPane.tsx b/src/renderer/src/components/settings/OrchestrationPane.tsx index 59aea3347c4..dad05eb048e 100644 --- a/src/renderer/src/components/settings/OrchestrationPane.tsx +++ b/src/renderer/src/components/settings/OrchestrationPane.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' -import { ArrowRightLeft, GitBranch, ListChecks, Workflow } from 'lucide-react' +import { ArrowRightLeft, GitBranch, ListChecks, Workflow, type LucideIcon } from 'lucide-react' import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands' +import type { SkillUsageExample } from '@/lib/skill-usage-example' import { AGENT_SKILL_CLI_PREREQUISITE_NOTICE, ensureOrcaCliAvailableForAgentSkillTerminal @@ -26,7 +27,7 @@ import { getWslCliDistroRequest } from './CliSkillRuntimeSetup' import { OrchestrationSkillAgentCoverage } from './OrchestrationSkillAgentCoverage' -import { OrchestrationExampleDialog } from './OrchestrationExamplesDialog' +import { SkillUsageExamplesSection } from './SkillUsageExamplesSection' import { OrchestrationSkillPromptDialog } from './OrchestrationSkillPromptDialog' import { translate } from '@/i18n/i18n' @@ -38,10 +39,13 @@ const EXAMPLE_ICONS = { 'child-worktrees': Workflow } as const +function resolveOrchestrationExampleIcon(example: SkillUsageExample): LucideIcon { + return EXAMPLE_ICONS[example.id as keyof typeof EXAMPLE_ICONS] ?? Workflow +} + export function OrchestrationPane(): React.JSX.Element { const searchQuery = useAppStore((s) => s.settingsSearchQuery) const showOrchestration = matchesSettingsSearch(searchQuery, getOrchestrationPaneSearchEntries()) - const [selectedExampleId, setSelectedExampleId] = useState(null) const [skillPromptOpen, setSkillPromptOpen] = useState(false) const activeSkillRuntime = useActiveProjectSkillRuntime() const orchestrationInstallCommand = !activeSkillRuntime.installDisabledReason @@ -150,6 +154,11 @@ export function OrchestrationPane(): React.JSX.Element { /> } onRecheck={refreshOrchestrationSkill} + // Why: the local-host-only freshness scan cannot vouch for a WSL runtime, + // so fall back to the presence-only pill there (mirrors the Computer Use card). + freshnessSkillName={ + activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : ORCHESTRATION_SKILL_NAME + } /> -
-
-

- {translate('auto.components.settings.OrchestrationPane.ae79504732', 'How to use it')} -

-

- {translate( - 'auto.components.settings.OrchestrationPane.52e0634e2c', - 'Ask a coordinator agent to use orchestration for handoffs, worktree handovers, and sequential or parallel child agents.' - )} -

-
- -
- {getOrchestrationUsageExamples().map((example) => { - const Icon = EXAMPLE_ICONS[example.id as keyof typeof EXAMPLE_ICONS] ?? Workflow - return ( - - ) - })} -
-
- - {getOrchestrationUsageExamples().map((example) => { - const Icon = EXAMPLE_ICONS[example.id as keyof typeof EXAMPLE_ICONS] ?? Workflow - return ( - setSelectedExampleId(open ? example.id : null)} - /> - ) - })} + ) } diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index ef50a504a3a..d31b4ce38b1 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -54,6 +54,7 @@ import { SshPane } from './SshPane' import { ExperimentalPane } from './ExperimentalPane' import { AgentsPane } from './AgentsPane' import { OrchestrationPane } from './OrchestrationPane' +import { LinearAgentSkillPane } from './LinearAgentSkillPane' import { AccountsPane } from './AccountsPane' import { StatsPane } from '../stats/StatsPane' import { IntegrationsPane } from './IntegrationsPane' @@ -101,6 +102,7 @@ import { useInstalledAgentSkill } from '@/hooks/useInstalledAgentSkills' import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime' +import { useLinearProviderConnected } from '@/hooks/useLinearProviderConnected' import { useSkillFreshness } from '@/hooks/useSkillFreshness' import { getSkillFreshnessDisplayStatus } from '@/lib/skill-freshness-display-status' import { deriveNeededSectionIds, getInitialMountedSectionIds } from './settings-load-performance' @@ -344,6 +346,9 @@ function Settings(): React.JSX.Element { const isMac = isMacUserAgent() const isWebClient = isWebClientLocation() const showDesktopOnlySettings = !isWebClient + // Why: the Linear capability section mirrors the nav registry's gate so the + // sidebar entry and the rendered section appear/disappear together. + const linearConnected = useLinearProviderConnected() const activeSkillRuntime = useActiveProjectSkillRuntime() const orchestrationSkill = useInstalledAgentSkill(ORCHESTRATION_SKILL_NAME, { discoveryTarget: activeSkillRuntime.discoveryTarget, @@ -1256,6 +1261,20 @@ function Settings(): React.JSX.Element { {isSectionMounted('orchestration') ? : null} + {linearConnected ? ( + + {isSectionMounted('linear') ? : null} + + ) : null} + {showDesktopOnlySettings ? ( <> {prompt} @@ -30,9 +30,7 @@ function OrchestrationExamplePromptText(props: { prompt: string }): React.JSX.El {part} {index < parts.length - 1 ? ( - - {ORCHESTRATION_SKILL_SLASH_COMMAND} - + {slashCommand} ) : null} ))} @@ -40,20 +38,23 @@ function OrchestrationExamplePromptText(props: { prompt: string }): React.JSX.El ) } -export function OrchestrationExampleDialog(props: { - example: OrchestrationUsageExample +export function SkillUsageExampleDialog(props: { + example: SkillUsageExample + // Why: the copyable prompt highlights the skill's slash command so the reader + // sees which skill to invoke; each skill passes its own (e.g. /orchestration). + slashCommand: string icon?: LucideIcon open: boolean onOpenChange: (open: boolean) => void }): React.JSX.Element { - const { example, icon: Icon, open, onOpenChange } = props + const { example, slashCommand, icon: Icon, open, onOpenChange } = props const copyPrompt = async (prompt: string): Promise => { try { await window.api.ui.writeClipboardText(prompt) toast.success( translate( - 'auto.components.settings.OrchestrationExamplesDialog.80c6f2feb8', + 'auto.components.settings.SkillUsageExampleDialog.copiedPrompt', 'Copied example prompt.' ) ) @@ -62,7 +63,7 @@ export function OrchestrationExampleDialog(props: { error instanceof Error ? error.message : translate( - 'auto.components.settings.OrchestrationExamplesDialog.4e46da1889', + 'auto.components.settings.SkillUsageExampleDialog.copyFailed', 'Failed to copy prompt.' ) ) @@ -93,7 +94,7 @@ export function OrchestrationExampleDialog(props: {

- +

diff --git a/src/renderer/src/components/settings/SkillUsageExamplesSection.tsx b/src/renderer/src/components/settings/SkillUsageExamplesSection.tsx new file mode 100644 index 00000000000..f4d615cf6bf --- /dev/null +++ b/src/renderer/src/components/settings/SkillUsageExamplesSection.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react' +import type { LucideIcon } from 'lucide-react' +import type { SkillUsageExample } from '@/lib/skill-usage-example' +import { SkillUsageExampleDialog } from './SkillUsageExampleDialog' + +// Why: orchestration and Linear both present "How to use it" example cards that +// open a copyable prompt dialog; sharing one section keeps the two in lockstep. +export function SkillUsageExamplesSection({ + heading, + description, + examples, + resolveIcon, + slashCommand +}: { + heading: string + description: string + examples: readonly SkillUsageExample[] + resolveIcon: (example: SkillUsageExample) => LucideIcon + slashCommand: string +}): React.JSX.Element { + const [selectedExampleId, setSelectedExampleId] = useState(null) + + return ( +
+
+

{heading}

+

{description}

+
+ +
+ {examples.map((example) => { + const Icon = resolveIcon(example) + return ( + + ) + })} +
+ + {examples.map((example) => ( + setSelectedExampleId(open ? example.id : null)} + /> + ))} +
+ ) +} 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 8992e73499a..c2f876bb0f4 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 @@ -23,6 +23,12 @@ const updateCapableCallers = new Map([ 'src/renderer/src/components/settings/ComputerUseSkillSetupPanel.tsx', ['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. + 'src/renderer/src/components/settings/LinearAgentSkillPane.tsx', + ['getLinearAgentSkillUpdateCommand', 'installedCommand={updateCommand}'] + ], [ 'src/renderer/src/components/settings/EphemeralVmsPane.tsx', ['EPHEMERAL_VMS_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}'] diff --git a/src/renderer/src/components/settings/linear-agent-skill-search.ts b/src/renderer/src/components/settings/linear-agent-skill-search.ts new file mode 100644 index 00000000000..3fdf6dbe2b9 --- /dev/null +++ b/src/renderer/src/components/settings/linear-agent-skill-search.ts @@ -0,0 +1,35 @@ +import { translate } from '@/i18n/i18n' +import { translateSearchKeyword } from './settings-search-keywords' +import { createLocalizedCatalog } from '@/i18n/localized-catalog' + +export const getLinearAgentSkillPaneSearchEntries = createLocalizedCatalog(() => [ + { + title: translate('auto.components.settings.linear.agent.skill.search.title', 'Linear'), + description: translate( + 'auto.components.settings.linear.agent.skill.search.description', + 'Give agents the skill to read and update your linked Linear tickets.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.linear.agent.skill.search.linear', + 'linear' + ), + ...translateSearchKeyword( + 'auto.components.settings.linear.agent.skill.search.tickets', + 'tickets' + ), + ...translateSearchKeyword( + 'auto.components.settings.linear.agent.skill.search.issues', + 'issues' + ), + ...translateSearchKeyword( + 'auto.components.settings.linear.agent.skill.search.skill', + 'skill' + ), + ...translateSearchKeyword( + 'auto.components.settings.linear.agent.skill.search.orcaLinear', + 'orca-linear' + ) + ] + } +]) diff --git a/src/renderer/src/hooks/useLinearProviderConnected.ts b/src/renderer/src/hooks/useLinearProviderConnected.ts new file mode 100644 index 00000000000..52735e755dc --- /dev/null +++ b/src/renderer/src/hooks/useLinearProviderConnected.ts @@ -0,0 +1,13 @@ +import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' +import { useAppStore } from '@/store' + +// Why: the Settings sidebar registry and the Settings page must agree on whether +// the Linear section is visible; sharing this selector keeps the nav entry and +// the rendered section from drifting. The context-key guard rejects a status +// fetched for a different runtime environment than the active one. +export function useLinearProviderConnected(): boolean { + const linearStatus = useAppStore((s) => s.linearStatus) + const linearStatusContextKey = useAppStore((s) => s.linearStatusContextKey) + const settings = useAppStore((s) => s.settings) + return linearStatusContextKey === getProviderRuntimeContextKey(settings) && linearStatus.connected +} diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts index 8d17ab1444f..b827cedcb06 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts @@ -13,13 +13,20 @@ const repo = { } satisfies Repo function ids( - args: { isMac?: boolean; isWindows?: boolean; isWebClient?: boolean; isDev?: boolean } = {} + args: { + isMac?: boolean + isWindows?: boolean + isWebClient?: boolean + isDev?: boolean + isLinearConnected?: boolean + } = {} ): string[] { return buildSettingsNavigationMetadata({ isMac: args.isMac ?? false, isWindows: args.isWindows ?? false, isWebClient: args.isWebClient ?? false, isDev: args.isDev ?? false, + isLinearConnected: args.isLinearConnected ?? false, repos: [repo] }).map((section) => section.id) } @@ -40,6 +47,27 @@ describe('settings navigation metadata', () => { ]) }) + it('adds the Linear capability section right after Orchestration only when connected', () => { + expect(ids()).not.toContain('linear') + + const connectedIds = ids({ isLinearConnected: true }) + expect(connectedIds).toContain('linear') + expect(connectedIds.indexOf('linear')).toBe(connectedIds.indexOf('orchestration') + 1) + + const linearSection = buildSettingsNavigationMetadata({ + isMac: false, + isWindows: false, + isWebClient: false, + isLinearConnected: true, + repos: [repo] + }).find((section) => section.id === 'linear') + expect(linearSection?.group).toBe('capabilities') + }) + + it('keeps the Linear capability section available on web clients when connected', () => { + expect(ids({ isWebClient: true, isLinearConnected: true })).toContain('linear') + }) + it('places Mobile under Set Up instead of its own sidebar group', () => { const sections = buildSettingsNavigationMetadata({ isMac: false, diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts index 60ab54ac4c4..b13ee5f9399 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts @@ -34,6 +34,7 @@ import { Wrench } from 'lucide-react' import { OrcaLogoSettingsIcon } from '@/components/settings/orca-logo-settings-icon' +import { LinearIcon } from '@/components/icons/LinearIcon' import type { Repo } from '../../../shared/types' import { getRepoKindLabel } from '../../../shared/repo-kind' import { useAppStore } from '@/store' @@ -55,6 +56,7 @@ import { getQuickCommandsPaneSearchEntries } from '@/components/settings/quick-c import { getBrowserPaneCombinedSearchEntries } from '@/components/settings/browser-pane-search' import { getNotificationsPaneSearchEntries } from '@/components/settings/notifications-search' import { getOrchestrationPaneSearchEntries } from '@/components/settings/orchestration-search' +import { getLinearAgentSkillPaneSearchEntries } from '@/components/settings/linear-agent-skill-search' import { getRuntimeEnvironmentsSearchEntry, getWebRuntimeEnvironmentsSearchEntry @@ -78,6 +80,7 @@ import { useWindowsTerminalCapabilities } from '@/lib/windows-terminal-capabilities' import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' +import { useLinearProviderConnected } from '@/hooks/useLinearProviderConnected' import { translate } from '@/i18n/i18n' export { isWebClientLocation } from '@/lib/web-client-location' @@ -113,6 +116,7 @@ export function buildSettingsNavigationMetadata({ isWindowsTerminalHost = isWindows, isWebClient, isDev = import.meta.env.DEV, + isLinearConnected = false, repos }: { isMac: boolean @@ -120,6 +124,7 @@ export function buildSettingsNavigationMetadata({ isWindowsTerminalHost?: boolean isWebClient: boolean isDev?: boolean + isLinearConnected?: boolean repos: readonly Repo[] }): SettingsNavSection[] { const showDesktopOnlySettings = !isWebClient @@ -179,6 +184,23 @@ export function buildSettingsNavigationMetadata({ searchEntries: getOrchestrationPaneSearchEntries(), group: 'capabilities' }, + // Why: only surfaced once Linear is connected — a capability that needs a + // linked provider before the agent skill has anything to act on. + ...(isLinearConnected + ? [ + { + id: 'linear', + title: translate('auto.hooks.useSettingsNavigationMetadata.linearTitle', 'Linear'), + description: translate( + 'auto.hooks.useSettingsNavigationMetadata.linearDescription', + 'Give agents the skill to read and update your linked Linear tickets.' + ), + icon: LinearIcon, + searchEntries: getLinearAgentSkillPaneSearchEntries(), + group: 'capabilities' + } + ] + : []), ...(showDesktopOnlySettings ? [ { @@ -584,6 +606,7 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] { const isMac = isMacUserAgent() const isWindows = isWindowsUserAgent() const isWebClient = isWebClientLocation() + const isLinearConnected = useLinearProviderConnected() const windowsTerminalCapabilityOwnerKey = getWindowsTerminalCapabilityOwnerKey( settings?.activeRuntimeEnvironmentId ) @@ -607,9 +630,10 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] { isWindowsTerminalHost, isWebClient, isDev: import.meta.env.DEV, + isLinearConnected, repos }), // oxlint-disable-next-line react-hooks/exhaustive-deps -- activeLocale is read implicitly by the translate() calls inside buildSettingsNavigationMetadata; without it the memo keeps the previous language's sections. - [isMac, isWindows, isWindowsTerminalHost, isWebClient, repos, activeLocale] + [isMac, isWindows, isWindowsTerminalHost, isWebClient, isLinearConnected, repos, activeLocale] ) } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 0b6f41f8173..ea864c13e70 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -611,6 +611,17 @@ "de50bef680": "macOS is blocking Orca notifications" } } + }, + "linear": { + "usage": { + "examples": { + "readTicket": "Read the linked ticket", + "postUpdate": "Post a progress update", + "moveState": "Move the ticket forward", + "attachPr": "Attach the PR link", + "triageFollowups": "Triage and create follow-ups" + } + } } }, "hooks": { @@ -728,7 +739,9 @@ "devSearchKeywordSonner": "sonner", "devSearchKeywordError": "error", "devSearchKeywordNotification": "notification", - "projectHostsSummary": "{{value0}} hosts" + "projectHostsSummary": "{{value0}} hosts", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "useAppMenuPaste": { "pasteTooLarge": "Paste is too large." @@ -6004,13 +6017,6 @@ "3743ed080c": "Set command", "f79084947b": "New app" }, - "OrchestrationExamplesDialog": { - "3d1aa105e3": "Copy prompt", - "9b4c004998": "Done", - "969cec9739": "Copy {{value0}} example prompt", - "4e46da1889": "Failed to copy prompt.", - "80c6f2feb8": "Copied example prompt." - }, "OrchestrationPane": { "52e0634e2c": "Ask a coordinator agent to use orchestration for handoffs, worktree handovers, and sequential or parallel child agents.", "ae79504732": "How to use it", @@ -6522,7 +6528,9 @@ "2309068a6f": "destructive", "65358016ea": "Discard", "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states." + "devDescription": "Dev-only tools for exercising UI states.", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "SettingsFormControls": { "42a4d15a30": "No matching fonts.", @@ -9111,6 +9119,10 @@ "description": "Let your agents read and edit Linear tasks.", "copyCommand": "Copy command" } + }, + "search": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets." } } } @@ -9191,6 +9203,21 @@ "availability": "Available on", "testFlight": "TestFlight", "androidApk": "Android APK" + }, + "SkillUsageExampleDialog": { + "copiedPrompt": "Copied example prompt.", + "copyFailed": "Failed to copy prompt.", + "copyExampleAria": "Copy {{value0}} example prompt", + "done": "Done", + "copyPrompt": "Copy prompt" + }, + "LinearAgentSkillPane": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets.", + "skillTitle": "Linear skill", + "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", + "howToUse": "How to use it", + "howToUseDescription": "Ask an agent working a Linear-linked worktree to read context, post updates, move the ticket, or attach the PR." } }, "right": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 2c5a35ddbb4..c198d05d8fe 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -588,6 +588,17 @@ "de50bef680": "macOS está bloqueando las notificaciones de Orca" } } + }, + "linear": { + "usage": { + "examples": { + "readTicket": "Read the linked ticket", + "postUpdate": "Post a progress update", + "moveState": "Move the ticket forward", + "attachPr": "Attach the PR link", + "triageFollowups": "Triage and create follow-ups" + } + } } }, "hooks": { @@ -705,7 +716,9 @@ "devSearchKeywordSonner": "sonner", "devSearchKeywordError": "error", "devSearchKeywordNotification": "notification", - "projectHostsSummary": "{{value0}} hosts" + "projectHostsSummary": "{{value0}} hosts", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "useAppMenuPaste": { "pasteTooLarge": "El contenido pegado es demasiado grande." @@ -5944,13 +5957,6 @@ "3743ed080c": "Establecer comando", "f79084947b": "Nueva aplicación" }, - "OrchestrationExamplesDialog": { - "3d1aa105e3": "Copiar prompt", - "9b4c004998": "Hecho", - "969cec9739": "Copiar prompt de ejemplo {{value0}}", - "4e46da1889": "No se pudo copiar el prompt.", - "80c6f2feb8": "Prompt de ejemplo copiado." - }, "OrchestrationPane": { "52e0634e2c": "Pídele a un agente coordinador que use la orquestación para handoffs, handoffs de worktree y agentes secundarios secuenciales o paralelos.", "ae79504732": "Cómo usarlo", @@ -6462,7 +6468,9 @@ "2309068a6f": "destructivo", "65358016ea": "Desechar", "dev": "Herramientas de desarrollo", - "devDescription": "Herramientas solo de desarrollo para probar estados de la UI." + "devDescription": "Herramientas solo de desarrollo para probar estados de la UI.", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "SettingsFormControls": { "42a4d15a30": "No hay fuentes coincidentes.", @@ -9088,6 +9096,10 @@ "description": "Permite que tus agentes lean y editen tareas de Linear.", "copyCommand": "Copiar comando" } + }, + "search": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets." } } } @@ -9168,6 +9180,21 @@ "availability": "Disponible en", "testFlight": "TestFlight", "androidApk": "APK de Android" + }, + "SkillUsageExampleDialog": { + "copiedPrompt": "Copied example prompt.", + "copyFailed": "Failed to copy prompt.", + "copyExampleAria": "Copy {{value0}} example prompt", + "done": "Done", + "copyPrompt": "Copy prompt" + }, + "LinearAgentSkillPane": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets.", + "skillTitle": "Linear skill", + "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", + "howToUse": "How to use it", + "howToUseDescription": "Ask an agent working a Linear-linked worktree to read context, post updates, move the ticket, or attach the PR." } }, "right": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index f622982f00e..ae240e364fe 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -588,6 +588,17 @@ "de50bef680": "macOS が Orca の通知をブロックしています" } } + }, + "linear": { + "usage": { + "examples": { + "readTicket": "Read the linked ticket", + "postUpdate": "Post a progress update", + "moveState": "Move the ticket forward", + "attachPr": "Attach the PR link", + "triageFollowups": "Triage and create follow-ups" + } + } } }, "hooks": { @@ -705,7 +716,9 @@ "devSearchKeywordSonner": "sonner", "devSearchKeywordError": "error", "devSearchKeywordNotification": "notification", - "projectHostsSummary": "{{value0}} 台のホスト" + "projectHostsSummary": "{{value0}} 台のホスト", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "useAppMenuPaste": { "pasteTooLarge": "貼り付け内容が大きすぎます。" @@ -5966,13 +5979,6 @@ "3743ed080c": "セットコマンド", "f79084947b": "新規アプリ" }, - "OrchestrationExamplesDialog": { - "3d1aa105e3": "プロンプトのコピー", - "9b4c004998": "完了", - "969cec9739": "{{value0}} プロンプトの例をコピーします", - "4e46da1889": "プロンプトのコピーに失敗しました。", - "80c6f2feb8": "プロンプトの例をコピーしました。" - }, "OrchestrationPane": { "52e0634e2c": "コーディネーター agents に、ハンドオフ、ワークツリー ハンドオーバー、および順次または並列の子 agents にオーケストレーションを使用するように依頼します。", "ae79504732": "使い方", @@ -6484,7 +6490,9 @@ "2309068a6f": "破壊的な", "65358016ea": "破棄", "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states." + "devDescription": "Dev-only tools for exercising UI states.", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "SettingsFormControls": { "42a4d15a30": "一致するフォントがありません。", @@ -9088,6 +9096,10 @@ "description": "エージェントにLinearタスクの読み取りと編集を許可します。", "copyCommand": "コマンドをコピー" } + }, + "search": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets." } } } @@ -9168,6 +9180,21 @@ "availability": "利用可能:", "testFlight": "TestFlight", "androidApk": "Android APK" + }, + "SkillUsageExampleDialog": { + "copiedPrompt": "Copied example prompt.", + "copyFailed": "Failed to copy prompt.", + "copyExampleAria": "Copy {{value0}} example prompt", + "done": "Done", + "copyPrompt": "Copy prompt" + }, + "LinearAgentSkillPane": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets.", + "skillTitle": "Linear skill", + "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", + "howToUse": "How to use it", + "howToUseDescription": "Ask an agent working a Linear-linked worktree to read context, post updates, move the ticket, or attach the PR." } }, "right": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index ea0603e7c97..8d2bb9ac40b 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -588,6 +588,17 @@ "de50bef680": "macOS가 Orca 알림을 차단하고 있습니다" } } + }, + "linear": { + "usage": { + "examples": { + "readTicket": "Read the linked ticket", + "postUpdate": "Post a progress update", + "moveState": "Move the ticket forward", + "attachPr": "Attach the PR link", + "triageFollowups": "Triage and create follow-ups" + } + } } }, "hooks": { @@ -705,7 +716,9 @@ "devSearchKeywordSonner": "sonner", "devSearchKeywordError": "error", "devSearchKeywordNotification": "notification", - "projectHostsSummary": "호스트 {{value0}}개" + "projectHostsSummary": "호스트 {{value0}}개", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "useAppMenuPaste": { "pasteTooLarge": "붙여넣기 내용이 너무 큽니다." @@ -5929,13 +5942,6 @@ "3743ed080c": "명령 설정", "f79084947b": "새로운 앱" }, - "OrchestrationExamplesDialog": { - "3d1aa105e3": "프롬프트 복사", - "9b4c004998": "완료", - "969cec9739": "{{value0}} 예시 프롬프트 복사", - "4e46da1889": "프롬프트를 복사하지 못했습니다.", - "80c6f2feb8": "예시 프롬프트를 복사했습니다." - }, "OrchestrationPane": { "52e0634e2c": "핸드오프, 작업 트리 핸드오버, 순차 또는 병렬 하위 agents에 대한 오케스트레이션을 사용하도록 코디네이터 agents에 요청하세요.", "ae79504732": "사용방법", @@ -6447,7 +6453,9 @@ "2309068a6f": "파괴적", "65358016ea": "버리기", "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states." + "devDescription": "Dev-only tools for exercising UI states.", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "SettingsFormControls": { "42a4d15a30": "일치하는 글꼴이 없습니다.", @@ -9088,6 +9096,10 @@ "description": "에이전트가 Linear 작업을 읽고 편집할 수 있도록 합니다.", "copyCommand": "명령 복사" } + }, + "search": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets." } } } @@ -9168,6 +9180,21 @@ "availability": "이용 가능:", "testFlight": "TestFlight", "androidApk": "Android APK" + }, + "SkillUsageExampleDialog": { + "copiedPrompt": "Copied example prompt.", + "copyFailed": "Failed to copy prompt.", + "copyExampleAria": "Copy {{value0}} example prompt", + "done": "Done", + "copyPrompt": "Copy prompt" + }, + "LinearAgentSkillPane": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets.", + "skillTitle": "Linear skill", + "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", + "howToUse": "How to use it", + "howToUseDescription": "Ask an agent working a Linear-linked worktree to read context, post updates, move the ticket, or attach the PR." } }, "right": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index f14765dabb1..e14e205c838 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -588,6 +588,17 @@ "de50bef680": "macOS 正在阻止 Orca 通知" } } + }, + "linear": { + "usage": { + "examples": { + "readTicket": "Read the linked ticket", + "postUpdate": "Post a progress update", + "moveState": "Move the ticket forward", + "attachPr": "Attach the PR link", + "triageFollowups": "Triage and create follow-ups" + } + } } }, "hooks": { @@ -705,7 +716,9 @@ "devSearchKeywordSonner": "sonner", "devSearchKeywordError": "error", "devSearchKeywordNotification": "notification", - "projectHostsSummary": "{{value0}} 个主机" + "projectHostsSummary": "{{value0}} 个主机", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "useAppMenuPaste": { "pasteTooLarge": "粘贴内容过大。" @@ -5929,13 +5942,6 @@ "3743ed080c": "设置命令", "f79084947b": "新应用程序" }, - "OrchestrationExamplesDialog": { - "3d1aa105e3": "复制提示", - "9b4c004998": "完成", - "969cec9739": "复制 {{value0}} 示例提示", - "4e46da1889": "复制提示失败。", - "80c6f2feb8": "复制示例提示。" - }, "OrchestrationPane": { "52e0634e2c": "要求协调智能体使用编排进行切换、工作树切换以及顺序或并行子智能体。", "ae79504732": "如何使用", @@ -6447,7 +6453,9 @@ "2309068a6f": "destructive", "65358016ea": "放弃", "dev": "开发工具", - "devDescription": "仅用于调试 UI 状态的开发工具。" + "devDescription": "仅用于调试 UI 状态的开发工具。", + "linearTitle": "Linear", + "linearDescription": "Give agents the skill to read and update your linked Linear tickets." }, "SettingsFormControls": { "42a4d15a30": "没有匹配的字体。", @@ -9088,6 +9096,10 @@ "description": "让您的智能体读取和编辑 Linear 任务。", "copyCommand": "复制命令" } + }, + "search": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets." } } } @@ -9168,6 +9180,21 @@ "availability": "可用平台:", "testFlight": "TestFlight", "androidApk": "Android APK" + }, + "SkillUsageExampleDialog": { + "copiedPrompt": "Copied example prompt.", + "copyFailed": "Failed to copy prompt.", + "copyExampleAria": "Copy {{value0}} example prompt", + "done": "Done", + "copyPrompt": "Copy prompt" + }, + "LinearAgentSkillPane": { + "title": "Linear", + "description": "Give agents the skill to read and update your linked Linear tickets.", + "skillTitle": "Linear skill", + "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", + "howToUse": "How to use it", + "howToUseDescription": "Ask an agent working a Linear-linked worktree to read context, post updates, move the ticket, or attach the PR." } }, "right": { diff --git a/src/renderer/src/lib/linear-usage-examples.ts b/src/renderer/src/lib/linear-usage-examples.ts new file mode 100644 index 00000000000..f5f77e33936 --- /dev/null +++ b/src/renderer/src/lib/linear-usage-examples.ts @@ -0,0 +1,43 @@ +import { translate } from '@/i18n/i18n' +import { createLocalizedCatalog } from '@/i18n/localized-catalog' +import type { SkillUsageExample } from './skill-usage-example' + +export const getLinearUsageExamples = createLocalizedCatalog((): SkillUsageExample[] => [ + { + id: 'read-ticket', + title: translate('auto.lib.linear.usage.examples.readTicket', 'Read the linked ticket'), + summary: "Pull the linked Linear issue's full context before starting work.", + prompt: + 'Use /orca-linear to read the linked Linear issue for this worktree, then summarize the goal and acceptance criteria before you start.' + }, + { + id: 'post-update', + title: translate('auto.lib.linear.usage.examples.postUpdate', 'Post a progress update'), + summary: 'Comment progress or a completion summary back to the Linear issue.', + prompt: + 'Use /orca-linear to post a completion update on the linked Linear issue with what changed and how it was verified.' + }, + { + id: 'move-state', + title: translate('auto.lib.linear.usage.examples.moveState', 'Move the ticket forward'), + summary: 'Advance the Linear workflow state as the work progresses.', + prompt: + 'Use /orca-linear to move the linked Linear issue to In Review now that the change is ready.' + }, + { + id: 'attach-pr', + title: translate('auto.lib.linear.usage.examples.attachPr', 'Attach the PR link'), + summary: 'Link the pull request to the Linear issue when you open it.', + prompt: 'Use /orca-linear to attach this PR to the linked Linear issue with a PR link.' + }, + { + id: 'triage-followups', + title: translate( + 'auto.lib.linear.usage.examples.triageFollowups', + 'Triage and create follow-ups' + ), + summary: 'Set assignee, priority, or estimate, and file parented follow-up tickets.', + prompt: + 'Use /orca-linear to triage the linked Linear issue — set priority and estimate — and create a parented follow-up ticket for the deferred cleanup.' + } +]) diff --git a/src/renderer/src/lib/orchestration-usage-examples.ts b/src/renderer/src/lib/orchestration-usage-examples.ts index 8840aa01f19..ab40edd5310 100644 --- a/src/renderer/src/lib/orchestration-usage-examples.ts +++ b/src/renderer/src/lib/orchestration-usage-examples.ts @@ -1,13 +1,8 @@ import { translate } from '@/i18n/i18n' import { createLocalizedCatalog } from '@/i18n/localized-catalog' -export type OrchestrationUsageExample = { - id: string - title: string - summary: string - prompt: string -} +import type { SkillUsageExample } from './skill-usage-example' -export const getOrchestrationUsageExamples = createLocalizedCatalog(() => [ +export const getOrchestrationUsageExamples = createLocalizedCatalog((): SkillUsageExample[] => [ { id: 'handoff', title: translate('auto.lib.orchestration.usage.examples.5e0d489fe1', 'Hand off an active task'), diff --git a/src/renderer/src/lib/skill-usage-example.ts b/src/renderer/src/lib/skill-usage-example.ts new file mode 100644 index 00000000000..b29fb93747c --- /dev/null +++ b/src/renderer/src/lib/skill-usage-example.ts @@ -0,0 +1,6 @@ +export type SkillUsageExample = { + id: string + title: string + summary: string + prompt: string +}