import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"; import type { ReactElement, RefObject } from "react"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { Pressable, StyleSheet as RNStyleSheet, Text, View } from "react-native"; import type { PressableStateCallbackType } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { createNameId } from "mnemonic-id"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ChevronDown, Folder, FolderPlus, GitBranch, GitPullRequest } from "lucide-react-native"; import { Composer } from "@/composer"; import { FileDropZone } from "@/components/file-drop/file-drop-zone"; import { resolveComposerAttachmentSubmitFormat, splitComposerAttachmentsForSubmit, } from "@/composer/attachments/submit"; import { HostStatusDot } from "@/components/host-status-dot"; import { HostPicker } from "@/components/hosts/host-picker"; import { ProjectIconView } from "@/components/project-icon-view"; import { Combobox, ComboboxItem } from "@/components/ui/combobox"; import type { ComboboxOption as ComboboxOptionType, ComboboxProps } from "@/components/ui/combobox"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { Shortcut } from "@/components/ui/shortcut"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region"; import { SidebarMenuToggle } from "@/components/headers/menu-header"; import { ScreenHeader } from "@/components/headers/screen-header"; import { HEADER_INNER_HEIGHT, MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout"; import { useToast } from "@/contexts/toast-context"; import { useAgentInputDraft } from "@/composer/draft/input-draft"; import { useForgeSearchQuery } from "@/git/use-forge-search-query"; import { useCheckoutStatusQuery } from "@/git/use-status-query"; import { ensureCheckoutStatus } from "@/git/checkout-status-cache"; import { useHostRuntimeClient, useHostRuntimeConnectionStatuses, useHostRuntimeIsConnected, useHosts, type HostRuntimeConnectionStatus, } from "@/runtime/host-runtime"; import { useHostFeature, useHostFeatureMap } from "@/runtime/host-features"; import type { HostProfile } from "@/types/host-connection"; import { navigateToWorkspace, useLastWorkspaceSelection, } from "@/stores/navigation-active-workspace-store"; import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store"; import { useWorkspace } from "@/stores/session-store-hooks"; import { buildNewWorkspaceDraftKey, generateDraftId } from "@/stores/draft-keys"; import { useDraftStore } from "@/stores/draft-store"; import { useOpenAddProject } from "@/hooks/use-open-add-project"; import { isActiveCreateFlowForDraft, useCreateFlowStore } from "@/stores/create-flow-store"; import { useWorkspaceDraftSubmissionStore, type PendingWorkspaceDraftSetup, } from "@/stores/workspace-draft-submission-store"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher"; import { useFormPreferences } from "@/hooks/use-form-preferences"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { getForgePresentation } from "@/git/forge"; import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state"; import { generateMessageId } from "@/types/stream"; import { toErrorMessage } from "@/utils/error-messages"; import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name"; import { getHostProjectSourceDirectory, getHostProjectId, getWorktreeSupportForHostProject, hostProjectFromRoute, hostProjectFromWorkspace, useHostProjects, type HostProjectListItem, } from "@/projects/host-projects"; import { useProjectIconDataByProjectViewKey } from "@/projects/project-icons"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import type { ComposerAttachment } from "@/attachments/types"; import { useDraftWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store"; import type { MessagePayload } from "@/composer/types"; import type { AgentAttachment, ForgeSearchItem } from "@getpaseo/protocol/messages"; import type { CreatePaseoWorktreeInput } from "@getpaseo/client/internal/daemon-client"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; import type { WorkspaceDraftTabSetup, WorkspaceTabTarget } from "@/workspace-tabs/model"; import { isEmptyWorkspaceSubmission, runCreateEmptyWorkspace } from "./new-workspace-empty"; import { getWorkspaceNamingAttachments, remapDraftCwdToWorkspace, } from "./new-workspace-fork-context"; import { resolveCheckoutRequest, type PickerCheckoutRequest, type PickerItem, } from "./new-workspace-picker-item"; import { clearPickerPrAttachmentForTargetChange, initialPickerSelectionState, reducePickerSelection, syncPickerPrAttachment, } from "./new-workspace-picker-state"; import { resolveNewWorkspaceAutomaticServerId, resolveNewWorkspaceInitialServerId, } from "./new-workspace-initial-context"; import { useNewWorkspaceProjectPicker } from "./new-workspace/project-picker"; const ThemedFolderPlus = withUnistyles(FolderPlus); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const addProjectIcon = ( ); function useIsNewWorkspaceDraftHandoffActive(input: { draftId: string | undefined; selectedServerId: string; }): boolean { const normalizedDraftId = input.draftId?.trim() ?? ""; return useCreateFlowStore((state) => isActiveCreateFlowForDraft({ draftId: normalizedDraftId, serverId: input.selectedServerId, pending: normalizedDraftId ? state.pendingByDraftId[normalizedDraftId] : null, }), ); } function resolveVisibleDraftContextScopeKeys(input: { isDraftHandoffActive: boolean; draftContextScopeKey: string; }): readonly string[] { if (input.isDraftHandoffActive || !input.draftContextScopeKey) { return []; } return [input.draftContextScopeKey]; } function isNewWorkspacePending(input: { pendingAction: "chat" | "empty" | null; isDraftHandoffActive: boolean; }): boolean { return input.pendingAction !== null || input.isDraftHandoffActive; } function buildFirstAgentContext(input: { prompt: string; attachments: AgentAttachment[]; }): { prompt?: string; attachments?: AgentAttachment[] } | undefined { const trimmedPrompt = input.prompt.trim(); if (!trimmedPrompt && input.attachments.length === 0) { return undefined; } return { ...(trimmedPrompt ? { prompt: trimmedPrompt } : {}), attachments: input.attachments, }; } interface NewWorkspaceScreenProps { serverId: string; sourceDirectory?: string; projectId?: string; displayName?: string; draftId?: string; } interface PickerOptionData { options: ComboboxOptionType[]; itemById: Map; } const BRANCH_OPTION_PREFIX = "branch:"; const PR_OPTION_PREFIX = "github-pr:"; const PROJECT_ICON_FALLBACK_FONT_SIZE = 10; // Stable reference so the keyboard-action handler doesn't re-register each render. const PROJECT_PICK_ACTIONS: readonly KeyboardActionId[] = ["workspace.project.pick"]; // Height of a single picker-trigger badge. The Base-row spacer reserves exactly // this so toggling Isolation to Local hides the row without shifting the form. const BADGE_HEIGHT = 28; function RefPickerBadgeContent({ selectedItem, triggerLabel, iconColor, iconSize, }: { selectedItem: PickerItem | null; triggerLabel: string; iconColor: string; iconSize: number; }) { return ( <> {selectedItem?.kind === "github-pr" ? ( ) : ( )} {triggerLabel} ); } function RefPickerTrigger({ pickerAnchorRef, onPress, disabled, badgePressableStyle, selectedItem, triggerLabel, accessibilityLabel, tooltipLabel, iconColor, iconSize, }: { pickerAnchorRef: React.RefObject; onPress: () => void; disabled: boolean; badgePressableStyle: React.ComponentProps["style"]; selectedItem: PickerItem | null; triggerLabel: string; accessibilityLabel: string; tooltipLabel: string; iconColor: string; iconSize: number; }) { return ( {tooltipLabel} ); } function ProjectPickerTrigger({ pickerAnchorRef, onPress, disabled, badgePressableStyle, label, projectViewKey, iconDataUri, iconColor, iconSize, }: { pickerAnchorRef: React.RefObject; onPress: () => void; disabled: boolean; badgePressableStyle: React.ComponentProps["style"]; label: string; projectViewKey: string | null; iconDataUri: string | null; iconColor: string; iconSize: number; }) { const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(label); const placeholderInitial = placeholderLabel.charAt(0).toUpperCase() || "?"; return ( {projectViewKey ? ( ) : ( )} {label} Choose project ); } function PickerOptionItem({ testID, label, description, selected, active, disabled, onPress, isBranch, iconColor, iconSize, }: { testID: string; label: string; description: string | undefined; selected: boolean; active: boolean; disabled: boolean; onPress: () => void; isBranch: boolean; iconColor: string; iconSize: number; }) { const leadingSlot = useMemo( () => ( {isBranch ? ( ) : ( )} ), [isBranch, iconSize, iconColor], ); return ( ); } function IsolationOptionItem({ optionId, label, selected, active, disabled, onPress, iconColor, iconSize, }: { optionId: string; label: string; selected: boolean; active: boolean; disabled: boolean; onPress: () => void; iconColor: string; iconSize: number; }) { const leadingSlot = useMemo( () => ( {optionId === "worktree" ? ( ) : ( )} ), [optionId, iconSize, iconColor], ); return ( ); } function ProjectOptionItem({ testID, projectViewKey, iconDataUri, label, description, selected, active, disabled, onPress, }: { testID: string; projectViewKey: string; iconDataUri: string | null; label: string; description: string | undefined; selected: boolean; active: boolean; disabled: boolean; onPress: () => void; }) { const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(label); const placeholderInitial = placeholderLabel.charAt(0).toUpperCase() || "?"; const leadingSlot = useMemo( () => ( ), [iconDataUri, placeholderInitial, projectViewKey], ); return ( ); } function branchOptionId(name: string): string { return `${BRANCH_OPTION_PREFIX}${name}`; } function prOptionId(number: number): string { return `${PR_OPTION_PREFIX}${number}`; } function NewWorkspacePickerOption({ option, selected, active, onPress, itemById, isPending, }: { option: ComboboxOptionType; selected: boolean; active: boolean; onPress: () => void; itemById: Map; isPending: boolean; }) { const { theme } = useUnistyles(); const { t } = useTranslation(); const item = itemById.get(option.id); if (!item) return ; const isBranch = item.kind === "branch"; const testID = isBranch ? `new-workspace-ref-picker-branch-${item.name}` : `new-workspace-ref-picker-pr-${item.item.number}`; const description = !isBranch && item.item.baseRefName ? t("newWorkspace.refPicker.intoBase", { baseRef: item.item.baseRefName }) : undefined; return ( ); } function NewWorkspaceProjectPickerOption({ option, selected, active, onPress, projectByOptionId, projectIconDataByProjectViewKey, selectedServerId, isPending, supportsWorkspaceMultiplicity, }: { option: ComboboxOptionType; selected: boolean; active: boolean; onPress: () => void; projectByOptionId: Map; projectIconDataByProjectViewKey: Map; selectedServerId: string; isPending: boolean; supportsWorkspaceMultiplicity: boolean; }) { const project = projectByOptionId.get(option.id); if (!project) return ; const sourceDirectory = getHostProjectSourceDirectory(project, selectedServerId) ?? project.iconWorkingDir; return ( host.worktreeSupport !== "unsupported")) } onPress={onPress} /> ); } function AddProjectPickerAction({ onPress }: { onPress: () => void }) { const { t } = useTranslation(); const openProjectKeys = useShortcutKeys("new-agent"); const shortcut = useMemo( () => (openProjectKeys ? : null), [openProjectKeys], ); return ( ); } function formatPrLabel(item: Pick): string { const presentation = getForgePresentation(item.forge ?? "github"); return `${presentation.numberPrefix}${item.number} ${item.title}`; } function pickerItemLabel(item: PickerItem): string { return item.kind === "branch" ? item.name : formatPrLabel(item.item); } function pickerItemTriggerLabel(item: PickerItem): string { return item.kind === "branch" ? item.name : formatPrLabel(item.item); } function newWorkspaceHostOptionTestID(serverId: string): string { return `new-workspace-host-picker-option-${serverId}`; } function computePickerOptionData( branchDetails: ReadonlyArray<{ name: string; committerDate: number }>, prItems: ReadonlyArray, ): PickerOptionData { const idMap = new Map(); interface TimedOption { option: ComboboxOptionType; timestamp: number; } const timedOptions: TimedOption[] = []; for (const branch of branchDetails) { const id = branchOptionId(branch.name); const option = { id, label: branch.name }; idMap.set(id, { kind: "branch", name: branch.name }); timedOptions.push({ option, timestamp: branch.committerDate }); } for (const pr of prItems) { if (!pr.headRefName) continue; const id = prOptionId(pr.number); const option = { id, label: formatPrLabel(pr) }; idMap.set(id, { kind: "github-pr", item: pr }); const updatedAtMs = pr.updatedAt ? Date.parse(pr.updatedAt) : 0; const timestamp = Number.isNaN(updatedAtMs) ? 0 : Math.floor(updatedAtMs / 1000); timedOptions.push({ option, timestamp }); } timedOptions.sort((a, b) => b.timestamp - a.timestamp); return { options: timedOptions.map((t) => t.option), itemById: idMap }; } function IsolationPickerTrigger({ pickerAnchorRef, onPress, disabled, badgePressableStyle, isolation, label, iconColor, iconSize, }: { pickerAnchorRef: React.RefObject; onPress: () => void; disabled: boolean; badgePressableStyle: React.ComponentProps["style"]; isolation: "local" | "worktree"; label: string; iconColor: string; iconSize: number; }) { return ( {isolation === "worktree" ? ( ) : ( )} {label} ); } // Wraps a single argument control in the mobile vertical stack. On desktop the // controls are laid out in one horizontal row, so no per-control wrapper is used. function FormRow({ children }: { children: React.ReactNode }) { return {children}; } interface WorkspaceIsolationState { isolation: "local" | "worktree"; setIsolation: (value: "local" | "worktree") => void; effectiveIsolation: "local" | "worktree"; canCreateWorktree: boolean; showRefPicker: boolean; } // Preserve the user's worktree choice while route metadata is provisional. Once // the authoritative placement arrives, unsupported projects fall back to local. function useWorkspaceIsolation(input: { supportsMultiplicity: boolean; worktreeSupport: "supported" | "unsupported" | "unknown"; }): WorkspaceIsolationState { const { supportsMultiplicity, worktreeSupport } = input; // The last isolation choice is remembered alongside the other New Workspace // form preferences (provider, model, mode). A manual in-screen pick overrides // the remembered default until the screen remounts. const { preferences, updatePreferences } = useFormPreferences(); const [manualIsolation, setManualIsolation] = useState<"local" | "worktree" | null>(null); const isolation = manualIsolation ?? preferences.isolation ?? "local"; const canCreateWorktree = supportsMultiplicity && worktreeSupport !== "unsupported"; const isWorktree = isolation === "worktree" && canCreateWorktree; const setIsolation = useCallback( (value: "local" | "worktree") => { setManualIsolation(value); void updatePreferences({ isolation: value }); }, [updatePreferences], ); return { isolation, setIsolation, effectiveIsolation: isWorktree ? "worktree" : "local", canCreateWorktree, showRefPicker: !supportsMultiplicity || isWorktree, }; } function isolationLabel(t: TFunction, isolation: "local" | "worktree"): string { return isolation === "worktree" ? t("newWorkspace.isolation.worktree") : t("newWorkspace.isolation.local"); } function getContentStyle(input: { isCompact: boolean; insetBottom: number }) { if (input.isCompact) { return [styles.content, styles.contentCompact, { paddingBottom: input.insetBottom }]; } return [styles.content, styles.contentCentered]; } function normalizeBranchDetails( data: | { branchDetails?: Array<{ name: string; committerDate: number }>; branches?: string[] } | undefined, ): Array<{ name: string; committerDate: number }> { const details = data?.branchDetails; if (details && details.length > 0) return details; const names = data?.branches ?? []; return names.map((name) => ({ name, committerDate: 0 })); } interface SubmitDraftInput { serverId: string; draftKey: string; draftId?: string; initialSetup?: WorkspaceDraftTabSetup; workspaceId: string; workspaceDirectory: string; text: string; attachments: ComposerAttachment[]; provider: AgentProvider; composerState: NewWorkspaceComposerState; supportsForgeSearch: boolean; } type NewWorkspaceComposerState = NonNullable< ReturnType["composerState"] >; interface WorkspaceDraftSubmissionConfig { cwd: string; provider: AgentProvider; modeId: string | null; model: string | null; thinkingOptionId: string | null; featureValues: Record | undefined; target: WorkspaceTabTarget; } async function createAndMergeWorkspace(input: { client: NonNullable>; createInput: Parameters< NonNullable>["createPaseoWorktree"] >[0]; mergeWorkspaces: ( serverId: string, workspaces: ReturnType[], ) => void; serverId: string; createFailedMessage: string; }): Promise> { const payload = await input.client.createPaseoWorktree(input.createInput); if (payload.error || !payload.workspace) { throw new Error(payload.error ?? input.createFailedMessage); } const normalizedWorkspace = normalizeWorkspaceDescriptor(payload.workspace); const workspaceForInitialMerge = input.createInput.firstAgentContext ? { ...normalizedWorkspace, status: "running" as const, statusEnteredAt: new Date() } : normalizedWorkspace; input.mergeWorkspaces(input.serverId, [workspaceForInitialMerge]); return normalizedWorkspace; } async function createMultiplicityWorkspace(input: { client: NonNullable>; isolation: "local" | "worktree"; project: HostProjectListItem; sourceDirectory: string; checkoutRequest: PickerCheckoutRequest | undefined; withInitialAgent: boolean; prompt: string; attachments: AgentAttachment[]; mergeWorkspaces: ( serverId: string, workspaces: ReturnType[], ) => void; serverId: string; createFailedMessage: string; }): Promise> { const projectId = getHostProjectId(input.project, input.serverId); if (!projectId) throw new Error("Project is not available on the selected host"); const isWorktree = input.isolation === "worktree"; const firstAgentContext = buildFirstAgentContext({ prompt: input.prompt, attachments: input.attachments, }); const payload = await input.client.createWorkspace({ source: isWorktree ? { kind: "worktree", cwd: input.sourceDirectory, projectId, worktreeSlug: createNameId(), ...input.checkoutRequest, } : { kind: "directory", path: input.sourceDirectory, projectId, }, ...(firstAgentContext ? { firstAgentContext } : {}), }); if (payload.error || !payload.workspace) { throw new Error(payload.error ?? input.createFailedMessage); } const normalizedWorkspace = normalizeWorkspaceDescriptor(payload.workspace); const workspaceForInitialMerge = input.withInitialAgent ? { ...normalizedWorkspace, status: "running" as const, statusEnteredAt: new Date() } : normalizedWorkspace; input.mergeWorkspaces(input.serverId, [workspaceForInitialMerge]); return normalizedWorkspace; } interface CreateChatAgentInput { payload: MessagePayload; composerState: ReturnType["composerState"]; forkDraftSetup?: PendingWorkspaceDraftSetup | null; ensureWorkspace: (input: { cwd: string; prompt: string; attachments: AgentAttachment[]; withInitialAgent: boolean; }) => Promise>; serverId: string; draftKey: string; draftId?: string; supportsForgeSearch: boolean; labels: { composerStateRequired: string; selectModel: string; }; } function buildWorkspaceDraftSetupFromComposer(input: { cwd: string; provider: AgentProvider; composerState: NewWorkspaceComposerState; }): WorkspaceDraftTabSetup { return { provider: input.provider, cwd: input.cwd, modeId: input.composerState.selectedMode || null, model: input.composerState.effectiveModelId || null, thinkingOptionId: input.composerState.effectiveThinkingOptionId || null, featureValues: input.composerState.featureValues ?? {}, }; } function buildWorkspaceDraftSetupForCreatedWorkspace(input: { forkDraftSetup: PendingWorkspaceDraftSetup | null | undefined; workspaceDirectory: string; provider: AgentProvider; composerState: NewWorkspaceComposerState; }): WorkspaceDraftTabSetup | undefined { if (!input.forkDraftSetup) { return undefined; } return buildWorkspaceDraftSetupFromComposer({ cwd: remapDraftCwdToWorkspace({ cwd: input.forkDraftSetup.setup.cwd, sourceDirectory: input.forkDraftSetup.sourceDirectory, workspaceDirectory: input.workspaceDirectory, }), provider: input.provider, composerState: input.composerState, }); } function buildComposerInitialValues(input: { workingDir: string | undefined; initialSetup?: WorkspaceDraftTabSetup | null; }): CreateAgentInitialValues | undefined { if (input.initialSetup) { return { workingDir: input.workingDir ?? input.initialSetup.cwd, provider: input.initialSetup.provider, modeId: input.initialSetup.modeId, model: input.initialSetup.model, thinkingOptionId: input.initialSetup.thinkingOptionId, }; } if (input.workingDir) { return { workingDir: input.workingDir }; } return undefined; } async function runCreateChatAgent(input: CreateChatAgentInput): Promise { const { payload, composerState, ensureWorkspace, serverId, draftKey } = input; const { text, attachments, cwd } = payload; if (!composerState) { throw new Error(input.labels.composerStateRequired); } const provider = composerState.selectedProvider; if (!provider) { throw new Error(input.labels.selectModel); } const attachmentSubmitFormat = resolveComposerAttachmentSubmitFormat({ supportsForgeAttachments: input.supportsForgeSearch, }); const { attachments: reviewAttachments } = splitComposerAttachmentsForSubmit(attachments, { format: attachmentSubmitFormat, }); const workspaceNamingAttachments = getWorkspaceNamingAttachments(reviewAttachments); const ensuredWorkspace = await ensureWorkspace({ cwd, prompt: text, attachments: workspaceNamingAttachments, withInitialAgent: true, }); const initialSetup = buildWorkspaceDraftSetupForCreatedWorkspace({ forkDraftSetup: input.forkDraftSetup, workspaceDirectory: ensuredWorkspace.workspaceDirectory, provider, composerState, }); submitWorkspaceDraft({ serverId, draftKey, draftId: input.draftId, initialSetup, workspaceId: ensuredWorkspace.id, workspaceDirectory: ensuredWorkspace.workspaceDirectory, text, attachments, provider, composerState, supportsForgeSearch: input.supportsForgeSearch, }); } function buildComposerConfig(input: { serverId: string; isConnected: boolean; workspaceDirectory: string | null; sourceDirectory: string | null; initialSetup?: WorkspaceDraftTabSetup | null; }): Parameters[0]["composer"] { const { serverId, isConnected, workspaceDirectory, sourceDirectory, initialSetup } = input; const workingDir = workspaceDirectory || sourceDirectory || undefined; return { initialServerId: serverId || null, initialValues: buildComposerInitialValues({ workingDir, initialSetup }), initialFeatureValues: initialSetup?.featureValues, isVisible: true, onlineServerIds: isConnected && serverId ? [serverId] : [], lockedWorkingDir: workingDir, }; } function usePendingWorkspaceDraftSetup( draftId: string | undefined, ): PendingWorkspaceDraftSetup | null { const normalizedDraftId = draftId?.trim() ?? ""; return useWorkspaceDraftSubmissionStore((state) => { if (!normalizedDraftId) { return null; } return state.setupByDraftId[normalizedDraftId] ?? null; }); } function resolveWorkspaceDraftSubmissionConfig(input: { draftId: string; workspaceDirectory: string; provider: AgentProvider; composerState: NewWorkspaceComposerState; initialSetup?: WorkspaceDraftTabSetup; }): WorkspaceDraftSubmissionConfig { const { draftId, workspaceDirectory, provider, composerState, initialSetup } = input; if (initialSetup) { return { cwd: initialSetup.cwd, provider: initialSetup.provider, modeId: initialSetup.modeId, model: initialSetup.model, thinkingOptionId: initialSetup.thinkingOptionId, featureValues: initialSetup.featureValues, target: { kind: "draft", draftId, setup: initialSetup }, }; } return { cwd: workspaceDirectory, provider, modeId: composerState.selectedMode || null, model: composerState.effectiveModelId || null, thinkingOptionId: composerState.effectiveThinkingOptionId || null, featureValues: composerState.featureValues, target: { kind: "draft", draftId }, }; } function submitWorkspaceDraft(input: SubmitDraftInput): void { const { serverId, draftKey, draftId: draftIdInput, workspaceId, workspaceDirectory, text, attachments, provider, composerState, initialSetup, } = input; const draftId = draftIdInput?.trim() || generateDraftId(); const clientMessageId = generateMessageId(); const timestamp = Date.now(); const wirePayload = splitComposerAttachmentsForSubmit(attachments, { format: resolveComposerAttachmentSubmitFormat({ supportsForgeAttachments: input.supportsForgeSearch, }), }); const submission = resolveWorkspaceDraftSubmissionConfig({ draftId, workspaceDirectory, provider, composerState, initialSetup, }); useCreateFlowStore.getState().setPending({ serverId, draftId, workspaceId, agentId: null, clientMessageId, text: text.trim(), timestamp, ...(wirePayload.images.length > 0 ? { images: wirePayload.images } : {}), ...(wirePayload.attachments.length > 0 ? { attachments: wirePayload.attachments } : {}), }); useWorkspaceDraftSubmissionStore.getState().setPending({ serverId, workspaceId, draftId, text: text.trim(), attachments, cwd: submission.cwd, provider: submission.provider, clientMessageId, timestamp, ...(submission.modeId ? { modeId: submission.modeId } : {}), ...(submission.model ? { model: submission.model } : {}), ...(submission.thinkingOptionId ? { thinkingOptionId: submission.thinkingOptionId } : {}), ...(submission.featureValues ? { featureValues: submission.featureValues } : {}), allowEmptyText: true, }); navigateToWorkspace({ serverId, workspaceId, target: submission.target, }); useDraftStore.getState().clearDraftInput({ draftKey, lifecycle: "sent" }); } function useNewWorkspaceHostSelector(input: { initialServerId: string; allServerIds: string[]; projects: HostProjectListItem[]; lastActiveProject: HostProjectListItem | null; hostConnectionStatusByServerId: ReadonlyMap; workspaceMultiplicityByServerId: ReadonlyMap; }) { const routeServerId = input.initialServerId.trim(); const defaultServerId = useMemo( () => resolveNewWorkspaceInitialServerId({ allServerIds: input.allServerIds, routeServerId: input.initialServerId, lastActiveProject: input.lastActiveProject, projects: input.projects, hostConnectionStatusByServerId: input.hostConnectionStatusByServerId, workspaceMultiplicityByServerId: input.workspaceMultiplicityByServerId, }), [ input.allServerIds, input.hostConnectionStatusByServerId, input.initialServerId, input.lastActiveProject, input.projects, input.workspaceMultiplicityByServerId, ], ); const [automaticSelection, setAutomaticSelection] = useState(() => ({ routeServerId, serverId: defaultServerId, })); const [manualSelection, setManualSelection] = useState<{ routeServerId: string; serverId: string; } | null>(null); const [hostPickerOpen, setHostPickerOpen] = useState(false); useEffect(() => { setAutomaticSelection((current) => { const nextServerId = current.routeServerId === routeServerId ? resolveNewWorkspaceAutomaticServerId({ allServerIds: input.allServerIds, routeServerId: input.initialServerId, lastActiveProject: input.lastActiveProject, projects: input.projects, hostConnectionStatusByServerId: input.hostConnectionStatusByServerId, workspaceMultiplicityByServerId: input.workspaceMultiplicityByServerId, currentServerId: current.serverId, nextServerId: defaultServerId, }) : defaultServerId; if (current.routeServerId === routeServerId && current.serverId === nextServerId) { return current; } return { routeServerId, serverId: nextServerId }; }); }, [ defaultServerId, input.allServerIds, input.hostConnectionStatusByServerId, input.initialServerId, input.lastActiveProject, input.projects, input.workspaceMultiplicityByServerId, routeServerId, ]); const automaticServerId = automaticSelection.routeServerId === routeServerId && input.allServerIds.includes(automaticSelection.serverId) ? automaticSelection.serverId : defaultServerId; const selectedServerId = manualSelection?.routeServerId === routeServerId && input.allServerIds.includes(manualSelection.serverId) ? manualSelection.serverId : automaticServerId; const handleSelectHost = useCallback( (id: string) => { setManualSelection({ routeServerId, serverId: id }); setHostPickerOpen(false); }, [routeServerId], ); const handleHostPickerOpenChange = useCallback((open: boolean) => { setHostPickerOpen(open); }, []); const openHostPicker = useCallback(() => { setHostPickerOpen(true); }, []); return { selectedServerId, hostPickerOpen, handleSelectHost, handleHostPickerOpenChange, openHostPicker, }; } interface NewWorkspaceInitialContextState { allHosts: HostProfile[]; selectedServerId: string; hostPickerOpen: boolean; handleSelectHost: (id: string) => void; handleHostPickerOpenChange: (open: boolean) => void; openHostPicker: () => void; projects: HostProjectListItem[]; routeProject: HostProjectListItem | null; lastActiveProject: HostProjectListItem | null; } function useNewWorkspaceInitialContext({ serverId, sourceDirectory: sourceDirectoryProp, projectId, displayName: displayNameProp, }: NewWorkspaceScreenProps): NewWorkspaceInitialContextState { const allHosts = useHosts(); const allServerIds = useMemo(() => allHosts.map((h) => h.serverId), [allHosts]); const projects = useHostProjects(allServerIds); const routeDisplayName = displayNameProp?.trim() ?? ""; const routeProject = useMemo( () => hostProjectFromRoute({ serverId, projectId, displayName: routeDisplayName, sourceDirectory: sourceDirectoryProp, }), [projectId, routeDisplayName, serverId, sourceDirectoryProp], ); const lastWorkspaceSelection = useLastWorkspaceSelection(); const lastWorkspaceServerId = useMemo( () => lastWorkspaceSelection && allServerIds.includes(lastWorkspaceSelection.serverId) ? lastWorkspaceSelection.serverId : null, [allServerIds, lastWorkspaceSelection], ); const lastWorkspaceId = lastWorkspaceServerId ? lastWorkspaceSelection!.workspaceId : null; const lastWorkspace = useWorkspace(lastWorkspaceServerId, lastWorkspaceId); const lastActiveProject = useMemo( () => lastWorkspaceServerId ? hostProjectFromWorkspace({ serverId: lastWorkspaceServerId, workspace: lastWorkspace }) : null, [lastWorkspace, lastWorkspaceServerId], ); const hostConnectionStatusByServerId = useHostRuntimeConnectionStatuses(allServerIds); const workspaceMultiplicityByServerId = useHostFeatureMap(allServerIds, "workspaceMultiplicity"); const { selectedServerId, hostPickerOpen, handleSelectHost, handleHostPickerOpenChange, openHostPicker, } = useNewWorkspaceHostSelector({ initialServerId: serverId, allServerIds, projects, lastActiveProject, hostConnectionStatusByServerId, workspaceMultiplicityByServerId, }); return { allHosts, selectedServerId, hostPickerOpen, handleSelectHost, handleHostPickerOpenChange, openHostPicker, projects, routeProject, lastActiveProject, }; } type RefPickerRenderOption = NonNullable; interface FormPickerControl { anchorRef: RefObject; open: () => void; openState: boolean; onOpenChange: (open: boolean) => void; } interface NewWorkspaceFormStackInput { isCompact: boolean; isPending: boolean; project: FormPickerControl & { options: ComboboxOptionType[]; triggerLabel: string; selectedProject: HostProjectListItem | null; iconDataByProjectViewKey: Map; selectedOptionId: string; onSelect: (id: string) => void; onAddProject: () => void; renderOption: RefPickerRenderOption; }; host: FormPickerControl & { allHosts: HostProfile[]; selectedServerId: string; onSelect: (id: string) => void; }; isolation: FormPickerControl & { effectiveIsolation: "local" | "worktree"; options: ComboboxOptionType[]; onSelect: (id: string) => void; renderOption: RefPickerRenderOption; canCreateWorktree: boolean; }; base: FormPickerControl & { selectedSourceDirectory: string | null; selectedItem: PickerItem | null; triggerLabel: string; options: ComboboxOptionType[]; selectedOptionId: string; onSelect: (id: string) => void; setSearchQuery: (query: string) => void; emptyText: string; renderOption: RefPickerRenderOption; showRefPicker: boolean; }; } function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactElement { const { theme } = useUnistyles(); const { t } = useTranslation(); const { isCompact, isPending, project, host, isolation, base } = input; const selectedHostLabel = host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host"; const showHostControl = host.allHosts.length > 1; const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation); const addProjectAction = useMemo( () => , [project.onAddProject], ); const badgePressableStyle = useCallback( ({ pressed, hovered }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.badge, Boolean(hovered) && !isPending && styles.badgeHovered, pressed && !isPending && styles.badgePressed, isPending && styles.badgeDisabled, ], [isPending], ); const projectControl = ( ); const hostControl = showHostControl ? ( {selectedHostLabel} ) : null; const isolationControl = isolation.canCreateWorktree ? ( ) : null; const baseControl = base.showRefPicker ? ( ) : null; return isCompact ? ( {projectControl} {hostControl ? {hostControl} : null} {/* Keep fixed row height when git-only controls are hidden. */} {isolationControl ? ( {isolationControl} ) : ( )} {baseControl ? {baseControl} : } ) : ( {projectControl} {hostControl} {isolationControl} {baseControl} ); } export function NewWorkspaceScreen({ serverId, sourceDirectory: sourceDirectoryProp, projectId, displayName: displayNameProp, draftId, }: NewWorkspaceScreenProps) { const queryClient = useQueryClient(); const { theme } = useUnistyles(); const { t } = useTranslation(); const insets = useSafeAreaInsets(); const isCompact = useIsCompactFormFactor(); const toast = useToast(); const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); const { allHosts, selectedServerId, hostPickerOpen, handleSelectHost, handleHostPickerOpenChange, openHostPicker, projects, routeProject, lastActiveProject, } = useNewWorkspaceInitialContext({ serverId, sourceDirectory: sourceDirectoryProp, projectId, displayName: displayNameProp, }); // COMPAT(workspaceMultiplicity): added in v0.1.97, drop the gate when floor >= v0.1.97 const supportsWorkspaceMultiplicity = useHostFeature(selectedServerId, "workspaceMultiplicity"); const supportsForgeSearch = useHostFeature(selectedServerId, "forgeSearch"); const [errorMessage, setErrorMessage] = useState(null); const [createdWorkspace, setCreatedWorkspace] = useState | null>(null); const [pendingAction, setPendingAction] = useState<"chat" | "empty" | null>(null); const [pickerOpen, setPickerOpen] = useState(false); const [projectPickerOpen, setProjectPickerOpen] = useState(false); const openAddProjectPicker = useOpenAddProject(); const [isolationPickerOpen, setIsolationPickerOpen] = useState(false); const [pickerSearchQuery, setPickerSearchQuery] = useState(""); const [debouncedPickerSearchQuery, setDebouncedPickerSearchQuery] = useState(""); const pickerAnchorRef = useRef(null); const projectPickerAnchorRef = useRef(null); const isolationPickerAnchorRef = useRef(null); const hostPickerAnchorRef = useRef(null); const isDraftHandoffActive = useIsNewWorkspaceDraftHandoffActive({ draftId, selectedServerId }); useEffect(() => { const trimmed = pickerSearchQuery.trim(); const timer = setTimeout(() => setDebouncedPickerSearchQuery(trimmed), 180); return () => clearTimeout(timer); }, [pickerSearchQuery]); const workspace = createdWorkspace; const client = useHostRuntimeClient(selectedServerId); const isConnected = useHostRuntimeIsConnected(selectedServerId); const { selectedProject, selectedSourceDirectory, projectPickerOptions, projectByOptionId, selectedProjectOptionId, projectTriggerLabel, handleSelectProjectOption: selectProjectOption, } = useNewWorkspaceProjectPicker({ selectedServerId, projects, routeProject, lastActiveProject, allowAllProjects: supportsWorkspaceMultiplicity, }); const projectIconTargets = useMemo( () => projects.flatMap((project) => { const iconWorkingDir = getHostProjectSourceDirectory(project, selectedServerId)?.trim(); if (!iconWorkingDir) { return []; } return [{ projectViewKey: project.viewKey, serverId: selectedServerId, iconWorkingDir }]; }), [projects, selectedServerId], ); const projectIconDataByProjectViewKey = useProjectIconDataByProjectViewKey({ projects: projectIconTargets, }); const draftKey = buildNewWorkspaceDraftKey(draftId); const forkDraftSetup = usePendingWorkspaceDraftSetup(draftId); const draftContextScopeKey = useDraftWorkspaceAttachmentScopeKey(draftId); const visibleDraftContextScopeKeys = useMemo( () => resolveVisibleDraftContextScopeKeys({ isDraftHandoffActive, draftContextScopeKey }), [draftContextScopeKey, isDraftHandoffActive], ); const chatDraft = useAgentInputDraft({ draftKey, composer: buildComposerConfig({ serverId: selectedServerId, isConnected, workspaceDirectory: workspace?.workspaceDirectory ?? null, sourceDirectory: selectedSourceDirectory, initialSetup: forkDraftSetup?.setup, }), }); const composerState = chatDraft.composerState; const [pickerSelection, dispatchPickerSelection] = useReducer( reducePickerSelection, initialPickerSelectionState, ); const selectedItem = pickerSelection.selectedItem; const handleGithubPrDetected = useCallback(() => { dispatchPickerSelection({ type: "pr-detected" }); }, []); const handleGithubPrAutoAttach = useCallback((item: ForgeSearchItem) => { dispatchPickerSelection({ type: "pr-added", item: { kind: "github-pr", item }, }); }, []); const withConnectedClient = useCallback(() => { if (!client || !isConnected) { throw new Error(t("newWorkspace.errors.hostDisconnected")); } return client; }, [client, isConnected, t]); const clientReady = isConnected && Boolean(client); const hasSelectedSourceDirectory = selectedSourceDirectory !== null; const pickerQueryEnabled = pickerOpen && clientReady && hasSelectedSourceDirectory; const { status: checkoutStatus } = useCheckoutStatusQuery({ serverId: selectedServerId, cwd: selectedSourceDirectory ?? "", }); const currentBranch = checkoutStatus?.currentBranch ?? null; const worktreeSupport = selectedProject ? getWorktreeSupportForHostProject({ project: selectedProject, serverId: selectedServerId }) : "unsupported"; const isPending = isNewWorkspacePending({ pendingAction, isDraftHandoffActive }); const { effectiveIsolation, setIsolation, canCreateWorktree, showRefPicker } = useWorkspaceIsolation({ supportsMultiplicity: supportsWorkspaceMultiplicity, worktreeSupport, }); const branchSuggestionsQuery = useQuery({ queryKey: [ "branch-suggestions", selectedServerId, selectedSourceDirectory, debouncedPickerSearchQuery, ], queryFn: async () => { if (!selectedSourceDirectory) { throw new Error("Choose a project"); } const connectedClient = withConnectedClient(); return connectedClient.getBranchSuggestions({ cwd: selectedSourceDirectory, query: debouncedPickerSearchQuery || undefined, limit: 20, }); }, enabled: pickerQueryEnabled, staleTime: 15_000, }); const githubPrSearchQuery = useForgeSearchQuery({ client, serverId: selectedServerId, cwd: selectedSourceDirectory ?? "", query: debouncedPickerSearchQuery, kinds: ["change_request"], supportsForgeSearch, enabled: pickerQueryEnabled, }); const branchDetails = useMemo( () => normalizeBranchDetails(branchSuggestionsQuery.data), [branchSuggestionsQuery.data], ); const forgeSearchAuthenticated = !githubPrSearchQuery.data || githubPrSearchQuery.data.authState === "authenticated"; const prItems: ForgeSearchItem[] = useMemo(() => { if (!forgeSearchAuthenticated) return []; return githubPrSearchQuery.data?.items ?? []; }, [forgeSearchAuthenticated, githubPrSearchQuery.data?.items]); const { options, itemById }: PickerOptionData = useMemo( () => computePickerOptionData(branchDetails, prItems), [branchDetails, prItems], ); const triggerLabel = useMemo(() => { if (selectedItem) return pickerItemTriggerLabel(selectedItem); return currentBranch ?? "main"; }, [currentBranch, selectedItem]); const selectedOptionId = useMemo(() => { if (!selectedItem) return ""; return selectedItem.kind === "branch" ? branchOptionId(selectedItem.name) : prOptionId(selectedItem.item.number); }, [selectedItem]); const selectPickerItem = useCallback( (item: PickerItem) => { const nextAttachments = syncPickerPrAttachment({ attachments: chatDraft.attachments, item, }); dispatchPickerSelection({ type: "picker-selected", item }); chatDraft.setAttachments(nextAttachments); setPickerOpen(false); }, [chatDraft], ); const handleSelectOption = useCallback( (id: string) => { const item = itemById.get(id); if (!item) return; selectPickerItem(item); }, [itemById, selectPickerItem], ); const clearPickerSelectionForTargetChange = useCallback( (currentTargetId: string, nextTargetId: string) => { const nextAttachments = clearPickerPrAttachmentForTargetChange({ attachments: chatDraft.attachments, currentTargetId, nextTargetId, }); if (nextAttachments === chatDraft.attachments) return; chatDraft.setAttachments(nextAttachments); dispatchPickerSelection({ type: "target-changed" }); }, [chatDraft], ); const handleSelectProjectOption = useCallback( (id: string) => { // selectProjectOption enforces selectability (worktree-only when // multiplicity is off, any project when it's on); don't re-gate here on // canCreateWorktree or non-git projects become unselectable. selectProjectOption(id); setProjectPickerOpen(false); clearPickerSelectionForTargetChange(selectedProjectOptionId, id); }, [clearPickerSelectionForTargetChange, selectProjectOption, selectedProjectOptionId], ); const handleSelectWorkspaceHost = useCallback( (id: string) => { handleSelectHost(id); clearPickerSelectionForTargetChange(selectedServerId, id); }, [clearPickerSelectionForTargetChange, handleSelectHost, selectedServerId], ); const handleAddProject = useCallback(() => { setProjectPickerOpen(false); openAddProjectPicker(selectedServerId); }, [openAddProjectPicker, selectedServerId]); const openPicker = useCallback(() => { setPickerOpen(true); }, []); const openProjectPicker = useCallback(() => { setProjectPickerOpen(true); }, []); // Cmd/Ctrl+P opens the project picker with its search focused so the user can // switch projects from the keyboard. Registered only while this screen is // mounted, so the shortcut doesn't swallow the browser's native print // elsewhere; gated on having projects to pick. const handleProjectPick = useCallback(() => { openProjectPicker(); return true; }, [openProjectPicker]); useKeyboardActionHandler({ handlerId: "new-workspace-project-pick", actions: PROJECT_PICK_ACTIONS, enabled: projectPickerOptions.length > 0, priority: 0, handle: handleProjectPick, }); const openIsolationPicker = useCallback(() => { setIsolationPickerOpen(true); }, []); const handleIsolationPickerOpenChange = useCallback((nextOpen: boolean) => { setIsolationPickerOpen(nextOpen); }, []); // "New worktree" is omitted entirely (not disabled) when the project isn't a // git checkout, since worktree isolation is impossible there. const isolationOptions = useMemo(() => { const localOption = { id: "local", label: isolationLabel(t, "local") }; if (!canCreateWorktree) return [localOption]; return [localOption, { id: "worktree", label: isolationLabel(t, "worktree") }]; }, [canCreateWorktree, t]); const handleSelectIsolationOption = useCallback( (id: string) => { setIsolation(id === "worktree" ? "worktree" : "local"); setIsolationPickerOpen(false); }, [setIsolation], ); const renderIsolationOption = useCallback( ({ option, selected, active, onPress, }: { option: ComboboxOptionType; selected: boolean; active: boolean; onPress: () => void; }) => { return ( ); }, [isPending, theme.colors.foregroundMuted, theme.iconSize.sm], ); const handleClearDraft = useCallback(() => { // No-op: screen navigates away on success, text should stay for retry on error }, []); const handlePickerOpenChange = useCallback((nextOpen: boolean) => { setPickerOpen(nextOpen); if (!nextOpen) { setPickerSearchQuery(""); } }, []); const handleProjectPickerOpenChange = useCallback((nextOpen: boolean) => { setProjectPickerOpen(nextOpen); }, []); const buildCreateWorktreeInput = useCallback( (input: { cwd: string; prompt: string; attachments: AgentAttachment[]; checkoutRequest: PickerCheckoutRequest | undefined; }): CreatePaseoWorktreeInput => { if (!selectedProject) { throw new Error("Choose a project"); } if (!selectedSourceDirectory) { throw new Error("Choose a host for this project"); } const firstAgentContext = buildFirstAgentContext(input); const hostProjectId = getHostProjectId(selectedProject, selectedServerId); if (!hostProjectId) { throw new Error("Project is not available on the selected host"); } return { cwd: selectedSourceDirectory, projectId: hostProjectId, worktreeSlug: createNameId(), ...(firstAgentContext ? { firstAgentContext } : {}), ...input.checkoutRequest, }; }, [selectedProject, selectedServerId, selectedSourceDirectory], ); const ensureWorkspace = useCallback( async (input: { cwd: string; prompt: string; attachments: AgentAttachment[]; withInitialAgent: boolean; }) => { if (createdWorkspace) { return createdWorkspace; } if (!selectedProject) { throw new Error("Choose a project"); } if (!selectedSourceDirectory) { throw new Error("Choose a host for this project"); } const connectedClient = withConnectedClient(); const createsWorktree = !supportsWorkspaceMultiplicity || effectiveIsolation === "worktree"; const checkoutRequest = createsWorktree ? resolveCheckoutRequest( selectedItem, await ensureCheckoutStatus({ queryClient, client: connectedClient, serverId: selectedServerId, cwd: selectedSourceDirectory, }), ) : undefined; const normalizedWorkspace = supportsWorkspaceMultiplicity ? await createMultiplicityWorkspace({ client: connectedClient, isolation: effectiveIsolation, project: selectedProject, sourceDirectory: selectedSourceDirectory, checkoutRequest, withInitialAgent: input.withInitialAgent, prompt: input.prompt, attachments: input.attachments, mergeWorkspaces, serverId: selectedServerId, createFailedMessage: t("newWorkspace.errors.createWorktreeFailed"), }) : await createAndMergeWorkspace({ client: connectedClient, createInput: buildCreateWorktreeInput({ ...input, checkoutRequest }), mergeWorkspaces, serverId: selectedServerId, createFailedMessage: t("newWorkspace.errors.createWorktreeFailed"), }); setCreatedWorkspace(normalizedWorkspace); return normalizedWorkspace; }, [ buildCreateWorktreeInput, createdWorkspace, effectiveIsolation, mergeWorkspaces, queryClient, selectedItem, selectedProject, selectedServerId, selectedSourceDirectory, supportsWorkspaceMultiplicity, t, withConnectedClient, ], ); const handleSubmitNewWorkspace = useCallback( async (payload: MessagePayload) => { try { setErrorMessage(null); await composerState?.persistFormPreferences(); if (isEmptyWorkspaceSubmission(payload)) { setPendingAction("empty"); await runCreateEmptyWorkspace({ payload, ensureWorkspace, serverId: selectedServerId, navigate: (targetServerId, workspaceId) => navigateToWorkspace({ serverId: targetServerId, workspaceId }), }); return; } setPendingAction("chat"); await runCreateChatAgent({ payload, composerState, forkDraftSetup, ensureWorkspace, serverId: selectedServerId, draftKey, draftId, supportsForgeSearch, labels: { composerStateRequired: t("newWorkspace.errors.composerStateRequired"), selectModel: t("newWorkspace.errors.selectModel"), }, }); } catch (error) { const message = toErrorMessage(error); setPendingAction(null); setErrorMessage(message); toast.error(message); } }, [ composerState, draftId, draftKey, ensureWorkspace, forkDraftSetup, selectedServerId, supportsForgeSearch, t, toast, ], ); const renderPickerOption = useCallback( (props: { option: ComboboxOptionType; selected: boolean; active: boolean; onPress: () => void; }) => , [isPending, itemById], ); const renderProjectOption = useCallback( (props: { option: ComboboxOptionType; selected: boolean; active: boolean; onPress: () => void; }) => ( ), [ isPending, projectByOptionId, projectIconDataByProjectViewKey, selectedServerId, supportsWorkspaceMultiplicity, ], ); const contentStyle = useMemo( () => getContentStyle({ isCompact, insetBottom: insets.bottom }), [isCompact, insets.bottom], ); const { style: composerKeyboardStyle } = useKeyboardShiftStyle({ mode: "translate", }); const centeredStyle = useMemo( () => [animatedStaticStyles.centered, composerKeyboardStyle], [composerKeyboardStyle], ); const agentControlsWithDisabled = useMemo( () => composerState ? { ...composerState.agentControls, disabled: isPending, } : undefined, [composerState, isPending], ); const pickerEmptyText = branchSuggestionsQuery.isFetching || githubPrSearchQuery.isFetching ? t("newWorkspace.refPicker.searching") : t("newWorkspace.refPicker.noMatchingRefs"); const formStack = useNewWorkspaceFormStack({ isCompact, isPending, project: { anchorRef: projectPickerAnchorRef, open: openProjectPicker, options: projectPickerOptions, triggerLabel: projectTriggerLabel, selectedProject, iconDataByProjectViewKey: projectIconDataByProjectViewKey, selectedOptionId: selectedProjectOptionId, onSelect: handleSelectProjectOption, onAddProject: handleAddProject, openState: projectPickerOpen, onOpenChange: handleProjectPickerOpenChange, renderOption: renderProjectOption, }, host: { allHosts, selectedServerId, onSelect: handleSelectWorkspaceHost, openState: hostPickerOpen, onOpenChange: handleHostPickerOpenChange, anchorRef: hostPickerAnchorRef, open: openHostPicker, }, isolation: { anchorRef: isolationPickerAnchorRef, open: openIsolationPicker, effectiveIsolation, options: isolationOptions, onSelect: handleSelectIsolationOption, openState: isolationPickerOpen, onOpenChange: handleIsolationPickerOpenChange, renderOption: renderIsolationOption, canCreateWorktree, }, base: { anchorRef: pickerAnchorRef, open: openPicker, selectedSourceDirectory, selectedItem, triggerLabel, options, selectedOptionId, onSelect: handleSelectOption, openState: pickerOpen, onOpenChange: handlePickerOpenChange, setSearchQuery: setPickerSearchQuery, emptyText: pickerEmptyText, renderOption: renderPickerOption, showRefPicker, }, }); const screenHeaderLeft = useMemo(() => , []); return ( {t("newWorkspace.title")} {formStack} {errorMessage ? {errorMessage} : null} ); } const animatedStaticStyles = RNStyleSheet.create({ centered: { width: "100%", maxWidth: MAX_CONTENT_WIDTH, }, }); const styles = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.surface0, userSelect: "none", }, content: { position: "relative", flex: 1, alignItems: "center", }, contentCentered: { justifyContent: "center", paddingBottom: HEADER_INNER_HEIGHT + theme.spacing[6], }, contentCompact: { justifyContent: "flex-end", }, composerTitleContainer: { marginBottom: theme.spacing[8], paddingLeft: theme.spacing[6], paddingRight: theme.spacing[4], }, composerTitle: { fontSize: theme.fontSize.xl, fontWeight: theme.fontWeight.normal, color: theme.colors.foreground, }, errorText: { fontSize: theme.fontSize.sm, color: theme.colors.destructive, lineHeight: 20, }, formStack: { marginBottom: theme.spacing[8], gap: theme.spacing[2], }, formStackDesktop: { flexDirection: "row", alignItems: "center", marginBottom: theme.spacing[8], // The badge adds its own left padding; offset it so the project icon's left // edge lands exactly on the "New workspace" title's left edge. paddingLeft: theme.spacing[4], gap: theme.spacing[2], }, // The row's left inset matches the heading's text x (composerTitleContainer // paddingLeft) so the control aligns with the "New workspace" glyph. The badge // adds its own left padding, so the row inset is reduced by that amount. row: { flexDirection: "row", alignItems: "center", paddingLeft: theme.spacing[4], gap: theme.spacing[1], }, baseSpacer: { height: BADGE_HEIGHT, }, badge: { flexDirection: "row", alignItems: "center", height: BADGE_HEIGHT, maxWidth: 240, overflow: "hidden", paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius["2xl"], gap: theme.spacing[1], }, badgeHovered: { backgroundColor: theme.colors.surface2, }, badgePressed: { backgroundColor: theme.colors.surface0, }, badgeDisabled: { opacity: 0.6, }, badgeText: { minWidth: 0, fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, flexShrink: 1, }, tooltipText: { fontSize: theme.fontSize.sm, color: theme.colors.popoverForeground, }, badgeIconBox: { width: theme.iconSize.md, height: theme.iconSize.md, alignItems: "center", justifyContent: "center", flexShrink: 0, }, projectIcon: { width: theme.iconSize.md, height: theme.iconSize.md, borderRadius: theme.borderRadius.sm, }, projectIconFallback: { width: theme.iconSize.md, height: theme.iconSize.md, borderRadius: theme.borderRadius.sm, alignItems: "center", justifyContent: "center", }, projectIconFallbackText: { // Single uppercase initial inside an iconSize.md (16px) square — below the // smallest font-size token, so it stays a literal sized to the box. fontSize: PROJECT_ICON_FALLBACK_FONT_SIZE, fontWeight: "600", }, rowIconBox: { width: theme.iconSize.md, height: theme.iconSize.md, alignItems: "center", justifyContent: "center", }, hostStatusDot: { width: 8, height: 8, borderRadius: 4, }, }));