Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions canvas/src/components/tabs/ConfigTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(["hermes", "external"]);

const FALLBACK_RUNTIME_OPTIONS: RuntimeOption[] = [
{ value: "", label: "LangGraph (default)", models: [] },
{ value: "claude-code", label: "Claude Code", models: [] },
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -511,6 +554,13 @@ export function ConfigTab({ workspaceId }: Props) {
{error && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">{error}</div>
)}
{!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-zinc-900/50 border border-zinc-700 rounded text-xs text-zinc-400">
{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."}
</div>
)}
{success && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-green-900/30 border border-green-800 rounded text-xs text-green-400">Saved</div>
)}
Expand Down
Loading