diff --git a/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx b/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx index f169591d5e1..bf36550e5a6 100644 --- a/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx +++ b/src/renderer/src/components/emulator-pane/MobileEmulatorAgentSetupGuideSteps.tsx @@ -7,7 +7,7 @@ import { ensureOrcaCliAvailableForAgentSkillTerminal } from '@/lib/agent-skill-cli-prerequisite' import { AgentSkillSetupPanel } from '../settings/AgentSkillSetupPanel' -import { StepBadge } from '../settings/BrowserUseStepBadge' +import { StepBadge } from '../settings/SetupStepBadge' import { Button } from '../ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { diff --git a/src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-cli-state.ts b/src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-cli-state.ts index b20565d823f..4e350eaf0a4 100644 --- a/src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-cli-state.ts +++ b/src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-cli-state.ts @@ -1,5 +1,5 @@ import type { CliInstallStatus } from '../../../../shared/cli-install-types' -import type { StepState } from '../settings/BrowserUseStepBadge' +import type { StepState } from '../settings/SetupStepBadge' export function getMobileEmulatorCliPathNeedsAttention(status: CliInstallStatus | null): boolean { return status?.state === 'installed' && status.pathConfigured === false diff --git a/src/renderer/src/components/integration-status-pill.tsx b/src/renderer/src/components/integration-status-pill.tsx index 418826cbe6b..a36faa2a926 100644 --- a/src/renderer/src/components/integration-status-pill.tsx +++ b/src/renderer/src/components/integration-status-pill.tsx @@ -4,7 +4,7 @@ export type IntegrationStatusTone = 'connected' | 'attention' | 'neutral' const TONE_CLASSES: Record = { connected: { - pill: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-300', + pill: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300', dot: 'bg-emerald-500' }, attention: { diff --git a/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx b/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx index 69d2360b7e7..533144f8bdf 100644 --- a/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx +++ b/src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx @@ -14,7 +14,9 @@ const mocks = vi.hoisted(() => ({ clipboardWrite: vi.fn(), terminalProps: [] as { command: string; description: string }[], toastError: vi.fn(), - toastSuccess: vi.fn() + toastSuccess: vi.fn(), + skillsChanged: vi.fn(), + skillsRefreshed: vi.fn() })) vi.mock('sonner', () => ({ @@ -24,6 +26,11 @@ vi.mock('sonner', () => ({ } })) +vi.mock('@/hooks/useInstalledAgentSkills', () => ({ + notifyInstalledAgentSkillsChanged: mocks.skillsChanged, + notifyInstalledAgentSkillsRefreshed: mocks.skillsRefreshed +})) + vi.mock('../onboarding/OnboardingInlineCommandTerminal', () => ({ OnboardingInlineCommandTerminal: (props: { command: string; description: string }) => { mocks.terminalProps.push(props) @@ -39,6 +46,12 @@ vi.mock('../onboarding/OnboardingInlineCommandTerminal', () => ({ } })) +vi.mock('../skills/SkillFreshnessStatusPill', () => ({ + SkillFreshnessStatusPill: ({ skillName }: { skillName: string }) => ( + {skillName} + ) +})) + function panelProps( overrides: Partial> = {} ): ComponentProps { @@ -124,6 +137,8 @@ describe('AgentSkillSetupPanel', () => { mocks.terminalProps.length = 0 mocks.toastError.mockReset() mocks.toastSuccess.mockReset() + mocks.skillsChanged.mockReset() + mocks.skillsRefreshed.mockReset() Object.defineProperty(window, 'api', { configurable: true, value: { @@ -157,6 +172,26 @@ describe('AgentSkillSetupPanel', () => { expect(buttonLabels(html)).toContain('Re-check') }) + it('surfaces skill freshness under hideHeader for guided setup hubs', () => { + const html = renderPanel({ + installed: true, + hideHeader: true, + description: null, + freshnessSkillName: 'orca-linear' + }) + + expect(html).toContain('data-testid="skill-freshness"') + expect(html).toContain('orca-linear') + expect(html).not.toContain('CLI skill') + }) + + it('does not render an empty description paragraph when description is null', () => { + const html = renderPanel({ description: null, hideHeader: true }) + + expect(html).not.toContain('text-muted-foreground">null') + expect(html).not.toMatch(/

<\/p>/) + }) + it('hides only re-check when installed re-checks are disabled', () => { const html = renderPanel({ installed: true, showRecheckWhenInstalled: false }) @@ -217,6 +252,25 @@ describe('AgentSkillSetupPanel', () => { expect(buttonMarkupByLabel(html, 'Install')).toContain('disabled=""') }) + it('notifies sibling surfaces only after re-check finishes', async () => { + let finishRecheck: (() => void) | null = null + const recheck = new Promise((resolve) => { + finishRecheck = resolve + }) + await renderInteractivePanel({ onRecheck: () => recheck }) + + await clickButton('Re-check') + expect(mocks.skillsRefreshed).not.toHaveBeenCalled() + + await act(async () => { + finishRecheck?.() + await recheck + }) + + expect(mocks.skillsRefreshed).toHaveBeenCalledOnce() + expect(mocks.skillsChanged).not.toHaveBeenCalled() + }) + it('opens not-installed setup with the install command for preview, copy, and terminal', async () => { await renderInteractivePanel({ installedCommand: UPDATE_COMMAND }) diff --git a/src/renderer/src/components/settings/AgentSkillSetupPanel.tsx b/src/renderer/src/components/settings/AgentSkillSetupPanel.tsx index 3af3a1eea96..96bf0af5a8a 100644 --- a/src/renderer/src/components/settings/AgentSkillSetupPanel.tsx +++ b/src/renderer/src/components/settings/AgentSkillSetupPanel.tsx @@ -6,7 +6,10 @@ import { SkillFreshnessStatusPill } from '../skills/SkillFreshnessStatusPill' import { OnboardingInlineCommandTerminal } from '../onboarding/OnboardingInlineCommandTerminal' import { Button } from '../ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' -import { notifyInstalledAgentSkillsChanged } from '@/hooks/useInstalledAgentSkills' +import { + notifyInstalledAgentSkillsChanged, + notifyInstalledAgentSkillsRefreshed +} from '@/hooks/useInstalledAgentSkills' import { useMountedRef } from '@/hooks/useMountedRef' import { isOrcaCliAvailableOnPath } from '@/lib/agent-skill-cli-prerequisite' import { cn } from '@/lib/utils' @@ -82,8 +85,8 @@ export function AgentSkillSetupPanel({ onBeforeOpenTerminal, showInstallWhenInstalled = true, showRecheckWhenInstalled = true, - installLabel = 'Install', - installedInstallLabel = 'Update', + installLabel, + installedInstallLabel, installVariant = 'outline', actionHint, openingHint, @@ -91,6 +94,12 @@ export function AgentSkillSetupPanel({ onRecheck, freshnessSkillName }: AgentSkillSetupPanelProps): React.JSX.Element { + const resolvedInstallLabel = + installLabel ?? + translate('auto.components.settings.AgentSkillSetupPanel.installLabel', 'Install') + const resolvedInstalledInstallLabel = + installedInstallLabel ?? + translate('auto.components.settings.AgentSkillSetupPanel.updateLabel', 'Update') const [terminalOpen, setTerminalOpen] = useState(false) const [terminalCommand, setTerminalCommand] = useState(null) const [terminalOpening, setTerminalOpening] = useState(false) @@ -211,8 +220,8 @@ export function AgentSkillSetupPanel({ {terminalOpening ? translate('auto.components.settings.AgentSkillSetupPanel.5f818f12ab', 'Preparing...') : installed - ? installedInstallLabel - : installLabel} + ? resolvedInstalledInstallLabel + : resolvedInstallLabel} ) : null} {!installed || showRecheckWhenInstalled ? ( @@ -222,9 +231,10 @@ export function AgentSkillSetupPanel({ size="sm" className="gap-1.5" onClick={() => { - void onRecheck() - // Broadcast so sibling readiness subscribers (e.g. Task Sources cards) refresh too. - notifyInstalledAgentSkillsChanged() + void Promise.resolve(onRecheck()).then(() => { + // Reuse the completed scan so sibling surfaces sync without rediscovery. + notifyInstalledAgentSkillsRefreshed() + }) }} disabled={loading} > @@ -256,9 +266,15 @@ export function AgentSkillSetupPanel({ className={variant === 'card' ? cn('px-5 pt-5', terminalOpen ? 'pb-2' : 'pb-5') : 'pt-1.5'} > {hideHeader ? ( - error ? ( -

{error}

- ) : null + <> + {error ?

{error}

: null} + {/* hideHeader drops the title row; keep freshness so guided hubs still surface updates. */} + {installed && freshnessSkillName ? ( +
+ +
+ ) : null} + ) : (
{leading} @@ -302,7 +318,9 @@ export function AgentSkillSetupPanel({
)}
-

{description}

+ {description != null ? ( +

{description}

+ ) : null} {actionRow} {actionHint ?
{actionHint}
: null} {!installed && preInstallNotice && preInstallNoticeVisible ? ( diff --git a/src/renderer/src/components/settings/BrowserUseCliStep.tsx b/src/renderer/src/components/settings/BrowserUseCliStep.tsx index e3e2070ec2d..62474b3c2ae 100644 --- a/src/renderer/src/components/settings/BrowserUseCliStep.tsx +++ b/src/renderer/src/components/settings/BrowserUseCliStep.tsx @@ -2,7 +2,7 @@ import type { CliInstallStatus } from '../../../../shared/cli-install-types' import { Button } from '../ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' import { SearchableSetting } from './SearchableSetting' -import { StepBadge } from './BrowserUseStepBadge' +import { StepBadge } from './SetupStepBadge' import { getBrowserUsePaneSearchEntries } from './browser-use-search' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx index 3ac98241a12..1332883e4a1 100644 --- a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx +++ b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx @@ -16,7 +16,7 @@ import { } from '../ui/dropdown-menu' import { useAppStore } from '../../store' import { SearchableSetting } from './SearchableSetting' -import { StepBadge } from './BrowserUseStepBadge' +import { StepBadge } from './SetupStepBadge' import { getBrowserUsePaneSearchEntries } from './browser-use-search' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/settings/BrowserUseSkillStep.tsx b/src/renderer/src/components/settings/BrowserUseSkillStep.tsx index 20788509441..a9a63cefa73 100644 --- a/src/renderer/src/components/settings/BrowserUseSkillStep.tsx +++ b/src/renderer/src/components/settings/BrowserUseSkillStep.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { AgentSkillSetupPanel } from './AgentSkillSetupPanel' -import { StepBadge } from './BrowserUseStepBadge' +import { StepBadge } from './SetupStepBadge' import { translate } from '@/i18n/i18n' type Props = { diff --git a/src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx b/src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx index 798eee95066..75da65cd885 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx @@ -2,62 +2,85 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { LinearAgentSkillGuide } from './LinearAgentSkillGuide' +const baseStatus = { + connected: true, + connectionChecking: false, + skillInstalled: false, + skillChecking: false, + visibleInTasks: true +} + describe('LinearAgentSkillGuide', () => { - it('renders the setup checklist only', () => { + it('renders the setup checklist with an inlined skill panel', () => { const markup = renderToStaticMarkup( Skill install panel
} /> ) expect(markup).toContain('Setup checklist') expect(markup).toContain('2 of 3 ready') - expect(markup).toContain('Open Task Sources setup') - expect(markup).not.toContain('Good to know') + expect(markup).toContain('Task Sources') + expect(markup).toContain('Skill install panel') + expect(markup).not.toContain('Ready below') + expect(markup).not.toContain('Install below') + expect(markup).not.toContain('Agent skill') + expect(markup).not.toContain('Open Task Sources setup') }) it('marks the checklist complete when every step is done', () => { const markup = renderToStaticMarkup( Skill panel} /> ) expect(markup).toContain('All set') }) - it('does not report stale completed checks as ready', () => { + it('keeps durable progress while a skill recheck is in flight', () => { const markup = renderToStaticMarkup( Skill panel} /> ) - expect(markup).toContain('2 of 3 ready') + expect(markup).toContain('Checking…') + expect(markup).not.toContain('2 of 3 ready') expect(markup).not.toContain('All set') }) + + it('keeps durable progress while a connection check is in flight', () => { + const markup = renderToStaticMarkup( + Skill panel} + /> + ) + + expect(markup).toContain('Checking…') + expect(markup).not.toContain('2 of 3 ready') + }) }) diff --git a/src/renderer/src/components/settings/LinearAgentSkillGuide.tsx b/src/renderer/src/components/settings/LinearAgentSkillGuide.tsx index 2f1f6a735d6..0bbcc77ec5f 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillGuide.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillGuide.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import { Check, Circle } from 'lucide-react' import { Button } from '@/components/ui/button' import { IntegrationStatusPill } from '@/components/integration-status-pill' @@ -15,6 +16,9 @@ type LinearAgentSkillGuideProps = { status: LinearSetupStepStatus onOpenTaskSources: () => void onManageLinearAccess: () => void + // Why: skill install/update lives once under step 2 so the page does not + // repeat an "Agent skill" section after the checklist. + skillPanel: ReactNode } function SetupStatusIcon({ @@ -24,9 +28,12 @@ function SetupStatusIcon({ done: boolean checking: boolean }): React.JSX.Element { + // Keep a fixed size-5 slot so checking/done/pending never shift the column. if (checking) { return ( - + + + ) } if (done) { @@ -43,85 +50,21 @@ function SetupStatusIcon({ ) } -// Explain how connection, skill install, and Tasks visibility fit together. +// Connect, skill, and Tasks visibility in one checklist — skill UI is inlined. export function LinearAgentSkillGuide({ status, onOpenTaskSources, - onManageLinearAccess + onManageLinearAccess, + skillPanel }: LinearAgentSkillGuideProps): React.JSX.Element { - const setupRows: { - id: string - done: boolean - checking: boolean - title: string - body: string - actionLabel: string - onAction: () => void - }[] = [ - { - id: 'connect', - done: status.connected, - checking: status.connectionChecking, - title: translate( - 'auto.components.settings.LinearAgentSkillGuide.setupConnectTitle', - '1. Connect Linear' - ), - body: translate( - 'auto.components.settings.LinearAgentSkillGuide.setupConnectBody', - 'Personal API key so Orca can list issues and open linked workspaces.' - ), - actionLabel: status.connected - ? translate('auto.components.settings.LinearAgentSkillGuide.manageKeys', 'Manage keys') - : translate('auto.components.settings.LinearAgentSkillGuide.addAccess', 'Add access'), - onAction: onManageLinearAccess - }, - { - id: 'skill', - done: status.skillInstalled, - checking: status.skillChecking, - title: translate( - 'auto.components.settings.LinearAgentSkillGuide.setupSkillTitle', - '2. Install the agent skill' - ), - body: translate( - 'auto.components.settings.LinearAgentSkillGuide.setupSkillBody', - 'Gives coding agents /orca-linear for read, update, triage, and PR attach.' - ), - actionLabel: status.skillInstalled - ? translate('auto.components.settings.LinearAgentSkillGuide.skillReady', 'Ready below') - : translate('auto.components.settings.LinearAgentSkillGuide.installBelow', 'Install below'), - onAction: () => { - const behavior = - window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true - ? 'auto' - : 'smooth' - document - .getElementById('linear-agent-skill-install') - ?.scrollIntoView({ behavior, block: 'start' }) - } - }, - { - id: 'visible', - done: status.visibleInTasks, - checking: false, - title: translate( - 'auto.components.settings.LinearAgentSkillGuide.setupVisibleTitle', - '3. Show Linear in Tasks' - ), - body: translate( - 'auto.components.settings.LinearAgentSkillGuide.setupVisibleBody', - 'Keeps Linear in the Tasks source picker and sidebar shortcuts.' - ), - actionLabel: translate( - 'auto.components.settings.LinearAgentSkillGuide.openTaskSources', - 'Task Sources' - ), - onAction: onOpenTaskSources - } - ] - - const completed = setupRows.filter((row) => row.done && !row.checking).length - const allReady = completed === setupRows.length && setupRows.every((row) => !row.checking) + // Count durable outcomes even while a recheck runs so the pill does not flash + // from "All set" down to "2 of 3 ready" during skill/connection scans. + const checking = status.connectionChecking || status.skillChecking + const completed = [status.connected, status.skillInstalled, status.visibleInTasks].filter( + Boolean + ).length + const total = 3 + const allReady = completed === total && !checking return (
@@ -140,58 +83,109 @@ export function LinearAgentSkillGuide({ )}

- - {allReady - ? translate('auto.components.settings.LinearAgentSkillGuide.setupReady', 'All set') - : translate( - 'auto.components.settings.LinearAgentSkillGuide.setupProgress', - '{{done}} of {{total}} ready', - { done: completed, total: setupRows.length } - )} + + {checking + ? translate('auto.components.settings.LinearAgentSkillGuide.setupChecking', 'Checking…') + : allReady + ? translate('auto.components.settings.LinearAgentSkillGuide.setupReady', 'All set') + : translate( + 'auto.components.settings.LinearAgentSkillGuide.setupProgress', + '{{done}} of {{total}} ready', + { done: completed, total } + )}
- {setupRows.map((row) => ( -
+
+
+ +
+
+

+ {translate( + 'auto.components.settings.LinearAgentSkillGuide.setupConnectTitle', + '1. Connect Linear' + )} +

+

+ {translate( + 'auto.components.settings.LinearAgentSkillGuide.setupConnectBody', + 'Personal API key so Orca can list issues and open linked workspaces.' + )} +

+
+ +
+ +
+
- +
-

{row.title}

-

{row.body}

+

+ {translate( + 'auto.components.settings.LinearAgentSkillGuide.setupSkillTitle', + '2. Install the agent skill' + )} +

+

+ {translate( + 'auto.components.settings.LinearAgentSkillGuide.setupSkillBody', + 'Gives coding agents /orca-linear for reading, updates, triage, and attaching pull or merge requests.' + )} +

-
- ))} -
+ {skillPanel} +
-

- {translate( - 'auto.components.settings.LinearAgentSkillGuide.setupHint', - 'Prefer one guided place for connect + skill + visibility?' - )}{' '} - -

+
+
+ +
+
+

+ {translate( + 'auto.components.settings.LinearAgentSkillGuide.setupVisibleTitle', + '3. Show Linear in Tasks' + )} +

+

+ {translate( + 'auto.components.settings.LinearAgentSkillGuide.setupVisibleBody', + 'Keeps Linear in the Tasks source picker and sidebar shortcuts.' + )} +

+
+ +
+
) } diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx index abff8cd64d8..e8e0dd442a0 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx @@ -19,7 +19,9 @@ const mocks = vi.hoisted(() => ({ skillInstalled: true, updateSkillName: 'orca-linear', linearConnected: true, - visibleTaskProviders: ['github', 'linear'] as string[] + visibleTaskProviders: ['github', 'linear'] as string[], + openSettingsPage: vi.fn(), + openSettingsTarget: vi.fn() })) vi.mock('./AgentSkillSetupPanel', () => ({ @@ -27,7 +29,8 @@ vi.mock('./AgentSkillSetupPanel', () => ({ mocks.panelProps.push(props) return (
-

{String(props.title)}

+ {!props.hideHeader &&

{String(props.title)}

} + {props.description != null &&

{String(props.description)}

} {props.installed ? 'Installed' : 'Not installed'} {String(props.command)} {String(props.installedCommand)} @@ -95,8 +98,8 @@ vi.mock('@/store', () => ({ }) => unknown ) => selector({ - openSettingsPage: vi.fn(), - openSettingsTarget: vi.fn(), + openSettingsPage: mocks.openSettingsPage, + openSettingsTarget: mocks.openSettingsTarget, settings: { visibleTaskProviders: mocks.visibleTaskProviders }, linearStatusChecked: true, linearStatusContextKey: 'runtime-key', @@ -134,19 +137,68 @@ describe('LinearAgentSkillPane', () => { mocks.updateSkillName = 'orca-linear' mocks.linearConnected = true mocks.visibleTaskProviders = ['github', 'linear'] + mocks.openSettingsPage.mockClear() + mocks.openSettingsTarget.mockClear() }) - it('renders guide content, skill section, and usage examples', () => { + it('keeps a clickable route to the Integrations credentials pane', async () => { + const rendered = await renderPane() + const link = [...rendered.querySelectorAll('button')].find( + (button) => button.textContent === 'Integrations' + ) + + expect(link).toBeDefined() + await act(async () => { + link?.click() + }) + + expect(mocks.openSettingsPage).toHaveBeenCalled() + expect(mocks.openSettingsTarget).toHaveBeenCalledWith({ + pane: 'integrations', + repoId: null, + sectionId: 'integrations-linear' + }) + }) + + it('routes the connected checklist credential action to Integrations', async () => { + const rendered = await renderPane() + const manageKeys = [...rendered.querySelectorAll('button')].find( + (button) => button.textContent === 'Manage keys' + ) + + expect(manageKeys).toBeDefined() + await act(async () => { + manageKeys?.click() + }) + + expect(mocks.openSettingsTarget).toHaveBeenCalledWith({ + pane: 'integrations', + repoId: null, + sectionId: 'integrations-linear' + }) + }) + + it('does not pass an icon the hideHeader layout would drop', () => { + renderToStaticMarkup() + + expect(mocks.panelProps.at(-1)).not.toHaveProperty('icon') + }) + + it('renders checklist with inlined skill panel and usage examples', () => { const markup = renderToStaticMarkup() expect(markup).toContain('Setup checklist') - expect(markup).toContain('Agent skill') - expect(markup).toContain('Linear skill') + expect(markup).toContain('2. Install the agent skill') + expect(mocks.panelProps.at(-1)).toEqual( + expect.objectContaining({ hideHeader: true, description: null }) + ) + expect(markup).not.toContain('Agent skill') + expect(markup).not.toContain('Ready below') expect(markup).toContain('Example prompts') expect(markup).toContain('Good to know') expect(markup).toContain('Start from a Linear issue') expect(markup).toContain('All set') - // Notes stay after examples so setup stays primary. + // Skill lives inside the checklist; notes stay after examples. expect(markup.indexOf('Example prompts')).toBeLessThan(markup.indexOf('Good to know')) const examples = getLinearUsageExamples() expect(examples).toHaveLength(5) @@ -162,7 +214,9 @@ describe('LinearAgentSkillPane', () => { const markup = renderToStaticMarkup() expect(markup).toContain('2 of 3 ready') - expect(markup).toContain('Install below') + expect(mocks.panelProps.at(-1)).toEqual( + expect.objectContaining({ hideHeader: true, description: null }) + ) }) it('passes the orca-linear install/update commands and freshness on a local runtime', async () => { diff --git a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx index c54f6dcc97e..8461d16cccd 100644 --- a/src/renderer/src/components/settings/LinearAgentSkillPane.tsx +++ b/src/renderer/src/components/settings/LinearAgentSkillPane.tsx @@ -1,42 +1,25 @@ -import { useMemo, useState } from 'react' +import { useState } from 'react' import { ArrowRightCircle, BookOpen, Link2, ListTodo, MessageSquarePlus } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import { LinearApiKeyDialog } from '@/components/linear-api-key-dialog' import { LinearIcon } from '@/components/icons/LinearIcon' +import { Button } from '@/components/ui/button' import { useAppStore } from '@/store' -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 { getLinearAgentSkillUpdateTarget } from '@/lib/linear-agent-skill-update-command' +import { ORCA_LINEAR_SKILL_NAME } from '@/lib/agent-feature-install-commands' 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 { useLinearProviderConnected } from '@/hooks/useLinearProviderConnected' import { normalizeVisibleTaskProviders } from '../../../../shared/task-providers' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' import { AgentSkillSetupPanel } from './AgentSkillSetupPanel' -import { - buildSkillCommandForRuntime, - ensureWslCliAvailableForAgentSkillTerminal, - getWslCliDistroRequest -} from './CliSkillRuntimeSetup' import { LinearAgentSkillGuide } from './LinearAgentSkillGuide' import { LinearAgentSkillNotes } from './LinearAgentSkillNotes' import { getLinearAgentSkillPaneSearchEntries } from './linear-agent-skill-search' import { SearchableSetting } from './SearchableSetting' import { SkillUsageExamplesSection } from './SkillUsageExamplesSection' +import { LINEAR_INTEGRATION_SECTION_ID } from './task-provider-integration-section-ids' +import { useLinearAgentSkillSetup } from './use-linear-agent-skill-setup' import { translate } from '@/i18n/i18n' -export { getLinearAgentSkillPaneSearchEntries } from './linear-agent-skill-search' const LINEAR_EXAMPLE_ICONS: Record = { 'read-ticket': BookOpen, @@ -50,9 +33,8 @@ function resolveLinearExampleIcon(example: SkillUsageExample): LucideIcon { return LINEAR_EXAMPLE_ICONS[example.id] ?? LinearIcon } -// Keep Linear education, skill management, and example prompts in one place. +// Checklist owns connect + skill + visibility; examples and notes sit below. export function LinearAgentSkillPane(): React.JSX.Element { - const activeSkillRuntime = useActiveProjectSkillRuntime() const openSettingsPage = useAppStore((state) => state.openSettingsPage) const openSettingsTarget = useAppStore((state) => state.openSettingsTarget) const settings = useAppStore((state) => state.settings) @@ -61,47 +43,21 @@ export function LinearAgentSkillPane(): React.JSX.Element { const linearConnected = useLinearProviderConnected() const checkLinearConnection = useAppStore((state) => state.checkLinearConnection) const [linearKeyDialogOpen, setLinearKeyDialogOpen] = useState(false) + const skillSetup = useLinearAgentSkillSetup() const openTaskSources = (): void => { openSettingsPage() openSettingsTarget({ pane: 'tasks', repoId: null }) } - 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 updateTarget = useMemo( - () => getLinearAgentSkillUpdateTarget(linearSkills, linearSkillInstalled), - [linearSkillInstalled, linearSkills] - ) - const updateCommand = useMemo(() => { - const command = updateTarget.command - return activeSkillRuntime.installDisabledReason - ? command - : buildSkillCommandForRuntime(command, activeSkillRuntime.agentRuntime) - }, [ - activeSkillRuntime.agentRuntime, - activeSkillRuntime.installDisabledReason, - updateTarget.command - ]) + const openIntegrationSettings = (): void => { + openSettingsPage() + openSettingsTarget({ + pane: 'integrations', + repoId: null, + sectionId: LINEAR_INTEGRATION_SECTION_ID + }) + } const visibleInTasks = normalizeVisibleTaskProviders(settings?.visibleTaskProviders).includes( 'linear' @@ -109,6 +65,36 @@ export function LinearAgentSkillPane(): React.JSX.Element { const connectionChecking = linearStatusContextKey !== getProviderRuntimeContextKey(settings) || !linearStatusChecked + const skillPanel = ( + + ) + return ( setLinearKeyDialogOpen(true)} + onManageLinearAccess={ + linearConnected ? openIntegrationSettings : () => setLinearKeyDialogOpen(true) + } + skillPanel={skillPanel} /> -
-
-

- {translate( - 'auto.components.settings.LinearAgentSkillPane.skillSectionTitle', - 'Agent skill' - )} -

-

- {translate( - 'auto.components.settings.LinearAgentSkillPane.skillSectionBody', - 'Install or update the skill agents need. You can also complete this step from Task Sources during first-time setup.' - )} -

-
- - } - 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} - // Freshness cannot verify WSL, so report presence there. - freshnessSkillName={ - activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : updateTarget.skillName - } - /> -
- +

+ {translate( + 'auto.components.settings.LinearAgentSkillPane.manageConnectionHint', + 'Review connected Linear workspaces and API keys in' + )}{' '} + +

+ { void checkLinearConnection(true) }} diff --git a/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx b/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx index ec1403f6ac7..facad4263db 100644 --- a/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx +++ b/src/renderer/src/components/settings/MobileEmulatorAgentControlRow.tsx @@ -12,7 +12,7 @@ import { cn } from '@/lib/utils' import { useMobileEmulatorAgentSetupState } from '../emulator-pane/use-mobile-emulator-agent-setup-state' import { AgentSkillSetupPanel } from './AgentSkillSetupPanel' import { buildSkillCommandForRuntime } from './CliSkillRuntimeSetup' -import { StepBadge } from './BrowserUseStepBadge' +import { StepBadge } from './SetupStepBadge' import { MobileEmulatorExamples } from './MobileEmulatorExamples' import { Button } from '../ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip' diff --git a/src/renderer/src/components/settings/BrowserUseStepBadge.tsx b/src/renderer/src/components/settings/SetupStepBadge.tsx similarity index 100% rename from src/renderer/src/components/settings/BrowserUseStepBadge.tsx rename to src/renderer/src/components/settings/SetupStepBadge.tsx diff --git a/src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx b/src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx index 3ac3992065f..878bef8125c 100644 --- a/src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx +++ b/src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx @@ -1,7 +1,8 @@ // @vitest-environment happy-dom +import { cleanup, fireEvent, render } from '@testing-library/react' import { renderToStaticMarkup } from 'react-dom/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TaskSourceLinearSetup } from './TaskSourceLinearSetup' const mocks = vi.hoisted(() => ({ @@ -60,18 +61,29 @@ vi.mock('@/store', () => ({ selector({ checkLinearConnection: mocks.checkLinearConnection }) })) -function renderSetup(overrides: { connected?: boolean; visible?: boolean } = {}): string { - return renderToStaticMarkup( +function setupElement( + overrides: { + connected?: boolean + visible?: boolean + onOpenIntegrations?: () => void + } = {} +): React.JSX.Element { + return ( ) } +function renderSetup(overrides: { connected?: boolean; visible?: boolean } = {}): string { + return renderToStaticMarkup(setupElement(overrides)) +} + describe('TaskSourceLinearSetup', () => { beforeEach(() => { mocks.skillInstalled = false @@ -79,6 +91,10 @@ describe('TaskSourceLinearSetup', () => { mocks.panelProps = [] }) + afterEach(() => { + cleanup() + }) + it('blocks first-time skill install until Linear is connected', () => { const markup = renderSetup({ connected: false }) @@ -111,4 +127,13 @@ describe('TaskSourceLinearSetup', () => { expect(markup).toContain('Not installed') expect(markup).not.toContain('Connect Linear first') }) + + it('routes connected credential management to Integrations', () => { + const onOpenIntegrations = vi.fn() + const rendered = render(setupElement({ connected: true, onOpenIntegrations })) + + fireEvent.click(rendered.getByRole('button', { name: 'Manage keys' })) + + expect(onOpenIntegrations).toHaveBeenCalledOnce() + }) }) diff --git a/src/renderer/src/components/settings/TaskSourceLinearSetup.tsx b/src/renderer/src/components/settings/TaskSourceLinearSetup.tsx index 4c5090f7e29..deb6ab2d72d 100644 --- a/src/renderer/src/components/settings/TaskSourceLinearSetup.tsx +++ b/src/renderer/src/components/settings/TaskSourceLinearSetup.tsx @@ -1,29 +1,11 @@ -import { useMemo, useState } from 'react' +import { useState } from 'react' import { LinearApiKeyDialog } from '@/components/linear-api-key-dialog' import { Button } from '@/components/ui/button' -import { - AGENT_SKILL_CLI_PREREQUISITE_NOTICE, - ensureOrcaCliAvailableForAgentSkillTerminal -} from '@/lib/agent-skill-cli-prerequisite' -import { - LINEAR_AGENT_SKILL_NAMES, - ORCA_LINEAR_SKILL_INSTALL_COMMAND -} from '@/lib/agent-feature-install-commands' -import { getLinearAgentSkillUpdateTarget } from '@/lib/linear-agent-skill-update-command' -import { - GLOBAL_AGENT_SKILL_SOURCE_KINDS, - useInstalledAgentSkillNames -} from '@/hooks/useInstalledAgentSkills' -import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime' import { useAppStore } from '@/store' import { AgentSkillSetupPanel } from './AgentSkillSetupPanel' -import { - buildSkillCommandForRuntime, - ensureWslCliAvailableForAgentSkillTerminal, - getWslCliDistroRequest -} from './CliSkillRuntimeSetup' import { TaskSourceShowInTasksStep } from './TaskSourceShowInTasksStep' import { TaskSourceStepRow } from './TaskSourceStepRow' +import { useLinearAgentSkillSetup } from './use-linear-agent-skill-setup' import { translate } from '@/i18n/i18n' type TaskSourceLinearSetupProps = { @@ -31,6 +13,7 @@ type TaskSourceLinearSetupProps = { checking: boolean visible: boolean onToggleVisible: () => void + onOpenIntegrations: () => void canHide: boolean } @@ -40,53 +23,22 @@ export function TaskSourceLinearSetup({ checking, visible, onToggleVisible, + onOpenIntegrations, canHide }: TaskSourceLinearSetupProps): React.JSX.Element { const checkLinearConnection = useAppStore((s) => s.checkLinearConnection) const [dialogOpen, setDialogOpen] = useState(false) - const activeSkillRuntime = useActiveProjectSkillRuntime() - - const { - installed: skillInstalled, - loading: skillLoading, - error: skillError, - skills: linearSkills, - refresh: refreshSkill - } = 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 updateTarget = useMemo( - () => getLinearAgentSkillUpdateTarget(linearSkills, skillInstalled), - [linearSkills, skillInstalled] - ) - const updateCommand = useMemo(() => { - const command = updateTarget.command - return activeSkillRuntime.installDisabledReason - ? command - : buildSkillCommandForRuntime(command, activeSkillRuntime.agentRuntime) - }, [ - activeSkillRuntime.agentRuntime, - activeSkillRuntime.installDisabledReason, - updateTarget.command - ]) + const skillSetup = useLinearAgentSkillSetup() const connectState = checking ? 'in-progress' : connected ? 'done' : 'pending' // Skill install is independent of API connection; only gate the first-time install CTA. - const skillState = skillLoading ? 'in-progress' : skillInstalled ? 'done' : 'pending' + const skillState = skillSetup.skillChecking + ? 'in-progress' + : skillSetup.skillInstalled + ? 'done' + : 'pending' // Keep the panel visible while scanning so we don't flash "connect first" over an installed skill. - const skillInstallBlocked = !connected && !skillInstalled && !skillLoading + const skillInstallBlocked = !connected && !skillSetup.skillInstalled && !skillSetup.skillChecking return ( <> @@ -107,12 +59,12 @@ export function TaskSourceLinearSetup({ type="button" size="sm" variant={connected ? 'outline' : 'default'} - onClick={() => setDialogOpen(true)} + onClick={connected ? onOpenIntegrations : () => setDialogOpen(true)} > {connected ? translate( 'auto.components.settings.TaskSourceLinearSetup.manageAccess', - 'Manage access' + 'Manage keys' ) : translate( 'auto.components.settings.TaskSourceLinearSetup.addAccess', @@ -153,7 +105,7 @@ export function TaskSourceLinearSetup({ )} description={translate( 'auto.components.settings.TaskSourceLinearSetup.skillDescription', - 'Gives agents the /orca-linear skill to read tickets, post updates, move states, and attach PRs.' + 'Gives agents /orca-linear to read tickets, post updates, move states, and attach pull or merge requests.' )} className={skillInstallBlocked ? 'opacity-60' : undefined} > @@ -173,8 +125,8 @@ export function TaskSourceLinearSetup({ 'Linear skill' )} description={null} - command={installCommand} - installedCommand={updateCommand} + command={skillSetup.installCommand} + installedCommand={skillSetup.updateCommand} terminalTitle={translate( 'auto.components.settings.TaskSourceLinearSetup.terminalTitle', 'Linear skill setup' @@ -184,30 +136,16 @@ export function TaskSourceLinearSetup({ 'Linear skill install terminal' )} terminalWorktreeId="settings-tasks-linear-skill-terminal" - terminalShellOverride={activeSkillRuntime.terminalShellOverride} - installed={skillInstalled} - loading={skillLoading} - error={activeSkillRuntime.installDisabledReason ?? skillError} - installDisabled={Boolean(activeSkillRuntime.installDisabledReason)} - 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={refreshSkill} - freshnessSkillName={ - activeSkillRuntime.agentRuntime?.runtime === 'wsl' - ? undefined - : updateTarget.skillName - } + terminalShellOverride={skillSetup.terminalShellOverride} + installed={skillSetup.skillInstalled} + loading={skillSetup.skillLoading} + error={skillSetup.error} + installDisabled={skillSetup.installDisabled} + preInstallNotice={skillSetup.preInstallNotice} + getPrerequisiteStatus={skillSetup.getPrerequisiteStatus} + onBeforeOpenTerminal={skillSetup.onBeforeOpenTerminal} + onRecheck={skillSetup.refreshSkill} + freshnessSkillName={skillSetup.freshnessSkillName} /> )} diff --git a/src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx b/src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx index 85b91695df9..71e0da02529 100644 --- a/src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx +++ b/src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx @@ -1,5 +1,9 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { TaskSourceProviderCard } from './TaskSourceProviderCard' const readiness = { @@ -10,6 +14,20 @@ const readiness = { visible: true } +let root: Root | null = null +let container: HTMLDivElement | null = null + +afterEach(async () => { + if (root) { + await act(async () => { + root?.unmount() + }) + } + root = null + container?.remove() + container = null +}) + describe('TaskSourceProviderCard', () => { it('shows the visibility action in only one place when expanded', () => { const markup = renderToStaticMarkup( @@ -49,7 +67,66 @@ describe('TaskSourceProviderCard', () => { ) expect(markup).toContain('aria-label="Hide Linear from Tasks"') + expect(markup).not.toContain('aria-pressed') + expect(markup).toContain('>Hide') + expect(markup).not.toContain('>Shown') expect(markup).toContain('aria-label="Show Linear setup steps"') expect(markup).not.toContain('Expanded content') }) + + it('stays expanded when the auto-expand target moves to another provider', async () => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + const card = (defaultExpanded: boolean): React.JSX.Element => ( + } + name="Linear" + description="Linear setup" + readiness={readiness} + visible + canHide + defaultExpanded={defaultExpanded} + onToggleVisible={vi.fn()} + > + Install terminal + + ) + + await act(async () => { + root?.render(card(true)) + }) + expect(container.textContent).toContain('Install terminal') + + // A slower provider check makes another card the auto-expand target. + await act(async () => { + root?.render(card(false)) + }) + + expect(container.textContent).toContain('Install terminal') + }) + + it('labels the locked final provider as shown instead of offering a hide action', () => { + const markup = renderToStaticMarkup( + } + name="Linear" + description="Linear setup" + readiness={readiness} + visible + canHide={false} + defaultExpanded={false} + onToggleVisible={vi.fn()} + /> + ) + + // aria-disabled, not disabled: the explanation must stay keyboard reachable. + expect(markup).toContain('aria-disabled="true"') + expect(markup).not.toContain('disabled=""') + expect(markup).toContain( + 'aria-label="Linear is shown in Tasks. At least one provider must stay visible."' + ) + expect(markup).toContain('>Shown') + expect(markup).not.toContain('>Hide') + }) }) diff --git a/src/renderer/src/components/settings/TaskSourceProviderCard.tsx b/src/renderer/src/components/settings/TaskSourceProviderCard.tsx index 2e2260b72bd..56e77fa5931 100644 --- a/src/renderer/src/components/settings/TaskSourceProviderCard.tsx +++ b/src/renderer/src/components/settings/TaskSourceProviderCard.tsx @@ -3,11 +3,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react' import { IntegrationStatusPill } from '@/components/integration-status-pill' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' -import { - getShowInTasksActionLabel, - getShowInTasksLabel, - getShowInTasksLastProviderHint -} from './TaskSourceShowInTasksStep' +import { getShowInTasksActionLabel, getShowInTasksLabel } from './TaskSourceShowInTasksStep' import { TASK_PROVIDER_SETUP_STATUS_TONE, getTaskProviderCompletedSteps, @@ -49,6 +45,11 @@ function getSetupStatusLabel(status: TaskProviderSetupStatus): string { 'auto.components.settings.TaskSourceProviderCard.statusSkillRequired', 'Skill required' ) + case 'unavailable': + return translate( + 'auto.components.settings.TaskSourceProviderCard.statusUnavailable', + 'Status unavailable' + ) case 'hidden': return translate( 'auto.components.settings.TaskSourceProviderCard.statusHidden', @@ -73,8 +74,16 @@ export function TaskSourceProviderCard({ onToggleVisible, children }: TaskSourceProviderCardProps): React.JSX.Element { - const [expandedOverride, setExpandedOverride] = useState(null) - const expanded = expandedOverride ?? defaultExpanded + const [expanded, setExpanded] = useState(defaultExpanded) + const [lastDefaultExpanded, setLastDefaultExpanded] = useState(defaultExpanded) + // Auto-expand only ever opens: collapsing is the user's call, so a readiness + // change that moves the auto-expand target cannot close a card in use. + if (lastDefaultExpanded !== defaultExpanded) { + setLastDefaultExpanded(defaultExpanded) + if (defaultExpanded) { + setExpanded(true) + } + } const status = getTaskProviderSetupStatus(readiness) const progress = getTaskProviderCompletedSteps(readiness) const visibilityLocked = visible && !canHide @@ -102,15 +111,10 @@ export function TaskSourceProviderCard({ {isTaskProviderChecking(readiness) || status === 'ready' || + status === 'unavailable' || status === 'hidden' ? null : ( - + // Only unfinished providers reach here, so the count is always partial. + {`${progress.completed}/${progress.total}`} )} @@ -124,13 +128,14 @@ export function TaskSourceProviderCard({ type="button" size="sm" variant={visible ? 'outline' : 'secondary'} - disabled={visibilityLocked} - title={visibilityLocked ? getShowInTasksLastProviderHint() : undefined} + // aria-disabled, not disabled: the last visible provider must stay + // reachable by keyboard so its label can explain why it is locked. + aria-disabled={visibilityLocked} + className={cn(visibilityLocked && 'cursor-not-allowed opacity-60')} aria-label={getShowInTasksActionLabel(visible, canHide, name)} - onClick={onToggleVisible} - aria-pressed={visible} + onClick={visibilityLocked ? undefined : onToggleVisible} > - {getShowInTasksLabel(visible)} + {getShowInTasksLabel(visible, canHide)} ) : null} diff --git a/src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx b/src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx new file mode 100644 index 00000000000..7fdd8e05229 --- /dev/null +++ b/src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx @@ -0,0 +1,41 @@ +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' +import { TaskSourceShowInTasksStep } from './TaskSourceShowInTasksStep' + +describe('TaskSourceShowInTasksStep', () => { + it('uses an explicit hide action when another provider can remain visible', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).not.toContain('aria-pressed') + expect(markup).toContain('>Hide') + }) + + it('explains why the final visible provider cannot be hidden', () => { + const markup = renderToStaticMarkup( + + ) + + // aria-disabled, not disabled: the explanation must stay keyboard reachable. + expect(markup).toContain('aria-disabled="true"') + expect(markup).not.toContain('disabled=""') + expect(markup).toContain( + 'aria-label="GitLab is shown in Tasks. At least one provider must stay visible."' + ) + expect(markup).toContain('>Shown') + expect(markup).toContain('At least one provider must stay visible in Tasks.') + }) +}) diff --git a/src/renderer/src/components/settings/TaskSourceShowInTasksStep.tsx b/src/renderer/src/components/settings/TaskSourceShowInTasksStep.tsx index 7e040de74d6..a6b11f1ad13 100644 --- a/src/renderer/src/components/settings/TaskSourceShowInTasksStep.tsx +++ b/src/renderer/src/components/settings/TaskSourceShowInTasksStep.tsx @@ -1,4 +1,5 @@ import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' import { TaskSourceStepRow } from './TaskSourceStepRow' import { translate } from '@/i18n/i18n' @@ -11,10 +12,13 @@ type TaskSourceShowInTasksStepProps = { description?: string } -export function getShowInTasksLabel(visible: boolean): string { - return visible - ? translate('auto.components.settings.TaskSourceShowInTasksStep.shown', 'Shown') - : translate('auto.components.settings.TaskSourceShowInTasksStep.show', 'Show') +export function getShowInTasksLabel(visible: boolean, canHide: boolean): string { + if (!visible) { + return translate('auto.components.settings.TaskSourceShowInTasksStep.show', 'Show') + } + return canHide + ? translate('auto.components.settings.TaskSourceShowInTasksStep.hide', 'Hide') + : translate('auto.components.settings.TaskSourceShowInTasksStep.shown', 'Shown') } export function getShowInTasksLastProviderHint(): string { @@ -30,7 +34,11 @@ export function getShowInTasksActionLabel( providerLabel: string ): string { if (visible && !canHide) { - return getShowInTasksLastProviderHint() + return translate( + 'auto.components.settings.TaskSourceShowInTasksStep.lastProviderAction', + '{{provider}} is shown in Tasks. At least one provider must stay visible.', + { provider: providerLabel } + ) } return visible ? translate( @@ -72,14 +80,20 @@ export function TaskSourceShowInTasksStep({ type="button" size="sm" variant={visible ? 'outline' : 'default'} - disabled={locked} - title={locked ? getShowInTasksLastProviderHint() : undefined} + // aria-disabled keeps the locked last provider in the tab order so its + // label (and the hint below) can explain why it cannot be hidden. + aria-disabled={locked} + className={cn(locked && 'cursor-not-allowed opacity-60')} aria-label={getShowInTasksActionLabel(visible, canHide, providerLabel)} - onClick={onToggleVisible} + onClick={locked ? undefined : onToggleVisible} > - {getShowInTasksLabel(visible)} + {getShowInTasksLabel(visible, canHide)} } - /> + > + {locked ? ( +

{getShowInTasksLastProviderHint()}

+ ) : null} + ) } diff --git a/src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx b/src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx new file mode 100644 index 00000000000..c85146cb6af --- /dev/null +++ b/src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx @@ -0,0 +1,43 @@ +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { JiraSetupSteps } from './TaskSourceSimpleSetup' + +const mocks = vi.hoisted(() => ({ + dialogOpen: [] as boolean[] +})) + +vi.mock('@/components/jira-connect-dialog', () => ({ + JiraConnectDialog: ({ open }: { open: boolean }) => { + mocks.dialogOpen.push(open) + return null + } +})) + +afterEach(() => { + cleanup() + mocks.dialogOpen = [] +}) + +describe('JiraSetupSteps', () => { + it('routes connected credential management to Integrations', () => { + const onOpenIntegrations = vi.fn() + const rendered = render( + + ) + + fireEvent.click(rendered.getByRole('button', { name: 'Manage keys' })) + + expect(onOpenIntegrations).toHaveBeenCalledOnce() + expect(mocks.dialogOpen.at(-1)).toBe(false) + }) +}) diff --git a/src/renderer/src/components/settings/TaskSourceSimpleSetup.tsx b/src/renderer/src/components/settings/TaskSourceSimpleSetup.tsx index 34d20a932a7..50f53cfa5f2 100644 --- a/src/renderer/src/components/settings/TaskSourceSimpleSetup.tsx +++ b/src/renderer/src/components/settings/TaskSourceSimpleSetup.tsx @@ -23,9 +23,21 @@ function getConnectStepState(props: ConnectStepProps): 'in-progress' | 'done' | export function CodeHostSetupSteps( props: ConnectStepProps & { providerLabel: string + unavailable?: boolean onOpenIntegrations: () => void + onRetryConnection: () => void } ): React.JSX.Element { + const connectionDescription = props.unavailable + ? translate( + 'auto.components.settings.TasksPane.connectionCheckUnavailable', + "Orca couldn't check this connection. Try again, or open Integrations for setup details." + ) + : translate( + 'auto.components.settings.TasksPane.connectCodeHostDescription', + 'Install and authenticate the CLI under Integrations so Orca can load issues.' + ) + return (
    - {props.connected - ? translate('auto.components.settings.TasksPane.openIntegrations', 'Integrations') - : translate( - 'auto.components.settings.TasksPane.connectInIntegrations', - 'Set up in Integrations' - )} + {props.unavailable + ? translate('auto.components.settings.TasksPane.retryConnection', 'Try again') + : props.connected + ? translate('auto.components.settings.TasksPane.openIntegrations', 'Integrations') + : translate( + 'auto.components.settings.TasksPane.connectInIntegrations', + 'Set up in Integrations' + )} } /> @@ -68,7 +79,7 @@ export function CodeHostSetupSteps( } export function JiraSetupSteps( - props: ConnectStepProps & { onConnected: () => void } + props: ConnectStepProps & { onConnected: () => void; onOpenIntegrations: () => void } ): React.JSX.Element { const [dialogOpen, setDialogOpen] = useState(false) @@ -88,10 +99,10 @@ export function JiraSetupSteps( type="button" size="sm" variant={props.connected ? 'outline' : 'default'} - onClick={() => setDialogOpen(true)} + onClick={props.connected ? props.onOpenIntegrations : () => setDialogOpen(true)} > {props.connected - ? translate('auto.components.settings.TasksPane.manageJira', 'Manage access') + ? translate('auto.components.settings.TasksPane.manageJira', 'Manage keys') : translate('auto.components.settings.TasksPane.addJira', 'Add Jira access')} } diff --git a/src/renderer/src/components/settings/TaskSourceStepRow.tsx b/src/renderer/src/components/settings/TaskSourceStepRow.tsx index 3c3e8f2773a..3c9f9f6c799 100644 --- a/src/renderer/src/components/settings/TaskSourceStepRow.tsx +++ b/src/renderer/src/components/settings/TaskSourceStepRow.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { StepBadge, type StepState } from './BrowserUseStepBadge' +import { StepBadge, type StepState } from './SetupStepBadge' import { cn } from '@/lib/utils' type TaskSourceStepRowProps = { diff --git a/src/renderer/src/components/settings/TasksPane.test.tsx b/src/renderer/src/components/settings/TasksPane.test.tsx index 5669df3f2a8..e87df95c0db 100644 --- a/src/renderer/src/components/settings/TasksPane.test.tsx +++ b/src/renderer/src/components/settings/TasksPane.test.tsx @@ -1,7 +1,9 @@ // @vitest-environment happy-dom +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { GlobalSettings, TaskProvider } from '../../../../shared/types' import type { TaskProviderReadiness } from './task-source-setup-state' import { TasksPane } from './TasksPane' @@ -13,7 +15,12 @@ const mocks = vi.hoisted(() => ({ refreshPreflightStatus: vi.fn(), checkLinearConnection: vi.fn(), checkJiraConnection: vi.fn(), - linearSetupProps: [] as { connected: boolean; checking: boolean }[] + linearSetupProps: [] as { + connected: boolean + checking: boolean + onOpenIntegrations: () => void + }[], + jiraSetupProps: [] as { onOpenIntegrations: () => void }[] })) vi.mock('./use-task-source-provider-readiness', () => ({ @@ -25,17 +32,39 @@ vi.mock('./use-integration-provider-status-refresh', () => ({ })) vi.mock('./TaskSourceLinearSetup', () => ({ - TaskSourceLinearSetup: (props: { connected: boolean; checking: boolean }) => { + TaskSourceLinearSetup: (props: { + connected: boolean + checking: boolean + onOpenIntegrations: () => void + }) => { mocks.linearSetupProps.push(props) return
    Linear setup steps
    } })) vi.mock('./TaskSourceSimpleSetup', () => ({ - CodeHostSetupSteps: (props: { providerLabel: string }) => ( -
    Code host setup
    + CodeHostSetupSteps: (props: { + providerLabel: string + unavailable?: boolean + onRetryConnection: () => void + }) => ( +
    + {props.unavailable ? ( + <> + Orca couldn't check this connection + + + ) : ( + 'Code host setup' + )} +
    ), - JiraSetupSteps: () =>
    Jira setup
    + JiraSetupSteps: (props: { onOpenIntegrations: () => void }) => { + mocks.jiraSetupProps.push(props) + return
    Jira setup
    + } })) vi.mock('@/store', () => ({ @@ -71,14 +100,37 @@ function renderPane(): string { return renderToStaticMarkup() } +let root: Root | null = null +let container: HTMLDivElement | null = null + +async function renderInteractivePane(): Promise { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + await act(async () => { + root?.render() + }) + return container +} + +async function rerenderInteractivePane(): Promise { + await act(async () => { + root?.render() + }) +} + describe('TasksPane', () => { beforeEach(() => { mocks.linearSetupProps = [] + mocks.jiraSetupProps = [] + mocks.openSettingsPage.mockClear() + mocks.openSettingsTarget.mockClear() mocks.readiness = { github: { connected: true, checking: false, visible: true }, gitlab: { connected: true, checking: false, visible: true }, + // Started-then-stalled: a Linear key is stored but agents have no skill. linear: { - connected: false, + connected: true, checking: false, skillInstalled: false, skillChecking: false, @@ -88,6 +140,17 @@ describe('TasksPane', () => { } }) + afterEach(async () => { + if (root) { + await act(async () => { + root?.unmount() + }) + } + root = null + container?.remove() + container = null + }) + it('frames Task Sources as a guided setup hub, not visibility-only toggles', () => { const markup = renderPane() @@ -99,6 +162,26 @@ describe('TasksPane', () => { expect(markup).toContain('API access, the agent skill, and Show in Tasks') }) + it('does not warn on a fresh install where nothing is connected yet', () => { + // Settings ship with every provider visible, so untouched providers are the + // default state; the cards still say "Connect required" on their own. + mocks.readiness.github = { connected: false, checking: false, visible: true } + mocks.readiness.gitlab = { connected: false, checking: false, visible: true } + mocks.readiness.linear = { + connected: false, + checking: false, + skillInstalled: false, + skillChecking: false, + visible: true + } + mocks.readiness.jira = { connected: false, checking: false, visible: true } + + const markup = renderPane() + + expect(markup).not.toContain(INCOMPLETE_BANNER) + expect(markup).toContain('Connect required') + }) + it('hides the incomplete banner when every visible provider is ready', () => { mocks.readiness.linear = { connected: true, @@ -132,10 +215,43 @@ describe('TasksPane', () => { renderPane() expect(mocks.linearSetupProps).toEqual([ - expect.objectContaining({ connected: false, checking: false }) + expect.objectContaining({ connected: true, checking: false }) ]) }) + it('deep-links connected Linear credential management to its integration card', async () => { + await renderInteractivePane() + + mocks.linearSetupProps.at(-1)?.onOpenIntegrations() + + expect(mocks.openSettingsPage).toHaveBeenCalledOnce() + expect(mocks.openSettingsTarget).toHaveBeenCalledWith({ + pane: 'integrations', + repoId: null, + sectionId: 'integrations-linear' + }) + }) + + it('deep-links connected Jira credential management to its integration card', async () => { + mocks.readiness.jira = { connected: true, checking: false, visible: true } + await renderInteractivePane() + const expandJira = Array.from(container?.querySelectorAll('button') ?? []).find( + (button) => button.getAttribute('aria-label') === 'Show Jira setup steps' + ) + + await act(async () => { + expandJira?.click() + }) + mocks.jiraSetupProps.at(-1)?.onOpenIntegrations() + + expect(mocks.openSettingsPage).toHaveBeenCalledOnce() + expect(mocks.openSettingsTarget).toHaveBeenCalledWith({ + pane: 'integrations', + repoId: null, + sectionId: 'integrations-jira' + }) + }) + it('auto-expands only the first incomplete provider', () => { mocks.readiness.gitlab = { connected: false, checking: false, visible: true } @@ -148,7 +264,7 @@ describe('TasksPane', () => { it('leaves hidden providers out of the incomplete warning', () => { mocks.readiness.linear = { - connected: false, + connected: true, checking: false, skillInstalled: false, skillChecking: false, @@ -157,4 +273,122 @@ describe('TasksPane', () => { expect(renderPane()).not.toContain(INCOMPLETE_BANNER) }) + + it('keeps Linear setup mounted while a skill recheck is in flight after auto-expand', async () => { + // Same component instance keeps the sticky auto-expand ref across rechecks. + await renderInteractivePane() + expect(container?.querySelector('[data-testid="linear-setup"]')).not.toBeNull() + expect(container?.textContent).toContain(INCOMPLETE_BANNER) + + mocks.readiness.linear = { + connected: true, + checking: false, + skillInstalled: false, + skillChecking: true, + visible: true + } + await rerenderInteractivePane() + + // Sticky expand keeps the (possibly open) install terminal mounted mid-scan. + expect(container?.querySelector('[data-testid="linear-setup"]')).not.toBeNull() + }) + + it('keeps the auto-expanded card open after hiding it instead of popping another open', async () => { + await renderInteractivePane() + expect(container?.querySelector('[data-testid="linear-setup"]')).not.toBeNull() + + // Hiding the stalled provider is the banner's own advice; GitHub must not + // silently take over the expansion on a later unrelated render. + mocks.readiness.linear = { + connected: true, + checking: false, + skillInstalled: false, + skillChecking: false, + visible: false + } + mocks.readiness.github = { connected: false, checking: false, visible: true } + await rerenderInteractivePane() + await rerenderInteractivePane() + + expect(container?.querySelector('[data-testid="linear-setup"]')).not.toBeNull() + expect(container?.textContent).not.toContain('Code host setup') + }) + + it('keeps Linear expanded when the slower code-host preflight lands after it', async () => { + // Cold open: nothing has resolved, so no card auto-expands yet. + mocks.readiness.github = { connected: false, checking: true, visible: true } + mocks.readiness.gitlab = { connected: false, checking: true, visible: true } + mocks.readiness.linear = { + connected: false, + checking: true, + skillInstalled: false, + skillChecking: true, + visible: true + } + await renderInteractivePane() + expect(container?.querySelector('[data-testid="linear-setup"]')).toBeNull() + + // Linear status + skill scan land first, so Linear auto-expands. + mocks.readiness.linear = { + connected: true, + checking: false, + skillInstalled: false, + skillChecking: false, + visible: true + } + await rerenderInteractivePane() + expect(container?.querySelector('[data-testid="linear-setup"]')).not.toBeNull() + + // gh/glab preflight lands last with gh unauthenticated: GitHub is now the + // first incomplete provider, but it must not steal Linear's expansion. + mocks.readiness.github = { connected: false, checking: false, visible: true } + mocks.readiness.gitlab = { connected: false, checking: false, visible: true } + await rerenderInteractivePane() + + expect(container?.querySelector('[data-testid="linear-setup"]')).not.toBeNull() + }) + + it('does not warn about an unconnected code host once Linear is finished', () => { + // A code host is single-step, so "not connected" is never a stalled setup. + mocks.readiness.linear = { + connected: true, + checking: false, + skillInstalled: true, + skillChecking: false, + visible: true + } + mocks.readiness.github = { connected: false, checking: false, visible: true } + + expect(renderPane()).not.toContain(INCOMPLETE_BANNER) + }) + + it('shows a retry action instead of setup instructions when preflight is unavailable', async () => { + mocks.readiness.github = { + connected: false, + checking: false, + unavailable: true, + visible: true + } + mocks.readiness.gitlab = { connected: true, checking: false, visible: true } + mocks.readiness.linear = { + connected: true, + checking: false, + skillInstalled: true, + skillChecking: false, + visible: true + } + await renderInteractivePane() + + expect(container?.textContent).toContain('Status unavailable') + expect(container?.textContent).toContain("Orca couldn't check this connection") + const retry = Array.from(container?.querySelectorAll('button') ?? []).find( + (button) => button.textContent === 'Try again' + ) + expect(retry).toBeDefined() + + await act(async () => { + retry?.click() + }) + expect(mocks.refreshPreflightStatus).toHaveBeenCalledWith({ force: true }) + }) }) diff --git a/src/renderer/src/components/settings/TasksPane.tsx b/src/renderer/src/components/settings/TasksPane.tsx index 2c324f33eff..a9bc0d167b9 100644 --- a/src/renderer/src/components/settings/TasksPane.tsx +++ b/src/renderer/src/components/settings/TasksPane.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from 'react' import { Github, Gitlab } from 'lucide-react' import type { GlobalSettings, TaskProvider } from '../../../../shared/types' import { @@ -15,9 +16,13 @@ import { CodeHostSetupSteps, JiraSetupSteps } from './TaskSourceSimpleSetup' import { TaskSourceLinearSetup } from './TaskSourceLinearSetup' import { TaskSourceProviderCard } from './TaskSourceProviderCard' import { - getAutoExpandedTaskProvider, - getIncompleteVisibleTaskProviders + getStalledVisibleTaskProviders, + resolveStickyAutoExpandedTaskProvider } from './task-source-setup-state' +import { + JIRA_INTEGRATION_SECTION_ID, + LINEAR_INTEGRATION_SECTION_ID +} from './task-provider-integration-section-ids' import { getTasksPaneSearchKeywords } from './tasks-search' import { useIntegrationProviderStatusRefresh } from './use-integration-provider-status-refresh' import { useTaskSourceProviderReadiness } from './use-task-source-provider-readiness' @@ -91,11 +96,23 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J const openSettingsPage = useAppStore((s) => s.openSettingsPage) const openSettingsTarget = useAppStore((s) => s.openSettingsTarget) const checkJiraConnection = useAppStore((s) => s.checkJiraConnection) + const refreshPreflightStatus = useAppStore((s) => s.refreshPreflightStatus) const readinessByProvider = useTaskSourceProviderReadiness(visibleProviders) useIntegrationProviderStatusRefresh() - const incompleteVisible = getIncompleteVisibleTaskProviders(TASK_PROVIDERS, readinessByProvider) - const autoExpandedProvider = getAutoExpandedTaskProvider(TASK_PROVIDERS, readinessByProvider) + // Warn only about started-then-stalled setup; untouched providers are the default. + const stalledVisible = getStalledVisibleTaskProviders(TASK_PROVIDERS, readinessByProvider) + // Sticky across rechecks so expanded Linear install terminals are not unmounted. + const previousAutoExpandedRef = useRef(null) + const autoExpandedProvider = resolveStickyAutoExpandedTaskProvider({ + providers: TASK_PROVIDERS, + readinessByProvider, + previousAutoExpanded: previousAutoExpandedRef.current + }) + // Commit-only write: a discarded render must not seed the sticky decision. + useEffect(() => { + previousAutoExpandedRef.current = autoExpandedProvider + }, [autoExpandedProvider]) const toggleProvider = (provider: TaskProvider): void => { const isVisible = visibleProviders.includes(provider) @@ -113,9 +130,13 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J }) } - const openIntegrations = (): void => { + const openIntegrations = (sectionId?: string): void => { openSettingsPage() - openSettingsTarget({ pane: 'integrations', repoId: null }) + openSettingsTarget({ + pane: 'integrations', + repoId: null, + ...(sectionId ? { sectionId } : {}) + }) } return ( @@ -128,11 +149,11 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J )} description={translate( 'auto.components.settings.TasksPane.setupDescription', - 'Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets.' + 'Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets. At least one provider must stay visible.' )} /> - {incompleteVisible.length > 0 ? ( + {stalledVisible.length > 0 ? (

    {translate( @@ -140,11 +161,18 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J 'Some visible providers still need setup' )}

    + {/* Linear is the only multi-step provider today; the generic body + covers any future provider that can stall the same way. */}

    - {translate( - 'auto.components.settings.TasksPane.incompleteBannerBody', - 'Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.' - )} + {stalledVisible.includes('linear') + ? translate( + 'auto.components.settings.TasksPane.incompleteBannerBodyWithLinear', + 'Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.' + ) + : translate( + 'auto.components.settings.TasksPane.incompleteBannerBody', + 'Hide providers you do not use, or expand a card and finish its steps.' + )}

    ) : null} @@ -184,6 +212,7 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J visible={visible} canHide={canHide} onToggleVisible={() => toggleProvider('linear')} + onOpenIntegrations={() => openIntegrations(LINEAR_INTEGRATION_SECTION_ID)} /> ) : provider === 'jira' ? ( toggleProvider('jira')} onConnected={() => void checkJiraConnection()} + onOpenIntegrations={() => openIntegrations(JIRA_INTEGRATION_SECTION_ID)} /> ) : ( toggleProvider(provider)} - onOpenIntegrations={openIntegrations} + onOpenIntegrations={() => openIntegrations()} + onRetryConnection={() => void refreshPreflightStatus({ force: true })} /> )} @@ -220,7 +252,7 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J variant="link" size="sm" className="h-auto p-0 text-xs align-baseline" - onClick={openIntegrations} + onClick={() => openIntegrations()} > {translate('auto.components.settings.TasksPane.integrationsLink', 'Integrations')} 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 72e2bb5ccd0..86aef0e9920 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,15 +24,24 @@ const updateCapableCallers = new Map([ ['COMPUTER_USE_SKILL_UPDATE_COMMAND', 'installedCommand={updateCommand}'] ], [ - // Why: the Linear settings section shares the update-target resolver so - // legacy-only installs keep the command and freshness identity aligned. + // Shared hook owns update-target resolution for Linear settings + Task Sources. + 'src/renderer/src/components/settings/use-linear-agent-skill-setup.ts', + [ + 'getLinearAgentSkillUpdateTarget', + 'updateTarget.command', + // The builder also reads the focused runtime environment, so memoizing + // either command on the runtime alone serves a stale Windows host command. + 'const installCommand = activeSkillRuntime.installDisabledReason', + 'const updateCommand = activeSkillRuntime.installDisabledReason' + ] + ], + [ 'src/renderer/src/components/settings/LinearAgentSkillPane.tsx', - ['getLinearAgentSkillUpdateTarget', 'installedCommand={updateCommand}'] + ['installedCommand={skillSetup.updateCommand}'] ], [ - // Task Sources must resolve the same Linear skill update target. 'src/renderer/src/components/settings/TaskSourceLinearSetup.tsx', - ['getLinearAgentSkillUpdateTarget', 'installedCommand={updateCommand}'] + ['installedCommand={skillSetup.updateCommand}'] ], [ 'src/renderer/src/components/settings/EphemeralVmsPane.tsx', @@ -80,10 +89,12 @@ const installOnlyCallers = new Map([ const directPanelCallers = new Set([ // BrowserUsePane and LinearAgentSkillSetupPrompt delegate through child setup // components that forward installedCommand and are validated separately above. + // use-linear-agent-skill-setup owns the update-target resolver but is not a panel host. ...[...updateCapableCallers.keys()].filter( (relativePath) => relativePath !== 'src/renderer/src/components/settings/BrowserUsePane.tsx' && - relativePath !== 'src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx' + relativePath !== 'src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx' && + relativePath !== 'src/renderer/src/components/settings/use-linear-agent-skill-setup.ts' ), ...installOnlyCallers.keys() ]) diff --git a/src/renderer/src/components/settings/integration-card-shell.tsx b/src/renderer/src/components/settings/integration-card-shell.tsx index 54c288d4258..0e5f80d6111 100644 --- a/src/renderer/src/components/settings/integration-card-shell.tsx +++ b/src/renderer/src/components/settings/integration-card-shell.tsx @@ -18,6 +18,7 @@ export function IntegrationCardShell(props: { statusTone: IntegrationCardStatusTone checking?: boolean className?: string + settingsSectionId?: string actions?: React.ReactNode children?: React.ReactNode }): React.JSX.Element { @@ -36,7 +37,7 @@ export function IntegrationCardShell(props: { ) return ( -
    +
    {props.icon}
    diff --git a/src/renderer/src/components/settings/jira-integration-card.test.tsx b/src/renderer/src/components/settings/jira-integration-card.test.tsx index 50b3990748c..0abbeb706ef 100644 --- a/src/renderer/src/components/settings/jira-integration-card.test.tsx +++ b/src/renderer/src/components/settings/jira-integration-card.test.tsx @@ -99,6 +99,7 @@ describe('JiraIntegrationCard account scope', () => { const rendered = await renderCard() + expect(rendered.querySelector('[data-settings-section="integrations-jira"]')).not.toBeNull() expect(rendered.textContent).toContain('Account scope: Remote server: runtime-1') expect(rendered.textContent).toContain('Acme Jira') expect(rendered.textContent).toContain('https://acme.atlassian.net · jira@example.test') diff --git a/src/renderer/src/components/settings/jira-integration-card.tsx b/src/renderer/src/components/settings/jira-integration-card.tsx index 8427e6f4cf7..e1889c2f073 100644 --- a/src/renderer/src/components/settings/jira-integration-card.tsx +++ b/src/renderer/src/components/settings/jira-integration-card.tsx @@ -13,6 +13,7 @@ import { IntegrationCardDetails, IntegrationCardShell } from './integration-card import { useIntegrationSubordinateRowClass } from './integration-card-presentation' import { getProviderAccountScope } from './provider-account-scope' import { ProviderHostScopeControl } from './ProviderHostScopeControl' +import { JIRA_INTEGRATION_SECTION_ID } from './task-provider-integration-section-ids' import { translate } from '@/i18n/i18n' type VerificationResult = { state: 'ok' | 'error'; error?: string } @@ -76,6 +77,7 @@ export function JiraIntegrationCard(): React.JSX.Element { return ( } name="Jira" description={ diff --git a/src/renderer/src/components/settings/linear-agent-skill-guide-content.ts b/src/renderer/src/components/settings/linear-agent-skill-guide-content.ts index e358fb5f9ce..63fbb4f75be 100644 --- a/src/renderer/src/components/settings/linear-agent-skill-guide-content.ts +++ b/src/renderer/src/components/settings/linear-agent-skill-guide-content.ts @@ -43,7 +43,7 @@ export function getLinearGuideNotes(): LinearGuideNote[] { ), body: translate( 'auto.components.settings.LinearAgentSkillGuide.noteKeysBody', - 'API keys and workspaces are stored for the active runtime. Manage them anytime under Integrations.' + 'API keys and workspaces are stored for the active runtime.' ) }, { diff --git a/src/renderer/src/components/settings/task-provider-integration-section-ids.ts b/src/renderer/src/components/settings/task-provider-integration-section-ids.ts new file mode 100644 index 00000000000..6cd7d9933af --- /dev/null +++ b/src/renderer/src/components/settings/task-provider-integration-section-ids.ts @@ -0,0 +1,2 @@ +export const LINEAR_INTEGRATION_SECTION_ID = 'integrations-linear' +export const JIRA_INTEGRATION_SECTION_ID = 'integrations-jira' diff --git a/src/renderer/src/components/settings/task-source-setup-state.test.ts b/src/renderer/src/components/settings/task-source-setup-state.test.ts index 99103d357d8..d2e4e6a5c27 100644 --- a/src/renderer/src/components/settings/task-source-setup-state.test.ts +++ b/src/renderer/src/components/settings/task-source-setup-state.test.ts @@ -3,9 +3,11 @@ import type { TaskProvider } from '../../../../shared/types' import { getAutoExpandedTaskProvider, getIncompleteVisibleTaskProviders, + getStalledVisibleTaskProviders, getTaskProviderCompletedSteps, getTaskProviderSetupStatus, isTaskProviderReady, + resolveStickyAutoExpandedTaskProvider, type TaskProviderReadiness } from './task-source-setup-state' @@ -89,6 +91,14 @@ describe('task-source-setup-state', () => { expect(getTaskProviderSetupStatus({ connected: false, checking: false, visible: true })).toBe( 'connect-required' ) + expect( + getTaskProviderSetupStatus({ + connected: false, + checking: false, + unavailable: true, + visible: true + }) + ).toBe('unavailable') expect( getTaskProviderSetupStatus({ connected: true, @@ -105,6 +115,17 @@ describe('task-source-setup-state', () => { ) }) + it('never reports an unavailable provider as ready', () => { + expect( + isTaskProviderReady({ + connected: true, + checking: false, + unavailable: true, + visible: true + }) + ).toBe(false) + }) + it('treats hidden providers as deliberately disabled regardless of connection state', () => { expect(getTaskProviderSetupStatus({ connected: false, checking: false, visible: false })).toBe( 'hidden' @@ -137,4 +158,133 @@ describe('task-source-setup-state', () => { it('auto-expands nothing once every visible provider is ready', () => { expect(getAutoExpandedTaskProvider(ORDER, buildReadiness())).toBeNull() }) + + it('does not warn about providers nobody has connected yet', () => { + // Settings ship with every provider visible, so an untouched provider is the + // default state rather than something to flag on a fresh install. + const untouched = buildReadiness({ + github: { connected: false }, + gitlab: { connected: false }, + linear: { connected: false, skillInstalled: false }, + jira: { connected: false } + }) + + expect(getIncompleteVisibleTaskProviders(ORDER, untouched)).toEqual([ + 'github', + 'gitlab', + 'linear', + 'jira' + ]) + expect(getStalledVisibleTaskProviders(ORDER, untouched)).toEqual([]) + }) + + it('warns about setup that started and stalled partway', () => { + const stalled = buildReadiness({ + github: { connected: false }, + linear: { skillInstalled: false } + }) + + // GitHub was never connected; Linear has a key but no skill. + expect(getStalledVisibleTaskProviders(ORDER, stalled)).toEqual(['linear']) + }) + + it('counts an installed skill as started even without an API key', () => { + const stalled = buildReadiness({ + linear: { connected: false, skillInstalled: true } + }) + + expect(getStalledVisibleTaskProviders(ORDER, stalled)).toEqual(['linear']) + }) + + it('keeps still-checking and hidden providers out of the warning', () => { + const readiness = buildReadiness({ + gitlab: { connected: true, checking: true, visible: true }, + linear: { skillInstalled: false, visible: false } + }) + + expect(getStalledVisibleTaskProviders(ORDER, readiness)).toEqual([]) + }) + + it('keeps the previous auto-expanded provider open while a recheck is in flight', () => { + const whileChecking = buildReadiness({ + linear: { skillInstalled: false, skillChecking: true } + }) + + // Fresh incomplete list excludes checking providers (banner path). + expect(getAutoExpandedTaskProvider(ORDER, whileChecking)).toBeNull() + // Sticky path keeps Linear open so install UI is not unmounted mid-scan. + expect( + resolveStickyAutoExpandedTaskProvider({ + providers: ORDER, + readinessByProvider: whileChecking, + previousAutoExpanded: 'linear' + }) + ).toBe('linear') + }) + + it('does not switch to a later incomplete provider during the previous provider recheck', () => { + const whileChecking = buildReadiness({ + linear: { skillInstalled: false, skillChecking: true }, + jira: { connected: false } + }) + + expect(getAutoExpandedTaskProvider(ORDER, whileChecking)).toBe('jira') + expect( + resolveStickyAutoExpandedTaskProvider({ + providers: ORDER, + readinessByProvider: whileChecking, + previousAutoExpanded: 'linear' + }) + ).toBe('linear') + }) + + it('does not hand the expansion to a provider whose check landed later', () => { + // gh/glab preflight is slower than the Linear status + skill scan, so GitHub + // only becomes eligible after Linear already auto-expanded. + const afterPreflight = buildReadiness({ + github: { connected: false }, + gitlab: { connected: false }, + linear: { skillInstalled: false } + }) + + expect(getAutoExpandedTaskProvider(ORDER, afterPreflight)).toBe('github') + expect( + resolveStickyAutoExpandedTaskProvider({ + providers: ORDER, + readinessByProvider: afterPreflight, + previousAutoExpanded: 'linear' + }) + ).toBe('linear') + }) + + it('keeps the auto-expand slot after the chosen provider finishes', () => { + // Releasing it would let the next render pop a still-incomplete card open. + const afterLinearCompletes = buildReadiness({ github: { connected: false } }) + + expect(getAutoExpandedTaskProvider(ORDER, afterLinearCompletes)).toBe('github') + expect( + resolveStickyAutoExpandedTaskProvider({ + providers: ORDER, + readinessByProvider: afterLinearCompletes, + previousAutoExpanded: 'linear' + }) + ).toBe('linear') + }) + + it('keeps the auto-expand slot when the chosen provider is hidden', () => { + // Hiding an unfinished provider is the banner's own advice, so it must not + // hand the expansion to another card. + const afterHidingLinear = buildReadiness({ + github: { connected: false }, + linear: { skillInstalled: false, visible: false } + }) + + expect( + resolveStickyAutoExpandedTaskProvider({ + providers: ORDER, + readinessByProvider: afterHidingLinear, + previousAutoExpanded: 'linear' + }) + ).toBe('linear') + }) }) diff --git a/src/renderer/src/components/settings/task-source-setup-state.ts b/src/renderer/src/components/settings/task-source-setup-state.ts index f81fef248bc..3ad27d48fa9 100644 --- a/src/renderer/src/components/settings/task-source-setup-state.ts +++ b/src/renderer/src/components/settings/task-source-setup-state.ts @@ -4,6 +4,7 @@ import type { IntegrationStatusTone } from '@/components/integration-status-pill export type TaskProviderReadiness = { connected: boolean checking: boolean + unavailable?: boolean /** Linear only — agent skill install. Other providers leave this undefined. */ skillInstalled?: boolean skillChecking?: boolean @@ -15,6 +16,7 @@ export type TaskProviderSetupStatus = | 'ready' | 'connect-required' | 'skill-required' + | 'unavailable' | 'hidden' | 'incomplete' @@ -28,6 +30,7 @@ export const TASK_PROVIDER_SETUP_STATUS_TONE: Record< hidden: 'neutral', 'connect-required': 'attention', 'skill-required': 'attention', + unavailable: 'attention', incomplete: 'attention' } @@ -55,7 +58,7 @@ export function getTaskProviderCompletedSteps(readiness: TaskProviderReadiness): } export function isTaskProviderReady(readiness: TaskProviderReadiness): boolean { - if (isTaskProviderChecking(readiness)) { + if (isTaskProviderChecking(readiness) || readiness.unavailable) { return false } const { completed, total } = getTaskProviderCompletedSteps(readiness) @@ -71,6 +74,9 @@ export function getTaskProviderSetupStatus( if (isTaskProviderChecking(readiness)) { return 'checking' } + if (readiness.unavailable) { + return 'unavailable' + } if (isTaskProviderReady(readiness)) { return 'ready' } @@ -98,6 +104,24 @@ export function getIncompleteVisibleTaskProviders( }) } +// A provider nobody has touched is the shipped default, not a problem: settings +// expose all four, so treating "never connected" as a warning would fire on +// every fresh install. Setup counts as started once a credential or the skill +// landed. +export function hasStartedTaskProviderSetup(readiness: TaskProviderReadiness): boolean { + return readiness.connected || readiness.skillInstalled === true +} + +// Warn only about setup that started and stalled partway. +export function getStalledVisibleTaskProviders( + providers: readonly TaskProvider[], + readinessByProvider: Record +): TaskProvider[] { + return getIncompleteVisibleTaskProviders(providers, readinessByProvider).filter((provider) => + hasStartedTaskProviderSetup(readinessByProvider[provider]) + ) +} + // Expand one unfinished provider because defaults expose every provider. export function getAutoExpandedTaskProvider( providers: readonly TaskProvider[], @@ -105,3 +129,19 @@ export function getAutoExpandedTaskProvider( ): TaskProvider | null { return getIncompleteVisibleTaskProviders(providers, readinessByProvider)[0] ?? null } + +// Auto-expand is a one-time first-open affordance. Once a card has claimed it, +// it keeps it for the life of the pane: releasing the slot when that provider +// finishes (or gets hidden) would let a later gh/glab preflight pop a different +// card open under the user, and could unmount an open install terminal. +export function resolveStickyAutoExpandedTaskProvider({ + providers, + readinessByProvider, + previousAutoExpanded +}: { + providers: readonly TaskProvider[] + readinessByProvider: Record + previousAutoExpanded: TaskProvider | null +}): TaskProvider | null { + return previousAutoExpanded ?? getAutoExpandedTaskProvider(providers, readinessByProvider) +} diff --git a/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx b/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx index 9fce3fc84fc..1b3610f8466 100644 --- a/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx +++ b/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx @@ -126,6 +126,7 @@ describe('LinearIntegrationCard account scope', () => { const rendered = await renderCard() + expect(rendered.querySelector('[data-settings-section="integrations-linear"]')).not.toBeNull() expect(rendered.textContent).toContain(`Account scope: ${LOCAL_HOST_LABEL}`) expect(rendered.textContent).toContain( 'Credentials and account checks for this provider are owned by this desktop client. Use Settings > Remote Orca Servers > Advanced to edit server-owned credentials.' diff --git a/src/renderer/src/components/settings/task-tracker-integration-cards.tsx b/src/renderer/src/components/settings/task-tracker-integration-cards.tsx index 6c2cc1b9575..e40c864a7ca 100644 --- a/src/renderer/src/components/settings/task-tracker-integration-cards.tsx +++ b/src/renderer/src/components/settings/task-tracker-integration-cards.tsx @@ -11,6 +11,7 @@ import { useIntegrationSubordinateRowClass } from './integration-card-presentati import { LinearAgentSkillInstallCta } from './linear-agent-skill-install-cta' import { getProviderAccountScope } from './provider-account-scope' import { ProviderHostScopeControl } from './ProviderHostScopeControl' +import { LINEAR_INTEGRATION_SECTION_ID } from './task-provider-integration-section-ids' import { translate } from '@/i18n/i18n' type VerificationResult = { state: 'ok' | 'error'; error?: string } @@ -68,6 +69,7 @@ export function LinearIntegrationCard(): React.JSX.Element { return ( } name="Linear" description={ diff --git a/src/renderer/src/components/settings/use-linear-agent-skill-setup.ts b/src/renderer/src/components/settings/use-linear-agent-skill-setup.ts new file mode 100644 index 00000000000..21a378a1b37 --- /dev/null +++ b/src/renderer/src/components/settings/use-linear-agent-skill-setup.ts @@ -0,0 +1,106 @@ +import { useCallback, useMemo } from 'react' +import { + AGENT_SKILL_CLI_PREREQUISITE_NOTICE, + ensureOrcaCliAvailableForAgentSkillTerminal +} from '@/lib/agent-skill-cli-prerequisite' +import { + LINEAR_AGENT_SKILL_NAMES, + ORCA_LINEAR_SKILL_INSTALL_COMMAND +} from '@/lib/agent-feature-install-commands' +import { getLinearAgentSkillUpdateTarget } from '@/lib/linear-agent-skill-update-command' +import { + GLOBAL_AGENT_SKILL_SOURCE_KINDS, + useInstalledAgentSkillNames +} from '@/hooks/useInstalledAgentSkills' +import { useActiveProjectSkillRuntime } from '@/hooks/useActiveProjectSkillRuntime' +import { + buildSkillCommandForRuntime, + ensureWslCliAvailableForAgentSkillTerminal, + getWslCliDistroRequest +} from './CliSkillRuntimeSetup' + +// Shared install/update wiring for Task Sources + Linear settings. +export function useLinearAgentSkillSetup(): { + installCommand: string + updateCommand: string + freshnessSkillName: string | undefined + skillInstalled: boolean + skillLoading: boolean + // Status surfaces (step badges, checklist pills) read this so a focus-triggered + // rescan does not flip a known result back to "checking". + skillChecking: boolean + installDisabled: boolean + error: string | null + terminalShellOverride: string | undefined + preInstallNotice: string + refreshSkill: () => Promise + getPrerequisiteStatus: () => Promise>> + onBeforeOpenTerminal: () => Promise +} { + const activeSkillRuntime = useActiveProjectSkillRuntime() + const { + installed: skillInstalled, + loading: skillLoading, + settled: skillSettled, + error: skillError, + skills: linearSkills, + refresh: refreshSkill + } = useInstalledAgentSkillNames(LINEAR_AGENT_SKILL_NAMES, { + discoveryTarget: activeSkillRuntime.discoveryTarget, + sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS + }) + + // Why: the built command also depends on the focused runtime environment, so + // memoizing it on the runtime alone can serve a stale Windows host command. + const installCommand = activeSkillRuntime.installDisabledReason + ? ORCA_LINEAR_SKILL_INSTALL_COMMAND + : buildSkillCommandForRuntime( + ORCA_LINEAR_SKILL_INSTALL_COMMAND, + activeSkillRuntime.agentRuntime + ) + const updateTarget = useMemo( + () => getLinearAgentSkillUpdateTarget(linearSkills, skillInstalled), + [linearSkills, skillInstalled] + ) + const updateCommand = activeSkillRuntime.installDisabledReason + ? updateTarget.command + : buildSkillCommandForRuntime(updateTarget.command, activeSkillRuntime.agentRuntime) + + // Freshness cannot verify WSL, so report presence there. + const freshnessSkillName = + activeSkillRuntime.agentRuntime?.runtime === 'wsl' ? undefined : updateTarget.skillName + + const getPrerequisiteStatus = useCallback( + () => + activeSkillRuntime.agentRuntime?.runtime === 'wsl' + ? window.api.cli.getWslInstallStatus( + getWslCliDistroRequest(activeSkillRuntime.agentRuntime) + ) + : window.api.cli.getInstallStatus(), + [activeSkillRuntime.agentRuntime] + ) + + const onBeforeOpenTerminal = useCallback(async () => { + await (activeSkillRuntime.agentRuntime?.runtime === 'wsl' + ? ensureWslCliAvailableForAgentSkillTerminal(activeSkillRuntime.agentRuntime) + : ensureOrcaCliAvailableForAgentSkillTerminal()) + }, [activeSkillRuntime.agentRuntime]) + + const installDisabled = Boolean(activeSkillRuntime.installDisabledReason) + + return { + installCommand, + updateCommand, + freshnessSkillName, + skillInstalled, + skillLoading, + skillChecking: skillLoading && !skillSettled, + installDisabled, + error: activeSkillRuntime.installDisabledReason ?? skillError, + terminalShellOverride: activeSkillRuntime.terminalShellOverride, + preInstallNotice: AGENT_SKILL_CLI_PREREQUISITE_NOTICE, + refreshSkill, + getPrerequisiteStatus, + onBeforeOpenTerminal + } +} diff --git a/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx b/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx new file mode 100644 index 00000000000..cb2cb636692 --- /dev/null +++ b/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx @@ -0,0 +1,172 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { TaskProvider } from '../../../../shared/types' +import type { TaskProviderReadiness } from './task-source-setup-state' +import { useTaskSourceProviderReadiness } from './use-task-source-provider-readiness' + +const mocks = vi.hoisted(() => ({ + state: {} as Record, + skill: { + installed: false, + loading: false, + settled: true, + error: null, + skills: [], + refresh: vi.fn() + } +})) + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: Record) => unknown) => selector(mocks.state) +})) + +vi.mock('@/lib/local-preflight-context', () => ({ + getLocalPreflightContext: () => null, + localPreflightContextKey: () => 'local' +})) + +vi.mock('@/lib/provider-runtime-context', () => ({ + getProviderRuntimeContextKey: () => 'local' +})) + +vi.mock('@/hooks/useActiveProjectSkillRuntime', () => ({ + useActiveProjectSkillRuntime: () => ({ installDisabledReason: null }) +})) + +vi.mock('@/hooks/useLinearProviderConnected', () => ({ + useLinearProviderConnected: () => Boolean(mocks.state.linearConnected) +})) + +vi.mock('@/hooks/useInstalledAgentSkills', () => ({ + GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['home'], + useInstalledAgentSkillNames: () => mocks.skill +})) + +const ALL_PROVIDERS: readonly TaskProvider[] = ['github', 'gitlab', 'linear', 'jira'] + +let root: Root | null = null +let container: HTMLDivElement | null = null +let latest: Record | null = null + +function Probe({ visibleProviders }: { visibleProviders: readonly TaskProvider[] }): null { + latest = useTaskSourceProviderReadiness(visibleProviders) + return null +} + +async function renderProbe( + visibleProviders: readonly TaskProvider[] = ALL_PROVIDERS +): Promise { + if (!container) { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + } + await act(async () => { + root?.render() + }) +} + +beforeEach(() => { + mocks.state = { + settings: {}, + preflightStatus: { + gh: { installed: true, authenticated: true }, + glab: { installed: true, authenticated: true } + }, + preflightStatusChecked: true, + preflightStatusContextKey: 'local', + preflightStatusError: null, + preflightStatusLoading: false, + jiraStatus: { connected: true }, + jiraStatusChecked: true, + jiraStatusContextKey: 'local', + linearStatusChecked: true, + linearStatusContextKey: 'local', + linearConnected: true + } + mocks.skill = { + installed: true, + loading: false, + settled: true, + error: null, + skills: [], + refresh: vi.fn() + } +}) + +afterEach(async () => { + if (root) { + await act(async () => { + root?.unmount() + }) + } + root = null + container?.remove() + container = null + latest = null +}) + +describe('useTaskSourceProviderReadiness', () => { + it('reports every provider connected once checks land cleanly', async () => { + await renderProbe() + + expect(latest?.github).toMatchObject({ connected: true, checking: false }) + expect(latest?.gitlab).toMatchObject({ connected: true, checking: false }) + expect(latest?.jira).toMatchObject({ connected: true, checking: false }) + expect(latest?.linear).toMatchObject({ + connected: true, + checking: false, + skillInstalled: true, + skillChecking: false + }) + }) + + it('does not read code-host connection facts out of a failed preflight snapshot', async () => { + // A failed refresh keeps the previous status object, so Integrations reports + // GitHub as disconnected; Task Sources must not disagree. + mocks.state.preflightStatusError = 'preflight failed' + + await renderProbe() + + expect(latest?.github.connected).toBe(false) + expect(latest?.gitlab.connected).toBe(false) + expect(latest?.github.unavailable).toBe(true) + expect(latest?.gitlab.unavailable).toBe(true) + }) + + it('keeps the known skill result while a focus-triggered rescan is in flight', async () => { + mocks.skill = { ...mocks.skill, loading: true, settled: true } + + await renderProbe() + + expect(latest?.linear).toMatchObject({ skillInstalled: true, skillChecking: false }) + }) + + it('reports the skill as checking until the first scan settles', async () => { + mocks.skill = { ...mocks.skill, installed: false, loading: true, settled: false } + + await renderProbe() + + expect(latest?.linear).toMatchObject({ skillInstalled: false, skillChecking: true }) + }) + + it('marks providers hidden when they are not in the visible list', async () => { + await renderProbe(['github', 'linear']) + + expect(latest?.github.visible).toBe(true) + expect(latest?.linear.visible).toBe(true) + expect(latest?.gitlab.visible).toBe(false) + expect(latest?.jira.visible).toBe(false) + }) + + it('recomputes visibility when the provider list changes', async () => { + await renderProbe(['github', 'linear']) + expect(latest?.jira.visible).toBe(false) + + await renderProbe(['github', 'linear', 'jira']) + expect(latest?.jira.visible).toBe(true) + }) +}) diff --git a/src/renderer/src/components/settings/use-task-source-provider-readiness.ts b/src/renderer/src/components/settings/use-task-source-provider-readiness.ts index a6b914f725f..97db089a677 100644 --- a/src/renderer/src/components/settings/use-task-source-provider-readiness.ts +++ b/src/renderer/src/components/settings/use-task-source-provider-readiness.ts @@ -19,6 +19,7 @@ export function useTaskSourceProviderReadiness( const preflightStatus = useAppStore((s) => s.preflightStatus) const preflightStatusChecked = useAppStore((s) => s.preflightStatusChecked) const preflightStatusContextKey = useAppStore((s) => s.preflightStatusContextKey) + const preflightStatusError = useAppStore((s) => s.preflightStatusError) const preflightStatusLoading = useAppStore((s) => s.preflightStatusLoading) const expectedPreflightContextKey = useAppStore((s) => localPreflightContextKey(getLocalPreflightContext(s)) @@ -32,20 +33,27 @@ export function useTaskSourceProviderReadiness( const providerRuntimeContextKey = getProviderRuntimeContextKey(settings) const activeSkillRuntime = useActiveProjectSkillRuntime() - const { installed: linearSkillInstalled, loading: linearSkillLoading } = - useInstalledAgentSkillNames(LINEAR_AGENT_SKILL_NAMES, { - discoveryTarget: activeSkillRuntime.discoveryTarget, - sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS - }) + const { + installed: linearSkillInstalled, + loading: linearSkillLoading, + settled: linearSkillSettled + } = useInstalledAgentSkillNames(LINEAR_AGENT_SKILL_NAMES, { + discoveryTarget: activeSkillRuntime.discoveryTarget, + sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS + }) const preflightCurrent = preflightStatusContextKey === expectedPreflightContextKey const reviewChecking = preflightStatusLoading || !preflightStatusChecked || !preflightCurrent + // A failed preflight leaves the previous status object in place, so mirror + // Integrations and refuse to read connection facts out of a stale snapshot. + const reviewReadyForConnection = !reviewChecking && preflightStatusError === null + const reviewUnavailable = !reviewChecking && preflightStatusError !== null const githubConnected = - !reviewChecking && + reviewReadyForConnection && preflightStatus?.gh?.installed === true && preflightStatus.gh.authenticated === true const gitlabConnected = - !reviewChecking && + reviewReadyForConnection && preflightStatus?.glab?.installed === true && preflightStatus.glab.authenticated === true const jiraChecking = jiraStatusContextKey !== providerRuntimeContextKey || !jiraStatusChecked @@ -61,18 +69,20 @@ export function useTaskSourceProviderReadiness( github: { connected: githubConnected, checking: reviewChecking, + unavailable: reviewUnavailable, visible: visible.has('github') }, gitlab: { connected: gitlabConnected, checking: reviewChecking, + unavailable: reviewUnavailable, visible: visible.has('gitlab') }, linear: { connected: linearConnected, checking: linearChecking, skillInstalled: linearSkillInstalled, - skillChecking: linearSkillLoading, + skillChecking: linearSkillLoading && !linearSkillSettled, visible: visible.has('linear') }, jira: { @@ -90,7 +100,9 @@ export function useTaskSourceProviderReadiness( linearConnected, linearSkillInstalled, linearSkillLoading, + linearSkillSettled, reviewChecking, + reviewUnavailable, visibleProvidersKey ]) } diff --git a/src/renderer/src/hooks/installed-agent-skill-discovery.ts b/src/renderer/src/hooks/installed-agent-skill-discovery.ts new file mode 100644 index 00000000000..af611cd6249 --- /dev/null +++ b/src/renderer/src/hooks/installed-agent-skill-discovery.ts @@ -0,0 +1,110 @@ +import type { SkillDiscoveryResult, SkillDiscoveryTarget } from '../../../shared/skills' + +let cachedDiscoveryByTarget = new Map() +let pendingDiscoveryByTarget = new Map>() +let pendingDiscoverySatisfiesForcedRefreshByTarget = new Map() + +function normalizeSkillDiscoveryTarget( + target: SkillDiscoveryTarget | undefined +): SkillDiscoveryTarget | undefined { + const projectRuntime = target?.projectRuntime + if (projectRuntime) { + if (projectRuntime.status === 'repair-required') { + return { projectRuntime } + } + if (projectRuntime.runtime.kind === 'wsl') { + return { + runtime: 'wsl', + wslDistro: projectRuntime.runtime.distro, + projectRuntime + } + } + return { + runtime: 'host', + projectRuntime + } + } + + if (target?.runtime !== 'wsl') { + return undefined + } + return { runtime: 'wsl', wslDistro: target.wslDistro?.trim() || null } +} + +export function getSkillDiscoveryTargetKey(target: SkillDiscoveryTarget | undefined): string { + if (target?.projectRuntime) { + return target.projectRuntime.status === 'resolved' + ? target.projectRuntime.runtime.cacheKey + : target.projectRuntime.repair.cacheKey + } + const normalizedTarget = normalizeSkillDiscoveryTarget(target) + return normalizedTarget?.runtime === 'wsl' ? `wsl:${normalizedTarget.wslDistro ?? ''}` : 'host' +} + +export function getCachedInstalledAgentSkillDiscovery( + target: SkillDiscoveryTarget | undefined +): SkillDiscoveryResult | null { + return cachedDiscoveryByTarget.get(getSkillDiscoveryTargetKey(target)) ?? null +} + +function startInstalledAgentSkillDiscovery( + force: boolean, + target: SkillDiscoveryTarget | undefined +): Promise { + const key = getSkillDiscoveryTargetKey(target) + const normalizedTarget = normalizeSkillDiscoveryTarget(target) + const discovery = window.api.skills + .discover(normalizedTarget) + .then((result) => { + cachedDiscoveryByTarget.set(key, result) + return result + }) + .finally(() => { + if (pendingDiscoveryByTarget.get(key) === discovery) { + pendingDiscoveryByTarget.delete(key) + pendingDiscoverySatisfiesForcedRefreshByTarget.delete(key) + } + }) + pendingDiscoveryByTarget.set(key, discovery) + pendingDiscoverySatisfiesForcedRefreshByTarget.set(key, force) + return discovery +} + +export async function discoverInstalledAgentSkills( + force: boolean, + target?: SkillDiscoveryTarget +): Promise { + const key = getSkillDiscoveryTargetKey(target) + const cachedDiscovery = cachedDiscoveryByTarget.get(key) + if (!force && cachedDiscovery) { + return cachedDiscovery + } + + const inFlightDiscovery = pendingDiscoveryByTarget.get(key) + if (inFlightDiscovery) { + if (!force || pendingDiscoverySatisfiesForcedRefreshByTarget.get(key)) { + return inFlightDiscovery + } + try { + await inFlightDiscovery + } catch { + // An explicit re-check should still read disk after an older background scan fails. + } + const nextPendingDiscovery = pendingDiscoveryByTarget.get(key) + if (nextPendingDiscovery && nextPendingDiscovery !== inFlightDiscovery) { + return nextPendingDiscovery + } + } + + return startInstalledAgentSkillDiscovery(force, target) +} + +export function invalidateInstalledAgentSkillDiscovery(): void { + cachedDiscoveryByTarget.clear() +} + +export function resetInstalledAgentSkillDiscoveryForTests(): void { + cachedDiscoveryByTarget = new Map() + pendingDiscoveryByTarget = new Map() + pendingDiscoverySatisfiesForcedRefreshByTarget = new Map() +} diff --git a/src/renderer/src/hooks/installed-agent-skills-change-event.ts b/src/renderer/src/hooks/installed-agent-skills-change-event.ts index 2e11a17ff03..3cee8c70dc1 100644 --- a/src/renderer/src/hooks/installed-agent-skills-change-event.ts +++ b/src/renderer/src/hooks/installed-agent-skills-change-event.ts @@ -1 +1,2 @@ export const INSTALLED_AGENT_SKILLS_CHANGED_EVENT = 'orca:installed-agent-skills-changed' +export const INSTALLED_AGENT_SKILLS_REFRESHED_EVENT = 'orca:installed-agent-skills-refreshed' diff --git a/src/renderer/src/hooks/useActiveProjectSkillRuntime.ts b/src/renderer/src/hooks/useActiveProjectSkillRuntime.ts index bc07518100e..c25044070a8 100644 --- a/src/renderer/src/hooks/useActiveProjectSkillRuntime.ts +++ b/src/renderer/src/hooks/useActiveProjectSkillRuntime.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useRef } from 'react' import { useShallow } from 'zustand/react/shallow' import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' import type { SkillDiscoveryTarget } from '../../../shared/skills' @@ -25,6 +25,16 @@ const EMPTY_ACTIVE_PROJECT_SKILL_RUNTIME: ActiveProjectSkillRuntime = Object.fre installDisabledReason: null }) +// Why: on Windows the runtime resolution is rebuilt from scratch on every +// worktree-store change, so a same-runtime result still arrives with a fresh +// identity. Downstream skill discovery keys effects off `discoveryTarget`, so +// that churn would re-run a scan (and blink its loading state) per store update. +// Serializing the whole value (rather than picking fields) keeps the comparison +// honest if the resolution grows a field the runtime cache keys do not encode. +function activeProjectSkillRuntimeIdentity(runtime: ActiveProjectSkillRuntime): string { + return JSON.stringify(runtime) +} + export function useActiveProjectSkillRuntime(): ActiveProjectSkillRuntime { const runtimeState = useAppStore( useShallow((state) => ({ @@ -39,7 +49,7 @@ export function useActiveProjectSkillRuntime(): ActiveProjectSkillRuntime { const currentPlatform = getCurrentPlatform() const windowsCapabilities = useWindowsTerminalCapabilities(currentPlatform === 'win32') - return useMemo(() => { + const resolved = useMemo(() => { const projectRuntime = getLocalProjectExecutionRuntimeContext( runtimeState, undefined, @@ -66,6 +76,15 @@ export function useActiveProjectSkillRuntime(): ActiveProjectSkillRuntime { installDisabledReason: getProjectSkillInstallDisabledReason(projectRuntime) } }, [currentPlatform, runtimeState, windowsCapabilities]) + + const stableRef = useRef(resolved) + if ( + activeProjectSkillRuntimeIdentity(stableRef.current) !== + activeProjectSkillRuntimeIdentity(resolved) + ) { + stableRef.current = resolved + } + return stableRef.current } function getCurrentPlatform(): NodeJS.Platform { diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx b/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx index 48274dd27be..cfad64f0ed3 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx +++ b/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx @@ -13,6 +13,7 @@ import { GLOBAL_AGENT_SKILL_SOURCE_KINDS, type InstalledAgentSkillState, _installedAgentSkillDiscoveryInternalsForTests, + notifyInstalledAgentSkillsRefreshed, useInstalledAgentSkillNames } from './useInstalledAgentSkills' @@ -245,4 +246,96 @@ describe('useInstalledAgentSkill', () => { projectRuntime: projectWslRuntime }) }) + + it('stays settled through a focus rescan so status surfaces do not flash', async () => { + const firstScan = deferred() + const focusScan = deferred() + const discover = vi + .fn<(target?: SkillDiscoveryTarget) => Promise>() + .mockReturnValueOnce(firstScan.promise) + .mockReturnValueOnce(focusScan.promise) + Object.defineProperty(window, 'api', { + configurable: true, + value: { skills: { discover } } + }) + + await renderProbe() + expect(latestState?.settled).toBe(false) + + firstScan.resolve(discoveryResult([skill({ name: 'orca-linear' })])) + await act(async () => { + await firstScan.promise + }) + expect(latestState?.settled).toBe(true) + expect(latestState?.installed).toBe(true) + + await act(async () => { + window.dispatchEvent(new Event('focus')) + }) + + // The forced rescan is in flight, but the previous result is still current. + expect(latestState?.loading).toBe(true) + expect(latestState?.settled).toBe(true) + expect(latestState?.installed).toBe(true) + + focusScan.resolve(discoveryResult([skill({ name: 'orca-linear' })])) + await act(async () => { + await focusScan.promise + }) + expect(latestState?.settled).toBe(true) + }) + + it('reuses cached discovery when another surface finishes re-checking', async () => { + const discover = vi + .fn<(target?: SkillDiscoveryTarget) => Promise>() + .mockResolvedValue(discoveryResult([skill({ name: 'orca-linear' })])) + Object.defineProperty(window, 'api', { + configurable: true, + value: { skills: { discover } } + }) + + await renderProbe() + await act(async () => { + await Promise.resolve() + }) + expect(discover).toHaveBeenCalledTimes(1) + + await act(async () => { + notifyInstalledAgentSkillsRefreshed() + await Promise.resolve() + }) + + expect(discover).toHaveBeenCalledTimes(1) + expect(latestState?.installed).toBe(true) + }) + + it('reports unsettled again when the discovery target changes', async () => { + const hostScan = deferred() + const wslScan = deferred() + const discover = vi + .fn<(target?: SkillDiscoveryTarget) => Promise>() + .mockReturnValueOnce(hostScan.promise) + .mockReturnValueOnce(wslScan.promise) + Object.defineProperty(window, 'api', { + configurable: true, + value: { skills: { discover } } + }) + + await renderProbe() + hostScan.resolve(discoveryResult([skill({ name: 'orca-linear' })])) + await act(async () => { + await hostScan.promise + }) + expect(latestState?.settled).toBe(true) + + await renderProbe({ runtime: 'wsl', wslDistro: 'Ubuntu' }) + expect(latestState?.settled).toBe(false) + + wslScan.resolve(discoveryResult([])) + await act(async () => { + await wslScan.promise + }) + expect(latestState?.settled).toBe(true) + expect(latestState?.installed).toBe(false) + }) }) diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.test.ts b/src/renderer/src/hooks/useInstalledAgentSkills.test.ts index 28453f00489..df6ec1a1f4b 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.test.ts +++ b/src/renderer/src/hooks/useInstalledAgentSkills.test.ts @@ -5,7 +5,8 @@ import { GLOBAL_AGENT_SKILL_SOURCE_KINDS, _installedAgentSkillDiscoveryInternalsForTests, hasInstalledAgentSkill, - hasInstalledAgentSkillNamed + hasInstalledAgentSkillNamed, + notifyInstalledAgentSkillsRefreshed } from './useInstalledAgentSkills' afterEach(() => { @@ -223,6 +224,31 @@ describe('discoverInstalledAgentSkills', () => { await expect(forcedRefresh).resolves.toBe(freshResult) }) + it('lets completed-scan broadcasts reuse the cached result', async () => { + const discover = vi + .fn<() => Promise>() + .mockResolvedValue(discoveryResult([skill({ name: 'orca-linear' })])) + vi.stubGlobal('window', { + api: { skills: { discover } }, + dispatchEvent: vi.fn(), + CustomEvent: class {} + }) + + await _installedAgentSkillDiscoveryInternalsForTests.discoverInstalledAgentSkills(true) + notifyInstalledAgentSkillsRefreshed() + const fromSubscribers = [1, 2, 3].map(() => + _installedAgentSkillDiscoveryInternalsForTests.discoverInstalledAgentSkills(false) + ) + + expect(discover).toHaveBeenCalledTimes(1) + for (const pending of fromSubscribers) { + await expect(pending).resolves.toMatchObject({ + skills: [expect.objectContaining({ name: 'orca-linear' })] + }) + } + expect(discover).toHaveBeenCalledTimes(1) + }) + it('caches host and WSL discovery results separately', async () => { const hostResult = discoveryResult([skill({ name: 'host-skill' })]) const wslResult = discoveryResult([skill({ name: 'wsl-skill' })]) diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.ts b/src/renderer/src/hooks/useInstalledAgentSkills.ts index e19bac655b1..2ed1cd08688 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.ts +++ b/src/renderer/src/hooks/useInstalledAgentSkills.ts @@ -7,7 +7,17 @@ import type { } from '../../../shared/skills' import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands' import { markOrchestrationSetupComplete } from '@/lib/orchestration-setup-state' -import { INSTALLED_AGENT_SKILLS_CHANGED_EVENT } from './installed-agent-skills-change-event' +import { + INSTALLED_AGENT_SKILLS_CHANGED_EVENT, + INSTALLED_AGENT_SKILLS_REFRESHED_EVENT +} from './installed-agent-skills-change-event' +import { + discoverInstalledAgentSkills, + getCachedInstalledAgentSkillDiscovery, + getSkillDiscoveryTargetKey, + invalidateInstalledAgentSkillDiscovery, + resetInstalledAgentSkillDiscoveryForTests +} from './installed-agent-skill-discovery' import { useMountedRef } from './useMountedRef' export const GLOBAL_AGENT_SKILL_SOURCE_KINDS = [ @@ -27,15 +37,16 @@ type InstalledAgentSkillMatchOptions = { export type InstalledAgentSkillState = { installed: boolean loading: boolean + // Why: a forced rescan (window focus, Re-check) keeps the previous result, so + // only the first scan per discovery target is genuinely unknown. Surfaces that + // render status read this instead of `loading` to avoid flashing on every + // alt-tab; `loading` still drives spinners on the action that triggered it. + settled: boolean error: string | null skills: readonly DiscoveredSkill[] refresh: () => Promise } -let cachedDiscoveryByTarget = new Map() -let pendingDiscoveryByTarget = new Map>() -let pendingDiscoverySatisfiesForcedRefreshByTarget = new Map() - function normalizeSkillName(value: string): string { return value.trim().toLowerCase() } @@ -77,111 +88,23 @@ export function hasInstalledAgentSkillNamed( } export function notifyInstalledAgentSkillsChanged(): void { - cachedDiscoveryByTarget.clear() + invalidateInstalledAgentSkillDiscovery() if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent(INSTALLED_AGENT_SKILLS_CHANGED_EVENT)) } } -function normalizeSkillDiscoveryTarget( - target: SkillDiscoveryTarget | undefined -): SkillDiscoveryTarget | undefined { - const projectRuntime = target?.projectRuntime - if (projectRuntime) { - if (projectRuntime.status === 'repair-required') { - return { projectRuntime } - } - if (projectRuntime.runtime.kind === 'wsl') { - return { - runtime: 'wsl', - wslDistro: projectRuntime.runtime.distro, - projectRuntime - } - } - return { - runtime: 'host', - projectRuntime - } +export function notifyInstalledAgentSkillsRefreshed(): void { + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent(INSTALLED_AGENT_SKILLS_REFRESHED_EVENT)) } - - if (target?.runtime !== 'wsl') { - return undefined - } - return { runtime: 'wsl', wslDistro: target.wslDistro?.trim() || null } -} - -function getSkillDiscoveryTargetKey(target: SkillDiscoveryTarget | undefined): string { - if (target?.projectRuntime) { - return target.projectRuntime.status === 'resolved' - ? target.projectRuntime.runtime.cacheKey - : target.projectRuntime.repair.cacheKey - } - const normalizedTarget = normalizeSkillDiscoveryTarget(target) - return normalizedTarget?.runtime === 'wsl' ? `wsl:${normalizedTarget.wslDistro ?? ''}` : 'host' -} - -function startInstalledAgentSkillDiscovery( - force: boolean, - target: SkillDiscoveryTarget | undefined -): Promise { - const key = getSkillDiscoveryTargetKey(target) - const normalizedTarget = normalizeSkillDiscoveryTarget(target) - const discovery = window.api.skills - .discover(normalizedTarget) - .then((result) => { - cachedDiscoveryByTarget.set(key, result) - return result - }) - .finally(() => { - if (pendingDiscoveryByTarget.get(key) === discovery) { - pendingDiscoveryByTarget.delete(key) - pendingDiscoverySatisfiesForcedRefreshByTarget.delete(key) - } - }) - pendingDiscoveryByTarget.set(key, discovery) - pendingDiscoverySatisfiesForcedRefreshByTarget.set(key, force) - return discovery -} - -async function discoverInstalledAgentSkills( - force: boolean, - target?: SkillDiscoveryTarget -): Promise { - const key = getSkillDiscoveryTargetKey(target) - const cachedDiscovery = cachedDiscoveryByTarget.get(key) - if (!force && cachedDiscovery) { - return cachedDiscovery - } - - const inFlightDiscovery = pendingDiscoveryByTarget.get(key) - if (inFlightDiscovery) { - if (!force || pendingDiscoverySatisfiesForcedRefreshByTarget.get(key)) { - return inFlightDiscovery - } - try { - await inFlightDiscovery - } catch { - // Why: an explicit re-check should still read current disk state even if - // the older background scan failed. - } - const nextPendingDiscovery = pendingDiscoveryByTarget.get(key) - if (nextPendingDiscovery && nextPendingDiscovery !== inFlightDiscovery) { - return nextPendingDiscovery - } - } - - return startInstalledAgentSkillDiscovery(force, target) } export const _installedAgentSkillDiscoveryInternalsForTests = { discoverInstalledAgentSkills, getSkillDiscoveryTargetKey, isOrchestrationSkillName, - reset(): void { - cachedDiscoveryByTarget = new Map() - pendingDiscoveryByTarget = new Map() - pendingDiscoverySatisfiesForcedRefreshByTarget = new Map() - } + reset: resetInstalledAgentSkillDiscoveryForTests } export function useInstalledAgentSkill( @@ -199,7 +122,7 @@ export function useInstalledAgentSkillNames( const skillNamesKey = skillNames.map(normalizeSkillName).join('\n') const candidateSkillNames = useMemo(() => skillNamesKey.split('\n'), [skillNamesKey]) const discoveryTargetKey = getSkillDiscoveryTargetKey(discoveryTarget) - const cachedDiscovery = cachedDiscoveryByTarget.get(discoveryTargetKey) ?? null + const cachedDiscovery = getCachedInstalledAgentSkillDiscovery(discoveryTarget) const [result, setResult] = useState(cachedDiscovery) const [loading, setLoading] = useState(enabled && !cachedDiscovery) const [error, setError] = useState(null) @@ -217,7 +140,7 @@ export function useInstalledAgentSkillNames( stateResetInputRef.current.discoveryTargetKey !== discoveryTargetKey || stateResetInputRef.current.enabled !== enabled ) { - const nextCachedDiscovery = cachedDiscoveryByTarget.get(discoveryTargetKey) ?? null + const nextCachedDiscovery = getCachedInstalledAgentSkillDiscovery(discoveryTarget) const nextLoading = enabled && !nextCachedDiscovery stateResetInputRef.current = { discoveryTargetKey, enabled } resultForRender = nextCachedDiscovery @@ -229,7 +152,7 @@ export function useInstalledAgentSkillNames( } const refresh = useCallback( - async (force = true): Promise => { + async (force = true, showLoading = true): Promise => { const requestDiscoveryTargetKey = discoveryTargetKey const requestGeneration = ++refreshGenerationRef.current const writeIfCurrent = (write: () => void): void => { @@ -243,14 +166,18 @@ export function useInstalledAgentSkillNames( } if (!enabled) { - writeIfCurrent(() => { - setLoading(false) - }) + if (showLoading) { + writeIfCurrent(() => { + setLoading(false) + }) + } return false } - writeIfCurrent(() => { - setLoading(true) - }) + if (showLoading) { + writeIfCurrent(() => { + setLoading(true) + }) + } let installedAfterRefresh = false try { const next = await discoverInstalledAgentSkills(force, discoveryTarget) @@ -270,9 +197,11 @@ export function useInstalledAgentSkillNames( ) }) } finally { - writeIfCurrent(() => { - setLoading(false) - }) + if (showLoading) { + writeIfCurrent(() => { + setLoading(false) + }) + } } return installedAfterRefresh }, @@ -290,13 +219,18 @@ export function useInstalledAgentSkillNames( const refreshFromExternalChange = (): void => { void refresh(true) } + const refreshFromCompletedScan = (): void => { + void refresh(false, false) + } // Why: skill install commands run outside React state, often in a terminal. // Refresh on focus and explicit install events so completion is detected. window.addEventListener('focus', refreshFromExternalChange) window.addEventListener(INSTALLED_AGENT_SKILLS_CHANGED_EVENT, refreshFromExternalChange) + window.addEventListener(INSTALLED_AGENT_SKILLS_REFRESHED_EVENT, refreshFromCompletedScan) return () => { window.removeEventListener('focus', refreshFromExternalChange) window.removeEventListener(INSTALLED_AGENT_SKILLS_CHANGED_EVENT, refreshFromExternalChange) + window.removeEventListener(INSTALLED_AGENT_SKILLS_REFRESHED_EVENT, refreshFromCompletedScan) } }, [enabled, refresh]) @@ -324,6 +258,7 @@ export function useInstalledAgentSkillNames( return { installed, loading: loadingForRender, + settled: enabled && resultForRender !== null, error: errorForRender, skills, refresh: forceRefresh diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 7537194b3a7..47e72dd8ffc 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -623,7 +623,7 @@ "readTicket": "Read the linked ticket", "postUpdate": "Post a progress update", "moveState": "Move the ticket forward", - "attachPr": "Attach the PR link", + "attachPr": "Attach the review link", "triageFollowups": "Triage and create follow-ups", "readTicketSummary": "Pull the linked Linear issue's full context before starting work.", "readTicketPrompt": "Use {{value0}} to read the linked Linear issue for this worktree, then summarize the goal and acceptance criteria before you start.", @@ -631,8 +631,8 @@ "postUpdatePrompt": "Use {{value0}} to post a completion update on the linked Linear issue with what changed and how it was verified.", "moveStateSummary": "Advance the Linear workflow state as the work progresses.", "moveStatePrompt": "Use {{value0}} to move the linked Linear issue to In Review now that the change is ready.", - "attachPrSummary": "Link the pull request to the Linear issue when you open it.", - "attachPrPrompt": "Use {{value0}} to attach this PR to the linked Linear issue with a PR link.", + "attachPrSummary": "Link the pull or merge request to the Linear issue when you open it.", + "attachPrPrompt": "Use {{value0}} to attach this pull or merge request to the linked Linear issue.", "triageFollowupsSummary": "Set assignee, priority, or estimate, and file parented follow-up tickets.", "triageFollowupsPrompt": "Use {{value0}} to triage the linked Linear issue — set priority and estimate — and create a parented follow-up ticket for the deferred cleanup." } @@ -5249,7 +5249,9 @@ "copyCommandAria": "Copy command", "runCommandDescription": "Press Enter to run the command.", "5f818f12ab": "Preparing...", - "4c05b9d7cb": "Preparing setup terminal." + "4c05b9d7cb": "Preparing setup terminal.", + "installLabel": "Install", + "updateLabel": "Update" }, "AgentsPane": { "d83834f5e6": "Detecting installed agents…", @@ -7039,38 +7041,34 @@ "53e9aabfc0": "Uses OpenSSH multiplexing when available. Turn off for hosts with custom SSH restrictions." }, "TasksPane": { - "3a72b9745e": "Choose which task providers appear in the Tasks page and sidebar shortcuts.", "f71d8a9dd3": "Task Providers", - "71644aba56": "Choose which task providers appear in the Tasks page source picker and sidebar shortcuts. At least one provider must stay visible.", - "93e72ef659": "Task Sources", - "8e1305fcc6": "Show Jira in the Tasks source picker and sidebar shortcuts.", "6b23a34f6d": "Jira", - "e4170c9615": "Show Linear in the Tasks source picker and sidebar shortcuts.", "09ae2d7c51": "Linear", - "dd67a1b6e1": "Show GitLab in the Tasks source picker and sidebar shortcuts.", "7c5d7fdc20": "GitLab", - "1db47236cd": "Show GitHub in the Tasks source picker and sidebar shortcuts.", "e14063e727": "GitHub", "connectProviderTitle": "Connect {{provider}}", "connectCodeHostDescription": "Install and authenticate the CLI under Integrations so Orca can load issues.", + "connectionCheckUnavailable": "Orca couldn't check this connection. Try again, or open Integrations for setup details.", + "retryConnection": "Try again", "openIntegrations": "Integrations", "connectInIntegrations": "Set up in Integrations", "connectJiraTitle": "Connect Jira", "connectJiraDescription": "Add a Jira Cloud site or self-hosted instance with an API token or PAT.", - "manageJira": "Manage access", + "manageJira": "Manage keys", "addJira": "Add Jira access", "githubDescription": "Browse GitHub issues and start workspaces from them.", "gitlabDescription": "Browse GitLab issues and start workspaces from them.", "linearDescription": "Connect Linear, install the agent skill, and show it in Tasks.", "jiraDescription": "Connect Jira Cloud or self-hosted Jira and show it in Tasks.", "setupTitle": "Task management setup", - "setupDescription": "Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets.", + "setupDescription": "Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets. At least one provider must stay visible.", "incompleteBannerTitle": "Some visible providers still need setup", - "incompleteBannerBody": "Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.", + "incompleteBannerBody": "Hide providers you do not use, or expand a card and finish its steps.", "providersDescription": "Each card walks through connection (and skill, for Linear) plus whether it appears in Tasks.", "integrationsHint": "Credentials for all providers also live under", "integrationsLink": "Integrations", - "skillHint": ". After Linear is connected, usage examples stay under Settings → Linear." + "skillHint": ". After Linear is connected, usage examples stay under Settings → Linear.", + "incompleteBannerBodyWithLinear": "Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks." }, "TerminalAppearanceSection": { "a14a427ae4": "Thickness of the pane divider line.", @@ -8709,7 +8707,6 @@ "3d81c26d78": "source", "cf0e3e0c2f": "provider", "2ec54bee51": "tasks", - "765f0c544d": "Choose which task providers appear in the Tasks page and sidebar shortcuts.", "5b8e4aace5": "Task Providers", "providersDescription": "Connect task providers, install the Linear agent skill, and choose what appears in Tasks.", "setup": "setup", @@ -9527,13 +9524,12 @@ "title": "Linear", "description": "How Linear works in Orca: browse issues, start linked workspaces, and let agents update tickets with /orca-linear.", "skillTitle": "Linear skill", - "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", - "skillSectionTitle": "Agent skill", - "skillSectionBody": "Install or update the skill agents need. You can also complete this step from Task Sources during first-time setup.", "howToUse": "Example prompts", "howToUseDescription": "Click a card to copy a prompt. Use these in a Linear-linked worktree after the skill is installed.", "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal" + "terminalAriaLabel": "Linear skill install terminal", + "manageConnectionHint": "Review connected Linear workspaces and API keys in", + "manageConnectionLink": "Integrations" }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." @@ -9850,9 +9846,7 @@ "manageKeys": "Manage keys", "addAccess": "Add access", "setupSkillTitle": "2. Install the agent skill", - "setupSkillBody": "Gives coding agents /orca-linear for read, update, triage, and PR attach.", - "skillReady": "Ready below", - "installBelow": "Install below", + "setupSkillBody": "Gives coding agents /orca-linear for reading, updates, triage, and attaching pull or merge requests.", "setupVisibleTitle": "3. Show Linear in Tasks", "setupVisibleBody": "Keeps Linear in the Tasks source picker and sidebar shortcuts.", "openTaskSources": "Task Sources", @@ -9860,8 +9854,6 @@ "setupBody": "All three are required for the full Tasks + agent loop. First-time path is also under Task Sources.", "setupReady": "All set", "setupProgress": "{{done}} of {{total}} ready", - "setupHint": "Prefer one guided place for connect + skill + visibility?", - "openFullSetup": "Open Task Sources setup", "notesTitle": "Good to know", "notesIntro": "Quick reminders once Linear is connected and the skill is installed.", "noteLinkedTitle": "Start from a Linear issue", @@ -9869,19 +9861,20 @@ "noteSlashTitle": "Mention /orca-linear", "noteSlashBody": "In chat, use /orca-linear (or ask in plain language) so the agent loads the skill for that turn.", "noteKeysTitle": "Keys follow the runtime", - "noteKeysBody": "API keys and workspaces are stored for the active runtime. Manage them anytime under Integrations.", + "noteKeysBody": "API keys and workspaces are stored for the active runtime.", "noteVisibilityTitle": "Hiding ≠ disconnect", - "noteVisibilityBody": "Hiding Linear in Task Sources only removes it from the picker. It does not remove your key or skill." + "noteVisibilityBody": "Hiding Linear in Task Sources only removes it from the picker. It does not remove your key or skill.", + "setupChecking": "Checking…" }, "TaskSourceLinearSetup": { "connectTitle": "Connect Linear", "connectDescription": "Add a Personal API key so Orca can browse issues and open workspaces with ticket context.", - "manageAccess": "Manage access", + "manageAccess": "Manage keys", "addAccess": "Add Linear access", "connectedHint": "Workspaces and keys are stored for the active runtime. You can add more access any time.", "recheck": "Re-check connection", "skillTitle": "Install Linear agent skill", - "skillDescription": "Gives agents the /orca-linear skill to read tickets, post updates, move states, and attach PRs.", + "skillDescription": "Gives agents /orca-linear to read tickets, post updates, move states, and attach pull or merge requests.", "skillBlocked": "Connect Linear first, then install the skill for agents.", "skillPanelTitle": "Linear skill", "terminalTitle": "Linear skill setup", @@ -9893,6 +9886,7 @@ "statusReady": "Ready", "statusConnectRequired": "Connect required", "statusSkillRequired": "Skill required", + "statusUnavailable": "Status unavailable", "statusHidden": "Hidden from Tasks", "statusIncomplete": "Needs setup", "collapseSetup": "Collapse {{provider}} setup steps", @@ -9901,8 +9895,10 @@ "TaskSourceShowInTasksStep": { "shown": "Shown", "show": "Show", + "hide": "Hide", "hideProviderAction": "Hide {{provider}} from Tasks", "showProviderAction": "Show {{provider}} in Tasks", + "lastProviderAction": "{{provider}} is shown in Tasks. At least one provider must stay visible.", "lastProviderHint": "At least one provider must stay visible in Tasks.", "title": "Show in Tasks", "description": "Include this provider in the Tasks source picker and sidebar shortcuts." diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 6d03b2de634..5267cfca2fa 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -600,7 +600,7 @@ "readTicket": "Read the linked ticket", "postUpdate": "Post a progress update", "moveState": "Move the ticket forward", - "attachPr": "Attach the PR link", + "attachPr": "Adjuntar el enlace de revisión", "triageFollowups": "Triage and create follow-ups", "readTicketSummary": "Pull the linked Linear issue's full context before starting work.", "readTicketPrompt": "Use {{value0}} to read the linked Linear issue for this worktree, then summarize the goal and acceptance criteria before you start.", @@ -608,8 +608,8 @@ "postUpdatePrompt": "Use {{value0}} to post a completion update on the linked Linear issue with what changed and how it was verified.", "moveStateSummary": "Advance the Linear workflow state as the work progresses.", "moveStatePrompt": "Use {{value0}} to move the linked Linear issue to In Review now that the change is ready.", - "attachPrSummary": "Link the pull request to the Linear issue when you open it.", - "attachPrPrompt": "Use {{value0}} to attach this PR to the linked Linear issue with a PR link.", + "attachPrSummary": "Vincula la pull request o merge request con la incidencia de Linear al abrirla.", + "attachPrPrompt": "Usa {{value0}} para adjuntar esta pull request o merge request a la incidencia de Linear vinculada.", "triageFollowupsSummary": "Set assignee, priority, or estimate, and file parented follow-up tickets.", "triageFollowupsPrompt": "Use {{value0}} to triage the linked Linear issue — set priority and estimate — and create a parented follow-up ticket for the deferred cleanup." } @@ -760,10 +760,10 @@ "devSearchKeywordNotification": "notification", "projectHostsSummary": "{{value0}} hosts", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", + "linearDescription": "Cómo funciona Linear en Orca, lista de configuración, habilidad del agente y ejemplos de prompts.", "pluginsTitle": "Plugins", "pluginsDescription": "Instala y administra plugins experimentales de Orca.", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "tasksDescription": "Conecta proveedores, instala la habilidad de Linear y elige qué aparece en Tareas." }, "useAppMenuPaste": { "pasteTooLarge": "El contenido pegado es demasiado grande." @@ -5226,7 +5226,9 @@ "copyCommandAria": "Copiar comando", "runCommandDescription": "Presiona Enter para ejecutar el comando.", "5f818f12ab": "Preparando...", - "4c05b9d7cb": "Preparando el terminal de configuración." + "4c05b9d7cb": "Preparando el terminal de configuración.", + "installLabel": "Instalar", + "updateLabel": "Actualizar" }, "AgentsPane": { "d83834f5e6": "Detectando agents instalados…", @@ -6749,8 +6751,8 @@ "dev": "Herramientas de desarrollo", "devDescription": "Herramientas solo de desarrollo para probar estados de la UI.", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "linearDescription": "Cómo funciona Linear en Orca, lista de configuración, habilidad del agente y ejemplos de prompts.", + "tasksDescription": "Conecta proveedores, instala la habilidad de Linear y elige qué aparece en Tareas." }, "SettingsFormControls": { "42a4d15a30": "No hay fuentes coincidentes.", @@ -6979,38 +6981,34 @@ "53e9aabfc0": "Usa multiplexación de OpenSSH cuando está disponible. Desactívalo para hosts con restricciones SSH personalizadas." }, "TasksPane": { - "3a72b9745e": "Elige qué proveedores de tareas aparecen en la página Tareas y en los accesos directos de la barra lateral.", "f71d8a9dd3": "Proveedores de tareas", - "71644aba56": "Elige qué proveedores de tareas aparecen en el selector de fuentes de la página Tareas y en los accesos directos de la barra lateral. Al menos un proveedor debe permanecer visible.", - "93e72ef659": "Fuentes de tareas", - "8e1305fcc6": "Muestra Jira en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "6b23a34f6d": "Jira", - "e4170c9615": "Muestra Linear en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "09ae2d7c51": "Linear", - "dd67a1b6e1": "Muestra GitLab en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "7c5d7fdc20": "GitLab", - "1db47236cd": "Muestra GitHub en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "e14063e727": "GitHub", - "connectProviderTitle": "Connect {{provider}}", - "connectCodeHostDescription": "Install and authenticate the CLI under Integrations so Orca can load issues.", - "openIntegrations": "Integrations", - "connectInIntegrations": "Set up in Integrations", - "connectJiraTitle": "Connect Jira", - "connectJiraDescription": "Add a Jira Cloud site or self-hosted instance with an API token or PAT.", - "manageJira": "Manage access", - "addJira": "Add Jira access", - "githubDescription": "Browse GitHub issues and start workspaces from them.", - "gitlabDescription": "Browse GitLab issues and start workspaces from them.", - "linearDescription": "Connect Linear, install the agent skill, and show it in Tasks.", - "jiraDescription": "Connect Jira Cloud or self-hosted Jira and show it in Tasks.", - "setupTitle": "Task management setup", - "setupDescription": "Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets.", - "incompleteBannerTitle": "Some visible providers still need setup", - "incompleteBannerBody": "Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.", - "providersDescription": "Each card walks through connection (and skill, for Linear) plus whether it appears in Tasks.", - "integrationsHint": "Credentials for all providers also live under", - "integrationsLink": "Integrations", - "skillHint": ". After Linear is connected, usage examples stay under Settings → Linear." + "connectProviderTitle": "Conectar {{provider}}", + "connectCodeHostDescription": "Instala y autentica la CLI en Integraciones para que Orca pueda cargar incidencias.", + "connectionCheckUnavailable": "Orca no pudo comprobar esta conexión. Inténtalo de nuevo o abre Integraciones para ver los detalles de configuración.", + "retryConnection": "Intentar de nuevo", + "openIntegrations": "Integraciones", + "connectInIntegrations": "Configurar en Integraciones", + "connectJiraTitle": "Conectar Jira", + "connectJiraDescription": "Añade un sitio de Jira Cloud o una instancia autohospedada con un token de API o PAT.", + "manageJira": "Administrar claves", + "addJira": "Añadir acceso a Jira", + "githubDescription": "Explora incidencias de GitHub e inicia espacios de trabajo desde ellas.", + "gitlabDescription": "Explora incidencias de GitLab e inicia espacios de trabajo desde ellas.", + "linearDescription": "Conecta Linear, instala la habilidad del agente y muéstralo en Tareas.", + "jiraDescription": "Conecta Jira Cloud o Jira autohospedado y muéstralo en Tareas.", + "setupTitle": "Configuración de gestión de tareas", + "setupDescription": "Completa la conexión y la visibilidad de cada proveedor en un solo lugar. Linear también necesita la habilidad del agente para que los agentes puedan leer y actualizar incidencias. Al menos un proveedor debe permanecer visible.", + "incompleteBannerTitle": "Algunos proveedores visibles aún necesitan configuración", + "incompleteBannerBody": "Oculta los proveedores que no uses o expande una tarjeta y completa sus pasos.", + "providersDescription": "Cada tarjeta guía la conexión (y la habilidad, en Linear) y si aparece en Tareas.", + "integrationsHint": "Las credenciales de todos los proveedores también están en", + "integrationsLink": "Integraciones", + "skillHint": ". Después de conectar Linear, los ejemplos de uso están en Ajustes → Linear.", + "incompleteBannerBodyWithLinear": "Oculta los proveedores que no uses o expande una tarjeta y completa sus pasos. Para Linear: acceso a la API, la habilidad del agente y Mostrar en Tareas." }, "TerminalAppearanceSection": { "a14a427ae4": "Grosor de la línea divisoria del panel.", @@ -8649,9 +8647,8 @@ "3d81c26d78": "fuente", "cf0e3e0c2f": "proveedor", "2ec54bee51": "tareas", - "765f0c544d": "Elige qué proveedores de tareas aparecen en la página Tareas y en los accesos directos de la barra lateral.", "5b8e4aace5": "Proveedores de tareas", - "providersDescription": "Connect task providers, install the Linear agent skill, and choose what appears in Tasks.", + "providersDescription": "Conecta proveedores de tareas, instala la habilidad de agente de Linear y elige qué aparece en Tareas.", "setup": "configuración", "apiKey": "clave de API", "skill": "habilidad", @@ -9407,13 +9404,13 @@ "notInstalled": "No instalado", "recheck": "Reverificar", "installedDescription": "Habilidad del agente instalada. Para actualizarla, ejecuta:", - "description": "Permite que tus agentes lean y editen tareas de Linear. La configuración guiada completa (conexión + skill + visibilidad) está en Ajustes → Task Sources.", + "description": "Permite que los agentes lean y editen tareas de Linear. La configuración guiada completa (conexión + habilidad + visibilidad) está en Ajustes → Fuentes de tareas.", "copyCommand": "Copiar comando" } }, "search": { "title": "Linear", - "description": "Linear skill status, usage examples, and links to Task Sources setup." + "description": "Estado de la habilidad de Linear, ejemplos de uso y enlaces a la configuración de Fuentes de tareas." } } } @@ -9502,15 +9499,14 @@ }, "LinearAgentSkillPane": { "title": "Linear", - "description": "How Linear works in Orca: browse issues, start linked workspaces, and let agents update tickets with /orca-linear.", - "skillTitle": "Linear skill", - "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", - "howToUse": "Example prompts", - "howToUseDescription": "Click a card to copy a prompt. Use these in a Linear-linked worktree after the skill is installed.", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "skillSectionTitle": "Agent skill", - "skillSectionBody": "Install or update the skill agents need. You can also complete this step from Task Sources during first-time setup." + "description": "Cómo funciona Linear en Orca: explora incidencias, inicia espacios de trabajo vinculados y permite que los agentes actualicen incidencias con /orca-linear.", + "skillTitle": "Habilidad de Linear", + "howToUse": "Prompts de ejemplo", + "howToUseDescription": "Haz clic en una tarjeta para copiar un prompt. Úsalos en un espacio de trabajo vinculado a Linear después de instalar la habilidad.", + "terminalTitle": "Configuración de la habilidad de Linear", + "terminalAriaLabel": "Terminal de instalación de la habilidad de Linear", + "manageConnectionHint": "Revisa los espacios de trabajo de Linear conectados y las claves de API en", + "manageConnectionLink": "Integraciones" }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." @@ -9822,67 +9818,67 @@ } }, "LinearAgentSkillGuide": { - "setupConnectTitle": "1. Connect Linear", - "setupConnectBody": "Personal API key so Orca can list issues and open linked workspaces.", - "manageKeys": "Manage keys", - "addAccess": "Add access", - "setupSkillTitle": "2. Install the agent skill", - "setupSkillBody": "Gives coding agents /orca-linear for read, update, triage, and PR attach.", - "skillReady": "Ready below", - "installBelow": "Install below", - "setupVisibleTitle": "3. Show Linear in Tasks", - "setupVisibleBody": "Keeps Linear in the Tasks source picker and sidebar shortcuts.", - "openTaskSources": "Task Sources", - "setupTitle": "Setup checklist", - "setupBody": "All three are required for the full Tasks + agent loop. First-time path is also under Task Sources.", - "setupReady": "All set", - "setupProgress": "{{done}} of {{total}} ready", - "setupHint": "Prefer one guided place for connect + skill + visibility?", - "openFullSetup": "Open Task Sources setup", - "notesTitle": "Good to know", - "notesIntro": "Quick reminders once Linear is connected and the skill is installed.", - "noteLinkedTitle": "Start from a Linear issue", - "noteLinkedBody": "Ticket actions work best in a worktree created from Tasks so the issue stays linked as context.", - "noteSlashTitle": "Mention /orca-linear", - "noteSlashBody": "In chat, use /orca-linear (or ask in plain language) so the agent loads the skill for that turn.", - "noteKeysTitle": "Keys follow the runtime", - "noteKeysBody": "API keys and workspaces are stored for the active runtime. Manage them anytime under Integrations.", - "noteVisibilityTitle": "Hiding ≠ disconnect", - "noteVisibilityBody": "Hiding Linear in Task Sources only removes it from the picker. It does not remove your key or skill." + "setupConnectTitle": "1. Conectar Linear", + "setupConnectBody": "Una clave de API personal para que Orca pueda listar incidencias y abrir espacios de trabajo vinculados.", + "manageKeys": "Administrar claves", + "addAccess": "Añadir acceso", + "setupSkillTitle": "2. Instalar la habilidad del agente", + "setupSkillBody": "Proporciona /orca-linear a los agentes para leer, actualizar, clasificar y adjuntar solicitudes de cambio.", + "setupVisibleTitle": "3. Mostrar Linear en Tareas", + "setupVisibleBody": "Mantiene Linear en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", + "openTaskSources": "Fuentes de tareas", + "setupTitle": "Lista de configuración", + "setupBody": "Los tres pasos son necesarios para el flujo completo entre Tareas y agentes. La configuración inicial también está en Fuentes de tareas.", + "setupReady": "Todo listo", + "setupProgress": "{{done}} de {{total}} listos", + "notesTitle": "Conviene saberlo", + "notesIntro": "Recordatorios rápidos después de conectar Linear e instalar la habilidad.", + "noteLinkedTitle": "Comienza desde una incidencia de Linear", + "noteLinkedBody": "Las acciones de incidencias funcionan mejor en un espacio de trabajo creado desde Tareas, para mantener la incidencia vinculada como contexto.", + "noteSlashTitle": "Menciona /orca-linear", + "noteSlashBody": "En el chat, usa /orca-linear (o pide la acción en lenguaje natural) para que el agente cargue la habilidad durante ese turno.", + "noteKeysTitle": "Las claves siguen al entorno de ejecución", + "noteKeysBody": "Las claves de API y los espacios de trabajo se guardan para el entorno de ejecución activo.", + "noteVisibilityTitle": "Ocultar no desconecta", + "noteVisibilityBody": "Ocultar Linear en Fuentes de tareas solo lo quita del selector. No elimina la clave ni la habilidad.", + "setupChecking": "Comprobando…" }, "TaskSourceLinearSetup": { - "connectTitle": "Connect Linear", - "connectDescription": "Add a Personal API key so Orca can browse issues and open workspaces with ticket context.", - "manageAccess": "Manage access", - "addAccess": "Add Linear access", - "connectedHint": "Workspaces and keys are stored for the active runtime. You can add more access any time.", - "recheck": "Re-check connection", - "skillTitle": "Install Linear agent skill", - "skillDescription": "Gives agents the /orca-linear skill to read tickets, post updates, move states, and attach PRs.", - "skillBlocked": "Connect Linear first, then install the skill for agents.", - "skillPanelTitle": "Linear skill", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "showDescription": "Include Linear in the Tasks page source picker and sidebar shortcuts." + "connectTitle": "Conectar Linear", + "connectDescription": "Añade una clave de API personal para que Orca pueda explorar incidencias y abrir espacios de trabajo con su contexto.", + "manageAccess": "Administrar claves", + "addAccess": "Añadir acceso a Linear", + "connectedHint": "Los espacios de trabajo y las claves se guardan para el entorno de ejecución activo. Puedes añadir más accesos en cualquier momento.", + "recheck": "Volver a comprobar la conexión", + "skillTitle": "Instalar la habilidad de agente de Linear", + "skillDescription": "Proporciona /orca-linear a los agentes para leer incidencias, publicar actualizaciones, cambiar estados y adjuntar solicitudes de cambio.", + "skillBlocked": "Conecta Linear primero y luego instala la habilidad para los agentes.", + "skillPanelTitle": "Habilidad de Linear", + "terminalTitle": "Configuración de la habilidad de Linear", + "terminalAriaLabel": "Terminal de instalación de la habilidad de Linear", + "showDescription": "Incluye Linear en el selector de fuentes de Tareas y en los accesos directos de la barra lateral." }, "TaskSourceProviderCard": { - "statusChecking": "Checking…", - "statusReady": "Ready", - "statusConnectRequired": "Connect required", - "statusSkillRequired": "Skill required", - "statusHidden": "Hidden from Tasks", - "statusIncomplete": "Needs setup", + "statusChecking": "Comprobando…", + "statusReady": "Listo", + "statusConnectRequired": "Requiere conexión", + "statusSkillRequired": "Requiere la habilidad", + "statusUnavailable": "Estado no disponible", + "statusHidden": "Oculto en Tareas", + "statusIncomplete": "Necesita configuración", "collapseSetup": "Contraer los pasos de configuración de {{provider}}", "expandSetup": "Mostrar los pasos de configuración de {{provider}}" }, "TaskSourceShowInTasksStep": { - "shown": "Shown", - "show": "Show", + "shown": "Visible", + "show": "Mostrar", + "hide": "Ocultar", "hideProviderAction": "Ocultar {{provider}} de Tareas", "showProviderAction": "Mostrar {{provider}} en Tareas", - "lastProviderHint": "At least one provider must stay visible in Tasks.", - "title": "Show in Tasks", - "description": "Include this provider in the Tasks source picker and sidebar shortcuts." + "lastProviderAction": "{{provider}} se muestra en Tareas. Al menos un proveedor debe permanecer visible.", + "lastProviderHint": "Al menos un proveedor debe permanecer visible en Tareas.", + "title": "Mostrar en Tareas", + "description": "Incluye este proveedor en el selector de fuentes de Tareas y en los accesos directos de la barra lateral." } }, "right": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index a10403cafa4..dcf501e7754 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -600,7 +600,7 @@ "readTicket": "Read the linked ticket", "postUpdate": "Post a progress update", "moveState": "Move the ticket forward", - "attachPr": "Attach the PR link", + "attachPr": "レビューリンクを添付", "triageFollowups": "Triage and create follow-ups", "readTicketSummary": "Pull the linked Linear issue's full context before starting work.", "readTicketPrompt": "Use {{value0}} to read the linked Linear issue for this worktree, then summarize the goal and acceptance criteria before you start.", @@ -608,8 +608,8 @@ "postUpdatePrompt": "Use {{value0}} to post a completion update on the linked Linear issue with what changed and how it was verified.", "moveStateSummary": "Advance the Linear workflow state as the work progresses.", "moveStatePrompt": "Use {{value0}} to move the linked Linear issue to In Review now that the change is ready.", - "attachPrSummary": "Link the pull request to the Linear issue when you open it.", - "attachPrPrompt": "Use {{value0}} to attach this PR to the linked Linear issue with a PR link.", + "attachPrSummary": "プルリクエストまたはマージリクエストを作成したら Linear の課題にリンクします。", + "attachPrPrompt": "{{value0}} を使って、このプルリクエストまたはマージリクエストをリンク済みの Linear 課題に添付してください。", "triageFollowupsSummary": "Set assignee, priority, or estimate, and file parented follow-up tickets.", "triageFollowupsPrompt": "Use {{value0}} to triage the linked Linear issue — set priority and estimate — and create a parented follow-up ticket for the deferred cleanup." } @@ -760,10 +760,10 @@ "devSearchKeywordNotification": "notification", "projectHostsSummary": "{{value0}} 台のホスト", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", + "linearDescription": "Orca での Linear の仕組み、セットアップチェックリスト、エージェントスキル、プロンプト例。", "pluginsTitle": "プラグイン", "pluginsDescription": "試験的な Orca プラグインをインストールして管理します。", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "tasksDescription": "プロバイダーを接続し、Linear スキルをインストールして、タスクに表示する項目を選択します。" }, "useAppMenuPaste": { "pasteTooLarge": "貼り付け内容が大きすぎます。" @@ -5211,7 +5211,9 @@ "copyCommandAria": "コマンドをコピー", "runCommandDescription": "Enter を押してコマンドを実行します。", "5f818f12ab": "準備中...", - "4c05b9d7cb": "セットアップ用ターミナルを準備しています。" + "4c05b9d7cb": "セットアップ用ターミナルを準備しています。", + "installLabel": "インストール", + "updateLabel": "更新" }, "AgentsPane": { "d83834f5e6": "インストールされている agents を検出しています…", @@ -6771,8 +6773,8 @@ "dev": "Dev Tools", "devDescription": "Dev-only tools for exercising UI states.", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "linearDescription": "Orca での Linear の仕組み、セットアップチェックリスト、エージェントスキル、プロンプト例。", + "tasksDescription": "プロバイダーを接続し、Linear スキルをインストールして、タスクに表示する項目を選択します。" }, "SettingsFormControls": { "42a4d15a30": "一致するフォントがありません。", @@ -7001,38 +7003,34 @@ "53e9aabfc0": "利用可能な場合は OpenSSH の多重化を使用します。独自の SSH 制限があるホストではオフにしてください。" }, "TasksPane": { - "3a72b9745e": "[タスク] ページとサイドバーのショートカットに表示するタスク プロバイダーを選択します。", "f71d8a9dd3": "タスクプロバイダー", - "71644aba56": "[タスク] ページのソース ピッカーとサイドバーのショートカットに表示するタスク プロバイダーを選択します。少なくとも 1 つのプロバイダーが表示されたままでなければなりません。", - "93e72ef659": "タスクソース", - "8e1305fcc6": "タスクのソース ピッカーとサイドバーのショートカットに Jira を表示します。", "6b23a34f6d": "Jira", - "e4170c9615": "タスクのソース ピッカーとサイドバーのショートカットに Linear を表示します。", "09ae2d7c51": "Linear", - "dd67a1b6e1": "タスクのソース ピッカーとサイドバーのショートカットに GitLab を表示します。", "7c5d7fdc20": "GitLab", - "1db47236cd": "タスクのソース ピッカーとサイドバーのショートカットに GitHub を表示します。", "e14063e727": "GitHub", - "connectProviderTitle": "Connect {{provider}}", - "connectCodeHostDescription": "Install and authenticate the CLI under Integrations so Orca can load issues.", - "openIntegrations": "Integrations", - "connectInIntegrations": "Set up in Integrations", - "connectJiraTitle": "Connect Jira", - "connectJiraDescription": "Add a Jira Cloud site or self-hosted instance with an API token or PAT.", - "manageJira": "Manage access", - "addJira": "Add Jira access", - "githubDescription": "Browse GitHub issues and start workspaces from them.", - "gitlabDescription": "Browse GitLab issues and start workspaces from them.", - "linearDescription": "Connect Linear, install the agent skill, and show it in Tasks.", - "jiraDescription": "Connect Jira Cloud or self-hosted Jira and show it in Tasks.", - "setupTitle": "Task management setup", - "setupDescription": "Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets.", - "incompleteBannerTitle": "Some visible providers still need setup", - "incompleteBannerBody": "Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.", - "providersDescription": "Each card walks through connection (and skill, for Linear) plus whether it appears in Tasks.", - "integrationsHint": "Credentials for all providers also live under", - "integrationsLink": "Integrations", - "skillHint": ". After Linear is connected, usage examples stay under Settings → Linear." + "connectProviderTitle": "{{provider}} に接続", + "connectCodeHostDescription": "Orca が課題を読み込めるように、連携で CLI をインストールして認証します。", + "connectionCheckUnavailable": "Orca はこの接続を確認できませんでした。もう一度試すか、連携を開いてセットアップの詳細を確認してください。", + "retryConnection": "もう一度試す", + "openIntegrations": "連携", + "connectInIntegrations": "連携で設定", + "connectJiraTitle": "Jira に接続", + "connectJiraDescription": "API トークンまたは PAT を使って Jira Cloud サイトまたはセルフホスト環境を追加します。", + "manageJira": "キーを管理", + "addJira": "Jira アクセスを追加", + "githubDescription": "GitHub の Issue を参照し、そこからワークスペースを開始します。", + "gitlabDescription": "GitLab の Issue を参照し、そこからワークスペースを開始します。", + "linearDescription": "Linear に接続し、エージェントスキルをインストールして、タスクに表示します。", + "jiraDescription": "Jira Cloud またはセルフホスト版 Jira に接続し、タスクに表示します。", + "setupTitle": "タスク管理のセットアップ", + "setupDescription": "各プロバイダーの接続と表示設定を 1 か所で完了できます。Linear では、エージェントが課題を読み書きするためのスキルも必要です。少なくとも 1 つのプロバイダーを表示する必要があります。", + "incompleteBannerTitle": "表示中の一部のプロバイダーは設定が必要です", + "incompleteBannerBody": "使用しないプロバイダーを非表示にするか、カードを展開して手順を完了してください。", + "providersDescription": "各カードで接続、タスクへの表示、Linear の場合はスキル設定を進められます。", + "integrationsHint": "すべてのプロバイダーの認証情報は次の場所でも管理できます:", + "integrationsLink": "連携", + "skillHint": "。Linear の接続後、使用例は設定 → Linear で確認できます。", + "incompleteBannerBodyWithLinear": "使用しないプロバイダーを非表示にするか、カードを展開して手順を完了してください。Linear では API アクセス、エージェントスキル、タスクへの表示が必要です。" }, "TerminalAppearanceSection": { "a14a427ae4": "ペイン分割線の太さ。", @@ -8671,9 +8669,8 @@ "3d81c26d78": "ソース", "cf0e3e0c2f": "プロバイダー", "2ec54bee51": "タスク", - "765f0c544d": "[タスク] ページとサイドバーのショートカットに表示するタスク プロバイダーを選択します。", "5b8e4aace5": "タスクプロバイダー", - "providersDescription": "Connect task providers, install the Linear agent skill, and choose what appears in Tasks.", + "providersDescription": "タスクプロバイダーを接続し、Linear エージェントスキルをインストールして、タスクに表示する項目を選択します。", "setup": "セットアップ", "apiKey": "APIキー", "skill": "スキル", @@ -9407,13 +9404,13 @@ "notInstalled": "未インストール", "recheck": "再確認", "installedDescription": "エージェントスキルがインストールされました。更新するには以下を実行してください:", - "description": "エージェントにLinearタスクの読み取りと編集を許可します。接続・スキル・表示のガイド付きセットアップは 設定 → Task Sources にあります。", + "description": "エージェントが Linear の課題を読み書きできるようにします。接続、スキル、表示設定を含むガイドは、設定 → タスクソースにあります。", "copyCommand": "コマンドをコピー" } }, "search": { "title": "Linear", - "description": "Linear skill status, usage examples, and links to Task Sources setup." + "description": "Linear スキルの状態、使用例、タスクソース設定へのリンク。" } } } @@ -9502,15 +9499,14 @@ }, "LinearAgentSkillPane": { "title": "Linear", - "description": "How Linear works in Orca: browse issues, start linked workspaces, and let agents update tickets with /orca-linear.", - "skillTitle": "Linear skill", - "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", - "howToUse": "Example prompts", - "howToUseDescription": "Click a card to copy a prompt. Use these in a Linear-linked worktree after the skill is installed.", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "skillSectionTitle": "Agent skill", - "skillSectionBody": "Install or update the skill agents need. You can also complete this step from Task Sources during first-time setup." + "description": "Orca で Linear の課題を参照し、リンク済みワークスペースを開始して、/orca-linear でエージェントから更新できます。", + "skillTitle": "Linear スキル", + "howToUse": "プロンプト例", + "howToUseDescription": "カードをクリックするとプロンプトをコピーできます。スキルのインストール後、Linear にリンクしたワークスペースで使用してください。", + "terminalTitle": "Linear スキルのセットアップ", + "terminalAriaLabel": "Linear スキルのインストールターミナル", + "manageConnectionHint": "接続済みの Linear ワークスペースと API キーは次の場所で確認できます:", + "manageConnectionLink": "連携" }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." @@ -9822,67 +9818,67 @@ } }, "LinearAgentSkillGuide": { - "setupConnectTitle": "1. Connect Linear", - "setupConnectBody": "Personal API key so Orca can list issues and open linked workspaces.", - "manageKeys": "Manage keys", - "addAccess": "Add access", - "setupSkillTitle": "2. Install the agent skill", - "setupSkillBody": "Gives coding agents /orca-linear for read, update, triage, and PR attach.", - "skillReady": "Ready below", - "installBelow": "Install below", - "setupVisibleTitle": "3. Show Linear in Tasks", - "setupVisibleBody": "Keeps Linear in the Tasks source picker and sidebar shortcuts.", - "openTaskSources": "Task Sources", - "setupTitle": "Setup checklist", - "setupBody": "All three are required for the full Tasks + agent loop. First-time path is also under Task Sources.", - "setupReady": "All set", - "setupProgress": "{{done}} of {{total}} ready", - "setupHint": "Prefer one guided place for connect + skill + visibility?", - "openFullSetup": "Open Task Sources setup", - "notesTitle": "Good to know", - "notesIntro": "Quick reminders once Linear is connected and the skill is installed.", - "noteLinkedTitle": "Start from a Linear issue", - "noteLinkedBody": "Ticket actions work best in a worktree created from Tasks so the issue stays linked as context.", - "noteSlashTitle": "Mention /orca-linear", - "noteSlashBody": "In chat, use /orca-linear (or ask in plain language) so the agent loads the skill for that turn.", - "noteKeysTitle": "Keys follow the runtime", - "noteKeysBody": "API keys and workspaces are stored for the active runtime. Manage them anytime under Integrations.", - "noteVisibilityTitle": "Hiding ≠ disconnect", - "noteVisibilityBody": "Hiding Linear in Task Sources only removes it from the picker. It does not remove your key or skill." + "setupConnectTitle": "1. Linear に接続", + "setupConnectBody": "Orca が課題を一覧表示し、リンク済みワークスペースを開くための個人 API キー。", + "manageKeys": "キーを管理", + "addAccess": "アクセスを追加", + "setupSkillTitle": "2. エージェントスキルをインストール", + "setupSkillBody": "エージェントが /orca-linear で課題の参照、更新、トリアージ、プル/マージリクエストの添付を行えるようにします。", + "setupVisibleTitle": "3. タスクに Linear を表示", + "setupVisibleBody": "Linear をタスクのソース選択とサイドバーのショートカットに表示します。", + "openTaskSources": "タスクソース", + "setupTitle": "セットアップチェックリスト", + "setupBody": "タスクとエージェントの一連の流れには 3 つの手順が必要です。初回設定はタスクソースでも行えます。", + "setupReady": "設定完了", + "setupProgress": "{{total}} 件中 {{done}} 件が完了", + "notesTitle": "知っておくと便利", + "notesIntro": "Linear の接続とスキルのインストール後に役立つポイントです。", + "noteLinkedTitle": "Linear の課題から開始", + "noteLinkedBody": "タスクから作成したワークスペースでは課題がコンテキストとしてリンクされるため、課題操作が最もスムーズです。", + "noteSlashTitle": "/orca-linear を指定", + "noteSlashBody": "チャットで /orca-linear を使うか自然言語で依頼すると、そのターンでエージェントがスキルを読み込みます。", + "noteKeysTitle": "キーは実行環境ごとに保存", + "noteKeysBody": "API キーとワークスペースはアクティブな実行環境に保存されます。", + "noteVisibilityTitle": "非表示にしても接続は維持", + "noteVisibilityBody": "タスクソースで Linear を非表示にしても、選択肢から外れるだけで、キーやスキルは削除されません。", + "setupChecking": "確認中…" }, "TaskSourceLinearSetup": { - "connectTitle": "Connect Linear", - "connectDescription": "Add a Personal API key so Orca can browse issues and open workspaces with ticket context.", - "manageAccess": "Manage access", - "addAccess": "Add Linear access", - "connectedHint": "Workspaces and keys are stored for the active runtime. You can add more access any time.", - "recheck": "Re-check connection", - "skillTitle": "Install Linear agent skill", - "skillDescription": "Gives agents the /orca-linear skill to read tickets, post updates, move states, and attach PRs.", - "skillBlocked": "Connect Linear first, then install the skill for agents.", - "skillPanelTitle": "Linear skill", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "showDescription": "Include Linear in the Tasks page source picker and sidebar shortcuts." + "connectTitle": "Linear に接続", + "connectDescription": "Orca が課題を参照し、そのコンテキストでワークスペースを開けるように個人 API キーを追加します。", + "manageAccess": "キーを管理", + "addAccess": "Linear アクセスを追加", + "connectedHint": "ワークスペースとキーはアクティブな実行環境に保存されます。アクセスはいつでも追加できます。", + "recheck": "接続を再確認", + "skillTitle": "Linear エージェントスキルをインストール", + "skillDescription": "エージェントが /orca-linear で課題の参照、更新の投稿、状態変更、プル/マージリクエストの添付を行えるようにします。", + "skillBlocked": "先に Linear に接続してから、エージェント用スキルをインストールしてください。", + "skillPanelTitle": "Linear スキル", + "terminalTitle": "Linear スキルのセットアップ", + "terminalAriaLabel": "Linear スキルのインストールターミナル", + "showDescription": "Linear をタスクのソース選択とサイドバーのショートカットに表示します。" }, "TaskSourceProviderCard": { - "statusChecking": "Checking…", - "statusReady": "Ready", - "statusConnectRequired": "Connect required", - "statusSkillRequired": "Skill required", - "statusHidden": "Hidden from Tasks", - "statusIncomplete": "Needs setup", + "statusChecking": "確認中…", + "statusReady": "準備完了", + "statusConnectRequired": "接続が必要", + "statusSkillRequired": "スキルが必要", + "statusUnavailable": "ステータスを確認できません", + "statusHidden": "タスクで非表示", + "statusIncomplete": "設定が必要", "collapseSetup": "{{provider}} のセットアップ手順を折りたたむ", "expandSetup": "{{provider}} のセットアップ手順を表示" }, "TaskSourceShowInTasksStep": { - "shown": "Shown", - "show": "Show", + "shown": "表示中", + "show": "表示", + "hide": "非表示", "hideProviderAction": "{{provider}} をタスクから非表示", "showProviderAction": "{{provider}} をタスクに表示", - "lastProviderHint": "At least one provider must stay visible in Tasks.", - "title": "Show in Tasks", - "description": "Include this provider in the Tasks source picker and sidebar shortcuts." + "lastProviderAction": "{{provider}} はタスクに表示されています。少なくとも 1 つのプロバイダーを表示する必要があります。", + "lastProviderHint": "タスクには少なくとも 1 つのプロバイダーを表示する必要があります。", + "title": "タスクに表示", + "description": "このプロバイダーをタスクのソース選択とサイドバーのショートカットに表示します。" } }, "right": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index bc45ac21af3..47f9c072210 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -600,7 +600,7 @@ "readTicket": "Read the linked ticket", "postUpdate": "Post a progress update", "moveState": "Move the ticket forward", - "attachPr": "Attach the PR link", + "attachPr": "리뷰 링크 첨부", "triageFollowups": "Triage and create follow-ups", "readTicketSummary": "Pull the linked Linear issue's full context before starting work.", "readTicketPrompt": "Use {{value0}} to read the linked Linear issue for this worktree, then summarize the goal and acceptance criteria before you start.", @@ -608,8 +608,8 @@ "postUpdatePrompt": "Use {{value0}} to post a completion update on the linked Linear issue with what changed and how it was verified.", "moveStateSummary": "Advance the Linear workflow state as the work progresses.", "moveStatePrompt": "Use {{value0}} to move the linked Linear issue to In Review now that the change is ready.", - "attachPrSummary": "Link the pull request to the Linear issue when you open it.", - "attachPrPrompt": "Use {{value0}} to attach this PR to the linked Linear issue with a PR link.", + "attachPrSummary": "풀 또는 머지 리퀘스트를 만들 때 Linear 이슈에 연결하세요.", + "attachPrPrompt": "{{value0}}을 사용해 이 풀 또는 머지 리퀘스트를 연결된 Linear 이슈에 첨부하세요.", "triageFollowupsSummary": "Set assignee, priority, or estimate, and file parented follow-up tickets.", "triageFollowupsPrompt": "Use {{value0}} to triage the linked Linear issue — set priority and estimate — and create a parented follow-up ticket for the deferred cleanup." } @@ -760,10 +760,10 @@ "devSearchKeywordNotification": "notification", "projectHostsSummary": "호스트 {{value0}}개", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", + "linearDescription": "Orca에서 Linear가 작동하는 방식, 설정 체크리스트, 에이전트 스킬, 프롬프트 예시입니다.", "pluginsTitle": "플러그인", "pluginsDescription": "실험적인 Orca 플러그인을 설치하고 관리합니다.", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "tasksDescription": "제공업체를 연결하고 Linear 스킬을 설치한 다음 작업에 표시할 항목을 선택하세요." }, "useAppMenuPaste": { "pasteTooLarge": "붙여넣기 내용이 너무 큽니다." @@ -5211,7 +5211,9 @@ "copyCommandAria": "명령 복사", "runCommandDescription": "Enter를 눌러 명령을 실행하세요.", "5f818f12ab": "준비 중...", - "4c05b9d7cb": "설정 터미널을 준비하고 있습니다." + "4c05b9d7cb": "설정 터미널을 준비하고 있습니다.", + "installLabel": "설치", + "updateLabel": "업데이트" }, "AgentsPane": { "d83834f5e6": "설치된 agents 감지 중…", @@ -6734,8 +6736,8 @@ "dev": "Dev Tools", "devDescription": "UI 상태를 시험해 보기 위한 개발 전용 도구입니다.", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "linearDescription": "Orca에서 Linear가 작동하는 방식, 설정 체크리스트, 에이전트 스킬, 프롬프트 예시입니다.", + "tasksDescription": "제공업체를 연결하고 Linear 스킬을 설치한 다음 작업에 표시할 항목을 선택하세요." }, "SettingsFormControls": { "42a4d15a30": "일치하는 글꼴이 없습니다.", @@ -6964,38 +6966,34 @@ "53e9aabfc0": "사용 가능한 경우 OpenSSH 멀티플렉싱을 사용합니다. 사용자 지정 SSH 제한이 있는 호스트에서는 끄세요." }, "TasksPane": { - "3a72b9745e": "작업 페이지와 사이드바 바로가기에 표시할 작업 제공자를 선택합니다.", "f71d8a9dd3": "작업 제공자", - "71644aba56": "작업 페이지 소스 선택기 및 사이드바 바로가기에 표시할 작업 제공자를 선택합니다. 하나 이상의 제공업체가 계속 표시되어야 합니다.", - "93e72ef659": "작업 소스", - "8e1305fcc6": "작업 소스 선택기 및 사이드바 바로가기에 Jira를 표시합니다.", "6b23a34f6d": "Jira", - "e4170c9615": "작업 소스 선택기 및 사이드바 바로가기에 Linear를 표시합니다.", "09ae2d7c51": "Linear", - "dd67a1b6e1": "작업 소스 선택기 및 사이드바 바로가기에 GitLab을 표시합니다.", "7c5d7fdc20": "GitLab", - "1db47236cd": "작업 소스 선택기 및 사이드바 바로가기에 GitHub를 표시합니다.", "e14063e727": "GitHub", - "connectProviderTitle": "Connect {{provider}}", - "connectCodeHostDescription": "Install and authenticate the CLI under Integrations so Orca can load issues.", - "openIntegrations": "Integrations", - "connectInIntegrations": "Set up in Integrations", - "connectJiraTitle": "Connect Jira", - "connectJiraDescription": "Add a Jira Cloud site or self-hosted instance with an API token or PAT.", - "manageJira": "Manage access", - "addJira": "Add Jira access", - "githubDescription": "Browse GitHub issues and start workspaces from them.", - "gitlabDescription": "Browse GitLab issues and start workspaces from them.", - "linearDescription": "Connect Linear, install the agent skill, and show it in Tasks.", - "jiraDescription": "Connect Jira Cloud or self-hosted Jira and show it in Tasks.", - "setupTitle": "Task management setup", - "setupDescription": "Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets.", - "incompleteBannerTitle": "Some visible providers still need setup", - "incompleteBannerBody": "Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.", - "providersDescription": "Each card walks through connection (and skill, for Linear) plus whether it appears in Tasks.", - "integrationsHint": "Credentials for all providers also live under", - "integrationsLink": "Integrations", - "skillHint": ". After Linear is connected, usage examples stay under Settings → Linear." + "connectProviderTitle": "{{provider}} 연결", + "connectCodeHostDescription": "Orca가 이슈를 불러올 수 있도록 연동에서 CLI를 설치하고 인증하세요.", + "connectionCheckUnavailable": "Orca가 이 연결을 확인하지 못했습니다. 다시 시도하거나 연동을 열어 설정 세부 정보를 확인하세요.", + "retryConnection": "다시 시도", + "openIntegrations": "연동", + "connectInIntegrations": "연동에서 설정", + "connectJiraTitle": "Jira 연결", + "connectJiraDescription": "API 토큰 또는 PAT로 Jira Cloud 사이트나 자체 호스팅 인스턴스를 추가하세요.", + "manageJira": "키 관리", + "addJira": "Jira 액세스 추가", + "githubDescription": "GitHub 이슈를 탐색하고 해당 이슈에서 워크스페이스를 시작하세요.", + "gitlabDescription": "GitLab 이슈를 탐색하고 해당 이슈에서 워크스페이스를 시작하세요.", + "linearDescription": "Linear를 연결하고 에이전트 스킬을 설치한 다음 작업에 표시하세요.", + "jiraDescription": "Jira Cloud 또는 자체 호스팅 Jira를 연결하고 작업에 표시하세요.", + "setupTitle": "작업 관리 설정", + "setupDescription": "각 제공업체의 연결과 표시 설정을 한곳에서 완료하세요. Linear에서는 에이전트가 이슈를 읽고 업데이트할 수 있도록 에이전트 스킬도 필요합니다. 제공업체는 하나 이상 표시되어야 합니다.", + "incompleteBannerTitle": "표시 중인 일부 제공업체에 설정이 필요합니다", + "incompleteBannerBody": "사용하지 않는 제공업체를 숨기거나 카드를 펼쳐 단계를 완료하세요.", + "providersDescription": "각 카드에서 연결, 작업 표시 여부, Linear의 경우 스킬 설정까지 진행할 수 있습니다.", + "integrationsHint": "모든 제공업체의 자격 증명은 다음에서도 관리할 수 있습니다.", + "integrationsLink": "연동", + "skillHint": ". Linear를 연결한 후 사용 예시는 설정 → Linear에서 확인할 수 있습니다.", + "incompleteBannerBodyWithLinear": "사용하지 않는 제공업체를 숨기거나 카드를 펼쳐 단계를 완료하세요. Linear에는 API 액세스, 에이전트 스킬, 작업에 표시가 필요합니다." }, "TerminalAppearanceSection": { "a14a427ae4": "창 구분선의 두께입니다.", @@ -8634,9 +8632,8 @@ "3d81c26d78": "소스", "cf0e3e0c2f": "공급자", "2ec54bee51": "작업", - "765f0c544d": "작업 페이지와 사이드바 바로가기에 표시할 작업 제공자를 선택합니다.", "5b8e4aace5": "작업 제공자", - "providersDescription": "Connect task providers, install the Linear agent skill, and choose what appears in Tasks.", + "providersDescription": "작업 제공업체를 연결하고 Linear 에이전트 스킬을 설치한 다음 작업에 표시할 항목을 선택하세요.", "setup": "설정", "apiKey": "API 키", "skill": "스킬", @@ -9407,13 +9404,13 @@ "notInstalled": "설치되지 않음", "recheck": "다시 확인", "installedDescription": "에이전트 스킬이 설치되었습니다. 업데이트하려면 다음을 실행하세요:", - "description": "에이전트가 Linear 작업을 읽고 편집할 수 있도록 합니다. 전체 가이드 설정(연결 + 스킬 + 표시)은 설정 → Task Sources에 있습니다.", + "description": "에이전트가 Linear 작업을 읽고 편집할 수 있도록 합니다. 전체 가이드 설정(연결 + 스킬 + 표시)은 설정 → 작업 소스에 있습니다.", "copyCommand": "명령 복사" } }, "search": { "title": "Linear", - "description": "Linear skill status, usage examples, and links to Task Sources setup." + "description": "Linear 스킬 상태, 사용 예시, 작업 소스 설정 링크입니다." } } } @@ -9502,15 +9499,14 @@ }, "LinearAgentSkillPane": { "title": "Linear", - "description": "How Linear works in Orca: browse issues, start linked workspaces, and let agents update tickets with /orca-linear.", - "skillTitle": "Linear skill", - "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", - "howToUse": "Example prompts", - "howToUseDescription": "Click a card to copy a prompt. Use these in a Linear-linked worktree after the skill is installed.", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "skillSectionTitle": "Agent skill", - "skillSectionBody": "Install or update the skill agents need. You can also complete this step from Task Sources during first-time setup." + "description": "Orca에서 Linear 이슈를 탐색하고 연결된 워크스페이스를 시작하며 /orca-linear로 에이전트가 이슈를 업데이트할 수 있습니다.", + "skillTitle": "Linear 스킬", + "howToUse": "프롬프트 예시", + "howToUseDescription": "카드를 클릭해 프롬프트를 복사하세요. 스킬을 설치한 후 Linear에 연결된 워크스페이스에서 사용하세요.", + "terminalTitle": "Linear 스킬 설정", + "terminalAriaLabel": "Linear 스킬 설치 터미널", + "manageConnectionHint": "연결된 Linear 워크스페이스와 API 키는 다음에서 확인할 수 있습니다.", + "manageConnectionLink": "연동" }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." @@ -9822,67 +9818,67 @@ } }, "LinearAgentSkillGuide": { - "setupConnectTitle": "1. Connect Linear", - "setupConnectBody": "Personal API key so Orca can list issues and open linked workspaces.", - "manageKeys": "Manage keys", - "addAccess": "Add access", - "setupSkillTitle": "2. Install the agent skill", - "setupSkillBody": "Gives coding agents /orca-linear for read, update, triage, and PR attach.", - "skillReady": "Ready below", - "installBelow": "Install below", - "setupVisibleTitle": "3. Show Linear in Tasks", - "setupVisibleBody": "Keeps Linear in the Tasks source picker and sidebar shortcuts.", - "openTaskSources": "Task Sources", - "setupTitle": "Setup checklist", - "setupBody": "All three are required for the full Tasks + agent loop. First-time path is also under Task Sources.", - "setupReady": "All set", - "setupProgress": "{{done}} of {{total}} ready", - "setupHint": "Prefer one guided place for connect + skill + visibility?", - "openFullSetup": "Open Task Sources setup", - "notesTitle": "Good to know", - "notesIntro": "Quick reminders once Linear is connected and the skill is installed.", - "noteLinkedTitle": "Start from a Linear issue", - "noteLinkedBody": "Ticket actions work best in a worktree created from Tasks so the issue stays linked as context.", - "noteSlashTitle": "Mention /orca-linear", - "noteSlashBody": "In chat, use /orca-linear (or ask in plain language) so the agent loads the skill for that turn.", - "noteKeysTitle": "Keys follow the runtime", - "noteKeysBody": "API keys and workspaces are stored for the active runtime. Manage them anytime under Integrations.", - "noteVisibilityTitle": "Hiding ≠ disconnect", - "noteVisibilityBody": "Hiding Linear in Task Sources only removes it from the picker. It does not remove your key or skill." + "setupConnectTitle": "1. Linear 연결", + "setupConnectBody": "Orca가 이슈를 나열하고 연결된 워크스페이스를 열 수 있도록 하는 개인 API 키입니다.", + "manageKeys": "키 관리", + "addAccess": "액세스 추가", + "setupSkillTitle": "2. 에이전트 스킬 설치", + "setupSkillBody": "에이전트가 /orca-linear로 이슈 읽기, 업데이트, 분류, 풀 또는 머지 리퀘스트 첨부를 할 수 있게 합니다.", + "setupVisibleTitle": "3. 작업에 Linear 표시", + "setupVisibleBody": "작업 소스 선택기와 사이드바 바로 가기에 Linear를 표시합니다.", + "openTaskSources": "작업 소스", + "setupTitle": "설정 체크리스트", + "setupBody": "작업과 에이전트의 전체 흐름에는 세 단계가 모두 필요합니다. 최초 설정은 작업 소스에서도 진행할 수 있습니다.", + "setupReady": "설정 완료", + "setupProgress": "{{total}}개 중 {{done}}개 완료", + "notesTitle": "알아두면 좋은 점", + "notesIntro": "Linear를 연결하고 스킬을 설치한 후 참고할 내용입니다.", + "noteLinkedTitle": "Linear 이슈에서 시작", + "noteLinkedBody": "작업에서 만든 워크스페이스는 이슈가 컨텍스트로 연결되어 있어 이슈 작업이 가장 원활합니다.", + "noteSlashTitle": "/orca-linear 언급", + "noteSlashBody": "채팅에서 /orca-linear를 사용하거나 자연어로 요청하면 해당 턴에 에이전트가 스킬을 불러옵니다.", + "noteKeysTitle": "키는 런타임별로 저장", + "noteKeysBody": "API 키와 워크스페이스는 활성 런타임에 저장됩니다.", + "noteVisibilityTitle": "숨겨도 연결은 유지", + "noteVisibilityBody": "작업 소스에서 Linear를 숨기면 선택기에서만 제거됩니다. 키나 스킬은 삭제되지 않습니다.", + "setupChecking": "확인 중…" }, "TaskSourceLinearSetup": { - "connectTitle": "Connect Linear", - "connectDescription": "Add a Personal API key so Orca can browse issues and open workspaces with ticket context.", - "manageAccess": "Manage access", - "addAccess": "Add Linear access", - "connectedHint": "Workspaces and keys are stored for the active runtime. You can add more access any time.", - "recheck": "Re-check connection", - "skillTitle": "Install Linear agent skill", - "skillDescription": "Gives agents the /orca-linear skill to read tickets, post updates, move states, and attach PRs.", - "skillBlocked": "Connect Linear first, then install the skill for agents.", - "skillPanelTitle": "Linear skill", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "showDescription": "Include Linear in the Tasks page source picker and sidebar shortcuts." + "connectTitle": "Linear 연결", + "connectDescription": "Orca가 이슈를 탐색하고 이슈 컨텍스트로 워크스페이스를 열 수 있도록 개인 API 키를 추가하세요.", + "manageAccess": "키 관리", + "addAccess": "Linear 액세스 추가", + "connectedHint": "워크스페이스와 키는 활성 런타임에 저장됩니다. 언제든 액세스를 더 추가할 수 있습니다.", + "recheck": "연결 다시 확인", + "skillTitle": "Linear 에이전트 스킬 설치", + "skillDescription": "에이전트가 /orca-linear로 이슈 읽기, 업데이트 게시, 상태 변경, 풀 또는 머지 리퀘스트 첨부를 할 수 있게 합니다.", + "skillBlocked": "먼저 Linear를 연결한 다음 에이전트용 스킬을 설치하세요.", + "skillPanelTitle": "Linear 스킬", + "terminalTitle": "Linear 스킬 설정", + "terminalAriaLabel": "Linear 스킬 설치 터미널", + "showDescription": "작업 소스 선택기와 사이드바 바로 가기에 Linear를 표시합니다." }, "TaskSourceProviderCard": { - "statusChecking": "Checking…", - "statusReady": "Ready", - "statusConnectRequired": "Connect required", - "statusSkillRequired": "Skill required", - "statusHidden": "Hidden from Tasks", - "statusIncomplete": "Needs setup", + "statusChecking": "확인 중…", + "statusReady": "준비됨", + "statusConnectRequired": "연결 필요", + "statusSkillRequired": "스킬 필요", + "statusUnavailable": "상태를 확인할 수 없음", + "statusHidden": "작업에서 숨김", + "statusIncomplete": "설정 필요", "collapseSetup": "{{provider}} 설정 단계 접기", "expandSetup": "{{provider}} 설정 단계 표시" }, "TaskSourceShowInTasksStep": { - "shown": "Shown", - "show": "Show", - "hideProviderAction": "Tasks에서 {{provider}} 숨기기", - "showProviderAction": "Tasks에 {{provider}} 표시", - "lastProviderHint": "At least one provider must stay visible in Tasks.", - "title": "Show in Tasks", - "description": "Include this provider in the Tasks source picker and sidebar shortcuts." + "shown": "표시 중", + "show": "표시", + "hide": "숨기기", + "hideProviderAction": "작업에서 {{provider}} 숨기기", + "showProviderAction": "작업에 {{provider}} 표시", + "lastProviderAction": "작업에 표시된 제공업체는 {{provider}}입니다. 제공업체는 하나 이상 표시되어야 합니다.", + "lastProviderHint": "작업에는 제공업체가 하나 이상 표시되어야 합니다.", + "title": "작업에 표시", + "description": "이 제공업체를 작업 소스 선택기와 사이드바 바로 가기에 표시합니다." } }, "right": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 70d153373a7..2210cf28ee9 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -600,7 +600,7 @@ "readTicket": "Read the linked ticket", "postUpdate": "Post a progress update", "moveState": "Move the ticket forward", - "attachPr": "Attach the PR link", + "attachPr": "附加评审链接", "triageFollowups": "Triage and create follow-ups", "readTicketSummary": "Pull the linked Linear issue's full context before starting work.", "readTicketPrompt": "Use {{value0}} to read the linked Linear issue for this worktree, then summarize the goal and acceptance criteria before you start.", @@ -608,8 +608,8 @@ "postUpdatePrompt": "Use {{value0}} to post a completion update on the linked Linear issue with what changed and how it was verified.", "moveStateSummary": "Advance the Linear workflow state as the work progresses.", "moveStatePrompt": "Use {{value0}} to move the linked Linear issue to In Review now that the change is ready.", - "attachPrSummary": "Link the pull request to the Linear issue when you open it.", - "attachPrPrompt": "Use {{value0}} to attach this PR to the linked Linear issue with a PR link.", + "attachPrSummary": "创建拉取或合并请求时,将其关联到 Linear 议题。", + "attachPrPrompt": "使用 {{value0}} 将此拉取或合并请求附加到关联的 Linear 议题。", "triageFollowupsSummary": "Set assignee, priority, or estimate, and file parented follow-up tickets.", "triageFollowupsPrompt": "Use {{value0}} to triage the linked Linear issue — set priority and estimate — and create a parented follow-up ticket for the deferred cleanup." } @@ -760,10 +760,10 @@ "devSearchKeywordNotification": "notification", "projectHostsSummary": "{{value0}} 个主机", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", + "linearDescription": "Linear 在 Orca 中的工作方式、设置清单、智能体技能和提示词示例。", "pluginsTitle": "插件", "pluginsDescription": "安装和管理实验性 Orca 插件。", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "tasksDescription": "连接提供商、安装 Linear 技能并选择要在任务中显示的内容。" }, "useAppMenuPaste": { "pasteTooLarge": "粘贴内容过大。" @@ -5211,7 +5211,9 @@ "copyCommandAria": "复制命令", "runCommandDescription": "按 Enter 运行命令。", "5f818f12ab": "正在准备...", - "4c05b9d7cb": "正在准备设置终端。" + "4c05b9d7cb": "正在准备设置终端。", + "installLabel": "安装", + "updateLabel": "更新" }, "AgentsPane": { "d83834f5e6": "检测已安装的智能体...", @@ -6734,8 +6736,8 @@ "dev": "开发工具", "devDescription": "仅用于调试 UI 状态的开发工具。", "linearTitle": "Linear", - "linearDescription": "How Linear works in Orca, setup checklist, agent skill, and example prompts.", - "tasksDescription": "Connect providers, install the Linear skill, and choose what appears in Tasks." + "linearDescription": "Linear 在 Orca 中的工作方式、设置清单、智能体技能和提示词示例。", + "tasksDescription": "连接提供商、安装 Linear 技能并选择要在任务中显示的内容。" }, "SettingsFormControls": { "42a4d15a30": "没有匹配的字体。", @@ -6964,38 +6966,34 @@ "53e9aabfc0": "可用时使用 OpenSSH 多路复用。对于有自定义 SSH 限制的主机,请关闭此项。" }, "TasksPane": { - "3a72b9745e": "选择要在“任务”页面和侧边栏快捷键中显示的任务提供商。", "f71d8a9dd3": "任务提供者", - "71644aba56": "选择要在“任务”页面源选择器和侧边栏快捷键中显示的任务提供商。至少需保留一个可见提供商。", - "93e72ef659": "任务来源", - "8e1305fcc6": "在任务源选择器和侧边栏快捷方式中显示 Jira。", "6b23a34f6d": "Jira", - "e4170c9615": "在任务源选择器和侧边栏快捷方式中显示 Linear。", "09ae2d7c51": "Linear", - "dd67a1b6e1": "在任务源选择器和侧边栏快捷方式中显示 GitLab。", "7c5d7fdc20": "GitLab", - "1db47236cd": "在任务源选择器和侧边栏快捷方式中显示 GitHub。", "e14063e727": "GitHub", - "connectProviderTitle": "Connect {{provider}}", - "connectCodeHostDescription": "Install and authenticate the CLI under Integrations so Orca can load issues.", - "openIntegrations": "Integrations", - "connectInIntegrations": "Set up in Integrations", - "connectJiraTitle": "Connect Jira", - "connectJiraDescription": "Add a Jira Cloud site or self-hosted instance with an API token or PAT.", - "manageJira": "Manage access", - "addJira": "Add Jira access", - "githubDescription": "Browse GitHub issues and start workspaces from them.", - "gitlabDescription": "Browse GitLab issues and start workspaces from them.", - "linearDescription": "Connect Linear, install the agent skill, and show it in Tasks.", - "jiraDescription": "Connect Jira Cloud or self-hosted Jira and show it in Tasks.", - "setupTitle": "Task management setup", - "setupDescription": "Finish connect + visibility for each provider in one place. Linear also needs the agent skill so coding agents can read and update tickets.", - "incompleteBannerTitle": "Some visible providers still need setup", - "incompleteBannerBody": "Hide providers you do not use, or expand a card and finish its steps. For Linear: API access, the agent skill, and Show in Tasks.", - "providersDescription": "Each card walks through connection (and skill, for Linear) plus whether it appears in Tasks.", - "integrationsHint": "Credentials for all providers also live under", - "integrationsLink": "Integrations", - "skillHint": ". After Linear is connected, usage examples stay under Settings → Linear." + "connectProviderTitle": "连接 {{provider}}", + "connectCodeHostDescription": "在集成中安装并验证 CLI,以便 Orca 加载议题。", + "connectionCheckUnavailable": "Orca 无法检查此连接。请重试,或打开“集成”查看设置详情。", + "retryConnection": "重试", + "openIntegrations": "集成", + "connectInIntegrations": "在集成中设置", + "connectJiraTitle": "连接 Jira", + "connectJiraDescription": "使用 API Token 或 PAT 添加 Jira Cloud 站点或自托管实例。", + "manageJira": "管理密钥", + "addJira": "添加 Jira 访问权限", + "githubDescription": "浏览 GitHub 议题并从中启动工作区。", + "gitlabDescription": "浏览 GitLab 议题并从中启动工作区。", + "linearDescription": "连接 Linear、安装智能体技能并将其显示在任务中。", + "jiraDescription": "连接 Jira Cloud 或自托管 Jira 并将其显示在任务中。", + "setupTitle": "任务管理设置", + "setupDescription": "在一处完成每个提供商的连接和显示设置。Linear 还需要智能体技能,以便智能体读取和更新议题。必须至少保留一个可见提供商。", + "incompleteBannerTitle": "部分可见提供商仍需设置", + "incompleteBannerBody": "隐藏不使用的提供商,或展开卡片并完成其中的步骤。", + "providersDescription": "每张卡片都会引导你完成连接、任务显示设置;Linear 还包括技能设置。", + "integrationsHint": "所有提供商的凭据也可在此管理:", + "integrationsLink": "集成", + "skillHint": "。连接 Linear 后,可在设置 → Linear 中查看使用示例。", + "incompleteBannerBodyWithLinear": "隐藏不使用的提供商,或展开卡片并完成其中的步骤。Linear 需要 API 访问权限、智能体技能以及在任务中显示。" }, "TerminalAppearanceSection": { "a14a427ae4": "窗格分隔线的厚度。", @@ -8634,9 +8632,8 @@ "3d81c26d78": "来源", "cf0e3e0c2f": "提供者", "2ec54bee51": "任务", - "765f0c544d": "选择要在“任务”页面和侧边栏快捷键中显示的任务提供商。", "5b8e4aace5": "任务提供者", - "providersDescription": "Connect task providers, install the Linear agent skill, and choose what appears in Tasks.", + "providersDescription": "连接任务提供商、安装 Linear 智能体技能并选择要在任务中显示的内容。", "setup": "设置", "apiKey": "API 密钥", "skill": "技能", @@ -9407,13 +9404,13 @@ "notInstalled": "未安装", "recheck": "重新检查", "installedDescription": "智能体技能已安装。要更新,请运行:", - "description": "让智能体读取和编辑 Linear 任务。完整引导设置(连接 + 技能 + 可见性)在 设置 → Task Sources。", + "description": "让智能体读取和编辑 Linear 任务。完整引导设置(连接 + 技能 + 可见性)位于设置 → 任务来源。", "copyCommand": "复制命令" } }, "search": { "title": "Linear", - "description": "Linear skill status, usage examples, and links to Task Sources setup." + "description": "Linear 技能状态、使用示例和任务来源设置链接。" } } } @@ -9502,15 +9499,14 @@ }, "LinearAgentSkillPane": { "title": "Linear", - "description": "How Linear works in Orca: browse issues, start linked workspaces, and let agents update tickets with /orca-linear.", - "skillTitle": "Linear skill", - "skillDescription": "Enables agents to read linked tickets and post updates to Linear through Orca.", - "howToUse": "Example prompts", - "howToUseDescription": "Click a card to copy a prompt. Use these in a Linear-linked worktree after the skill is installed.", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "skillSectionTitle": "Agent skill", - "skillSectionBody": "Install or update the skill agents need. You can also complete this step from Task Sources during first-time setup." + "description": "在 Orca 中浏览 Linear 议题、启动关联工作区,并让智能体通过 /orca-linear 更新议题。", + "skillTitle": "Linear 技能", + "howToUse": "提示词示例", + "howToUseDescription": "点击卡片即可复制提示词。安装技能后,请在关联 Linear 的工作区中使用。", + "terminalTitle": "Linear 技能设置", + "terminalAriaLabel": "Linear 技能安装终端", + "manageConnectionHint": "可在此查看已连接的 Linear 工作区和 API 密钥:", + "manageConnectionLink": "集成" }, "MobileRelayBetaNotice": { "notice": "Orca Relay is in beta." @@ -9822,67 +9818,67 @@ } }, "LinearAgentSkillGuide": { - "setupConnectTitle": "1. Connect Linear", - "setupConnectBody": "Personal API key so Orca can list issues and open linked workspaces.", - "manageKeys": "Manage keys", - "addAccess": "Add access", - "setupSkillTitle": "2. Install the agent skill", - "setupSkillBody": "Gives coding agents /orca-linear for read, update, triage, and PR attach.", - "skillReady": "Ready below", - "installBelow": "Install below", - "setupVisibleTitle": "3. Show Linear in Tasks", - "setupVisibleBody": "Keeps Linear in the Tasks source picker and sidebar shortcuts.", - "openTaskSources": "Task Sources", - "setupTitle": "Setup checklist", - "setupBody": "All three are required for the full Tasks + agent loop. First-time path is also under Task Sources.", - "setupReady": "All set", - "setupProgress": "{{done}} of {{total}} ready", - "setupHint": "Prefer one guided place for connect + skill + visibility?", - "openFullSetup": "Open Task Sources setup", - "notesTitle": "Good to know", - "notesIntro": "Quick reminders once Linear is connected and the skill is installed.", - "noteLinkedTitle": "Start from a Linear issue", - "noteLinkedBody": "Ticket actions work best in a worktree created from Tasks so the issue stays linked as context.", - "noteSlashTitle": "Mention /orca-linear", - "noteSlashBody": "In chat, use /orca-linear (or ask in plain language) so the agent loads the skill for that turn.", - "noteKeysTitle": "Keys follow the runtime", - "noteKeysBody": "API keys and workspaces are stored for the active runtime. Manage them anytime under Integrations.", - "noteVisibilityTitle": "Hiding ≠ disconnect", - "noteVisibilityBody": "Hiding Linear in Task Sources only removes it from the picker. It does not remove your key or skill." + "setupConnectTitle": "1. 连接 Linear", + "setupConnectBody": "用于让 Orca 列出议题并打开关联工作区的个人 API 密钥。", + "manageKeys": "管理密钥", + "addAccess": "添加访问权限", + "setupSkillTitle": "2. 安装智能体技能", + "setupSkillBody": "让智能体使用 /orca-linear 读取、更新、分类议题并附加拉取或合并请求。", + "setupVisibleTitle": "3. 在任务中显示 Linear", + "setupVisibleBody": "在任务来源选择器和侧边栏快捷方式中保留 Linear。", + "openTaskSources": "任务来源", + "setupTitle": "设置清单", + "setupBody": "完整的任务与智能体流程需要完成全部三个步骤。首次设置也可在任务来源中进行。", + "setupReady": "全部就绪", + "setupProgress": "已完成 {{done}}/{{total}}", + "notesTitle": "使用须知", + "notesIntro": "连接 Linear 并安装技能后的一些快速提示。", + "noteLinkedTitle": "从 Linear 议题开始", + "noteLinkedBody": "从任务创建的工作区会将议题保留为关联上下文,因此议题操作最顺畅。", + "noteSlashTitle": "提及 /orca-linear", + "noteSlashBody": "在聊天中使用 /orca-linear(或用自然语言提出请求),智能体会在该轮加载此技能。", + "noteKeysTitle": "密钥按运行环境保存", + "noteKeysBody": "API 密钥和工作区存储在当前运行环境中。", + "noteVisibilityTitle": "隐藏不等于断开连接", + "noteVisibilityBody": "在任务来源中隐藏 Linear 只会将其从选择器中移除,不会删除密钥或技能。", + "setupChecking": "检查中…" }, "TaskSourceLinearSetup": { - "connectTitle": "Connect Linear", - "connectDescription": "Add a Personal API key so Orca can browse issues and open workspaces with ticket context.", - "manageAccess": "Manage access", - "addAccess": "Add Linear access", - "connectedHint": "Workspaces and keys are stored for the active runtime. You can add more access any time.", - "recheck": "Re-check connection", - "skillTitle": "Install Linear agent skill", - "skillDescription": "Gives agents the /orca-linear skill to read tickets, post updates, move states, and attach PRs.", - "skillBlocked": "Connect Linear first, then install the skill for agents.", - "skillPanelTitle": "Linear skill", - "terminalTitle": "Linear skill setup", - "terminalAriaLabel": "Linear skill install terminal", - "showDescription": "Include Linear in the Tasks page source picker and sidebar shortcuts." + "connectTitle": "连接 Linear", + "connectDescription": "添加个人 API 密钥,让 Orca 能够浏览议题并使用议题上下文打开工作区。", + "manageAccess": "管理密钥", + "addAccess": "添加 Linear 访问权限", + "connectedHint": "工作区和密钥存储在当前运行环境中。你可以随时添加更多访问权限。", + "recheck": "重新检查连接", + "skillTitle": "安装 Linear 智能体技能", + "skillDescription": "让智能体使用 /orca-linear 读取议题、发布更新、更改状态并附加拉取或合并请求。", + "skillBlocked": "请先连接 Linear,然后为智能体安装技能。", + "skillPanelTitle": "Linear 技能", + "terminalTitle": "Linear 技能设置", + "terminalAriaLabel": "Linear 技能安装终端", + "showDescription": "在任务来源选择器和侧边栏快捷方式中显示 Linear。" }, "TaskSourceProviderCard": { - "statusChecking": "Checking…", - "statusReady": "Ready", - "statusConnectRequired": "Connect required", - "statusSkillRequired": "Skill required", - "statusHidden": "Hidden from Tasks", - "statusIncomplete": "Needs setup", + "statusChecking": "检查中…", + "statusReady": "已就绪", + "statusConnectRequired": "需要连接", + "statusSkillRequired": "需要技能", + "statusUnavailable": "状态不可用", + "statusHidden": "已从任务中隐藏", + "statusIncomplete": "需要设置", "collapseSetup": "收起 {{provider}} 设置步骤", "expandSetup": "显示 {{provider}} 设置步骤" }, "TaskSourceShowInTasksStep": { - "shown": "Shown", - "show": "Show", + "shown": "已显示", + "show": "显示", + "hide": "隐藏", "hideProviderAction": "从任务中隐藏 {{provider}}", "showProviderAction": "在任务中显示 {{provider}}", - "lastProviderHint": "At least one provider must stay visible in Tasks.", - "title": "Show in Tasks", - "description": "Include this provider in the Tasks source picker and sidebar shortcuts." + "lastProviderAction": "{{provider}} 已显示在任务中。必须至少保留一个可见提供商。", + "lastProviderHint": "任务中必须至少保留一个可见提供商。", + "title": "在任务中显示", + "description": "在任务来源选择器和侧边栏快捷方式中显示此提供商。" } }, "right": { diff --git a/src/renderer/src/lib/linear-usage-examples.ts b/src/renderer/src/lib/linear-usage-examples.ts index af6b0a5f1e8..38c60bfc8c4 100644 --- a/src/renderer/src/lib/linear-usage-examples.ts +++ b/src/renderer/src/lib/linear-usage-examples.ts @@ -47,14 +47,14 @@ export const getLinearUsageExamples = createLocalizedCatalog((): SkillUsageExamp }, { id: 'attach-pr', - title: translate('auto.lib.linear.usage.examples.attachPr', 'Attach the PR link'), + title: translate('auto.lib.linear.usage.examples.attachPr', 'Attach the review link'), summary: translate( 'auto.lib.linear.usage.examples.attachPrSummary', - 'Link the pull request to the Linear issue when you open it.' + 'Link the pull or merge request to the Linear issue when you open it.' ), prompt: translate( 'auto.lib.linear.usage.examples.attachPrPrompt', - 'Use {{value0}} to attach this PR to the linked Linear issue with a PR link.', + 'Use {{value0}} to attach this pull or merge request to the linked Linear issue.', { value0: LINEAR_SLASH_COMMAND } ) },