Redesign new agent page UI layout
- Move host selector to header (right side) with icon + name + status dot - Combine provider/model/mode into single "Agent" dropdown - Replace Git branch/worktree toggles with segmented IsolationControl (None/Branch/Worktree) - Only show Git section for git repositories (hide for non-git directories) - Wrap config section in ScrollView for proper scrolling - Simplify layout: Working Directory → Agent → Git (when applicable) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4cf0ea73a1
commit
26da925de6
@@ -3,20 +3,18 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Pressable,
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} 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, useUnistyles } from "react-native-unistyles";
|
||||||
import { ChevronRight, Plus } from "lucide-react-native";
|
import { ChevronRight, Plus, Monitor } from "lucide-react-native";
|
||||||
import { BackHeader } from "@/components/headers/back-header";
|
import { BackHeader } from "@/components/headers/back-header";
|
||||||
import { AgentList } from "@/components/agent-list";
|
import { AgentList } from "@/components/agent-list";
|
||||||
import { AgentInputArea } from "@/components/agent-input-area";
|
import { AgentInputArea } from "@/components/agent-input-area";
|
||||||
import {
|
import {
|
||||||
AssistantDropdown,
|
|
||||||
DropdownSheet,
|
DropdownSheet,
|
||||||
GitOptionsSection,
|
GitOptionsSection,
|
||||||
ModelDropdown,
|
|
||||||
PermissionsDropdown,
|
|
||||||
WorkingDirectoryDropdown,
|
WorkingDirectoryDropdown,
|
||||||
} from "@/components/agent-form/agent-form-dropdowns";
|
} from "@/components/agent-form/agent-form-dropdowns";
|
||||||
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
import { useDaemonRequest } from "@/hooks/use-daemon-request";
|
||||||
@@ -120,20 +118,23 @@ export default function DraftAgentScreen() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const [openDropdown, setOpenDropdown] = useState<
|
const [openDropdown, setOpenDropdown] = useState<
|
||||||
"host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch" | null
|
"host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch" | "agent" | null
|
||||||
>(null);
|
>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [baseBranch, setBaseBranch] = useState("");
|
const [baseBranch, setBaseBranch] = useState("");
|
||||||
const [createNewBranch, setCreateNewBranch] = useState(false);
|
// Isolation mode: "none" | "branch" | "worktree"
|
||||||
|
const [isolationMode, setIsolationMode] = useState<"none" | "branch" | "worktree">("none");
|
||||||
const [branchName, setBranchName] = useState("");
|
const [branchName, setBranchName] = useState("");
|
||||||
const [createWorktree, setCreateWorktree] = useState(false);
|
|
||||||
const [worktreeSlug, setWorktreeSlug] = useState("");
|
const [worktreeSlug, setWorktreeSlug] = useState("");
|
||||||
const [branchNameEdited, setBranchNameEdited] = useState(false);
|
const [branchNameEdited, setBranchNameEdited] = useState(false);
|
||||||
const [worktreeSlugEdited, setWorktreeSlugEdited] = useState(false);
|
const [worktreeSlugEdited, setWorktreeSlugEdited] = useState(false);
|
||||||
const shouldSyncBaseBranchRef = useRef(true);
|
const shouldSyncBaseBranchRef = useRef(true);
|
||||||
|
// Derive old flags from isolationMode for backwards compatibility
|
||||||
|
const createNewBranch = isolationMode === "branch" || isolationMode === "worktree";
|
||||||
|
const createWorktree = isolationMode === "worktree";
|
||||||
const openDropdownSheet = useCallback(
|
const openDropdownSheet = useCallback(
|
||||||
(key: "host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch") => {
|
(key: "host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch" | "agent") => {
|
||||||
setOpenDropdown(key);
|
setOpenDropdown(key);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -393,14 +394,12 @@ export default function DraftAgentScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
createNewBranch ||
|
isolationMode !== "none" ||
|
||||||
createWorktree ||
|
|
||||||
baseBranch.trim().length > 0 ||
|
baseBranch.trim().length > 0 ||
|
||||||
branchName.trim().length > 0 ||
|
branchName.trim().length > 0 ||
|
||||||
worktreeSlug.trim().length > 0
|
worktreeSlug.trim().length > 0
|
||||||
) {
|
) {
|
||||||
setCreateNewBranch(false);
|
setIsolationMode("none");
|
||||||
setCreateWorktree(false);
|
|
||||||
setBaseBranch("");
|
setBaseBranch("");
|
||||||
setBranchName("");
|
setBranchName("");
|
||||||
setWorktreeSlug("");
|
setWorktreeSlug("");
|
||||||
@@ -411,8 +410,7 @@ export default function DraftAgentScreen() {
|
|||||||
}, [
|
}, [
|
||||||
baseBranch,
|
baseBranch,
|
||||||
branchName,
|
branchName,
|
||||||
createNewBranch,
|
isolationMode,
|
||||||
createWorktree,
|
|
||||||
isNonGitDirectory,
|
isNonGitDirectory,
|
||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
]);
|
]);
|
||||||
@@ -657,62 +655,28 @@ export default function DraftAgentScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.agentPanel}>
|
<View style={styles.agentPanel}>
|
||||||
<BackHeader title="New Agent" onBack={handleBackToHome} />
|
<BackHeader
|
||||||
|
title="New Agent"
|
||||||
|
onBack={handleBackToHome}
|
||||||
|
rightContent={
|
||||||
|
<Pressable
|
||||||
|
style={styles.hostBadge}
|
||||||
|
onPress={() => openDropdownSheet("host")}
|
||||||
|
>
|
||||||
|
<Monitor size={14} color={theme.colors.mutedForeground} />
|
||||||
|
<Text style={styles.hostBadgeLabel}>{hostLabel}</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.hostStatusDot,
|
||||||
|
hostEntry?.status === "online" && styles.hostStatusDotOnline,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<View style={styles.contentContainer}>
|
<ScrollView style={styles.contentContainer} contentContainerStyle={styles.configScrollContent}>
|
||||||
<View style={styles.configSection}>
|
<View style={styles.configSection}>
|
||||||
{renderConfigRow({
|
|
||||||
label: "Host",
|
|
||||||
value: hostLabel,
|
|
||||||
meta: hostStatus,
|
|
||||||
onPress: () => openDropdownSheet("host"),
|
|
||||||
})}
|
|
||||||
<AssistantDropdown
|
|
||||||
providerDefinitions={providerDefinitions}
|
|
||||||
disabled={false}
|
|
||||||
selectedProvider={selectedProvider}
|
|
||||||
isOpen={openDropdown === "provider"}
|
|
||||||
onOpen={() => openDropdownSheet("provider")}
|
|
||||||
onClose={closeDropdown}
|
|
||||||
onSelect={setProviderFromUser}
|
|
||||||
label="Provider"
|
|
||||||
placeholder="Select provider"
|
|
||||||
sheetTitle="Choose Provider"
|
|
||||||
wrapInContainer={false}
|
|
||||||
renderTrigger={renderDropdownTrigger}
|
|
||||||
/>
|
|
||||||
<PermissionsDropdown
|
|
||||||
disabled={false}
|
|
||||||
modeOptions={modeOptions}
|
|
||||||
selectedMode={selectedMode}
|
|
||||||
isOpen={openDropdown === "mode"}
|
|
||||||
onOpen={() => openDropdownSheet("mode")}
|
|
||||||
onClose={closeDropdown}
|
|
||||||
onSelect={setModeFromUser}
|
|
||||||
label="Mode"
|
|
||||||
placeholder="Select mode"
|
|
||||||
sheetTitle="Mode"
|
|
||||||
wrapInContainer={false}
|
|
||||||
renderTrigger={renderDropdownTrigger}
|
|
||||||
/>
|
|
||||||
<ModelDropdown
|
|
||||||
models={availableModels}
|
|
||||||
selectedModel={selectedModel}
|
|
||||||
isLoading={isModelLoading}
|
|
||||||
error={modelError}
|
|
||||||
isOpen={openDropdown === "model"}
|
|
||||||
onOpen={() => {
|
|
||||||
refreshProviderModels();
|
|
||||||
openDropdownSheet("model");
|
|
||||||
}}
|
|
||||||
onClose={closeDropdown}
|
|
||||||
onSelect={setModelFromUser}
|
|
||||||
onClear={() => setModelFromUser("")}
|
|
||||||
onRefresh={refreshProviderModels}
|
|
||||||
label="Model"
|
|
||||||
wrapInContainer={false}
|
|
||||||
renderTrigger={renderDropdownTrigger}
|
|
||||||
/>
|
|
||||||
<WorkingDirectoryDropdown
|
<WorkingDirectoryDropdown
|
||||||
workingDir={workingDir}
|
workingDir={workingDir}
|
||||||
errorMessage=""
|
errorMessage=""
|
||||||
@@ -726,7 +690,13 @@ export default function DraftAgentScreen() {
|
|||||||
wrapInContainer={false}
|
wrapInContainer={false}
|
||||||
renderTrigger={renderDropdownTrigger}
|
renderTrigger={renderDropdownTrigger}
|
||||||
/>
|
/>
|
||||||
{trimmedWorkingDir.length > 0 ? (
|
{renderConfigRow({
|
||||||
|
label: "Agent",
|
||||||
|
value: `${providerDefinitions.find((p) => p.id === selectedProvider)?.label ?? selectedProvider} · ${selectedModel || "auto"}`,
|
||||||
|
onPress: () => openDropdownSheet("agent"),
|
||||||
|
})}
|
||||||
|
{/* Git section - only show for git repos */}
|
||||||
|
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
||||||
<GitOptionsSection
|
<GitOptionsSection
|
||||||
baseBranch={baseBranch}
|
baseBranch={baseBranch}
|
||||||
onBaseBranchChange={handleBaseBranchChange}
|
onBaseBranchChange={handleBaseBranchChange}
|
||||||
@@ -736,43 +706,37 @@ export default function DraftAgentScreen() {
|
|||||||
helperText={gitHelperText}
|
helperText={gitHelperText}
|
||||||
isGitDisabled={Boolean(gitHelperText)}
|
isGitDisabled={Boolean(gitHelperText)}
|
||||||
warning={
|
warning={
|
||||||
!createWorktree && repoInfo?.isDirty
|
repoInfo?.isDirty
|
||||||
? "Working directory has uncommitted changes"
|
? "Working directory has uncommitted changes"
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
createNewBranch={createNewBranch}
|
isolationMode={isolationMode}
|
||||||
onToggleCreateNewBranch={(next) => {
|
onIsolationModeChange={(mode) => {
|
||||||
setCreateNewBranch(next);
|
setIsolationMode(mode);
|
||||||
if (next) {
|
if (mode === "none") {
|
||||||
|
setBranchName("");
|
||||||
|
setWorktreeSlug("");
|
||||||
|
setBranchNameEdited(false);
|
||||||
|
setWorktreeSlugEdited(false);
|
||||||
|
} else if (mode === "branch") {
|
||||||
if (!branchNameEdited) {
|
if (!branchNameEdited) {
|
||||||
const slug = slugifyWorktreeName(baseBranch || "");
|
const slug = slugifyWorktreeName(baseBranch || "");
|
||||||
setBranchName(slug);
|
setBranchName(slug);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (mode === "worktree") {
|
||||||
setBranchName("");
|
|
||||||
setBranchNameEdited(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
branchName={branchName}
|
|
||||||
onBranchNameChange={(value) => {
|
|
||||||
setBranchName(slugifyWorktreeName(value));
|
|
||||||
setBranchNameEdited(true);
|
|
||||||
}}
|
|
||||||
createWorktree={createWorktree}
|
|
||||||
onToggleCreateWorktree={(next) => {
|
|
||||||
setCreateWorktree(next);
|
|
||||||
if (next) {
|
|
||||||
if (!worktreeSlugEdited) {
|
if (!worktreeSlugEdited) {
|
||||||
const slug = slugifyWorktreeName(
|
const slug = slugifyWorktreeName(
|
||||||
branchName || baseBranch || ""
|
branchName || baseBranch || ""
|
||||||
);
|
);
|
||||||
setWorktreeSlug(slug);
|
setWorktreeSlug(slug);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setWorktreeSlug("");
|
|
||||||
setWorktreeSlugEdited(false);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
branchName={branchName}
|
||||||
|
onBranchNameChange={(value) => {
|
||||||
|
setBranchName(slugifyWorktreeName(value));
|
||||||
|
setBranchNameEdited(true);
|
||||||
|
}}
|
||||||
worktreeSlug={worktreeSlug}
|
worktreeSlug={worktreeSlug}
|
||||||
onWorktreeSlugChange={(value) => {
|
onWorktreeSlugChange={(value) => {
|
||||||
setWorktreeSlug(slugifyWorktreeName(value));
|
setWorktreeSlug(slugifyWorktreeName(value));
|
||||||
@@ -824,13 +788,118 @@ export default function DraftAgentScreen() {
|
|||||||
)}
|
)}
|
||||||
</DropdownSheet>
|
</DropdownSheet>
|
||||||
|
|
||||||
|
{/* Agent dropdown sheet - provider + model + mode */}
|
||||||
|
<DropdownSheet
|
||||||
|
title="Agent"
|
||||||
|
visible={openDropdown === "agent"}
|
||||||
|
onClose={closeDropdown}
|
||||||
|
>
|
||||||
|
<View style={styles.agentSheetSection}>
|
||||||
|
<Text style={styles.agentSheetSectionLabel}>Provider</Text>
|
||||||
|
<View style={styles.dropdownSheetList}>
|
||||||
|
{providerDefinitions.map((definition) => {
|
||||||
|
const isSelected = definition.id === selectedProvider;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={definition.id}
|
||||||
|
style={[
|
||||||
|
styles.dropdownSheetOption,
|
||||||
|
isSelected && styles.dropdownSheetOptionSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => {
|
||||||
|
setProviderFromUser(definition.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.dropdownSheetOptionLabel}>
|
||||||
|
{definition.label}
|
||||||
|
</Text>
|
||||||
|
{definition.description ? (
|
||||||
|
<Text style={styles.dropdownSheetOptionDescription}>
|
||||||
|
{definition.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.agentSheetSection}>
|
||||||
|
<Text style={styles.agentSheetSectionLabel}>Model</Text>
|
||||||
|
<View style={styles.dropdownSheetList}>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.dropdownSheetOption,
|
||||||
|
!selectedModel && styles.dropdownSheetOptionSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setModelFromUser("")}
|
||||||
|
>
|
||||||
|
<Text style={styles.dropdownSheetOptionLabel}>
|
||||||
|
Automatic (provider default)
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
{availableModels.map((model) => {
|
||||||
|
const isSelected = model.id === selectedModel;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={model.id}
|
||||||
|
style={[
|
||||||
|
styles.dropdownSheetOption,
|
||||||
|
isSelected && styles.dropdownSheetOptionSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setModelFromUser(model.id)}
|
||||||
|
>
|
||||||
|
<Text style={styles.dropdownSheetOptionLabel}>
|
||||||
|
{model.label}
|
||||||
|
</Text>
|
||||||
|
{model.description ? (
|
||||||
|
<Text style={styles.dropdownSheetOptionDescription}>
|
||||||
|
{model.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{modeOptions.length > 0 ? (
|
||||||
|
<View style={styles.agentSheetSection}>
|
||||||
|
<Text style={styles.agentSheetSectionLabel}>Mode</Text>
|
||||||
|
<View style={styles.dropdownSheetList}>
|
||||||
|
{modeOptions.map((mode) => {
|
||||||
|
const isSelected = mode.id === selectedMode;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={mode.id}
|
||||||
|
style={[
|
||||||
|
styles.dropdownSheetOption,
|
||||||
|
isSelected && styles.dropdownSheetOptionSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setModeFromUser(mode.id)}
|
||||||
|
>
|
||||||
|
<Text style={styles.dropdownSheetOptionLabel}>
|
||||||
|
{mode.label}
|
||||||
|
</Text>
|
||||||
|
{mode.description ? (
|
||||||
|
<Text style={styles.dropdownSheetOptionDescription}>
|
||||||
|
{mode.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</DropdownSheet>
|
||||||
|
|
||||||
{errorMessage ? (
|
{errorMessage ? (
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={styles.streamContainer} />
|
</ScrollView>
|
||||||
</View>
|
|
||||||
<AgentInputArea
|
<AgentInputArea
|
||||||
agentId={DRAFT_AGENT_ID}
|
agentId={DRAFT_AGENT_ID}
|
||||||
serverId={selectedServerId ?? ""}
|
serverId={selectedServerId ?? ""}
|
||||||
@@ -881,10 +950,13 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
configScrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
configSection: {
|
configSection: {
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingTop: theme.spacing[3],
|
paddingTop: theme.spacing[3],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[4],
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
configRow: {
|
configRow: {
|
||||||
@@ -949,9 +1021,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
marginTop: theme.spacing[1],
|
marginTop: theme.spacing[1],
|
||||||
},
|
},
|
||||||
streamContainer: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
paddingVertical: theme.spacing[3],
|
paddingVertical: theme.spacing[3],
|
||||||
@@ -964,4 +1033,36 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.destructiveForeground,
|
color: theme.colors.destructiveForeground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
|
hostBadge: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
|
backgroundColor: theme.colors.muted,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
|
},
|
||||||
|
hostBadgeLabel: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
hostStatusDot: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
|
backgroundColor: theme.colors.mutedForeground,
|
||||||
|
},
|
||||||
|
hostStatusDotOnline: {
|
||||||
|
backgroundColor: theme.colors.palette.green[500],
|
||||||
|
},
|
||||||
|
agentSheetSection: {
|
||||||
|
marginBottom: theme.spacing[4],
|
||||||
|
},
|
||||||
|
agentSheetSectionLabel: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
marginBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -609,6 +609,57 @@ export function ToggleRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IsolationMode = "none" | "branch" | "worktree";
|
||||||
|
|
||||||
|
interface IsolationControlProps {
|
||||||
|
value: IsolationMode;
|
||||||
|
onChange: (mode: IsolationMode) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsolationControl({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}: IsolationControlProps): ReactElement {
|
||||||
|
const options: Array<{ id: IsolationMode; label: string }> = [
|
||||||
|
{ id: "none", label: "None" },
|
||||||
|
{ id: "branch", label: "Branch" },
|
||||||
|
{ id: "worktree", label: "Worktree" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.segmentedControl, disabled && styles.segmentedControlDisabled]}>
|
||||||
|
{options.map((option) => {
|
||||||
|
const isSelected = option.id === value;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={option.id}
|
||||||
|
onPress={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
onChange(option.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
styles.segmentedOption,
|
||||||
|
isSelected && styles.segmentedOptionSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.segmentedOptionText,
|
||||||
|
isSelected && styles.segmentedOptionTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface GitOptionsSectionProps {
|
export interface GitOptionsSectionProps {
|
||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
onBaseBranchChange: (value: string) => void;
|
onBaseBranchChange: (value: string) => void;
|
||||||
@@ -617,12 +668,10 @@ export interface GitOptionsSectionProps {
|
|||||||
repoError: string | null;
|
repoError: string | null;
|
||||||
helperText?: string | null;
|
helperText?: string | null;
|
||||||
warning: string | null;
|
warning: string | null;
|
||||||
createNewBranch: boolean;
|
isolationMode: IsolationMode;
|
||||||
onToggleCreateNewBranch: (value: boolean) => void;
|
onIsolationModeChange: (mode: IsolationMode) => void;
|
||||||
branchName: string;
|
branchName: string;
|
||||||
onBranchNameChange: (value: string) => void;
|
onBranchNameChange: (value: string) => void;
|
||||||
createWorktree: boolean;
|
|
||||||
onToggleCreateWorktree: (value: boolean) => void;
|
|
||||||
worktreeSlug: string;
|
worktreeSlug: string;
|
||||||
onWorktreeSlugChange: (value: string) => void;
|
onWorktreeSlugChange: (value: string) => void;
|
||||||
gitValidationError: string | null;
|
gitValidationError: string | null;
|
||||||
@@ -640,12 +689,10 @@ export function GitOptionsSection({
|
|||||||
repoError,
|
repoError,
|
||||||
helperText,
|
helperText,
|
||||||
warning,
|
warning,
|
||||||
createNewBranch,
|
isolationMode,
|
||||||
onToggleCreateNewBranch,
|
onIsolationModeChange,
|
||||||
branchName,
|
branchName,
|
||||||
onBranchNameChange,
|
onBranchNameChange,
|
||||||
createWorktree,
|
|
||||||
onToggleCreateWorktree,
|
|
||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
onWorktreeSlugChange,
|
onWorktreeSlugChange,
|
||||||
gitValidationError,
|
gitValidationError,
|
||||||
@@ -668,6 +715,10 @@ export function GitOptionsSection({
|
|||||||
const baseInputRef = useRef<TextInput | null>(null);
|
const baseInputRef = useRef<TextInput | null>(null);
|
||||||
const gitInputsDisabled = Boolean(isGitDisabled) || status === "loading";
|
const gitInputsDisabled = Boolean(isGitDisabled) || status === "loading";
|
||||||
|
|
||||||
|
// Show warning only for "branch" mode (requires clean state for checkout)
|
||||||
|
// Worktree mode doesn't require clean state
|
||||||
|
const showDirtyWarning = isolationMode === "branch" && warning;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isBaseDropdownOpen) {
|
if (isBaseDropdownOpen) {
|
||||||
setBranchSearch("");
|
setBranchSearch("");
|
||||||
@@ -677,25 +728,19 @@ export function GitOptionsSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.formSection}>
|
<View style={styles.formSection}>
|
||||||
<Text style={styles.label}>Git Setup</Text>
|
|
||||||
<Text style={styles.helperText}>
|
|
||||||
Choose a base branch, then optionally create a feature branch or
|
|
||||||
isolated worktree.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<DropdownField
|
<DropdownField
|
||||||
label="Base Branch"
|
label="Git"
|
||||||
value={baseBranch}
|
value={baseBranch}
|
||||||
placeholder={currentBranchLabel || "main"}
|
placeholder={currentBranchLabel || "main"}
|
||||||
onPress={onToggleBaseDropdown}
|
onPress={onToggleBaseDropdown}
|
||||||
disabled={gitInputsDisabled}
|
disabled={gitInputsDisabled}
|
||||||
errorMessage={repoError}
|
errorMessage={repoError}
|
||||||
warningMessage={!gitValidationError && !isGitDisabled ? warning : null}
|
warningMessage={!gitValidationError && !isGitDisabled ? (showDirtyWarning ? warning : null) : null}
|
||||||
helperText={
|
helperText={
|
||||||
helperText ??
|
helperText ??
|
||||||
(status === "loading"
|
(status === "loading"
|
||||||
? "Inspecting repository…"
|
? "Inspecting repository…"
|
||||||
: "Search existing branches, then tap to select.")
|
: undefined)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DropdownSheet
|
<DropdownSheet
|
||||||
@@ -756,39 +801,22 @@ export function GitOptionsSection({
|
|||||||
) : null}
|
) : null}
|
||||||
</DropdownSheet>
|
</DropdownSheet>
|
||||||
|
|
||||||
<ToggleRow
|
<View style={styles.formSection}>
|
||||||
label="New Branch"
|
<Text style={styles.label}>Isolation</Text>
|
||||||
description="Create a feature branch before launching the agent"
|
<IsolationControl
|
||||||
value={createNewBranch}
|
value={isolationMode}
|
||||||
onToggle={onToggleCreateNewBranch}
|
onChange={onIsolationModeChange}
|
||||||
disabled={isGitDisabled}
|
disabled={isGitDisabled}
|
||||||
/>
|
|
||||||
{createNewBranch ? (
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
placeholder="feature-branch-name"
|
|
||||||
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
|
||||||
value={branchName}
|
|
||||||
onChangeText={onBranchNameChange}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
</View>
|
||||||
|
|
||||||
<ToggleRow
|
{isolationMode !== "none" ? (
|
||||||
label="Create Worktree"
|
|
||||||
description="Use an isolated directory so your current branch stays untouched"
|
|
||||||
value={createWorktree}
|
|
||||||
onToggle={onToggleCreateWorktree}
|
|
||||||
disabled={isGitDisabled}
|
|
||||||
/>
|
|
||||||
{createWorktree ? (
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder={branchName || "feature-worktree"}
|
placeholder={isolationMode === "worktree" ? "worktree-name" : "feature-branch-name"}
|
||||||
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
||||||
value={worktreeSlug}
|
value={isolationMode === "worktree" ? worktreeSlug : branchName}
|
||||||
onChangeText={onWorktreeSlugChange}
|
onChangeText={isolationMode === "worktree" ? onWorktreeSlugChange : onBranchNameChange}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
@@ -988,4 +1016,32 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
|
segmentedControl: {
|
||||||
|
flexDirection: "row",
|
||||||
|
backgroundColor: theme.colors.muted,
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
padding: theme.spacing[1],
|
||||||
|
},
|
||||||
|
segmentedControlDisabled: {
|
||||||
|
opacity: theme.opacity[50],
|
||||||
|
},
|
||||||
|
segmentedOption: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
borderRadius: theme.borderRadius.md,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
segmentedOptionSelected: {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
},
|
||||||
|
segmentedOptionText: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
},
|
||||||
|
segmentedOptionTextSelected: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -397,3 +397,91 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
|||||||
**FILES CHANGED**:
|
**FILES CHANGED**:
|
||||||
- `packages/app/src/app/agent/[serverId]/[agentId].tsx:419-437`
|
- `packages/app/src/app/agent/[serverId]/[agentId].tsx:419-437`
|
||||||
|
|
||||||
|
- [⏳] **REFACTOR (App)**: Redesign new agent page UI layout per mockup.
|
||||||
|
|
||||||
|
**TARGET LAYOUT**:
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ ← New Agent 🖥 local 🟢 │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ WORKING DIRECTORY │
|
||||||
|
│ ┌───────────────────────────────────────────────┐ │
|
||||||
|
│ │ 📁 voice-dev 〉 │ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ AGENT │
|
||||||
|
│ ┌───────────────────────────────────────────────┐ │
|
||||||
|
│ │ ✦ Claude · opus-4.5 〉 │ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ GIT (only if working dir is a git repo) │
|
||||||
|
│ ┌───────────────────────────────────────────────┐ │
|
||||||
|
│ │ 🌿 main 〉 │ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
│ ⚠️ Uncommitted changes (only if Branch selected) │
|
||||||
|
│ │
|
||||||
|
│ Isolation (only if working dir is a git repo) │
|
||||||
|
│ ┌────────────┬────────────┬────────────┐ │
|
||||||
|
│ │ None │ Branch │ Worktree │ │
|
||||||
|
│ └────────────┴────────────┴────────────┘ │
|
||||||
|
│ [Branch name input - only if Branch/Worktree] │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────────────┤
|
||||||
|
│ ┌───────────────────────────────────────────────┐ │
|
||||||
|
│ │ Message agent... │ │
|
||||||
|
│ └───────────────────────────────────────────────┘ │
|
||||||
|
│ 📎 🎤 ⟨⦿⟩ │
|
||||||
|
│ Bypass Permissions ▼ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**CHANGES REQUIRED**:
|
||||||
|
|
||||||
|
1. **Host in header** (not a form row):
|
||||||
|
- Move server/host selector to header bar, right side
|
||||||
|
- Show as icon + name + status dot (🖥 local 🟢)
|
||||||
|
- Tappable to change host
|
||||||
|
- Remove the separate "Host" form row
|
||||||
|
|
||||||
|
2. **Rename "Model" to "Agent"**:
|
||||||
|
- Single row showing provider + model: `✦ Claude · opus-4.5`
|
||||||
|
- Tapping opens picker with TWO dropdowns: provider first, then model
|
||||||
|
- Provider shown as icon or word prefix
|
||||||
|
|
||||||
|
3. **Git section visibility**:
|
||||||
|
- ONLY show "GIT" row and "Isolation" control if working directory is a git repo
|
||||||
|
- If not a git repo, hide entire git section (no greyed out state, just hidden)
|
||||||
|
|
||||||
|
4. **Isolation as segmented control** (replaces toggle checkboxes):
|
||||||
|
- Three options: None | Branch | Worktree
|
||||||
|
- Default: None (no isolation, use current branch as-is)
|
||||||
|
- **None**: No branch input shown, no git operations
|
||||||
|
- **Branch**: Shows branch name input below. User can select existing branch OR type new name to create
|
||||||
|
- **Worktree**: Shows branch name input below. Creates new worktree. Worktree implies branch creation.
|
||||||
|
|
||||||
|
5. **Branch name input**:
|
||||||
|
- Only visible when isolation is Branch or Worktree
|
||||||
|
- Auto-generated from prompt (slugified) like before
|
||||||
|
- User can edit manually
|
||||||
|
- For Branch: can also select from existing branches dropdown
|
||||||
|
- For Worktree: always new branch name (worktree slug derived from it)
|
||||||
|
|
||||||
|
6. **Uncommitted changes warning**:
|
||||||
|
- ONLY show when isolation is "Branch" (switching branches requires clean state)
|
||||||
|
- Do NOT show for "None" (staying on current branch)
|
||||||
|
- Do NOT show for "Worktree" (creating worktree does NOT require clean state)
|
||||||
|
|
||||||
|
7. **Mode under input area**:
|
||||||
|
- Move mode selector (Bypass Permissions dropdown) BELOW the text input
|
||||||
|
- Check how `[agentId].tsx` shows mode when agent is running - use same pattern
|
||||||
|
- **CRITICAL**: Refactor `AgentInputArea` to be a pure display component
|
||||||
|
- Both new.tsx and [agentId].tsx should use the SAME component to prevent drift
|
||||||
|
|
||||||
|
**REFACTORING REQUIREMENT**:
|
||||||
|
The `AgentInputArea` component should be refactored to accept all UI elements as props/children so both screens use identical rendering. Do NOT duplicate mode dropdown logic. Study `[agentId].tsx` to see how it shows mode during agent runs.
|
||||||
|
|
||||||
|
**FILES TO MODIFY**:
|
||||||
|
- `packages/app/src/app/agent/new.tsx` - Layout changes
|
||||||
|
- `packages/app/src/components/agent-input-area.tsx` - Refactor for reuse
|
||||||
|
- `packages/app/src/components/agent-form/agent-form-dropdowns.tsx` - Segmented control
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user