diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15ef419..941d2a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,4 +213,4 @@ jobs: magent tools doctor magent system security-report - name: Cross-platform contract tests - run: python -m pytest -q tests/unit/test_permissions_bypass.py tests/unit/test_safe_names.py tests/unit/test_durability_and_state.py tests/unit/test_auth_store.py tests/unit/test_tools_safety.py tests/unit/test_provider_support.py tests/unit/test_security_assurance.py -k "not document_tools_create_docx_and_pptx and not visual_artifact_tools_create_svg_diagram_and_image" + run: python -m pytest -q tests/unit/test_process_liveness.py tests/unit/test_permissions_bypass.py tests/unit/test_safe_names.py tests/unit/test_durability_and_state.py tests/unit/test_auth_store.py tests/unit/test_tools_safety.py tests/unit/test_provider_support.py tests/unit/test_security_assurance.py -k "not document_tools_create_docx_and_pptx and not visual_artifact_tools_create_svg_diagram_and_image" diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9eea3..e6bb327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.3.0 — Unreleased + +See [release notes](docs/RELEASE_NOTES_1.3.0.md). + ## Unreleased ## 1.2.0 (2026-09-06) diff --git a/README.md b/README.md index ef14541..74c0799 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@
+Next release: [1.3.0 preparation notes](docs/RELEASE_NOTES_1.3.0.md). + MagAgent logo # 🐦‍⬛ MagAgent diff --git a/docs/RELEASE_NOTES_1.3.0.md b/docs/RELEASE_NOTES_1.3.0.md new file mode 100644 index 0000000..1b26a66 --- /dev/null +++ b/docs/RELEASE_NOTES_1.3.0.md @@ -0,0 +1,15 @@ +# MagAgent 1.3.0 + +Release notes — September 12, 2026. + +- External approval decisions now wake the issuing broker; request creation and decisions share full storage transactions. Corruption and lock timeout fail closed. Windows locking uses an actual byte lock. +- Stdio decisions validate their AAIS envelope and reviewed digest. The Web UI reports the authority outcome, retains dialog focus, and shows origin and expiry. +- `magent capabilities --json` reports installed WebMCP prerequisites without a model call; the existing `magent tools doctor` remains available. +- Run center exposes recorded file changes, checkpoints and final audit evidence before recovery, and recognizes the runtime's waiting state. Task status changes are explicitly distinguished from restarting a stopped harness. +- Config safety and workbench maintenance now pass the type checker and were removed from its exclusion list. + +Approval and task records remain local to their existing authority. Inspect uncertain external effects before retrying; changing a task status is not a guarantee of execution resumption. + +AGS, OAP and AAIS document/wire formats remain unchanged. Local validation evidence and remaining platform gates are recorded in the ecosystem release report. + +Recovery ownership checks use a non-destructive Windows process query, with a regression test that verifies live child processes survive inspection. diff --git a/pyproject.toml b/pyproject.toml index 737cf05..85a03e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mag-agent" -version = "1.2.0" +version = "1.3.0" description = "MagAgent — CLI AI coding agent with persistent MagGraph memory. Like the Magpie: intelligent, tool-using, and never forgets." readme = "README.md" license = { text = "Apache-2.0" } @@ -181,7 +181,6 @@ module = [ "magent.cli.commands.plugins", "magent.cli.main", "magent.config", - "magent.config_safety", "magent.gateway.adapters.slack", "magent.goal_orchestrator", "magent.model_capabilities", @@ -190,7 +189,6 @@ module = [ "magent.tools.web", "magent.utils", "magent.workbench", - "magent.workbench_maintenance", ] ignore_errors = true diff --git a/src/magent/__init__.py b/src/magent/__init__.py index c2aa901..10149f1 100644 --- a/src/magent/__init__.py +++ b/src/magent/__init__.py @@ -1,4 +1,4 @@ """MagAgent — CLI AI coding agent powered by MagGraph persistent memory.""" -__version__ = "1.2.0" +__version__ = "1.3.0" __author__ = "Alex Merced" diff --git a/src/magent/approval_broker.py b/src/magent/approval_broker.py index 5de0106..c64f435 100644 --- a/src/magent/approval_broker.py +++ b/src/magent/approval_broker.py @@ -10,9 +10,11 @@ import copy import json +import os import signal import sys import threading +import time from collections.abc import Callable, Mapping from contextlib import suppress from dataclasses import dataclass @@ -29,7 +31,7 @@ validate, ) -from magent.workbench_store import WorkbenchStore +from magent.workbench_store import WorkbenchStore, WorkbenchStoreError Envelope = dict[str, Any] Publisher = Callable[[Envelope], None] @@ -109,8 +111,17 @@ def _empty() -> Envelope: } def _state(self) -> Envelope: - state = self.store.read(self.STORE_NAME, self._empty()) - return state if isinstance(state, dict) else self._empty() + state = self.store.read(self.STORE_NAME, self._empty(), strict=True) + if ( + not isinstance(state, dict) + or state.get("schema") != self._empty()["schema"] + or any( + key not in state or not isinstance(state[key], type(default)) + for key, default in self._empty().items() + ) + ): + raise WorkbenchStoreError("Invalid approval state; explicit recovery is required") + return state def _next_sequence(self, state: Envelope, key: str = "sequence") -> int: value = int(state.get(key, 0)) + 1 @@ -173,7 +184,7 @@ def request( ) -> str: exact_action = copy.deepcopy(dict(action)) digest = action_digest(exact_action) - with self._lock: + with self._lock, self.store.lock(self.STORE_NAME): state = self._state() remembered = next( ( @@ -229,6 +240,7 @@ def request( ) request_id = str(envelope["request"]["id"]) state.setdefault("pending", {})[request_id] = envelope + state.setdefault("owners", {})[request_id] = os.getpid() state.setdefault("events", []).append(envelope) state["events"] = state["events"][-1000:] self.store.write(self.STORE_NAME, state) @@ -240,18 +252,29 @@ def request( self._waiters[request_id] = waiter self._publishers[request_id] = publish publish(copy.deepcopy(envelope)) - if not waiter.resolved.wait(timeout): - with suppress(ConflictError, ValueError): - self.decide( - request_id, - decision="deny", - scope="once", - actor={ - "id": "magent.timeout", - "type": "policy", - "authenticated_by": "authority", - }, - ) + deadline = time.monotonic() + timeout + while not waiter.resolved.wait(min(0.1, max(0, deadline - time.monotonic()))): + with self._lock, self.store.lock(self.STORE_NAME): + durable = self._state().get("resolutions", {}).get(request_id) + if durable: + waiter.resolution = durable + publish(copy.deepcopy(durable)) + break + if time.monotonic() >= deadline: + with suppress(ConflictError, ValueError): + self.decide( + request_id, + decision="deny", + scope="once", + actor={ + "id": "magent.timeout", + "type": "policy", + "authenticated_by": "authority", + }, + ) + with self._lock, self.store.lock(self.STORE_NAME): + waiter.resolution = self._state().get("resolutions", {}).get(request_id) + break with self._lock: resolution = waiter.resolution self._waiters.pop(request_id, None) @@ -259,6 +282,8 @@ def request( if not resolution: return "deny" body = resolution["resolution"] + if body["outcome"] == "approved" and action_digest(waiter.current_action()) != digest: + return "deny" return str(body.get("effective_scope", "deny")) if body["outcome"] == "approved" else "deny" def decide( @@ -269,6 +294,7 @@ def decide( scope: str, actor: Mapping[str, Any], decision_id: str | None = None, + reviewed_digest: str | None = None, ) -> Envelope: publisher: Publisher | None = None waiter: _Waiter | None = None @@ -286,6 +312,16 @@ def decide( raise ConflictError(f"request {request_id} was already resolved") if pending is None: raise ValueError(f"unknown pending approval: {request_id}") + if ( + reviewed_digest is not None + and reviewed_digest != pending["request"]["action_digest"] + ): + raise ConflictError("Decision digest does not match the reviewed action") + owner = state.get("owners", {}).get(request_id) + if decision == "approve" and owner is not None and not self._owner_alive(owner): + raise ConflictError( + "The issuing process stopped; inspect recovery before starting new work" + ) decided = create_decision( pending, decision=decision, @@ -308,6 +344,7 @@ def decide( state.setdefault("decisions", {})[request_id] = decided state.setdefault("resolutions", {})[request_id] = resolution state.setdefault("events", []).append(resolution) + state["events"] = state["events"][-1000:] if resolution["resolution"]["outcome"] == "approved" and scope in { "session", "persistent", @@ -375,12 +412,35 @@ def cancel_active(self) -> int: ) return len(request_ids) + @staticmethod + def _owner_alive(pid: int | None) -> bool: + from magent.process_liveness import process_alive + + return process_alive(pid) + + def recovery(self) -> Envelope: + with self._lock, self.store.lock(self.STORE_NAME): + state = self._state() + owners = state.get("owners", {}) + return { + "orphaned": [ + key + for key in state["pending"] + if key in owners and not self._owner_alive(owners[key]) + ], + "unknown_owner": [key for key in state["pending"] if key not in owners], + "receipts": list(state["resolutions"].values())[-100:], + "guidance": "Stopped owners are not restarted. Inspect completed effects before creating a new run.", + } + def snapshot(self) -> Envelope: with self._lock: state = self._state() machine = ApprovalStore(last_sequence=int(state.get("sequence", 0))) - for envelope in state.get("pending", {}).values(): - machine.add(validate(envelope)) + for key, envelope in state.get("pending", {}).items(): + owner = state.get("owners", {}).get(key) + if owner is None or self._owner_alive(owner): + machine.add(validate(envelope)) return machine.snapshot(stream=self.stream) def events_after(self, sequence: int) -> list[Envelope]: @@ -409,7 +469,7 @@ def read_decisions() -> None: try: for line in sys.stdin: try: - envelope = json.loads(line) + envelope = validate(json.loads(line)) if envelope.get("type") != "approval.decided": continue decision = envelope["decision"] @@ -423,6 +483,7 @@ def read_decisions() -> None: "authenticated_by": "aais-ndjson-stdio", }, decision_id=str(decision.get("id") or "") or None, + reviewed_digest=str(decision["action_digest"]), ) except Exception as error: # malformed input never grants authority print( diff --git a/src/magent/capability_readiness.py b/src/magent/capability_readiness.py index 487e018..66e6461 100644 --- a/src/magent/capability_readiness.py +++ b/src/magent/capability_readiness.py @@ -3,7 +3,13 @@ from __future__ import annotations import importlib.util +import json +import subprocess from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any + +from magent import __version__ @dataclass(frozen=True) @@ -50,3 +56,54 @@ def readiness_report() -> dict[str, object]: "capabilities": capabilities, "full_install": 'python -m pip install "mag-agent[full]"', } + + +def capability_report() -> dict[str, Any]: + reasons: list[str] = [] + package = importlib.util.find_spec("playwright") is not None + browser = False + if package: + try: + from playwright._impl._driver import compute_driver_executable + + node, cli = compute_driver_executable() + script = ( + "process.stdout.write(require(" + + json.dumps(str(Path(cli).parent)) + + ").chromium.executablePath())" + ) + result = subprocess.run( + [node, "-e", script], capture_output=True, text=True, timeout=3, check=True + ) + browser = Path(result.stdout).is_file() + except Exception: + reasons.append("Browser driver could not be inspected.") + if not package: + reasons.append("Install the webmcp optional dependency pack.") + if not browser: + reasons.append("Install the Playwright Chromium browser.") + try: + from magent.browser import normalize_webmcp_origins + from magent.config import load_config + + origins = normalize_webmcp_origins(load_config().get("webmcp", "origins", default=None)) + except (ValueError, RuntimeError): + origins = () + reasons.append("Configure at least one valid exact HTTPS origin.") + return { + "schema": "agent-runtime.capabilities.v1", + "harness": "magagent", + "version": __version__, + "supported": {"aais": True, "webmcp": True, "oap": "1.0", "ags": "1.0"}, + "ready": {"webmcp": bool(package and browser and origins)}, + "webmcp": { + "package_installed": package, + "browser_installed": browser, + "origins": list(origins), + "registry_status": "not_checked", + "reasons": reasons, + }, + "limits": [ + "Live registry, credentials and profile policy are checked by the harness at execution." + ], + } diff --git a/src/magent/cli/main.py b/src/magent/cli/main.py index 8fee734..d391418 100644 --- a/src/magent/cli/main.py +++ b/src/magent/cli/main.py @@ -410,6 +410,18 @@ def main( _run_repl(username, config, main_provider, extract_provider, cwd, profile=effective_profile) +@app.command("capabilities") +def capabilities_cmd(json_output: bool = typer.Option(True, "--json/--no-json")) -> None: + """Inspect installed runtime capabilities without running a model or opening a site.""" + from magent.capability_readiness import capability_report + + report = capability_report() + if json_output: + console.print_json(data=report) + else: + console.print(report) + + @app.command("ask", rich_help_panel="Everyday Agent Work") def ask_cmd( task: str = typer.Argument(..., help="One-shot task to run non-interactively"), diff --git a/src/magent/config_safety.py b/src/magent/config_safety.py index 3269573..99f9cf8 100644 --- a/src/magent/config_safety.py +++ b/src/magent/config_safety.py @@ -27,7 +27,7 @@ def config_paths(username: str | None = None) -> dict[str, str]: def show_config(username: str | None = None) -> dict[str, Any]: """Return config file paths and text for global/current-user config.""" - result = {"ok": True, "files": {}} + result: dict[str, Any] = {"ok": True, "files": {}} for key, raw_path in config_paths(username).items(): path = Path(raw_path) text = path.read_text(encoding="utf-8") if path.exists() else "" @@ -50,7 +50,7 @@ def redact_config_text(text: str) -> str: key = stripped.split("=", 1)[0].strip().strip('"').strip("'").lower() if any(secret in key for secret in SECRET_KEYS): prefix = line.split("=", 1)[0].rstrip() - lines.append(f"{prefix} = \"***\"") + lines.append(f'{prefix} = "***"') else: lines.append(line) return "\n".join(lines) + ("\n" if text.endswith("\n") else "") @@ -94,7 +94,11 @@ def diff_config(backup_id: str | None = None, username: str | None = None) -> di current = Path(raw_path) old = backup / f"{key}.toml" before = old.read_text(encoding="utf-8").splitlines(keepends=True) if old.exists() else [] - after = current.read_text(encoding="utf-8").splitlines(keepends=True) if current.exists() else [] + after = ( + current.read_text(encoding="utf-8").splitlines(keepends=True) + if current.exists() + else [] + ) diffs[key] = "".join( difflib.unified_diff( before, diff --git a/src/magent/docs/command-reference.md b/src/magent/docs/command-reference.md index 1d89510..a80cac5 100644 --- a/src/magent/docs/command-reference.md +++ b/src/magent/docs/command-reference.md @@ -110,6 +110,7 @@ Generated from the active Typer command tree. - `magent auth` - `magent browser` - `magent cache` +- `magent capabilities` - `magent checkpoint` - `magent ci` - `magent code` diff --git a/src/magent/process_liveness.py b/src/magent/process_liveness.py new file mode 100644 index 0000000..4757e82 --- /dev/null +++ b/src/magent/process_liveness.py @@ -0,0 +1,33 @@ +"""Non-destructive process liveness checks for recovery ownership.""" + +import os +import sys + + +def process_alive(pid: int | None) -> bool: + if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0: + return False + if sys.platform == "win32": + # os.kill(pid, 0) terminates a Windows process. Query its wait state instead. + import ctypes + from ctypes import wintypes + + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + kernel.OpenProcess.restype = wintypes.HANDLE + kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD] + kernel.WaitForSingleObject.restype = wintypes.DWORD + kernel.CloseHandle.argtypes = [wintypes.HANDLE] + kernel.CloseHandle.restype = wintypes.BOOL + handle = kernel.OpenProcess(0x00100000, False, pid) # SYNCHRONIZE only + if not handle: + return False + try: + return kernel.WaitForSingleObject(handle, 0) == 0x00000102 # WAIT_TIMEOUT + finally: + kernel.CloseHandle(handle) + try: + os.kill(pid, 0) + return True + except (OSError, ValueError, OverflowError): + return False diff --git a/src/magent/ui.py b/src/magent/ui.py index c8eeff3..386afe2 100644 --- a/src/magent/ui.py +++ b/src/magent/ui.py @@ -1172,6 +1172,8 @@ def _dispatch(self, method: str) -> None: ) elif parsed.path == "/api/approvals/snapshot": self._json(approval_broker.snapshot()) + elif parsed.path == "/api/approvals/recovery": + self._json(approval_broker.recovery()) elif parsed.path == "/api/approvals/events": after = _int_or(query.get("after", ["0"])[0], 0) self._json({"events": approval_broker.events_after(after)}) @@ -1188,6 +1190,7 @@ def _dispatch(self, method: str) -> None: "authenticated_by": "magent-loopback-token", }, decision_id=str(body.get("decision_id") or "") or None, + reviewed_digest=body.get("action_digest"), ) except Exception as error: # AAIS conflicts are safe client races. self._json({"ok": False, "error": str(error)}, status=409) diff --git a/src/magent/webui/static/assets/index-Dzs2HP-v.js b/src/magent/webui/static/assets/index-BT5XGUGZ.js similarity index 96% rename from src/magent/webui/static/assets/index-Dzs2HP-v.js rename to src/magent/webui/static/assets/index-BT5XGUGZ.js index c9252b0..2bd7058 100644 --- a/src/magent/webui/static/assets/index-Dzs2HP-v.js +++ b/src/magent/webui/static/assets/index-BT5XGUGZ.js @@ -19,8 +19,8 @@ Error generating stack: `+e.message+` fs:write:src/** shell:exec:pytest*`,onChange:e=>F({...P,permissions:e.target.value})})]})]}),(0,N.jsx)(`p`,{className:`context-note`,children:`Saving updates the draft only. Use Save graph above to validate and write it.`}),(0,N.jsxs)(`div`,{className:`graph-card-dialog-actions`,children:[(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:Ke,children:`Delete card`}),(0,N.jsx)(`span`,{}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>F(null),children:`Cancel`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Apply changes`})]})]})})]})}function je(e){let t=String(e.source||``).toLowerCase(),n=String(e.trust||``).toLowerCase();return t===`managed`||n===`managed`?{key:`managed`,label:`Managed`}:t.includes(`/.agentprofiles/`)?{key:`universal`,label:`Universal`}:t.includes(`/.agents/`)?{key:`portable`,label:`Portable`}:t.includes(`/.magent/agents/`)?{key:`project`,label:`Project`}:t.includes(`/.config/magent/`)||t===`user`?{key:`user`,label:`User`}:{key:n||`project`,label:n||`Project`}}var Me={tools:[],skills:[],mcp_servers:[],permission_modes:[`paranoid`,`balanced`,`silent`,`yolo`],network_modes:[`none`,`ask`,`read`,`full`]};function Ne({profiles:e,refresh:t,setError:n,notify:r}){let[i,a]=(0,l.useState)(e[0]?.name??``),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(!1),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(!1),[h,g]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[x,S]=(0,l.useState)(`project`),[C,w]=(0,l.useState)(!1),[T,E]=(0,l.useState)(0),[ee,D]=(0,l.useState)([]),[O,te]=(0,l.useState)(Me),[ne,re]=(0,l.useState)(``),[k,A]=(0,l.useState)({name:``,description:``,instructions:``,provider:``,model:``,tools:[],skills:[],mcp_servers:[],permission_mode:``,network_mode:``,scope:`project`}),ie=(0,l.useRef)(null);(0,l.useEffect)(()=>{if(!C){E(0);return}let e=Date.now(),t=window.setInterval(()=>E(Math.floor((Date.now()-e)/1e3)),1e3);return()=>window.clearInterval(t)},[C]),(0,l.useEffect)(()=>{_(`/api/onboarding/providers`).then(e=>D(e.providers||[])).catch(()=>D([]))},[]),(0,l.useEffect)(()=>{_(`/api/profiles/contract`).then(e=>te({...Me,...e.choices||{}})).catch(()=>te(Me))},[]);async function j(e){try{let t=await _(`/api/profiles/export?name=${encodeURIComponent(e)}`),n=URL.createObjectURL(new Blob([JSON.stringify(t.document,null,2)],{type:`application/json`})),r=window.document.createElement(`a`);r.href=n,r.download=t.filename.replace(/\.md$/,`.json`),r.click(),URL.revokeObjectURL(n)}catch(e){n(e.message)}}async function M(e){try{await v(`/api/profiles/import`,{document:JSON.parse(await e.text()),scope:`project`}),await t(),r(`Profile imported.`)}catch(e){n(String(e).includes(`JSON`)?`That file is not a JSON agent profile. Export one first to see the shape.`:e.message)}}(0,l.useEffect)(()=>{!i&&e[0]&&a(e[0].name)},[e,i]),(0,l.useEffect)(()=>{if(!i)return;let e=!1;return(async()=>{try{let t=await _(`/api/profile?name=${encodeURIComponent(i)}`);if(!e){let e=t.profile||{},r=t.effective_profile||{},i=e.document?.metadata||{};s({...e,...r,description:String(i.description||``)}),n(``)}}catch(t){e||n(t.message)}})(),()=>{e=!0}},[i,n]);async function ae(e){e.preventDefault();try{await v(`/api/profiles`,{...k,...k.provider||k.model?{model:{provider:k.provider||void 0,model:k.model||void 0}}:{},...d?{expected_digest:o?.profile_digest||o?.digest||``}:{},tools:k.tools,skills:k.skills,mcp_servers:k.mcp_servers,permission_mode:k.permission_mode||void 0,network_mode:k.network_mode||void 0,scope:k.scope}),u(!1),A({name:``,description:``,instructions:``,provider:``,model:``,tools:[],skills:[],mcp_servers:[],permission_mode:``,network_mode:``,scope:`project`}),await t(),r(`Profile ${k.name} ${d?`updated`:`created`}.`),a(k.name),f(!1)}catch(e){n(e.message)}}async function oe(e){if(e.preventDefault(),h.trim()){w(!0);try{let e=(await v(`/api/profiles/generate`,{prompt:h.trim(),name:y.trim()})).document||{},t=e.metadata||{},n=e.spec||{},i=n.tools||{},a=e=>(e||[]).map(e=>typeof e==`string`?e:e.name).filter(Boolean);A({name:String(t.name||y),description:String(t.description||``),instructions:String(n.role?.instructions||``),provider:String(n.model?.provider||``),model:String(n.model?.id||``),tools:(i.allow||[]).map(String),skills:a(i.skills||[]),mcp_servers:a(i.mcp_servers||[]),permission_mode:String({deny:`paranoid`,ask:`balanced`,allow:`silent`}[n.permissions?.default]||``),network_mode:String({deny:`none`,ask:`ask`,allow:`full`}[n.permissions?.network]||``),scope:x}),f(!1),m(!1),u(!0),r(`Generated a validated profile draft. Review it before creating the profile.`)}catch(e){n(e.message)}finally{w(!1)}}}let P=o?.permissions,F=!!(o&&o.source!==`managed`);async function I(){if(i)try{let e=(await _(`/api/profiles/export?name=${encodeURIComponent(i)}`)).document||{},t=e.metadata||{},n=e.spec||{},r=n.tools||{},a=e=>(e||[]).map(e=>typeof e==`string`?e:e.name).filter(Boolean);A({name:i,description:String(t.description||o?.description||``),instructions:String(n.role?.instructions||``),provider:String(n.model?.provider||``),model:String(n.model?.id||``),tools:(r.allow||[]).map(String),skills:a(r.skills||[]),mcp_servers:a(r.mcp_servers||[]),permission_mode:String({deny:`paranoid`,ask:`balanced`,allow:`silent`}[n.permissions?.default]||n.permissions?.default||``),network_mode:String({deny:`none`,allow:`read`}[n.permissions?.network]||n.permissions?.network||``),scope:je({source:o?.source,trust:o?.trust}).key===`portable`?`portable`:je({source:o?.source,trust:o?.trust}).key===`universal`?`universal`:je({source:o?.source,trust:o?.trust}).key===`user`?`user`:`project`}),f(!0),u(!0)}catch(e){n(e.message)}}async function se(e){if(e.preventDefault(),!(!i||!ne.trim()))try{await v(`/api/profiles/clone`,{source:i,name:ne.trim(),scope:`project`}),await t(),a(ne.trim()),re(``),r(`Created editable profile @${ne.trim()}.`)}catch(e){n(e.message)}}async function L(){if(!(!i||!o)&&window.confirm(`Delete profile @${i}? Existing conversations keep their transcript but can no longer run with it.`))try{await v(`/api/profiles/delete`,{name:i,expected_digest:o.profile_digest||o.digest||``}),a(``),s(null),await t(),r(`Profile ${i} deleted.`)}catch(e){n(e.message)}}return(0,N.jsxs)(`div`,{className:`page`,children:[(0,N.jsxs)(`div`,{className:`page-header`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`OPEN AGENT PROFILE`}),(0,N.jsx)(`h1`,{children:`Profiles`}),(0,N.jsx)(`p`,{children:`Inspect effective authority or create a reusable specialist.`})]}),(0,N.jsxs)(`div`,{className:`profile-actions`,children:[(0,N.jsx)(`input`,{ref:ie,type:`file`,accept:`.json,application/json`,hidden:!0,onChange:e=>{let t=e.target.files?.[0];t&&M(t),e.target.value=``}}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>ie.current?.click(),children:`↑ Import`}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,disabled:!i,onClick:()=>i&&void j(i),children:`↓ Export`}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>m(!0),children:`✦ Generate profile`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>{f(!1),A({name:``,description:``,instructions:``,provider:``,model:``,tools:[],skills:[],mcp_servers:[],permission_mode:``,network_mode:``,scope:`project`}),u(!0)},children:`+ New profile`})]})]}),(0,N.jsxs)(`div`,{className:`profile-layout`,children:[(0,N.jsxs)(`div`,{className:`profile-list`,children:[(0,N.jsxs)(`div`,{className:`profile-origin-legend`,children:[(0,N.jsx)(`b`,{children:`Profile sources`}),(0,N.jsxs)(`span`,{children:[(0,N.jsx)(`i`,{className:`managed`}),` Managed: built-in, read-only`]}),(0,N.jsxs)(`span`,{children:[(0,N.jsx)(`i`,{className:`project`}),` Project: this workspace's .magent directory`]}),(0,N.jsxs)(`span`,{children:[(0,N.jsx)(`i`,{className:`portable`}),` Portable: this project's .agents directory`]}),(0,N.jsxs)(`span`,{children:[(0,N.jsx)(`i`,{className:`universal`}),` Universal: shared from ~/.agentprofiles`]}),(0,N.jsxs)(`span`,{children:[(0,N.jsx)(`i`,{className:`user`}),` User: MagAgent user configuration`]})]}),e.map(e=>(0,N.jsx)(`button`,{type:`button`,className:`profile-row ${i===e.name?`active`:``}`,onClick:()=>a(e.name),children:(()=>{let t=je(e);return(0,N.jsxs)(`span`,{className:`profile-row-heading`,children:[(0,N.jsxs)(`b`,{children:[`@`,e.name]}),(0,N.jsx)(`small`,{className:`profile-origin ${t.key}`,children:t.label})]})})()},e.name)),!e.length&&(0,N.jsx)(`div`,{className:`empty-small`,children:(0,N.jsx)(`b`,{children:`No profiles`})})]}),(0,N.jsx)(`div`,{className:`detail-card`,children:o?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`PROFILE DETAIL`}),(0,N.jsxs)(`h3`,{children:[`@`,o.name||i]}),(0,N.jsx)(`p`,{children:o.description||`No description.`}),(0,N.jsxs)(`dl`,{children:[(0,N.jsx)(`dt`,{children:`Provider`}),(0,N.jsx)(`dd`,{children:o.provider||`inherit`}),(0,N.jsx)(`dt`,{children:`Model`}),(0,N.jsx)(`dd`,{children:o.model||`inherit`}),(0,N.jsx)(`dt`,{children:`Permission mode`}),(0,N.jsx)(`dd`,{children:o.permission_mode||`inherit`}),(0,N.jsx)(`dt`,{children:`Network`}),(0,N.jsx)(`dd`,{children:o.network_access||`inherit`}),(0,N.jsx)(`dt`,{children:`Digest`}),(0,N.jsx)(`dd`,{className:`mono`,children:(o.profile_digest||o.digest||`—`).slice(0,24)})]}),o.tools?.length?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`TOOLS`}),(0,N.jsx)(`div`,{className:`chip-row`,children:o.tools.map(e=>(0,N.jsx)(`span`,{className:`chip`,children:e},e))})]}):null,o.skills?.length?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`SKILLS`}),(0,N.jsx)(`div`,{className:`chip-row`,children:o.skills.map(e=>(0,N.jsx)(`span`,{className:`chip`,children:e},e))})]}):null,o.mcp_servers?.length?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`MCP SERVERS`}),(0,N.jsx)(`div`,{className:`chip-row`,children:o.mcp_servers.map(e=>(0,N.jsx)(`span`,{className:`chip`,children:e},e))})]}):null,P&&(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`EFFECTIVE AUTHORITY`}),(0,N.jsx)(`div`,{className:`authority-grid`,children:Array.isArray(P)?P.map(e=>(0,N.jsx)(`div`,{children:(0,N.jsx)(`b`,{children:e})},e)):Object.entries(P).map(([e,t])=>(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`small`,{children:e}),(0,N.jsx)(`b`,{children:String(t)})]},e))})]}),(0,N.jsx)(`p`,{className:`context-note`,children:`A profile can narrow the configured tools and permissions, never widen them.`}),F?(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>void I(),children:`Edit profile`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void L(),children:`Delete profile`})]}):(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`p`,{className:`context-note`,children:`Managed profiles are immutable OAP baselines. Make an editable project copy to choose its tools, skills, MCP servers, model, and narrower permissions.`}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>re(`${i}-custom`),children:`Customize as a copy`})]})]}):(0,N.jsx)(`div`,{className:`empty-panel`,children:(0,N.jsx)(`h2`,{children:`Select a profile`})})})]}),c&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`New profile`,onClick:e=>{e.target===e.currentTarget&&u(!1)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:ae,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:d?`EDIT PROFILE`:`NEW PROFILE`}),(0,N.jsx)(`h2`,{children:d?`Edit @${i}`:`Create a specialist`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>{u(!1),f(!1)},"aria-label":`Close`,children:`×`})]}),(0,N.jsx)(`label`,{htmlFor:`profileName`,children:`Name`}),(0,N.jsx)(`input`,{id:`profileName`,required:!0,value:k.name,disabled:d,onChange:e=>A({...k,name:e.target.value})}),(0,N.jsx)(`label`,{htmlFor:`profileDescription`,children:`Description`}),(0,N.jsx)(`input`,{id:`profileDescription`,value:k.description,onChange:e=>A({...k,description:e.target.value})}),(0,N.jsx)(`label`,{htmlFor:`profileInstructions`,children:`Role instructions`}),(0,N.jsx)(`textarea`,{id:`profileInstructions`,rows:4,value:k.instructions,onChange:e=>A({...k,instructions:e.target.value})}),(0,N.jsx)(`label`,{htmlFor:`profileScope`,children:`Save location`}),(0,N.jsxs)(`select`,{id:`profileScope`,value:k.scope,disabled:d,onChange:e=>A({...k,scope:e.target.value}),children:[(0,N.jsx)(`option`,{value:`project`,children:`Project · .magent/agents`}),(0,N.jsx)(`option`,{value:`portable`,children:`Portable with project · .agents`}),(0,N.jsx)(`option`,{value:`universal`,children:`Universal across harnesses · ~/.agentprofiles`}),(0,N.jsx)(`option`,{value:`user`,children:`This MagAgent user · config agents`})]}),d&&(0,N.jsx)(`p`,{className:`context-note`,children:`Editing preserves the existing profile location. Export or clone it to move it to another scope.`}),(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`label`,{htmlFor:`profileProvider`,children:`Provider`}),(0,N.jsxs)(`select`,{id:`profileProvider`,value:k.provider,onChange:e=>{let t=ee.find(t=>(t.name||t.id)===e.target.value);A({...k,provider:e.target.value,model:t?.default_model||k.model})},children:[(0,N.jsx)(`option`,{value:``,children:`Inherit workspace provider`}),ee.map(e=>{let t=e.name||e.id||``;return(0,N.jsx)(`option`,{value:t,children:e.display_name||t},t)})]})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`label`,{htmlFor:`profileModel`,children:`Model`}),(0,N.jsx)(`input`,{id:`profileModel`,list:`profileModels`,placeholder:`inherit`,value:k.model,onChange:e=>A({...k,model:e.target.value})}),(0,N.jsx)(`datalist`,{id:`profileModels`,children:ee.filter(e=>!k.provider||(e.name||e.id)===k.provider).map(e=>(0,N.jsx)(`option`,{value:e.default_model||``},`${e.name}-model`))})]})]}),(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`label`,{htmlFor:`profilePermission`,children:`Permission ceiling`}),(0,N.jsxs)(`select`,{id:`profilePermission`,value:k.permission_mode,onChange:e=>A({...k,permission_mode:e.target.value}),children:[(0,N.jsx)(`option`,{value:``,children:`Inherit workspace mode`}),O.permission_modes.map(e=>(0,N.jsx)(`option`,{value:e,children:e},e))]})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`label`,{htmlFor:`profileNetwork`,children:`Network ceiling`}),(0,N.jsxs)(`select`,{id:`profileNetwork`,value:k.network_mode,onChange:e=>A({...k,network_mode:e.target.value}),children:[(0,N.jsx)(`option`,{value:``,children:`Inherit workspace network`}),O.network_modes.map(e=>(0,N.jsx)(`option`,{value:e,children:e},e))]})]})]}),(0,N.jsxs)(`fieldset`,{className:`capability-picker`,children:[(0,N.jsx)(`legend`,{children:`Allowed tools`}),(0,N.jsx)(`p`,{children:`Unchecked tools are unavailable to this profile. Selecting none inherits the harness tool set.`}),(0,N.jsx)(`div`,{children:O.tools.map(e=>(0,N.jsxs)(`label`,{children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:k.tools.includes(e.name),onChange:t=>A({...k,tools:t.target.checked?[...k.tools,e.name]:k.tools.filter(t=>t!==e.name)})}),(0,N.jsxs)(`span`,{children:[(0,N.jsx)(`b`,{children:e.name}),(0,N.jsx)(`small`,{children:e.description})]})]},e.name))})]}),(O.skills.length>0||O.mcp_servers.length>0)&&(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`fieldset`,{className:`compact-picker`,children:[(0,N.jsx)(`legend`,{children:`Skills`}),O.skills.map(e=>(0,N.jsxs)(`label`,{children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:k.skills.includes(e.name),onChange:t=>A({...k,skills:t.target.checked?[...k.skills,e.name]:k.skills.filter(t=>t!==e.name)})}),e.name]},e.name))]}),(0,N.jsxs)(`fieldset`,{className:`compact-picker`,children:[(0,N.jsx)(`legend`,{children:`MCP servers`}),O.mcp_servers.map(e=>(0,N.jsxs)(`label`,{children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:k.mcp_servers.includes(e),onChange:t=>A({...k,mcp_servers:t.target.checked?[...k.mcp_servers,e]:k.mcp_servers.filter(t=>t!==e)})}),e]},e))]})]}),(0,N.jsx)(`p`,{className:`context-note`,children:`Leave both blank to inherit the workspace route. A profile can narrow authority, never widen it.`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:d?`Save changes`:`Create profile`})]})}),p&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Generate profile`,onClick:e=>{e.target===e.currentTarget&&!C&&m(!1)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:oe,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`OAP PROFILE AUTHOR`}),(0,N.jsx)(`h2`,{children:`Generate a specialist`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,disabled:C,onClick:()=>m(!1),"aria-label":`Close`,children:`×`})]}),(0,N.jsx)(`p`,{children:`Describe who the agent should be, when to use it, and any limits it needs. MagAgent will only reference locally available capabilities and will open the validated result for review.`}),(0,N.jsx)(`label`,{htmlFor:`generationPrompt`,children:`What should this agent do?`}),(0,N.jsx)(`textarea`,{id:`generationPrompt`,required:!0,rows:7,value:h,disabled:C,onChange:e=>g(e.target.value),placeholder:`Create a cautious accessibility reviewer that inspects frontend changes, explains WCAG issues, and never edits files without asking…`}),(0,N.jsxs)(`label`,{htmlFor:`generationName`,children:[`Preferred name `,(0,N.jsx)(`small`,{children:`(optional)`})]}),(0,N.jsx)(`input`,{id:`generationName`,pattern:`[a-z0-9][a-z0-9._-]*`,value:y,disabled:C,onChange:e=>b(e.target.value),placeholder:`accessibility-reviewer`}),(0,N.jsx)(`label`,{htmlFor:`generationScope`,children:`Where should it be saved after review?`}),(0,N.jsxs)(`select`,{id:`generationScope`,value:x,disabled:C,onChange:e=>S(e.target.value),children:[(0,N.jsx)(`option`,{value:`project`,children:`Project · .magent/agents`}),(0,N.jsx)(`option`,{value:`portable`,children:`Portable with project · .agents`}),(0,N.jsx)(`option`,{value:`universal`,children:`Universal across harnesses · ~/.agentprofiles`}),(0,N.jsx)(`option`,{value:`user`,children:`This MagAgent user`})]}),(0,N.jsx)(`p`,{className:`context-note`,children:`Generation creates a draft, not authority. You review the complete OAP profile and effective permissions before it is saved.`}),C&&(0,N.jsxs)(`div`,{className:`operation-health`,role:`status`,"aria-live":`polite`,children:[(0,N.jsx)(`span`,{className:`operation-spinner`,"aria-hidden":`true`}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`b`,{children:`Authoring and validating the profile`}),(0,N.jsx)(`p`,{children:`The model is matching your prompt to locally available tools, skills, and policy.`})]}),(0,N.jsxs)(`span`,{children:[T,`s`]})]}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,disabled:C,children:C?`Generating validated draft…`:`Generate validated draft`})]})}),ne&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Customize managed profile`,onClick:e=>{e.target===e.currentTarget&&re(``)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:se,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`EDITABLE COPY`}),(0,N.jsxs)(`h2`,{children:[`Customize @`,i]})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>re(``),children:`×`})]}),(0,N.jsx)(`p`,{children:`The managed original stays intact. This project copy starts with the same OAP policy and can be narrowed independently.`}),(0,N.jsxs)(`label`,{children:[`New profile name`,(0,N.jsx)(`input`,{required:!0,pattern:`[a-z0-9][a-z0-9._-]*`,value:ne,onChange:e=>re(e.target.value)})]}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Create editable copy`})]})})]})}var Pe=[`keyword`,`hybrid`,`semantic`];function Fe(e){let t=Number(e||0);if(!t)return`0 B`;let n=[`B`,`KB`,`MB`],r=Math.min(n.length-1,Math.floor(Math.log(t)/Math.log(1024)));return`${(t/1024**r).toFixed(+!!r)} ${n[r]}`}function Ie({setError:e,notify:t}){let[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(`keyword`),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(null),[h,g]=(0,l.useState)(!1),[y,b]=(0,l.useState)({id:``,type:`fact`,body:``,links:``}),x=(0,l.useCallback)(async()=>{try{r(await _(`/api/memory/overview`))}catch(t){e(t.message)}},[e]);(0,l.useEffect)(()=>{x()},[x]);async function S(){if(!i.trim()){u(null);return}f(!0);try{let e=await _(`/api/memory/search?q=${encodeURIComponent(i)}&mode=${o}`);u(e.results||[])}catch(t){e(t.message)}finally{f(!1)}}async function C(t){try{let n=await _(`/api/memory/node?id=${encodeURIComponent(t)}`);if(!n.ok){e(n.error||`That memory node could not be read.`);return}m(n)}catch(t){e(t.message)}}function w(){b({id:``,type:`fact`,body:``,links:``}),g(!0)}function T(){p&&(b({id:p.id||``,type:p.type||`fact`,body:p.body||``,links:(p.links||[]).join(`, `)}),g(!0))}async function E(n){n.preventDefault();let r=!!(p&&p.id===y.id);try{let e=await v(`/api/memory/nodes`,{action:r?`update`:`create`,id:y.id,type:y.type,body:y.body,links:y.links.split(`,`).map(e=>e.trim()).filter(Boolean)});g(!1),m(e),await x(),t(`Memory ${r?`updated`:`created`}.`)}catch(t){e(t.message)}}async function ee(){if(!(!p?.id||!window.confirm(`Delete memory “${p.id}”? This cannot be undone.`)))try{await v(`/api/memory/nodes`,{action:`delete`,id:p.id}),m(null),await x(),t(`Memory deleted.`)}catch(t){e(t.message)}}let D=c??n?.nodes??[],O=n?.stats||{};return(0,N.jsxs)(`div`,{className:`page-view view active memory-page`,children:[(0,N.jsxs)(`div`,{className:`page-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{className:`eyebrow`,children:`KNOWLEDGE`}),(0,N.jsx)(`h1`,{children:`Memory`}),(0,N.jsx)(`p`,{children:`Create, review, and maintain the durable facts that shape agent replies.`})]}),(0,N.jsxs)(`div`,{className:`header-actions`,children:[(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,onClick:()=>void x(),children:`Refresh`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:w,children:`+ New memory`})]})]}),n?.error&&(0,N.jsx)(`div`,{className:`graph-error`,role:`alert`,children:n.error}),n&&n.available===!1?(0,N.jsx)(`p`,{className:`memory-empty`,children:n.note}):(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`div`,{className:`memory-stats`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{children:`NODES`}),(0,N.jsx)(`strong`,{children:String(O.nodes??`—`)})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{children:`LINKS`}),(0,N.jsx)(`strong`,{children:String(O.edges_total??`—`)})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{children:`ON DISK`}),(0,N.jsx)(`strong`,{children:Fe(O.disk_bytes)})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{children:`DUPLICATE GROUPS`}),(0,N.jsx)(`strong`,{children:String(n?.quality?.duplicate_groups??`—`)})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{children:`SUPPRESSED`}),(0,N.jsx)(`strong`,{children:String(n?.quality?.suppressed?.length??0)})]})]}),(0,N.jsxs)(`form`,{className:`memory-search`,onSubmit:e=>{e.preventDefault(),S()},children:[(0,N.jsx)(`label`,{className:`sr-only`,htmlFor:`memoryQuery`,children:`Search memory`}),(0,N.jsx)(`input`,{id:`memoryQuery`,value:i,placeholder:`Search what the agent remembers…`,onChange:e=>a(e.target.value)}),(0,N.jsx)(`label`,{className:`sr-only`,htmlFor:`memoryMode`,children:`Search mode`}),(0,N.jsx)(`select`,{id:`memoryMode`,value:o,onChange:e=>s(e.target.value),children:Pe.map(e=>(0,N.jsx)(`option`,{value:e,children:e},e))}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,disabled:d,children:d?`Searching…`:`Search`}),c&&(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,onClick:()=>{u(null),a(``)},children:`Clear`})]}),(0,N.jsx)(`p`,{className:`memory-count`,children:c?`${c.length} match${c.length===1?``:`es`} for “${i}”`:n?.note||`${D.length} of ${n?.total??D.length} nodes`}),(0,N.jsxs)(`div`,{className:`memory-layout`,children:[(0,N.jsxs)(`div`,{className:`memory-list`,children:[D.length===0&&(0,N.jsx)(`p`,{className:`memory-empty`,children:`Nothing to show.`}),D.map(e=>(0,N.jsxs)(`button`,{type:`button`,className:`memory-row ${p?.id===e.id?`active`:``}`,onClick:()=>void C(e.id),children:[(0,N.jsx)(`strong`,{children:e.id}),(0,N.jsx)(`span`,{className:`tag`,children:e.type||`note`}),(0,N.jsx)(`small`,{children:e.excerpt})]},e.id))]}),(0,N.jsx)(`div`,{className:`memory-detail`,children:p?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`span`,{className:`eyebrow`,children:p.type||`note`}),(0,N.jsx)(`h3`,{children:p.id}),(0,N.jsx)(`p`,{className:`memory-path`,children:p.path}),(0,N.jsx)(`pre`,{children:p.body}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:T,children:`Edit`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void ee(),children:`Delete`})]}),p.truncated&&(0,N.jsx)(`p`,{className:`memory-empty`,children:`This note is longer than the browser shows. Read the file for the rest.`}),(0,N.jsxs)(`div`,{className:`memory-links`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{className:`eyebrow`,children:`LINKS OUT`}),p.links?.length?p.links.map(e=>(0,N.jsx)(`button`,{type:`button`,className:`link-chip`,onClick:()=>void C(e),children:e},e)):(0,N.jsx)(`p`,{className:`memory-empty`,children:`None.`})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`span`,{className:`eyebrow`,children:`LINKS IN`}),p.backlinks?.length?p.backlinks.map(e=>(0,N.jsx)(`button`,{type:`button`,className:`link-chip`,onClick:()=>void C(e),children:e},e)):(0,N.jsx)(`p`,{className:`memory-empty`,children:`Nothing refers to this note.`})]})]})]}):(0,N.jsx)(`p`,{className:`memory-empty`,children:`Choose a note to read it and see what links to it.`})})]})]}),h&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Memory editor`,onClick:e=>{e.target===e.currentTarget&&g(!1)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:E,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`DURABLE MEMORY`}),(0,N.jsx)(`h2`,{children:p?.id===y.id&&y.id?`Edit memory`:`Create memory`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>g(!1),children:`×`})]}),(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`label`,{htmlFor:`memoryId`,children:`ID`}),(0,N.jsx)(`input`,{id:`memoryId`,required:!0,disabled:p?.id===y.id&&!!y.id,value:y.id,onChange:e=>b({...y,id:e.target.value})})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`label`,{htmlFor:`memoryType`,children:`Type`}),(0,N.jsxs)(`select`,{id:`memoryType`,value:y.type,onChange:e=>b({...y,type:e.target.value}),children:[(0,N.jsx)(`option`,{value:`fact`,children:`Fact`}),(0,N.jsx)(`option`,{value:`preference`,children:`Preference`}),(0,N.jsx)(`option`,{value:`project`,children:`Project`}),(0,N.jsx)(`option`,{value:`decision`,children:`Decision`}),(0,N.jsx)(`option`,{value:`bookmark`,children:`Bookmark`})]})]})]}),(0,N.jsx)(`label`,{htmlFor:`memoryBody`,children:`Body`}),(0,N.jsx)(`textarea`,{id:`memoryBody`,rows:9,required:!0,value:y.body,onChange:e=>b({...y,body:e.target.value})}),(0,N.jsxs)(`label`,{htmlFor:`memoryLinks`,children:[`Links `,(0,N.jsx)(`small`,{children:`comma-separated memory IDs`})]}),(0,N.jsx)(`input`,{id:`memoryLinks`,value:y.links,onChange:e=>b({...y,links:e.target.value})}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Save memory`})]})})]})}function Le({fields:e,profiles:t,refresh:n,setError:r,notify:i}){let[a,o]=(0,l.useState)(``),[s,c]=(0,l.useState)([]),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(``),[g,y]=(0,l.useState)(`keyring`),[b,x]=(0,l.useState)([]);(0,l.useEffect)(()=>{_(`/api/onboarding/providers`).then(e=>{c(e.providers||[]),d(e.default_provider||``),p(e.default_model||``),x(e.image_models||[])}).catch(()=>c([]))},[]);async function S(e){if(e.preventDefault(),u){o(`provider-setup`);try{await v(`/api/onboarding/configure`,{provider:u,model:f,credential:m,credential_storage:g}),h(``),await n(),i(`Provider configuration saved.`)}catch(e){r(e.message)}finally{o(``)}}}async function C(e,t){o(e.path);try{await v(`/api/settings`,{path:e.path,value:t}),await n(),i(`${e.label} saved.`)}catch(e){r(e.message)}finally{o(``)}}return(0,N.jsxs)(`div`,{className:`page`,children:[(0,N.jsx)(`div`,{className:`page-header`,children:(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`GUIDED CONFIGURATION`}),(0,N.jsx)(`h1`,{children:`Settings`}),(0,N.jsx)(`p`,{children:`Safe defaults only. Credentials stay in your environment or the OS keyring.`})]})}),(0,N.jsxs)(`form`,{className:`provider-setup-card`,onSubmit:S,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`PROVIDER ONBOARDING`}),(0,N.jsx)(`h2`,{children:`Configure model access`}),(0,N.jsx)(`p`,{children:`Choose the workspace default and optionally store its credential. Existing credentials are never displayed.`})]}),(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`label`,{children:[`Provider`,(0,N.jsxs)(`select`,{value:u,onChange:e=>{let t=e.target.value,n=s.find(e=>e.name===t);d(t),p(n?.default_model||``)},children:[(0,N.jsx)(`option`,{value:``,children:`Choose a provider…`}),s.map(e=>(0,N.jsxs)(`option`,{value:e.name,children:[e.display_name||e.name,e.configured?` · configured`:``]},e.name))]})]}),(0,N.jsxs)(`label`,{children:[`Default model`,(0,N.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:s.find(e=>e.name===u)?.default_model||`provider default`})]})]}),u&&s.find(e=>e.name===u)?.needs_key&&(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`API key `,(0,N.jsx)(`small`,{children:`(leave blank to keep the existing credential)`}),(0,N.jsx)(`input`,{type:`password`,autoComplete:`off`,value:m,onChange:e=>h(e.target.value),placeholder:s.find(e=>e.name===u)?.credential_ready?`Credential already configured`:`Paste provider API key`})]}),(0,N.jsxs)(`label`,{children:[`Credential storage`,(0,N.jsxs)(`select`,{value:g,onChange:e=>y(e.target.value),children:[(0,N.jsx)(`option`,{value:`keyring`,children:`OS keyring (recommended)`}),(0,N.jsx)(`option`,{value:`config`,children:`MagAgent config file`})]})]}),g===`config`&&(0,N.jsx)(`p`,{className:`credential-warning`,children:`The key will be written to MagAgent's user config. File permissions are tightened, but the OS keyring is safer when available.`})]}),(0,N.jsxs)(`div`,{className:`provider-setup-actions`,children:[(0,N.jsx)(`span`,{children:s.find(e=>e.name===u)?.credential_reason||`Select a provider to inspect readiness.`}),(0,N.jsx)(`button`,{className:`primary-button`,disabled:!u||a===`provider-setup`,children:a===`provider-setup`?`Saving…`:`Save provider`})]})]}),e.length?(0,N.jsx)(`div`,{className:`setting-list`,children:e.map(e=>(0,N.jsxs)(`article`,{className:`setting-card`,children:[(0,N.jsxs)(`label`,{htmlFor:`setting-${e.path}`,children:[(0,N.jsx)(`b`,{children:e.label}),e.description&&(0,N.jsx)(`small`,{children:e.description})]}),e.path===`models.image_maker`?(0,N.jsxs)(`select`,{id:`setting-${e.path}`,value:String(e.value??``),disabled:a===e.path,onChange:t=>void C(e,t.target.value),children:[e.value&&!b.some(t=>t.value===e.value)&&(0,N.jsxs)(`option`,{value:String(e.value),children:[String(e.value),` · current custom route`]}),b.some(e=>e.available)?(0,N.jsx)(`option`,{value:``,children:`Not configured`}):(0,N.jsx)(`option`,{value:``,children:`No image models detected — configure a supported provider`}),b.map(e=>(0,N.jsxs)(`option`,{value:e.value,disabled:!e.available,children:[e.label,e.available?``:` · provider unavailable`]},e.id))]}):e.path===`defaults.provider`?(0,N.jsxs)(`select`,{id:`setting-${e.path}`,value:String(e.value??``),disabled:a===e.path,onChange:t=>void C(e,t.target.value),children:[e.value&&!s.some(t=>t.name===e.value)&&(0,N.jsx)(`option`,{value:String(e.value),children:String(e.value)}),(0,N.jsx)(`option`,{value:``,children:`Choose a provider…`}),s.map(e=>(0,N.jsx)(`option`,{value:e.name,children:e.display_name||e.name},e.name))]}):e.path===`defaults.model`?(0,N.jsxs)(`select`,{id:`setting-${e.path}`,value:String(e.value??``),disabled:a===e.path,onChange:t=>void C(e,t.target.value),children:[e.value&&!s.some(t=>t.default_model===e.value)&&(0,N.jsx)(`option`,{value:String(e.value),children:String(e.value)}),(0,N.jsx)(`option`,{value:``,children:`Use provider default`}),Array.from(new Set(s.map(e=>e.default_model).filter(Boolean))).map(e=>(0,N.jsx)(`option`,{value:e,children:e},e))]}):e.path===`agent_profiles.default_profile`?(0,N.jsxs)(`select`,{id:`setting-${e.path}`,value:String(e.value??``),disabled:a===e.path,onChange:t=>void C(e,t.target.value),children:[(0,N.jsx)(`option`,{value:``,children:`No default profile`}),t.map(e=>(0,N.jsxs)(`option`,{value:e.name,children:[`@`,e.name]},e.name))]}):e.choices?.length?(0,N.jsx)(`select`,{id:`setting-${e.path}`,value:String(e.value??``),disabled:a===e.path,onChange:t=>void C(e,t.target.value),children:e.choices.map(e=>(0,N.jsx)(`option`,{value:e,children:e},e))}):e.type===`boolean`?(0,N.jsx)(`input`,{id:`setting-${e.path}`,type:`checkbox`,checked:!!e.value,disabled:a===e.path,onChange:t=>void C(e,t.target.checked)}):(0,N.jsx)(`input`,{id:`setting-${e.path}`,type:e.type===`integer`?`number`:`text`,defaultValue:String(e.value??``),disabled:a===e.path,onBlur:t=>{t.target.value!==String(e.value??``)&&C(e,e.type===`integer`?Number(t.target.value):t.target.value)}})]},e.path))}):(0,N.jsxs)(`div`,{className:`empty-panel`,children:[(0,N.jsx)(`h2`,{children:`Nothing editable here`}),(0,N.jsxs)(`p`,{children:[`Run `,(0,N.jsx)(`code`,{children:`magent configure`}),` for the full wizard.`]})]})]})}function Re({title:e,value:t}){let n=t==null?`—`:typeof t==`object`?JSON.stringify(t,null,2):String(t),r=n.includes(` `)||n.length>60;return(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:e}),r?(0,N.jsx)(`pre`,{children:n}):(0,N.jsx)(`b`,{children:n})]})}function ze({setError:e}){let[t,n]=(0,l.useState)(null),[r,i]=(0,l.useState)(!0);async function a(){i(!0);try{let[e,t]=await Promise.all([_(`/api/cockpit`),_(`/api/state`)]);n({...t,...e})}catch(t){e(t.message)}finally{i(!1)}}(0,l.useEffect)(()=>{a()},[]);let o=Object.entries(t||{}).filter(([e])=>e!==`ok`&&e!==`error`);return(0,N.jsxs)(`div`,{className:`page`,children:[(0,N.jsxs)(`div`,{className:`page-header`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`LOCAL DIAGNOSTICS`}),(0,N.jsx)(`h1`,{children:`Operations`}),(0,N.jsx)(`p`,{children:`Workspace health, plans, patches, models, and recent activity.`})]}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>void a(),disabled:r,children:r?`Refreshing…`:`Refresh`})]}),o.length?(0,N.jsx)(`div`,{className:`ops-grid`,children:o.map(([e,t])=>(0,N.jsx)(Re,{title:e.replace(/_/g,` `),value:t},e))}):(0,N.jsx)(`div`,{className:`empty-panel`,children:(0,N.jsx)(`h2`,{children:r?`Loading…`:`Nothing to report`})})]})}function Be({selected:e,setSelected:t,activeConversation:n,setError:r,notify:i}){let[a,o]=(0,l.useState)([]),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(null),[f,p]=(0,l.useState)({}),[m,h]=(0,l.useState)(``),[g,y]=(0,l.useState)(`git status --short`),[b,x]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[w,T]=(0,l.useState)(``),[E,ee]=(0,l.useState)(``);async function D(){try{let[e,t]=await Promise.all([_(`/api/workspace/files?q=${encodeURIComponent(s)}`),_(`/api/workspace/git`)]);o(e.files||[]),p(t)}catch(e){r(e.message)}}(0,l.useEffect)(()=>{D()},[]);let O=(0,l.useMemo)(()=>a.filter(e=>e.artifact),[a]);async function te(e){try{d(await _(`/api/workspace/file?path=${encodeURIComponent(e)}`))}catch(e){r(e.message)}}async function ne(e){try{let t=await _(`/api/workspace/diff?staged=${e}`);h(t.diff||`No changes in this view.`)}catch(e){r(e.message)}}async function re(a){if(a.size>5242880){r(`Uploads are limited to 5 MB.`);return}let o=await new Promise((e,t)=>{let n=new FileReader;n.onerror=()=>t(n.error),n.onload=()=>e(String(n.result).split(`,`,2)[1]||``),n.readAsDataURL(a)});try{let r=await v(`/api/workspace/upload`,{name:a.name,data:o,conversation_id:n||`shared`});t([...new Set([...e,r.file.path])].slice(0,20)),i(`${a.name} attached`),await D()}catch(e){r(e.message)}}async function k(){C(!0);try{let e=await v(`/api/workspace/terminal`,{command:g});x(`${e.stdout||``}${e.stderr||``}`||`Exited ${e.returncode}`)}catch(e){r(e.message)}finally{C(!1)}}async function A(e,t){if(!(e===`discard`&&!window.confirm(`Discard uncommitted changes in ${t}? This cannot be undone.`)))try{await v(`/api/workspace/git`,{action:e,path:t}),i(`${e} completed for ${t}`),await D()}catch(e){r(e.message)}}async function ie(){try{await v(`/api/workspace/worktrees`,{action:`create`,branch:w,directory:E,create_branch:!0}),i(`Worktree created for ${w}`),T(``),ee(``),await D()}catch(e){r(e.message)}}async function j(e){if(window.confirm(`Remove the Git worktree at ${e}? Uncommitted worktrees are refused by Git.`))try{await v(`/api/workspace/worktrees`,{action:`remove`,directory:e}),i(`Worktree removed`),await D()}catch(e){r(e.message)}}function M(e){let t=e.slice(3).trim();return t.includes(` -> `)&&t.split(` -> `).pop()||t}return(0,N.jsxs)(`section`,{className:`view active page-view`,children:[(0,N.jsxs)(`div`,{className:`page-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`PROJECT CONTEXT`}),(0,N.jsx)(`h1`,{children:`Workspace`}),(0,N.jsx)(`p`,{children:`Inspect files and artifacts, attach context, review Git, and run explicit local commands.`})]}),(0,N.jsxs)(`label`,{className:`upload-button`,children:[`Attach file`,(0,N.jsx)(`input`,{type:`file`,onChange:e=>e.target.files?.[0]&&void re(e.target.files[0])})]})]}),(0,N.jsxs)(`div`,{className:`workspace-tools`,children:[(0,N.jsxs)(`div`,{className:`detail-card workspace-browser`,children:[(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`input`,{"aria-label":`Search workspace files`,value:s,onChange:e=>c(e.target.value),placeholder:`Filter files`}),(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,onClick:()=>void D(),children:`Search`})]}),(0,N.jsxs)(`p`,{className:`selection-note`,children:[e.length,`/20 files attached to the next message`]}),(0,N.jsx)(`div`,{className:`file-list`,children:a.map(n=>(0,N.jsxs)(`div`,{className:`file-row`,children:[(0,N.jsx)(`input`,{type:`checkbox`,"aria-label":`Attach ${n.path}`,checked:e.includes(n.path),onChange:()=>t(e.includes(n.path)?e.filter(e=>e!==n.path):[...e,n.path].slice(0,20))}),(0,N.jsx)(`button`,{type:`button`,onClick:()=>void te(n.path),children:n.path}),(0,N.jsxs)(`small`,{children:[Math.ceil(n.size/1024),` KB`]})]},n.path))})]}),(0,N.jsxs)(`article`,{className:`detail-card file-preview`,children:[(0,N.jsx)(`h2`,{children:u?.path||`Preview`}),!u&&(0,N.jsx)(`p`,{children:`Select a file to inspect it without leaving MagAgent.`}),u?.text&&(0,N.jsx)(`pre`,{children:u.content}),u?.data_url&&u.mime.startsWith(`image/`)&&(0,N.jsx)(`img`,{src:u.data_url,alt:u.path}),u?.data_url&&!u.mime.startsWith(`image/`)&&(0,N.jsxs)(`p`,{children:[`Binary preview ready (`,u.mime,`).`]})]})]}),(0,N.jsxs)(`div`,{className:`ops-grid`,children:[(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Git status`}),(0,N.jsx)(`div`,{className:`git-file-list`,children:(f.status||[]).filter(e=>!e.startsWith(`##`)).map(e=>{let t=M(e);return(0,N.jsxs)(`div`,{className:`git-file-row`,children:[(0,N.jsx)(`code`,{children:e}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`button`,{type:`button`,onClick:()=>void A(`stage`,t),children:`Stage`}),(0,N.jsx)(`button`,{type:`button`,onClick:()=>void A(`unstage`,t),children:`Unstage`}),(0,N.jsx)(`button`,{className:`danger-link`,type:`button`,onClick:()=>void A(`discard`,t),children:`Discard`})]})]},e)})}),!(f.status||[]).filter(e=>!e.startsWith(`##`)).length&&(0,N.jsx)(`p`,{children:`Clean or not a Git repository.`}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,onClick:()=>void ne(!1),children:`Working diff`}),(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,onClick:()=>void ne(!0),children:`Staged diff`})]})]}),(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Branches & worktrees`}),(0,N.jsx)(`pre`,{children:(f.branches||[]).join(` -`)}),(f.worktrees||[]).map(e=>(0,N.jsxs)(`div`,{className:`git-file-row`,children:[(0,N.jsxs)(`code`,{children:[e.worktree,` `,e.branch||``]}),e.worktree&&e.current!==`true`&&(0,N.jsx)(`div`,{children:(0,N.jsx)(`button`,{className:`danger-link`,type:`button`,onClick:()=>void j(e.worktree),children:`Remove`})})]},e.worktree)),(0,N.jsxs)(`div`,{className:`worktree-form`,children:[(0,N.jsx)(`input`,{"aria-label":`New worktree branch`,value:w,onChange:e=>T(e.target.value),placeholder:`feature/name`}),(0,N.jsx)(`input`,{"aria-label":`New worktree directory`,value:E,onChange:e=>ee(e.target.value),placeholder:`../project-feature`}),(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,disabled:!w||!E,onClick:()=>void ie(),children:`Create branch + worktree`})]})]}),(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Artifacts`}),(0,N.jsxs)(`p`,{children:[O.length,` previewable project files discovered.`]}),(0,N.jsx)(`div`,{className:`artifact-links`,children:O.slice(0,12).map(e=>(0,N.jsx)(`button`,{type:`button`,onClick:()=>void te(e.path),children:e.name},e.path))})]})]}),m&&(0,N.jsx)(`pre`,{className:`diff-view`,children:m}),(0,N.jsxs)(`article`,{className:`detail-card terminal-card`,children:[(0,N.jsx)(`h2`,{children:`Command console`}),(0,N.jsx)(`p`,{children:`Commands run as an argument list in the selected workspace, with no shell expansion and a 60-second limit.`}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`input`,{"aria-label":`Workspace command`,value:g,onChange:e=>y(e.target.value),onKeyDown:e=>{e.key===`Enter`&&k()}}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,disabled:S,onClick:()=>void k(),children:S?`Running…`:`Run`})]}),b&&(0,N.jsx)(`pre`,{children:b})]})]})}function Ve({setError:e,notify:t}){let[n,r]=(0,l.useState)({}),[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(60),[c,u]=(0,l.useState)(()=>localStorage.getItem(`magent-notifications`)===`on`),[d,f]=(0,l.useState)([]),p=(0,l.useRef)({});async function m(){try{let[e,t]=await Promise.all([_(`/api/run-center`),_(`/api/tasks`)]);if(r(e),f(t.tasks||[]),c&&`Notification`in window&&Notification.permission===`granted`){let n=[...e.chat_runs||[],...e.graph_runs||[],...t.tasks||[]];for(let e of n){let t=e,n=String(t.id||t.run_id||t.job_id||t.task_id||``),r=String(t.state||t.status||``),i=p.current[n];n&&i&&i!==r&&[`succeeded`,`failed`,`cancelled`,`completed`].includes(r)&&new Notification(`MagAgent ${r}`,{body:String(t.title||t.summary||n)}),n&&(p.current[n]=r)}}}catch(t){e(t.message)}}(0,l.useEffect)(()=>{m();let e=window.setInterval(()=>void m(),5e3);return()=>window.clearInterval(e)},[c]);async function h(){try{await v(`/api/schedules`,{path:i,interval_minutes:o}),a(``),t(`Graph schedule created`),await m()}catch(t){e(t.message)}}async function g(t,n){try{await v(`/api/schedules/action`,{id:t,action:n}),await m()}catch(t){e(t.message)}}async function y(n,r){let i=n.id||n.task_id||``;try{await v(`/api/tasks/action`,{task_id:i,action:r}),t(`Task ${r} requested`),await m()}catch(t){e(t.message)}}async function b(){if(!(`Notification`in window)){e(`This browser does not support desktop notifications.`);return}let t=!c&&await Notification.requestPermission()===`granted`;u(t),localStorage.setItem(`magent-notifications`,t?`on`:`off`)}return(0,N.jsxs)(`section`,{className:`view active page-view`,children:[(0,N.jsxs)(`div`,{className:`page-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`BACKGROUND WORK`}),(0,N.jsx)(`h1`,{children:`Run center`}),(0,N.jsx)(`p`,{children:`Monitor chat and graph execution, manage durable tasks, and schedule governed graphs.`})]}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>void b(),children:c?`Notifications on`:`Enable notifications`})]}),(0,N.jsxs)(`div`,{className:`ops-grid`,children:[(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Chat runs`}),(0,N.jsx)(`pre`,{children:JSON.stringify(n.chat_runs||[],null,2)})]}),(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Graph runs`}),(0,N.jsx)(`pre`,{children:JSON.stringify(n.graph_runs||[],null,2)})]})]}),(0,N.jsxs)(`article`,{className:`detail-card schedule-card`,children:[(0,N.jsx)(`h2`,{children:`Durable tasks`}),!d.length&&(0,N.jsx)(`p`,{children:`No durable execution tasks yet.`}),(0,N.jsx)(`div`,{className:`schedule-list`,children:d.map(e=>{let t=e.id||e.task_id||``,n=e.state||e.status||`unknown`;return(0,N.jsxs)(`div`,{className:`schedule-row`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`b`,{children:e.title||t}),(0,N.jsxs)(`small`,{children:[n,e.summary?` · ${e.summary}`:``]})]}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[n===`running`&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`pause`),children:`Pause`}),n===`paused`&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`resume`),children:`Resume`}),![`succeeded`,`cancelled`].includes(n)&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`cancel`),children:`Cancel`}),[`failed`,`cancelled`].includes(n)&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`retry`),children:`Retry`})]})]},t)})})]}),(0,N.jsxs)(`article`,{className:`detail-card schedule-card`,children:[(0,N.jsx)(`h2`,{children:`Schedule a graph`}),(0,N.jsx)(`p`,{children:`Schedules run only while this local UI server is active and still pass graph validation and human-gate checks.`}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`input`,{"aria-label":`Graph path`,value:i,onChange:e=>a(e.target.value),placeholder:`workflows/review.agraph.yaml`}),(0,N.jsx)(`input`,{"aria-label":`Interval minutes`,type:`number`,min:1,value:o,onChange:e=>s(Number(e.target.value))}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,disabled:!i,onClick:()=>void h(),children:`Schedule`})]}),(0,N.jsx)(`div`,{className:`schedule-list`,children:(n.schedules||[]).map(e=>(0,N.jsxs)(`div`,{className:`schedule-row`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`b`,{children:e.path}),(0,N.jsxs)(`small`,{children:[`Every `,e.interval_minutes,` min · `,e.status,e.last_error?` · ${e.last_error}`:``]})]}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`button`,{type:`button`,onClick:()=>void g(e.id,`run`),children:`Run now`}),(0,N.jsx)(`button`,{type:`button`,onClick:()=>void g(e.id,e.status===`active`?`pause`:`resume`),children:e.status===`active`?`Pause`:`Resume`}),(0,N.jsx)(`button`,{type:`button`,onClick:()=>void g(e.id,`delete`),children:`Delete`})]})]},e.id))})]})]})}var He=e=>({kind:e,name:``,source:``,description:``,body:``,transport:`stdio`,protocol_mode:`auto`,command:``,args:``,cwd:``,env:``,url:``,headers:``,credential_env:``,timeout:30,allow_deprecated_transport:!1,enabled:!0});function Ue(e,t=!1){return Object.entries(e||{}).filter(([e])=>!(t&&e.toLowerCase()===`authorization`)).map(([e,t])=>`${e}=${t}`).join(` +`)}),(f.worktrees||[]).map(e=>(0,N.jsxs)(`div`,{className:`git-file-row`,children:[(0,N.jsxs)(`code`,{children:[e.worktree,` `,e.branch||``]}),e.worktree&&e.current!==`true`&&(0,N.jsx)(`div`,{children:(0,N.jsx)(`button`,{className:`danger-link`,type:`button`,onClick:()=>void j(e.worktree),children:`Remove`})})]},e.worktree)),(0,N.jsxs)(`div`,{className:`worktree-form`,children:[(0,N.jsx)(`input`,{"aria-label":`New worktree branch`,value:w,onChange:e=>T(e.target.value),placeholder:`feature/name`}),(0,N.jsx)(`input`,{"aria-label":`New worktree directory`,value:E,onChange:e=>ee(e.target.value),placeholder:`../project-feature`}),(0,N.jsx)(`button`,{className:`secondary-button`,type:`button`,disabled:!w||!E,onClick:()=>void ie(),children:`Create branch + worktree`})]})]}),(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Artifacts`}),(0,N.jsxs)(`p`,{children:[O.length,` previewable project files discovered.`]}),(0,N.jsx)(`div`,{className:`artifact-links`,children:O.slice(0,12).map(e=>(0,N.jsx)(`button`,{type:`button`,onClick:()=>void te(e.path),children:e.name},e.path))})]})]}),m&&(0,N.jsx)(`pre`,{className:`diff-view`,children:m}),(0,N.jsxs)(`article`,{className:`detail-card terminal-card`,children:[(0,N.jsx)(`h2`,{children:`Command console`}),(0,N.jsx)(`p`,{children:`Commands run as an argument list in the selected workspace, with no shell expansion and a 60-second limit.`}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`input`,{"aria-label":`Workspace command`,value:g,onChange:e=>y(e.target.value),onKeyDown:e=>{e.key===`Enter`&&k()}}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,disabled:S,onClick:()=>void k(),children:S?`Running…`:`Run`})]}),b&&(0,N.jsx)(`pre`,{children:b})]})]})}function Ve({setError:e,notify:t}){let[n,r]=(0,l.useState)({}),[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(60),[c,u]=(0,l.useState)(()=>localStorage.getItem(`magent-notifications`)===`on`),[d,f]=(0,l.useState)([]),p=(0,l.useRef)({});async function m(){try{let[e,t]=await Promise.all([_(`/api/run-center`),_(`/api/tasks`)]);if(r(e),f(t.tasks||[]),c&&`Notification`in window&&Notification.permission===`granted`){let n=[...e.chat_runs||[],...e.graph_runs||[],...t.tasks||[]];for(let e of n){let t=e,n=String(t.id||t.run_id||t.job_id||t.task_id||``),r=String(t.state||t.status||``),i=p.current[n];n&&i&&i!==r&&[`succeeded`,`failed`,`cancelled`,`completed`].includes(r)&&new Notification(`MagAgent ${r}`,{body:String(t.title||t.summary||n)}),n&&(p.current[n]=r)}}}catch(t){e(t.message)}}(0,l.useEffect)(()=>{m();let e=window.setInterval(()=>void m(),5e3);return()=>window.clearInterval(e)},[c]);async function h(){try{await v(`/api/schedules`,{path:i,interval_minutes:o}),a(``),t(`Graph schedule created`),await m()}catch(t){e(t.message)}}async function g(t,n){try{await v(`/api/schedules/action`,{id:t,action:n}),await m()}catch(t){e(t.message)}}async function y(n,r){let i=n.id||n.task_id||``;try{await v(`/api/tasks/action`,{task_id:i,action:r}),t(`Task ${r} requested`),await m()}catch(t){e(t.message)}}async function b(){if(!(`Notification`in window)){e(`This browser does not support desktop notifications.`);return}let t=!c&&await Notification.requestPermission()===`granted`;u(t),localStorage.setItem(`magent-notifications`,t?`on`:`off`)}return(0,N.jsxs)(`section`,{className:`view active page-view`,children:[(0,N.jsxs)(`div`,{className:`page-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`BACKGROUND WORK`}),(0,N.jsx)(`h1`,{children:`Run center`}),(0,N.jsx)(`p`,{children:`Monitor chat and graph execution, manage durable tasks, and schedule governed graphs.`})]}),(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>void b(),children:c?`Notifications on`:`Enable notifications`})]}),(0,N.jsxs)(`div`,{className:`ops-grid`,children:[(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Chat runs`}),(0,N.jsx)(`pre`,{children:JSON.stringify(n.chat_runs||[],null,2)})]}),(0,N.jsxs)(`article`,{className:`ops-card`,children:[(0,N.jsx)(`h3`,{children:`Graph runs`}),(0,N.jsx)(`pre`,{children:JSON.stringify(n.graph_runs||[],null,2)})]})]}),(0,N.jsxs)(`article`,{className:`detail-card schedule-card`,children:[(0,N.jsx)(`h2`,{children:`Durable tasks`}),!d.length&&(0,N.jsx)(`p`,{children:`No durable execution tasks yet.`}),(0,N.jsx)(`div`,{className:`schedule-list`,children:d.map(e=>{let t=e.id||e.task_id||``,n=e.state||e.status||`unknown`;return(0,N.jsxs)(`div`,{className:`schedule-row`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`b`,{children:e.title||t}),(0,N.jsxs)(`small`,{children:[n,e.summary?` · ${e.summary}`:``]}),(0,N.jsxs)(`details`,{children:[(0,N.jsx)(`summary`,{children:`Review recovery evidence`}),(0,N.jsx)(`p`,{children:`Inspect completed effects before retrying. Changing task status alone does not restart a stopped harness.`}),(0,N.jsx)(`pre`,{children:JSON.stringify({files_changed:e.files_changed||[],checkpoints:e.checkpoints||[],final_audit:e.final_audit||{},metadata:e.metadata||{}},null,2)})]})]}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[n===`running`&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`pause`),children:`Pause`}),[`paused`,`waiting`].includes(n)&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`resume`),children:`Resume`}),![`succeeded`,`cancelled`].includes(n)&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`cancel`),children:`Cancel`}),[`failed`,`cancelled`].includes(n)&&(0,N.jsx)(`button`,{type:`button`,onClick:()=>void y(e,`retry`),children:`Retry`})]})]},t)})})]}),(0,N.jsxs)(`article`,{className:`detail-card schedule-card`,children:[(0,N.jsx)(`h2`,{children:`Schedule a graph`}),(0,N.jsx)(`p`,{children:`Schedules run only while this local UI server is active and still pass graph validation and human-gate checks.`}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`input`,{"aria-label":`Graph path`,value:i,onChange:e=>a(e.target.value),placeholder:`workflows/review.agraph.yaml`}),(0,N.jsx)(`input`,{"aria-label":`Interval minutes`,type:`number`,min:1,value:o,onChange:e=>s(Number(e.target.value))}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,disabled:!i,onClick:()=>void h(),children:`Schedule`})]}),(0,N.jsx)(`div`,{className:`schedule-list`,children:(n.schedules||[]).map(e=>(0,N.jsxs)(`div`,{className:`schedule-row`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`b`,{children:e.path}),(0,N.jsxs)(`small`,{children:[`Every `,e.interval_minutes,` min · `,e.status,e.last_error?` · ${e.last_error}`:``]})]}),(0,N.jsxs)(`div`,{className:`toolbar-row`,children:[(0,N.jsx)(`button`,{type:`button`,onClick:()=>void g(e.id,`run`),children:`Run now`}),(0,N.jsx)(`button`,{type:`button`,onClick:()=>void g(e.id,e.status===`active`?`pause`:`resume`),children:e.status===`active`?`Pause`:`Resume`}),(0,N.jsx)(`button`,{type:`button`,onClick:()=>void g(e.id,`delete`),children:`Delete`})]})]},e.id))})]})]})}var He=e=>({kind:e,name:``,source:``,description:``,body:``,transport:`stdio`,protocol_mode:`auto`,command:``,args:``,cwd:``,env:``,url:``,headers:``,credential_env:``,timeout:30,allow_deprecated_transport:!1,enabled:!0});function Ue(e,t=!1){return Object.entries(e||{}).filter(([e])=>!(t&&e.toLowerCase()===`authorization`)).map(([e,t])=>`${e}=${t}`).join(` `)}function We(e,t){let n={};for(let r of e.split(` `).map(e=>e.trim()).filter(Boolean)){let e=r.indexOf(`=`);if(e<1||!r.slice(e+1).trim())throw Error(`${t} must use NAME=value, one entry per line.`);n[r.slice(0,e).trim()]=r.slice(e+1).trim()}return n}function Ge(e){let t=(e.headers?.Authorization||e.headers?.authorization||``).match(/^Bearer \$\{([^}]+)\}$/i)?.[1]||``;return{...He(`mcp`),original:e.name,name:e.name,enabled:e.enabled,transport:e.transport||(e.url?`streamable-http`:`stdio`),protocol_mode:e.protocol_mode||`auto`,command:e.command||``,args:(e.args||[]).join(` `),cwd:e.cwd||``,env:Ue(e.env),url:e.url||``,headers:Ue(e.headers,!!t),credential_env:t,timeout:e.timeout||30,allow_deprecated_transport:!!e.allow_deprecated_transport}}function Ke(e){let t=We(e.headers,`Headers`);if(e.credential_env.trim()){let n=e.credential_env.trim();if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(n))throw Error(`Credential environment variable must be a valid environment name.`);t.Authorization=`Bearer \${${n}}`}return{kind:`mcp`,action:`save`,name:e.name,transport:e.transport,protocol_mode:e.protocol_mode,command:e.command,args:e.args.split(` -`).map(e=>e.trim()).filter(Boolean),cwd:e.cwd,env:We(e.env,`Environment variables`),url:e.url,headers:t,timeout:e.timeout,allow_deprecated_transport:e.allow_deprecated_transport,enabled:e.enabled}}function qe({setError:e,notify:t}){let[n,r]=(0,l.useState)({}),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)([]),[h,g]=(0,l.useState)(``);async function y(){try{let[e,t]=await Promise.all([_(`/api/extensions`),_(`/api/onboarding/providers`)]);r(e),m(t.image_models||[])}catch(t){e(t.message)}}(0,l.useEffect)(()=>{y()},[]);async function b(n,r){try{await v(`/api/extensions/plugins`,{name:n,enabled:r}),t(`${n} ${r?`enabled`:`disabled`}`),await y()}catch(t){e(t.message)}}async function x(n){if(n.preventDefault(),i)try{await v(`/api/extensions/manage`,i.kind===`mcp`?Ke(i):{kind:i.kind,action:i.kind===`plugin`?`install`:`save`,name:i.name,source:i.source,description:i.description,body:i.body}),t(`${i.kind} saved.`),a(null),await y()}catch(t){e(t.message)}}async function S(){if(!(!i||i.kind!==`mcp`)){s(!0);try{t((await v(`/api/extensions/mcp/test`,Ke(i))).message||`MCP connection succeeded.`)}catch(t){e(t.message)}finally{s(!1)}}}async function C(n,r){if(window.confirm(`Delete ${n} “${r}”?`))try{await v(`/api/extensions/manage`,{kind:n,action:`delete`,name:r}),t(`${n} deleted.`),await y()}catch(t){e(t.message)}}async function w(n){if(n.preventDefault(),c)try{await v(`/api/settings`,{path:c.id===`image`?`models.image_maker`:`tools.browser_enabled`,value:c.id===`image`?(d===`__custom__`?h:d).trim():!c.enabled,scope:`global`}),t(`${c.label} updated.`),u(null),await y()}catch(t){e(t.message)}}return(0,N.jsxs)(`section`,{className:`view active page-view extensions-page`,children:[(0,N.jsx)(`div`,{className:`page-head`,children:(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`TOOLS & INTEGRATIONS`}),(0,N.jsx)(`h1`,{children:`Extensions`}),(0,N.jsx)(`p`,{children:`Install local plugins and manage project skills and MCP connections. Credentials are referenced by environment variable and are never revealed here.`})]})}),(0,N.jsx)(`div`,{className:`card-grid`,children:(n.capabilities||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.label}),(0,N.jsx)(`p`,{children:e.description}),(0,N.jsx)(`span`,{className:`tag`,children:e.enabled?`Ready`:`Not configured`}),typeof e.configured==`string`&&e.configured&&(0,N.jsx)(`p`,{className:`mono`,children:e.configured}),[`image`,`browser`].includes(e.id)&&(0,N.jsx)(`div`,{className:`detail-actions`,children:(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>{u(e);let t=typeof e.configured==`string`?e.configured:``,n=p.some(e=>e.value===t);f(t&&!n?`__custom__`:t),g(t&&!n?t:``)},children:e.id===`browser`?e.enabled?`Disable`:`Enable`:e.enabled?`Edit configuration`:`Configure`})})]},e.id))}),(0,N.jsxs)(`div`,{className:`section-heading`,children:[(0,N.jsx)(`h2`,{children:`Plugins`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>a(He(`plugin`)),children:`+ Install plugin`})]}),(0,N.jsxs)(`div`,{className:`card-grid`,children:[(n.plugins||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.name}),(0,N.jsxs)(`p`,{children:[`Version `,e.version||`unknown`,` · integrity `,e.integrity||`unrecorded`]}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`secondary-button`,disabled:e.valid===!1&&!e.enabled,type:`button`,onClick:()=>void b(e.name,!e.enabled),children:e.enabled?`Disable`:`Enable`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void C(`plugin`,e.name),children:`Uninstall`})]})]},e.name)),!(n.plugins||[]).length&&(0,N.jsx)(`p`,{children:`No plugins installed.`})]}),(0,N.jsxs)(`div`,{className:`section-heading`,children:[(0,N.jsx)(`h2`,{children:`Project skills`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>a(He(`skill`)),children:`+ New skill`})]}),(0,N.jsx)(`div`,{className:`card-grid`,children:(n.skills||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.name}),(0,N.jsx)(`p`,{children:e.description}),(0,N.jsxs)(`span`,{className:`tag`,children:[`v`,e.version]}),e.editable&&(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>a({...He(`skill`),original:e.name,name:e.name,description:e.description||``,body:String(e.body||``).replace(/^---[\s\S]*?---\s*/,``)}),children:`Edit`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void C(`skill`,e.name),children:`Delete`})]})]},`${e.name}-${e.version}`))}),(0,N.jsxs)(`div`,{className:`section-heading`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`h2`,{children:`MCP servers`}),(0,N.jsx)(`p`,{className:`context-note`,children:`Auto-negotiation supports modern MCP and legacy-compatible servers. Transport and protocol version are separate choices.`})]}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>a(He(`mcp`)),children:`+ New server`})]}),(0,N.jsx)(`div`,{className:`card-grid`,children:(n.mcp_servers||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.name}),(0,N.jsx)(`span`,{className:`tag`,children:e.enabled?`Enabled`:`Disabled`}),(0,N.jsx)(`span`,{className:`tag`,children:e.transport||`stdio`}),(0,N.jsxs)(`span`,{className:`tag`,children:[`protocol `,e.protocol_mode||`auto`]}),(0,N.jsx)(`p`,{children:e.command||e.url||`No launch target`}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>a(Ge(e)),children:`Edit`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void C(`mcp`,e.name),children:`Delete`})]})]},e.name))}),i&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`${i.kind} editor`,onClick:e=>{e.target===e.currentTarget&&a(null)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:x,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:i.kind.toUpperCase()}),(0,N.jsxs)(`h2`,{children:[i.original?`Edit`:`Create or install`,` `,i.kind]})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>a(null),children:`×`})]}),(0,N.jsxs)(`label`,{children:[`Name`,(0,N.jsx)(`input`,{required:!0,value:i.name,disabled:!!i.original,onChange:e=>a({...i,name:e.target.value})})]}),i.kind===`plugin`&&(0,N.jsxs)(`label`,{children:[`Local source folder`,(0,N.jsx)(`input`,{required:!0,placeholder:`/path/to/plugin`,value:i.source,onChange:e=>a({...i,source:e.target.value})})]}),i.kind===`skill`&&(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`Description`,(0,N.jsx)(`input`,{required:!0,value:i.description,onChange:e=>a({...i,description:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Instructions`,(0,N.jsx)(`textarea`,{required:!0,rows:10,value:i.body,onChange:e=>a({...i,body:e.target.value})})]})]}),i.kind===`mcp`&&(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`label`,{children:[`Transport`,(0,N.jsxs)(`select`,{value:i.transport,onChange:e=>a({...i,transport:e.target.value}),children:[(0,N.jsx)(`option`,{value:`stdio`,children:`Local process (stdio)`}),(0,N.jsx)(`option`,{value:`streamable-http`,children:`Remote Streamable HTTP`}),(0,N.jsx)(`option`,{value:`legacy-sse`,children:`Legacy SSE (deprecated)`})]})]}),(0,N.jsxs)(`label`,{children:[`Protocol compatibility`,(0,N.jsxs)(`select`,{value:i.protocol_mode,onChange:e=>a({...i,protocol_mode:e.target.value}),children:[(0,N.jsx)(`option`,{value:`auto`,children:`Auto-negotiate (recommended)`}),(0,N.jsx)(`option`,{value:`modern`,children:`Modern MCP only`}),(0,N.jsx)(`option`,{value:`legacy`,children:`Legacy MCP only`})]})]})]}),i.transport===`stdio`?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`Command`,(0,N.jsx)(`input`,{required:!0,placeholder:`npx`,value:i.command,onChange:e=>a({...i,command:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Arguments `,(0,N.jsx)(`small`,{children:`one per line`}),(0,N.jsx)(`textarea`,{rows:4,value:i.args,onChange:e=>a({...i,args:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Working directory `,(0,N.jsx)(`small`,{children:`optional`}),(0,N.jsx)(`input`,{placeholder:`/path/to/server`,value:i.cwd,onChange:e=>a({...i,cwd:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Environment mapping `,(0,N.jsxs)(`small`,{children:[`NAME=value, one per line; use $`,"${ENV_VAR}",` for credentials`]}),(0,N.jsx)(`textarea`,{rows:4,placeholder:"API_TOKEN=${API_TOKEN}",value:i.env,onChange:e=>a({...i,env:e.target.value})})]})]}):(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`Server URL`,(0,N.jsx)(`input`,{required:!0,type:`url`,placeholder:`https://example.com/mcp`,value:i.url,onChange:e=>a({...i,url:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Bearer credential environment variable `,(0,N.jsx)(`small`,{children:`optional`}),(0,N.jsx)(`input`,{placeholder:`MCP_ACCESS_TOKEN`,value:i.credential_env,onChange:e=>a({...i,credential_env:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Additional headers `,(0,N.jsx)(`small`,{children:`NAME=value, one per line; environment references supported`}),(0,N.jsx)(`textarea`,{rows:4,placeholder:"X-Tenant=${MCP_TENANT}",value:i.headers,onChange:e=>a({...i,headers:e.target.value})})]})]}),(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`label`,{children:[`Connection timeout (seconds)`,(0,N.jsx)(`input`,{type:`number`,min:1,max:300,value:i.timeout,onChange:e=>a({...i,timeout:Number(e.target.value)})})]}),(0,N.jsxs)(`label`,{className:`checkbox-row`,children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:i.enabled,onChange:e=>a({...i,enabled:e.target.checked})}),` Enable after saving`]})]}),i.transport===`legacy-sse`&&(0,N.jsxs)(`label`,{className:`checkbox-row warning-choice`,children:[(0,N.jsx)(`input`,{type:`checkbox`,required:!0,checked:i.allow_deprecated_transport,onChange:e=>a({...i,allow_deprecated_transport:e.target.checked})}),` I understand legacy SSE is deprecated and explicitly allow it.`]}),(0,N.jsx)(`p`,{className:`context-note`,children:`“Modern” and “legacy” select protocol eras; the exact server version is negotiated during connection. Use Auto unless the server documents a fixed era.`}),(0,N.jsxs)(`div`,{className:`mcp-test-row`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,disabled:o,onClick:()=>void S(),children:o?`Testing…`:`Test connection`}),(0,N.jsx)(`span`,{children:`Testing may start the local command or contact the remote URL, but does not save it.`})]})]}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Save`})]})}),c&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Configure ${c.label}`,onClick:e=>{e.target===e.currentTarget&&u(null)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:w,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`CAPABILITY`}),(0,N.jsx)(`h2`,{children:c.id===`browser`?`${c.enabled?`Disable`:`Enable`} browser automation`:`Configure image generation`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>u(null),children:`×`})]}),c.id===`image`?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`p`,{children:`Select an image model whose provider is available on this machine. Configure missing provider access in Settings first.`}),(0,N.jsxs)(`label`,{children:[`Image model`,(0,N.jsxs)(`select`,{value:d,onChange:e=>f(e.target.value),children:[(0,N.jsx)(`option`,{value:``,children:`Not configured`}),p.filter(e=>e.id!==`custom`).map(e=>(0,N.jsxs)(`option`,{value:e.value,disabled:!e.available,children:[e.label,e.available?``:` · configure provider first`]},e.id)),(0,N.jsx)(`option`,{value:`__custom__`,children:`Custom provider/model…`})]})]}),d===`__custom__`&&(0,N.jsxs)(`label`,{children:[`Custom route`,(0,N.jsx)(`input`,{required:!0,placeholder:`provider/model`,value:h,onChange:e=>g(e.target.value)})]}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>{f(``),g(``)},children:`Clear`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Save configuration`})]})]}):(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`p`,{children:`Profile and permission ceilings still apply when browser automation is enabled.`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:c.enabled?`Disable`:`Enable`})]})]})})]})}function Je({rows:e,onClose:t}){return(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Keyboard shortcuts`,onClick:e=>{e.target===e.currentTarget&&t()},children:(0,N.jsxs)(`div`,{className:`modal shortcuts-sheet`,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`KEYBOARD`}),(0,N.jsx)(`h2`,{children:`Shortcuts`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:t,"aria-label":`Close`,children:`×`})]}),(0,N.jsx)(`ul`,{children:e.map(e=>(0,N.jsxs)(`li`,{children:[(0,N.jsx)(`span`,{children:e.describe}),(0,N.jsx)(`kbd`,{children:e.keys})]},`${e.describe}-${e.keys}`))})]})})}function Ye({setError:e,notify:t}){let[n,r]=(0,l.useState)([]),[i,a]=(0,l.useState)(!1),o=(0,l.useCallback)(async()=>{try{let e=await _(`/api/approvals/snapshot`);r(e.snapshot?.pending??[])}catch{}},[]);(0,l.useEffect)(()=>{o();let e=window.setInterval(()=>void o(),800);return()=>window.clearInterval(e)},[o]);let s=(0,l.useCallback)(async(n,r)=>{a(!0);try{let e=await v(`/api/approvals/decide`,{request_id:n.id,decision:r.decision,scope:r.scope,decision_id:`dec_web_${crypto.randomUUID().replaceAll(`-`,``)}`});if(!e.ok)throw Error(e.error||`The approval could not be resolved.`);t(r.decision===`approve`?`Permission approved.`:`Permission denied.`),await o()}catch(t){e(t.message),await o()}finally{a(!1)}},[t,o,e]),c=n[0];return c?(0,N.jsx)(`div`,{className:`modal-backdrop approval-backdrop`,role:`presentation`,children:(0,N.jsxs)(`section`,{className:`modal approval-modal`,role:`alertdialog`,"aria-modal":`true`,"aria-label":`Permission required`,children:[(0,N.jsxs)(`small`,{children:[`PERMISSION REQUIRED · `,n.length,` PENDING`]}),(0,N.jsx)(`h2`,{children:c.action.summary}),(0,N.jsxs)(`div`,{className:`approval-risk ${c.risk.level}`,children:[(0,N.jsxs)(`b`,{children:[c.risk.level,` risk`]}),c.risk.reasons.map(e=>(0,N.jsx)(`span`,{children:e},e))]}),(0,N.jsxs)(`dl`,{className:`approval-detail`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Action`}),(0,N.jsx)(`dd`,{children:c.action.name})]}),c.action.working_directory&&(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Project`}),(0,N.jsx)(`dd`,{children:c.action.working_directory})]}),c.origin.node_id&&(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Graph card`}),(0,N.jsx)(`dd`,{children:c.origin.node_id})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Exact arguments`}),(0,N.jsx)(`dd`,{children:(0,N.jsx)(`pre`,{children:JSON.stringify(c.action.arguments,null,2)})})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Digest`}),(0,N.jsx)(`dd`,{children:(0,N.jsx)(`code`,{children:c.action_digest})})]})]}),!!c.action.effects?.length&&(0,N.jsx)(`ul`,{children:c.action.effects.map(e=>(0,N.jsx)(`li`,{children:e},e))}),(0,N.jsx)(`div`,{className:`approval-actions`,children:c.choices.map(e=>(0,N.jsx)(`button`,{className:e.decision===`approve`?`primary-button`:`secondary-button`,disabled:i,type:`button`,onClick:()=>void s(c,e),children:e.label},`${e.decision}:${e.scope}`))}),(0,N.jsx)(`small`,{children:`MagAgent will revalidate the exact action and current policy before execution.`})]})}):null}var Xe=[{id:`chat`,glyph:`✦`,label:`Chats`},{id:`bots`,glyph:`◉`,label:`Bots`},{id:`graphs`,glyph:`⌘`,label:`Graphs`},{id:`profiles`,glyph:`◇`,label:`Profiles`},{id:`memory`,glyph:`❖`,label:`Memory`},{id:`workspace`,glyph:`▤`,label:`Files`},{id:`runs`,glyph:`▶`,label:`Runs`},{id:`extensions`,glyph:`⬡`,label:`Tools`},{id:`settings`,glyph:`⚙`,label:`Settings`},{id:`operations`,glyph:`⌁`,label:`Ops`}];function Ze(){let[e,t]=(0,l.useState)(M),[n,r]=(0,l.useState)(`chat`),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)([]),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)([]),[p,h]=(0,l.useState)([]),[g,y]=(0,l.useState)(``),[b,x]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[w,T]=(0,l.useState)(!1),[D,O]=(0,l.useState)(!1),[k,ae]=(0,l.useState)(!1),[P,F]=(0,l.useState)(!0),[I,se]=(0,l.useState)([]),L=(0,l.useCallback)(e=>{x(e),window.setTimeout(()=>x(``),2600)},[]),R=(0,l.useCallback)(async()=>{let e=await _(`/api/conversations`);s(e.conversations||[]),u(t=>{let n=e.conversations||[];return t&&n.some(e=>e.id===t)?t:n[0]?.id||null})},[]),ce=(0,l.useCallback)(async()=>{let e=await _(`/api/bootstrap`);a(e),s(e.conversations||[]),f(e.profiles?.profiles||[]),h(e.settings?.fields||[]),u(t=>{let n=e.conversations||[];return t&&n.some(e=>e.id===t)?t:n[0]?.id||null})},[]);(0,l.useEffect)(()=>{m(),(async()=>{try{if((await _(`/api/onboarding/readiness`).catch(()=>({ready:!0}))).ready===!1){O(!0);return}await ce()}catch(e){y(e.message)}finally{T(!0)}})()},[ce]);let le=(0,l.useMemo)(()=>[{key:`k`,mod:!0,describe:`Focus the message box`,run:()=>{r(`chat`),window.setTimeout(()=>document.querySelector(`.composer textarea`)?.focus(),0)}},{key:`f`,mod:!0,describe:`Search conversations`,run:()=>{r(`chat`),window.setTimeout(()=>document.querySelector(`#conversationSearch`)?.focus(),0)}},{key:`n`,mod:!0,shift:!0,describe:`New chat`,run:()=>{r(`chat`),window.dispatchEvent(new CustomEvent(`magent:new-conversation`))}},...Xe.map((e,t)=>({key:String(t+1),mod:!0,describe:`Go to ${e.label}`,run:()=>r(e.id)})),{key:`/`,describe:`Show keyboard shortcuts`,run:()=>C(!0)},{key:`Escape`,describe:`Close a dialog or clear an error`,run:()=>{C(!1),y(``)}}],[]);(0,l.useEffect)(()=>E(le),[le]);let ue=(0,l.useCallback)(async(e,t=[],n=i?.project||`.`,a=``,o=i?.permission_mode||`balanced`)=>{try{let i=await v(`/api/conversations`,{kind:e,profiles:t,project:n,coordinator:a,permission_mode:o,title:e===`bot`&&t[0]?`Chat with ${t[0]}`:`New conversation`});await R(),u(i.conversation.id),ae(!1),r(`chat`)}catch(e){y(e.message)}},[i?.project,i?.permission_mode,R]),fe=(0,l.useCallback)(async e=>{try{await v(`/api/conversations/delete`,{conversation_id:e.id}),await R(),L(`Conversation deleted.`)}catch(e){y(e.message)}},[L,R]),me=(0,l.useCallback)(async(e,t)=>{try{await v(`/api/conversations/update`,{conversation_id:e.id,project:t}),await R(),L(`Conversation project updated.`)}catch(e){y(e.message)}},[L,R]),ge=(0,l.useCallback)(async(e,t)=>{try{await v(`/api/conversations/update`,{conversation_id:e.id,permission_mode:t}),await R(),L(`Permission mode changed to ${t}.`)}catch(e){y(e.message)}},[L,R]),_e=o.find(e=>e.id===c)||null;return w?D?(0,N.jsx)(oe,{setError:y,onReady:()=>{O(!1),ce()}}):(0,N.jsxs)(`div`,{className:`app-shell`,children:[(0,N.jsxs)(`aside`,{className:`rail`,"aria-label":`Primary navigation`,children:[(0,N.jsx)(`div`,{className:`brand-mark`,children:`M`}),(0,N.jsx)(`nav`,{children:Xe.map(e=>(0,N.jsxs)(`button`,{type:`button`,className:`rail-button ${n===e.id?`active`:``}`,"data-view":e.id,title:e.label,"aria-current":n===e.id?`page`:void 0,onClick:()=>r(e.id),children:[e.glyph,(0,N.jsx)(`span`,{children:e.label})]},e.id))}),(0,N.jsxs)(`button`,{type:`button`,className:`rail-button theme-toggle`,onClick:()=>{let n=j(e);t(n),A(n),ie(n)},"aria-label":`Theme: ${te[e]}. Activate to change.`,title:`Theme: ${te[e]}`,children:[(0,N.jsx)(`span`,{"aria-hidden":`true`,children:re[e]}),(0,N.jsx)(`i`,{children:ne[e]})]}),(0,N.jsx)(`div`,{className:`connection-dot`,title:`Local connection`})]}),(0,N.jsx)(pe,{conversations:o,activeId:c,setActiveId:u,profiles:d,boot:i,onCreate:ue,onDelete:fe,onProjectChange:me,setError:y,open:k,setOpen:ae}),(0,N.jsxs)(`main`,{className:`workspace`,children:[g&&(0,N.jsxs)(`div`,{className:`error-banner`,role:`alert`,children:[g,(0,N.jsx)(`button`,{type:`button`,onClick:()=>y(``),"aria-label":`Dismiss`,children:`×`})]}),(0,N.jsxs)(`div`,{className:`workspace-head`,children:[(0,N.jsx)(`button`,{className:`icon-button sidebar-open`,type:`button`,onClick:()=>ae(!0),"aria-label":`Show conversations`,children:`☰`}),(0,N.jsx)(`div`,{className:`workspace-title`,children:n===`chat`?(0,N.jsx)(`h1`,{children:_e?.title||`New conversation`}):(0,N.jsx)(`b`,{})}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>F(e=>!e),"aria-label":`Toggle conversation details`,"aria-expanded":P,children:`☷`})]}),n===`chat`&&(0,N.jsx)(de,{active:_e,refresh:R,setError:y,notify:L,context:I,clearContext:()=>se([])}),n===`bots`&&(0,N.jsx)(ve,{profiles:d,setError:y,onStart:()=>r(`chat`),refresh:R,setActiveId:u}),(0,N.jsx)(`div`,{className:`persistent-view ${n===`graphs`?`active`:``}`,"aria-hidden":n!==`graphs`,children:(0,N.jsx)(Ae,{profiles:d,setError:y,notify:L})}),(0,N.jsx)(`div`,{className:`persistent-view ${n===`profiles`?`active`:``}`,"aria-hidden":n!==`profiles`,children:(0,N.jsx)(Ne,{profiles:d,refresh:ce,setError:y,notify:L})}),n===`memory`&&(0,N.jsx)(Ie,{setError:y,notify:L}),n===`workspace`&&(0,N.jsx)(Be,{selected:I,setSelected:se,activeConversation:c,setError:y,notify:L}),n===`runs`&&(0,N.jsx)(Ve,{setError:y,notify:L}),n===`extensions`&&(0,N.jsx)(qe,{setError:y,notify:L}),n===`settings`&&(0,N.jsx)(Le,{fields:p,profiles:d,refresh:ce,setError:y,notify:L}),n===`operations`&&(0,N.jsx)(ze,{setError:y})]}),P&&(0,N.jsx)(he,{active:_e,boot:i,onPermissionChange:ge,onClose:()=>F(!1)}),S&&(0,N.jsx)(Je,{rows:[...le.map(e=>({describe:e.describe,keys:ee(e)})),{describe:`Send the message`,keys:`Enter`},{describe:`Newline in the message box`,keys:`Shift+Enter`}],onClose:()=>C(!1)}),(0,N.jsx)(Ye,{setError:y,notify:L}),b&&(0,N.jsx)(`div`,{className:`toast`,role:`status`,children:b})]}):(0,N.jsxs)(`div`,{className:`splash`,children:[(0,N.jsx)(`div`,{className:`brand-mark`,children:`M`}),(0,N.jsx)(`p`,{children:`Opening the workspace…`})]})}(0,u.createRoot)(document.getElementById(`root`)).render((0,N.jsx)(l.StrictMode,{children:(0,N.jsx)(Ze,{})})); \ No newline at end of file +`).map(e=>e.trim()).filter(Boolean),cwd:e.cwd,env:We(e.env,`Environment variables`),url:e.url,headers:t,timeout:e.timeout,allow_deprecated_transport:e.allow_deprecated_transport,enabled:e.enabled}}function qe({setError:e,notify:t}){let[n,r]=(0,l.useState)({}),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)([]),[h,g]=(0,l.useState)(``);async function y(){try{let[e,t]=await Promise.all([_(`/api/extensions`),_(`/api/onboarding/providers`)]);r(e),m(t.image_models||[])}catch(t){e(t.message)}}(0,l.useEffect)(()=>{y()},[]);async function b(n,r){try{await v(`/api/extensions/plugins`,{name:n,enabled:r}),t(`${n} ${r?`enabled`:`disabled`}`),await y()}catch(t){e(t.message)}}async function x(n){if(n.preventDefault(),i)try{await v(`/api/extensions/manage`,i.kind===`mcp`?Ke(i):{kind:i.kind,action:i.kind===`plugin`?`install`:`save`,name:i.name,source:i.source,description:i.description,body:i.body}),t(`${i.kind} saved.`),a(null),await y()}catch(t){e(t.message)}}async function S(){if(!(!i||i.kind!==`mcp`)){s(!0);try{t((await v(`/api/extensions/mcp/test`,Ke(i))).message||`MCP connection succeeded.`)}catch(t){e(t.message)}finally{s(!1)}}}async function C(n,r){if(window.confirm(`Delete ${n} “${r}”?`))try{await v(`/api/extensions/manage`,{kind:n,action:`delete`,name:r}),t(`${n} deleted.`),await y()}catch(t){e(t.message)}}async function w(n){if(n.preventDefault(),c)try{await v(`/api/settings`,{path:c.id===`image`?`models.image_maker`:`tools.browser_enabled`,value:c.id===`image`?(d===`__custom__`?h:d).trim():!c.enabled,scope:`global`}),t(`${c.label} updated.`),u(null),await y()}catch(t){e(t.message)}}return(0,N.jsxs)(`section`,{className:`view active page-view extensions-page`,children:[(0,N.jsx)(`div`,{className:`page-head`,children:(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`TOOLS & INTEGRATIONS`}),(0,N.jsx)(`h1`,{children:`Extensions`}),(0,N.jsx)(`p`,{children:`Install local plugins and manage project skills and MCP connections. Credentials are referenced by environment variable and are never revealed here.`})]})}),(0,N.jsx)(`div`,{className:`card-grid`,children:(n.capabilities||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.label}),(0,N.jsx)(`p`,{children:e.description}),(0,N.jsx)(`span`,{className:`tag`,children:e.enabled?`Ready`:`Not configured`}),typeof e.configured==`string`&&e.configured&&(0,N.jsx)(`p`,{className:`mono`,children:e.configured}),[`image`,`browser`].includes(e.id)&&(0,N.jsx)(`div`,{className:`detail-actions`,children:(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>{u(e);let t=typeof e.configured==`string`?e.configured:``,n=p.some(e=>e.value===t);f(t&&!n?`__custom__`:t),g(t&&!n?t:``)},children:e.id===`browser`?e.enabled?`Disable`:`Enable`:e.enabled?`Edit configuration`:`Configure`})})]},e.id))}),(0,N.jsxs)(`div`,{className:`section-heading`,children:[(0,N.jsx)(`h2`,{children:`Plugins`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>a(He(`plugin`)),children:`+ Install plugin`})]}),(0,N.jsxs)(`div`,{className:`card-grid`,children:[(n.plugins||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.name}),(0,N.jsxs)(`p`,{children:[`Version `,e.version||`unknown`,` · integrity `,e.integrity||`unrecorded`]}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`secondary-button`,disabled:e.valid===!1&&!e.enabled,type:`button`,onClick:()=>void b(e.name,!e.enabled),children:e.enabled?`Disable`:`Enable`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void C(`plugin`,e.name),children:`Uninstall`})]})]},e.name)),!(n.plugins||[]).length&&(0,N.jsx)(`p`,{children:`No plugins installed.`})]}),(0,N.jsxs)(`div`,{className:`section-heading`,children:[(0,N.jsx)(`h2`,{children:`Project skills`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>a(He(`skill`)),children:`+ New skill`})]}),(0,N.jsx)(`div`,{className:`card-grid`,children:(n.skills||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.name}),(0,N.jsx)(`p`,{children:e.description}),(0,N.jsxs)(`span`,{className:`tag`,children:[`v`,e.version]}),e.editable&&(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>a({...He(`skill`),original:e.name,name:e.name,description:e.description||``,body:String(e.body||``).replace(/^---[\s\S]*?---\s*/,``)}),children:`Edit`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void C(`skill`,e.name),children:`Delete`})]})]},`${e.name}-${e.version}`))}),(0,N.jsxs)(`div`,{className:`section-heading`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`h2`,{children:`MCP servers`}),(0,N.jsx)(`p`,{className:`context-note`,children:`Auto-negotiation supports modern MCP and legacy-compatible servers. Transport and protocol version are separate choices.`})]}),(0,N.jsx)(`button`,{className:`primary-button`,type:`button`,onClick:()=>a(He(`mcp`)),children:`+ New server`})]}),(0,N.jsx)(`div`,{className:`card-grid`,children:(n.mcp_servers||[]).map(e=>(0,N.jsxs)(`article`,{className:`detail-card`,children:[(0,N.jsx)(`h3`,{children:e.name}),(0,N.jsx)(`span`,{className:`tag`,children:e.enabled?`Enabled`:`Disabled`}),(0,N.jsx)(`span`,{className:`tag`,children:e.transport||`stdio`}),(0,N.jsxs)(`span`,{className:`tag`,children:[`protocol `,e.protocol_mode||`auto`]}),(0,N.jsx)(`p`,{children:e.command||e.url||`No launch target`}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>a(Ge(e)),children:`Edit`}),(0,N.jsx)(`button`,{className:`danger-button`,type:`button`,onClick:()=>void C(`mcp`,e.name),children:`Delete`})]})]},e.name))}),i&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`${i.kind} editor`,onClick:e=>{e.target===e.currentTarget&&a(null)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:x,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:i.kind.toUpperCase()}),(0,N.jsxs)(`h2`,{children:[i.original?`Edit`:`Create or install`,` `,i.kind]})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>a(null),children:`×`})]}),(0,N.jsxs)(`label`,{children:[`Name`,(0,N.jsx)(`input`,{required:!0,value:i.name,disabled:!!i.original,onChange:e=>a({...i,name:e.target.value})})]}),i.kind===`plugin`&&(0,N.jsxs)(`label`,{children:[`Local source folder`,(0,N.jsx)(`input`,{required:!0,placeholder:`/path/to/plugin`,value:i.source,onChange:e=>a({...i,source:e.target.value})})]}),i.kind===`skill`&&(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`Description`,(0,N.jsx)(`input`,{required:!0,value:i.description,onChange:e=>a({...i,description:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Instructions`,(0,N.jsx)(`textarea`,{required:!0,rows:10,value:i.body,onChange:e=>a({...i,body:e.target.value})})]})]}),i.kind===`mcp`&&(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`label`,{children:[`Transport`,(0,N.jsxs)(`select`,{value:i.transport,onChange:e=>a({...i,transport:e.target.value}),children:[(0,N.jsx)(`option`,{value:`stdio`,children:`Local process (stdio)`}),(0,N.jsx)(`option`,{value:`streamable-http`,children:`Remote Streamable HTTP`}),(0,N.jsx)(`option`,{value:`legacy-sse`,children:`Legacy SSE (deprecated)`})]})]}),(0,N.jsxs)(`label`,{children:[`Protocol compatibility`,(0,N.jsxs)(`select`,{value:i.protocol_mode,onChange:e=>a({...i,protocol_mode:e.target.value}),children:[(0,N.jsx)(`option`,{value:`auto`,children:`Auto-negotiate (recommended)`}),(0,N.jsx)(`option`,{value:`modern`,children:`Modern MCP only`}),(0,N.jsx)(`option`,{value:`legacy`,children:`Legacy MCP only`})]})]})]}),i.transport===`stdio`?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`Command`,(0,N.jsx)(`input`,{required:!0,placeholder:`npx`,value:i.command,onChange:e=>a({...i,command:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Arguments `,(0,N.jsx)(`small`,{children:`one per line`}),(0,N.jsx)(`textarea`,{rows:4,value:i.args,onChange:e=>a({...i,args:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Working directory `,(0,N.jsx)(`small`,{children:`optional`}),(0,N.jsx)(`input`,{placeholder:`/path/to/server`,value:i.cwd,onChange:e=>a({...i,cwd:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Environment mapping `,(0,N.jsxs)(`small`,{children:[`NAME=value, one per line; use $`,"${ENV_VAR}",` for credentials`]}),(0,N.jsx)(`textarea`,{rows:4,placeholder:"API_TOKEN=${API_TOKEN}",value:i.env,onChange:e=>a({...i,env:e.target.value})})]})]}):(0,N.jsxs)(N.Fragment,{children:[(0,N.jsxs)(`label`,{children:[`Server URL`,(0,N.jsx)(`input`,{required:!0,type:`url`,placeholder:`https://example.com/mcp`,value:i.url,onChange:e=>a({...i,url:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Bearer credential environment variable `,(0,N.jsx)(`small`,{children:`optional`}),(0,N.jsx)(`input`,{placeholder:`MCP_ACCESS_TOKEN`,value:i.credential_env,onChange:e=>a({...i,credential_env:e.target.value})})]}),(0,N.jsxs)(`label`,{children:[`Additional headers `,(0,N.jsx)(`small`,{children:`NAME=value, one per line; environment references supported`}),(0,N.jsx)(`textarea`,{rows:4,placeholder:"X-Tenant=${MCP_TENANT}",value:i.headers,onChange:e=>a({...i,headers:e.target.value})})]})]}),(0,N.jsxs)(`div`,{className:`form-grid`,children:[(0,N.jsxs)(`label`,{children:[`Connection timeout (seconds)`,(0,N.jsx)(`input`,{type:`number`,min:1,max:300,value:i.timeout,onChange:e=>a({...i,timeout:Number(e.target.value)})})]}),(0,N.jsxs)(`label`,{className:`checkbox-row`,children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:i.enabled,onChange:e=>a({...i,enabled:e.target.checked})}),` Enable after saving`]})]}),i.transport===`legacy-sse`&&(0,N.jsxs)(`label`,{className:`checkbox-row warning-choice`,children:[(0,N.jsx)(`input`,{type:`checkbox`,required:!0,checked:i.allow_deprecated_transport,onChange:e=>a({...i,allow_deprecated_transport:e.target.checked})}),` I understand legacy SSE is deprecated and explicitly allow it.`]}),(0,N.jsx)(`p`,{className:`context-note`,children:`“Modern” and “legacy” select protocol eras; the exact server version is negotiated during connection. Use Auto unless the server documents a fixed era.`}),(0,N.jsxs)(`div`,{className:`mcp-test-row`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,disabled:o,onClick:()=>void S(),children:o?`Testing…`:`Test connection`}),(0,N.jsx)(`span`,{children:`Testing may start the local command or contact the remote URL, but does not save it.`})]})]}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Save`})]})}),c&&(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Configure ${c.label}`,onClick:e=>{e.target===e.currentTarget&&u(null)},children:(0,N.jsxs)(`form`,{className:`modal`,onSubmit:w,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`CAPABILITY`}),(0,N.jsx)(`h2`,{children:c.id===`browser`?`${c.enabled?`Disable`:`Enable`} browser automation`:`Configure image generation`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>u(null),children:`×`})]}),c.id===`image`?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`p`,{children:`Select an image model whose provider is available on this machine. Configure missing provider access in Settings first.`}),(0,N.jsxs)(`label`,{children:[`Image model`,(0,N.jsxs)(`select`,{value:d,onChange:e=>f(e.target.value),children:[(0,N.jsx)(`option`,{value:``,children:`Not configured`}),p.filter(e=>e.id!==`custom`).map(e=>(0,N.jsxs)(`option`,{value:e.value,disabled:!e.available,children:[e.label,e.available?``:` · configure provider first`]},e.id)),(0,N.jsx)(`option`,{value:`__custom__`,children:`Custom provider/model…`})]})]}),d===`__custom__`&&(0,N.jsxs)(`label`,{children:[`Custom route`,(0,N.jsx)(`input`,{required:!0,placeholder:`provider/model`,value:h,onChange:e=>g(e.target.value)})]}),(0,N.jsxs)(`div`,{className:`detail-actions`,children:[(0,N.jsx)(`button`,{className:`ghost-button`,type:`button`,onClick:()=>{f(``),g(``)},children:`Clear`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:`Save configuration`})]})]}):(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)(`p`,{children:`Profile and permission ceilings still apply when browser automation is enabled.`}),(0,N.jsx)(`button`,{className:`primary-button`,type:`submit`,children:c.enabled?`Disable`:`Enable`})]})]})})]})}function Je({rows:e,onClose:t}){return(0,N.jsx)(`div`,{className:`modal-backdrop`,role:`dialog`,"aria-modal":`true`,"aria-label":`Keyboard shortcuts`,onClick:e=>{e.target===e.currentTarget&&t()},children:(0,N.jsxs)(`div`,{className:`modal shortcuts-sheet`,children:[(0,N.jsxs)(`div`,{className:`dialog-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`div`,{className:`eyebrow`,children:`KEYBOARD`}),(0,N.jsx)(`h2`,{children:`Shortcuts`})]}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:t,"aria-label":`Close`,children:`×`})]}),(0,N.jsx)(`ul`,{children:e.map(e=>(0,N.jsxs)(`li`,{children:[(0,N.jsx)(`span`,{children:e.describe}),(0,N.jsx)(`kbd`,{children:e.keys})]},`${e.describe}-${e.keys}`))})]})})}function Ye(e,t){(0,l.useEffect)(()=>{let n=e.current;if(!n||!t)return;let r=document.activeElement,i=()=>Array.from(n.querySelectorAll(`button:not(:disabled), input, select, textarea, a[href], summary, [tabindex="0"]`));(i()[0]??n).focus();let a=e=>{if(e.key!==`Tab`)return;let t=i();if(!t.length){e.preventDefault(),n.focus();return}let r=t[0],a=t[t.length-1];e.shiftKey&&(document.activeElement===r||!n.contains(document.activeElement))?(e.preventDefault(),a.focus()):!e.shiftKey&&(document.activeElement===a||!n.contains(document.activeElement))&&(e.preventDefault(),r.focus())},o=e=>{n.contains(e.target)||(i()[0]??n).focus()};return document.addEventListener(`keydown`,a),document.addEventListener(`focusin`,o),()=>{document.removeEventListener(`keydown`,a),document.removeEventListener(`focusin`,o),r?.isConnected&&r.focus()}},[e,t])}function Xe({setError:e,notify:t}){let[n,r]=(0,l.useState)([]),[i,a]=(0,l.useState)(!1),o=(0,l.useCallback)(async()=>{try{let e=await _(`/api/approvals/snapshot`);r(e.snapshot?.pending??[])}catch{}},[]);(0,l.useEffect)(()=>{o();let e=window.setInterval(()=>void o(),800);return()=>window.clearInterval(e)},[o]);let s=(0,l.useCallback)(async(n,r)=>{a(!0);try{let e=await v(`/api/approvals/decide`,{request_id:n.id,action_digest:n.action_digest,decision:r.decision,scope:r.scope,decision_id:`dec_web_${crypto.randomUUID().replaceAll(`-`,``)}`});if(!e.ok)throw Error(e.error||`The approval could not be resolved.`);t(`Harness outcome: ${e.resolution?.resolution.outcome??`unknown`}`),await o()}catch(t){e(t.message),await o()}finally{a(!1)}},[t,o,e]),c=n[0],u=(0,l.useRef)(null);return Ye(u,c?.id),c?(0,N.jsx)(`div`,{className:`modal-backdrop approval-backdrop`,role:`presentation`,children:(0,N.jsxs)(`section`,{ref:u,tabIndex:-1,className:`modal approval-modal`,role:`alertdialog`,"aria-modal":`true`,"aria-label":`Permission required`,children:[(0,N.jsxs)(`small`,{children:[`PERMISSION REQUIRED · `,n.length,` PENDING`]}),(0,N.jsx)(`h2`,{children:c.action.summary}),(0,N.jsxs)(`p`,{children:[`From `,Object.values(c.origin).join(` · `)]}),c.expires_at&&(0,N.jsxs)(`p`,{children:[`Expires `,new Date(c.expires_at).toLocaleString()]}),(0,N.jsxs)(`div`,{className:`approval-risk ${c.risk.level}`,children:[(0,N.jsxs)(`b`,{children:[c.risk.level,` risk`]}),c.risk.reasons.map(e=>(0,N.jsx)(`span`,{children:e},e))]}),(0,N.jsxs)(`dl`,{className:`approval-detail`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Action`}),(0,N.jsx)(`dd`,{children:c.action.name})]}),c.action.working_directory&&(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Project`}),(0,N.jsx)(`dd`,{children:c.action.working_directory})]}),c.origin.node_id&&(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Graph card`}),(0,N.jsx)(`dd`,{children:c.origin.node_id})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Exact arguments`}),(0,N.jsx)(`dd`,{children:(0,N.jsx)(`pre`,{children:JSON.stringify(c.action.arguments,null,2)})})]}),(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`dt`,{children:`Digest`}),(0,N.jsx)(`dd`,{children:(0,N.jsx)(`code`,{children:c.action_digest})})]})]}),!!c.action.effects?.length&&(0,N.jsx)(`ul`,{children:c.action.effects.map(e=>(0,N.jsx)(`li`,{children:e},e))}),(0,N.jsx)(`div`,{className:`approval-actions`,children:c.choices.map(e=>(0,N.jsx)(`button`,{className:e.decision===`approve`?`primary-button`:`secondary-button`,disabled:i||!!(c.expires_at&&Date.parse(c.expires_at)<=Date.now()),type:`button`,onClick:()=>void s(c,e),children:e.label},`${e.decision}:${e.scope}`))}),(0,N.jsx)(`small`,{children:`MagAgent will revalidate the exact action and current policy before execution.`})]})}):null}var Ze=[{id:`chat`,glyph:`✦`,label:`Chats`},{id:`bots`,glyph:`◉`,label:`Bots`},{id:`graphs`,glyph:`⌘`,label:`Graphs`},{id:`profiles`,glyph:`◇`,label:`Profiles`},{id:`memory`,glyph:`❖`,label:`Memory`},{id:`workspace`,glyph:`▤`,label:`Files`},{id:`runs`,glyph:`▶`,label:`Runs`},{id:`extensions`,glyph:`⬡`,label:`Tools`},{id:`settings`,glyph:`⚙`,label:`Settings`},{id:`operations`,glyph:`⌁`,label:`Ops`}];function Qe(){let[e,t]=(0,l.useState)(M),[n,r]=(0,l.useState)(`chat`),[i,a]=(0,l.useState)(null),[o,s]=(0,l.useState)([]),[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)([]),[p,h]=(0,l.useState)([]),[g,y]=(0,l.useState)(``),[b,x]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[w,T]=(0,l.useState)(!1),[D,O]=(0,l.useState)(!1),[k,ae]=(0,l.useState)(!1),[P,F]=(0,l.useState)(!0),[I,se]=(0,l.useState)([]),L=(0,l.useCallback)(e=>{x(e),window.setTimeout(()=>x(``),2600)},[]),R=(0,l.useCallback)(async()=>{let e=await _(`/api/conversations`);s(e.conversations||[]),u(t=>{let n=e.conversations||[];return t&&n.some(e=>e.id===t)?t:n[0]?.id||null})},[]),ce=(0,l.useCallback)(async()=>{let e=await _(`/api/bootstrap`);a(e),s(e.conversations||[]),f(e.profiles?.profiles||[]),h(e.settings?.fields||[]),u(t=>{let n=e.conversations||[];return t&&n.some(e=>e.id===t)?t:n[0]?.id||null})},[]);(0,l.useEffect)(()=>{m(),(async()=>{try{if((await _(`/api/onboarding/readiness`).catch(()=>({ready:!0}))).ready===!1){O(!0);return}await ce()}catch(e){y(e.message)}finally{T(!0)}})()},[ce]);let le=(0,l.useMemo)(()=>[{key:`k`,mod:!0,describe:`Focus the message box`,run:()=>{r(`chat`),window.setTimeout(()=>document.querySelector(`.composer textarea`)?.focus(),0)}},{key:`f`,mod:!0,describe:`Search conversations`,run:()=>{r(`chat`),window.setTimeout(()=>document.querySelector(`#conversationSearch`)?.focus(),0)}},{key:`n`,mod:!0,shift:!0,describe:`New chat`,run:()=>{r(`chat`),window.dispatchEvent(new CustomEvent(`magent:new-conversation`))}},...Ze.map((e,t)=>({key:String(t+1),mod:!0,describe:`Go to ${e.label}`,run:()=>r(e.id)})),{key:`/`,describe:`Show keyboard shortcuts`,run:()=>C(!0)},{key:`Escape`,describe:`Close a dialog or clear an error`,run:()=>{C(!1),y(``)}}],[]);(0,l.useEffect)(()=>E(le),[le]);let ue=(0,l.useCallback)(async(e,t=[],n=i?.project||`.`,a=``,o=i?.permission_mode||`balanced`)=>{try{let i=await v(`/api/conversations`,{kind:e,profiles:t,project:n,coordinator:a,permission_mode:o,title:e===`bot`&&t[0]?`Chat with ${t[0]}`:`New conversation`});await R(),u(i.conversation.id),ae(!1),r(`chat`)}catch(e){y(e.message)}},[i?.project,i?.permission_mode,R]),fe=(0,l.useCallback)(async e=>{try{await v(`/api/conversations/delete`,{conversation_id:e.id}),await R(),L(`Conversation deleted.`)}catch(e){y(e.message)}},[L,R]),me=(0,l.useCallback)(async(e,t)=>{try{await v(`/api/conversations/update`,{conversation_id:e.id,project:t}),await R(),L(`Conversation project updated.`)}catch(e){y(e.message)}},[L,R]),ge=(0,l.useCallback)(async(e,t)=>{try{await v(`/api/conversations/update`,{conversation_id:e.id,permission_mode:t}),await R(),L(`Permission mode changed to ${t}.`)}catch(e){y(e.message)}},[L,R]),_e=o.find(e=>e.id===c)||null;return w?D?(0,N.jsx)(oe,{setError:y,onReady:()=>{O(!1),ce()}}):(0,N.jsxs)(`div`,{className:`app-shell`,children:[(0,N.jsxs)(`aside`,{className:`rail`,"aria-label":`Primary navigation`,children:[(0,N.jsx)(`div`,{className:`brand-mark`,children:`M`}),(0,N.jsx)(`nav`,{children:Ze.map(e=>(0,N.jsxs)(`button`,{type:`button`,className:`rail-button ${n===e.id?`active`:``}`,"data-view":e.id,title:e.label,"aria-current":n===e.id?`page`:void 0,onClick:()=>r(e.id),children:[e.glyph,(0,N.jsx)(`span`,{children:e.label})]},e.id))}),(0,N.jsxs)(`button`,{type:`button`,className:`rail-button theme-toggle`,onClick:()=>{let n=j(e);t(n),A(n),ie(n)},"aria-label":`Theme: ${te[e]}. Activate to change.`,title:`Theme: ${te[e]}`,children:[(0,N.jsx)(`span`,{"aria-hidden":`true`,children:re[e]}),(0,N.jsx)(`i`,{children:ne[e]})]}),(0,N.jsx)(`div`,{className:`connection-dot`,title:`Local connection`})]}),(0,N.jsx)(pe,{conversations:o,activeId:c,setActiveId:u,profiles:d,boot:i,onCreate:ue,onDelete:fe,onProjectChange:me,setError:y,open:k,setOpen:ae}),(0,N.jsxs)(`main`,{className:`workspace`,children:[g&&(0,N.jsxs)(`div`,{className:`error-banner`,role:`alert`,children:[g,(0,N.jsx)(`button`,{type:`button`,onClick:()=>y(``),"aria-label":`Dismiss`,children:`×`})]}),(0,N.jsxs)(`div`,{className:`workspace-head`,children:[(0,N.jsx)(`button`,{className:`icon-button sidebar-open`,type:`button`,onClick:()=>ae(!0),"aria-label":`Show conversations`,children:`☰`}),(0,N.jsx)(`div`,{className:`workspace-title`,children:n===`chat`?(0,N.jsx)(`h1`,{children:_e?.title||`New conversation`}):(0,N.jsx)(`b`,{})}),(0,N.jsx)(`button`,{className:`icon-button`,type:`button`,onClick:()=>F(e=>!e),"aria-label":`Toggle conversation details`,"aria-expanded":P,children:`☷`})]}),n===`chat`&&(0,N.jsx)(de,{active:_e,refresh:R,setError:y,notify:L,context:I,clearContext:()=>se([])}),n===`bots`&&(0,N.jsx)(ve,{profiles:d,setError:y,onStart:()=>r(`chat`),refresh:R,setActiveId:u}),(0,N.jsx)(`div`,{className:`persistent-view ${n===`graphs`?`active`:``}`,"aria-hidden":n!==`graphs`,children:(0,N.jsx)(Ae,{profiles:d,setError:y,notify:L})}),(0,N.jsx)(`div`,{className:`persistent-view ${n===`profiles`?`active`:``}`,"aria-hidden":n!==`profiles`,children:(0,N.jsx)(Ne,{profiles:d,refresh:ce,setError:y,notify:L})}),n===`memory`&&(0,N.jsx)(Ie,{setError:y,notify:L}),n===`workspace`&&(0,N.jsx)(Be,{selected:I,setSelected:se,activeConversation:c,setError:y,notify:L}),n===`runs`&&(0,N.jsx)(Ve,{setError:y,notify:L}),n===`extensions`&&(0,N.jsx)(qe,{setError:y,notify:L}),n===`settings`&&(0,N.jsx)(Le,{fields:p,profiles:d,refresh:ce,setError:y,notify:L}),n===`operations`&&(0,N.jsx)(ze,{setError:y})]}),P&&(0,N.jsx)(he,{active:_e,boot:i,onPermissionChange:ge,onClose:()=>F(!1)}),S&&(0,N.jsx)(Je,{rows:[...le.map(e=>({describe:e.describe,keys:ee(e)})),{describe:`Send the message`,keys:`Enter`},{describe:`Newline in the message box`,keys:`Shift+Enter`}],onClose:()=>C(!1)}),(0,N.jsx)(Xe,{setError:y,notify:L}),b&&(0,N.jsx)(`div`,{className:`toast`,role:`status`,children:b})]}):(0,N.jsxs)(`div`,{className:`splash`,children:[(0,N.jsx)(`div`,{className:`brand-mark`,children:`M`}),(0,N.jsx)(`p`,{children:`Opening the workspace…`})]})}(0,u.createRoot)(document.getElementById(`root`)).render((0,N.jsx)(l.StrictMode,{children:(0,N.jsx)(Qe,{})})); \ No newline at end of file diff --git a/src/magent/webui/static/index.html b/src/magent/webui/static/index.html index a3d33a7..6e1e09b 100644 --- a/src/magent/webui/static/index.html +++ b/src/magent/webui/static/index.html @@ -5,7 +5,7 @@ MagAgent - + diff --git a/src/magent/workbench_maintenance.py b/src/magent/workbench_maintenance.py index 786e864..373f361 100644 --- a/src/magent/workbench_maintenance.py +++ b/src/magent/workbench_maintenance.py @@ -30,7 +30,9 @@ def workbench_stats(store: Any) -> dict[str, Any]: "store": path.stem, "path": str(path), "bytes": size, - "records": len(data) if isinstance(data, list) else (len(data) if isinstance(data, dict) else 0), + "records": len(data) + if isinstance(data, list) + else (len(data) if isinstance(data, dict) else 0), "high_volume": path.stem in HIGH_VOLUME_STORES, } ) @@ -58,20 +60,24 @@ def prune_workbench( keep_counts = dict(HIGH_VOLUME_STORES) if keep is not None: keep_counts = {name: max(0, keep) for name in keep_counts} - changes = [] + changes: list[dict[str, Any]] = [] for name, keep_count in keep_counts.items(): path = Path(store.root) / f"{name}.json" data = _read_json(path) if not isinstance(data, list): continue original = len(data) - recent = [item for item in data if _record_time(item) and _record_time(item) >= cutoff] + recent = [ + item for item in data if (stamp := _record_time(item)) is not None and stamp >= cutoff + ] kept_by_count = data[-keep_count:] if keep_count else [] merged = _dedupe_records([*recent, *kept_by_count]) removed = original - len(merged) if removed > 0 and not dry_run: store.write(name, merged) - changes.append({"store": name, "before": original, "after": len(merged), "removed": removed}) + changes.append( + {"store": name, "before": original, "after": len(merged), "removed": removed} + ) removed_total = sum(int(item["removed"]) for item in changes) return { "ok": True, @@ -149,7 +155,10 @@ def _dir_size(path: Path) -> int: def _recommendations(files: list[dict[str, Any]], checkpoint_bytes: int) -> list[str]: recommendations = [] for item in files: - if item["store"] in HIGH_VOLUME_STORES and item["records"] > HIGH_VOLUME_STORES[item["store"]]: + if ( + item["store"] in HIGH_VOLUME_STORES + and item["records"] > HIGH_VOLUME_STORES[item["store"]] + ): recommendations.append(f"Prune `{item['store']}`; it has {item['records']} records.") if item["bytes"] > 2_000_000: recommendations.append(f"Compact `{item['store']}`; JSON file is over 2 MB.") diff --git a/src/magent/workbench_store.py b/src/magent/workbench_store.py index 47d0062..33e7d6b 100644 --- a/src/magent/workbench_store.py +++ b/src/magent/workbench_store.py @@ -23,6 +23,7 @@ import contextlib import json import os +import tempfile import time from collections.abc import Callable, Iterator from datetime import UTC, datetime @@ -85,13 +86,15 @@ def _lock_path(self, name: str) -> Path: # ------------------------------------------------------------------- io - def read(self, name: str, default: Any) -> Any: + def read(self, name: str, default: Any, *, strict: bool = False) -> Any: path = self._path(name) if not path.exists(): return default try: return json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as error: + if strict: + raise WorkbenchStoreError(f"Stored state requires recovery: {path}") from error # Do not hand back `default`: the caller will write it straight # back and the real data is gone for good. target = _quarantine(path, error) @@ -108,9 +111,9 @@ def write(self, name: str, data: Any) -> None: # temp file → fsync → atomic replace. A reader always sees either the # previous file or the complete new one, never a truncated middle. - temp = path.with_name(f".{path.name}.{os.getpid()}.tmp") + descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + temp = Path(name) try: - descriptor = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(descriptor, "w", encoding="utf-8") as handle: handle.write(payload) handle.flush() @@ -134,30 +137,36 @@ def write(self, name: str, data: Any) -> None: @contextlib.contextmanager def lock(self, name: str) -> Iterator[None]: """Hold an advisory lock across a read-modify-write cycle.""" - if fcntl is None: # pragma: no cover - Windows fallback - yield - return - path = self._lock_path(name) path.parent.mkdir(parents=True, exist_ok=True) deadline = time.monotonic() + LOCK_TIMEOUT_SECONDS descriptor = os.open(path, os.O_RDWR | os.O_CREAT, 0o600) try: + if fcntl is None: # pragma: no cover - exercised by Windows CI + import msvcrt + + if os.fstat(descriptor).st_size == 0: + os.write(descriptor, b"0") while True: try: - fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) + if fcntl is None: # pragma: no cover + os.lseek(descriptor, 0, os.SEEK_SET) + msvcrt.locking(descriptor, msvcrt.LK_NBLCK, 1) + else: + fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) break - except OSError: + except OSError as error: if time.monotonic() >= deadline: - # Never deadlock the CLI on a stale lock: proceed - # unlocked rather than hang forever. - self.warnings.append(f"Timed out waiting for {path.name}; proceeding unlocked") - break + raise WorkbenchStoreError(f"Timed out waiting for {path.name}") from error time.sleep(LOCK_RETRY_SECONDS) yield finally: with contextlib.suppress(OSError): - fcntl.flock(descriptor, fcntl.LOCK_UN) + if fcntl is None: # pragma: no cover + os.lseek(descriptor, 0, os.SEEK_SET) + msvcrt.locking(descriptor, msvcrt.LK_UNLCK, 1) + else: + fcntl.flock(descriptor, fcntl.LOCK_UN) os.close(descriptor) def mutate(self, name: str, default: Any, change: Callable[[Any], Any]) -> Any: @@ -187,7 +196,9 @@ def change(data: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, return self.mutate(name, [], change) def update_item(self, name: str, item_id: str, **updates: Any) -> dict[str, Any] | None: - def change(data: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: + def change( + data: list[dict[str, Any]], + ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: for record in data: if record.get("id") == item_id: record.update(updates) diff --git a/tests/unit/test_approval_broker.py b/tests/unit/test_approval_broker.py index fbea9c2..d994d40 100644 --- a/tests/unit/test_approval_broker.py +++ b/tests/unit/test_approval_broker.py @@ -11,6 +11,45 @@ from magent.workbench_store import WorkbenchStore +def test_external_decision_wakes_original_broker(tmp_path: Path): + from concurrent.futures import ThreadPoolExecutor + + owner, presenter = broker(tmp_path), broker(tmp_path) + with ThreadPoolExecutor() as pool: + run = pool.submit( + owner.request_legacy, + "Run: node --check app.js", + 2, + origin={"session_id": "session-1"}, + publish=lambda _: None, + timeout=5, + ) + request = wait_for_request(presenter) + with pytest.raises(ConflictError): + presenter.decide( + request["id"], + decision="approve", + scope="once", + actor={"id": "tester", "type": "human"}, + reviewed_digest="sha256:" + "0" * 64, + ) + presenter.decide( + request["id"], decision="approve", scope="once", actor={"id": "tester", "type": "human"} + ) + assert run.result(timeout=3) == "once" + + +def test_corrupt_authority_state_requires_recovery(tmp_path: Path): + from magent.workbench_store import WorkbenchStoreError + + instance = broker(tmp_path) + path = tmp_path / "aais_approvals.json" + path.write_text('{"pending":') + with pytest.raises(WorkbenchStoreError, match="recovery"): + instance.snapshot() + assert path.read_text() == '{"pending":' + + def broker(tmp_path: Path) -> ApprovalBroker: store = WorkbenchStore.__new__(WorkbenchStore) store.username = "test" @@ -207,3 +246,37 @@ def test_session_grant_is_exact_and_does_not_cross_sessions(tmp_path: Path): actor={"id": "tester", "type": "human", "authenticated_by": "test"}, ) other.join(20) + + +def test_dead_owner_is_recovered_without_new_approval(tmp_path, monkeypatch): + from aais import create_request + + instance = broker(tmp_path) + envelope = create_request( + action={ + "kind": "tool.call", + "name": "shell.exec", + "summary": "Check syntax", + "arguments": {}, + }, + origin={"harness": "magagent", "session_id": "fixture"}, + risk={"level": "low", "reasons": ["Protected action"]}, + choices=[ + {"decision": "approve", "scope": "once", "label": "Allow once"}, + {"decision": "deny", "scope": "once", "label": "Deny"}, + ], + sequence=1, + stream="test", + ) + state = instance._empty() + key = envelope["request"]["id"] + state["pending"][key] = envelope + state["owners"] = {key: 12345} + instance.store.write(instance.STORE_NAME, state) + monkeypatch.setattr(instance, "_owner_alive", lambda pid: False) + assert instance.recovery()["orphaned"] == [key] + assert instance.snapshot()["snapshot"]["pending"] == [] + with pytest.raises(ConflictError, match="issuing process stopped"): + instance.decide( + key, decision="approve", scope="once", actor={"id": "test", "type": "human"} + ) diff --git a/tests/unit/test_durability_and_state.py b/tests/unit/test_durability_and_state.py index 4bec1ad..fc5c9ac 100644 --- a/tests/unit/test_durability_and_state.py +++ b/tests/unit/test_durability_and_state.py @@ -87,7 +87,7 @@ def fail_replace(_source: Path, _target: Path) -> None: assert not list(store.root.glob("*.tmp")) @pytest.mark.skipif(os.name != "posix", reason="advisory locks are POSIX here") - def test_lock_timeout_warns_and_proceeds(self, store: WorkbenchStore, monkeypatch) -> None: + def test_lock_timeout_fails_closed(self, store: WorkbenchStore, monkeypatch) -> None: assert store_module.fcntl is not None def busy_lock(_descriptor: int, operation: int) -> None: @@ -98,10 +98,11 @@ def busy_lock(_descriptor: int, operation: int) -> None: monkeypatch.setattr(store_module.fcntl, "flock", busy_lock) monkeypatch.setattr(store_module.time, "monotonic", lambda: next(times)) - with store.lock("tasks"): - pass - - assert "Timed out waiting" in store.warnings[-1] + with ( + pytest.raises(store_module.WorkbenchStoreError, match="Timed out waiting"), + store.lock("tasks"), + ): + pytest.fail("A busy transaction must never proceed without its lock") def test_singular_uses_removesuffix(self) -> None: """`rstrip("s")` ate every trailing s: "progress" became "progre".""" diff --git a/tests/unit/test_process_liveness.py b/tests/unit/test_process_liveness.py new file mode 100644 index 0000000..0bc0585 --- /dev/null +++ b/tests/unit/test_process_liveness.py @@ -0,0 +1,26 @@ +"""Recovery inspection must never terminate its owner, particularly on Windows.""" + +import os +import subprocess +import sys + +from magent.process_liveness import process_alive + + +def test_liveness_preserves_running_child_and_detects_exit(): + env = { + k: v + for k, v in os.environ.items() + if not k.startswith("COV_CORE_") and k != "COVERAGE_PROCESS_START" + } + with subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"], env=env) as child: + try: + assert process_alive(child.pid) + assert child.poll() is None + finally: + child.terminate() + child.wait(timeout=10) + assert not process_alive(child.pid) + assert not process_alive(None) + assert not process_alive(-1) + assert not process_alive(0) diff --git a/webui/package-lock.json b/webui/package-lock.json index d887503..263476d 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -1,12 +1,12 @@ { "name": "magent-webui", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magent-webui", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "react": "19.2.8", "react-dom": "19.2.8" diff --git a/webui/package.json b/webui/package.json index 0b7719b..ad1a156 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,7 +1,7 @@ { "name": "magent-webui", "private": true, - "version": "1.2.0", + "version": "1.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/webui/src/views/ApprovalCenter.tsx b/webui/src/views/ApprovalCenter.tsx index 9f6f393..c6e532a 100644 --- a/webui/src/views/ApprovalCenter.tsx +++ b/webui/src/views/ApprovalCenter.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useState } from "react"; +import { useModalFocus } from "./use-modal-focus"; +import { useCallback, useEffect, useRef, useState } from "react"; import { post, request } from "../api"; type Choice = { @@ -60,14 +61,15 @@ export function ApprovalCenter({ async (approval: PendingApproval, choice: Choice) => { setBusy(true); try { - const result = await post<{ ok: boolean; error?: string }>("/api/approvals/decide", { + const result = await post<{ ok: boolean; error?: string; resolution?: { resolution: { outcome: string; message?: string } } }>("/api/approvals/decide", { request_id: approval.id, + action_digest: approval.action_digest, decision: choice.decision, scope: choice.scope, decision_id: `dec_web_${crypto.randomUUID().replaceAll("-", "")}`, }); if (!result.ok) throw new Error(result.error || "The approval could not be resolved."); - notify(choice.decision === "approve" ? "Permission approved." : "Permission denied."); + notify(`Harness outcome: ${result.resolution?.resolution.outcome ?? "unknown"}`); await refresh(); } catch (problem) { setError((problem as Error).message); @@ -80,12 +82,16 @@ export function ApprovalCenter({ ); const approval = pending[0]; + const dialog = useRef(null); + useModalFocus(dialog, approval?.id); if (!approval) return null; return (
-
+
PERMISSION REQUIRED · {pending.length} PENDING

{approval.action.summary}

+

From {Object.values(approval.origin).join(" · ")}

+ {approval.expires_at &&

Expires {new Date(approval.expires_at).toLocaleString()}

}
{approval.risk.level} risk {approval.risk.reasons.map((reason) => {reason})} @@ -102,7 +108,7 @@ export function ApprovalCenter({ {approval.choices.map((choice) => ( } - {state === "paused" && } + {["paused", "waiting"].includes(state) && } {!['succeeded', 'cancelled'].includes(state) && } {['failed', 'cancelled'].includes(state) && }
diff --git a/webui/src/views/use-modal-focus.ts b/webui/src/views/use-modal-focus.ts new file mode 100644 index 0000000..40a8699 --- /dev/null +++ b/webui/src/views/use-modal-focus.ts @@ -0,0 +1,34 @@ +import { useEffect, type RefObject } from "react"; + +export function useModalFocus(ref: RefObject, identity?: string) { + useEffect(() => { + const dialog = ref.current; + if (!dialog || !identity) return; + const previous = document.activeElement as HTMLElement | null; + const controls = () => Array.from(dialog.querySelectorAll( + 'button:not(:disabled), input, select, textarea, a[href], summary, [tabindex="0"]', + )); + (controls()[0] ?? dialog).focus(); + const keydown = (event: KeyboardEvent) => { + if (event.key !== "Tab") return; + const items = controls(); + if (!items.length) { event.preventDefault(); dialog.focus(); return; } + const first = items[0], last = items[items.length - 1]; + if (event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement))) { + event.preventDefault(); last.focus(); + } else if (!event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement))) { + event.preventDefault(); first.focus(); + } + }; + const focusin = (event: FocusEvent) => { + if (!dialog.contains(event.target as Node)) (controls()[0] ?? dialog).focus(); + }; + document.addEventListener("keydown", keydown); + document.addEventListener("focusin", focusin); + return () => { + document.removeEventListener("keydown", keydown); + document.removeEventListener("focusin", focusin); + if (previous?.isConnected) previous.focus(); + }; + }, [ref, identity]); +}