From d2a4e3a46a5a32cd2054508123a648c89de6b222 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Sun, 14 Jun 2026 23:16:17 +0000 Subject: [PATCH] fix(canvas#2594): surface env-derived model/provider hint + block Save until picked Env-resolved workspaces (MODEL secret empty, config.yaml blank) run fine because the runtime derives model/provider from persona env baked at provision. The Config tab used to show empty required PROVIDER/MODEL dropdowns, making the workspace look broken and allowing a confusing Save-with-blanks action. Changes: - Read the new `source` field from GET /workspaces/:id/model and track `modelSource` in ConfigTab state. - When source is `unresolved` and no model is selected, show an info hint explaining that provider/model are derived from the runtime env. - Block Save / Save & Restart while model source is unresolved and no model has been picked, so users can't silently save empty values. - Add unit tests for the unresolved source path. The canvas cannot read the live container env directly; this change surfaces the derivation clearly and forces an explicit model selection before persisting. A future backend endpoint that exposes effective env-resolved values can replace the generic hint with concrete values. Fixes #2594 --- canvas/src/components/tabs/ConfigTab.tsx | 31 ++++- .../__tests__/ConfigTab.env-resolved.test.tsx | 113 ++++++++++++++++++ 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 canvas/src/components/tabs/__tests__/ConfigTab.env-resolved.test.tsx diff --git a/canvas/src/components/tabs/ConfigTab.tsx b/canvas/src/components/tabs/ConfigTab.tsx index 8d05f20e..9308d798 100644 --- a/canvas/src/components/tabs/ConfigTab.tsx +++ b/canvas/src/components/tabs/ConfigTab.tsx @@ -443,6 +443,12 @@ export function ConfigTab({ workspaceId }: Props) { // save propagate-and-restart even when the user didn't touch the model. // Capturing the actual rendered value covers both. const [originalModel, setOriginalModel] = useState(""); + // core#2594: source of the loaded model value. "workspace_secrets" means + // the model is persisted in the DB; "unresolved" means the workspace is + // running with a runtime-env-derived model and the canvas has no stored + // value to display. Used to show a "derived from environment" hint and to + // block Save from appearing to wipe env-derived routing. + const [modelSource, setModelSource] = useState<"workspace_secrets" | "unresolved" | null>(null); const successTimerRef = useRef>(undefined); useEffect(() => { @@ -480,11 +486,12 @@ export function ConfigTab({ workspaceId }: Props) { const [wsRes, modelRes] = await Promise.all([ api.get<{ runtime?: string; tier?: number }>(`/workspaces/${workspaceId}`) .catch(() => ({} as { runtime?: string; tier?: number })), - api.get<{ model?: string }>(`/workspaces/${workspaceId}/model`) - .catch(() => ({} as { model?: string })), + api.get<{ model?: string; source?: "workspace_secrets" | "unresolved" }>(`/workspaces/${workspaceId}/model`) + .catch(() => ({} as { model?: string; source?: "workspace_secrets" | "unresolved" })), ]); const wsMetadataRuntime = (wsRes.runtime || "").trim(); const wsMetadataModel = (modelRes.model || "").trim(); + setModelSource(modelRes.source ?? (wsMetadataModel ? "workspace_secrets" : "unresolved")); const wsMetadataTier: number | null = typeof wsRes.tier === "number" ? wsRes.tier : null; setProvider(""); @@ -915,6 +922,12 @@ export function ConfigTab({ workspaceId }: Props) { const providerDirty = provider !== originalProvider; const isDirty = (rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml) || providerDirty; + // core#2594: an env-resolved workspace has no stored model. Saving while + // the model dropdown is still empty can't "wipe" routing (handleSave skips + // an empty /model PUT), but it is confusing and can stall the user on other + // fields without surfacing why. Block save until a model is picked. + const modelUnresolved = modelSource === "unresolved" && !currentModelId; + const canSave = isDirty && !modelUnresolved; if (loading) { return
Loading config...
; @@ -1001,6 +1014,16 @@ export function ConfigTab({ workspaceId }: Props) { ))} + {/* core#2594: env-resolved workspaces have no stored model/provider. + Surface that clearly so users don't see empty required dropdowns + on a healthy workspace, and can't hit Save expecting it to stay + empty (which would otherwise look like it "wiped" routing). */} + {modelSource === "unresolved" && !currentModelId && ( +
+ Provider and model are derived from the workspace runtime environment.{" "} + They are not stored in config.yaml. Select a model below to persist them. +
+ )} {/* Shared Provider→Model selector. Same component renders in MissingKeysModal (deploy onboarding) so the dropdown UX is identical across all three surfaces. Provider field maps @@ -1311,7 +1334,7 @@ export function ConfigTab({ workspaceId }: Props) {