diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index e1c049678..d51118acd 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -769,6 +769,7 @@ function RootStack() {
+
diff --git a/packages/app/src/app/h/[serverId]/new.tsx b/packages/app/src/app/h/[serverId]/new.tsx
new file mode 100644
index 000000000..051822df3
--- /dev/null
+++ b/packages/app/src/app/h/[serverId]/new.tsx
@@ -0,0 +1,21 @@
+import { useLocalSearchParams } from "expo-router";
+import { NewWorkspaceScreen } from "@/screens/new-workspace-screen";
+
+export default function HostNewWorkspaceRoute() {
+ const params = useLocalSearchParams<{ serverId?: string; dir?: string; name?: string }>();
+ const serverId = typeof params.serverId === "string" ? params.serverId : "";
+ const sourceDirectory = typeof params.dir === "string" ? params.dir : "";
+ const displayName = typeof params.name === "string" ? params.name : undefined;
+
+ if (!sourceDirectory) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx
index a8aaa051d..a22cc525d 100644
--- a/packages/app/src/components/combined-model-selector.tsx
+++ b/packages/app/src/components/combined-model-selector.tsx
@@ -2,13 +2,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
View,
Text,
- TextInput,
Pressable,
Platform,
ActivityIndicator,
type GestureResponderEvent,
} from "react-native";
-import { BottomSheetTextInput } from "@gorhom/bottom-sheet";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import {
ArrowLeft,
@@ -24,7 +22,7 @@ import type {
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
const IS_WEB = Platform.OS === "web";
-import { Combobox, ComboboxItem } from "@/components/ui/combobox";
+import { Combobox, ComboboxItem, SearchInput } from "@/components/ui/combobox";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { getProviderIcon } from "@/components/provider-icons";
import type { FavoriteModelRow } from "@/hooks/use-form-preferences";
@@ -347,45 +345,6 @@ function GroupedProviderRows({
);
}
-function ProviderSearchInput({
- value,
- onChangeText,
- autoFocus = false,
-}: {
- value: string;
- onChangeText: (text: string) => void;
- autoFocus?: boolean;
-}) {
- const { theme } = useUnistyles();
- const inputRef = useRef(null);
- const InputComponent = Platform.OS === "web" ? TextInput : BottomSheetTextInput;
-
- useEffect(() => {
- if (autoFocus && Platform.OS === "web" && inputRef.current) {
- const timer = setTimeout(() => {
- inputRef.current?.focus();
- }, 50);
- return () => clearTimeout(timer);
- }
- }, [autoFocus]);
-
- return (
-
-
-
-
- );
-}
function SelectorContent({
view,
@@ -674,7 +633,8 @@ export function CombinedModelSelector({
}}
/>
) : null}
- ({
color: theme.colors.foregroundMuted,
},
level2Header: {
- backgroundColor: theme.colors.surface1,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.border,
},
backButton: {
flexDirection: "row",
@@ -853,17 +810,4 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
- providerSearchContainer: {
- flexDirection: "row",
- alignItems: "center",
- paddingHorizontal: theme.spacing[3],
- gap: theme.spacing[2],
- ...(IS_WEB ? {} : { marginHorizontal: theme.spacing[1] }),
- },
- providerSearchInput: {
- flex: 1,
- paddingVertical: theme.spacing[3],
- color: theme.colors.foreground,
- fontSize: theme.fontSize.sm,
- },
}));
diff --git a/packages/app/src/components/composer.tsx b/packages/app/src/components/composer.tsx
index a30d59139..f337fec01 100644
--- a/packages/app/src/components/composer.tsx
+++ b/packages/app/src/components/composer.tsx
@@ -610,40 +610,44 @@ export function Composer({
) : null;
- const rightContent = (
-
- {!isVoiceModeForAgent && hasAgent ? (
-
- [
- styles.realtimeVoiceButton as any,
- (hovered ? styles.iconButtonHovered : undefined) as any,
- (!isConnected || voice?.isVoiceSwitching ? styles.buttonDisabled : undefined) as any,
- ]}
- >
- {voice?.isVoiceSwitching ? (
-
- ) : (
-
- )}
-
-
-
- Voice mode
- {voiceToggleKeys ? (
-
- ) : null}
-
-
-
- ) : null}
- {cancelButton}
-
- );
+ const showVoiceModeButton = !isVoiceModeForAgent && hasAgent;
+ const rightContent =
+ showVoiceModeButton || cancelButton ? (
+
+ {showVoiceModeButton ? (
+
+ [
+ styles.realtimeVoiceButton as any,
+ (hovered ? styles.iconButtonHovered : undefined) as any,
+ (!isConnected || voice?.isVoiceSwitching ? styles.buttonDisabled : undefined) as any,
+ ]}
+ >
+ {({ hovered }) =>
+ voice?.isVoiceSwitching ? (
+
+ ) : (
+
+ )
+ }
+
+
+
+ Voice mode
+ {voiceToggleKeys ? (
+
+ ) : null}
+
+
+
+ ) : null}
+ {cancelButton}
+
+ ) : null;
const leftContent =
resolveStatusControlMode(statusControls) === "draft" && statusControls ? (
diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx
index 6280ccc93..8240bc1c0 100644
--- a/packages/app/src/components/message-input.tsx
+++ b/packages/app/src/components/message-input.tsx
@@ -111,9 +111,11 @@ export interface MessageInputRef {
getNativeElement?: () => HTMLElement | null;
}
-const MIN_INPUT_HEIGHT = 30;
+const MIN_INPUT_HEIGHT_MOBILE = 30;
+const MIN_INPUT_HEIGHT_DESKTOP = 46;
const MAX_INPUT_HEIGHT = 160;
const IS_WEB = Platform.OS === "web";
+const MIN_INPUT_HEIGHT = IS_WEB ? MIN_INPUT_HEIGHT_DESKTOP : MIN_INPUT_HEIGHT_MOBILE;
type WebTextInputKeyPressEvent = NativeSyntheticEvent<
TextInputKeyPressEventData & {
@@ -1007,7 +1009,9 @@ export const MessageInput = forwardRef(funct
(!isConnected || disabled) && styles.buttonDisabled,
]}
>
-
+ {({ hovered }) => (
+
+ )}
@@ -1041,13 +1045,15 @@ export const MessageInput = forwardRef(funct
isDictating && styles.voiceButtonRecording,
]}
>
- {isDictating ? (
-
- ) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? (
-
- ) : (
-
- )}
+ {({ hovered }) =>
+ isDictating ? (
+
+ ) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? (
+
+ ) : (
+
+ )
+ }
@@ -1249,6 +1255,7 @@ const styles = StyleSheet.create(((theme: any) => ({
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
+ marginHorizontal: -6,
},
leftButtonGroup: {
flexDirection: "row",
diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx
index 90290fdb5..4367d5b13 100644
--- a/packages/app/src/components/sidebar-workspace-list.tsx
+++ b/packages/app/src/components/sidebar-workspace-list.tsx
@@ -47,7 +47,7 @@ import type { DraggableListDragHandleProps } from "./draggable-list.types";
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
import { getIsElectronRuntime, isCompactFormFactor } from "@/constants/layout";
import { projectIconQueryKey } from "@/hooks/use-project-icon-query";
-import { parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes";
+import { buildHostNewWorkspaceRoute, parseHostWorkspaceRouteFromPathname } from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import {
type SidebarProjectEntry,
@@ -87,7 +87,6 @@ import { type PrHint, useWorkspacePrHint } from "@/hooks/use-checkout-pr-status-
import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model";
import { useNavigationActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
-import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
import { openExternalUrl } from "@/utils/open-external-url";
import {
@@ -721,20 +720,13 @@ function ProjectHeaderRow({
const { theme } = useUnistyles();
const [isHovered, setIsHovered] = useState(false);
const isMobileBreakpoint = isCompactFormFactor();
- const beginWorkspaceSetup = useWorkspaceSetupStore((state) => state.beginWorkspaceSetup);
const handleBeginWorkspaceSetup = useCallback(() => {
if (!serverId) {
return;
}
- beginWorkspaceSetup({
- serverId,
- sourceDirectory: project.iconWorkingDir,
- displayName,
- creationMethod: "create_worktree",
- navigationMethod: "navigate",
- });
+ router.navigate(buildHostNewWorkspaceRoute(serverId, project.iconWorkingDir, { displayName }) as any);
onWorkspacePress?.();
- }, [beginWorkspaceSetup, displayName, onWorkspacePress, project.iconWorkingDir, serverId]);
+ }, [displayName, onWorkspacePress, project.iconWorkingDir, serverId]);
useKeyboardActionHandler({
handlerId: `worktree-new-${project.projectKey}`,
@@ -1152,18 +1144,16 @@ function WorkspaceRowWithMenu({
return;
}
+ redirectAfterArchive();
+
void archiveWorktree({
serverId: workspace.serverId,
cwd: workspaceDirectory,
worktreePath: workspaceDirectory,
- })
- .then(() => {
- redirectAfterArchive();
- })
- .catch((error) => {
- const message = error instanceof Error ? error.message : "Failed to archive worktree";
- toast.error(message);
- });
+ }).catch((error) => {
+ const message = error instanceof Error ? error.message : "Failed to archive worktree";
+ toast.error(message);
+ });
})();
}, [
archiveWorktree,
@@ -1200,17 +1190,20 @@ function WorkspaceRowWithMenu({
}
setIsArchivingWorkspace(true);
- try {
- const payload = await client.archiveWorkspace(Number(workspace.workspaceId));
- if (payload.error) {
- throw new Error(payload.error);
+ redirectAfterArchive();
+
+ void (async () => {
+ try {
+ const payload = await client.archiveWorkspace(Number(workspace.workspaceId));
+ if (payload.error) {
+ throw new Error(payload.error);
+ }
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : "Failed to hide workspace");
+ } finally {
+ setIsArchivingWorkspace(false);
}
- redirectAfterArchive();
- } catch (error) {
- toast.error(error instanceof Error ? error.message : "Failed to hide workspace");
- } finally {
- setIsArchivingWorkspace(false);
- }
+ })();
})();
}, [
isArchivingWorkspace,
@@ -1355,17 +1348,20 @@ function NonGitProjectRowWithMenuContent({
}
setIsArchivingWorkspace(true);
- try {
- const payload = await client.archiveWorkspace(Number(workspace.workspaceId));
- if (payload.error) {
- throw new Error(payload.error);
+ redirectAfterArchive();
+
+ void (async () => {
+ try {
+ const payload = await client.archiveWorkspace(Number(workspace.workspaceId));
+ if (payload.error) {
+ throw new Error(payload.error);
+ }
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : "Failed to hide workspace");
+ } finally {
+ setIsArchivingWorkspace(false);
}
- redirectAfterArchive();
- } catch (error) {
- toast.error(error instanceof Error ? error.message : "Failed to hide workspace");
- } finally {
- setIsArchivingWorkspace(false);
- }
+ })();
})();
}, [
isArchivingWorkspace,
@@ -1707,18 +1703,21 @@ function ProjectBlock({
}
setIsRemovingProject(true);
- try {
- for (const ws of project.workspaces) {
+
+ void Promise.allSettled(
+ project.workspaces.map(async (ws) => {
const payload = await client.archiveWorkspace(Number(ws.workspaceId));
if (payload.error) {
throw new Error(payload.error);
}
+ }),
+ ).then((results) => {
+ const failed = results.filter((r) => r.status === "rejected");
+ if (failed.length > 0) {
+ toast.error("Failed to remove some workspaces");
}
- } catch (error) {
- toast.error(error instanceof Error ? error.message : "Failed to remove project");
- } finally {
setIsRemovingProject(false);
- }
+ });
})();
}, [isRemovingProject, serverId, displayName, toast, project.workspaces]);
diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx
index ab4ec98f8..a13ab5c45 100644
--- a/packages/app/src/components/ui/combobox.tsx
+++ b/packages/app/src/components/ui/combobox.tsx
@@ -659,13 +659,7 @@ export function Combobox({
>
);
- const defaultContent = (
- <>
- {effectiveOptionsPosition === "above-search" ? optionsList : null}
- {searchable ? searchInput : null}
- {effectiveOptionsPosition === "below-search" ? optionsList : null}
- >
- );
+ const defaultContent = optionsList;
const content = children ?? defaultContent;
@@ -690,6 +684,7 @@ export function Combobox({
{title}
{stickyHeader}
+ {!children && searchable ? searchInput : null}
) : (
<>
+ {searchable ? searchInput : null}
{effectiveOptionsPosition === "above-search" ? (
{optionsList}
- ) : null}
- {searchable ? searchInput : null}
- {effectiveOptionsPosition === "below-search" ? (
+ ) : (
{optionsList}
- ) : null}
+ )}
>
)}
@@ -783,15 +777,12 @@ const styles = StyleSheet.create((theme) => ({
searchInputContainer: {
flexDirection: "row",
alignItems: "center",
- borderWidth: 1,
- borderColor: theme.colors.border,
- backgroundColor: theme.colors.surface1,
- borderRadius: theme.borderRadius.lg,
paddingHorizontal: theme.spacing[3],
- marginHorizontal: theme.spacing[2],
- marginBottom: theme.spacing[2],
- marginTop: theme.spacing[1],
gap: theme.spacing[2],
+ backgroundColor: theme.colors.surface1,
+ borderBottomWidth: 1,
+ borderBottomColor: theme.colors.border,
+ ...(IS_WEB ? {} : { marginHorizontal: theme.spacing[1] }),
},
searchInput: {
flex: 1,
diff --git a/packages/app/src/components/ui/tooltip.tsx b/packages/app/src/components/ui/tooltip.tsx
index 1b08ed45b..6aad9afbc 100644
--- a/packages/app/src/components/ui/tooltip.tsx
+++ b/packages/app/src/components/ui/tooltip.tsx
@@ -246,12 +246,11 @@ export function TooltipTrigger({
asChild = false,
triggerRefProp = "ref",
...props
-}: PropsWithChildren<
- PressableProps & {
+}: PressableProps & {
asChild?: boolean;
triggerRefProp?: string;
}
->): ReactElement {
+): ReactElement {
const ctx = useTooltipContext("TooltipTrigger");
const openTimerRef = useRef | null>(null);
diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx
new file mode 100644
index 000000000..1b55c8789
--- /dev/null
+++ b/packages/app/src/screens/new-workspace-screen.tsx
@@ -0,0 +1,375 @@
+import { useCallback, useMemo, useRef, useState } from "react";
+import { Pressable, Text, View } from "react-native";
+import { StyleSheet, useUnistyles } from "react-native-unistyles";
+import { createNameId } from "mnemonic-id";
+import { useQuery } from "@tanstack/react-query";
+import { ChevronDown, GitBranch } from "lucide-react-native";
+import { Composer } from "@/components/composer";
+import { Combobox, ComboboxItem } from "@/components/ui/combobox";
+import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox";
+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,
+ HEADER_INNER_HEIGHT_MOBILE,
+ HEADER_TOP_PADDING_MOBILE,
+ MAX_CONTENT_WIDTH,
+} from "@/constants/layout";
+import { useToast } from "@/contexts/toast-context";
+import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
+import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
+import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
+import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
+import { encodeImages } from "@/utils/encode-images";
+import { toErrorMessage } from "@/utils/error-messages";
+import {
+ requireWorkspaceExecutionAuthority,
+ requireWorkspaceRecordId,
+} from "@/utils/workspace-execution";
+import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
+import type { ImageAttachment, MessagePayload } from "@/components/message-input";
+
+interface NewWorkspaceScreenProps {
+ serverId: string;
+ sourceDirectory: string;
+ displayName?: string;
+}
+
+export function NewWorkspaceScreen({
+ serverId,
+ sourceDirectory,
+ displayName: displayNameProp,
+}: NewWorkspaceScreenProps) {
+ const { theme } = useUnistyles();
+ const toast = useToast();
+ const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
+ const setAgents = useSessionStore((state) => state.setAgents);
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [createdWorkspace, setCreatedWorkspace] = useState | null>(null);
+ const [pendingAction, setPendingAction] = useState<"chat" | null>(null);
+ const [selectedBranch, setSelectedBranch] = useState(null);
+ const [branchPickerOpen, setBranchPickerOpen] = useState(false);
+ const branchAnchorRef = useRef(null);
+
+ const displayName = displayNameProp?.trim() ?? "";
+ const workspace = createdWorkspace;
+ const client = useHostRuntimeClient(serverId);
+ const isConnected = useHostRuntimeIsConnected(serverId);
+ const chatDraft = useAgentInputDraft({
+ draftKey: `new-workspace:${serverId}:${sourceDirectory}`,
+ composer: {
+ initialServerId: serverId || null,
+ initialValues: workspace?.workspaceDirectory
+ ? { workingDir: workspace.workspaceDirectory }
+ : undefined,
+ isVisible: true,
+ onlineServerIds: isConnected && serverId ? [serverId] : [],
+ lockedWorkingDir: workspace?.workspaceDirectory || sourceDirectory || undefined,
+ },
+ });
+ const composerState = chatDraft.composerState;
+
+ const withConnectedClient = useCallback(() => {
+ if (!client || !isConnected) {
+ throw new Error("Host is not connected");
+ }
+ return client;
+ }, [client, isConnected]);
+
+ const checkoutStatusQuery = useQuery({
+ queryKey: ["checkout-status", serverId, sourceDirectory],
+ queryFn: async () => {
+ const connectedClient = withConnectedClient();
+ return connectedClient.getCheckoutStatus(sourceDirectory);
+ },
+ enabled: isConnected && !!client,
+ });
+
+ const currentBranch = checkoutStatusQuery.data?.currentBranch ?? null;
+
+ const branchSuggestionsQuery = useQuery({
+ queryKey: ["branch-suggestions", serverId, sourceDirectory],
+ queryFn: async () => {
+ const connectedClient = withConnectedClient();
+ return connectedClient.getBranchSuggestions({ cwd: sourceDirectory, limit: 20 });
+ },
+ enabled: isConnected && !!client,
+ });
+
+ const branchOptions: ComboboxOptionType[] = useMemo(
+ () =>
+ (branchSuggestionsQuery.data?.branches ?? []).map((branch) => ({
+ id: branch,
+ label: branch,
+ })),
+ [branchSuggestionsQuery.data?.branches],
+ );
+
+ const ensureWorkspace = useCallback(async () => {
+ if (createdWorkspace) {
+ return createdWorkspace;
+ }
+
+ const connectedClient = withConnectedClient();
+ const payload = await connectedClient.createPaseoWorktree({
+ cwd: sourceDirectory,
+ worktreeSlug: createNameId(),
+ });
+
+ if (payload.error || !payload.workspace) {
+ throw new Error(payload.error ?? "Failed to create worktree");
+ }
+
+ const normalizedWorkspace = normalizeWorkspaceDescriptor(payload.workspace);
+ mergeWorkspaces(serverId, [normalizedWorkspace]);
+ setCreatedWorkspace(normalizedWorkspace);
+ return normalizedWorkspace;
+ }, [
+ createdWorkspace,
+ mergeWorkspaces,
+ serverId,
+ sourceDirectory,
+ withConnectedClient,
+ ]);
+
+ const handleCreateChatAgent = useCallback(
+ async ({ text, images }: MessagePayload) => {
+ try {
+ setPendingAction("chat");
+ setErrorMessage(null);
+ const workspace = await ensureWorkspace();
+ const connectedClient = withConnectedClient();
+ if (!composerState) {
+ throw new Error("Composer state is required");
+ }
+
+ const encodedImages = await encodeImages(images);
+ const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
+ const agent = await connectedClient.createAgent({
+ provider: composerState.selectedProvider,
+ cwd: workspaceDirectory,
+ workspaceId: requireWorkspaceRecordId(workspace.id),
+ ...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
+ ? { modeId: composerState.selectedMode }
+ : {}),
+ ...(composerState.effectiveModelId ? { model: composerState.effectiveModelId } : {}),
+ ...(composerState.effectiveThinkingOptionId
+ ? { thinkingOptionId: composerState.effectiveThinkingOptionId }
+ : {}),
+ ...(text.trim() ? { initialPrompt: text.trim() } : {}),
+ ...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}),
+ });
+
+ setAgents(serverId, (previous) => {
+ const next = new Map(previous);
+ next.set(agent.id, normalizeAgentSnapshot(agent, serverId));
+ return next;
+ });
+ navigateToPreparedWorkspaceTab({
+ serverId,
+ workspaceId: workspace.id,
+ target: { kind: "agent", agentId: agent.id },
+ navigationMethod: "replace",
+ });
+ } catch (error) {
+ const message = toErrorMessage(error);
+ setErrorMessage(message);
+ toast.error(message);
+ } finally {
+ setPendingAction(null);
+ }
+ },
+ [composerState, ensureWorkspace, serverId, setAgents, toast, withConnectedClient],
+ );
+
+ const workspaceTitle =
+ workspace?.name ||
+ workspace?.projectDisplayName ||
+ displayName ||
+ sourceDirectory.split(/[\\/]/).filter(Boolean).pop() ||
+ sourceDirectory;
+
+ const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
+ const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
+ addImagesRef.current = addImages;
+ }, []);
+
+ return (
+
+
+
+
+
+ New workspace
+
+
+ {workspaceTitle}
+
+
+ >
+ }
+ leftStyle={styles.headerLeft}
+ borderless
+ />
+
+
+
+
+
+
+ setBranchPickerOpen(true)}
+ style={({ pressed, hovered }) => [
+ styles.badge,
+ hovered && styles.badgeHovered,
+ pressed && styles.badgePressed,
+ ]}
+ accessibilityRole="button"
+ accessibilityLabel="Branch"
+ >
+
+
+ {selectedBranch ?? currentBranch ?? "main"}
+
+
+
+ setSelectedBranch(id)}
+ searchable
+ searchPlaceholder="Search branches"
+ title="Branch"
+ open={branchPickerOpen}
+ onOpenChange={setBranchPickerOpen}
+ desktopPlacement="bottom-start"
+ anchorRef={branchAnchorRef}
+ renderOption={({ option, selected, active, onPress }) => (
+
+ }
+ />
+ )}
+ />
+
+
+ {errorMessage ? {errorMessage} : null}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ container: {
+ flex: 1,
+ backgroundColor: theme.colors.surface0,
+ userSelect: "none",
+ },
+ content: {
+ position: "relative",
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ paddingBottom: {
+ xs: HEADER_INNER_HEIGHT_MOBILE + HEADER_TOP_PADDING_MOBILE + theme.spacing[6],
+ md: HEADER_INNER_HEIGHT + theme.spacing[6],
+ },
+ },
+ centered: {
+ width: "100%",
+ maxWidth: MAX_CONTENT_WIDTH,
+ },
+ headerLeft: {
+ gap: theme.spacing[2],
+ },
+ headerTitleContainer: {
+ flexShrink: 1,
+ minWidth: 0,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ },
+ headerTitle: {
+ fontSize: theme.fontSize.base,
+ fontWeight: {
+ xs: "400",
+ md: "300",
+ },
+ color: theme.colors.foreground,
+ flexShrink: 0,
+ },
+ headerProjectTitle: {
+ color: theme.colors.foregroundMuted,
+ fontSize: theme.fontSize.base,
+ flexShrink: 1,
+ },
+ errorText: {
+ fontSize: theme.fontSize.sm,
+ color: theme.colors.destructive,
+ lineHeight: 20,
+ },
+ optionsRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: theme.spacing[2],
+ paddingHorizontal: theme.spacing[4] + theme.spacing[4] - 6,
+ marginTop: -theme.spacing[2],
+ },
+ badge: {
+ flexDirection: "row",
+ alignItems: "center",
+ height: 28,
+ paddingHorizontal: theme.spacing[2],
+ borderRadius: theme.borderRadius["2xl"],
+ gap: theme.spacing[1],
+ },
+ badgeHovered: {
+ backgroundColor: theme.colors.surface2,
+ },
+ badgePressed: {
+ backgroundColor: theme.colors.surface0,
+ },
+ badgeText: {
+ fontSize: theme.fontSize.sm,
+ color: theme.colors.foregroundMuted,
+ },
+}));
diff --git a/packages/app/src/utils/host-routes.ts b/packages/app/src/utils/host-routes.ts
index 35e6da757..89d61e047 100644
--- a/packages/app/src/utils/host-routes.ts
+++ b/packages/app/src/utils/host-routes.ts
@@ -347,6 +347,23 @@ export function buildHostOpenProjectRoute(serverId: string): string {
return `${base}/open-project`;
}
+export function buildHostNewWorkspaceRoute(
+ serverId: string,
+ sourceDirectory: string,
+ options?: { displayName?: string },
+): string {
+ const base = buildHostRootRoute(serverId);
+ if (base === "/") {
+ return "/";
+ }
+ const params = new URLSearchParams();
+ params.set("dir", sourceDirectory);
+ if (options?.displayName) {
+ params.set("name", options.displayName);
+ }
+ return `${base}/new?${params.toString()}`;
+}
+
export function buildHostSettingsRoute(serverId: string): string {
const base = buildHostRootRoute(serverId);
if (base === "/") {