From 8694696f45d95fdfbebebc3f53a2275895782688 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Feb 2026 23:42:13 +0700 Subject: [PATCH] Update files --- .../agent-form/agent-form-dropdowns.tsx | 112 +++-- .../app/src/components/agent-status-bar.tsx | 52 +- packages/app/src/components/git-diff-pane.tsx | 41 +- packages/app/src/components/ui/combobox.tsx | 62 ++- .../app/src/components/ui/dropdown-menu.tsx | 23 +- .../src/screens/agent/draft-agent-screen.tsx | 465 +++++++++++------- packages/app/src/styles/theme.ts | 8 +- .../server/agent/providers/claude-agent.ts | 17 +- 8 files changed, 488 insertions(+), 292 deletions(-) diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index f48a89334..738b1310c 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -8,6 +8,7 @@ import { ActivityIndicator, Platform, } from "react-native"; +import type { StyleProp, ViewStyle, TextProps } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { BottomSheetModal, @@ -16,7 +17,7 @@ import { BottomSheetBackgroundProps, } from "@gorhom/bottom-sheet"; import Animated from "react-native-reanimated"; -import { ChevronDown, ChevronRight, Pencil, Check, X } from "lucide-react-native"; +import { ChevronDown, ChevronRight, Pencil, Check, X, Bot, Brain, Shield } from "lucide-react-native"; import { theme as defaultTheme } from "@/styles/theme"; import type { AgentMode, @@ -312,6 +313,8 @@ interface ComboSelectProps { allowCustomValue?: boolean; isLoading?: boolean; onSelect: (id: string) => void; + icon?: ReactElement; + showLabel?: boolean; } export function ComboSelect({ @@ -324,6 +327,8 @@ export function ComboSelect({ allowCustomValue = false, isLoading, onSelect, + icon, + showLabel = true, }: ComboSelectProps): ReactElement { const [isOpen, setIsOpen] = useState(false); const anchorRef = useRef(null); @@ -336,7 +341,7 @@ export function ComboSelect({ return ( <> - ; + icon?: ReactElement; + showLabel?: boolean; + containerStyle?: StyleProp; + valueEllipsizeMode?: TextProps["ellipsizeMode"]; + testID?: string; } -function CompactSelectField({ +export function FormSelectTrigger({ label, value, placeholder, @@ -378,6 +390,11 @@ function CompactSelectField({ disabled, isLoading, controlRef, + icon, + showLabel = true, + containerStyle, + valueEllipsizeMode, + testID, }: CompactSelectFieldProps): ReactElement { const getWebKey = useCallback((event: unknown): string | null => { if (!event || typeof event !== "object") return null; @@ -411,25 +428,38 @@ function CompactSelectField({ - {label} - {isLoading ? ( - - ) : ( - - {value || placeholder || "Select..."} - - )} + {icon ? {icon} : null} + + {showLabel ? ( + {label} + ) : null} + {isLoading ? ( + + ) : ( + + {value || placeholder || "Select..."} + + )} + + ); } @@ -466,31 +496,28 @@ export function AgentConfigRow({ providerDefinitions.map((def) => ({ id: def.id, label: def.label, - description: def.description, })), [providerDefinitions] ); const modeSelectOptions: ComboSelectOption[] = useMemo(() => { if (modeOptions.length === 0) { - return [{ id: "", label: "Default", description: "Provider default mode" }]; + return [{ id: "", label: "Default" }]; } return modeOptions.map((mode) => ({ id: mode.id, label: mode.label, - description: mode.description, })); }, [modeOptions]); const modelSelectOptions: ComboSelectOption[] = useMemo(() => { const opts: ComboSelectOption[] = [ - { id: "", label: "Auto", description: "Provider default model" }, + { id: "", label: "Auto" }, ]; for (const model of models) { opts.push({ id: model.id, label: model.label, - description: model.description, }); } return opts; @@ -502,18 +529,20 @@ export function AgentConfigRow({ } + showLabel={false} /> } + showLabel={false} /> } + showLabel={false} /> @@ -563,7 +596,6 @@ export function AssistantDropdown({ providerDefinitions.map((def) => ({ id: def.id, label: def.label, - description: def.description, })), [providerDefinitions] ); @@ -1405,26 +1437,46 @@ const styles = StyleSheet.create((theme) => ({ borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, borderRadius: theme.borderRadius.lg, - paddingVertical: theme.spacing[2], + paddingVertical: theme.spacing[1], paddingHorizontal: theme.spacing[3], gap: theme.spacing[1], }, + compactSelectControlInline: { + minHeight: 40, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: theme.spacing[1], + }, compactSelectControlDisabled: { opacity: theme.opacity[50], }, + compactSelectTopRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + }, + compactSelectLeading: { + alignItems: "center", + justifyContent: "center", + marginRight: theme.spacing[1], + }, + compactSelectValueContainer: { + flex: 1, + minWidth: 0, + gap: theme.spacing[1], + }, compactSelectLabel: { color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, - textTransform: "uppercase", - letterSpacing: 0.5, }, compactSelectValue: { color: theme.colors.foreground, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base, }, compactSelectPlaceholder: { color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base, }, })); diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index b305c2be1..8de83d230 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -6,7 +6,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; @@ -67,7 +66,9 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { return models.find((m) => m.id === agent.model) ?? null; }, [models, agent.model]); - const displayModel = selectedModel?.label ?? agent.model ?? "default"; + const displayModel = selectedModel + ? selectedModel.label + : agent.model ?? "default"; const thinkingOptions = selectedModel?.thinkingOptions ?? null; const selectedThinkingId = @@ -106,14 +107,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { maxWidth={dropdownMaxWidth} testID="agent-mode-menu" > - Mode {agent.availableModes.map((mode) => { const isActive = mode.id === agent.currentModeId; return ( handleModeChange(mode.id)} > {mode.label} @@ -147,14 +146,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { maxWidth={dropdownMaxWidth} testID="agent-model-menu" > - Model {models?.map((model) => { const isActive = model.id === agent.model; return ( { if (!client) { return; @@ -193,14 +190,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { maxWidth={dropdownMaxWidth} testID="agent-thinking-menu" > - Thinking {thinkingOptions.map((opt) => { const isActive = opt.id === selectedThinkingId; return ( { if (!client) { return; @@ -245,7 +240,6 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { testID="agent-preferences-sheet" > - Model [ @@ -260,14 +254,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { - Model {models?.map((model) => { const isActive = model.id === agent.model; return ( { if (!client) { return; @@ -287,7 +279,6 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { {thinkingOptions && thinkingOptions.length > 1 && ( - Thinking [ @@ -298,21 +289,19 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { accessibilityLabel="Select thinking option" testID="agent-preferences-thinking" > - {displayThinking} - - - - Thinking - {thinkingOptions.map((opt) => { - const isActive = opt.id === selectedThinkingId; - return ( - { - if (!client) { - return; + {displayThinking} + + + + {thinkingOptions.map((opt) => { + const isActive = opt.id === selectedThinkingId; + return ( + { + if (!client) { + return; } void client .setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id) @@ -361,7 +350,7 @@ const styles = StyleSheet.create((theme) => ({ }, modeBadgeText: { color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, prefsButton: { @@ -378,11 +367,6 @@ const styles = StyleSheet.create((theme) => ({ sheetSection: { gap: theme.spacing[2], }, - sheetLabel: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.semibold, - }, sheetSelect: { flexDirection: "row", alignItems: "center", @@ -401,7 +385,7 @@ const styles = StyleSheet.create((theme) => ({ sheetSelectText: { flex: 1, color: theme.colors.foreground, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, }, })); diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index 6dce8be56..5ca761621 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -1050,14 +1050,24 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) { accessibilityRole="button" accessibilityLabel={gitActions.primary.label} > - {gitActions.primary.status === "pending" ? ( - - ) : ( - + + {gitActions.primary.icon} {getActionDisplayLabel(gitActions.primary)} - )} + {gitActions.primary.status === "pending" ? ( + + ) : null} + {gitActions.secondary.length > 0 ? ( @@ -1292,15 +1302,21 @@ const styles = StyleSheet.create((theme) => ({ }, splitButtonPrimary: { paddingHorizontal: theme.spacing[3], - paddingVertical: theme.spacing[2], + paddingVertical: theme.spacing[1], + justifyContent: "center", + position: "relative", + }, + splitButtonPrimaryInner: { + position: "relative", + alignItems: "center", justifyContent: "center", }, splitButtonPrimaryDisabled: { opacity: 0.6, }, splitButtonText: { - fontSize: theme.fontSize.xs, - lineHeight: theme.fontSize.xs * 1.5, + fontSize: theme.fontSize.sm, + lineHeight: theme.fontSize.sm * 1.5, color: theme.colors.foreground, fontWeight: theme.fontWeight.medium, }, @@ -1310,9 +1326,12 @@ const styles = StyleSheet.create((theme) => ({ justifyContent: "center", gap: theme.spacing[2], }, - splitButtonSpinner: { - height: theme.fontSize.xs * 1.5, - width: theme.fontSize.xs * 1.5, + splitButtonContentHidden: { + opacity: 0, + }, + splitButtonSpinnerOverlay: { + position: "absolute", + transform: [{ scale: 0.8 }], }, splitButtonCaret: { width: 36, diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index 86556d2f6..c500ad788 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -20,13 +20,7 @@ import { } from "@gorhom/bottom-sheet"; import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import { Check, Search } from "lucide-react-native"; -import { - flip, - offset as floatingOffset, - shift, - size as floatingSize, - useFloating, -} from "@floating-ui/react-native"; +import { flip, offset as floatingOffset, shift, size as floatingSize, useFloating } from "@floating-ui/react-native"; import { getNextActiveIndex } from "./combobox-keyboard"; const IS_WEB = Platform.OS === "web"; @@ -132,21 +126,24 @@ export function ComboboxItem({ [ + style={({ pressed, hovered = false }) => [ styles.comboboxItem, + hovered && styles.comboboxItemHovered, pressed && styles.comboboxItemPressed, active && styles.comboboxItemActive, ]} > - - {selected ? : null} - {label} {description ? ( {description} ) : null} + {selected ? ( + + + + ) : null} ); } @@ -214,8 +211,8 @@ export function Combobox({ const middleware = useMemo( () => [ - floatingOffset({ mainAxis: 4 }), - flip({ padding: collisionPadding }), + floatingOffset(Platform.OS === "web" ? 0 : 4), + ...(Platform.OS === "web" ? [] : [flip({ padding: collisionPadding })]), shift({ padding: collisionPadding }), floatingSize({ padding: collisionPadding, @@ -238,7 +235,7 @@ export function Combobox({ ); const { refs, floatingStyles, update } = useFloating({ - placement: "bottom-start", + placement: Platform.OS === "web" ? "top-start" : "bottom-start", middleware, sameScrollView: false, elements: { @@ -551,10 +548,14 @@ const styles = StyleSheet.create((theme) => ({ searchInputContainer: { flexDirection: "row", alignItems: "center", - borderBottomWidth: 1, - borderBottomColor: theme.colors.border, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.surface1, + borderRadius: theme.borderRadius.lg, paddingHorizontal: theme.spacing[3], - marginBottom: theme.spacing[1], + marginHorizontal: theme.spacing[2], + marginBottom: theme.spacing[2], + marginTop: theme.spacing[1], gap: theme.spacing[2], }, searchInput: { @@ -571,17 +572,27 @@ const styles = StyleSheet.create((theme) => ({ paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], borderRadius: theme.borderRadius.md, + ...(IS_WEB + ? {} + : { + marginHorizontal: theme.spacing[1], + marginBottom: theme.spacing[1], + }), + }, + comboboxItemHovered: { + backgroundColor: theme.colors.surface1, }, comboboxItemPressed: { - backgroundColor: theme.colors.surface2, + backgroundColor: theme.colors.surface1, }, comboboxItemActive: { - backgroundColor: theme.colors.surface2, + backgroundColor: theme.colors.surface1, }, - comboboxItemCheckSlot: { + comboboxItemTrailingSlot: { width: 16, alignItems: "center", justifyContent: "center", + marginLeft: "auto", }, comboboxItemContent: { flex: 1, @@ -603,7 +614,7 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, }, bottomSheetBackground: { - backgroundColor: theme.colors.surface2, + backgroundColor: theme.colors.surface0, borderTopLeftRadius: theme.borderRadius["2xl"], borderTopRightRadius: theme.borderRadius["2xl"], }, @@ -616,13 +627,14 @@ const styles = StyleSheet.create((theme) => ({ }, comboboxTitle: { fontSize: theme.fontSize.lg, - fontWeight: theme.fontWeight.semibold, + fontWeight: theme.fontWeight.medium, color: theme.colors.foreground, - textAlign: "center", + textAlign: "left", }, comboboxScrollContent: { paddingBottom: theme.spacing[8], - paddingHorizontal: theme.spacing[1], + paddingHorizontal: theme.spacing[2], + paddingTop: theme.spacing[1], }, desktopOverlay: { flex: 1, @@ -638,7 +650,7 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface0, borderRadius: theme.borderRadius.lg, borderWidth: 1, - borderColor: theme.colors.borderAccent, + borderColor: theme.colors.border, shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, diff --git a/packages/app/src/components/ui/dropdown-menu.tsx b/packages/app/src/components/ui/dropdown-menu.tsx index 9a2ce5b20..2c1da28fd 100644 --- a/packages/app/src/components/ui/dropdown-menu.tsx +++ b/packages/app/src/components/ui/dropdown-menu.tsx @@ -458,6 +458,9 @@ export function DropdownMenuItem({ label = successLabel; } + const trailingContent = + trailing ?? (!showSelectedCheck && selected ? : null); + return ( [ styles.item, selected ? (selectedVariant === "accent" ? styles.itemSelectedAccent : styles.itemSelected) : null, + selected && (hovered || pressed) && selectedVariant !== "accent" + ? styles.itemSelectedInteractive + : null, isDisabled ? styles.itemDisabled : null, - (hovered || pressed) && !isDisabled ? styles.itemHovered : null, + hovered && !pressed && !isDisabled ? styles.itemHovered : null, + pressed && !isDisabled ? styles.itemPressed : null, ]} > {showSelectedCheck ? ( @@ -507,7 +514,7 @@ export function DropdownMenuItem({ ) : null} - {trailing ? {trailing} : null} + {trailingContent ? {trailingContent} : null} ); } @@ -565,12 +572,20 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], paddingHorizontal: theme.spacing[3], paddingVertical: theme.spacing[2], + borderWidth: theme.borderWidth[1], + borderColor: "transparent", }, itemHovered: { - backgroundColor: theme.colors.surface2, + backgroundColor: theme.colors.surface1, + }, + itemPressed: { + backgroundColor: theme.colors.surface1, }, itemSelected: { - backgroundColor: theme.colors.surface2, + backgroundColor: theme.colors.surface1, + }, + itemSelectedInteractive: { + backgroundColor: theme.colors.surface1, }, itemSelectedAccent: { backgroundColor: theme.colors.accent, diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx index 6279abee9..9b4539f31 100644 --- a/packages/app/src/screens/agent/draft-agent-screen.tsx +++ b/packages/app/src/screens/agent/draft-agent-screen.tsx @@ -10,20 +10,16 @@ import { Platform, } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; -import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; -import { Monitor } from "lucide-react-native"; -import { MenuHeader } from "@/components/headers/menu-header"; +import { Folder, GitBranch, Menu, Monitor, PanelLeft } from "lucide-react-native"; +import { HeaderToggleButton } from "@/components/headers/header-toggle-button"; import { AgentInputArea } from "@/components/agent-input-area"; import { AgentStreamView } from "@/components/agent-stream-view"; -import { - DropdownSheet, - GitOptionsSection, - WorkingDirectoryDropdown, - AgentConfigRow, -} from "@/components/agent-form/agent-form-dropdowns"; +import { AgentConfigRow, FormSelectTrigger } from "@/components/agent-form/agent-form-dropdowns"; +import { Combobox } from "@/components/ui/combobox"; import { FileDropZone } from "@/components/file-drop-zone"; import { useQuery } from "@tanstack/react-query"; import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state"; @@ -34,6 +30,7 @@ import { import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { formatConnectionStatus } from "@/utils/daemons"; +import { usePanelStore } from "@/stores/panel-store"; import { useSessionStore } from "@/stores/session-store"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { MAX_CONTENT_WIDTH } from "@/constants/layout"; @@ -123,6 +120,9 @@ export function DraftAgentScreen({ const insets = useSafeAreaInsets(); const { connectionStates } = useDaemonConnections(); const { daemons } = useDaemonRegistry(); + const mobileView = usePanelStore((state) => state.mobileView); + const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); + const toggleAgentList = usePanelStore((state) => state.toggleAgentList); const params = useLocalSearchParams(); const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); @@ -204,15 +204,26 @@ export function DraftAgentScreen({ : undefined; const hostLabel = hostEntry?.daemon.label ?? selectedServerId ?? "Select host"; - const hostStatus = hostEntry?.status - ? formatConnectionStatus(hostEntry.status) - : undefined; + const isMobile = + UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; + const isSidebarOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen; + const SidebarIcon = isMobile ? Menu : PanelLeft; + const sidebarIconColor = !isMobile && isSidebarOpen + ? theme.colors.foreground + : theme.colors.foregroundMuted; - const [openDropdown, setOpenDropdown] = useState<"host" | null>(null); + const [isHostOpen, setIsHostOpen] = useState(false); const [worktreeMode, setWorktreeMode] = useState<"none" | "create" | "attach">("none"); const [baseBranch, setBaseBranch] = useState(""); const [worktreeSlug, setWorktreeSlug] = useState(""); const [selectedWorktreePath, setSelectedWorktreePath] = useState(""); + const [isWorkingDirOpen, setIsWorkingDirOpen] = useState(false); + const [isWorktreePickerOpen, setIsWorktreePickerOpen] = useState(false); + const [isBranchOpen, setIsBranchOpen] = useState(false); + const hostAnchorRef = useRef(null); + const workingDirAnchorRef = useRef(null); + const worktreeAnchorRef = useRef(null); + const branchAnchorRef = useRef(null); const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null); const setPendingCreateAttempt = useCreateFlowStore((state) => state.setPending); const updatePendingAgentId = useCreateFlowStore((state) => state.updateAgentId); @@ -276,15 +287,26 @@ export function DraftAgentScreen({ const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => { addImagesRef.current = addImages; }, []); - const openDropdownSheet = useCallback((key: "host") => { - setOpenDropdown(key); - }, []); - const closeDropdown = useCallback(() => { - setOpenDropdown(null); - }, []); const sessionAgents = useSessionStore((state) => selectedServerId ? state.sessions[selectedServerId]?.agents : undefined ); + const worktreePathLastCreatedAt = useMemo(() => { + const map = new Map(); + if (!sessionAgents) { + return map; + } + sessionAgents.forEach((agent) => { + if (!agent.cwd) { + return; + } + const ts = agent.createdAt.getTime(); + const prev = map.get(agent.cwd); + if (!prev || ts > prev) { + map.set(agent.cwd, ts); + } + }); + return map; + }, [sessionAgents]); const agentWorkingDirSuggestions = useMemo(() => { if (!selectedServerId || !sessionAgents) { return []; @@ -346,7 +368,6 @@ export function DraftAgentScreen({ }); const checkout = checkoutStatusQuery.data ?? null; - const refetchCheckoutStatus = checkoutStatusQuery.refetch; const checkoutQueryError = checkoutStatusQuery.error instanceof Error ? checkoutStatusQuery.error.message : null; const checkoutPayloadError = checkout?.error ? checkout.error.message : null; @@ -398,7 +419,6 @@ export function DraftAgentScreen({ return payload.worktrees ?? []; }, enabled: - isAttachWorktree && Boolean(worktreeListRoot || trimmedWorkingDir) && !repoAvailabilityError && Boolean(sessionClient) && @@ -409,17 +429,23 @@ export function DraftAgentScreen({ refetchOnMount: "always", }); const worktreeOptions = useMemo(() => { - return (worktreeListQuery.data ?? []).map((worktree) => ({ + const options = (worktreeListQuery.data ?? []).map((worktree) => ({ path: worktree.worktreePath, label: worktree.branchName ?? worktree.head ?? "Unknown branch", })); - }, [worktreeListQuery.data]); + return options.sort((a, b) => { + const aTs = worktreePathLastCreatedAt.get(a.path) ?? 0; + const bTs = worktreePathLastCreatedAt.get(b.path) ?? 0; + if (aTs !== bTs) { + return bTs - aTs; + } + return a.label.localeCompare(b.label); + }); + }, [worktreeListQuery.data, worktreePathLastCreatedAt]); const worktreeOptionsError = worktreeListQuery.error instanceof Error ? worktreeListQuery.error.message : null; const worktreeOptionsStatus: "idle" | "loading" | "ready" | "error" = - !isAttachWorktree - ? "idle" - : worktreeListQuery.isPending || worktreeListQuery.isFetching + worktreeListQuery.isPending || worktreeListQuery.isFetching ? "loading" : worktreeListQuery.isError ? "error" @@ -432,22 +458,6 @@ export function DraftAgentScreen({ ? "Select a worktree to attach" : null; - const handleWorktreeModeChange = useCallback( - (mode: "none" | "create" | "attach") => { - setWorktreeMode(mode); - if (mode === "create" && !worktreeSlug) { - setWorktreeSlug(createNameId()); - } - if (mode !== "attach") { - setSelectedWorktreePath(""); - } - if (mode !== "none") { - refetchCheckoutStatus(); - } - }, - [worktreeSlug, refetchCheckoutStatus] - ); - const validateWorktreeName = useCallback( (name: string): { valid: boolean; error?: string } => { if (!name) { @@ -579,6 +589,59 @@ export function DraftAgentScreen({ } }, [isNonGitDirectory, worktreeMode]); + const selectedWorktreeLabel = + worktreeOptions.find((option) => option.path === selectedWorktreePath)?.label ?? ""; + const worktreeTriggerValue = + worktreeMode === "create" + ? "Create new worktree" + : selectedWorktreeLabel || "Select worktree"; + const hostOptions = useMemo( + () => + Array.from(connectionStates.values()).map(({ daemon, status }) => ({ + id: daemon.serverId, + label: daemon.label?.trim() ? daemon.label : daemon.serverId, + description: formatConnectionStatus(status), + })), + [connectionStates] + ); + const worktreeComboOptions = useMemo( + () => [ + { + id: "__create_new__", + label: "Create new worktree", + description: "Create a new isolated worktree", + }, + { + id: "__none__", + label: "None", + description: "Do not use a worktree", + }, + ...worktreeOptions.map((option) => ({ + id: option.path, + label: option.label, + description: option.path, + })), + ], + [worktreeOptions] + ); + + const branchComboOptions = useMemo(() => { + const branchSet = new Set(); + const currentBranch = checkout?.isGit ? checkout.currentBranch?.trim() : null; + if (currentBranch && currentBranch !== "HEAD") { + branchSet.add(currentBranch); + } + if (baseBranch.trim()) { + branchSet.add(baseBranch.trim()); + } + for (const option of worktreeOptions) { + if (option.label) { + branchSet.add(option.label); + } + } + return Array.from(branchSet).map((name) => ({ id: name, label: name })); + }, [baseBranch, checkout, worktreeOptions]); + const createAgentClient = useSessionStore((state) => selectedServerId ? state.sessions[selectedServerId]?.client ?? null : null ); @@ -823,27 +886,27 @@ export function DraftAgentScreen({ - openDropdownSheet("host")} - > - - {hostLabel} - - {hostStatus ? ( - {hostStatus} - ) : null} - - } - /> + + + + + {machine.tag === "creating" && draftAgent && selectedServerId ? ( @@ -859,13 +922,29 @@ export function DraftAgentScreen({ ) : ( - + + setIsWorkingDirOpen(true)} + icon={} + showLabel={false} + valueEllipsizeMode="middle" + /> + setIsHostOpen(true)} + icon={} + showLabel={false} + /> + {isDirectoryNotExists && ( @@ -873,6 +952,36 @@ export function DraftAgentScreen({ )} + {trimmedWorkingDir.length > 0 && !isNonGitDirectory ? ( + + setIsWorktreePickerOpen(true)} + icon={} + showLabel={false} + valueEllipsizeMode="middle" + /> + {worktreeMode === "create" ? ( + setIsBranchOpen(true)} + disabled={repoInfoStatus === "loading"} + icon={} + showLabel={false} + /> + ) : null} + + ) : null} - {trimmedWorkingDir.length > 0 && !isNonGitDirectory ? ( - - ) : null} + {baseBranchError ? {baseBranchError} : null} + {repoInfoError ? {repoInfoError} : null} + {gitBlockingError ? {gitBlockingError} : null} + {attachWorktreeError ? {attachWorktreeError} : null} + {worktreeOptionsError ? {worktreeOptionsError} : null} - setSelectedServerIdFromUser(serverId)} title="Host" - visible={openDropdown === "host"} - onClose={closeDropdown} - > - {connectionStates.size === 0 ? ( - - No hosts available yet. - - ) : ( - - {Array.from(connectionStates.values()).map(({ daemon, status }) => { - const isSelected = daemon.serverId === selectedServerId; - const label = daemon.label?.trim() ? daemon.label : daemon.serverId; - return ( - { - setSelectedServerIdFromUser(daemon.serverId); - closeDropdown(); - }} - > - - {label} - - - {formatConnectionStatus(status)} - - - ); - })} - - )} - + searchPlaceholder="Search hosts..." + open={isHostOpen} + onOpenChange={setIsHostOpen} + anchorRef={hostAnchorRef} + /> + + { + if (id === "__create_new__") { + setWorktreeMode("create"); + if (!worktreeSlug) { + setWorktreeSlug(createNameId()); + } + setSelectedWorktreePath(""); + return; + } + if (id === "__none__") { + setWorktreeMode("none"); + setSelectedWorktreePath(""); + return; + } + handleSelectWorktreePath(id); + setWorktreeMode("attach"); + }} + title="Select worktree" + searchPlaceholder="Search worktrees..." + open={isWorktreePickerOpen} + onOpenChange={setIsWorktreePickerOpen} + emptyText="No worktrees found" + anchorRef={worktreeAnchorRef} + /> + + ({ id: path, label: path }))} + value={workingDir} + onSelect={setWorkingDirFromUser} + searchPlaceholder="/path/to/project" + emptyText={ + agentWorkingDirSuggestions.length > 0 + ? "No agent directories match your search." + : "We'll suggest directories from agents on this host once they exist." + } + allowCustomValue + customValuePrefix="Use" + customValueDescription="Launch the agent in this directory" + title="Working directory" + open={isWorkingDirOpen} + onOpenChange={setIsWorkingDirOpen} + anchorRef={workingDirAnchorRef} + /> + + {formErrorMessage ? ( @@ -980,6 +1113,11 @@ const styles = StyleSheet.create((theme) => ({ agentPanel: { flex: 1, }, + menuToggleContainer: { + paddingHorizontal: theme.spacing[2], + paddingTop: theme.spacing[2], + alignItems: "flex-start", + }, contentContainer: { flex: 1, overflow: "hidden", @@ -998,7 +1136,7 @@ const styles = StyleSheet.create((theme) => ({ justifyContent: "flex-end", }, configSection: { - paddingHorizontal: theme.spacing[4], + paddingHorizontal: theme.spacing[0], paddingTop: theme.spacing[3], paddingBottom: theme.spacing[4], gap: theme.spacing[2], @@ -1006,46 +1144,31 @@ const styles = StyleSheet.create((theme) => ({ alignSelf: "center", width: "100%", }, - dropdownHelper: { - fontSize: theme.fontSize.sm, - color: theme.colors.foregroundMuted, + topSelectorRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], }, - dropdownSheetList: { - marginTop: theme.spacing[3], + topSelectorPrimary: { + flex: 7, }, - dropdownSheetOption: { - paddingVertical: theme.spacing[3], - paddingHorizontal: theme.spacing[4], - borderRadius: theme.borderRadius.lg, - borderWidth: 1, - borderColor: theme.colors.border, - backgroundColor: theme.colors.surface0, - marginBottom: theme.spacing[2], + topSelectorSecondary: { + flex: 3, }, - dropdownSheetOptionSelected: { - borderColor: theme.colors.palette.blue[400], - backgroundColor: "rgba(59, 130, 246, 0.18)", - }, - dropdownSheetOptionLabel: { - color: theme.colors.foreground, - fontWeight: theme.fontWeight.semibold, - }, - dropdownSheetOptionDescription: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, - marginTop: theme.spacing[1], + halfSelector: { + flex: 1, }, errorContainer: { paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], - marginHorizontal: theme.spacing[4], + marginHorizontal: theme.spacing[0], marginBottom: theme.spacing[2], borderRadius: theme.borderRadius.lg, backgroundColor: theme.colors.destructive, }, errorText: { color: theme.colors.destructiveForeground, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base, }, warningContainer: { paddingHorizontal: theme.spacing[3], @@ -1055,35 +1178,11 @@ const styles = StyleSheet.create((theme) => ({ }, warningText: { color: "#000000", - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.semibold, }, - hostBadge: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - paddingHorizontal: theme.spacing[3], - paddingVertical: theme.spacing[2], - backgroundColor: theme.colors.surface2, - borderRadius: theme.borderRadius.full, - }, - hostBadgeLabel: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.xs, - fontWeight: theme.fontWeight.semibold, - }, - hostBadgeStatus: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.xs, - fontWeight: theme.fontWeight.medium, - }, - hostStatusDot: { - width: 6, - height: 6, - borderRadius: theme.borderRadius.full, - backgroundColor: theme.colors.foregroundMuted, - }, - hostStatusDotOnline: { - backgroundColor: theme.colors.palette.green[500], + errorInlineText: { + color: theme.colors.palette.red[500], + fontSize: theme.fontSize.base, }, })); diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts index fd4511b5a..328d12538 100644 --- a/packages/app/src/styles/theme.ts +++ b/packages/app/src/styles/theme.ts @@ -117,7 +117,7 @@ const lightSemanticColors = { // Borders - shifted one step lighter border: "#e4e4e7", // (was zinc-200, now zinc-200 - keep for contrast) - borderAccent: "#e4e4e7", // (was zinc-300, now zinc-200) + borderAccent: "#ececf1", // Softer accent border for low-emphasis outlines // Brand accent: "#20744A", @@ -141,7 +141,7 @@ const lightSemanticColors = { secondaryForeground: "#09090b", muted: "#f4f4f5", mutedForeground: "#71717a", - accentBorder: "#e4e4e7", + accentBorder: "#ececf1", input: "#f4f4f5", ring: "#18181b", } as const; @@ -159,7 +159,7 @@ const darkSemanticColors = { // Borders border: "#27272a", - borderAccent: "#3f3f46", + borderAccent: "#34343a", // Brand accent: "#20744A", @@ -183,7 +183,7 @@ const darkSemanticColors = { secondaryForeground: "#fafafa", muted: "#27272a", mutedForeground: "#a1a1aa", - accentBorder: "#3f3f46", + accentBorder: "#34343a", input: "#27272a", ring: "#d4d4d8", } as const; diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index d23ff8c17..6eaf9bc0d 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -63,6 +63,21 @@ interface TurnContext { streamedReasoningThisTurn: boolean; } +function normalizeClaudeModelLabel(model: ModelInfo): string { + const fallback = model.displayName?.trim() || model.value; + const prefix = model.description?.split(/[·•]/)[0]?.trim() || ""; + if (!prefix) return fallback; + + // Prefer concrete versioned labels from description (e.g. "Opus 4.6", + // "Sonnet 4.5"), especially when displayName is generic like + // "Default (recommended)". + if (/\d/.test(prefix)) { + return prefix; + } + + return fallback; +} + const CLAUDE_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, @@ -371,7 +386,7 @@ export class ClaudeAgentClient implements AgentClient { return models.map((model) => ({ provider: "claude" as const, id: model.value, - label: model.displayName, + label: normalizeClaudeModelLabel(model), description: model.description, thinkingOptions: [ { id: "off", label: "Off", isDefault: true },