feat: improve orchestration settings pane with skill coverage and usage examples (#4869)

* feat: improve orchestration settings pane with skill coverage and usage

- Add per-agent skill coverage panel showing which detected agents have the orchestration skill installed, with Ready/Missing status chips
- Add five usage example cards (handoff, worktree handoff, phased workflow, parallel, worktree PRs) with copy-prompt dialogs
- Add "Copy install command" link that opens a dialog for users who prefer their own terminal
- Expose `skills` array from `useInstalledAgentSkill` so callers can inspect discovered paths
- Rename install button to "Update" when skill is already installed
- Add dev-only preview flags for testing the uninstalled and CLI-ready states

* feat: remove dev-only CLI preview scaffolding from OrchestrationPane

Dead code cleanup — the dev-preview flag helpers and mock CliInstallStatus
factories were never shipped to users and added noise around the real
skill-detection path. Props now flow directly from useInstalledAgentSkill
without the preview indirection layer.
This commit is contained in:
Jinjing
2026-06-08 01:05:04 -07:00
committed by GitHub
parent 27b2d1bc3e
commit a1f693b3ff
14 changed files with 795 additions and 18 deletions
@@ -41,7 +41,7 @@ describe('AgentSkillSetupPanel', () => {
const html = renderPanel({ installed: true })
expect(html).toContain('Installed')
expect(buttonLabels(html)).toContain('Install')
expect(buttonLabels(html)).toContain('Update')
expect(buttonLabels(html)).toContain('Re-check')
})
@@ -49,7 +49,7 @@ describe('AgentSkillSetupPanel', () => {
const html = renderPanel({ installed: true, showRecheckWhenInstalled: false })
expect(html).toContain('Installed')
expect(buttonLabels(html)).toContain('Install')
expect(buttonLabels(html)).toContain('Update')
expect(buttonLabels(html)).not.toContain('Re-check')
})
@@ -34,6 +34,10 @@ type AgentSkillSetupPanelProps = {
onBeforeOpenTerminal?: () => void | Promise<void>
showInstallWhenInstalled?: boolean
showRecheckWhenInstalled?: boolean
installLabel?: string
installedInstallLabel?: string
actionHint?: ReactNode
footer?: ReactNode
onRecheck: () => void | Promise<void>
}
@@ -60,6 +64,10 @@ export function AgentSkillSetupPanel({
onBeforeOpenTerminal,
showInstallWhenInstalled = true,
showRecheckWhenInstalled = true,
installLabel = 'Install',
installedInstallLabel = 'Update',
actionHint,
footer,
onRecheck
}: AgentSkillSetupPanelProps): React.JSX.Element {
const [terminalOpen, setTerminalOpen] = useState(false)
@@ -135,7 +143,7 @@ export function AgentSkillSetupPanel({
disabled={terminalOpen || installDisabled}
>
<Terminal className="size-3.5" />
Install
{installed ? installedInstallLabel : installLabel}
</Button>
) : null}
{!installed || showRecheckWhenInstalled ? (
@@ -188,12 +196,20 @@ export function AgentSkillSetupPanel({
<div className="mt-3 max-w-none">
<p className="text-[13px] leading-snug text-muted-foreground">{description}</p>
{actionRow}
{actionHint ? <div className="mt-2">{actionHint}</div> : null}
{!installed && preInstallNotice && preInstallNoticeVisible ? (
<p className="mt-3 text-[12px] leading-snug text-muted-foreground">
{preInstallNotice}
</p>
) : null}
</div>
{footer ? (
<div
className={cn('border-t border-border/60', terminalOpen ? 'mt-2 pt-4' : 'mt-5 pt-5')}
>
{footer}
</div>
) : null}
</div>
{terminalOpen ? (
<div className={cn(variant === 'card' ? 'px-5 pb-5' : 'mt-2')}>
@@ -0,0 +1,110 @@
import { Fragment } from 'react'
import { Copy, type LucideIcon } from 'lucide-react'
import { toast } from 'sonner'
import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import type { OrchestrationUsageExample } from '@/lib/orchestration-usage-examples'
const ORCHESTRATION_SKILL_SLASH_COMMAND = `/${ORCHESTRATION_SKILL_NAME}`
function OrchestrationExamplePromptText(props: { prompt: string }): React.JSX.Element {
const { prompt } = props
const parts = prompt.split(ORCHESTRATION_SKILL_SLASH_COMMAND)
if (parts.length === 1) {
return <>{prompt}</>
}
return (
<>
{parts.map((part, index) => (
<Fragment key={index}>
{part}
{index < parts.length - 1 ? (
<span className="font-semibold text-foreground">
{ORCHESTRATION_SKILL_SLASH_COMMAND}
</span>
) : null}
</Fragment>
))}
</>
)
}
export function OrchestrationExampleDialog(props: {
example: OrchestrationUsageExample
icon?: LucideIcon
open: boolean
onOpenChange: (open: boolean) => void
}): React.JSX.Element {
const { example, icon: Icon, open, onOpenChange } = props
const copyPrompt = async (prompt: string): Promise<void> => {
try {
await window.api.ui.writeClipboardText(prompt)
toast.success('Copied example prompt.')
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to copy prompt.')
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-[560px]">
<div className="px-6 pt-6 pr-14">
<DialogHeader className="gap-3">
<div className="flex items-start gap-3">
{Icon ? (
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/30 text-muted-foreground">
<Icon className="size-4" />
</div>
) : null}
<div className="min-w-0 space-y-1.5">
<DialogTitle className="text-base leading-snug">{example.title}</DialogTitle>
<DialogDescription className="text-xs leading-relaxed">
{example.summary}
</DialogDescription>
</div>
</div>
</DialogHeader>
</div>
<div className="px-6 py-5">
<div className="group relative rounded-md border border-border/70 bg-editor-surface shadow-xs">
<p className="px-3 py-3 pr-11 font-mono text-[12px] leading-relaxed text-foreground">
<OrchestrationExamplePromptText prompt={example.prompt} />
</p>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute top-2 right-2 shrink-0 opacity-70 transition-opacity group-hover:opacity-100"
aria-label={`Copy ${example.title} example prompt`}
onClick={() => void copyPrompt(example.prompt)}
>
<Copy className="size-3.5" />
</Button>
</div>
</div>
<DialogFooter className="gap-2 border-t border-border/60 bg-muted/10 px-6 py-4">
<Button type="button" variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
Done
</Button>
<Button type="button" size="sm" onClick={() => void copyPrompt(example.prompt)}>
<Copy className="size-4" />
Copy prompt
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -1,25 +1,61 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { ORCHESTRATION_USAGE_EXAMPLES } from '@/lib/orchestration-usage-examples'
import { OrchestrationPane } from './OrchestrationPane'
vi.mock('@/hooks/useInstalledAgentSkills', () => ({
GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['global'],
GLOBAL_AGENT_SKILL_SOURCE_KINDS: ['home'],
useInstalledAgentSkill: () => ({
installed: true,
loading: false,
error: null,
skills: [
{
id: 'claude',
name: 'orchestration',
description: null,
providers: ['claude'],
sourceKind: 'home',
sourceLabel: 'Claude home',
rootPath: '/Users/test/.claude/skills',
directoryPath: '/Users/test/.claude/skills/orchestration',
skillFilePath: '/Users/test/.claude/skills/orchestration/SKILL.md',
installed: true,
fileCount: 1,
updatedAt: null
}
],
refresh: vi.fn()
})
}))
vi.mock('@/hooks/useDetectedAgents', () => ({
useDetectedAgents: () => ({
detectedIds: ['claude', 'codex', 'gemini'],
isLoading: false,
isRefreshing: false,
refresh: vi.fn()
})
}))
describe('OrchestrationPane', () => {
it('shows skill install status without a separate enable switch', () => {
it('keeps skill setup visible after install and shows agent coverage plus examples', () => {
const markup = renderToStaticMarkup(<OrchestrationPane />)
expect(markup).toContain('Orchestration skill')
expect(markup).toContain('Installed')
expect(markup).not.toContain('rounded-xl')
expect(markup).not.toMatch(/<button\b[^>]*>[\s\S]*?Install[\s\S]*?<\/button>/)
expect(markup).not.toContain('role="switch"')
expect(markup).toContain('Agent coverage')
expect(markup).toContain('Copy install command')
expect(markup).toContain('detected agents')
expect(markup).toContain('Gemini')
expect(markup).toContain('Ready')
expect(markup).toContain('How to use it')
expect(markup).not.toContain('See examples')
expect(ORCHESTRATION_USAGE_EXAMPLES).toHaveLength(5)
for (const example of ORCHESTRATION_USAGE_EXAMPLES) {
expect(markup).toContain(example.title)
}
expect(markup).toMatch(/<button\b[^>]*>[\s\S]*?Update[\s\S]*?<\/button>/)
expect(markup).toContain('Re-check')
})
})
@@ -1,10 +1,12 @@
import { Workflow } from 'lucide-react'
import { useState } from 'react'
import { ArrowRightLeft, GitBranch, ListChecks, Workflow } from 'lucide-react'
import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands'
import {
AGENT_SKILL_CLI_PREREQUISITE_NOTICE,
ensureOrcaCliAvailableForAgentSkillTerminal
} from '@/lib/agent-skill-cli-prerequisite'
import { ORCHESTRATION_SKILL_INSTALL_COMMAND } from '@/lib/orchestration-install-command'
import { ORCHESTRATION_USAGE_EXAMPLES } from '@/lib/orchestration-usage-examples'
import {
GLOBAL_AGENT_SKILL_SOURCE_KINDS,
useInstalledAgentSkill
@@ -14,15 +16,29 @@ import { matchesSettingsSearch } from './settings-search'
import { useAppStore } from '../../store'
import { ORCHESTRATION_PANE_SEARCH_ENTRIES } from './orchestration-search'
import { AgentSkillSetupPanel } from './AgentSkillSetupPanel'
import { OrchestrationSkillAgentCoverage } from './OrchestrationSkillAgentCoverage'
import { OrchestrationExampleDialog } from './OrchestrationExamplesDialog'
import { OrchestrationSkillPromptDialog } from './OrchestrationSkillPromptDialog'
const EXAMPLE_ICONS = {
handoff: ArrowRightLeft,
'worktree-handoff': ArrowRightLeft,
'child-sequence': ListChecks,
'child-parallel': GitBranch,
'child-worktrees': Workflow
} as const
export function OrchestrationPane(): React.JSX.Element {
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const showOrchestration = matchesSettingsSearch(searchQuery, ORCHESTRATION_PANE_SEARCH_ENTRIES)
const [selectedExampleId, setSelectedExampleId] = useState<string | null>(null)
const [skillPromptOpen, setSkillPromptOpen] = useState(false)
const {
installed: orchestrationSkillDetected,
loading: orchestrationSkillLoading,
error: orchestrationSkillError,
skills: discoveredSkills,
refresh: refreshOrchestrationSkill
} = useInstalledAgentSkill(ORCHESTRATION_SKILL_NAME, {
sourceKinds: GLOBAL_AGENT_SKILL_SOURCE_KINDS
@@ -35,12 +51,11 @@ export function OrchestrationPane(): React.JSX.Element {
return (
<SearchableSetting
title="Agent Orchestration"
description="Coordinate multiple coding agents via messaging, task DAGs, dispatch, and decision gates."
description="Coordinate coding agents across handoffs, worktree handovers, and child-agent work."
keywords={ORCHESTRATION_PANE_SEARCH_ENTRIES[0].keywords}
className="space-y-3 py-2"
className="space-y-5 py-2"
>
<AgentSkillSetupPanel
variant="inline"
title="Orchestration skill"
description="Enables agents to hand off context and coordinate work through Orca."
command={ORCHESTRATION_SKILL_INSTALL_COMMAND}
@@ -56,9 +71,82 @@ export function OrchestrationPane(): React.JSX.Element {
useAppStore.getState().recordFeatureInteraction('agent-orchestration-setup')
await ensureOrcaCliAvailableForAgentSkillTerminal()
}}
showInstallWhenInstalled={false}
actionHint={
<p className="text-[12px] leading-snug text-muted-foreground">
Prefer your own terminal?{' '}
<button
type="button"
className="font-medium text-foreground underline-offset-2 hover:underline"
onClick={() => setSkillPromptOpen(true)}
>
Copy install command
</button>
</p>
}
footer={
<OrchestrationSkillAgentCoverage
embedded
skills={discoveredSkills}
loading={orchestrationSkillLoading}
/>
}
onRecheck={refreshOrchestrationSkill}
/>
<OrchestrationSkillPromptDialog
command={ORCHESTRATION_SKILL_INSTALL_COMMAND}
open={skillPromptOpen}
onOpenChange={setSkillPromptOpen}
/>
<div className="space-y-4 border-t border-border/60 pt-6">
<div className="space-y-3">
<h3 className="text-sm font-medium text-foreground">How to use it</h3>
<p className="text-xs text-muted-foreground">
Ask a coordinator agent to use orchestration for handoffs, worktree handovers, and
sequential or parallel child agents.
</p>
</div>
<div className="grid gap-2 sm:grid-cols-2">
{ORCHESTRATION_USAGE_EXAMPLES.map((example) => {
const Icon = EXAMPLE_ICONS[example.id as keyof typeof EXAMPLE_ICONS] ?? Workflow
return (
<button
key={example.id}
type="button"
className="rounded-md border border-border/60 bg-muted/20 px-4 py-3 text-left transition-colors hover:bg-muted/35 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
onClick={() => setSelectedExampleId(example.id)}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-background text-muted-foreground">
<Icon className="size-4" />
</div>
<div className="min-w-0 space-y-1">
<p className="text-sm font-medium text-foreground">{example.title}</p>
<p className="text-xs leading-relaxed text-muted-foreground">
{example.summary}
</p>
</div>
</div>
</button>
)
})}
</div>
</div>
{ORCHESTRATION_USAGE_EXAMPLES.map((example) => {
const Icon = EXAMPLE_ICONS[example.id as keyof typeof EXAMPLE_ICONS] ?? Workflow
return (
<OrchestrationExampleDialog
key={`${example.id}-dialog`}
example={example}
icon={Icon}
open={selectedExampleId === example.id}
onOpenChange={(open) => setSelectedExampleId(open ? example.id : null)}
/>
)
})}
</SearchableSetting>
)
}
@@ -33,7 +33,6 @@ export function OrchestrationSetupCard(props: {
useAppStore.getState().recordFeatureInteraction('agent-orchestration-setup')
await ensureOrcaCliAvailableForAgentSkillTerminal()
}}
showRecheckWhenInstalled={false}
onRecheck={skill.refresh}
/>
)
@@ -0,0 +1,44 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { OrchestrationSkillAgentCoverage } from './OrchestrationSkillAgentCoverage'
vi.mock('@/hooks/useDetectedAgents', () => ({
useDetectedAgents: () => ({
detectedIds: ['claude', 'codex'],
isLoading: false,
isRefreshing: false,
refresh: vi.fn()
})
}))
describe('OrchestrationSkillAgentCoverage', () => {
it('shows each detected agent with an explicit skill status', () => {
const markup = renderToStaticMarkup(
<OrchestrationSkillAgentCoverage
loading={false}
skills={[
{
id: 'claude-skill',
name: 'orchestration',
description: null,
providers: ['claude'],
sourceKind: 'home',
sourceLabel: 'Claude home',
rootPath: '/Users/test/.claude/skills',
directoryPath: '/Users/test/.claude/skills/orchestration',
skillFilePath: '/Users/test/.claude/skills/orchestration/SKILL.md',
installed: true,
fileCount: 1,
updatedAt: null
}
]}
/>
)
expect(markup).toContain('Claude')
expect(markup).toContain('Codex')
expect(markup).toContain('Ready')
expect(markup).toContain('Missing')
expect(markup).not.toContain('View details')
})
})
@@ -0,0 +1,104 @@
import type { DiscoveredSkill } from '../../../../shared/skills'
import type { OrchestrationSkillAgentStatus } from '@/lib/orchestration-skill-coverage'
import { AgentIcon } from '@/lib/agent-catalog'
import { useDetectedAgents } from '@/hooks/useDetectedAgents'
import { getOrchestrationSkillAgentStatuses } from '@/lib/orchestration-skill-coverage'
import { cn } from '@/lib/utils'
function getAgentCoverageSummary(props: {
loading: boolean
totalCount: number
installedCount: number
fullCoverage: boolean
noCoverage: boolean
}): string {
const { loading, totalCount, installedCount, fullCoverage, noCoverage } = props
if (loading) {
return 'Checking installed agents and skill paths…'
}
if (totalCount === 0) {
return 'No agent CLIs detected on PATH. Install agents in Settings → Agents, then re-check.'
}
if (fullCoverage) {
return `All ${totalCount} detected agents have the skill.`
}
if (noCoverage) {
return 'Install the skill above, then re-check.'
}
return `${installedCount} of ${totalCount} detected agents have the skill.`
}
function AgentCoverageChip({
status
}: {
status: OrchestrationSkillAgentStatus
}): React.JSX.Element {
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs',
status.installed
? 'border-status-success-border bg-status-success-background text-foreground'
: 'border-border/60 bg-muted/20 text-muted-foreground'
)}
>
<AgentIcon agent={status.agent} size={12} />
<span className="font-medium text-foreground">{status.label}</span>
<span
className={cn(
'text-[10px] font-medium',
status.installed ? 'text-status-success' : 'text-muted-foreground'
)}
>
{status.installed ? 'Ready' : 'Missing'}
</span>
</span>
)
}
export function OrchestrationSkillAgentCoverage(props: {
skills: readonly DiscoveredSkill[]
loading: boolean
embedded?: boolean
className?: string
}): React.JSX.Element {
const { skills, loading: skillsLoading, embedded = false, className } = props
const { detectedIds, isLoading: agentsLoading } = useDetectedAgents()
const loading = skillsLoading || agentsLoading || detectedIds === null
const agentStatuses = getOrchestrationSkillAgentStatuses(skills, detectedIds ?? [])
const installedCount = agentStatuses.filter((status) => status.installed).length
const totalCount = agentStatuses.length
const fullCoverage = !loading && totalCount > 0 && installedCount === totalCount
const noCoverage = !loading && totalCount > 0 && installedCount === 0
const showAgentChips = !loading && totalCount > 0 && !fullCoverage
const summary = getAgentCoverageSummary({
loading,
totalCount,
installedCount,
fullCoverage,
noCoverage
})
return (
<div
className={cn(
embedded ? 'space-y-2.5' : 'space-y-4 border-t border-border/60 pt-6',
className
)}
>
<div className="space-y-1">
<h3 className="text-sm font-medium text-foreground">Agent coverage</h3>
<p className="text-xs leading-relaxed text-muted-foreground">{summary}</p>
</div>
{showAgentChips ? (
<div className="flex flex-wrap gap-1.5">
{agentStatuses.map((status) => (
<AgentCoverageChip key={status.agent} status={status} />
))}
</div>
) : null}
</div>
)
}
@@ -0,0 +1,73 @@
import { Copy } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
export function OrchestrationSkillPromptDialog(props: {
command: string
open: boolean
onOpenChange: (open: boolean) => void
}): React.JSX.Element {
const { command, open, onOpenChange } = props
const copyCommand = async (): Promise<void> => {
try {
await window.api.ui.writeClipboardText(command)
toast.success('Copied install command.')
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to copy install command.')
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-[560px]">
<div className="px-6 pt-6 pr-14">
<DialogHeader className="gap-2">
<DialogTitle className="text-base leading-snug">
Install orchestration skill
</DialogTitle>
<DialogDescription className="text-xs leading-relaxed">
Run this command in a terminal to install the orchestration skill for your agents.
</DialogDescription>
</DialogHeader>
</div>
<div className="px-6 py-5">
<div className="group relative rounded-md border border-border/70 bg-editor-surface shadow-xs">
<p className="px-3 py-3 pr-11 font-mono text-[12px] leading-relaxed break-all text-foreground">
{command}
</p>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="absolute top-2 right-2 shrink-0 opacity-70 transition-opacity group-hover:opacity-100"
aria-label="Copy orchestration skill install command"
onClick={() => void copyCommand()}
>
<Copy className="size-3.5" />
</Button>
</div>
</div>
<DialogFooter className="gap-2 border-t border-border/60 bg-muted/10 px-6 py-4">
<Button type="button" variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
Done
</Button>
<Button type="button" size="sm" onClick={() => void copyCommand()}>
<Copy className="size-4" />
Copy command
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -15,7 +15,12 @@ export const ORCHESTRATION_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
'task',
'DAG',
'worker',
'coordinator'
'coordinator',
'claude',
'codex',
'examples',
'handoff',
'child agents'
]
}
]
@@ -28,6 +28,7 @@ export type InstalledAgentSkillState = {
installed: boolean
loading: boolean
error: string | null
skills: readonly DiscoveredSkill[]
refresh: () => Promise<void>
}
@@ -227,10 +228,11 @@ export function useInstalledAgentSkill(
}
}, [enabled, refresh])
const skills = useMemo(() => (enabled && result ? result.skills : []), [enabled, result])
const installed = useMemo(
() =>
enabled && result ? hasInstalledAgentSkill(result.skills, skillName, { sourceKinds }) : false,
[enabled, result, skillName, sourceKinds]
() => (enabled ? hasInstalledAgentSkill(skills, skillName, { sourceKinds }) : false),
[enabled, skills, skillName, sourceKinds]
)
useEffect(() => {
@@ -247,6 +249,7 @@ export function useInstalledAgentSkill(
installed,
loading,
error,
skills,
refresh: forceRefresh
}
}
@@ -0,0 +1,112 @@
import { describe, expect, it } from 'vitest'
import type { DiscoveredSkill } from '../../../shared/skills'
import {
agentHasOrchestrationSkill,
getOrchestrationSkillAgentStatuses
} from './orchestration-skill-coverage'
function skill(overrides: Partial<DiscoveredSkill>): DiscoveredSkill {
return {
id: 'skill-1',
name: 'orchestration',
description: null,
providers: ['agent-skills'],
sourceKind: 'home',
sourceLabel: 'Agent skills home',
rootPath: '/Users/test/.agents/skills',
directoryPath: '/Users/test/.agents/skills/orchestration',
skillFilePath: '/Users/test/.agents/skills/orchestration/SKILL.md',
installed: true,
fileCount: 1,
updatedAt: null,
...overrides
}
}
describe('orchestration skill agent coverage', () => {
it('marks shared-path agents from the global ~/.agents/skills install', () => {
const skills = [
skill({
providers: ['agent-skills'],
sourceKind: 'home',
rootPath: '/Users/test/.agents/skills',
directoryPath: '/Users/test/.agents/skills/orchestration'
})
]
expect(getOrchestrationSkillAgentStatuses(skills, ['codex', 'gemini', 'droid'])).toEqual([
{ agent: 'codex', label: 'Codex', installed: true },
{ agent: 'gemini', label: 'Gemini', installed: true },
{ agent: 'droid', label: 'Droid', installed: true }
])
})
it('marks Claude from ~/.claude/skills without requiring a dedicated Codex path', () => {
const skills = [
skill({
providers: ['claude'],
sourceKind: 'home',
rootPath: '/Users/test/.claude/skills',
directoryPath: '/Users/test/.claude/skills/orchestration'
})
]
expect(agentHasOrchestrationSkill('claude', skills)).toBe(true)
expect(agentHasOrchestrationSkill('codex', skills)).toBe(false)
expect(agentHasOrchestrationSkill('gemini', skills)).toBe(false)
})
it('marks Codex from plugin cache installs', () => {
expect(
agentHasOrchestrationSkill('codex', [
skill({
providers: ['codex', 'agent-skills'],
sourceKind: 'plugin',
sourceLabel: 'Codex plugin cache',
rootPath: '/Users/test/.codex/plugins/cache',
directoryPath: '/Users/test/.codex/plugins/cache/vendor/orchestration'
})
])
).toBe(true)
})
it('ignores repo-scoped orchestration installs', () => {
expect(
agentHasOrchestrationSkill('gemini', [
skill({
providers: ['agent-skills'],
sourceKind: 'repo',
rootPath: '/workspace/.agents/skills',
directoryPath: '/workspace/.agents/skills/orchestration'
})
])
).toBe(false)
})
it('matches orchestration by directory name when frontmatter uses a display name', () => {
expect(
agentHasOrchestrationSkill('claude', [
skill({
name: 'Orca Orchestration',
providers: ['claude'],
sourceKind: 'home',
rootPath: '/Users/test/.claude/skills',
directoryPath: '/Users/test/.claude/skills/orchestration'
})
])
).toBe(true)
})
it('matches Windows skill paths', () => {
expect(
agentHasOrchestrationSkill('codex', [
skill({
providers: ['codex'],
sourceKind: 'home',
rootPath: 'C:\\Users\\test\\.codex\\skills',
directoryPath: 'C:\\Users\\test\\.codex\\skills\\orchestration'
})
])
).toBe(true)
})
})
@@ -0,0 +1,143 @@
import type { DiscoveredSkill } from '../../../shared/skills'
import type { TuiAgent } from '../../../shared/types'
import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands'
import { getAgentLabel } from '@/lib/agent-catalog'
import { TUI_AGENT_AUTO_PICK_ORDER } from '../../../shared/tui-agent-selection'
export type OrchestrationSkillLocationId =
| 'claude-home'
| 'codex-home'
| 'codex-plugin-cache'
| 'agents-home'
export type OrchestrationSkillAgentStatus = {
agent: TuiAgent
label: string
installed: boolean
}
type OrchestrationSkillLocationDefinition = {
id: OrchestrationSkillLocationId
matchesSkill: (skill: DiscoveredSkill) => boolean
}
const ORCHESTRATION_SKILL_LOCATIONS: readonly OrchestrationSkillLocationDefinition[] = [
{
id: 'claude-home',
matchesSkill: (skill) =>
isGlobalOrchestrationSkill(skill) &&
pathContainsSegments(skill.rootPath, ['.claude', 'skills'])
},
{
id: 'codex-home',
matchesSkill: (skill) =>
isGlobalOrchestrationSkill(skill) &&
pathContainsSegments(skill.rootPath, ['.codex', 'skills'])
},
{
id: 'codex-plugin-cache',
matchesSkill: (skill) =>
isGlobalOrchestrationSkill(skill) &&
pathContainsSegments(skill.rootPath, ['.codex', 'plugins', 'cache'])
},
{
id: 'agents-home',
matchesSkill: (skill) =>
isGlobalOrchestrationSkill(skill) &&
pathContainsSegments(skill.rootPath, ['.agents', 'skills'])
}
]
const ORCHESTRATION_SKILL_LOCATION_IDS_BY_AGENT: Partial<
Record<TuiAgent, readonly OrchestrationSkillLocationId[]>
> = {
claude: ['claude-home', 'agents-home'],
openclaude: ['claude-home', 'agents-home'],
codex: ['codex-home', 'codex-plugin-cache', 'agents-home']
}
function normalizeSkillName(value: string): string {
return value.trim().toLowerCase()
}
function basenameFromPath(pathValue: string): string {
return pathValue.split(/[\\/]/).filter(Boolean).at(-1) ?? pathValue
}
function normalizePath(pathValue: string): string {
return pathValue.replace(/\\/g, '/').toLowerCase()
}
function pathContainsSegments(pathValue: string, segments: readonly string[]): boolean {
const parts = normalizePath(pathValue).split('/').filter(Boolean)
const target = segments.map((segment) => segment.toLowerCase())
if (target.length === 0 || parts.length < target.length) {
return false
}
for (let index = 0; index <= parts.length - target.length; index += 1) {
if (target.every((segment, offset) => parts[index + offset] === segment)) {
return true
}
}
return false
}
function isOrchestrationSkill(skill: DiscoveredSkill): boolean {
if (!skill.installed) {
return false
}
const expected = normalizeSkillName(ORCHESTRATION_SKILL_NAME)
return (
normalizeSkillName(skill.name) === expected ||
normalizeSkillName(basenameFromPath(skill.directoryPath)) === expected
)
}
function isGlobalOrchestrationSkill(skill: DiscoveredSkill): boolean {
return isOrchestrationSkill(skill) && skill.sourceKind !== 'repo'
}
function getOrchestrationSkillLocationIdsForAgent(
agent: TuiAgent
): readonly OrchestrationSkillLocationId[] {
return ORCHESTRATION_SKILL_LOCATION_IDS_BY_AGENT[agent] ?? ['agents-home']
}
function isOrchestrationSkillInstalledAtLocation(
skills: readonly DiscoveredSkill[],
locationId: OrchestrationSkillLocationId
): boolean {
const location = ORCHESTRATION_SKILL_LOCATIONS.find((entry) => entry.id === locationId)
if (!location) {
return false
}
return skills.some((skill) => location.matchesSkill(skill))
}
export function agentHasOrchestrationSkill(
agent: TuiAgent,
skills: readonly DiscoveredSkill[]
): boolean {
return getOrchestrationSkillLocationIdsForAgent(agent).some((locationId) =>
isOrchestrationSkillInstalledAtLocation(skills, locationId)
)
}
export function sortOrchestrationAgents(agents: readonly TuiAgent[]): TuiAgent[] {
const order = new Map(TUI_AGENT_AUTO_PICK_ORDER.map((agent, index) => [agent, index]))
return [...agents].sort(
(left, right) =>
(order.get(left) ?? Number.MAX_SAFE_INTEGER) - (order.get(right) ?? Number.MAX_SAFE_INTEGER)
)
}
export function getOrchestrationSkillAgentStatuses(
skills: readonly DiscoveredSkill[],
detectedAgents: readonly TuiAgent[]
): OrchestrationSkillAgentStatus[] {
return sortOrchestrationAgents(detectedAgents).map((agent) => ({
agent,
label: getAgentLabel(agent),
installed: agentHasOrchestrationSkill(agent, skills)
}))
}
@@ -0,0 +1,44 @@
export type OrchestrationUsageExample = {
id: string
title: string
summary: string
prompt: string
}
export const ORCHESTRATION_USAGE_EXAMPLES: readonly OrchestrationUsageExample[] = [
{
id: 'handoff',
title: 'Hand off an active task',
summary: 'Move ownership to another agent with enough context to continue.',
prompt:
'Use /orchestration to hand this billing settings task to the idle Claude agent. Include the goal, current context, and what they should finish next.'
},
{
id: 'worktree-handoff',
title: 'Hand off to another worktree',
summary: 'Move work to an agent that is already running in a different branch.',
prompt:
'Use /orchestration to hand this settings cleanup to the agent in the settings-polish worktree. Send the goal, relevant files, and expected result.'
},
{
id: 'child-sequence',
title: 'Run a phased workflow',
summary: 'Use child agents one after another when each phase depends on the last.',
prompt:
'Use /orchestration to run this auth refactor in phases: plan, backend, UI, then tests. Start each child agent after the previous phase is done.'
},
{
id: 'child-parallel',
title: 'Run independent work in parallel',
summary: 'Split non-overlapping investigation or implementation tasks across child agents.',
prompt:
'Use /orchestration to split this auth refactor across parallel child agents: API contract, backend call sites, UI flow, and test gaps.'
},
{
id: 'child-worktrees',
title: 'Split a large change into smaller PRs',
summary: 'Give each child agent its own worktree so parallel implementation stays reviewable.',
prompt:
'Use /orchestration to split this onboarding update into smaller PRs, each in its own child worktree: setup state, settings UI, copy, and tests.'
}
]