refactor(app): remove launcher tabs from workspace flow

This commit is contained in:
Mohamed Boudra
2026-04-04 15:37:26 +07:00
parent 2ca7dd71e2
commit 5e1b24b217
24 changed files with 473 additions and 735 deletions
@@ -13,6 +13,8 @@ import {
type BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet";
import { X } from "lucide-react-native";
import { FileDropZone } from "@/components/file-drop-zone";
import type { ImageAttachment } from "@/components/message-input";
const styles = StyleSheet.create((theme) => ({
desktopOverlay: {
@@ -32,16 +34,21 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.xl,
borderWidth: 1,
borderColor: theme.colors.surface2,
overflow: "hidden",
},
header: {
paddingHorizontal: theme.spacing[6],
paddingVertical: theme.spacing[4],
flexDirection: "row",
alignItems: "center",
alignItems: "flex-start",
justifyContent: "space-between",
borderBottomWidth: 1,
borderBottomColor: theme.colors.surface2,
gap: theme.spacing[3],
},
headerTitleGroup: {
flex: 1,
gap: theme.spacing[2],
minWidth: 0,
},
title: {
color: theme.colors.foreground,
@@ -69,9 +76,10 @@ const styles = StyleSheet.create((theme) => ({
paddingBottom: theme.spacing[3],
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
alignItems: "flex-start",
borderBottomWidth: 1,
borderBottomColor: theme.colors.surface2,
gap: theme.spacing[3],
},
bottomSheetContent: {
padding: theme.spacing[6],
@@ -97,22 +105,31 @@ function SheetBackground({ style }: BottomSheetBackgroundProps) {
export interface AdaptiveModalSheetProps {
title: string;
/** Optional content rendered below the title in the header area. */
subtitle?: ReactNode;
visible: boolean;
onClose: () => void;
children: ReactNode;
snapPoints?: string[];
stackBehavior?: "push" | "switch" | "replace";
testID?: string;
/** Override the max width of the desktop card. */
desktopMaxWidth?: number;
/** When provided, wraps the card content in a FileDropZone. */
onFilesDropped?: (files: ImageAttachment[]) => void;
}
export function AdaptiveModalSheet({
title,
subtitle,
visible,
onClose,
children,
snapPoints,
stackBehavior,
testID,
desktopMaxWidth,
onFilesDropped,
}: AdaptiveModalSheetProps) {
const { theme } = useUnistyles();
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
@@ -120,6 +137,19 @@ export function AdaptiveModalSheet({
const dismissingForVisibilityRef = useRef(false);
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
useEffect(() => {
if (isMobile || !visible || Platform.OS !== "web" || typeof window === "undefined") return;
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onClose();
}
}
// Capture phase: RN Web TextInput stops propagation, so bubbling listeners never fire.
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, [isMobile, visible, onClose]);
useEffect(() => {
if (!isMobile) return;
if (visible) {
@@ -168,7 +198,10 @@ export function AdaptiveModalSheet({
keyboardBlurBehavior="restore"
>
<View style={styles.bottomSheetHeader}>
<Text style={styles.title}>{title}</Text>
<View style={styles.headerTitleGroup}>
<Text style={styles.title}>{title}</Text>
{subtitle}
</View>
<Pressable accessibilityLabel="Close" style={styles.closeButton} onPress={onClose}>
<X size={16} color={theme.colors.foregroundMuted} />
</Pressable>
@@ -184,6 +217,28 @@ export function AdaptiveModalSheet({
);
}
const cardInner = (
<>
<View style={styles.header}>
<View style={styles.headerTitleGroup}>
<Text style={styles.title}>{title}</Text>
{subtitle}
</View>
<Pressable accessibilityLabel="Close" style={styles.closeButton} onPress={onClose}>
<X size={16} color={theme.colors.foregroundMuted} />
</Pressable>
</View>
<ScrollView
style={styles.desktopScroll}
contentContainerStyle={styles.desktopContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</ScrollView>
</>
);
const desktopContent = (
<View style={styles.desktopOverlay} testID={testID}>
<Pressable
@@ -191,21 +246,12 @@ export function AdaptiveModalSheet({
style={{ ...StyleSheet.absoluteFillObject }}
onPress={onClose}
/>
<View style={styles.desktopCard}>
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
<Pressable accessibilityLabel="Close" style={styles.closeButton} onPress={onClose}>
<X size={16} color={theme.colors.foregroundMuted} />
</Pressable>
</View>
<ScrollView
style={styles.desktopScroll}
contentContainerStyle={styles.desktopContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</ScrollView>
<View style={[styles.desktopCard, desktopMaxWidth != null && { maxWidth: desktopMaxWidth }]}>
{onFilesDropped ? (
<FileDropZone onFilesDropped={onFilesDropped}>
{cardInner}
</FileDropZone>
) : cardInner}
</View>
</View>
);
@@ -6,7 +6,7 @@ import { useStoreWithEqualityFn } from "zustand/traditional";
import { Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import { useQuery } from "@tanstack/react-query";
import { useProviderModels } from "@/hooks/use-provider-models";
import { useSessionStore } from "@/stores/session-store";
import {
buildFavoriteModelKey,
@@ -39,7 +39,7 @@ import {
getStatusSelectorHint,
resolveAgentModelSelection,
} from "@/components/agent-status-bar.utils";
import { isProviderModelsQueryLoading } from "@/components/agent-status-bar.model-loading";
type StatusOption = {
id: string;
@@ -626,52 +626,15 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
);
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
const modelsQuery = useQuery({
queryKey: ["providerModels", serverId, agent?.provider ?? "__missing_provider__"],
enabled: Boolean(client && agent?.provider),
staleTime: 5 * 60 * 1000,
queryFn: async () => {
if (!client || !agent) {
throw new Error("Daemon client unavailable");
}
const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
if (payload.error) {
throw new Error(payload.error);
}
return payload.models ?? [];
},
});
const { allProviderModels: providerModelsMap, isLoading: isProviderModelsLoading } =
useProviderModels(serverId);
const agentProviderDefinitions = useMemo(() => {
const definition = AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === agent?.provider);
return definition ? [definition] : [];
}, [agent?.provider]);
const agentProviderModelQuery = useQuery({
queryKey: ["providerModels", serverId, agent?.provider, agent?.cwd ?? ""],
enabled: Boolean(client && agent?.cwd && agent?.provider),
staleTime: 5 * 60 * 1000,
queryFn: async () => {
if (!client || !agent) {
throw new Error("Daemon client unavailable");
}
const payload = await client.listProviderModels(agent.provider, { cwd: agent.cwd });
if (payload.error) {
throw new Error(payload.error);
}
return payload.models ?? [];
},
});
const agentProviderModels = useMemo(() => {
const map = new Map<string, AgentModelDefinition[]>();
if (agent?.provider && agentProviderModelQuery.data) {
map.set(agent.provider, agentProviderModelQuery.data);
}
return map;
}, [agent?.provider, agentProviderModelQuery.data]);
const models = modelsQuery.data ?? null;
const models = agent?.provider ? (providerModelsMap.get(agent.provider) ?? null) : null;
const displayMode =
availableModes.find((mode) => mode.id === agent?.currentModeId)?.label ||
@@ -719,7 +682,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
}
selectedModeId={agent.currentModeId ?? undefined}
providerDefinitions={agentProviderDefinitions}
allProviderModels={agentProviderModels}
allProviderModels={providerModelsMap}
onSelectMode={(modeId) => {
if (!client) {
return;
@@ -782,7 +745,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
console.warn("[AgentStatusBar] setAgentThinkingOption failed", error);
});
}}
isModelLoading={isProviderModelsQueryLoading(modelsQuery)}
isModelLoading={isProviderModelsLoading}
disabled={!client}
/>
);
@@ -68,6 +68,7 @@ interface SelectorContentProps {
onToggleFavorite?: (provider: string, modelId: string) => void;
onDrillDown: (providerId: string, providerLabel: string) => void;
onBack?: () => void;
isLoading?: boolean;
}
function resolveDefaultModelLabel(models: AgentModelDefinition[] | undefined): string {
@@ -338,6 +339,7 @@ function SelectorContent({
onToggleFavorite,
onDrillDown,
onBack,
isLoading,
}: SelectorContentProps) {
const allRows = useMemo(
() => buildModelRows(providerDefinitions, allProviderModels),
@@ -404,8 +406,17 @@ function SelectorContent({
{favoriteRows.length === 0 && groupedRegularRows.length === 0 ? (
<View style={styles.emptyState}>
<Search size={16} color="#777" />
<Text style={styles.emptyStateText}>No models match your search</Text>
{isLoading ? (
<>
<ActivityIndicator size="small" color="#777" />
<Text style={styles.emptyStateText}>Loading models…</Text>
</>
) : (
<>
<Search size={16} color="#777" />
<Text style={styles.emptyStateText}>No models match your search</Text>
</>
)}
</View>
) : null}
</View>
@@ -583,6 +594,7 @@ export function CombinedModelSelector({
onSelect={handleSelect}
canSelectProvider={canSelectProvider}
onToggleFavorite={onToggleFavorite}
isLoading={isLoading}
onDrillDown={(providerId, providerLabel) => {
setView({ kind: "provider", providerId, providerLabel });
}}
+4
View File
@@ -84,6 +84,8 @@ interface ComposerProps {
onAttentionPromptSend?: () => void;
/** Controlled status controls rendered in input area (draft flows). */
statusControls?: DraftAgentStatusBarProps;
/** Extra styles merged onto the message input wrapper (e.g. elevated background). */
inputWrapperStyle?: import("react-native").ViewStyle;
}
const EMPTY_ARRAY: readonly QueuedMessage[] = [];
@@ -111,6 +113,7 @@ export function Composer({
onAttentionInputFocus,
onAttentionPromptSend,
statusControls,
inputWrapperStyle,
}: ComposerProps) {
markScrollInvestigationRender(`Composer:${serverId}:${agentId}`);
const { theme } = useUnistyles();
@@ -711,6 +714,7 @@ export function Composer({
}
}}
onHeightChange={onComposerHeightChange}
inputWrapperStyle={inputWrapperStyle}
/>
</View>
</View>
@@ -96,6 +96,8 @@ export interface MessageInputProps {
onSelectionChange?: (selection: { start: number; end: number }) => void;
onFocusChange?: (focused: boolean) => void;
onHeightChange?: (height: number) => void;
/** Extra styles merged onto the input wrapper (e.g. elevated background). */
inputWrapperStyle?: import("react-native").ViewStyle;
}
export interface MessageInputRef {
@@ -213,6 +215,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
onSelectionChange: onSelectionChangeCallback,
onFocusChange,
onHeightChange,
inputWrapperStyle,
},
ref,
) {
@@ -902,7 +905,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
return (
<View ref={rootRef} style={styles.container} testID="message-input-root">
{/* Regular input */}
<Animated.View ref={inputWrapperRef} style={[styles.inputWrapper, inputAnimatedStyle]}>
<Animated.View ref={inputWrapperRef} style={[styles.inputWrapper, inputWrapperStyle, inputAnimatedStyle]}>
{/* Image preview pills */}
{hasImages && (
<View style={styles.imagePreviewContainer} testID="message-input-image-preview">
@@ -87,7 +87,8 @@ interface SplitContainerProps {
onCloseTabsToLeft: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
onCreateLauncherTab: (input: { paneId?: string }) => void;
onCreateDraftTab: (input: { paneId?: string }) => void;
onCreateTerminalTab: (input: { paneId?: string }) => void;
buildPaneContentModel: (input: {
paneId: string;
isPaneFocused: boolean;
@@ -263,7 +264,8 @@ export function SplitContainer({
onCloseTabsToLeft,
onCloseTabsToRight,
onCloseOtherTabs,
onCreateLauncherTab,
onCreateDraftTab,
onCreateTerminalTab,
buildPaneContentModel,
onFocusPane,
onSplitPane,
@@ -530,7 +532,8 @@ export function SplitContainer({
onCloseTabsToLeft={onCloseTabsToLeft}
onCloseTabsToRight={onCloseTabsToRight}
onCloseOtherTabs={onCloseOtherTabs}
onCreateLauncherTab={onCreateLauncherTab}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminalTab={onCreateTerminalTab}
buildPaneContentModel={buildPaneContentModel}
onFocusPane={onFocusPane}
onSplitPane={onSplitPane}
@@ -650,7 +653,8 @@ function SplitNodeView({
onCloseTabsToLeft,
onCloseTabsToRight,
onCloseOtherTabs,
onCreateLauncherTab,
onCreateDraftTab,
onCreateTerminalTab,
buildPaneContentModel,
onFocusPane,
onSplitPane,
@@ -683,7 +687,8 @@ function SplitNodeView({
onCloseTabsToLeft={onCloseTabsToLeft}
onCloseTabsToRight={onCloseTabsToRight}
onCloseOtherTabs={onCloseOtherTabs}
onCreateLauncherTab={onCreateLauncherTab}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminalTab={onCreateTerminalTab}
buildPaneContentModel={buildPaneContentModel}
onFocusPane={onFocusPane}
onSplitPane={onSplitPane}
@@ -731,7 +736,8 @@ function SplitNodeView({
onCloseTabsToLeft={onCloseTabsToLeft}
onCloseTabsToRight={onCloseTabsToRight}
onCloseOtherTabs={onCloseOtherTabs}
onCreateLauncherTab={onCreateLauncherTab}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminalTab={onCreateTerminalTab}
buildPaneContentModel={buildPaneContentModel}
onFocusPane={onFocusPane}
onSplitPane={onSplitPane}
@@ -778,7 +784,8 @@ function SplitPaneView({
onCloseTabsToLeft,
onCloseTabsToRight,
onCloseOtherTabs,
onCreateLauncherTab,
onCreateDraftTab,
onCreateTerminalTab,
buildPaneContentModel,
onFocusPane,
onSplitPane,
@@ -887,7 +894,8 @@ function SplitPaneView({
onCloseTabsToLeft={(tabId) => onCloseTabsToLeft(tabId, paneTabs)}
onCloseTabsToRight={(tabId) => onCloseTabsToRight(tabId, paneTabs)}
onCloseOtherTabs={(tabId) => onCloseOtherTabs(tabId, paneTabs)}
onCreateLauncherTab={onCreateLauncherTab}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminalTab={onCreateTerminalTab}
onReorderTabs={(nextTabs) => {
onReorderTabsInPane(
pane.id,
@@ -1,23 +1,32 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { ActivityIndicator, Pressable, Text, View } from "react-native";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Image, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { createNameId } from "mnemonic-id";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Composer } from "@/components/composer";
import { useToast } from "@/contexts/toast-context";
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
import { useProjectIconQuery } from "@/hooks/use-project-icon-query";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
import { encodeImages } from "@/utils/encode-images";
import { toErrorMessage } from "@/utils/error-messages";
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
import {
requireWorkspaceExecutionAuthority,
requireWorkspaceRecordId,
} from "@/utils/workspace-execution";
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
import type { MessagePayload } from "./message-input";
import type { ImageAttachment, MessagePayload } from "./message-input";
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
if (!icon) {
return null;
}
return `data:${icon.mimeType};base64,${icon.data}`;
}
export function WorkspaceSetupDialog() {
const { theme } = useUnistyles();
@@ -31,7 +40,7 @@ export function WorkspaceSetupDialog() {
const [createdWorkspace, setCreatedWorkspace] = useState<ReturnType<
typeof normalizeWorkspaceDescriptor
> | null>(null);
const [pendingAction, setPendingAction] = useState<"chat" | "terminal" | null>(null);
const [pendingAction, setPendingAction] = useState<"chat" | null>(null);
const serverId = pendingWorkspaceSetup?.serverId ?? "";
const sourceDirectory = pendingWorkspaceSetup?.sourceDirectory ?? "";
@@ -48,7 +57,7 @@ export function WorkspaceSetupDialog() {
: undefined,
isVisible: pendingWorkspaceSetup !== null,
onlineServerIds: isConnected && serverId ? [serverId] : [],
lockedWorkingDir: workspace?.workspaceDirectory || undefined,
lockedWorkingDir: workspace?.workspaceDirectory || sourceDirectory || undefined,
},
});
const composerState = chatDraft.composerState;
@@ -56,6 +65,12 @@ export function WorkspaceSetupDialog() {
throw new Error("Workspace setup composer state is required");
}
const { icon: projectIcon } = useProjectIconQuery({
serverId,
cwd: sourceDirectory,
});
const iconDataUri = toProjectIconDataUri(projectIcon);
useEffect(() => {
setErrorMessage(null);
setCreatedWorkspace(null);
@@ -208,34 +223,6 @@ export function WorkspaceSetupDialog() {
],
);
const handleCreateTerminal = useCallback(async () => {
try {
setPendingAction("terminal");
setErrorMessage(null);
const workspace = await ensureWorkspace();
const connectedClient = withConnectedClient();
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
const payload = await connectedClient.createTerminal(workspaceDirectory);
if (payload.error || !payload.terminal) {
throw new Error(payload.error ?? "Failed to open terminal");
}
if (!getIsStillActive()) {
return;
}
navigateAfterCreation(workspace.id, { kind: "terminal", terminalId: payload.terminal.id });
} catch (error) {
const message = toErrorMessage(error);
setErrorMessage(message);
toast.error(message);
} finally {
if (getIsStillActive()) {
setPendingAction(null);
}
}
}, [ensureWorkspace, getIsStillActive, navigateAfterCreation, toast, withConnectedClient]);
const workspaceTitle =
workspace?.name ||
@@ -243,25 +230,53 @@ export function WorkspaceSetupDialog() {
displayName ||
sourceDirectory.split(/[\\/]/).filter(Boolean).pop() ||
sourceDirectory;
const workspacePath = workspace?.workspaceDirectory || "Workspace will be created before launch.";
const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(workspaceTitle);
const placeholderInitial = placeholderLabel.charAt(0).toUpperCase();
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
addImagesRef.current?.(files);
}, []);
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
addImagesRef.current = addImages;
}, []);
const composerInputWrapperStyle = useMemo(
() => ({ backgroundColor: theme.colors.surface2 }),
[theme.colors.surface2],
);
if (!pendingWorkspaceSetup || !sourceDirectory) {
return null;
}
const subtitleContent = (
<View style={styles.subtitleRow}>
{iconDataUri ? (
<Image source={{ uri: iconDataUri }} style={styles.projectIcon} />
) : (
<View style={styles.projectIconFallback}>
<Text style={styles.projectIconFallbackText}>{placeholderInitial}</Text>
</View>
)}
<Text style={styles.projectTitle} numberOfLines={1}>
{workspaceTitle}
</Text>
</View>
);
return (
<AdaptiveModalSheet
title="Set up workspace"
title="Create workspace"
subtitle={subtitleContent}
visible={true}
onClose={handleClose}
snapPoints={["82%", "94%"]}
testID="workspace-setup-dialog"
desktopMaxWidth={640}
onFilesDropped={handleFilesDropped}
>
<View style={styles.header}>
<Text style={styles.workspaceTitle}>{workspaceTitle}</Text>
<Text style={styles.workspacePath}>{workspacePath}</Text>
</View>
<View style={styles.section}>
<Composer
agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
@@ -285,55 +300,48 @@ export function WorkspaceSetupDialog() {
}
: undefined
}
inputWrapperStyle={composerInputWrapperStyle}
onAddImages={handleAddImagesCallback}
/>
</View>
<Pressable
accessibilityRole="button"
disabled={pendingAction !== null}
onPress={() => void handleCreateTerminal()}
style={[
styles.terminalLink,
pendingAction !== null && pendingAction !== "terminal" ? { opacity: 0.5 } : undefined,
]}
>
{pendingAction === "terminal" ? (
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
) : null}
<Text style={styles.terminalLinkText}>{"\u2192"} Open terminal</Text>
</Pressable>
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
</AdaptiveModalSheet>
);
}
const styles = StyleSheet.create((theme) => ({
header: {
gap: theme.spacing[1],
subtitleRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
workspaceTitle: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground,
projectIcon: {
width: theme.iconSize.md,
height: theme.iconSize.md,
borderRadius: theme.borderRadius.sm,
},
workspacePath: {
fontSize: theme.fontSize.xs,
projectIconFallback: {
width: theme.iconSize.md,
height: theme.iconSize.md,
borderRadius: theme.borderRadius.sm,
borderWidth: 1,
borderColor: theme.colors.border,
alignItems: "center",
justifyContent: "center",
},
projectIconFallbackText: {
color: theme.colors.foregroundMuted,
fontSize: 9,
},
projectTitle: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
section: {
gap: theme.spacing[3],
marginHorizontal: -theme.spacing[6],
},
terminalLink: {
flexDirection: "row",
alignItems: "center",
alignSelf: "flex-end",
gap: theme.spacing[2],
},
terminalLinkText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
marginVertical: -theme.spacing[2],
},
errorText: {
fontSize: theme.fontSize.sm,