logancyang_obsidian-copilot/src/agentMode/backends/shared/BinaryPathSetting.tsx
Zero Liu d72e3ee05a
refactor(agent-mode): redesign agent binary configuration UI (#2517)
Replace the per-backend install/settings panels with a shared Configure
dialog layout. Introduces ConfigDialogShell + ConfigSection, InstallCommandRow,
and an installStatus badge/status-line module (with tests), and reworks the
Claude, Codex, and opencode Configure dialogs onto it. The settings page now
shows each backend's icon, an install-status badge, and a Configure button,
dropping the old BinaryInstallContent and SimpleBackendSettingsPanel helpers.
Copy is simplified for non-technical users and dialog spacing/dividers are
made consistent.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 15:15:52 -07:00

115 lines
3.6 KiB
TypeScript

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { logError } from "@/logger";
import { detectBinary } from "@/utils/detectBinary";
import { Notice } from "obsidian";
import React from "react";
interface Props {
binaryName: string;
placeholder: string;
initialPath: string;
/** Optional hint surfaced when auto-detect finds nothing. */
notFoundHint?: string;
/** Validate & persist on Apply. Returns null on success, error message on failure. */
onSave: (path: string) => Promise<string | null>;
/** When true, a successful auto-detect immediately invokes `onSave`. */
persistOnAutoDetect?: boolean;
/**
* Custom detector. Used when the backend has a richer install lookup than
* a generic `which`/`where` PATH search — e.g. Claude knows about
* `~/.local/bin/claude`, Volta, asdf, NVM. Falls back to
* {@link detectBinary} when omitted.
*/
detect?: () => Promise<string | null>;
}
/**
* Shared "binary path" setting row used by every Agent Mode backend that
* spawns a local executable. Owns the Input + Auto-detect + Apply UX with
* busy/error state. Callers parameterize the binary name, placeholder, and
* persistence callback; the surrounding `<SettingItem>` (title/description)
* stays in the backend-specific panel.
*/
export const BinaryPathSetting: React.FC<Props> = ({
binaryName,
placeholder,
initialPath,
notFoundHint,
onSave,
persistOnAutoDetect = false,
detect,
}) => {
const [pathInput, setPathInput] = React.useState(initialPath);
const [error, setError] = React.useState<string | null>(null);
const [busy, setBusy] = React.useState(false);
React.useEffect(() => {
setPathInput(initialPath);
}, [initialPath]);
const apply = React.useCallback(async (): Promise<void> => {
const trimmed = pathInput.trim();
if (!trimmed) {
setError("Path is required.");
return;
}
const err = await onSave(trimmed);
if (err) {
setError(err);
return;
}
setError(null);
}, [pathInput, onSave]);
const autoDetect = React.useCallback(async (): Promise<void> => {
if (busy) return;
setBusy(true);
setError(null);
try {
const found = detect ? await detect() : await detectBinary(binaryName);
if (!found) {
setError(
notFoundHint ??
`${binaryName} not found on PATH. Install it or paste a custom path manually.`
);
return;
}
setPathInput(found);
if (persistOnAutoDetect) {
const err = await onSave(found);
if (err) {
setError(err);
return;
}
}
new Notice(`Found ${binaryName} at ${found}`);
} catch (e) {
logError(`[AgentMode] auto-detect ${binaryName} failed`, e);
setError(e instanceof Error ? e.message : String(e));
} finally {
setBusy(false);
}
}, [binaryName, busy, notFoundHint, onSave, persistOnAutoDetect, detect]);
return (
<div className="tw-flex tw-w-full tw-flex-col tw-gap-2">
<div className="tw-flex tw-items-center tw-gap-2">
<Input
type="text"
placeholder={placeholder}
value={pathInput}
onChange={(e) => setPathInput(e.target.value)}
className="tw-flex-1"
/>
<Button variant="secondary" size="default" onClick={autoDetect} disabled={busy}>
Auto-detect
</Button>
<Button variant="default" size="default" onClick={apply} disabled={busy}>
Apply
</Button>
</div>
{error && <div className="tw-text-sm tw-text-error">{error}</div>}
</div>
);
};