From 06273b11ef35ca6273c76c61a759a094db9b5d61 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 23 Apr 2026 14:58:36 -0700 Subject: [PATCH] fix(canvas/config): load runtime+model from workspace metadata + hide misleading config.yaml error for hermes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canvas Config tab had 3 bugs visible on hermes workspaces (#1894): 1. Runtime dropdown showed "LangGraph (default)" even when the workspace's actual runtime was hermes — because the form only loaded runtime from config.yaml, and hermes doesn't use the platform's config.yaml template. 2. Model field was empty for the same reason. 3. "No config.yaml found" error appeared on hermes workspaces despite everything being fine — hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Worse, clicking Save with the empty form would silently flip `runtime` back from `hermes` to `LangGraph (default)`. ## Fix - loadConfig now always fetches workspace metadata (runtime + model) via GET /workspaces/:id and GET /workspaces/:id/model BEFORE attempting the config.yaml fetch. These act as the source of truth for runtime and model when config.yaml doesn't set them. - RUNTIMES_WITH_OWN_CONFIG set lists runtimes that manage their own config outside the platform template (hermes, external). For these: - Missing config.yaml is NOT an error — no red banner shown. - An informational gray banner tells the user where to edit the runtime's config (e.g. "edit ~/.hermes/config.yaml via Terminal tab or the hermes CLI" for hermes). Closes #1894. Verified 2026-04-23 on user's hongmingwang tenant which runs hermes. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/tabs/ConfigTab.tsx | 54 +++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) 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
)}