From 7aab032578c451da95889072412950c7404c9c95 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 2 Mar 2026 16:08:59 -0600 Subject: [PATCH] refactor(ui): extract ChoosePathButton into reusable PathInstructionsModal Move the directory picker button and instructions modal out of AgentConfigForm into its own component, reused by claude-local and codex-local config fields. Co-Authored-By: Claude Opus 4.6 --- .../adapters/claude-local/config-fields.tsx | 42 ++--- ui/src/adapters/codex-local/config-fields.tsx | 42 ++--- ui/src/components/AgentConfigForm.tsx | 43 +----- ui/src/components/PathInstructionsModal.tsx | 143 ++++++++++++++++++ 4 files changed, 191 insertions(+), 79 deletions(-) create mode 100644 ui/src/components/PathInstructionsModal.tsx diff --git a/ui/src/adapters/claude-local/config-fields.tsx b/ui/src/adapters/claude-local/config-fields.tsx index b57024b6..5dd4f5ab 100644 --- a/ui/src/adapters/claude-local/config-fields.tsx +++ b/ui/src/adapters/claude-local/config-fields.tsx @@ -6,6 +6,7 @@ import { DraftNumberInput, help, } from "../../components/agent-config-primitives"; +import { ChoosePathButton } from "../../components/PathInstructionsModal"; const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; @@ -23,25 +24,28 @@ export function ClaudeLocalConfigFields({ }: AdapterConfigFieldsProps) { return ( - - isCreate - ? set!({ instructionsFilePath: v }) - : mark("adapterConfig", "instructionsFilePath", v || undefined) - } - immediate - className={inputClass} - placeholder="/absolute/path/to/AGENTS.md" - /> +
+ + isCreate + ? set!({ instructionsFilePath: v }) + : mark("adapterConfig", "instructionsFilePath", v || undefined) + } + immediate + className={inputClass} + placeholder="/absolute/path/to/AGENTS.md" + /> + +
); } diff --git a/ui/src/adapters/codex-local/config-fields.tsx b/ui/src/adapters/codex-local/config-fields.tsx index e487a7bc..86baff6c 100644 --- a/ui/src/adapters/codex-local/config-fields.tsx +++ b/ui/src/adapters/codex-local/config-fields.tsx @@ -5,6 +5,7 @@ import { DraftInput, help, } from "../../components/agent-config-primitives"; +import { ChoosePathButton } from "../../components/PathInstructionsModal"; const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; @@ -25,25 +26,28 @@ export function CodexLocalConfigFields({ return ( <> - - isCreate - ? set!({ instructionsFilePath: v }) - : mark("adapterConfig", "instructionsFilePath", v || undefined) - } - immediate - className={inputClass} - placeholder="/absolute/path/to/AGENTS.md" - /> +
+ + isCreate + ? set!({ instructionsFilePath: v }) + : mark("adapterConfig", "instructionsFilePath", v || undefined) + } + immediate + className={inputClass} + placeholder="/absolute/path/to/AGENTS.md" + /> + +
0 ? maybePath : null; -} - /* ---- Form ---- */ export function AgentConfigForm(props: AgentConfigFormProps) { @@ -277,8 +272,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) { // Section toggle state — advanced always starts collapsed const [runPolicyAdvancedOpen, setRunPolicyAdvancedOpen] = useState(false); - const [cwdPickerNotice, setCwdPickerNotice] = useState(null); - // Popover states const [modelOpen, setModelOpen] = useState(false); const [thinkingEffortOpen, setThinkingEffortOpen] = useState(false); @@ -487,40 +480,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) { className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40" placeholder="/path/to/project" /> - + - {cwdPickerNotice && ( -

{cwdPickerNotice}

- )} )} diff --git a/ui/src/components/PathInstructionsModal.tsx b/ui/src/components/PathInstructionsModal.tsx new file mode 100644 index 00000000..b910dead --- /dev/null +++ b/ui/src/components/PathInstructionsModal.tsx @@ -0,0 +1,143 @@ +import { useState } from "react"; +import { Apple, Monitor, Terminal } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; + +type Platform = "mac" | "windows" | "linux"; + +const platforms: { id: Platform; label: string; icon: typeof Apple }[] = [ + { id: "mac", label: "macOS", icon: Apple }, + { id: "windows", label: "Windows", icon: Monitor }, + { id: "linux", label: "Linux", icon: Terminal }, +]; + +const instructions: Record = { + mac: { + steps: [ + "Open Finder and navigate to the folder.", + "Right-click (or Control-click) the folder.", + "Hold the Option (⌥) key — \"Copy\" changes to \"Copy as Pathname\".", + "Click \"Copy as Pathname\", then paste here.", + ], + tip: "You can also open Terminal, type cd, drag the folder into the terminal window, and press Enter. Then type pwd to see the full path.", + }, + windows: { + steps: [ + "Open File Explorer and navigate to the folder.", + "Click in the address bar at the top — the full path will appear.", + "Copy the path, then paste here.", + ], + tip: "Alternatively, hold Shift and right-click the folder, then select \"Copy as path\".", + }, + linux: { + steps: [ + "Open a terminal and navigate to the directory with cd.", + "Run pwd to print the full path.", + "Copy the output and paste here.", + ], + tip: "In most file managers, Ctrl+L reveals the full path in the address bar.", + }, +}; + +function detectPlatform(): Platform { + const ua = navigator.userAgent.toLowerCase(); + if (ua.includes("mac")) return "mac"; + if (ua.includes("win")) return "windows"; + return "linux"; +} + +interface PathInstructionsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function PathInstructionsModal({ + open, + onOpenChange, +}: PathInstructionsModalProps) { + const [platform, setPlatform] = useState(detectPlatform); + + const current = instructions[platform]; + + return ( + + + + How to get a full path + + Paste the absolute path (e.g.{" "} + /Users/you/project + ) into the input field. + + + + {/* Platform tabs */} +
+ {platforms.map((p) => ( + + ))} +
+ + {/* Steps */} +
    + {current.steps.map((step, i) => ( +
  1. + + {i + 1}. + + {step} +
  2. + ))} +
+ + {current.tip && ( +

+ {current.tip} +

+ )} +
+
+ ); +} + +/** + * Small "Choose" button that opens the PathInstructionsModal. + * Drop-in replacement for the old showDirectoryPicker buttons. + */ +export function ChoosePathButton({ className }: { className?: string }) { + const [open, setOpen] = useState(false); + return ( + <> + + + + ); +}