Update files

This commit is contained in:
Mohamed Boudra
2026-02-06 23:42:13 +07:00
parent 1a01e836bc
commit 8694696f45
8 changed files with 488 additions and 292 deletions
@@ -8,6 +8,7 @@ import {
ActivityIndicator, ActivityIndicator,
Platform, Platform,
} from "react-native"; } from "react-native";
import type { StyleProp, ViewStyle, TextProps } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { import {
BottomSheetModal, BottomSheetModal,
@@ -16,7 +17,7 @@ import {
BottomSheetBackgroundProps, BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import Animated from "react-native-reanimated"; 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 { theme as defaultTheme } from "@/styles/theme";
import type { import type {
AgentMode, AgentMode,
@@ -312,6 +313,8 @@ interface ComboSelectProps {
allowCustomValue?: boolean; allowCustomValue?: boolean;
isLoading?: boolean; isLoading?: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
icon?: ReactElement;
showLabel?: boolean;
} }
export function ComboSelect({ export function ComboSelect({
@@ -324,6 +327,8 @@ export function ComboSelect({
allowCustomValue = false, allowCustomValue = false,
isLoading, isLoading,
onSelect, onSelect,
icon,
showLabel = true,
}: ComboSelectProps): ReactElement { }: ComboSelectProps): ReactElement {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<View>(null); const anchorRef = useRef<View>(null);
@@ -336,7 +341,7 @@ export function ComboSelect({
return ( return (
<> <>
<CompactSelectField <FormSelectTrigger
label={label} label={label}
value={displayValue} value={displayValue}
placeholder={placeholder} placeholder={placeholder}
@@ -344,6 +349,8 @@ export function ComboSelect({
disabled={disabled} disabled={disabled}
isLoading={isLoading} isLoading={isLoading}
controlRef={anchorRef} controlRef={anchorRef}
icon={icon}
showLabel={showLabel}
/> />
<Combobox <Combobox
options={options} options={options}
@@ -368,9 +375,14 @@ interface CompactSelectFieldProps {
disabled?: boolean; disabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
controlRef?: React.RefObject<View | null>; controlRef?: React.RefObject<View | null>;
icon?: ReactElement;
showLabel?: boolean;
containerStyle?: StyleProp<ViewStyle>;
valueEllipsizeMode?: TextProps["ellipsizeMode"];
testID?: string;
} }
function CompactSelectField({ export function FormSelectTrigger({
label, label,
value, value,
placeholder, placeholder,
@@ -378,6 +390,11 @@ function CompactSelectField({
disabled, disabled,
isLoading, isLoading,
controlRef, controlRef,
icon,
showLabel = true,
containerStyle,
valueEllipsizeMode,
testID,
}: CompactSelectFieldProps): ReactElement { }: CompactSelectFieldProps): ReactElement {
const getWebKey = useCallback((event: unknown): string | null => { const getWebKey = useCallback((event: unknown): string | null => {
if (!event || typeof event !== "object") return null; if (!event || typeof event !== "object") return null;
@@ -411,25 +428,38 @@ function CompactSelectField({
<Pressable <Pressable
ref={controlRef} ref={controlRef}
onPress={onPress} onPress={onPress}
testID={testID}
// @ts-ignore - tabIndex is web-only // @ts-ignore - tabIndex is web-only
tabIndex={0} tabIndex={0}
accessibilityRole="button" accessibilityRole="button"
// @ts-ignore - onKeyDown is web-only // @ts-ignore - onKeyDown is web-only
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={disabled} disabled={disabled}
style={[styles.compactSelectControl, disabled && styles.compactSelectControlDisabled]} style={[
styles.compactSelectControl,
!showLabel && styles.compactSelectControlInline,
containerStyle,
disabled && styles.compactSelectControlDisabled,
]}
> >
<Text style={styles.compactSelectLabel}>{label}</Text> {icon ? <View style={styles.compactSelectLeading}>{icon}</View> : null}
{isLoading ? ( <View style={styles.compactSelectValueContainer}>
<ActivityIndicator size="small" color={defaultTheme.colors.foregroundMuted} /> {showLabel ? (
) : ( <Text style={styles.compactSelectLabel}>{label}</Text>
<Text ) : null}
style={value ? styles.compactSelectValue : styles.compactSelectPlaceholder} {isLoading ? (
numberOfLines={1} <ActivityIndicator size="small" color={defaultTheme.colors.foregroundMuted} />
> ) : (
{value || placeholder || "Select..."} <Text
</Text> style={value ? styles.compactSelectValue : styles.compactSelectPlaceholder}
)} numberOfLines={1}
ellipsizeMode={valueEllipsizeMode}
>
{value || placeholder || "Select..."}
</Text>
)}
</View>
<ChevronDown size={16} color={defaultTheme.colors.foregroundMuted} />
</Pressable> </Pressable>
); );
} }
@@ -466,31 +496,28 @@ export function AgentConfigRow({
providerDefinitions.map((def) => ({ providerDefinitions.map((def) => ({
id: def.id, id: def.id,
label: def.label, label: def.label,
description: def.description,
})), })),
[providerDefinitions] [providerDefinitions]
); );
const modeSelectOptions: ComboSelectOption[] = useMemo(() => { const modeSelectOptions: ComboSelectOption[] = useMemo(() => {
if (modeOptions.length === 0) { if (modeOptions.length === 0) {
return [{ id: "", label: "Default", description: "Provider default mode" }]; return [{ id: "", label: "Default" }];
} }
return modeOptions.map((mode) => ({ return modeOptions.map((mode) => ({
id: mode.id, id: mode.id,
label: mode.label, label: mode.label,
description: mode.description,
})); }));
}, [modeOptions]); }, [modeOptions]);
const modelSelectOptions: ComboSelectOption[] = useMemo(() => { const modelSelectOptions: ComboSelectOption[] = useMemo(() => {
const opts: ComboSelectOption[] = [ const opts: ComboSelectOption[] = [
{ id: "", label: "Auto", description: "Provider default model" }, { id: "", label: "Auto" },
]; ];
for (const model of models) { for (const model of models) {
opts.push({ opts.push({
id: model.id, id: model.id,
label: model.label, label: model.label,
description: model.description,
}); });
} }
return opts; return opts;
@@ -502,18 +529,20 @@ export function AgentConfigRow({
<View style={styles.agentConfigRow}> <View style={styles.agentConfigRow}>
<View style={styles.agentConfigColumn}> <View style={styles.agentConfigColumn}>
<ComboSelect <ComboSelect
label="PROVIDER" label="Provider"
title="Select provider" title="Select provider"
value={selectedProvider} value={selectedProvider}
options={providerOptions} options={providerOptions}
placeholder="Select..." placeholder="Select..."
disabled={disabled} disabled={disabled}
onSelect={onSelectProvider} onSelect={onSelectProvider}
icon={<Bot size={16} color={defaultTheme.colors.foregroundMuted} />}
showLabel={false}
/> />
</View> </View>
<View style={styles.agentConfigColumn}> <View style={styles.agentConfigColumn}>
<ComboSelect <ComboSelect
label="MODEL" label="Model"
title="Select model" title="Select model"
value={selectedModel} value={selectedModel}
options={modelSelectOptions} options={modelSelectOptions}
@@ -521,17 +550,21 @@ export function AgentConfigRow({
disabled={disabled} disabled={disabled}
isLoading={isModelLoading} isLoading={isModelLoading}
onSelect={onSelectModel} onSelect={onSelectModel}
icon={<Brain size={16} color={defaultTheme.colors.foregroundMuted} />}
showLabel={false}
/> />
</View> </View>
<View style={styles.agentConfigColumn}> <View style={styles.agentConfigColumn}>
<ComboSelect <ComboSelect
label="MODE" label="Mode"
title="Select mode" title="Select mode"
value={effectiveSelectedMode} value={effectiveSelectedMode}
options={modeSelectOptions} options={modeSelectOptions}
placeholder="Default" placeholder="Default"
disabled={disabled || modeOptions.length === 0} disabled={disabled || modeOptions.length === 0}
onSelect={onSelectMode} onSelect={onSelectMode}
icon={<Shield size={16} color={defaultTheme.colors.foregroundMuted} />}
showLabel={false}
/> />
</View> </View>
</View> </View>
@@ -563,7 +596,6 @@ export function AssistantDropdown({
providerDefinitions.map((def) => ({ providerDefinitions.map((def) => ({
id: def.id, id: def.id,
label: def.label, label: def.label,
description: def.description,
})), })),
[providerDefinitions] [providerDefinitions]
); );
@@ -1405,26 +1437,46 @@ const styles = StyleSheet.create((theme) => ({
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border, borderColor: theme.colors.border,
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
gap: theme.spacing[1], gap: theme.spacing[1],
}, },
compactSelectControlInline: {
minHeight: 40,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[1],
},
compactSelectControlDisabled: { compactSelectControlDisabled: {
opacity: theme.opacity[50], 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: { compactSelectLabel: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
textTransform: "uppercase",
letterSpacing: 0.5,
}, },
compactSelectValue: { compactSelectValue: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base,
}, },
compactSelectPlaceholder: { compactSelectPlaceholder: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base,
}, },
})); }));
@@ -6,7 +6,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; 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; return models.find((m) => m.id === agent.model) ?? null;
}, [models, agent.model]); }, [models, agent.model]);
const displayModel = selectedModel?.label ?? agent.model ?? "default"; const displayModel = selectedModel
? selectedModel.label
: agent.model ?? "default";
const thinkingOptions = selectedModel?.thinkingOptions ?? null; const thinkingOptions = selectedModel?.thinkingOptions ?? null;
const selectedThinkingId = const selectedThinkingId =
@@ -106,14 +107,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
maxWidth={dropdownMaxWidth} maxWidth={dropdownMaxWidth}
testID="agent-mode-menu" testID="agent-mode-menu"
> >
<DropdownMenuLabel>Mode</DropdownMenuLabel>
{agent.availableModes.map((mode) => { {agent.availableModes.map((mode) => {
const isActive = mode.id === agent.currentModeId; const isActive = mode.id === agent.currentModeId;
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={mode.id} key={mode.id}
selected={isActive} selected={isActive}
description={mode.description}
onSelect={() => handleModeChange(mode.id)} onSelect={() => handleModeChange(mode.id)}
> >
{mode.label} {mode.label}
@@ -147,14 +146,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
maxWidth={dropdownMaxWidth} maxWidth={dropdownMaxWidth}
testID="agent-model-menu" testID="agent-model-menu"
> >
<DropdownMenuLabel>Model</DropdownMenuLabel>
{models?.map((model) => { {models?.map((model) => {
const isActive = model.id === agent.model; const isActive = model.id === agent.model;
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={model.id} key={model.id}
selected={isActive} selected={isActive}
description={model.description}
onSelect={() => { onSelect={() => {
if (!client) { if (!client) {
return; return;
@@ -193,14 +190,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
maxWidth={dropdownMaxWidth} maxWidth={dropdownMaxWidth}
testID="agent-thinking-menu" testID="agent-thinking-menu"
> >
<DropdownMenuLabel>Thinking</DropdownMenuLabel>
{thinkingOptions.map((opt) => { {thinkingOptions.map((opt) => {
const isActive = opt.id === selectedThinkingId; const isActive = opt.id === selectedThinkingId;
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={opt.id} key={opt.id}
selected={isActive} selected={isActive}
description={opt.description}
onSelect={() => { onSelect={() => {
if (!client) { if (!client) {
return; return;
@@ -245,7 +240,6 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
testID="agent-preferences-sheet" testID="agent-preferences-sheet"
> >
<View style={styles.sheetSection}> <View style={styles.sheetSection}>
<Text style={styles.sheetLabel}>Model</Text>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
style={({ pressed }) => [ style={({ pressed }) => [
@@ -260,14 +254,12 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
<ChevronDown size={16} color={theme.colors.foregroundMuted} /> <ChevronDown size={16} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start"> <DropdownMenuContent side="top" align="start">
<DropdownMenuLabel>Model</DropdownMenuLabel>
{models?.map((model) => { {models?.map((model) => {
const isActive = model.id === agent.model; const isActive = model.id === agent.model;
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={model.id} key={model.id}
selected={isActive} selected={isActive}
description={model.description}
onSelect={() => { onSelect={() => {
if (!client) { if (!client) {
return; return;
@@ -287,7 +279,6 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
{thinkingOptions && thinkingOptions.length > 1 && ( {thinkingOptions && thinkingOptions.length > 1 && (
<View style={styles.sheetSection}> <View style={styles.sheetSection}>
<Text style={styles.sheetLabel}>Thinking</Text>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
style={({ pressed }) => [ style={({ pressed }) => [
@@ -298,21 +289,19 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
accessibilityLabel="Select thinking option" accessibilityLabel="Select thinking option"
testID="agent-preferences-thinking" testID="agent-preferences-thinking"
> >
<Text style={styles.sheetSelectText}>{displayThinking}</Text> <Text style={styles.sheetSelectText}>{displayThinking}</Text>
<ChevronDown size={16} color={theme.colors.foregroundMuted} /> <ChevronDown size={16} color={theme.colors.foregroundMuted} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start"> <DropdownMenuContent side="top" align="start">
<DropdownMenuLabel>Thinking</DropdownMenuLabel> {thinkingOptions.map((opt) => {
{thinkingOptions.map((opt) => { const isActive = opt.id === selectedThinkingId;
const isActive = opt.id === selectedThinkingId; return (
return ( <DropdownMenuItem
<DropdownMenuItem key={opt.id}
key={opt.id} selected={isActive}
selected={isActive} onSelect={() => {
description={opt.description} if (!client) {
onSelect={() => { return;
if (!client) {
return;
} }
void client void client
.setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id) .setAgentThinkingOption(agentId, opt.id === "default" ? null : opt.id)
@@ -361,7 +350,7 @@ const styles = StyleSheet.create((theme) => ({
}, },
modeBadgeText: { modeBadgeText: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
}, },
prefsButton: { prefsButton: {
@@ -378,11 +367,6 @@ const styles = StyleSheet.create((theme) => ({
sheetSection: { sheetSection: {
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
sheetLabel: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
},
sheetSelect: { sheetSelect: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -401,7 +385,7 @@ const styles = StyleSheet.create((theme) => ({
sheetSelectText: { sheetSelectText: {
flex: 1, flex: 1,
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.semibold,
}, },
})); }));
+30 -11
View File
@@ -1050,14 +1050,24 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={gitActions.primary.label} accessibilityLabel={gitActions.primary.label}
> >
{gitActions.primary.status === "pending" ? ( <View style={styles.splitButtonPrimaryInner}>
<ActivityIndicator size="small" color={theme.colors.foreground} style={styles.splitButtonSpinner} /> <View
) : ( style={[
<View style={styles.splitButtonContent}> styles.splitButtonContent,
gitActions.primary.status === "pending" && styles.splitButtonContentHidden,
]}
>
{gitActions.primary.icon} {gitActions.primary.icon}
<Text style={styles.splitButtonText}>{getActionDisplayLabel(gitActions.primary)}</Text> <Text style={styles.splitButtonText}>{getActionDisplayLabel(gitActions.primary)}</Text>
</View> </View>
)} {gitActions.primary.status === "pending" ? (
<ActivityIndicator
size="small"
color={theme.colors.foreground}
style={styles.splitButtonSpinnerOverlay}
/>
) : null}
</View>
</Pressable> </Pressable>
{gitActions.secondary.length > 0 ? ( {gitActions.secondary.length > 0 ? (
<DropdownMenu> <DropdownMenu>
@@ -1292,15 +1302,21 @@ const styles = StyleSheet.create((theme) => ({
}, },
splitButtonPrimary: { splitButtonPrimary: {
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[1],
justifyContent: "center",
position: "relative",
},
splitButtonPrimaryInner: {
position: "relative",
alignItems: "center",
justifyContent: "center", justifyContent: "center",
}, },
splitButtonPrimaryDisabled: { splitButtonPrimaryDisabled: {
opacity: 0.6, opacity: 0.6,
}, },
splitButtonText: { splitButtonText: {
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.sm,
lineHeight: theme.fontSize.xs * 1.5, lineHeight: theme.fontSize.sm * 1.5,
color: theme.colors.foreground, color: theme.colors.foreground,
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
}, },
@@ -1310,9 +1326,12 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "center", justifyContent: "center",
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
splitButtonSpinner: { splitButtonContentHidden: {
height: theme.fontSize.xs * 1.5, opacity: 0,
width: theme.fontSize.xs * 1.5, },
splitButtonSpinnerOverlay: {
position: "absolute",
transform: [{ scale: 0.8 }],
}, },
splitButtonCaret: { splitButtonCaret: {
width: 36, width: 36,
+37 -25
View File
@@ -20,13 +20,7 @@ import {
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated"; import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { Check, Search } from "lucide-react-native"; import { Check, Search } from "lucide-react-native";
import { import { flip, offset as floatingOffset, shift, size as floatingSize, useFloating } from "@floating-ui/react-native";
flip,
offset as floatingOffset,
shift,
size as floatingSize,
useFloating,
} from "@floating-ui/react-native";
import { getNextActiveIndex } from "./combobox-keyboard"; import { getNextActiveIndex } from "./combobox-keyboard";
const IS_WEB = Platform.OS === "web"; const IS_WEB = Platform.OS === "web";
@@ -132,21 +126,24 @@ export function ComboboxItem({
<Pressable <Pressable
testID={testID} testID={testID}
onPress={onPress} onPress={onPress}
style={({ pressed }) => [ style={({ pressed, hovered = false }) => [
styles.comboboxItem, styles.comboboxItem,
hovered && styles.comboboxItemHovered,
pressed && styles.comboboxItemPressed, pressed && styles.comboboxItemPressed,
active && styles.comboboxItemActive, active && styles.comboboxItemActive,
]} ]}
> >
<View style={styles.comboboxItemCheckSlot}>
{selected ? <Check size={16} color={theme.colors.foreground} /> : null}
</View>
<View style={styles.comboboxItemContent}> <View style={styles.comboboxItemContent}>
<Text numberOfLines={1} style={styles.comboboxItemLabel}>{label}</Text> <Text numberOfLines={1} style={styles.comboboxItemLabel}>{label}</Text>
{description ? ( {description ? (
<Text numberOfLines={2} style={styles.comboboxItemDescription}>{description}</Text> <Text numberOfLines={2} style={styles.comboboxItemDescription}>{description}</Text>
) : null} ) : null}
</View> </View>
{selected ? (
<View style={styles.comboboxItemTrailingSlot}>
<Check size={16} color={theme.colors.foregroundMuted} />
</View>
) : null}
</Pressable> </Pressable>
); );
} }
@@ -214,8 +211,8 @@ export function Combobox({
const middleware = useMemo( const middleware = useMemo(
() => [ () => [
floatingOffset({ mainAxis: 4 }), floatingOffset(Platform.OS === "web" ? 0 : 4),
flip({ padding: collisionPadding }), ...(Platform.OS === "web" ? [] : [flip({ padding: collisionPadding })]),
shift({ padding: collisionPadding }), shift({ padding: collisionPadding }),
floatingSize({ floatingSize({
padding: collisionPadding, padding: collisionPadding,
@@ -238,7 +235,7 @@ export function Combobox({
); );
const { refs, floatingStyles, update } = useFloating({ const { refs, floatingStyles, update } = useFloating({
placement: "bottom-start", placement: Platform.OS === "web" ? "top-start" : "bottom-start",
middleware, middleware,
sameScrollView: false, sameScrollView: false,
elements: { elements: {
@@ -551,10 +548,14 @@ const styles = StyleSheet.create((theme) => ({
searchInputContainer: { searchInputContainer: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
borderBottomWidth: 1, borderWidth: 1,
borderBottomColor: theme.colors.border, borderColor: theme.colors.border,
backgroundColor: theme.colors.surface1,
borderRadius: theme.borderRadius.lg,
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
marginBottom: theme.spacing[1], marginHorizontal: theme.spacing[2],
marginBottom: theme.spacing[2],
marginTop: theme.spacing[1],
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
searchInput: { searchInput: {
@@ -571,17 +572,27 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[2],
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
...(IS_WEB
? {}
: {
marginHorizontal: theme.spacing[1],
marginBottom: theme.spacing[1],
}),
},
comboboxItemHovered: {
backgroundColor: theme.colors.surface1,
}, },
comboboxItemPressed: { comboboxItemPressed: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
}, },
comboboxItemActive: { comboboxItemActive: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
}, },
comboboxItemCheckSlot: { comboboxItemTrailingSlot: {
width: 16, width: 16,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
marginLeft: "auto",
}, },
comboboxItemContent: { comboboxItemContent: {
flex: 1, flex: 1,
@@ -603,7 +614,7 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
}, },
bottomSheetBackground: { bottomSheetBackground: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface0,
borderTopLeftRadius: theme.borderRadius["2xl"], borderTopLeftRadius: theme.borderRadius["2xl"],
borderTopRightRadius: theme.borderRadius["2xl"], borderTopRightRadius: theme.borderRadius["2xl"],
}, },
@@ -616,13 +627,14 @@ const styles = StyleSheet.create((theme) => ({
}, },
comboboxTitle: { comboboxTitle: {
fontSize: theme.fontSize.lg, fontSize: theme.fontSize.lg,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground, color: theme.colors.foreground,
textAlign: "center", textAlign: "left",
}, },
comboboxScrollContent: { comboboxScrollContent: {
paddingBottom: theme.spacing[8], paddingBottom: theme.spacing[8],
paddingHorizontal: theme.spacing[1], paddingHorizontal: theme.spacing[2],
paddingTop: theme.spacing[1],
}, },
desktopOverlay: { desktopOverlay: {
flex: 1, flex: 1,
@@ -638,7 +650,7 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface0, backgroundColor: theme.colors.surface0,
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: theme.colors.borderAccent, borderColor: theme.colors.border,
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2, shadowOpacity: 0.2,
@@ -458,6 +458,9 @@ export function DropdownMenuItem({
label = successLabel; label = successLabel;
} }
const trailingContent =
trailing ?? (!showSelectedCheck && selected ? <Check size={16} color={theme.colors.foregroundMuted} /> : null);
return ( return (
<Pressable <Pressable
testID={testID} testID={testID}
@@ -473,8 +476,12 @@ export function DropdownMenuItem({
style={({ pressed, hovered }) => [ style={({ pressed, hovered }) => [
styles.item, styles.item,
selected ? (selectedVariant === "accent" ? styles.itemSelectedAccent : styles.itemSelected) : null, selected ? (selectedVariant === "accent" ? styles.itemSelectedAccent : styles.itemSelected) : null,
selected && (hovered || pressed) && selectedVariant !== "accent"
? styles.itemSelectedInteractive
: null,
isDisabled ? styles.itemDisabled : null, isDisabled ? styles.itemDisabled : null,
(hovered || pressed) && !isDisabled ? styles.itemHovered : null, hovered && !pressed && !isDisabled ? styles.itemHovered : null,
pressed && !isDisabled ? styles.itemPressed : null,
]} ]}
> >
{showSelectedCheck ? ( {showSelectedCheck ? (
@@ -507,7 +514,7 @@ export function DropdownMenuItem({
</Text> </Text>
) : null} ) : null}
</View> </View>
{trailing ? <View style={styles.trailingSlot}>{trailing}</View> : null} {trailingContent ? <View style={styles.trailingSlot}>{trailingContent}</View> : null}
</Pressable> </Pressable>
); );
} }
@@ -565,12 +572,20 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[2],
borderWidth: theme.borderWidth[1],
borderColor: "transparent",
}, },
itemHovered: { itemHovered: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
},
itemPressed: {
backgroundColor: theme.colors.surface1,
}, },
itemSelected: { itemSelected: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface1,
},
itemSelectedInteractive: {
backgroundColor: theme.colors.surface1,
}, },
itemSelectedAccent: { itemSelectedAccent: {
backgroundColor: theme.colors.accent, backgroundColor: theme.colors.accent,
@@ -10,20 +10,16 @@ import {
Platform, Platform,
} from "react-native"; } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router"; 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 { useSafeAreaInsets } from "react-native-safe-area-context";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
import { Monitor } from "lucide-react-native"; import { Folder, GitBranch, Menu, Monitor, PanelLeft } from "lucide-react-native";
import { MenuHeader } from "@/components/headers/menu-header"; import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { AgentInputArea } from "@/components/agent-input-area"; import { AgentInputArea } from "@/components/agent-input-area";
import { AgentStreamView } from "@/components/agent-stream-view"; import { AgentStreamView } from "@/components/agent-stream-view";
import { import { AgentConfigRow, FormSelectTrigger } from "@/components/agent-form/agent-form-dropdowns";
DropdownSheet, import { Combobox } from "@/components/ui/combobox";
GitOptionsSection,
WorkingDirectoryDropdown,
AgentConfigRow,
} from "@/components/agent-form/agent-form-dropdowns";
import { FileDropZone } from "@/components/file-drop-zone"; import { FileDropZone } from "@/components/file-drop-zone";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state"; import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
@@ -34,6 +30,7 @@ import {
import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { formatConnectionStatus } from "@/utils/daemons"; import { formatConnectionStatus } from "@/utils/daemons";
import { usePanelStore } from "@/stores/panel-store";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { useCreateFlowStore } from "@/stores/create-flow-store"; import { useCreateFlowStore } from "@/stores/create-flow-store";
import { MAX_CONTENT_WIDTH } from "@/constants/layout"; import { MAX_CONTENT_WIDTH } from "@/constants/layout";
@@ -123,6 +120,9 @@ export function DraftAgentScreen({
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { connectionStates } = useDaemonConnections(); const { connectionStates } = useDaemonConnections();
const { daemons } = useDaemonRegistry(); 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<DraftAgentParams>(); const params = useLocalSearchParams<DraftAgentParams>();
const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
@@ -204,15 +204,26 @@ export function DraftAgentScreen({
: undefined; : undefined;
const hostLabel = const hostLabel =
hostEntry?.daemon.label ?? selectedServerId ?? "Select host"; hostEntry?.daemon.label ?? selectedServerId ?? "Select host";
const hostStatus = hostEntry?.status const isMobile =
? formatConnectionStatus(hostEntry.status) UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
: undefined; 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 [worktreeMode, setWorktreeMode] = useState<"none" | "create" | "attach">("none");
const [baseBranch, setBaseBranch] = useState(""); const [baseBranch, setBaseBranch] = useState("");
const [worktreeSlug, setWorktreeSlug] = useState(""); const [worktreeSlug, setWorktreeSlug] = useState("");
const [selectedWorktreePath, setSelectedWorktreePath] = useState(""); const [selectedWorktreePath, setSelectedWorktreePath] = useState("");
const [isWorkingDirOpen, setIsWorkingDirOpen] = useState(false);
const [isWorktreePickerOpen, setIsWorktreePickerOpen] = useState(false);
const [isBranchOpen, setIsBranchOpen] = useState(false);
const hostAnchorRef = useRef<View>(null);
const workingDirAnchorRef = useRef<View>(null);
const worktreeAnchorRef = useRef<View>(null);
const branchAnchorRef = useRef<View>(null);
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null); const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const setPendingCreateAttempt = useCreateFlowStore((state) => state.setPending); const setPendingCreateAttempt = useCreateFlowStore((state) => state.setPending);
const updatePendingAgentId = useCreateFlowStore((state) => state.updateAgentId); const updatePendingAgentId = useCreateFlowStore((state) => state.updateAgentId);
@@ -276,15 +287,26 @@ export function DraftAgentScreen({
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => { const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
addImagesRef.current = addImages; addImagesRef.current = addImages;
}, []); }, []);
const openDropdownSheet = useCallback((key: "host") => {
setOpenDropdown(key);
}, []);
const closeDropdown = useCallback(() => {
setOpenDropdown(null);
}, []);
const sessionAgents = useSessionStore((state) => const sessionAgents = useSessionStore((state) =>
selectedServerId ? state.sessions[selectedServerId]?.agents : undefined selectedServerId ? state.sessions[selectedServerId]?.agents : undefined
); );
const worktreePathLastCreatedAt = useMemo(() => {
const map = new Map<string, number>();
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(() => { const agentWorkingDirSuggestions = useMemo(() => {
if (!selectedServerId || !sessionAgents) { if (!selectedServerId || !sessionAgents) {
return []; return [];
@@ -346,7 +368,6 @@ export function DraftAgentScreen({
}); });
const checkout = checkoutStatusQuery.data ?? null; const checkout = checkoutStatusQuery.data ?? null;
const refetchCheckoutStatus = checkoutStatusQuery.refetch;
const checkoutQueryError = const checkoutQueryError =
checkoutStatusQuery.error instanceof Error ? checkoutStatusQuery.error.message : null; checkoutStatusQuery.error instanceof Error ? checkoutStatusQuery.error.message : null;
const checkoutPayloadError = checkout?.error ? checkout.error.message : null; const checkoutPayloadError = checkout?.error ? checkout.error.message : null;
@@ -398,7 +419,6 @@ export function DraftAgentScreen({
return payload.worktrees ?? []; return payload.worktrees ?? [];
}, },
enabled: enabled:
isAttachWorktree &&
Boolean(worktreeListRoot || trimmedWorkingDir) && Boolean(worktreeListRoot || trimmedWorkingDir) &&
!repoAvailabilityError && !repoAvailabilityError &&
Boolean(sessionClient) && Boolean(sessionClient) &&
@@ -409,17 +429,23 @@ export function DraftAgentScreen({
refetchOnMount: "always", refetchOnMount: "always",
}); });
const worktreeOptions = useMemo(() => { const worktreeOptions = useMemo(() => {
return (worktreeListQuery.data ?? []).map((worktree) => ({ const options = (worktreeListQuery.data ?? []).map((worktree) => ({
path: worktree.worktreePath, path: worktree.worktreePath,
label: worktree.branchName ?? worktree.head ?? "Unknown branch", 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 = const worktreeOptionsError =
worktreeListQuery.error instanceof Error ? worktreeListQuery.error.message : null; worktreeListQuery.error instanceof Error ? worktreeListQuery.error.message : null;
const worktreeOptionsStatus: "idle" | "loading" | "ready" | "error" = const worktreeOptionsStatus: "idle" | "loading" | "ready" | "error" =
!isAttachWorktree worktreeListQuery.isPending || worktreeListQuery.isFetching
? "idle"
: worktreeListQuery.isPending || worktreeListQuery.isFetching
? "loading" ? "loading"
: worktreeListQuery.isError : worktreeListQuery.isError
? "error" ? "error"
@@ -432,22 +458,6 @@ export function DraftAgentScreen({
? "Select a worktree to attach" ? "Select a worktree to attach"
: null; : 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( const validateWorktreeName = useCallback(
(name: string): { valid: boolean; error?: string } => { (name: string): { valid: boolean; error?: string } => {
if (!name) { if (!name) {
@@ -579,6 +589,59 @@ export function DraftAgentScreen({
} }
}, [isNonGitDirectory, worktreeMode]); }, [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<string>();
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) => const createAgentClient = useSessionStore((state) =>
selectedServerId ? state.sessions[selectedServerId]?.client ?? null : null selectedServerId ? state.sessions[selectedServerId]?.client ?? null : null
); );
@@ -823,27 +886,27 @@ export function DraftAgentScreen({
<FileDropZone onFilesDropped={handleFilesDropped}> <FileDropZone onFilesDropped={handleFilesDropped}>
<View style={styles.container}> <View style={styles.container}>
<View style={styles.agentPanel}> <View style={styles.agentPanel}>
<MenuHeader <View
title="New agent" style={[
rightContent={ styles.menuToggleContainer,
<Pressable isMobile ? { paddingTop: insets.top + theme.spacing[2] } : null,
style={styles.hostBadge} ]}
onPress={() => openDropdownSheet("host")} >
> <HeaderToggleButton
<Monitor size={14} color={theme.colors.foregroundMuted} /> onPress={toggleAgentList}
<Text style={styles.hostBadgeLabel}>{hostLabel}</Text> tooltipLabel="Toggle sidebar"
<View tooltipKeys={["mod", "B"]}
style={[ tooltipSide="right"
styles.hostStatusDot, testID="menu-button"
hostEntry?.status === "online" && styles.hostStatusDotOnline, nativeID="menu-button"
]} accessible
/> accessibilityRole="button"
{hostStatus ? ( accessibilityLabel={isSidebarOpen ? "Close menu" : "Open menu"}
<Text style={styles.hostBadgeStatus}>{hostStatus}</Text> accessibilityState={{ expanded: isSidebarOpen }}
) : null} >
</Pressable> <SidebarIcon size={isMobile ? 20 : 16} color={sidebarIconColor} />
} </HeaderToggleButton>
/> </View>
<Animated.View style={[styles.contentContainer, animatedKeyboardStyle]}> <Animated.View style={[styles.contentContainer, animatedKeyboardStyle]}>
{machine.tag === "creating" && draftAgent && selectedServerId ? ( {machine.tag === "creating" && draftAgent && selectedServerId ? (
@@ -859,13 +922,29 @@ export function DraftAgentScreen({
) : ( ) : (
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}> <ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
<View style={styles.configSection}> <View style={styles.configSection}>
<WorkingDirectoryDropdown <View style={styles.topSelectorRow}>
workingDir={workingDir} <FormSelectTrigger
errorMessage="" controlRef={workingDirAnchorRef}
disabled={false} containerStyle={styles.topSelectorPrimary}
suggestedPaths={agentWorkingDirSuggestions} label="Working directory"
onSelectPath={setWorkingDirFromUser} value={workingDir}
/> placeholder="/path/to/project"
onPress={() => setIsWorkingDirOpen(true)}
icon={<Folder size={16} color={theme.colors.foregroundMuted} />}
showLabel={false}
valueEllipsizeMode="middle"
/>
<FormSelectTrigger
controlRef={hostAnchorRef}
containerStyle={styles.topSelectorSecondary}
label="Host"
value={hostLabel}
placeholder="Select host"
onPress={() => setIsHostOpen(true)}
icon={<Monitor size={16} color={theme.colors.foregroundMuted} />}
showLabel={false}
/>
</View>
{isDirectoryNotExists && ( {isDirectoryNotExists && (
<View style={styles.warningContainer}> <View style={styles.warningContainer}>
<Text style={styles.warningText}> <Text style={styles.warningText}>
@@ -873,6 +952,36 @@ export function DraftAgentScreen({
</Text> </Text>
</View> </View>
)} )}
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
<View style={styles.topSelectorRow}>
<FormSelectTrigger
controlRef={worktreeAnchorRef}
containerStyle={
worktreeMode === "create" ? styles.halfSelector : styles.topSelectorPrimary
}
label="Worktree"
value={worktreeTriggerValue}
placeholder="Select worktree"
onPress={() => setIsWorktreePickerOpen(true)}
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
showLabel={false}
valueEllipsizeMode="middle"
/>
{worktreeMode === "create" ? (
<FormSelectTrigger
controlRef={branchAnchorRef}
containerStyle={styles.halfSelector}
label="Base branch"
value={baseBranch}
placeholder="From branch"
onPress={() => setIsBranchOpen(true)}
disabled={repoInfoStatus === "loading"}
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
showLabel={false}
/>
) : null}
</View>
) : null}
<AgentConfigRow <AgentConfigRow
providerDefinitions={providerDefinitions} providerDefinitions={providerDefinitions}
selectedProvider={selectedProvider} selectedProvider={selectedProvider}
@@ -885,65 +994,89 @@ export function DraftAgentScreen({
isModelLoading={isModelLoading} isModelLoading={isModelLoading}
onSelectModel={setModelFromUser} onSelectModel={setModelFromUser}
/> />
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? ( {baseBranchError ? <Text style={styles.errorInlineText}>{baseBranchError}</Text> : null}
<GitOptionsSection {repoInfoError ? <Text style={styles.errorInlineText}>{repoInfoError}</Text> : null}
worktreeMode={worktreeMode} {gitBlockingError ? <Text style={styles.errorInlineText}>{gitBlockingError}</Text> : null}
onWorktreeModeChange={handleWorktreeModeChange} {attachWorktreeError ? <Text style={styles.errorInlineText}>{attachWorktreeError}</Text> : null}
worktreeSlug={worktreeSlug} {worktreeOptionsError ? <Text style={styles.errorInlineText}>{worktreeOptionsError}</Text> : null}
currentBranch={checkout?.isGit ? checkout.currentBranch ?? null : null}
baseBranch={baseBranch}
onBaseBranchChange={handleBaseBranchChange}
status={repoInfoStatus}
repoError={repoInfoError}
gitValidationError={gitBlockingError}
baseBranchError={baseBranchError}
worktreeOptions={worktreeOptions}
selectedWorktreePath={selectedWorktreePath}
worktreeOptionsStatus={worktreeOptionsStatus}
worktreeOptionsError={worktreeOptionsError}
attachWorktreeError={attachWorktreeError}
onSelectWorktreePath={handleSelectWorktreePath}
/>
) : null}
</View> </View>
<DropdownSheet <Combobox
options={hostOptions}
value={selectedServerId ?? ""}
onSelect={(serverId) => setSelectedServerIdFromUser(serverId)}
title="Host" title="Host"
visible={openDropdown === "host"} searchPlaceholder="Search hosts..."
onClose={closeDropdown} open={isHostOpen}
> onOpenChange={setIsHostOpen}
{connectionStates.size === 0 ? ( anchorRef={hostAnchorRef}
<Text style={styles.dropdownHelper}> />
No hosts available yet.
</Text> <Combobox
) : ( options={worktreeComboOptions}
<View style={styles.dropdownSheetList}> value={
{Array.from(connectionStates.values()).map(({ daemon, status }) => { worktreeMode === "create"
const isSelected = daemon.serverId === selectedServerId; ? "__create_new__"
const label = daemon.label?.trim() ? daemon.label : daemon.serverId; : worktreeMode === "attach"
return ( ? selectedWorktreePath
<Pressable : "__none__"
key={daemon.serverId} }
style={[ onSelect={(id) => {
styles.dropdownSheetOption, if (id === "__create_new__") {
isSelected && styles.dropdownSheetOptionSelected, setWorktreeMode("create");
]} if (!worktreeSlug) {
onPress={() => { setWorktreeSlug(createNameId());
setSelectedServerIdFromUser(daemon.serverId); }
closeDropdown(); setSelectedWorktreePath("");
}} return;
> }
<Text style={styles.dropdownSheetOptionLabel}> if (id === "__none__") {
{label} setWorktreeMode("none");
</Text> setSelectedWorktreePath("");
<Text style={styles.dropdownSheetOptionDescription}> return;
{formatConnectionStatus(status)} }
</Text> handleSelectWorktreePath(id);
</Pressable> setWorktreeMode("attach");
); }}
})} title="Select worktree"
</View> searchPlaceholder="Search worktrees..."
)} open={isWorktreePickerOpen}
</DropdownSheet> onOpenChange={setIsWorktreePickerOpen}
emptyText="No worktrees found"
anchorRef={worktreeAnchorRef}
/>
<Combobox
options={agentWorkingDirSuggestions.map((path) => ({ 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}
/>
<Combobox
options={branchComboOptions}
value={baseBranch}
onSelect={handleBaseBranchChange}
searchPlaceholder="Choose a base branch..."
allowCustomValue
customValuePrefix="Use"
customValueDescription="Use this branch name"
title="Select base branch"
open={isBranchOpen}
onOpenChange={setIsBranchOpen}
anchorRef={branchAnchorRef}
/>
{formErrorMessage ? ( {formErrorMessage ? (
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
@@ -980,6 +1113,11 @@ const styles = StyleSheet.create((theme) => ({
agentPanel: { agentPanel: {
flex: 1, flex: 1,
}, },
menuToggleContainer: {
paddingHorizontal: theme.spacing[2],
paddingTop: theme.spacing[2],
alignItems: "flex-start",
},
contentContainer: { contentContainer: {
flex: 1, flex: 1,
overflow: "hidden", overflow: "hidden",
@@ -998,7 +1136,7 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "flex-end", justifyContent: "flex-end",
}, },
configSection: { configSection: {
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[0],
paddingTop: theme.spacing[3], paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[4], paddingBottom: theme.spacing[4],
gap: theme.spacing[2], gap: theme.spacing[2],
@@ -1006,46 +1144,31 @@ const styles = StyleSheet.create((theme) => ({
alignSelf: "center", alignSelf: "center",
width: "100%", width: "100%",
}, },
dropdownHelper: { topSelectorRow: {
fontSize: theme.fontSize.sm, flexDirection: "row",
color: theme.colors.foregroundMuted, alignItems: "center",
gap: theme.spacing[2],
}, },
dropdownSheetList: { topSelectorPrimary: {
marginTop: theme.spacing[3], flex: 7,
}, },
dropdownSheetOption: { topSelectorSecondary: {
paddingVertical: theme.spacing[3], flex: 3,
paddingHorizontal: theme.spacing[4],
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
marginBottom: theme.spacing[2],
}, },
dropdownSheetOptionSelected: { halfSelector: {
borderColor: theme.colors.palette.blue[400], flex: 1,
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],
}, },
errorContainer: { errorContainer: {
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3], paddingVertical: theme.spacing[3],
marginHorizontal: theme.spacing[4], marginHorizontal: theme.spacing[0],
marginBottom: theme.spacing[2], marginBottom: theme.spacing[2],
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.destructive, backgroundColor: theme.colors.destructive,
}, },
errorText: { errorText: {
color: theme.colors.destructiveForeground, color: theme.colors.destructiveForeground,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base,
}, },
warningContainer: { warningContainer: {
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
@@ -1055,35 +1178,11 @@ const styles = StyleSheet.create((theme) => ({
}, },
warningText: { warningText: {
color: "#000000", color: "#000000",
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.semibold,
}, },
hostBadge: { errorInlineText: {
flexDirection: "row", color: theme.colors.palette.red[500],
alignItems: "center", fontSize: theme.fontSize.base,
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],
}, },
})); }));
+4 -4
View File
@@ -117,7 +117,7 @@ const lightSemanticColors = {
// Borders - shifted one step lighter // Borders - shifted one step lighter
border: "#e4e4e7", // (was zinc-200, now zinc-200 - keep for contrast) 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 // Brand
accent: "#20744A", accent: "#20744A",
@@ -141,7 +141,7 @@ const lightSemanticColors = {
secondaryForeground: "#09090b", secondaryForeground: "#09090b",
muted: "#f4f4f5", muted: "#f4f4f5",
mutedForeground: "#71717a", mutedForeground: "#71717a",
accentBorder: "#e4e4e7", accentBorder: "#ececf1",
input: "#f4f4f5", input: "#f4f4f5",
ring: "#18181b", ring: "#18181b",
} as const; } as const;
@@ -159,7 +159,7 @@ const darkSemanticColors = {
// Borders // Borders
border: "#27272a", border: "#27272a",
borderAccent: "#3f3f46", borderAccent: "#34343a",
// Brand // Brand
accent: "#20744A", accent: "#20744A",
@@ -183,7 +183,7 @@ const darkSemanticColors = {
secondaryForeground: "#fafafa", secondaryForeground: "#fafafa",
muted: "#27272a", muted: "#27272a",
mutedForeground: "#a1a1aa", mutedForeground: "#a1a1aa",
accentBorder: "#3f3f46", accentBorder: "#34343a",
input: "#27272a", input: "#27272a",
ring: "#d4d4d8", ring: "#d4d4d8",
} as const; } as const;
@@ -63,6 +63,21 @@ interface TurnContext {
streamedReasoningThisTurn: boolean; 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 = { const CLAUDE_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true, supportsStreaming: true,
supportsSessionPersistence: true, supportsSessionPersistence: true,
@@ -371,7 +386,7 @@ export class ClaudeAgentClient implements AgentClient {
return models.map((model) => ({ return models.map((model) => ({
provider: "claude" as const, provider: "claude" as const,
id: model.value, id: model.value,
label: model.displayName, label: normalizeClaudeModelLabel(model),
description: model.description, description: model.description,
thinkingOptions: [ thinkingOptions: [
{ id: "off", label: "Off", isDefault: true }, { id: "off", label: "Off", isDefault: true },