feat(run): tool-aware model picker and run panel labels for codex lanes
Model dropdown now shows curated Claude vs Codex models per lane's tool, and the run panel title/subtitle/binary-missing text switch between "Chạy Claude"/"Chạy Codex" accordingly.
This commit is contained in:
@@ -88,6 +88,7 @@ export function LaneConsolePane({
|
||||
|
||||
const currentLane = laneId !== null ? lanes.find((l) => l.id === laneId) : null;
|
||||
const laneTool = currentLane?.tool ?? "claude";
|
||||
const toolLabel = laneTool === "codex" ? "Codex" : "Claude";
|
||||
|
||||
useEffect(() => {
|
||||
onHasActiveRunChange?.(handle !== null);
|
||||
@@ -419,7 +420,9 @@ export function LaneConsolePane({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">
|
||||
{t("title", { tool: toolLabel })}
|
||||
</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
@@ -432,7 +435,7 @@ export function LaneConsolePane({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-fg-muted max-w-3xl">{t("subtitle")}</p>
|
||||
<p className="text-xs text-fg-muted max-w-3xl">{t("subtitle", { tool: toolLabel })}</p>
|
||||
</div>
|
||||
<ActiveRunsSwitcher
|
||||
activeRuns={activeRuns}
|
||||
@@ -449,7 +452,7 @@ export function LaneConsolePane({
|
||||
{binaryStatus && !binaryStatus[laneTool]?.found && (
|
||||
<div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t("binary.missing")}</span>
|
||||
<span>{t("binary.missing", { cli: laneTool, tool: toolLabel })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
RotateCcw,
|
||||
Lock,
|
||||
} from "lucide-react";
|
||||
import { api, RUN_MODEL_CHOICES, RUN_EFFORT_CHOICES } from "../../lib/api";
|
||||
import { api, RUN_MODEL_CHOICES, RUN_MODEL_CHOICES_CODEX, RUN_EFFORT_CHOICES } from "../../lib/api";
|
||||
import type {
|
||||
CwdSuggestion,
|
||||
DashboardRunHistoryItem,
|
||||
@@ -233,7 +233,7 @@ export function RunSetup(props: RunSetupProps) {
|
||||
)}
|
||||
</Field>
|
||||
<Field label={t("fields.model")}>
|
||||
<ModelPicker value={props.model} onChange={props.onModelChange} />
|
||||
<ModelPicker value={props.model} onChange={props.onModelChange} tool={props.tool} />
|
||||
</Field>
|
||||
{props.tool === "codex" ? (
|
||||
<>
|
||||
@@ -712,10 +712,19 @@ function SessionPicker({
|
||||
// the "inherit from settings" choice, so use a non-empty marker.
|
||||
const MODEL_CUSTOM = "__custom__";
|
||||
|
||||
function ModelPicker({ value, onChange }: { value: string; onChange: (s: string) => void }) {
|
||||
function ModelPicker({
|
||||
value,
|
||||
onChange,
|
||||
tool,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (s: string) => void;
|
||||
tool: LaneTool;
|
||||
}) {
|
||||
const { t } = useTranslation("run");
|
||||
const choices = tool === "codex" ? RUN_MODEL_CHOICES_CODEX : RUN_MODEL_CHOICES;
|
||||
// "Custom" is selected when the value isn't one of our curated IDs.
|
||||
const knownIds = useMemo(() => RUN_MODEL_CHOICES.map((c) => c.id), []);
|
||||
const knownIds = useMemo(() => choices.map((c) => c.id), [choices]);
|
||||
const isCustom = value !== "" && !knownIds.includes(value);
|
||||
const [showCustom, setShowCustom] = useState(isCustom);
|
||||
|
||||
@@ -724,14 +733,14 @@ function ModelPicker({ value, onChange }: { value: string; onChange: (s: string)
|
||||
// a browser-native <select>.
|
||||
const options = useMemo(
|
||||
() => [
|
||||
...RUN_MODEL_CHOICES.map((c) => ({
|
||||
...choices.map((c) => ({
|
||||
value: c.id === "" ? "" : c.id,
|
||||
label: c.id === "" ? t("fields.modelInheritLabel") : c.label,
|
||||
hint: c.hint,
|
||||
})),
|
||||
{ value: MODEL_CUSTOM, label: t("fields.modelCustom") },
|
||||
],
|
||||
[t]
|
||||
[t, choices]
|
||||
);
|
||||
|
||||
const onSelect = (v: string) => {
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("../../../lib/api", () => ({
|
||||
},
|
||||
},
|
||||
RUN_MODEL_CHOICES: [],
|
||||
RUN_MODEL_CHOICES_CODEX: [],
|
||||
RUN_EFFORT_CHOICES: [],
|
||||
}));
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ describe("RunSetup — selections report through callbacks", () => {
|
||||
|
||||
it("reports the model from the picker, including a custom id", () => {
|
||||
const { spies } = renderSetup();
|
||||
pickFromSelect(i18n.t("run:fields.modelInheritLabel"), "Sonnet 4.6");
|
||||
expect(spies.onModelChange).toHaveBeenCalledWith("sonnet");
|
||||
pickFromSelect(i18n.t("run:fields.modelInheritLabel"), "Sonnet 5");
|
||||
expect(spies.onModelChange).toHaveBeenCalledWith("claude-sonnet-5");
|
||||
// "Custom…" is a sentinel, not a model id — it must not be reported as one;
|
||||
// the free-text box it reveals is what reports.
|
||||
fireEvent.click(screen.getByText(i18n.t("run:fields.modelInheritLabel")));
|
||||
|
||||
Reference in New Issue
Block a user