diff --git a/canvas/src/components/tabs/ConfigTab.tsx b/canvas/src/components/tabs/ConfigTab.tsx index 7d177ebf..4bf4b09f 100644 --- a/canvas/src/components/tabs/ConfigTab.tsx +++ b/canvas/src/components/tabs/ConfigTab.tsx @@ -104,6 +104,13 @@ interface RuntimeOption { // Fallback used when /templates can't be fetched (offline, older backend). // Keep in sync with manifest.json workspace_templates as a defensive default. // Model + env suggestions only flow when the backend is reachable. +// Runtimes that manage their own config outside the platform's config.yaml +// template. For these, a missing config.yaml is expected — the user manages +// config via the runtime's own mechanism (e.g. hermes edits +// ~/.hermes/config.yaml on the workspace EC2 via the Terminal tab or its +// own CLI). Showing a "No config.yaml found" error for these is misleading. +const RUNTIMES_WITH_OWN_CONFIG = new Set(["hermes", "external"]); + const FALLBACK_RUNTIME_OPTIONS: RuntimeOption[] = [ { value: "", label: "LangGraph (default)", models: [] }, { value: "claude-code", label: "Claude Code", models: [] }, @@ -134,14 +141,50 @@ export function ConfigTab({ workspaceId }: Props) { const loadConfig = useCallback(async () => { setLoading(true); setError(null); + + // ALWAYS load workspace metadata first (runtime + model). These are the + // source of truth regardless of whether the runtime uses our config.yaml + // template. Without this the form falls back to empty/default values on + // a hermes workspace (which doesn't use our template), creating the + // appearance that the saved runtime is unset — and worse, clicking Save + // would silently flip `runtime` from `hermes` back to the dropdown + // default `LangGraph`. See GH #1894. + let wsMetadataRuntime = ""; + let wsMetadataModel = ""; + try { + const ws = await api.get<{ runtime?: string }>(`/workspaces/${workspaceId}`); + wsMetadataRuntime = (ws.runtime || "").trim(); + } catch { /* fall back to config.yaml */ } + try { + const m = await api.get<{ model?: string }>(`/workspaces/${workspaceId}/model`); + wsMetadataModel = (m.model || "").trim(); + } catch { /* non-fatal */ } + try { const res = await api.get<{ content: string }>(`/workspaces/${workspaceId}/files/config.yaml`); const parsed = parseYaml(res.content); setOriginalYaml(res.content); setRawDraft(res.content); - setConfig({ ...DEFAULT_CONFIG, ...parsed } as ConfigData); + // Merge: config.yaml wins for fields it declares, but workspace metadata + // wins for runtime + model when config.yaml doesn't set them. + const merged = { ...DEFAULT_CONFIG, ...parsed } as ConfigData; + if (!merged.runtime && wsMetadataRuntime) merged.runtime = wsMetadataRuntime; + if (!merged.model && wsMetadataModel) merged.model = wsMetadataModel; + setConfig(merged); } catch { - setError("No config.yaml found"); + // No platform-managed config.yaml. Some runtimes (hermes, external) + // manage their own config outside this template; that's expected, not + // an error. Populate the form from workspace metadata so the user + // still sees the saved runtime + model. + const runtimeManagesOwnConfig = RUNTIMES_WITH_OWN_CONFIG.has(wsMetadataRuntime); + if (!runtimeManagesOwnConfig) { + setError("No config.yaml found"); + } + setConfig({ + ...DEFAULT_CONFIG, + runtime: wsMetadataRuntime, + model: wsMetadataModel, + } as ConfigData); } finally { setLoading(false); } @@ -511,6 +554,13 @@ export function ConfigTab({ workspaceId }: Props) { {error && (
{error}
)} + {!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && ( +
+ {config.runtime === "hermes" + ? "Hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Edit it via the Terminal tab or the hermes CLI, not this form." + : "This runtime manages its own config outside the platform template."} +
+ )} {success && (
Saved
)}