refactor(settings): unify Linear setup into guided flows across Task Sou

Extract shared Linear skill setup logic into `useLinearAgentSkillSetup` hook so
Task Sources and LinearAgentSkillPane both follow the same configuration path.
Embed the skill install panel inside LinearAgentSkillGuide instead of placing it
after, eliminating duplicate "Agent skill" section headers. Introduce task
provider setup state helpers: distinguish fresh untouched providers from
started-then-stalled ones (only warn on the latter), keep auto-expanded cards
open across rechecks via sticky tracking, and handle unavailable/failed
preflight status. Rename BrowserUseStepBadge to SetupStepBadge (used by
multiple setup types now). Route connected credential management to Integrations
cards instead of re-opening dialogs. Add settled flag to skill discovery so
focus-triggered rescans don't flash UI on known results.
This commit is contained in:
Jinjing
2026-07-30 01:54:44 -07:00
parent 35dd9d0f3a
commit 7b3e084316
50 changed files with 2190 additions and 1019 deletions
@@ -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 {
@@ -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
@@ -4,7 +4,7 @@ export type IntegrationStatusTone = 'connected' | 'attention' | 'neutral'
const TONE_CLASSES: Record<IntegrationStatusTone, { pill: string; dot: string }> = {
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: {
@@ -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 }) => (
<span data-testid="skill-freshness">{skillName}</span>
)
}))
function panelProps(
overrides: Partial<ComponentProps<typeof AgentSkillSetupPanel>> = {}
): ComponentProps<typeof AgentSkillSetupPanel> {
@@ -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 class="text-\[13px\] leading-snug text-muted-foreground"><\/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<void>((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 })
@@ -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<string | null>(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}
</Button>
) : 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 ? (
<p className="text-[12px] text-destructive">{error}</p>
) : null
<>
{error ? <p className="text-[12px] text-destructive">{error}</p> : null}
{/* hideHeader drops the title row; keep freshness so guided hubs still surface updates. */}
{installed && freshnessSkillName ? (
<div className="mb-2">
<SkillFreshnessStatusPill skillName={freshnessSkillName} />
</div>
) : null}
</>
) : (
<div className="flex items-center gap-4">
{leading}
@@ -302,7 +318,9 @@ export function AgentSkillSetupPanel({
</div>
)}
<div className={cn('max-w-none', hideHeader ? null : 'mt-3')}>
<p className="text-[13px] leading-snug text-muted-foreground">{description}</p>
{description != null ? (
<p className="text-[13px] leading-snug text-muted-foreground">{description}</p>
) : null}
{actionRow}
{actionHint ? <div className="mt-2">{actionHint}</div> : null}
{!installed && preInstallNotice && preInstallNoticeVisible ? (
@@ -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'
@@ -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'
@@ -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 = {
@@ -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(
<LinearAgentSkillGuide
status={{
connected: true,
connectionChecking: false,
skillInstalled: false,
skillChecking: false,
visibleInTasks: true
}}
status={baseStatus}
onOpenTaskSources={vi.fn()}
onManageLinearAccess={vi.fn()}
skillPanel={<div data-testid="skill-panel">Skill install panel</div>}
/>
)
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(
<LinearAgentSkillGuide
status={{
connected: true,
connectionChecking: false,
skillInstalled: true,
skillChecking: false,
visibleInTasks: true
...baseStatus,
skillInstalled: true
}}
onOpenTaskSources={vi.fn()}
onManageLinearAccess={vi.fn()}
skillPanel={<div>Skill panel</div>}
/>
)
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(
<LinearAgentSkillGuide
status={{
connected: true,
connectionChecking: true,
...baseStatus,
skillInstalled: true,
skillChecking: false,
visibleInTasks: true
skillChecking: true
}}
onOpenTaskSources={vi.fn()}
onManageLinearAccess={vi.fn()}
skillPanel={<div>Skill panel</div>}
/>
)
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(
<LinearAgentSkillGuide
status={{
...baseStatus,
skillInstalled: true,
connectionChecking: true
}}
onOpenTaskSources={vi.fn()}
onManageLinearAccess={vi.fn()}
skillPanel={<div>Skill panel</div>}
/>
)
expect(markup).toContain('Checking…')
expect(markup).not.toContain('2 of 3 ready')
})
})
@@ -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 (
<Circle className="size-3.5 animate-pulse text-muted-foreground motion-reduce:animate-none" />
<span className="flex size-5 items-center justify-center text-muted-foreground">
<Circle className="size-3.5 animate-pulse motion-reduce:animate-none" />
</span>
)
}
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 (
<section className="space-y-3 rounded-xl border border-border/60 bg-card/30 p-4">
@@ -140,58 +83,109 @@ export function LinearAgentSkillGuide({
)}
</p>
</div>
<IntegrationStatusPill tone={allReady ? 'connected' : 'attention'}>
{allReady
? translate('auto.components.settings.LinearAgentSkillGuide.setupReady', 'All set')
: translate(
'auto.components.settings.LinearAgentSkillGuide.setupProgress',
'{{done}} of {{total}} ready',
{ done: completed, total: setupRows.length }
)}
<IntegrationStatusPill tone={checking ? 'neutral' : allReady ? 'connected' : 'attention'}>
{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 }
)}
</IntegrationStatusPill>
</div>
<div className="divide-y divide-border/50">
{setupRows.map((row) => (
<div key={row.id} className="flex flex-wrap items-start gap-3 py-3">
<div className="flex flex-wrap items-start gap-3 py-3">
<div className="mt-0.5">
<SetupStatusIcon done={status.connected} checking={status.connectionChecking} />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium text-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupConnectTitle',
'1. Connect Linear'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupConnectBody',
'Personal API key so Orca can list issues and open linked workspaces.'
)}
</p>
</div>
<Button
type="button"
size="sm"
variant={status.connected ? 'outline' : 'default'}
className="shrink-0"
onClick={onManageLinearAccess}
>
{status.connected
? translate(
'auto.components.settings.LinearAgentSkillGuide.manageKeys',
'Manage keys'
)
: translate('auto.components.settings.LinearAgentSkillGuide.addAccess', 'Add access')}
</Button>
</div>
<div className="space-y-3 py-3">
<div className="flex flex-wrap items-start gap-3">
<div className="mt-0.5">
<SetupStatusIcon done={row.done} checking={row.checking} />
<SetupStatusIcon done={status.skillInstalled} checking={status.skillChecking} />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium text-foreground">{row.title}</p>
<p className="text-xs text-muted-foreground">{row.body}</p>
<p className="text-sm font-medium text-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupSkillTitle',
'2. Install the agent skill'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupSkillBody',
'Gives coding agents /orca-linear for reading, updates, triage, and attaching pull or merge requests.'
)}
</p>
</div>
<Button
type="button"
size="sm"
variant={row.done ? 'outline' : 'default'}
className="shrink-0"
onClick={row.onAction}
>
{row.actionLabel}
</Button>
</div>
))}
</div>
{skillPanel}
</div>
<p className="text-[11px] text-muted-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupHint',
'Prefer one guided place for connect + skill + visibility?'
)}{' '}
<Button
type="button"
variant="link"
size="sm"
className="h-auto p-0 text-[11px] align-baseline"
onClick={onOpenTaskSources}
>
{translate(
'auto.components.settings.LinearAgentSkillGuide.openFullSetup',
'Open Task Sources setup'
)}
</Button>
</p>
<div className="flex flex-wrap items-start gap-3 py-3">
<div className="mt-0.5">
<SetupStatusIcon done={status.visibleInTasks} checking={false} />
</div>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-medium text-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupVisibleTitle',
'3. Show Linear in Tasks'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.LinearAgentSkillGuide.setupVisibleBody',
'Keeps Linear in the Tasks source picker and sidebar shortcuts.'
)}
</p>
</div>
<Button
type="button"
size="sm"
variant={status.visibleInTasks ? 'outline' : 'default'}
className="shrink-0"
onClick={onOpenTaskSources}
>
{translate(
'auto.components.settings.LinearAgentSkillGuide.openTaskSources',
'Task Sources'
)}
</Button>
</div>
</div>
</section>
)
}
@@ -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 (
<section>
<h3>{String(props.title)}</h3>
{!props.hideHeader && <h3>{String(props.title)}</h3>}
{props.description != null && <p>{String(props.description)}</p>}
<span>{props.installed ? 'Installed' : 'Not installed'}</span>
<code>{String(props.command)}</code>
<code>{String(props.installedCommand)}</code>
@@ -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(<LinearAgentSkillPane />)
expect(mocks.panelProps.at(-1)).not.toHaveProperty('icon')
})
it('renders checklist with inlined skill panel and usage examples', () => {
const markup = renderToStaticMarkup(<LinearAgentSkillPane />)
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(<LinearAgentSkillPane />)
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 () => {
@@ -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<string, LucideIcon> = {
'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 = (
<AgentSkillSetupPanel
variant="inline"
hideHeader
title={translate('auto.components.settings.LinearAgentSkillPane.skillTitle', 'Linear skill')}
description={null}
command={skillSetup.installCommand}
installedCommand={skillSetup.updateCommand}
terminalTitle={translate(
'auto.components.settings.LinearAgentSkillPane.terminalTitle',
'Linear skill setup'
)}
terminalAriaLabel={translate(
'auto.components.settings.LinearAgentSkillPane.terminalAriaLabel',
'Linear skill install terminal'
)}
terminalWorktreeId="settings-linear-skill-terminal"
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}
/>
)
return (
<SearchableSetting
title={translate('auto.components.settings.LinearAgentSkillPane.title', 'Linear')}
@@ -123,77 +109,17 @@ export function LinearAgentSkillPane(): React.JSX.Element {
status={{
connected: linearConnected,
connectionChecking,
skillInstalled: linearSkillInstalled,
skillChecking: linearSkillLoading,
skillInstalled: skillSetup.skillInstalled,
skillChecking: skillSetup.skillChecking,
visibleInTasks
}}
onOpenTaskSources={openTaskSources}
onManageLinearAccess={() => setLinearKeyDialogOpen(true)}
onManageLinearAccess={
linearConnected ? openIntegrationSettings : () => setLinearKeyDialogOpen(true)
}
skillPanel={skillPanel}
/>
<div id="linear-agent-skill-install" className="space-y-3 scroll-mt-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">
{translate(
'auto.components.settings.LinearAgentSkillPane.skillSectionTitle',
'Agent skill'
)}
</h3>
<p className="text-xs text-muted-foreground">
{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.'
)}
</p>
</div>
<AgentSkillSetupPanel
title={translate(
'auto.components.settings.LinearAgentSkillPane.skillTitle',
'Linear skill'
)}
description={translate(
'auto.components.settings.LinearAgentSkillPane.skillDescription',
'Enables agents to read linked tickets and post updates to Linear through Orca.'
)}
command={installCommand}
installedCommand={updateCommand}
terminalTitle={translate(
'auto.components.settings.LinearAgentSkillPane.terminalTitle',
'Linear skill setup'
)}
terminalAriaLabel={translate(
'auto.components.settings.LinearAgentSkillPane.terminalAriaLabel',
'Linear skill install terminal'
)}
terminalWorktreeId="settings-linear-skill-terminal"
terminalShellOverride={activeSkillRuntime.terminalShellOverride}
installed={linearSkillInstalled}
loading={linearSkillLoading}
error={activeSkillRuntime.installDisabledReason ?? linearSkillError}
installDisabled={Boolean(activeSkillRuntime.installDisabledReason)}
icon={<LinearIcon className="size-5" />}
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
}
/>
</div>
<SkillUsageExamplesSection
heading={translate(
'auto.components.settings.LinearAgentSkillPane.howToUse',
@@ -210,14 +136,32 @@ export function LinearAgentSkillPane(): React.JSX.Element {
<LinearAgentSkillNotes />
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.LinearAgentSkillPane.manageConnectionHint',
'Review connected Linear workspaces and API keys in'
)}{' '}
<Button
type="button"
variant="link"
size="sm"
className="h-auto p-0 text-xs align-baseline"
onClick={openIntegrationSettings}
>
{translate(
'auto.components.settings.LinearAgentSkillPane.manageConnectionLink',
'Integrations'
)}
</Button>
</p>
<LinearApiKeyDialog
open={linearKeyDialogOpen}
onOpenChange={setLinearKeyDialogOpen}
connectLabel={
linearConnected
? translate('auto.components.settings.LinearAgentSkillGuide.manageKeys', 'Manage keys')
: translate('auto.components.settings.LinearAgentSkillGuide.addAccess', 'Add access')
}
connectLabel={translate(
'auto.components.settings.LinearAgentSkillGuide.addAccess',
'Add access'
)}
onConnected={() => {
void checkLinearConnection(true)
}}
@@ -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'
@@ -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 (
<TaskSourceLinearSetup
connected={overrides.connected ?? false}
checking={false}
visible={overrides.visible ?? true}
canHide
onToggleVisible={vi.fn()}
onOpenIntegrations={overrides.onOpenIntegrations ?? vi.fn()}
/>
)
}
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()
})
})
@@ -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}
/>
)}
</TaskSourceStepRow>
@@ -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</button>')
expect(markup).not.toContain('>Shown</button>')
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 => (
<TaskSourceProviderCard
icon={<span />}
name="Linear"
description="Linear setup"
readiness={readiness}
visible
canHide
defaultExpanded={defaultExpanded}
onToggleVisible={vi.fn()}
>
<span>Install terminal</span>
</TaskSourceProviderCard>
)
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(
<TaskSourceProviderCard
icon={<span />}
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</button>')
expect(markup).not.toContain('>Hide</button>')
})
})
@@ -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<boolean | null>(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({
</IntegrationStatusPill>
{isTaskProviderChecking(readiness) ||
status === 'ready' ||
status === 'unavailable' ||
status === 'hidden' ? null : (
<span
className={cn(
'rounded-full px-2 py-0.5 text-[10px] font-medium',
progress.completed === progress.total
? 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-400'
: 'bg-muted text-muted-foreground'
)}
>
// Only unfinished providers reach here, so the count is always partial.
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{`${progress.completed}/${progress.total}`}
</span>
)}
@@ -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)}
</Button>
) : null}
<Button
@@ -152,7 +157,7 @@ export function TaskSourceProviderCard({
{ provider: name }
)
}
onClick={() => setExpandedOverride(!expanded)}
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ChevronDown className="size-4" /> : <ChevronRight className="size-4" />}
</Button>
@@ -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(
<TaskSourceShowInTasksStep
index={2}
providerLabel="GitLab"
visible
canHide
onToggleVisible={vi.fn()}
/>
)
expect(markup).not.toContain('aria-pressed')
expect(markup).toContain('>Hide</button>')
})
it('explains why the final visible provider cannot be hidden', () => {
const markup = renderToStaticMarkup(
<TaskSourceShowInTasksStep
index={2}
providerLabel="GitLab"
visible
canHide={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="GitLab is shown in Tasks. At least one provider must stay visible."'
)
expect(markup).toContain('>Shown</button>')
expect(markup).toContain('At least one provider must stay visible in Tasks.')
})
})
@@ -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)}
</Button>
}
/>
>
{locked ? (
<p className="text-[11px] text-muted-foreground">{getShowInTasksLastProviderHint()}</p>
) : null}
</TaskSourceStepRow>
)
}
@@ -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(
<JiraSetupSteps
connected
checking={false}
visible
canHide
onToggleVisible={vi.fn()}
onConnected={vi.fn()}
onOpenIntegrations={onOpenIntegrations}
/>
)
fireEvent.click(rendered.getByRole('button', { name: 'Manage keys' }))
expect(onOpenIntegrations).toHaveBeenCalledOnce()
expect(mocks.dialogOpen.at(-1)).toBe(false)
})
})
@@ -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 (
<ol className="divide-y divide-border/50">
<TaskSourceStepRow
@@ -36,23 +48,22 @@ export function CodeHostSetupSteps(
'Connect {{provider}}',
{ provider: props.providerLabel }
)}
description={translate(
'auto.components.settings.TasksPane.connectCodeHostDescription',
'Install and authenticate the CLI under Integrations so Orca can load issues.'
)}
description={connectionDescription}
action={
<Button
type="button"
size="sm"
variant={props.connected ? 'outline' : 'default'}
onClick={props.onOpenIntegrations}
onClick={props.unavailable ? props.onRetryConnection : props.onOpenIntegrations}
>
{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'
)}
</Button>
}
/>
@@ -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')}
</Button>
}
@@ -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 = {
@@ -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 <div data-testid="linear-setup">Linear setup steps</div>
}
}))
vi.mock('./TaskSourceSimpleSetup', () => ({
CodeHostSetupSteps: (props: { providerLabel: string }) => (
<div data-testid={`code-host-${props.providerLabel}`}>Code host setup</div>
CodeHostSetupSteps: (props: {
providerLabel: string
unavailable?: boolean
onRetryConnection: () => void
}) => (
<div data-testid={`code-host-${props.providerLabel}`}>
{props.unavailable ? (
<>
<span>Orca couldn&apos;t check this connection</span>
<button type="button" onClick={props.onRetryConnection}>
Try again
</button>
</>
) : (
'Code host setup'
)}
</div>
),
JiraSetupSteps: () => <div data-testid="jira-setup">Jira setup</div>
JiraSetupSteps: (props: { onOpenIntegrations: () => void }) => {
mocks.jiraSetupProps.push(props)
return <div data-testid="jira-setup">Jira setup</div>
}
}))
vi.mock('@/store', () => ({
@@ -71,14 +100,37 @@ function renderPane(): string {
return renderToStaticMarkup(<TasksPane settings={baseSettings} updateSettings={vi.fn()} />)
}
let root: Root | null = null
let container: HTMLDivElement | null = null
async function renderInteractivePane(): Promise<HTMLDivElement> {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
await act(async () => {
root?.render(<TasksPane settings={baseSettings} updateSettings={vi.fn()} />)
})
return container
}
async function rerenderInteractivePane(): Promise<void> {
await act(async () => {
root?.render(<TasksPane settings={baseSettings} updateSettings={vi.fn()} />)
})
}
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 })
})
})
@@ -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<TaskProvider | null>(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 ? (
<div className="rounded-xl border border-amber-500/25 bg-amber-500/5 px-3.5 py-3 text-xs text-muted-foreground">
<p className="font-medium text-amber-800 dark:text-amber-200">
{translate(
@@ -140,11 +161,18 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J
'Some visible providers still need setup'
)}
</p>
{/* Linear is the only multi-step provider today; the generic body
covers any future provider that can stall the same way. */}
<p className="mt-1">
{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.'
)}
</p>
</div>
) : 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' ? (
<JiraSetupSteps
@@ -193,16 +222,19 @@ export function TasksPane({ settings, updateSettings }: TasksPaneProps): React.J
canHide={canHide}
onToggleVisible={() => toggleProvider('jira')}
onConnected={() => void checkJiraConnection()}
onOpenIntegrations={() => openIntegrations(JIRA_INTEGRATION_SECTION_ID)}
/>
) : (
<CodeHostSetupSteps
providerLabel={meta.label}
connected={readiness.connected}
checking={readiness.checking}
unavailable={readiness.unavailable}
visible={visible}
canHide={canHide}
onToggleVisible={() => toggleProvider(provider)}
onOpenIntegrations={openIntegrations}
onOpenIntegrations={() => openIntegrations()}
onRetryConnection={() => void refreshPreflightStatus({ force: true })}
/>
)}
</TaskSourceProviderCard>
@@ -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')}
</Button>
@@ -24,15 +24,24 @@ const updateCapableCallers = new Map<string, readonly string[]>([
['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<string, readonly string[]>([
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()
])
@@ -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 (
<div className={shellClass}>
<div className={shellClass} data-settings-section={props.settingsSectionId}>
<div className="flex flex-wrap items-start gap-3">
<span className="shrink-0 text-muted-foreground">{props.icon}</span>
<div className="min-w-0 flex-1 basis-[16rem] space-y-0.5">
@@ -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')
@@ -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 (
<IntegrationCardShell
settingsSectionId={JIRA_INTEGRATION_SECTION_ID}
icon={<JiraIcon className="size-5" />}
name="Jira"
description={
@@ -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.'
)
},
{
@@ -0,0 +1,2 @@
export const LINEAR_INTEGRATION_SECTION_ID = 'integrations-linear'
export const JIRA_INTEGRATION_SECTION_ID = 'integrations-jira'
@@ -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')
})
})
@@ -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, TaskProviderReadiness>
): 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<TaskProvider, TaskProviderReadiness>
previousAutoExpanded: TaskProvider | null
}): TaskProvider | null {
return previousAutoExpanded ?? getAutoExpandedTaskProvider(providers, readinessByProvider)
}
@@ -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.'
@@ -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 (
<IntegrationCardShell
settingsSectionId={LINEAR_INTEGRATION_SECTION_ID}
icon={<LinearIcon className="size-5" />}
name="Linear"
description={
@@ -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<boolean>
getPrerequisiteStatus: () => Promise<Awaited<ReturnType<typeof window.api.cli.getInstallStatus>>>
onBeforeOpenTerminal: () => Promise<void>
} {
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
}
}
@@ -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<string, unknown>,
skill: {
installed: false,
loading: false,
settled: true,
error: null,
skills: [],
refresh: vi.fn()
}
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => 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<TaskProvider, TaskProviderReadiness> | null = null
function Probe({ visibleProviders }: { visibleProviders: readonly TaskProvider[] }): null {
latest = useTaskSourceProviderReadiness(visibleProviders)
return null
}
async function renderProbe(
visibleProviders: readonly TaskProvider[] = ALL_PROVIDERS
): Promise<void> {
if (!container) {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
}
await act(async () => {
root?.render(<Probe visibleProviders={visibleProviders} />)
})
}
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)
})
})
@@ -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
])
}
@@ -0,0 +1,110 @@
import type { SkillDiscoveryResult, SkillDiscoveryTarget } from '../../../shared/skills'
let cachedDiscoveryByTarget = new Map<string, SkillDiscoveryResult>()
let pendingDiscoveryByTarget = new Map<string, Promise<SkillDiscoveryResult>>()
let pendingDiscoverySatisfiesForcedRefreshByTarget = new Map<string, boolean>()
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<SkillDiscoveryResult> {
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<SkillDiscoveryResult> {
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()
}
@@ -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'
@@ -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 {
@@ -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<SkillDiscoveryResult>()
const focusScan = deferred<SkillDiscoveryResult>()
const discover = vi
.fn<(target?: SkillDiscoveryTarget) => Promise<SkillDiscoveryResult>>()
.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<SkillDiscoveryResult>>()
.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<SkillDiscoveryResult>()
const wslScan = deferred<SkillDiscoveryResult>()
const discover = vi
.fn<(target?: SkillDiscoveryTarget) => Promise<SkillDiscoveryResult>>()
.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)
})
})
@@ -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<SkillDiscoveryResult>>()
.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' })])
+45 -110
View File
@@ -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<boolean>
}
let cachedDiscoveryByTarget = new Map<string, SkillDiscoveryResult>()
let pendingDiscoveryByTarget = new Map<string, Promise<SkillDiscoveryResult>>()
let pendingDiscoverySatisfiesForcedRefreshByTarget = new Map<string, boolean>()
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<SkillDiscoveryResult> {
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<SkillDiscoveryResult> {
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<SkillDiscoveryResult | null>(cachedDiscovery)
const [loading, setLoading] = useState(enabled && !cachedDiscovery)
const [error, setError] = useState<string | null>(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<boolean> => {
async (force = true, showLoading = true): Promise<boolean> => {
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
+25 -29
View File
@@ -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."
+95 -99
View File
@@ -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": {
+95 -99
View File
@@ -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": {
+97 -101
View File
@@ -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": {
+95 -99
View File
@@ -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": {
@@ -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 }
)
},