diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx
index 5f2d5c07e..9a3b9b3ec 100644
--- a/mobile/app/h/[hostId]/index.tsx
+++ b/mobile/app/h/[hostId]/index.tsx
@@ -1242,7 +1242,7 @@ export function HostScreen({
repoIcon={repoIconsByName.get(item.repo) ?? null}
hideRepo={groupMode === 'repo'}
onPress={openWorktreeSession}
- onLongPress={setActionTarget}
+ onLongPress={item.workspaceKind === 'folder-workspace' ? undefined : setActionTarget}
/>
)}
/>
diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx
index bb9640693..6a66420f8 100644
--- a/mobile/app/h/[hostId]/session/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx
@@ -792,6 +792,7 @@ export default function SessionScreen() {
created?: string
warning?: string
}>()
+ const isFolderWorkspaceRoute = worktreeId.startsWith('folder:')
const router = useRouter()
const insets = useSafeAreaInsets()
// Why: shared client per host owned by RpcClientProvider. See
@@ -4209,18 +4210,20 @@ export default function SessionScreen() {
>
- [
- styles.filesButton,
- pressed && styles.filesButtonPressed,
- activePanel === 'sourceControl' && styles.filesButtonActive
- ]}
- onPress={() => handlePanelTap('sourceControl')}
- hitSlop={8}
- accessibilityLabel="Open source control"
- >
-
-
+ {!isFolderWorkspaceRoute && (
+ [
+ styles.filesButton,
+ pressed && styles.filesButtonPressed,
+ activePanel === 'sourceControl' && styles.filesButtonActive
+ ]}
+ onPress={() => handlePanelTap('sourceControl')}
+ hitSlop={8}
+ accessibilityLabel="Open source control"
+ >
+
+
+ )}
{prRepoContextLoaded && prIsGithubRepo ? (
[
diff --git a/mobile/src/components/WorktreeListRow.tsx b/mobile/src/components/WorktreeListRow.tsx
index 0695a49e9..aa4d50fdc 100644
--- a/mobile/src/components/WorktreeListRow.tsx
+++ b/mobile/src/components/WorktreeListRow.tsx
@@ -18,10 +18,12 @@ function displayBranch(branch: string): string {
// Minimal row shape needed for rendering — a structural subset of the screen's
// Worktree so this component stays decoupled from the screen's local type.
export type WorktreeListRowItem = {
+ workspaceKind?: 'git' | 'folder-workspace'
worktreeId: string
repo: string
branch: string
displayName: string
+ path?: string
liveTerminalCount: number
preview: string
unread: boolean
@@ -48,7 +50,7 @@ type Props = {
hideRepo?: boolean
status: WorktreeRollupStatus
onPress: (item: T) => void
- onLongPress: (item: T) => void
+ onLongPress?: (item: T) => void
}
export function WorktreeListRow({
@@ -62,6 +64,10 @@ export function WorktreeListRow({
onPress,
onLongPress
}: Props) {
+ const isFolderWorkspace = item.workspaceKind === 'folder-workspace'
+ const folderMeta = item.comment?.trim() || item.path || 'Folder'
+ const metaText = isFolderWorkspace ? folderMeta : displayBranch(item.branch)
+
return (
[
@@ -71,10 +77,14 @@ export function WorktreeListRow({
]}
disabled={isReadOnly}
onPress={() => onPress(item)}
- onLongPress={() => {
- triggerMediumImpact()
- onLongPress(item)
- }}
+ onLongPress={
+ onLongPress
+ ? () => {
+ triggerMediumImpact()
+ onLongPress(item)
+ }
+ : undefined
+ }
delayLongPress={400}
>
@@ -109,6 +119,11 @@ export function WorktreeListRow({
)}
+ {isFolderWorkspace && (
+
+ Folder
+
+ )}
({
>
)}
- {displayBranch(item.branch)}
+ {metaText}
{/* Only agents get a secondary activity line, matching desktop. A plain
@@ -212,6 +227,16 @@ const styles = StyleSheet.create({
fontSize: 10,
color: colors.textSecondary
},
+ folderBadge: {
+ backgroundColor: colors.bgRaised,
+ paddingHorizontal: 5,
+ paddingVertical: 1,
+ borderRadius: 4
+ },
+ folderBadgeText: {
+ fontSize: 10,
+ color: colors.textSecondary
+ },
worktreeMetaRow: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/mobile/src/worktree/workspace-list-sections.ts b/mobile/src/worktree/workspace-list-sections.ts
index bc1ea0525..6ca8aeee7 100644
--- a/mobile/src/worktree/workspace-list-sections.ts
+++ b/mobile/src/worktree/workspace-list-sections.ts
@@ -7,6 +7,7 @@ import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types'
import type { MobileGroupMode, MobileSortMode } from './workspace-view-settings'
export type Worktree = {
+ workspaceKind?: 'git' | 'folder-workspace'
worktreeId: string
repoId: string
repo: string
diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts
index 32e2e84dd..20f030526 100644
--- a/src/main/runtime/orca-runtime.test.ts
+++ b/src/main/runtime/orca-runtime.test.ts
@@ -9940,6 +9940,45 @@ describe('OrcaRuntimeService', () => {
])
})
+ it('creates mobile session terminals for folder workspaces in a headless runtime server', async () => {
+ const folderPath = await mkdtemp(join(tmpdir(), 'orca-mobile-folder-workspace-'))
+ const spawn = vi.fn().mockResolvedValue({ id: 'pty-mobile-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.syncWindowGraph(0, { tabs: [], leaves: [] })
+
+ const result = await runtime.createMobileSessionTerminal(`id:${TEST_FOLDER_WORKSPACE_KEY}`)
+
+ const spawnCall = spawn.mock.calls[0]?.[0] as
+ | { cwd?: string; env?: Record; worktreeId?: string }
+ | undefined
+ const spawnedEnv = spawnCall?.env ?? {}
+ expect(spawnCall).toMatchObject({
+ cwd: folderPath,
+ worktreeId: TEST_FOLDER_WORKSPACE_KEY,
+ persistHostSessionBinding: true
+ })
+ 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(result.tab).toMatchObject({
+ type: 'terminal',
+ status: 'ready',
+ terminal: expect.stringMatching(/^term_/),
+ isActive: true
+ })
+ })
+
it('spawns fresh headless SSH mobile session terminals instead of reattaching synthetic local ids', async () => {
const remoteRepo = { ...store.getRepo(TEST_REPO_ID)!, connectionId: 'ssh-1' }
const remoteStore = {
@@ -12208,6 +12247,7 @@ describe('OrcaRuntimeService', () => {
expect(summaries).toEqual({
worktrees: [
{
+ workspaceKind: 'git',
worktreeId: 'repo-1::/tmp/worktree-a',
repoId: 'repo-1',
repo: 'repo',
@@ -12280,6 +12320,38 @@ describe('OrcaRuntimeService', () => {
expect(summary?.linkedPR).toEqual({ number: 7, state: 'open' })
})
+ it('includes folder workspaces in compact worktree summaries for mobile', async () => {
+ const folderWorkspace = makeFolderWorkspace({
+ name: 'GG',
+ comment: 'dujiao-next-eval'
+ })
+ const projectGroup = makeFolderProjectGroup({ name: 'Store' })
+ const runtime = new OrcaRuntimeService(
+ createFolderWorkspaceRuntimeStore(folderWorkspace, projectGroup) as never
+ )
+
+ const { worktrees } = await runtime.getWorktreePs()
+ const folderSummary = worktrees.find(
+ (worktree) => worktree.worktreeId === TEST_FOLDER_WORKSPACE_KEY
+ )
+
+ expect(folderSummary).toMatchObject({
+ workspaceKind: 'folder-workspace',
+ worktreeId: TEST_FOLDER_WORKSPACE_KEY,
+ repoId: `folder-workspace:${TEST_FOLDER_PROJECT_GROUP_ID}`,
+ repo: 'Store',
+ path: TEST_FOLDER_WORKSPACE_PATH,
+ branch: '',
+ displayName: 'GG',
+ comment: 'dujiao-next-eval',
+ isPinned: false,
+ unread: false,
+ liveTerminalCount: 0,
+ hasAttachedPty: false,
+ status: 'inactive'
+ })
+ })
+
it('attaches inline agent rows from the latest OSC 9999 status', async () => {
const runtime = new OrcaRuntimeService(store)
const leafId = '22222222-2222-4222-8222-222222222222'
diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts
index 338af45ed..e6a63c232 100644
--- a/src/main/runtime/orca-runtime.ts
+++ b/src/main/runtime/orca-runtime.ts
@@ -173,12 +173,14 @@ import {
isPathInsideOrEqual,
normalizeRuntimePathForComparison
} from '../../shared/cross-platform-path'
+import { isWslUncPath } from '../../shared/wsl-paths'
import {
folderWorkspaceKey,
isWorkspaceKey,
parseWorkspaceKey,
worktreeWorkspaceKey
} from '../../shared/workspace-scope'
+import { folderWorkspaceToWorktree } from '../../shared/folder-workspace-worktree'
import type {
FolderWorkspacePathStatus,
FolderWorkspacePathStatusRequest
@@ -1534,6 +1536,7 @@ type TerminalWorkspaceLaunchScope = {
id: string
path: string
connectionId: string | null
+ repo: Repo | null
folderWorkspace: FolderWorkspace | null
}
@@ -7044,6 +7047,7 @@ export class OrcaRuntimeService {
linkedPR = { number: meta.linkedPR, state: 'unknown' }
}
summaries.set(worktree.id, {
+ workspaceKind: 'git',
worktreeId: worktree.id,
repoId: worktree.repoId,
repo: repo?.displayName ?? worktree.repoId,
@@ -7070,6 +7074,43 @@ export class OrcaRuntimeService {
})
}
+ const projectGroupById = new Map(
+ (this.store?.getProjectGroups?.() ?? []).map((group) => [group.id, group])
+ )
+ for (const folderWorkspace of this.store?.getFolderWorkspaces?.() ?? []) {
+ const projectGroup = projectGroupById.get(folderWorkspace.projectGroupId)
+ if (!projectGroup?.parentPath) {
+ continue
+ }
+ const worktree = folderWorkspaceToWorktree(folderWorkspace)
+ summaries.set(worktree.id, {
+ workspaceKind: 'folder-workspace',
+ worktreeId: worktree.id,
+ repoId: worktree.repoId,
+ repo: projectGroup.name,
+ path: worktree.path,
+ branch: worktree.branch,
+ parentWorktreeId: null,
+ childWorktreeIds: [],
+ displayName: worktree.displayName,
+ linkedIssue: worktree.linkedIssue ?? null,
+ linkedPR: null,
+ linkedLinearIssue: worktree.linkedLinearIssue ?? null,
+ linkedGitLabMR: worktree.linkedGitLabMR ?? null,
+ linkedGitLabIssue: worktree.linkedGitLabIssue ?? null,
+ comment: worktree.comment,
+ isPinned: worktree.isPinned,
+ isActive: false,
+ unread: worktree.isUnread,
+ liveTerminalCount: 0,
+ hasAttachedPty: false,
+ lastOutputAt: null,
+ preview: '',
+ status: 'inactive',
+ agents: []
+ })
+ }
+
const countedPtyIds = new Set()
for (const leaf of this.leaves.values()) {
const summary = this.getSummaryForRuntimeWorktreeId(
@@ -8302,6 +8343,16 @@ export class OrcaRuntimeService {
return getAgentLaunchPlatformForRepo(repo, projectRuntime)
}
+ private getAgentLaunchPlatformForWorkspace(scope: TerminalWorkspaceLaunchScope): NodeJS.Platform {
+ if (scope.repo) {
+ return this.getAgentLaunchPlatformForRepo(scope.repo)
+ }
+ if (scope.connectionId) {
+ return isWindowsAbsolutePathLike(scope.path) ? 'win32' : 'linux'
+ }
+ return isWslUncPath(scope.path) ? 'linux' : process.platform
+ }
+
async getRepoSlug(repoSelector: string): Promise<{ owner: string; repo: string } | null> {
const repo = await this.resolveRepoSelector(repoSelector)
const options = this.getHostedReviewExecutionOptions(repo)
@@ -12939,8 +12990,8 @@ export class OrcaRuntimeService {
} = {}
): Promise {
this.assertGraphReady()
- const worktree = await this.resolveWorktreeSelector(worktreeSelector)
- const worktreeId = worktree.id
+ const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector)
+ const worktreeId = workspace.id
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId)
let afterDesktopTabId: string | undefined
if (opts.afterTabId) {
@@ -12951,7 +13002,7 @@ export class OrcaRuntimeService {
}
afterDesktopTabId = anchor.type === 'terminal' ? anchor.parentTabId : anchor.id
}
- const command = await this.resolveMobileSessionTerminalCommand(worktree, opts)
+ const command = await this.resolveMobileSessionTerminalCommand(workspace, opts)
const win = this.getAvailableAuthoritativeWindow()
if (!win) {
@@ -13004,7 +13055,7 @@ export class OrcaRuntimeService {
}
private async resolveMobileSessionTerminalCommand(
- worktree: Worktree,
+ workspace: TerminalWorkspaceLaunchScope,
opts: { command?: string; agent?: TuiAgent }
): Promise {
if (opts.command || !opts.agent) {
@@ -13017,10 +13068,9 @@ export class OrcaRuntimeService {
if (!isTuiAgentEnabled(opts.agent, settings.disabledTuiAgents)) {
throw new Error('Selected agent is disabled. Choose an enabled agent before creating.')
}
- const repo = this.store.getRepo(worktree.repoId)
// Why: mobile may be running on iOS while the actual terminal shell is
// Windows/macOS/Linux or an SSH Linux host; quote for the host shell.
- const platform = repo ? this.getAgentLaunchPlatformForRepo(repo) : process.platform
+ const platform = this.getAgentLaunchPlatformForWorkspace(workspace)
const startupPlan = buildAgentStartupPlan({
agent: opts.agent,
prompt: '',
@@ -13033,10 +13083,14 @@ export class OrcaRuntimeService {
if (!startupPlan) {
throw new Error(`Could not build launch command for ${opts.agent}.`)
}
- if (repo?.connectionId) {
- await this.markRemoteWorkspaceTrustedForAgent(opts.agent, repo.connectionId, worktree.path)
+ if (workspace.connectionId) {
+ await this.markRemoteWorkspaceTrustedForAgent(
+ opts.agent,
+ workspace.connectionId,
+ workspace.path
+ )
} else {
- this.markLocalWorkspaceTrustedForAgent(opts.agent, worktree.path)
+ this.markLocalWorkspaceTrustedForAgent(opts.agent, workspace.path)
}
return startupPlan.launchCommand
}
@@ -13049,12 +13103,11 @@ export class OrcaRuntimeService {
identity?: { tabId: string; leafId: string; sessionId?: string },
launchAgent?: TuiAgent
): Promise {
- const worktree = await this.resolveWorktreeSelector(`id:${worktreeId}`)
- const repo = this.store?.getRepo(worktree.repoId)
+ const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
// Why: SshPtyProvider treats sessionId as a relay reattach request. Only
// synthesize local serve ids; SSH fresh terminals must call pty.spawn.
const stableSessionId =
- identity?.sessionId ?? (repo?.connectionId ? undefined : `serve-${randomUUID()}`)
+ identity?.sessionId ?? (workspace.connectionId ? undefined : `serve-${randomUUID()}`)
const terminal = await this.createTerminal(`id:${worktreeId}`, {
focus: false,
command,
@@ -13833,6 +13886,7 @@ export class OrcaRuntimeService {
id: folderWorkspaceKey(workspace.id),
path: workspace.folderPath,
connectionId: this.resolveFolderWorkspaceConnectionId(workspace),
+ repo: null,
folderWorkspace: workspace
}
}
@@ -13854,6 +13908,7 @@ export class OrcaRuntimeService {
id: worktree.id,
path: worktree.path,
connectionId: repo?.connectionId ?? null,
+ repo,
folderWorkspace: null
}
}
diff --git a/src/shared/runtime-types.ts b/src/shared/runtime-types.ts
index 181885f73..a6e511a11 100644
--- a/src/shared/runtime-types.ts
+++ b/src/shared/runtime-types.ts
@@ -433,6 +433,7 @@ export type RuntimeWorktreeAgentRow = {
}
export type RuntimeWorktreePsSummary = {
+ workspaceKind?: 'git' | 'folder-workspace'
worktreeId: string
repoId: string
repo: string