From 65a447fdd089d055cab4d4fe902a3b0dfb0bd6ae Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Sat, 29 Aug 2026 17:12:54 +0800 Subject: [PATCH] feat(console,src-tauri): "Agent configs" view, local Config folder only (studio#128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last item on #128's runbook. New Debug drawer tab, next to the existing Activity/MCP/Config ones — lists agent names under the local "Config folder" (#130) that have a config.toml, and shows the selected one's content read-only. Deliberately local-filesystem-only, no S3 (Brett: "forget about s3 now"): list_local_agent_configs/read_local_agent_config are plain std::fs calls in src-tauri, no sidecar/MCP tool involved at all, unlike everything else added this batch — the local folder is Studio's own mirror the New Fleet wizard writes alongside its S3 upload (still to be wired — the wizard itself doesn't write here yet, this PR is just the read side), not a read-through to the S3 source of truth. Whatever an admin agent (or the wizard, once wired) writes into the same folder shows up on the next Refresh / tab switch — reading a directory needs no separate sync mechanism. Agent names render via DOM construction (createElement + textContent), not innerHTML string concatenation — they come from local directory listings, not worth trusting as HTML even though the risk is low (Studio itself controls what gets written there today). Verification: npm run typecheck clean, npm test 100/100 passing, npm run build succeeds. Rust side not locally compiled — same pre-existing limitation as every other Rust change this week, though this one is lower risk than most (plain std::fs, no new dependency, no capability grant needed — custom commands aren't plugin-gated the way tauri-plugin-dialog was in #130). Ref #128 — this closes out the runbook's item list (the wizard→folder write side is a natural follow-up once someone needs it, not blocking). --- console/index.html | 9 +++++ console/src/main.ts | 74 ++++++++++++++++++++++++++++++++++++++++++ console/src/styles.css | 31 ++++++++++++++++++ src-tauri/src/lib.rs | 35 ++++++++++++++++++++ 4 files changed, 149 insertions(+) diff --git a/console/index.html b/console/index.html index 0802a80..c01a320 100644 --- a/console/index.html +++ b/console/index.html @@ -253,6 +253,7 @@ + @@ -316,6 +317,14 @@

+ diff --git a/console/src/main.ts b/console/src/main.ts index 7c108cc..f866c93 100644 --- a/console/src/main.ts +++ b/console/src/main.ts @@ -215,6 +215,80 @@ const mcp = mcpEl ? createPane(mcpEl, () => flag("mcpio")) : null; }); })(); +// "Agent configs" Debug drawer tab (studio#128) — reads purely from the +// local Config folder set above, no S3 (Brett: "forget about s3 now"). +// Whatever an admin agent (or the New Fleet wizard) writes into that same +// folder shows up here on the next Refresh / tab switch, no separate sync +// mechanism needed — it's just reading a directory. +(function setupAgentConfigsTab(): void { + const selectEl = document.getElementById("agent-configs-select") as HTMLSelectElement | null; + const refreshBtn = document.getElementById("agent-configs-refresh"); + const statusEl = document.getElementById("agent-configs-status"); + const contentEl = document.getElementById("agent-configs-content"); + const tabBtn = document.querySelector('[data-target="debug-agent-configs"]'); + if (!selectEl || !refreshBtn || !statusEl || !contentEl) return; + const FOLDER_KEY = "oab-studio.configFolder"; + + const currentFolder = (): string | null => { + try { + return localStorage.getItem(FOLDER_KEY); + } catch { + return null; + } + }; + + const showContent = async (agent: string, folder: string): Promise => { + const invoke = tauriInvoke(); + if (!invoke) return; + try { + const text = await invoke("read_local_agent_config", { folder, agent }); + contentEl.textContent = text; + statusEl.textContent = ""; + } catch (e) { + contentEl.textContent = ""; + statusEl.textContent = `read failed: ${errText(e)}`; + } + }; + + const refresh = async (): Promise => { + const invoke = tauriInvoke(); + const folder = currentFolder(); + contentEl.textContent = ""; + selectEl.innerHTML = ""; + if (!invoke) return; + if (!folder) { + statusEl.textContent = "set a Config folder first (Config tab)"; + return; + } + try { + const agents = await invoke("list_local_agent_configs", { folder }); + if (agents.length === 0) { + statusEl.textContent = "no agent configs found in this folder yet"; + return; + } + // DOM construction, not innerHTML — agent names come from local + // directory listings, not worth trusting as HTML. + for (const agent of agents) { + const opt = document.createElement("option"); + opt.value = agent; + opt.textContent = agent; + selectEl.appendChild(opt); + } + statusEl.textContent = ""; + void showContent(selectEl.value, folder); + } catch (e) { + statusEl.textContent = `list failed: ${errText(e)}`; + } + }; + + refreshBtn.addEventListener("click", () => void refresh()); + selectEl.addEventListener("change", () => { + const folder = currentFolder(); + if (folder) void showContent(selectEl.value, folder); + }); + tabBtn?.addEventListener("click", () => void refresh()); +})(); + // Build stamp (injected by vite) — shown under the brand and logged on launch, // so it's obvious which commit this build is. const BUILD = `v${__APP_VERSION__} · ${__BUILD_SHA__}`; diff --git a/console/src/styles.css b/console/src/styles.css index 0af8a95..9d89a8d 100644 --- a/console/src/styles.css +++ b/console/src/styles.css @@ -1189,6 +1189,37 @@ button.act:disabled { padding: 16px; overflow: auto; } +/* Agent configs tab (studio#128) — local-folder-only view of a deployed + agent's config.toml (mirrors the log panes' monospace/scroll shape). */ +.pane.agent-configs { + padding: 16px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; +} +.agent-configs-row { + display: flex; + gap: 12px; + align-items: center; +} +.agent-configs-row .compose-select { + flex: 1; +} +.agent-configs-content { + margin: 0; + padding: 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font-family: ui-monospace, monospace; + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; + overflow: auto; + min-height: 120px; +} .config-form { display: flex; flex-direction: column; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c5ef653..6e551da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -441,6 +441,39 @@ async fn resolve_vendor_image_tags(core: tauri::State<'_, Core>, vendor: String) } } +/// Lists agent names under the local "Config folder" (studio#128) that +/// have a `config.toml` — backs the Debug drawer's "Agent configs" tab. +/// Pure local filesystem, no sidecar/AWS/k8s involved at all: the local +/// folder is Studio's own mirror the New Fleet wizard writes alongside its +/// S3 upload, not a read-through to the S3 source of truth (Brett: +/// "forget about s3 now" for this feature — see the Config folder +/// setting's own doc comment for the full reasoning). +#[tauri::command] +fn list_local_agent_configs(folder: String) -> Result, String> { + let entries = std::fs::read_dir(&folder).map_err(|e| format!("read {folder}: {e}"))?; + let mut names = Vec::new(); + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.is_dir() && path.join("config.toml").is_file() { + if let Some(name) = entry.file_name().to_str() { + names.push(name.to_string()); + } + } + } + names.sort(); + Ok(names) +} + +/// Reads one agent's `config.toml` from the local "Config folder" — +/// `//config.toml`, the same layout +/// `list_local_agent_configs` scans. +#[tauri::command] +fn read_local_agent_config(folder: String, agent: String) -> Result { + let path = std::path::Path::new(&folder).join(&agent).join("config.toml"); + std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display())) +} + /// Bridge command: namespaces in a kubeconfig context (studio#104), via the /// sidecar's `list_namespaces` tool — backs the New Fleet wizard's namespace /// field's autocomplete. @@ -830,6 +863,8 @@ pub fn run() { list_aws_profiles, list_k8s_contexts, resolve_vendor_image_tags, + list_local_agent_configs, + read_local_agent_config, list_namespaces, list_service_accounts, k8s_fleet_config,