Update files

This commit is contained in:
Mohamed Boudra
2026-02-07 00:07:04 +07:00
parent 8694696f45
commit f26d7f5552
5 changed files with 261 additions and 167 deletions
+129 -88
View File
@@ -416,7 +416,6 @@ export default function SettingsScreen() {
const [pendingRemoveHost, setPendingRemoveHost] = useState<HostProfile | null>(null);
const [isRemovingHost, setIsRemovingHost] = useState(false);
const [editingDaemon, setEditingDaemon] = useState<HostProfile | null>(null);
const [editLabel, setEditLabel] = useState("");
const [isSavingEdit, setIsSavingEdit] = useState(false);
const isLoading = settingsLoading || daemonLoading;
const isMountedRef = useRef(true);
@@ -460,13 +459,11 @@ export default function SettingsScreen() {
const handleEditDaemon = useCallback((profile: HostProfile) => {
setEditingDaemon(profile);
setEditLabel(profile.label ?? "");
}, []);
const handleCloseEditDaemon = useCallback(() => {
if (isSavingEdit) return;
setEditingDaemon(null);
setEditLabel("");
}, [isSavingEdit]);
const closeAddConnectionFlow = useCallback(() => {
@@ -510,17 +507,11 @@ export default function SettingsScreen() {
pendingEditReopenServerId,
]);
useEffect(() => {
if (!editingDaemon) return;
if (editingDaemonLive) return;
handleCloseEditDaemon();
}, [editingDaemon, editingDaemonLive, handleCloseEditDaemon]);
const handleSaveEditDaemon = useCallback(async () => {
const handleSaveEditDaemon = useCallback(async (nextLabelRaw: string) => {
if (!editingDaemon) return;
if (isSavingEdit) return;
const nextLabel = editLabel.trim();
const nextLabel = nextLabelRaw.trim();
if (!nextLabel) {
Alert.alert("Label required", "Enter a label for this host.");
return;
@@ -536,7 +527,7 @@ export default function SettingsScreen() {
} finally {
setIsSavingEdit(false);
}
}, [editLabel, editingDaemon, handleCloseEditDaemon, isSavingEdit, updateHost]);
}, [editingDaemon, handleCloseEditDaemon, isSavingEdit, updateHost]);
const handleRemoveConnection = useCallback(
async (serverId: string, connectionId: string) => {
@@ -757,84 +748,14 @@ export default function SettingsScreen() {
</AdaptiveModalSheet>
) : null}
<AdaptiveModalSheet
title="Edit host"
<EditHostModal
visible={Boolean(editingDaemon)}
host={editingDaemonLive ?? editingDaemon}
isSaving={isSavingEdit}
onClose={handleCloseEditDaemon}
testID="edit-host-modal"
>
<View style={styles.formField}>
<Text style={styles.label}>Label</Text>
<AdaptiveTextInput
style={styles.input}
value={editLabel}
onChangeText={setEditLabel}
placeholder="My Host"
placeholderTextColor={defaultTheme.colors.mutedForeground}
/>
</View>
{editingDaemonLive ? (
<View style={styles.formField}>
<Text style={styles.label}>Connections</Text>
<View style={{ gap: 8 }}>
{editingDaemonLive.connections.map((conn) => {
const title =
conn.type === "relay"
? `Relay (${conn.relayEndpoint})`
: `Direct (${conn.endpoint})`;
return (
<View
key={conn.id}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
paddingVertical: 10,
paddingHorizontal: 12,
borderRadius: 12,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface2,
}}
>
<Text style={{ color: theme.colors.foreground, fontSize: 12, flex: 1 }}>
{title}
</Text>
<Pressable
onPress={() => void handleRemoveConnection(editingDaemonLive.serverId, conn.id)}
>
<Text style={{ color: theme.colors.destructive, fontSize: 12, fontWeight: "500" }}>
Remove
</Text>
</Pressable>
</View>
);
})}
</View>
</View>
) : null}
<View style={styles.formActionsRow}>
<Button
variant="secondary"
size="sm"
onPress={handleCloseEditDaemon}
disabled={isSavingEdit}
>
Cancel
</Button>
<Button
variant="default"
size="sm"
onPress={() => void handleSaveEditDaemon()}
disabled={isSavingEdit}
>
{isSavingEdit ? "Saving..." : "Save"}
</Button>
</View>
</AdaptiveModalSheet>
onSave={(label) => void handleSaveEditDaemon(label)}
onRemoveConnection={handleRemoveConnection}
/>
{/* Appearance */}
<View style={styles.section}>
@@ -895,6 +816,126 @@ export default function SettingsScreen() {
);
}
interface EditHostModalProps {
visible: boolean;
host: HostProfile | null;
isSaving: boolean;
onClose: () => void;
onSave: (label: string) => void;
onRemoveConnection: (serverId: string, connectionId: string) => Promise<void>;
}
function EditHostModal({
visible,
host,
isSaving,
onClose,
onSave,
onRemoveConnection,
}: EditHostModalProps) {
const [draftLabel, setDraftLabel] = useState("");
const activeServerIdRef = useRef<string | null>(null);
useEffect(() => {
if (!visible || !host) return;
if (activeServerIdRef.current !== host.serverId) {
setDraftLabel(host.label ?? "");
activeServerIdRef.current = host.serverId;
return;
}
if (!draftLabel.trim()) {
setDraftLabel(host.label ?? "");
}
}, [visible, host, draftLabel]);
useEffect(() => {
if (!visible) {
activeServerIdRef.current = null;
}
}, [visible]);
return (
<AdaptiveModalSheet
title="Edit host"
visible={visible}
onClose={onClose}
testID="edit-host-modal"
>
<View style={styles.formField}>
<Text style={styles.label}>Label</Text>
<AdaptiveTextInput
style={styles.input}
value={draftLabel}
onChangeText={setDraftLabel}
placeholder="My Host"
placeholderTextColor={defaultTheme.colors.mutedForeground}
/>
</View>
{host ? (
<View style={styles.formField}>
<Text style={styles.label}>Connections</Text>
<View style={{ gap: 8 }}>
{host.connections.map((conn) => {
const title =
conn.type === "relay"
? `Relay (${conn.relayEndpoint})`
: `Direct (${conn.endpoint})`;
return (
<View
key={conn.id}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
paddingVertical: 10,
paddingHorizontal: 12,
borderRadius: 12,
borderWidth: 1,
borderColor: defaultTheme.colors.border,
backgroundColor: defaultTheme.colors.surface2,
}}
>
<Text style={{ color: defaultTheme.colors.foreground, fontSize: 12, flex: 1 }}>
{title}
</Text>
<Pressable
onPress={() => void onRemoveConnection(host.serverId, conn.id)}
>
<Text style={{ color: defaultTheme.colors.destructive, fontSize: 12, fontWeight: "500" }}>
Remove
</Text>
</Pressable>
</View>
);
})}
</View>
</View>
) : null}
<View style={styles.formActionsRow}>
<Button
variant="secondary"
size="sm"
onPress={onClose}
disabled={isSaving}
>
Cancel
</Button>
<Button
variant="default"
size="sm"
onPress={() => onSave(draftLabel)}
disabled={isSaving}
>
{isSaving ? "Saving..." : "Save"}
</Button>
</View>
</AdaptiveModalSheet>
);
}
interface DaemonCardProps {
daemon: HostProfile;
connectionStatus: ConnectionStatus;
@@ -163,6 +163,15 @@ export function SelectField({
[getWebKey, onPress, preventWebDefault]
);
const normalizedValue = (value ?? "").trim();
const normalizedPlaceholder = (placeholder ?? "").trim();
const hasConcreteValue =
normalizedValue.length > 0 &&
(normalizedPlaceholder.length === 0 || normalizedValue !== normalizedPlaceholder);
const displayText = hasConcreteValue
? normalizedValue
: (normalizedPlaceholder || "Select...");
return (
<View style={styles.selectFieldContainer}>
<Pressable
@@ -423,6 +432,14 @@ export function FormSelectTrigger({
},
[getWebKey, onPress, preventWebDefault]
);
const normalizedValue = (value ?? "").trim();
const normalizedPlaceholder = (placeholder ?? "").trim();
const hasConcreteValue =
normalizedValue.length > 0 &&
(normalizedPlaceholder.length === 0 || normalizedValue !== normalizedPlaceholder);
const displayText = hasConcreteValue
? normalizedValue
: (normalizedPlaceholder || "Select...");
return (
<Pressable
@@ -451,11 +468,11 @@ export function FormSelectTrigger({
<ActivityIndicator size="small" color={defaultTheme.colors.foregroundMuted} />
) : (
<Text
style={value ? styles.compactSelectValue : styles.compactSelectPlaceholder}
style={hasConcreteValue ? styles.compactSelectValue : styles.compactSelectPlaceholder}
numberOfLines={1}
ellipsizeMode={valueEllipsizeMode}
>
{value || placeholder || "Select..."}
{displayText}
</Text>
)}
</View>
@@ -1426,7 +1443,10 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[1],
},
agentConfigRow: {
flexDirection: "row",
flexDirection: {
xs: "column",
md: "row",
},
gap: theme.spacing[2],
},
agentConfigColumn: {
@@ -1206,7 +1206,10 @@ const stylesheet = StyleSheet.create((theme) => ({
},
list: {
flex: 1,
paddingHorizontal: theme.spacing[2],
paddingHorizontal: {
xs: theme.spacing[2],
md: theme.spacing[4],
},
},
streamItemWrapper: {
width: "100%",
+10 -25
View File
@@ -1050,24 +1050,18 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
accessibilityRole="button"
accessibilityLabel={gitActions.primary.label}
>
<View style={styles.splitButtonPrimaryInner}>
<View
style={[
styles.splitButtonContent,
gitActions.primary.status === "pending" && styles.splitButtonContentHidden,
]}
>
{gitActions.primary.status === "pending" ? (
<ActivityIndicator
size="small"
color={theme.colors.foreground}
style={styles.splitButtonSpinnerOnly}
/>
) : (
<View style={styles.splitButtonContent}>
{gitActions.primary.icon}
<Text style={styles.splitButtonText}>{getActionDisplayLabel(gitActions.primary)}</Text>
</View>
{gitActions.primary.status === "pending" ? (
<ActivityIndicator
size="small"
color={theme.colors.foreground}
style={styles.splitButtonSpinnerOverlay}
/>
) : null}
</View>
)}
</Pressable>
{gitActions.secondary.length > 0 ? (
<DropdownMenu>
@@ -1306,11 +1300,6 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "center",
position: "relative",
},
splitButtonPrimaryInner: {
position: "relative",
alignItems: "center",
justifyContent: "center",
},
splitButtonPrimaryDisabled: {
opacity: 0.6,
},
@@ -1326,11 +1315,7 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "center",
gap: theme.spacing[2],
},
splitButtonContentHidden: {
opacity: 0,
},
splitButtonSpinnerOverlay: {
position: "absolute",
splitButtonSpinnerOnly: {
transform: [{ scale: 0.8 }],
},
splitButtonCaret: {
@@ -30,6 +30,7 @@ import {
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { formatConnectionStatus } from "@/utils/daemons";
import { shortenPath } from "@/utils/shorten-path";
import { usePanelStore } from "@/stores/panel-store";
import { useSessionStore } from "@/stores/session-store";
import { useCreateFlowStore } from "@/stores/create-flow-store";
@@ -199,11 +200,25 @@ export function DraftAgentScreen({
isCreateFlow: true,
onlineServerIds,
});
const daemonLabelByServerId = useMemo(() => {
const map = new Map<string, string>();
for (const daemon of daemons) {
const label = daemon.label?.trim();
map.set(daemon.serverId, label && label.length > 0 ? label : daemon.serverId);
}
return map;
}, [daemons]);
const hostEntry = selectedServerId
? connectionStates.get(selectedServerId)
: undefined;
const selectedDaemonLabel = selectedServerId
? daemonLabelByServerId.get(selectedServerId)
: null;
const hostLabel =
hostEntry?.daemon.label ?? selectedServerId ?? "Select host";
selectedDaemonLabel ??
hostEntry?.daemon.label ??
selectedServerId ??
"Select host";
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const isSidebarOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
@@ -561,9 +576,8 @@ export function DraftAgentScreen({
const handleSelectWorktreePath = useCallback(
(path: string) => {
setSelectedWorktreePath(path);
setWorkingDirFromUser(path);
},
[setWorkingDirFromUser]
[]
);
useEffect(() => {
@@ -591,35 +605,36 @@ export function DraftAgentScreen({
const selectedWorktreeLabel =
worktreeOptions.find((option) => option.path === selectedWorktreePath)?.label ?? "";
const displayWorkingDir = shortenPath(workingDir);
const worktreeTriggerValue =
worktreeMode === "create"
? "Create new worktree"
: selectedWorktreeLabel || "Select worktree";
const hostOptions = useMemo(
() =>
Array.from(connectionStates.values()).map(({ daemon, status }) => ({
daemons.map((daemon) => ({
id: daemon.serverId,
label: daemon.label?.trim() ? daemon.label : daemon.serverId,
description: formatConnectionStatus(status),
label: daemonLabelByServerId.get(daemon.serverId) ?? daemon.serverId,
description: formatConnectionStatus(
connectionStates.get(daemon.serverId)?.status ?? "idle"
),
})),
[connectionStates]
[connectionStates, daemonLabelByServerId, daemons]
);
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",
},
{
id: "__create_new__",
label: "Create new worktree",
},
...worktreeOptions.map((option) => ({
id: option.path,
label: option.label,
description: option.path,
description: shortenPath(option.path),
})),
],
[worktreeOptions]
@@ -922,12 +937,12 @@ export function DraftAgentScreen({
) : (
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
<View style={styles.configSection}>
<View style={styles.topSelectorRow}>
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
<FormSelectTrigger
controlRef={workingDirAnchorRef}
containerStyle={styles.topSelectorPrimary}
containerStyle={isMobile ? styles.fullSelector : styles.topSelectorPrimary}
label="Working directory"
value={workingDir}
value={displayWorkingDir}
placeholder="/path/to/project"
onPress={() => setIsWorkingDirOpen(true)}
icon={<Folder size={16} color={theme.colors.foregroundMuted} />}
@@ -936,7 +951,7 @@ export function DraftAgentScreen({
/>
<FormSelectTrigger
controlRef={hostAnchorRef}
containerStyle={styles.topSelectorSecondary}
containerStyle={isMobile ? styles.fullSelector : styles.topSelectorSecondary}
label="Host"
value={hostLabel}
placeholder="Select host"
@@ -952,36 +967,7 @@ export function DraftAgentScreen({
</Text>
</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}
{isMobile ? <View style={styles.formSeparator} /> : null}
<AgentConfigRow
providerDefinitions={providerDefinitions}
selectedProvider={selectedProvider}
@@ -994,6 +980,43 @@ export function DraftAgentScreen({
isModelLoading={isModelLoading}
onSelectModel={setModelFromUser}
/>
{isMobile && trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
<View style={styles.formSeparator} />
) : null}
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
<FormSelectTrigger
controlRef={worktreeAnchorRef}
containerStyle={
isMobile
? styles.fullSelector
: 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={isMobile ? styles.fullSelector : 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}
{baseBranchError ? <Text style={styles.errorInlineText}>{baseBranchError}</Text> : null}
{repoInfoError ? <Text style={styles.errorInlineText}>{repoInfoError}</Text> : null}
{gitBlockingError ? <Text style={styles.errorInlineText}>{gitBlockingError}</Text> : null}
@@ -1046,7 +1069,10 @@ export function DraftAgentScreen({
/>
<Combobox
options={agentWorkingDirSuggestions.map((path) => ({ id: path, label: path }))}
options={agentWorkingDirSuggestions.map((path) => ({
id: path,
label: shortenPath(path),
}))}
value={workingDir}
onSelect={setWorkingDirFromUser}
searchPlaceholder="/path/to/project"
@@ -1136,7 +1162,10 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "flex-end",
},
configSection: {
paddingHorizontal: theme.spacing[0],
paddingHorizontal: {
xs: theme.spacing[4],
md: theme.spacing[0],
},
paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[4],
gap: theme.spacing[2],
@@ -1149,6 +1178,22 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
gap: theme.spacing[2],
},
stackedSelectorGroup: {
gap: theme.spacing[2],
},
fullSelector: {
width: "100%",
},
formSeparator: {
height: theme.borderWidth[1],
backgroundColor: {
xs: theme.colors.surface1,
sm: theme.colors.surface1,
md: theme.colors.border,
},
marginHorizontal: theme.spacing[1],
marginVertical: theme.spacing[2],
},
topSelectorPrimary: {
flex: 7,
},