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 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-02 16:08:59 -06:00
parent e9cc1fb8d9
commit 7aab032578
4 changed files with 191 additions and 79 deletions

View File

@@ -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 (
<Field label="Agent instructions file" hint={instructionsFileHint}>
<DraftInput
value={
isCreate
? values!.instructionsFilePath ?? ""
: eff(
"adapterConfig",
"instructionsFilePath",
String(config.instructionsFilePath ?? ""),
)
}
onCommit={(v) =>
isCreate
? set!({ instructionsFilePath: v })
: mark("adapterConfig", "instructionsFilePath", v || undefined)
}
immediate
className={inputClass}
placeholder="/absolute/path/to/AGENTS.md"
/>
<div className="flex items-center gap-2">
<DraftInput
value={
isCreate
? values!.instructionsFilePath ?? ""
: eff(
"adapterConfig",
"instructionsFilePath",
String(config.instructionsFilePath ?? ""),
)
}
onCommit={(v) =>
isCreate
? set!({ instructionsFilePath: v })
: mark("adapterConfig", "instructionsFilePath", v || undefined)
}
immediate
className={inputClass}
placeholder="/absolute/path/to/AGENTS.md"
/>
<ChoosePathButton />
</div>
</Field>
);
}

View File

@@ -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 (
<>
<Field label="Agent instructions file" hint={instructionsFileHint}>
<DraftInput
value={
isCreate
? values!.instructionsFilePath ?? ""
: eff(
"adapterConfig",
"instructionsFilePath",
String(config.instructionsFilePath ?? ""),
)
}
onCommit={(v) =>
isCreate
? set!({ instructionsFilePath: v })
: mark("adapterConfig", "instructionsFilePath", v || undefined)
}
immediate
className={inputClass}
placeholder="/absolute/path/to/AGENTS.md"
/>
<div className="flex items-center gap-2">
<DraftInput
value={
isCreate
? values!.instructionsFilePath ?? ""
: eff(
"adapterConfig",
"instructionsFilePath",
String(config.instructionsFilePath ?? ""),
)
}
onCommit={(v) =>
isCreate
? set!({ instructionsFilePath: v })
: mark("adapterConfig", "instructionsFilePath", v || undefined)
}
immediate
className={inputClass}
placeholder="/absolute/path/to/AGENTS.md"
/>
<ChoosePathButton />
</div>
</Field>
<ToggleField
label="Bypass sandbox"

View File

@@ -35,6 +35,7 @@ import { defaultCreateValues } from "./agent-config-defaults";
import { getUIAdapter } from "../adapters";
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
import { MarkdownEditor } from "./MarkdownEditor";
import { ChoosePathButton } from "./PathInstructionsModal";
/* ---- Create mode values ---- */
@@ -130,12 +131,6 @@ const claudeThinkingEffortOptions = [
] as const;
function extractPickedDirectoryPath(handle: unknown): string | null {
if (typeof handle !== "object" || handle === null) return null;
const maybePath = (handle as { path?: unknown }).path;
return typeof maybePath === "string" && maybePath.length > 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<string | null>(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"
/>
<button
type="button"
className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0"
onClick={async () => {
try {
setCwdPickerNotice(null);
// @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet
const handle = await window.showDirectoryPicker({ mode: "read" });
const absolutePath = extractPickedDirectoryPath(handle);
if (absolutePath) {
if (isCreate) set!({ cwd: absolutePath });
else mark("adapterConfig", "cwd", absolutePath);
return;
}
const selectedName =
typeof handle === "object" &&
handle !== null &&
typeof (handle as { name?: unknown }).name === "string"
? String((handle as { name: string }).name)
: "selected folder";
setCwdPickerNotice(
`Directory picker only exposed "${selectedName}". Paste the absolute path manually.`,
);
} catch {
// user cancelled or API unsupported
}
}}
>
Choose
</button>
<ChoosePathButton />
</div>
{cwdPickerNotice && (
<p className="mt-1 text-xs text-amber-400">{cwdPickerNotice}</p>
)}
</Field>
)}

View File

@@ -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<Platform, { steps: string[]; tip?: string }> = {
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<Platform>(detectPlatform);
const current = instructions[platform];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-base">How to get a full path</DialogTitle>
<DialogDescription>
Paste the absolute path (e.g.{" "}
<code className="text-xs bg-muted px-1 py-0.5 rounded">/Users/you/project</code>
) into the input field.
</DialogDescription>
</DialogHeader>
{/* Platform tabs */}
<div className="flex gap-1 rounded-md border border-border p-0.5">
{platforms.map((p) => (
<button
key={p.id}
type="button"
className={cn(
"flex flex-1 items-center justify-center gap-1.5 rounded px-2 py-1 text-xs transition-colors",
platform === p.id
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
)}
onClick={() => setPlatform(p.id)}
>
<p.icon className="h-3.5 w-3.5" />
{p.label}
</button>
))}
</div>
{/* Steps */}
<ol className="space-y-2 text-sm">
{current.steps.map((step, i) => (
<li key={i} className="flex gap-2">
<span className="text-muted-foreground font-mono text-xs mt-0.5 shrink-0">
{i + 1}.
</span>
<span>{step}</span>
</li>
))}
</ol>
{current.tip && (
<p className="text-xs text-muted-foreground border-l-2 border-border pl-3">
{current.tip}
</p>
)}
</DialogContent>
</Dialog>
);
}
/**
* 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 (
<>
<button
type="button"
className={cn(
"inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0",
className,
)}
onClick={() => setOpen(true)}
>
Choose
</button>
<PathInstructionsModal open={open} onOpenChange={setOpen} />
</>
);
}