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 currentLane = laneId !== null ? lanes.find((l) => l.id === laneId) : null;
|
||||||
const laneTool = currentLane?.tool ?? "claude";
|
const laneTool = currentLane?.tool ?? "claude";
|
||||||
|
const toolLabel = laneTool === "codex" ? "Codex" : "Claude";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onHasActiveRunChange?.(handle !== null);
|
onHasActiveRunChange?.(handle !== null);
|
||||||
@@ -419,7 +420,9 @@ export function LaneConsolePane({
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<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 ? (
|
{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="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" />
|
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||||
@@ -432,7 +435,7 @@ export function LaneConsolePane({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<ActiveRunsSwitcher
|
<ActiveRunsSwitcher
|
||||||
activeRuns={activeRuns}
|
activeRuns={activeRuns}
|
||||||
@@ -449,7 +452,7 @@ export function LaneConsolePane({
|
|||||||
{binaryStatus && !binaryStatus[laneTool]?.found && (
|
{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">
|
<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" />
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
<span>{t("binary.missing")}</span>
|
<span>{t("binary.missing", { cli: laneTool, tool: toolLabel })}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
RotateCcw,
|
RotateCcw,
|
||||||
Lock,
|
Lock,
|
||||||
} from "lucide-react";
|
} 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 {
|
import type {
|
||||||
CwdSuggestion,
|
CwdSuggestion,
|
||||||
DashboardRunHistoryItem,
|
DashboardRunHistoryItem,
|
||||||
@@ -233,7 +233,7 @@ export function RunSetup(props: RunSetupProps) {
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t("fields.model")}>
|
<Field label={t("fields.model")}>
|
||||||
<ModelPicker value={props.model} onChange={props.onModelChange} />
|
<ModelPicker value={props.model} onChange={props.onModelChange} tool={props.tool} />
|
||||||
</Field>
|
</Field>
|
||||||
{props.tool === "codex" ? (
|
{props.tool === "codex" ? (
|
||||||
<>
|
<>
|
||||||
@@ -712,10 +712,19 @@ function SessionPicker({
|
|||||||
// the "inherit from settings" choice, so use a non-empty marker.
|
// the "inherit from settings" choice, so use a non-empty marker.
|
||||||
const MODEL_CUSTOM = "__custom__";
|
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 { 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.
|
// "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 isCustom = value !== "" && !knownIds.includes(value);
|
||||||
const [showCustom, setShowCustom] = useState(isCustom);
|
const [showCustom, setShowCustom] = useState(isCustom);
|
||||||
|
|
||||||
@@ -724,14 +733,14 @@ function ModelPicker({ value, onChange }: { value: string; onChange: (s: string)
|
|||||||
// a browser-native <select>.
|
// a browser-native <select>.
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...RUN_MODEL_CHOICES.map((c) => ({
|
...choices.map((c) => ({
|
||||||
value: c.id === "" ? "" : c.id,
|
value: c.id === "" ? "" : c.id,
|
||||||
label: c.id === "" ? t("fields.modelInheritLabel") : c.label,
|
label: c.id === "" ? t("fields.modelInheritLabel") : c.label,
|
||||||
hint: c.hint,
|
hint: c.hint,
|
||||||
})),
|
})),
|
||||||
{ value: MODEL_CUSTOM, label: t("fields.modelCustom") },
|
{ value: MODEL_CUSTOM, label: t("fields.modelCustom") },
|
||||||
],
|
],
|
||||||
[t]
|
[t, choices]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelect = (v: string) => {
|
const onSelect = (v: string) => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ vi.mock("../../../lib/api", () => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
RUN_MODEL_CHOICES: [],
|
RUN_MODEL_CHOICES: [],
|
||||||
|
RUN_MODEL_CHOICES_CODEX: [],
|
||||||
RUN_EFFORT_CHOICES: [],
|
RUN_EFFORT_CHOICES: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ describe("RunSetup — selections report through callbacks", () => {
|
|||||||
|
|
||||||
it("reports the model from the picker, including a custom id", () => {
|
it("reports the model from the picker, including a custom id", () => {
|
||||||
const { spies } = renderSetup();
|
const { spies } = renderSetup();
|
||||||
pickFromSelect(i18n.t("run:fields.modelInheritLabel"), "Sonnet 4.6");
|
pickFromSelect(i18n.t("run:fields.modelInheritLabel"), "Sonnet 5");
|
||||||
expect(spies.onModelChange).toHaveBeenCalledWith("sonnet");
|
expect(spies.onModelChange).toHaveBeenCalledWith("claude-sonnet-5");
|
||||||
// "Custom…" is a sentinel, not a model id — it must not be reported as one;
|
// "Custom…" is a sentinel, not a model id — it must not be reported as one;
|
||||||
// the free-text box it reveals is what reports.
|
// the free-text box it reveals is what reports.
|
||||||
fireEvent.click(screen.getByText(i18n.t("run:fields.modelInheritLabel")));
|
fireEvent.click(screen.getByText(i18n.t("run:fields.modelInheritLabel")));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
"binary": {
|
"binary": {
|
||||||
"found": "Using {{path}}",
|
"found": "Using {{path}}",
|
||||||
"missing": "The `claude` CLI isn't on your PATH. Install Claude Code or set PATH so the dashboard can spawn it."
|
"missing": "The `{{cli}}` CLI isn't on your PATH. Install {{tool}} or set PATH so the dashboard can spawn it."
|
||||||
},
|
},
|
||||||
"concurrency": {
|
"concurrency": {
|
||||||
"active_one": "{{count}} run already in flight",
|
"active_one": "{{count}} run already in flight",
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"effort": "Thinking effort",
|
"effort": "Thinking effort",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"modelCustom": "Custom…",
|
"modelCustom": "Custom…",
|
||||||
"modelCustomPlaceholder": "model id (e.g. claude-opus-4-7-20260101)",
|
"modelCustomPlaceholder": "model id (e.g. claude-opus-5)",
|
||||||
"modelInheritLabel": "Inherit from settings",
|
"modelInheritLabel": "Inherit from settings",
|
||||||
"permissionAcceptEdits": "acceptEdits (recommended)",
|
"permissionAcceptEdits": "acceptEdits (recommended)",
|
||||||
"permissionBypass": "bypassPermissions (DANGER - allows everything without asking)",
|
"permissionBypass": "bypassPermissions (DANGER - allows everything without asking)",
|
||||||
@@ -153,8 +153,8 @@
|
|||||||
"permissionMode": "Mode",
|
"permissionMode": "Mode",
|
||||||
"session": "Session"
|
"session": "Session"
|
||||||
},
|
},
|
||||||
"subtitle": "Spin up a Claude Code session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.",
|
"subtitle": "Spin up a {{tool}} session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.",
|
||||||
"title": "Run Claude",
|
"title": "Run {{tool}}",
|
||||||
"tokens": {
|
"tokens": {
|
||||||
"cacheRead": "Cache hit",
|
"cacheRead": "Cache hit",
|
||||||
"cost": "Cost",
|
"cost": "Cost",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
"binary": {
|
"binary": {
|
||||||
"found": "Đang dùng {{path}}",
|
"found": "Đang dùng {{path}}",
|
||||||
"missing": "Không thấy CLI `claude` trong PATH. Cài đặt Claude Code hoặc đặt PATH."
|
"missing": "Không thấy CLI `{{cli}}` trong PATH. Cài đặt {{tool}} hoặc đặt PATH."
|
||||||
},
|
},
|
||||||
"concurrency": {
|
"concurrency": {
|
||||||
"active_other": "Có {{count}} run đang chạy",
|
"active_other": "Có {{count}} run đang chạy",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"effort": "Mức độ suy nghĩ",
|
"effort": "Mức độ suy nghĩ",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"modelCustom": "Tuỳ chỉnh…",
|
"modelCustom": "Tuỳ chỉnh…",
|
||||||
"modelCustomPlaceholder": "model id (vd: claude-opus-4-7-20260101)",
|
"modelCustomPlaceholder": "model id (vd: claude-opus-5)",
|
||||||
"modelInheritLabel": "Kế thừa từ settings",
|
"modelInheritLabel": "Kế thừa từ settings",
|
||||||
"permissionAcceptEdits": "acceptEdits (khuyên dùng)",
|
"permissionAcceptEdits": "acceptEdits (khuyên dùng)",
|
||||||
"permissionBypass": "bypassPermissions (NGUY HIỂM - cho phép tất cả mà không hỏi)",
|
"permissionBypass": "bypassPermissions (NGUY HIỂM - cho phép tất cả mà không hỏi)",
|
||||||
@@ -152,8 +152,8 @@
|
|||||||
"permissionMode": "Mode",
|
"permissionMode": "Mode",
|
||||||
"session": "Session"
|
"session": "Session"
|
||||||
},
|
},
|
||||||
"subtitle": "Khởi chạy một phiên Claude Code ngay trong dashboard. Stream output trực tiếp, hội thoại đa lượt, và chia sẻ cùng pipeline hooks/analytics với phiên ở terminal.",
|
"subtitle": "Khởi chạy một phiên {{tool}} ngay trong dashboard. Stream output trực tiếp, hội thoại đa lượt, và chia sẻ cùng pipeline hooks/analytics với phiên ở terminal.",
|
||||||
"title": "Chạy Claude",
|
"title": "Chạy {{tool}}",
|
||||||
"tokens": {
|
"tokens": {
|
||||||
"cacheRead": "Cache hit",
|
"cacheRead": "Cache hit",
|
||||||
"cost": "Chi phí",
|
"cost": "Chi phí",
|
||||||
|
|||||||
+14
-12
@@ -2719,18 +2719,20 @@ export const RUN_EFFORT_CHOICES: EffortChoice[] = [
|
|||||||
// configured default and the rest map to concrete `--model` values.
|
// configured default and the rest map to concrete `--model` values.
|
||||||
export const RUN_MODEL_CHOICES: ModelChoice[] = [
|
export const RUN_MODEL_CHOICES: ModelChoice[] = [
|
||||||
{ id: "", label: "Inherit from settings", hint: "Use whatever your settings.json model is" },
|
{ id: "", label: "Inherit from settings", hint: "Use whatever your settings.json model is" },
|
||||||
{
|
{ id: "claude-opus-5", label: "Opus 5", hint: "Highest capability" },
|
||||||
id: "claude-opus-4-8[1m]",
|
{ id: "claude-sonnet-5", label: "Sonnet 5", hint: "Balanced capability and speed" },
|
||||||
label: "Opus 4.8 (1M context)",
|
{ id: "claude-fable-5", label: "Fable 5", hint: "Fast output, same capability tier as Opus" },
|
||||||
hint: "Highest capability, 1M token window",
|
{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5", hint: "Fastest, lightest" },
|
||||||
},
|
];
|
||||||
{
|
|
||||||
id: "claude-opus-4-7[1m]",
|
// Curated model list for Codex lanes - same shape as RUN_MODEL_CHOICES, sent
|
||||||
label: "Opus 4.7 (1M context)",
|
// via `codex -m`. "" inherits Codex's own configured default.
|
||||||
hint: "Previous Opus, 1M token window",
|
export const RUN_MODEL_CHOICES_CODEX: ModelChoice[] = [
|
||||||
},
|
{ id: "", label: "Inherit from settings", hint: "Use whatever your Codex config model is" },
|
||||||
{ id: "sonnet", label: "Sonnet 4.6", hint: "Balanced capability and speed" },
|
{ id: "gpt-5.6-sol", label: "GPT-5.6 Sol", hint: "Highest capability" },
|
||||||
{ id: "haiku", label: "Haiku 4.5", hint: "Fastest, lightest" },
|
{ id: "gpt-5.6-terra", label: "GPT-5.6 Terra", hint: "Balanced capability and speed" },
|
||||||
|
{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna", hint: "Fastest, lightest" },
|
||||||
|
{ id: "gpt-5.5", label: "GPT-5.5", hint: "Previous generation" },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Result of a transcript import run - returned by `api.import.rescan`,
|
/** Result of a transcript import run - returned by `api.import.rescan`,
|
||||||
|
|||||||
@@ -5142,7 +5142,7 @@ exports[`screen snapshots > Run 1`] = `
|
|||||||
<p
|
<p
|
||||||
class="text-xs text-fg-muted max-w-3xl"
|
class="text-xs text-fg-muted max-w-3xl"
|
||||||
>
|
>
|
||||||
Spin up a Claude Code session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.
|
Spin up a Claude session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user