From 26da925de69d8ef409416fd88d14dbfc825e3ac9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Dec 2025 21:40:35 +0700 Subject: [PATCH] Redesign new agent page UI layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- packages/app/src/app/agent/new.tsx | 295 ++++++++++++------ .../agent-form/agent-form-dropdowns.tsx | 146 ++++++--- plan.md | 88 ++++++ 3 files changed, 387 insertions(+), 142 deletions(-) diff --git a/packages/app/src/app/agent/new.tsx b/packages/app/src/app/agent/new.tsx index dc26827b2..95402712d 100644 --- a/packages/app/src/app/agent/new.tsx +++ b/packages/app/src/app/agent/new.tsx @@ -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() { )} - + openDropdownSheet("host")} + > + + {hostLabel} + + + } + /> - + - {renderConfigRow({ - label: "Host", - value: hostLabel, - meta: hostStatus, - onPress: () => openDropdownSheet("host"), - })} - openDropdownSheet("provider")} - onClose={closeDropdown} - onSelect={setProviderFromUser} - label="Provider" - placeholder="Select provider" - sheetTitle="Choose Provider" - wrapInContainer={false} - renderTrigger={renderDropdownTrigger} - /> - openDropdownSheet("mode")} - onClose={closeDropdown} - onSelect={setModeFromUser} - label="Mode" - placeholder="Select mode" - sheetTitle="Mode" - wrapInContainer={false} - renderTrigger={renderDropdownTrigger} - /> - { - refreshProviderModels(); - openDropdownSheet("model"); - }} - onClose={closeDropdown} - onSelect={setModelFromUser} - onClear={() => setModelFromUser("")} - onRefresh={refreshProviderModels} - label="Model" - 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 ? ( { - 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() { )} + {/* Agent dropdown sheet - provider + model + mode */} + + + Provider + + {providerDefinitions.map((definition) => { + const isSelected = definition.id === selectedProvider; + return ( + { + setProviderFromUser(definition.id); + }} + > + + {definition.label} + + {definition.description ? ( + + {definition.description} + + ) : null} + + ); + })} + + + + + Model + + setModelFromUser("")} + > + + Automatic (provider default) + + + {availableModels.map((model) => { + const isSelected = model.id === selectedModel; + return ( + setModelFromUser(model.id)} + > + + {model.label} + + {model.description ? ( + + {model.description} + + ) : null} + + ); + })} + + + + {modeOptions.length > 0 ? ( + + Mode + + {modeOptions.map((mode) => { + const isSelected = mode.id === selectedMode; + return ( + setModeFromUser(mode.id)} + > + + {mode.label} + + {mode.description ? ( + + {mode.description} + + ) : null} + + ); + })} + + + ) : null} + + {errorMessage ? ( {errorMessage} ) : null} - - + ({ 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], + }, })); diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index 7a2386aee..1cdcbeefa 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -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 ( + + {options.map((option) => { + const isSelected = option.id === value; + return ( + { + if (!disabled) { + onChange(option.id); + } + }} + style={[ + styles.segmentedOption, + isSelected && styles.segmentedOptionSelected, + ]} + > + + {option.label} + + + ); + })} + + ); +} + 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(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 ( - Git Setup - - Choose a base branch, then optionally create a feature branch or - isolated worktree. - - - - {createNewBranch ? ( - + Isolation + - ) : null} + - - {createWorktree ? ( + {isolationMode !== "none" ? ( @@ -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, + }, })); diff --git a/plan.md b/plan.md index d06d94808..b41c81f9a 100644 --- a/plan.md +++ b/plan.md @@ -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 +