Fix mobile folder workspace visibility (#5679)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
c9bd61376f
commit
06d674b35d
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
>
|
||||
<Folder size={18} color={colors.textSecondary} strokeWidth={2.1} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.filesButton,
|
||||
pressed && styles.filesButtonPressed,
|
||||
activePanel === 'sourceControl' && styles.filesButtonActive
|
||||
]}
|
||||
onPress={() => handlePanelTap('sourceControl')}
|
||||
hitSlop={8}
|
||||
accessibilityLabel="Open source control"
|
||||
>
|
||||
<GitBranch size={18} color={colors.textSecondary} strokeWidth={2.1} />
|
||||
</Pressable>
|
||||
{!isFolderWorkspaceRoute && (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.filesButton,
|
||||
pressed && styles.filesButtonPressed,
|
||||
activePanel === 'sourceControl' && styles.filesButtonActive
|
||||
]}
|
||||
onPress={() => handlePanelTap('sourceControl')}
|
||||
hitSlop={8}
|
||||
accessibilityLabel="Open source control"
|
||||
>
|
||||
<GitBranch size={18} color={colors.textSecondary} strokeWidth={2.1} />
|
||||
</Pressable>
|
||||
)}
|
||||
{prRepoContextLoaded && prIsGithubRepo ? (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
|
|
|
|||
|
|
@ -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<T extends WorktreeListRowItem> = {
|
|||
hideRepo?: boolean
|
||||
status: WorktreeRollupStatus
|
||||
onPress: (item: T) => void
|
||||
onLongPress: (item: T) => void
|
||||
onLongPress?: (item: T) => void
|
||||
}
|
||||
|
||||
export function WorktreeListRow<T extends WorktreeListRowItem>({
|
||||
|
|
@ -62,6 +64,10 @@ export function WorktreeListRow<T extends WorktreeListRowItem>({
|
|||
onPress,
|
||||
onLongPress
|
||||
}: Props<T>) {
|
||||
const isFolderWorkspace = item.workspaceKind === 'folder-workspace'
|
||||
const folderMeta = item.comment?.trim() || item.path || 'Folder'
|
||||
const metaText = isFolderWorkspace ? folderMeta : displayBranch(item.branch)
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
|
|
@ -71,10 +77,14 @@ export function WorktreeListRow<T extends WorktreeListRowItem>({
|
|||
]}
|
||||
disabled={isReadOnly}
|
||||
onPress={() => onPress(item)}
|
||||
onLongPress={() => {
|
||||
triggerMediumImpact()
|
||||
onLongPress(item)
|
||||
}}
|
||||
onLongPress={
|
||||
onLongPress
|
||||
? () => {
|
||||
triggerMediumImpact()
|
||||
onLongPress(item)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
delayLongPress={400}
|
||||
>
|
||||
<View style={styles.indicatorCol}>
|
||||
|
|
@ -109,6 +119,11 @@ export function WorktreeListRow<T extends WorktreeListRowItem>({
|
|||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{isFolderWorkspace && (
|
||||
<View style={styles.folderBadge}>
|
||||
<Text style={styles.folderBadgeText}>Folder</Text>
|
||||
</View>
|
||||
)}
|
||||
<WorktreeMetaGlyphs
|
||||
comment={item.comment}
|
||||
linkedLinearIssue={item.linkedLinearIssue}
|
||||
|
|
@ -130,7 +145,7 @@ export function WorktreeListRow<T extends WorktreeListRowItem>({
|
|||
</>
|
||||
)}
|
||||
<Text style={styles.branchName} numberOfLines={1}>
|
||||
{displayBranch(item.branch)}
|
||||
{metaText}
|
||||
</Text>
|
||||
</View>
|
||||
{/* 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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, string>; 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'
|
||||
|
|
|
|||
|
|
@ -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<string>()
|
||||
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<RuntimeMobileSessionCreateTerminalResult> {
|
||||
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<string | undefined> {
|
||||
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<RuntimeMobileSessionCreateTerminalResult> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -433,6 +433,7 @@ export type RuntimeWorktreeAgentRow = {
|
|||
}
|
||||
|
||||
export type RuntimeWorktreePsSummary = {
|
||||
workspaceKind?: 'git' | 'folder-workspace'
|
||||
worktreeId: string
|
||||
repoId: string
|
||||
repo: string
|
||||
|
|
|
|||
Loading…
Reference in New Issue