Add multi-repo folder workspaces (v1) (#5172)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-06-11 02:34:01 -07:00
committed by GitHub
co-authored by Orca
parent 7fbb3003f2
commit 91e354ccb1
99 changed files with 8285 additions and 761 deletions
+211
View File
@@ -1,11 +1,14 @@
/* eslint-disable max-lines -- Why: runtime behavior is stateful and cross-cutting, so these tests stay in one file to preserve the end-to-end invariants around handles, waits, and graph sync. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import { EventEmitter } from 'events'
import { randomUUID } from 'crypto'
import { lstat, mkdir, mkdtemp, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { ipcMain } from 'electron'
import type {
FolderWorkspace,
ProjectGroup,
TerminalLayoutSnapshot,
WorktreeLineage,
WorktreeMeta,
@@ -506,6 +509,10 @@ const TEST_REPO_ID = 'repo-1'
const TEST_REPO_PATH = '/tmp/repo'
const TEST_WORKTREE_PATH = '/tmp/worktree-a'
const TEST_WORKTREE_ID = `${TEST_REPO_ID}::${TEST_WORKTREE_PATH}`
const TEST_FOLDER_PROJECT_GROUP_ID = 'folder-project-group-1'
const TEST_FOLDER_WORKSPACE_ID = 'folder-workspace-1'
const TEST_FOLDER_WORKSPACE_KEY = `folder:${TEST_FOLDER_WORKSPACE_ID}`
const TEST_FOLDER_WORKSPACE_PATH = '/tmp/platform'
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
const HEADLESS_LEAF_ID = '11111111-1111-4111-8111-111111111111'
const HEADLESS_SECOND_LEAF_ID = '22222222-2222-4222-8222-222222222222'
@@ -623,6 +630,52 @@ function createRuntime(): OrcaRuntimeService {
return new OrcaRuntimeService(store)
}
function makeFolderProjectGroup(overrides: Partial<ProjectGroup> = {}): ProjectGroup {
return {
id: TEST_FOLDER_PROJECT_GROUP_ID,
name: 'Platform',
parentPath: TEST_FOLDER_WORKSPACE_PATH,
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1,
...overrides
}
}
function makeFolderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWorkspace {
return {
...overrides,
id: overrides.id ?? TEST_FOLDER_WORKSPACE_ID,
projectGroupId: overrides.projectGroupId ?? TEST_FOLDER_PROJECT_GROUP_ID,
name: overrides.name ?? 'Refund fix',
folderPath: overrides.folderPath ?? TEST_FOLDER_WORKSPACE_PATH,
linkedTask: overrides.linkedTask ?? null,
comment: overrides.comment ?? '',
isArchived: overrides.isArchived ?? false,
isUnread: overrides.isUnread ?? false,
isPinned: overrides.isPinned ?? false,
sortOrder: overrides.sortOrder ?? 0,
lastActivityAt: overrides.lastActivityAt ?? 1,
createdAt: overrides.createdAt ?? 1,
updatedAt: overrides.updatedAt ?? 1
}
}
function createFolderWorkspaceRuntimeStore(
folderWorkspace: FolderWorkspace = makeFolderWorkspace(),
projectGroup: ProjectGroup = makeFolderProjectGroup()
) {
return {
...store,
getProjectGroups: () => [projectGroup],
getFolderWorkspaces: () => [folderWorkspace]
}
}
function makeRpcRequest(method: string, params?: unknown): RpcRequest {
return { id: 'req-1', authToken: 'tok', method, params }
}
@@ -3952,6 +4005,86 @@ describe('OrcaRuntimeService', () => {
})
})
it.each([
{ label: 'canonical folder workspace selector', selector: TEST_FOLDER_WORKSPACE_KEY },
{ label: 'id-prefixed folder workspace selector', selector: `id:${TEST_FOLDER_WORKSPACE_KEY}` }
])('creates background terminal sessions for a $label', async ({ selector }) => {
const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-workspace-'))
const spawn = vi.fn().mockResolvedValue({ id: 'pty-folder' })
const folderWorkspace = makeFolderWorkspace({ folderPath })
const projectGroup = makeFolderProjectGroup({ parentPath: folderPath })
const runtime = new OrcaRuntimeService(
createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never
)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await expect(
runtime.createTerminal(selector, {
command: 'codex',
title: 'multi-repo worker'
})
).resolves.toMatchObject({
worktreeId: TEST_FOLDER_WORKSPACE_KEY,
title: 'multi-repo worker',
surface: 'background'
})
const spawnCall = spawn.mock.calls[0]?.[0] as
| { cwd?: string; env?: Record<string, string>; worktreeId?: string }
| undefined
const spawnedEnv = spawnCall?.env ?? {}
expect(spawnCall).toMatchObject({
cwd: folderPath,
worktreeId: TEST_FOLDER_WORKSPACE_KEY
})
expectStablePaneKeyEnv(spawnedEnv)
expect(spawnedEnv.ORCA_WORKSPACE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY)
expect(spawnedEnv.ORCA_PROJECT_GROUP_ID).toBe(TEST_FOLDER_PROJECT_GROUP_ID)
expect(spawnedEnv.ORCA_WORKSPACE_ROOT).toBe(folderPath)
expect(spawnedEnv.ORCA_WORKTREE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY)
})
it('rejects folder workspace terminal creation when the backing path is missing', async () => {
const missingPath = join(tmpdir(), `orca-missing-folder-workspace-${randomUUID()}`)
const spawn = vi.fn().mockResolvedValue({ id: 'pty-folder' })
const folderWorkspace = makeFolderWorkspace({ folderPath: missingPath })
const projectGroup = makeFolderProjectGroup({ parentPath: missingPath })
const runtime = new OrcaRuntimeService(
createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never
)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await expect(runtime.createTerminal(TEST_FOLDER_WORKSPACE_KEY)).rejects.toThrow(
'folder_workspace_path_missing'
)
expect(spawn).not.toHaveBeenCalled()
})
it('rejects folder workspace folderPath updates when the new path is missing', async () => {
const missingPath = join(tmpdir(), `orca-missing-folder-update-${randomUUID()}`)
const folderWorkspace = makeFolderWorkspace()
const runtimeStore = {
...createFolderWorkspaceRuntimeStore(folderWorkspace),
updateFolderWorkspace: vi.fn()
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
await expect(
runtime.updateFolderWorkspace(TEST_FOLDER_WORKSPACE_ID, { folderPath: missingPath })
).rejects.toThrow('folder_workspace_path_missing')
expect(runtimeStore.updateFolderWorkspace).not.toHaveBeenCalled()
})
it('enables Claude Agent Teams only for direct Claude launches when configured in-process', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const runtimeStore = {
@@ -4166,6 +4299,84 @@ describe('OrcaRuntimeService', () => {
})
})
it('splits folder workspace pty-backed terminal sessions with folder cwd and env', async () => {
const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-split-'))
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'pty-folder-source' })
.mockResolvedValueOnce({ id: 'pty-folder-split' })
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-folder' })
const folderWorkspace = makeFolderWorkspace({ folderPath })
const projectGroup = makeFolderProjectGroup({ parentPath: folderPath })
const runtime = new OrcaRuntimeService(
createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never
)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
runtime.setNotifier({
worktreesChanged: vi.fn(),
reposChanged: vi.fn(),
activateWorktree: vi.fn(),
createTerminal: vi.fn(),
revealTerminalSession,
splitTerminal: vi.fn(),
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn(),
terminalDriverChanged: vi.fn()
})
runtime.attachWindow(1)
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
const { handle } = await runtime.createTerminal(TEST_FOLDER_WORKSPACE_KEY)
const sourceCall = spawn.mock.calls[0]?.[0] as
| { cwd?: string; env?: Record<string, string>; worktreeId?: string }
| undefined
const sourceEnv = sourceCall?.env ?? {}
const sourceLeafId = sourceEnv.ORCA_PANE_KEY.slice(`${sourceEnv.ORCA_TAB_ID}:`.length)
await expect(runtime.splitTerminal(handle, { direction: 'vertical' })).resolves.toMatchObject({
handle: expect.stringMatching(/^term_/),
tabId: sourceEnv.ORCA_TAB_ID,
paneRuntimeId: -1
})
const splitCall = spawn.mock.calls[1]?.[0] as
| { cwd?: string; env?: Record<string, string>; worktreeId?: string }
| undefined
const splitEnv = splitCall?.env ?? {}
const splitLeafId = splitEnv.ORCA_PANE_KEY.slice(`${sourceEnv.ORCA_TAB_ID}:`.length)
expect(sourceCall).toMatchObject({
cwd: folderPath,
worktreeId: TEST_FOLDER_WORKSPACE_KEY
})
expect(splitCall).toMatchObject({
cwd: folderPath,
worktreeId: TEST_FOLDER_WORKSPACE_KEY
})
expectStablePaneKeyEnv(splitEnv)
expect(splitEnv.ORCA_TAB_ID).toBe(sourceEnv.ORCA_TAB_ID)
expect(splitEnv.ORCA_WORKSPACE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY)
expect(splitEnv.ORCA_PROJECT_GROUP_ID).toBe(TEST_FOLDER_PROJECT_GROUP_ID)
expect(splitEnv.ORCA_WORKSPACE_ROOT).toBe(folderPath)
expect(splitEnv.ORCA_WORKTREE_ID).toBe(TEST_FOLDER_WORKSPACE_KEY)
expect(revealTerminalSession).toHaveBeenLastCalledWith(TEST_FOLDER_WORKSPACE_KEY, {
ptyId: 'pty-folder-split',
title: null,
activate: true,
tabId: sourceEnv.ORCA_TAB_ID,
leafId: splitLeafId,
splitFromLeafId: sourceLeafId,
splitDirection: 'vertical'
})
})
it('returns a background handle when inactive tab adoption fails after spawn', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const revealTerminalSession = vi.fn().mockRejectedValue(new Error('Renderer timed out'))