/** * @file RunSetup.tsx * @description The pre-run setup panel: everything the user picks before a run * exists. Moved verbatim out of `pages/Run.tsx` (where it was `ConfigCard`) so * the Run page and the Workspace page can both mount the same panel. * * What lives here: * - `RunSetup` — mode (conversation / headless), fresh-vs-resume source, the * prompt editor, and the cwd / model / permission-mode / effort fields, * plus the concurrency hint and the Start button. Its disabled state is * driven by the `binaryFound` prop, so a missing `claude` binary is a * surfaced state here rather than a probe of its own. * above the panel, with its own localStorage-persisted minimized state. * - the pickers the panel owns: `CwdAutocomplete`, `SessionPicker`, * `ModelPicker`, and the small `ModeOption` / `Field` layout helpers. * * Props only for `RunSetup`: no `/stage` call, no lane API call, and no run * lifecycle — the page owns `api.run.start` and hands the result back through * `onStart`. `SessionPicker` keeps the one API call it already owned (listing * past sessions to resume) and `PromptEditor` keeps its `@`-file lookup. * * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Play, RefreshCw, AlertCircle, ChevronDown, ShieldAlert, X, FolderOpen, Home, History as HistoryIcon, Search, RotateCcw, Lock, } from "lucide-react"; import { api, RUN_MODEL_CHOICES, RUN_EFFORT_CHOICES } from "../../lib/api"; import type { CwdSuggestion, DashboardRunHistoryItem, RunListResponse, EffortLevel, PermissionMode, RunMode, } from "../../lib/api"; import type { Session } from "../../lib/types"; import { Select } from "../Select"; import { PromptEditor } from "./RunConsole"; import type { SlashCommand } from "./RunConsole"; // ── Limitations banner (above the config card) ──────────────────────── interface RunSetupProps { mode: RunMode; onModeChange: (m: RunMode) => void; prompt: string; onPromptChange: (s: string) => void; cwd: string; onCwdChange: (s: string) => void; cwdSuggestions: CwdSuggestion[]; model: string; onModelChange: (s: string) => void; permissionMode: PermissionMode; onPermissionModeChange: (m: PermissionMode) => void; effort: EffortLevel; onEffortChange: (e: EffortLevel) => void; binaryFound: boolean; busy: boolean; onStart: () => void; activeRuns: RunListResponse | null; resumeSession: Session | null; onResumeSessionChange: (s: Session | null) => void; /** The selected lane's cwd - narrows the resume picker to that lane's own * sessions. Undefined when no lane is selected (the picker then lists * everything, same as before lanes existed). */ laneCwd?: string; slashCommands: SlashCommand[]; runHistory: DashboardRunHistoryItem[]; onResumeFromHistory: (item: DashboardRunHistoryItem) => void; } export function RunSetup(props: RunSetupProps) { const { t } = useTranslation("run"); const atCap = props.activeRuns != null && props.activeRuns.activeCount >= props.activeRuns.maxConcurrent; const isResume = !!props.resumeSession; const [resumePicked, setResumePicked] = useState(isResume); // Keep "resume picked" in sync with the parent. Two cases: // 1. Parent set a resume session (e.g. user clicked Resume in the runs // modal) - flip the radio so the picker is shown and the selection // is visible. // 2. Parent cleared the session and mode flipped to headless - clear // the radio so the form is honest. useEffect(() => { if (isResume && !resumePicked) setResumePicked(true); else if (!isResume && resumePicked && props.mode === "headless") setResumePicked(false); }, [isResume, resumePicked, props.mode]); return (
{/* Mode and source on one line. Both are two-way choices made once at spawn time, so a segmented row carries them; the longer explanations live in each button's title rather than in a paragraph. */}
props.onModeChange("conversation")} /> { props.onModeChange("headless"); setResumePicked(false); }} />
{props.mode === "conversation" && ( <>
{ setResumePicked(false); props.onResumeSessionChange(null); }} /> setResumePicked(true)} />
{resumePicked && (
)} )}
{/* Prompt */}
{t("hint.shortcut")} · / for slash commands · @ for file references
{/* Advanced fields */}
{isResume && props.resumeSession ? (
{props.resumeSession.cwd}
) : (
)}
value={props.permissionMode} onChange={props.onPermissionModeChange} options={[ { value: "acceptEdits", label: t("fields.permissionAcceptEdits") }, { value: "default", label: t("fields.permissionDefault") }, { value: "plan", label: t("fields.permissionPlan") }, { value: "bypassPermissions", label: t("fields.permissionBypass") }, ]} /> value={props.effort} onChange={props.onEffortChange} options={RUN_EFFORT_CHOICES.map((c) => ({ value: c.id, label: c.label, hint: c.hint, }))} />
{props.permissionMode === "bypassPermissions" && (
{t("hint.permissionWarning")}
)} {/* Footer: contextual run-state hint + run button */}
{atCap ? ( {t("concurrency.atCap", { max: props.activeRuns?.maxConcurrent ?? 0 })} ) : props.activeRuns && props.activeRuns.activeCount > 0 ? ( {t("concurrency.active", { count: props.activeRuns.activeCount })} ) : null}
); } /** One segment of a two-way inline choice. The explanation rides on `title` * instead of a hint line, which is what keeps the row to one line. */ function Seg({ active, label, title, onClick, }: { active: boolean; label: string; title?: string; onClick: () => void; }) { return ( ); } function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{children}
); } // ── CWD autocomplete ────────────────────────────────────────────────── export function CwdAutocomplete({ value, onChange, suggestions, inputId, }: { value: string; onChange: (s: string) => void; suggestions: CwdSuggestion[]; /** Sets the input's `id` so an external `