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:
Mohamed Boudra
2025-12-25 21:40:35 +07:00
co-authored by Claude Opus 4.5
parent 4cf0ea73a1
commit 26da925de6
3 changed files with 387 additions and 142 deletions
+198 -97
View File
@@ -3,20 +3,18 @@ import {
View,
Text,
Pressable,
ScrollView,
useWindowDimensions,
} from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
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 { AgentList } from "@/components/agent-list";
import { AgentInputArea } from "@/components/agent-input-area";
import {
AssistantDropdown,
DropdownSheet,
GitOptionsSection,
ModelDropdown,
PermissionsDropdown,
WorkingDirectoryDropdown,
} from "@/components/agent-form/agent-form-dropdowns";
import { useDaemonRequest } from "@/hooks/use-daemon-request";
@@ -120,20 +118,23 @@ export default function DraftAgentScreen() {
: undefined;
const [openDropdown, setOpenDropdown] = useState<
"host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch" | null
"host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch" | "agent" | null
>(null);
const [errorMessage, setErrorMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
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 [createWorktree, setCreateWorktree] = useState(false);
const [worktreeSlug, setWorktreeSlug] = useState("");
const [branchNameEdited, setBranchNameEdited] = useState(false);
const [worktreeSlugEdited, setWorktreeSlugEdited] = useState(false);
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(
(key: "host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch") => {
(key: "host" | "provider" | "mode" | "model" | "workingDir" | "baseBranch" | "agent") => {
setOpenDropdown(key);
},
[]
@@ -393,14 +394,12 @@ export default function DraftAgentScreen() {
return;
}
if (
createNewBranch ||
createWorktree ||
isolationMode !== "none" ||
baseBranch.trim().length > 0 ||
branchName.trim().length > 0 ||
worktreeSlug.trim().length > 0
) {
setCreateNewBranch(false);
setCreateWorktree(false);
setIsolationMode("none");
setBaseBranch("");
setBranchName("");
setWorktreeSlug("");
@@ -411,8 +410,7 @@ export default function DraftAgentScreen() {
}, [
baseBranch,
branchName,
createNewBranch,
createWorktree,
isolationMode,
isNonGitDirectory,
worktreeSlug,
]);
@@ -657,62 +655,28 @@ export default function DraftAgentScreen() {
)}
<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}>
{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
workingDir={workingDir}
errorMessage=""
@@ -726,7 +690,13 @@ export default function DraftAgentScreen() {
wrapInContainer={false}
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
baseBranch={baseBranch}
onBaseBranchChange={handleBaseBranchChange}
@@ -736,43 +706,37 @@ export default function DraftAgentScreen() {
helperText={gitHelperText}
isGitDisabled={Boolean(gitHelperText)}
warning={
!createWorktree && repoInfo?.isDirty
repoInfo?.isDirty
? "Working directory has uncommitted changes"
: null
}
createNewBranch={createNewBranch}
onToggleCreateNewBranch={(next) => {
setCreateNewBranch(next);
if (next) {
isolationMode={isolationMode}
onIsolationModeChange={(mode) => {
setIsolationMode(mode);
if (mode === "none") {
setBranchName("");
setWorktreeSlug("");
setBranchNameEdited(false);
setWorktreeSlugEdited(false);
} else if (mode === "branch") {
if (!branchNameEdited) {
const slug = slugifyWorktreeName(baseBranch || "");
setBranchName(slug);
}
} else {
setBranchName("");
setBranchNameEdited(false);
}
}}
branchName={branchName}
onBranchNameChange={(value) => {
setBranchName(slugifyWorktreeName(value));
setBranchNameEdited(true);
}}
createWorktree={createWorktree}
onToggleCreateWorktree={(next) => {
setCreateWorktree(next);
if (next) {
} else if (mode === "worktree") {
if (!worktreeSlugEdited) {
const slug = slugifyWorktreeName(
branchName || baseBranch || ""
);
setWorktreeSlug(slug);
}
} else {
setWorktreeSlug("");
setWorktreeSlugEdited(false);
}
}}
branchName={branchName}
onBranchNameChange={(value) => {
setBranchName(slugifyWorktreeName(value));
setBranchNameEdited(true);
}}
worktreeSlug={worktreeSlug}
onWorktreeSlugChange={(value) => {
setWorktreeSlug(slugifyWorktreeName(value));
@@ -824,13 +788,118 @@ export default function DraftAgentScreen() {
)}
</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 ? (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{errorMessage}</Text>
</View>
) : null}
<View style={styles.streamContainer} />
</View>
</ScrollView>
<AgentInputArea
agentId={DRAFT_AGENT_ID}
serverId={selectedServerId ?? ""}
@@ -881,10 +950,13 @@ const styles = StyleSheet.create((theme) => ({
contentContainer: {
flex: 1,
},
configScrollContent: {
flexGrow: 1,
},
configSection: {
paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[3],
paddingBottom: theme.spacing[2],
paddingBottom: theme.spacing[4],
gap: theme.spacing[2],
},
configRow: {
@@ -949,9 +1021,6 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
marginTop: theme.spacing[1],
},
streamContainer: {
flex: 1,
},
errorContainer: {
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
@@ -964,4 +1033,36 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.destructiveForeground,
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 {
baseBranch: string;
onBaseBranchChange: (value: string) => void;
@@ -617,12 +668,10 @@ export interface GitOptionsSectionProps {
repoError: string | null;
helperText?: string | null;
warning: string | null;
createNewBranch: boolean;
onToggleCreateNewBranch: (value: boolean) => void;
isolationMode: IsolationMode;
onIsolationModeChange: (mode: IsolationMode) => void;
branchName: string;
onBranchNameChange: (value: string) => void;
createWorktree: boolean;
onToggleCreateWorktree: (value: boolean) => void;
worktreeSlug: string;
onWorktreeSlugChange: (value: string) => void;
gitValidationError: string | null;
@@ -640,12 +689,10 @@ export function GitOptionsSection({
repoError,
helperText,
warning,
createNewBranch,
onToggleCreateNewBranch,
isolationMode,
onIsolationModeChange,
branchName,
onBranchNameChange,
createWorktree,
onToggleCreateWorktree,
worktreeSlug,
onWorktreeSlugChange,
gitValidationError,
@@ -668,6 +715,10 @@ export function GitOptionsSection({
const baseInputRef = useRef<TextInput | null>(null);
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(() => {
if (isBaseDropdownOpen) {
setBranchSearch("");
@@ -677,25 +728,19 @@ export function GitOptionsSection({
return (
<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
label="Base Branch"
label="Git"
value={baseBranch}
placeholder={currentBranchLabel || "main"}
onPress={onToggleBaseDropdown}
disabled={gitInputsDisabled}
errorMessage={repoError}
warningMessage={!gitValidationError && !isGitDisabled ? warning : null}
warningMessage={!gitValidationError && !isGitDisabled ? (showDirtyWarning ? warning : null) : null}
helperText={
helperText ??
(status === "loading"
? "Inspecting repository…"
: "Search existing branches, then tap to select.")
: undefined)
}
/>
<DropdownSheet
@@ -756,39 +801,22 @@ export function GitOptionsSection({
) : null}
</DropdownSheet>
<ToggleRow
label="New Branch"
description="Create a feature branch before launching the agent"
value={createNewBranch}
onToggle={onToggleCreateNewBranch}
disabled={isGitDisabled}
/>
{createNewBranch ? (
<TextInput
style={styles.input}
placeholder="feature-branch-name"
placeholderTextColor={defaultTheme.colors.mutedForeground}
value={branchName}
onChangeText={onBranchNameChange}
autoCapitalize="none"
autoCorrect={false}
<View style={styles.formSection}>
<Text style={styles.label}>Isolation</Text>
<IsolationControl
value={isolationMode}
onChange={onIsolationModeChange}
disabled={isGitDisabled}
/>
) : null}
</View>
<ToggleRow
label="Create Worktree"
description="Use an isolated directory so your current branch stays untouched"
value={createWorktree}
onToggle={onToggleCreateWorktree}
disabled={isGitDisabled}
/>
{createWorktree ? (
{isolationMode !== "none" ? (
<TextInput
style={styles.input}
placeholder={branchName || "feature-worktree"}
placeholder={isolationMode === "worktree" ? "worktree-name" : "feature-branch-name"}
placeholderTextColor={defaultTheme.colors.mutedForeground}
value={worktreeSlug}
onChangeText={onWorktreeSlugChange}
value={isolationMode === "worktree" ? worktreeSlug : branchName}
onChangeText={isolationMode === "worktree" ? onWorktreeSlugChange : onBranchNameChange}
autoCapitalize="none"
autoCorrect={false}
/>
@@ -988,4 +1016,32 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
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,
},
}));
+88
View File
@@ -397,3 +397,91 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
**FILES CHANGED**:
- `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