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 === "/") {