diff --git a/console/index.html b/console/index.html
index 05ef879..34eb947 100644
--- a/console/index.html
+++ b/console/index.html
@@ -50,9 +50,54 @@
+
diff --git a/console/src/config.ts b/console/src/config.ts
new file mode 100644
index 0000000..f1c7177
--- /dev/null
+++ b/console/src/config.ts
@@ -0,0 +1,94 @@
+// Config tab: read the current oab-mcp target from the backend, let the user pin
+// cluster / profile / region, and on save persist + reload the core onto it.
+// Only meaningful inside the Tauri shell; the browser build disables the form.
+//
+// The backend target is provider-tagged (`McpTarget`, config.rs). Today the only
+// variant is ECS, so the form maps 1:1 to `{ provider: "ecs", cluster, profile,
+// region }`; a future provider would add its own fields/section.
+
+export interface EcsTarget {
+ provider: "ecs";
+ cluster: string;
+ profile?: string | null;
+ region?: string | null;
+}
+export type McpTarget = EcsTarget;
+
+type Invoke = (cmd: string, args?: Record) => Promise;
+
+function tauriInvoke(): Invoke | null {
+ const t = (globalThis as { __TAURI__?: { core?: { invoke?: Invoke } } }).__TAURI__;
+ return t?.core?.invoke ?? null;
+}
+
+// Tauri command rejections arrive as plain strings, not Error objects.
+function errText(e: unknown): string {
+ return e instanceof Error ? e.message : String(e);
+}
+
+export interface ConfigHooks {
+ /** Fired with the target loaded from disk at startup. */
+ onLoaded?: (t: McpTarget) => void;
+ /** Fired after a successful save + core reload. */
+ onSaved?: (t: McpTarget) => void;
+}
+
+export function initConfigTab(hooks: ConfigHooks = {}): void {
+ const form = document.getElementById("config-form") as HTMLFormElement | null;
+ const profile = document.getElementById("cfg-profile") as HTMLInputElement | null;
+ const region = document.getElementById("cfg-region") as HTMLInputElement | null;
+ const cluster = document.getElementById("cfg-cluster") as HTMLInputElement | null;
+ const save = document.getElementById("cfg-save") as HTMLButtonElement | null;
+ const status = document.getElementById("cfg-status");
+ if (!form || !profile || !region || !cluster) return;
+
+ const setStatus = (msg: string, cls = ""): void => {
+ if (status) {
+ status.textContent = msg;
+ status.className = cls ? `config-status ${cls}` : "config-status";
+ }
+ };
+
+ const invoke = tauriInvoke();
+ if (!invoke) {
+ // Browser build — no core to configure.
+ setStatus("browser build — config unavailable");
+ for (const el of form.querySelectorAll(
+ "input, button",
+ )) {
+ el.disabled = true;
+ }
+ return;
+ }
+
+ // Populate the form from the persisted (or env-seeded) target.
+ invoke("mcp_target_get")
+ .then((t) => {
+ cluster.value = t.cluster ?? "";
+ profile.value = t.profile ?? "";
+ region.value = t.region ?? "";
+ hooks.onLoaded?.(t);
+ })
+ .catch((e) => setStatus(`load failed: ${errText(e)}`, "err"));
+
+ form.addEventListener("submit", async (ev) => {
+ ev.preventDefault();
+ const target: McpTarget = {
+ provider: "ecs",
+ cluster: cluster.value.trim() || "oab",
+ profile: profile.value.trim() || null,
+ region: region.value.trim() || null,
+ };
+ if (save) save.disabled = true;
+ setStatus("saving & reloading core…");
+ try {
+ await invoke("mcp_target_set", { target });
+ setStatus("saved — core reloaded", "ok");
+ hooks.onSaved?.(target);
+ } catch (e) {
+ setStatus(`save failed: ${errText(e)}`, "err");
+ } finally {
+ if (save) save.disabled = false;
+ }
+ });
+}
diff --git a/console/src/main.ts b/console/src/main.ts
index 7fbc72a..d3c8e3e 100644
--- a/console/src/main.ts
+++ b/console/src/main.ts
@@ -1,4 +1,5 @@
import { defaultSource } from "./source";
+import { initConfigTab } from "./config";
import {
renderRoster,
renderIdentity,
@@ -750,6 +751,9 @@ async function boot(): Promise {
note("info", `app: polling cluster "${activeCluster}" every ${POLL_MS / 1000}s`);
setupUpdater();
await startCore();
+ // Config tab: pin the oab-mcp target (cluster/profile/region → hermetic env);
+ // on save the backend reloads the core, so refresh the roster after.
+ initConfigTab({ onSaved: () => void tick() });
void refreshConfig();
void refreshIdentity();
void refreshRemote();
diff --git a/console/src/styles.css b/console/src/styles.css
index c6e83d9..5cdedfa 100644
--- a/console/src/styles.css
+++ b/console/src/styles.css
@@ -810,3 +810,63 @@ button.act:disabled {
border-color: var(--s-unhealthy);
color: var(--s-unhealthy);
}
+
+/* Config tab — pin the oab-mcp target (cluster / profile / region). */
+.pane.config {
+ padding: 16px;
+ overflow: auto;
+}
+.config-form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-width: 420px;
+}
+.config-form label {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ font-size: 12px;
+ color: var(--muted);
+}
+.config-form input {
+ padding: 6px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg);
+ color: var(--text);
+ font: inherit;
+}
+.config-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+.config-actions button {
+ padding: 6px 12px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel);
+ color: var(--text);
+ cursor: pointer;
+}
+.config-actions button:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+.config-status {
+ font-size: 12px;
+ color: var(--muted);
+}
+.config-status.ok {
+ color: var(--ok);
+}
+.config-status.err {
+ color: var(--s-unhealthy);
+}
+.config-hint {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--muted);
+}