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,