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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
143
ui/src/components/PathInstructionsModal.tsx
Normal file
143
ui/src/components/PathInstructionsModal.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user