Add Claude Agent Teams native pane launcher (#4892)

* Add Claude Agent Teams native pane launcher

Co-authored-by: Orca <help@stably.ai>

* Fix Agent Teams CI coverage checks

Co-authored-by: Orca <help@stably.ai>

* Fix Claude Agent Teams split direction mapping

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-06-08 16:11:32 -04:00
committed by GitHub
co-authored by Orca
parent 9b819ce43f
commit 7ea359bd60
43 changed files with 1953 additions and 30 deletions
+63
View File
@@ -1,7 +1,41 @@
import { spawn } from 'child_process'
import type { CommandHandler } from '../dispatch'
import { formatCliStatus, formatStatus, printResult } from '../format'
import { RuntimeClientError, serveOrcaApp } from '../runtime-client'
function envRecord(): Record<string, string> {
return Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)
)
}
function withTeammateModeAuto(args: string[]): string[] {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
if (arg === '--teammate-mode' || arg.startsWith('--teammate-mode=')) {
return args
}
}
return ['--teammate-mode', 'auto', ...args]
}
async function runClaudeAgentTeams(env: Record<string, string>, args: string[]): Promise<number> {
return await new Promise((resolve, reject) => {
const child = spawn('claude', withTeammateModeAuto(args), {
stdio: 'inherit',
env
})
child.once('error', reject)
child.once('exit', (code, signal) => {
if (typeof code === 'number') {
resolve(code)
return
}
resolve(signal ? 1 : 0)
})
})
}
function getOptionalServePort(flags: Map<string, string | boolean>): string | null {
if (!flags.has('port')) {
return null
@@ -18,6 +52,35 @@ function getOptionalServePort(flags: Map<string, string | boolean>): string | nu
}
export const CORE_HANDLERS: Record<string, CommandHandler> = {
'claude-teams': async ({ client }) => {
if (process.platform === 'win32') {
throw new RuntimeClientError(
'unsupported_platform',
'Claude Agent Teams native panes are not supported on Windows.'
)
}
const paneKey = process.env.ORCA_PANE_KEY
if (!paneKey) {
throw new RuntimeClientError(
'invalid_environment',
'orca claude-teams must be run inside an Orca terminal.'
)
}
const response = await client.call<{ launch: { env: Record<string, string> } }>(
'agentTeams.prepareLaunch',
{
paneKey,
env: envRecord()
}
)
process.exitCode = await runClaudeAgentTeams(
{
...envRecord(),
...response.result.launch.env
},
[]
)
},
open: async ({ client, json }) => {
const result = await client.openOrca()
printResult(result, json, formatCliStatus)
+52 -2
View File
@@ -7,13 +7,15 @@ const {
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn()
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', () => {
@@ -79,6 +81,17 @@ vi.mock('./runtime/environments', () => ({
resolveEnvironment: vi.fn()
}))
vi.mock('child_process', async () => {
const { EventEmitter } = await import('events')
return {
spawn: spawnMock.mockImplementation(() => {
const child = new EventEmitter()
process.nextTick(() => child.emit('exit', 0, null))
return child
})
}
})
import {
buildCurrentWorktreeSelector,
COMMAND_SPECS,
@@ -147,6 +160,7 @@ describe('orca cli worktree awareness', () => {
getDefaultUserDataPathMock.mockClear()
addEnvironmentFromPairingCodeMock.mockReset()
listEnvironmentsMock.mockReset()
spawnMock.mockClear()
addEnvironmentFromPairingCodeMock.mockReturnValue({
id: 'env-1',
name: 'desk',
@@ -240,6 +254,42 @@ describe('orca cli worktree awareness', () => {
expect(logSpy).toHaveBeenCalledTimes(1)
})
it.skipIf(process.platform === 'win32')(
'prepares and starts Claude Agent Teams in the current Orca terminal',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(['claude-teams'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('agentTeams.prepareLaunch', {
paneKey: 'tab-1:11111111-1111-4111-8111-111111111111',
env: expect.objectContaining({
ORCA_PANE_KEY: 'tab-1:11111111-1111-4111-8111-111111111111'
})
})
expect(spawnMock).toHaveBeenCalledWith('claude', ['--teammate-mode', 'auto'], {
stdio: 'inherit',
env: expect.objectContaining({
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX_PANE: '%1'
})
})
}
)
it('rejects remote `worktree current` without listing worktrees from client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+29
View File
@@ -23,6 +23,10 @@ function shouldIgnoreRemoteSelection(commandPath: string[]): boolean {
}
export async function main(argv = process.argv.slice(2), cwd = process.cwd()): Promise<void> {
if (argv[0] === 'agent-teams-tmux') {
await runAgentTeamsTmuxShim(argv.slice(1))
return
}
const parsed = normalizeCommandPositionals(COMMAND_SPECS, parseArgs(argv))
const helpPath = resolveHelpPath(parsed)
if (helpPath !== null) {
@@ -76,6 +80,31 @@ export async function main(argv = process.argv.slice(2), cwd = process.cwd()): P
}
}
async function runAgentTeamsTmuxShim(argv: string[]): Promise<void> {
try {
const client = new RuntimeClient(undefined, 10_000)
const response = await client.call<{
tmux: { stdout: string; stderr: string; exitCode: number }
}>(
'agentTeams.tmuxCompat',
{
teamId: process.env.ORCA_AGENT_TEAMS_TEAM_ID,
token: process.env.ORCA_AGENT_TEAMS_TOKEN,
envPane: process.env.TMUX_PANE,
cwd: process.cwd(),
argv
},
{ timeoutMs: 10_000 }
)
process.stdout.write(response.result.tmux.stdout)
process.stderr.write(response.result.tmux.stderr)
process.exitCode = response.result.tmux.exitCode
} catch (error) {
reportCliError(error, false, { commandPath: ['agent-teams-tmux'] })
process.exitCode = 1
}
}
if (require.main === module) {
void main()
}
+10
View File
@@ -35,6 +35,16 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
allowedFlags: [...GLOBAL_FLAGS],
examples: ['orca status', 'orca status --json']
},
{
path: ['claude-teams'],
summary: 'Start Claude Code Agent Teams in the current Orca terminal',
usage: 'orca claude-teams',
allowedFlags: [...GLOBAL_FLAGS],
notes: [
'Must be run from inside an Orca terminal. Starts Claude Code Agent Teams in the current pane and opens teammates as native Orca splits.'
],
examples: ['orca claude-teams']
},
{
path: ['repo', 'list'],
summary: 'List repos registered in Orca',