From f9b992227c73db902e39a2cab5f227e0a83d0eb3 Mon Sep 17 00:00:00 2001
From: Brett Chien
Date: Sat, 29 Aug 2026 16:24:58 +0800
Subject: [PATCH 1/2] feat(console,src-tauri): local "Config folder" setting
(studio#128)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a "Config folder" field to the Debug drawer's Config tab — a
user-picked local directory (native folder picker via tauri-plugin-dialog)
that later work will mirror each agent's generated config.toml into
(//config.toml, alongside the existing S3 upload
provision_agent/provision_agent_k8s already do — unchanged) and that the
"view an agent's config" screen (also studio#128, separate follow-up) will
read from directly, no S3 round-trip.
This slice is just the setting itself: plugin registration + capability
grant (dialog:allow-open — the app's capabilities/default.json had no
dialog permission at all before this) + picker UI, persisted like the
existing theme/log-level settings (localStorage), not Tauri's hidden
app-config-dir — this setting points at other local files, so storing it
in the hidden config dir would just be an extra layer of indirection.
Wiring the wizard/view-screen to actually use it lands with those pieces.
Verification: npm run typecheck clean, npm test 106/106 passing, npm run
build succeeds. Rust side (plugin registration, Cargo.toml) not locally
compiled — same pre-existing limitation as every other Rust change this
week; tauri-plugin-dialog's exact API (invoke command name
"plugin:dialog|open", options shape, return type for directory+non-multiple
mode) was checked against the plugin's actual TypeScript source before
using it, not guessed.
Ref #128.
---
console/index.html | 13 +++++++++
console/src/main.ts | 41 +++++++++++++++++++++++++++++
console/src/styles.css | 32 ++++++++++++++++++++++
src-tauri/Cargo.toml | 5 ++++
src-tauri/capabilities/default.json | 1 +
src-tauri/src/lib.rs | 1 +
6 files changed, 93 insertions(+)
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/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 4d6d0e0..2eb2d58 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -55,6 +55,11 @@ tauri-plugin-updater = "2"
# commit rather than the floating `dev` branch for reproducibility.
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", rev = "c4c45d503ea115a839aae718d02f79e7c7f0f673" }
+# Native folder-picker dialog (studio#128) — lets the operator choose the
+# local "Config folder" the New Fleet wizard mirrors generated config.toml
+# files into, instead of a hidden hardcoded path.
+tauri-plugin-dialog = "2"
+
# Its own workspace so the desktop build stays out of the root workspace's
# `cargo build --workspace` (kept to the small crates + CI).
[workspace]
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 4a8d596..aacca3c 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -9,6 +9,7 @@
"core:default",
"shell:allow-stdin-write",
"shell:allow-kill",
+ "dialog:allow-open",
{
"identifier": "shell:allow-spawn",
"allow": [
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 62c03ca..94f5492 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -773,6 +773,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
+ .plugin(tauri_plugin_dialog::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
From cd6787e170c9bcfd12dc704b8ded020e4be10f03 Mon Sep 17 00:00:00 2001
From: Brett Chien
Date: Sat, 29 Aug 2026 16:31:15 +0800
Subject: [PATCH 2/2] feat(oabctl,studio-cp,oab-mcp,src-tauri): resolve vendor
Stable/Beta image tags from GHCR (studio#128)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Item 7 of #128's runbook. Backs the New Fleet wizard's Vendor + Image tag
fields — resolves a vendor name to real, currently-published
ghcr.io/openabdev/openab image tags instead of the operator guessing a tag
that might not exist.
Initially thought this needed a GitHub auth mechanism Studio doesn't have
(GitHub's Packages REST API requires a read:packages-scoped token even for
public packages) — Brett pushed back that reading a public GHCR image
shouldn't need auth, which was right: ghcr.io itself speaks the standard
OCI Distribution API, not just GitHub's REST API, and a public package
supports the same anonymous pull-token flow `docker pull` uses with no
login (`GET /token?scope=repository::pull`, verified live against
openabdev/openab before writing any code). Switched to that.
- oabctl: new `vendor_images` module. `pre-beta-` (Beta, the
hourly rolling build) and the newest openab GitHub release whose
matching `-` image is confirmed to exist (Stable) —
verified via a manifest HEAD request per candidate (existence check, no
image bytes transferred), not a bulk tag-list scan (this repo has
thousands of tags across multiple pages). A release tag existing doesn't
guarantee a matching image was ever published (`build-images.yml` is a
manual `workflow_dispatch`, disconnected from cutting a release), so
Stable is verified against GHCR directly rather than inferred from the
release list alone. GitHub's releases API also needed a name-based
filter alongside `prerelease`: at least one real release
(`openab-0.10.0-beta.3`) has `prerelease: false` despite its name.
- studio-cp: thin `resolve_vendor_image_tags` wrapper.
- oab-mcp: new `resolve_vendor_image_tags` tool. Never errors on a failed
GHCR/GitHub call — leaves the corresponding field `None`, the console
falls back to a plain editable text field either way (Brett already
confirmed manual image tag entry stays available).
- src-tauri: bridge command, same shape as the other read-only wizard
pickers (list_k8s_contexts etc).
Console wiring (actually using this in the Vendor select) lands with the
rest of the wizard UI (#128's items 1/3/4/5), not this PR.
Ref #128.
---
crates/oab-mcp/src/lib.rs | 26 +++++-
crates/oabctl/src/lib.rs | 1 +
crates/oabctl/src/vendor_images.rs | 139 +++++++++++++++++++++++++++++
crates/studio-cp/src/lib.rs | 9 ++
src-tauri/src/lib.rs | 25 ++++++
5 files changed, 199 insertions(+), 1 deletion(-)
create mode 100644 crates/oabctl/src/vendor_images.rs
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