mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
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:
@@ -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
@@ -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(() => {})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user