diff --git a/console/index.html b/console/index.html
index 22acdf1..e1dada7 100644
--- a/console/index.html
+++ b/console/index.html
@@ -273,6 +273,19 @@
explicit auth error, not a silent drift.
+
+
+
+ — not set —
+
+
+
+ Local folder the New Fleet wizard mirrors each agent's generated
+ config.toml into (<folder>/<agent-name>/config.toml),
+ alongside the S3 copy it deploys from — this is what "view an
+ agent's config" reads from, no S3 round-trip.
+
+
diff --git a/console/src/main.ts b/console/src/main.ts
index 8875e51..7c108cc 100644
--- a/console/src/main.ts
+++ b/console/src/main.ts
@@ -174,6 +174,47 @@ const mcp = mcpEl ? createPane(mcpEl, () => flag("mcpio")) : null;
});
})();
+// Local "Config folder" (studio#128) — where the New Fleet wizard mirrors
+// each agent's generated config.toml (alongside the S3 copy it deploys
+// from) and where the "view an agent's config" screen reads from, no S3
+// round-trip. Persisted like the theme/log-level settings above
+// (localStorage), not Tauri's app-config-dir — this setting *points at*
+// other local files, storing it in the hidden config dir would just be an
+// extra layer of indirection for no reason.
+(function setupConfigFolder(): void {
+ const pathEl = document.getElementById("config-folder-path");
+ const pickBtn = document.getElementById("config-folder-pick");
+ if (!pathEl || !pickBtn) return;
+ const KEY = "oab-studio.configFolder";
+
+ const render = (): void => {
+ let saved: string | null = null;
+ try {
+ saved = localStorage.getItem(KEY);
+ } catch {
+ /* storage unavailable — falls through to the placeholder */
+ }
+ pathEl.textContent = saved || "— not set —";
+ };
+ render();
+
+ pickBtn.addEventListener("click", () => {
+ const invoke = tauriInvoke();
+ if (!invoke) return;
+ invoke("plugin:dialog|open", { options: { directory: true } })
+ .then((path) => {
+ if (!path) return;
+ try {
+ localStorage.setItem(KEY, path);
+ } catch {
+ /* storage unavailable — the choice still applies this session */
+ }
+ render();
+ })
+ .catch((e) => note("error", `config folder pick failed: ${errText(e)}`));
+ });
+})();
+
// 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 ea74ab1..03ec2d3 100644
--- a/console/src/styles.css
+++ b/console/src/styles.css
@@ -1244,6 +1244,38 @@ button.act:disabled {
color: var(--muted);
}
+.config-folder {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-width: 420px;
+ margin-top: 20px;
+ padding-top: 16px;
+ border-top: 1px solid var(--border);
+}
+.config-folder > label {
+ font-size: 12px;
+ color: var(--muted);
+}
+.config-folder-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+.config-folder-path {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+ color: var(--text);
+ padding: 6px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg);
+}
+
/* Compose (agent-deployment ADR, slice 1): the deploy panel's bundle-preview
step (`deploy.ts`'s `#deploy-compose`) — the standalone authoring tab this
used to also style is gone, see compose.ts. */
diff --git a/crates/oab-mcp/src/lib.rs b/crates/oab-mcp/src/lib.rs
index 0d34b5d..e08ae05 100644
--- a/crates/oab-mcp/src/lib.rs
+++ b/crates/oab-mcp/src/lib.rs
@@ -248,6 +248,17 @@ pub fn tools() -> Vec {
"properties": {}
})),
),
+ Tool::new(
+ "resolve_vendor_image_tags",
+ "Resolve a vendor's real, currently-published Stable/Beta image tags on ghcr.io/openabdev/openab (studio#128 — backs the New Fleet wizard's Vendor + Image tag fields). \"Beta\" is the hourly rolling pre-beta- build; \"Stable\" is the newest openab release whose matching - image is confirmed to actually exist (a release existing doesn't guarantee a matching image was ever built — the build workflow is a manual, disconnected step). Anonymous GHCR/GitHub access, no auth needed (public package/repo). Either or both fields come back null if nothing verified — not an error; the caller should fall back to a plain editable text field.",
+ as_map(json!({
+ "type": "object",
+ "properties": {
+ "vendor": { "type": "string", "description": "Vendor name, e.g. \"claude\", \"codex\", \"cursor\", \"kiro\", \"antigravity\"." }
+ },
+ "required": ["vendor"]
+ })),
+ ),
Tool::new(
"list_namespaces",
"List namespaces in the cluster a kubeconfig context resolves to. Read-only; backs the New Fleet wizard's namespace