diff --git a/app/breakglass/agent_session.py b/app/breakglass/agent_session.py index a360e40..7e209d2 100644 --- a/app/breakglass/agent_session.py +++ b/app/breakglass/agent_session.py @@ -1,26 +1,13 @@ -"""Drive the breakglass Claude agent and stream its work to the browser. +"""Claude CLI argv + stream-json → UI-event translation for the breakglass agent. -Each chat turn runs ``claude -p --output-format stream-json`` in the session's -persistent workspace; the first turn opens the session with ``--session-id`` and -later turns ``--resume`` it, so the conversation has memory across turns. The -CLI's JSON events are translated to a small, stable SSE vocabulary the UI -renders (``session`` / ``text`` / ``tool`` / ``result`` / ``error``) — we do not -leak the raw event firehose to the client. - -Subprocesses use ``asyncio.create_subprocess_exec`` (list argv, no shell): the -prompt and ids are argv elements, never interpreted by a shell. +The session lifecycle (running turns, attaching clients) lives in ``session.py``; +this module is just the two helpers it builds on: + * ``_turn_argv`` — the no-shell list argv for one ``claude -p`` turn. + * ``translate_event`` — map a raw stream-json event to the small UI vocabulary + (session / text / tool / result), dropping the hook/thinking-token noise. """ -import asyncio -import json -import os -from subprocess import PIPE -from typing import AsyncIterator - from . import config -# Sessions we've already opened (so the next turn resumes instead of re-creating). -_started: set[str] = set() - def _turn_argv(session_id: str, prompt: str, resume: bool, model: str) -> list[str]: argv = [ @@ -66,7 +53,7 @@ def translate_event(obj: dict) -> dict | None: }) if not events: return None - # The server flattens a "batch" into individual SSE frames. + # The session log flattens a "batch" into individual events. return events[0] if len(events) == 1 else {"kind": "batch", "events": events} if etype == "result": @@ -78,68 +65,3 @@ def translate_event(obj: dict) -> dict | None: } return None - - -async def run_turn( - session_id: str, prompt: str, model: str | None = None -) -> AsyncIterator[dict]: - """Run one chat turn, yielding translated UI events as they arrive.""" - resume = session_id in _started - model = model or config.DEFAULT_MODEL - workspace = os.path.join(config.SESSIONS_DIR, session_id) - os.makedirs(workspace, exist_ok=True) - - argv = _turn_argv(session_id, prompt, resume, model) - proc = await asyncio.create_subprocess_exec( - *argv, cwd=workspace, stdout=PIPE, stderr=PIPE, - ) - _started.add(session_id) - assert proc.stdout is not None and proc.stderr is not None - - try: - async def _pump() -> AsyncIterator[dict]: - async for raw in proc.stdout: - line = raw.decode(errors="replace").strip() - if not line: - continue - try: - obj = json.loads(line) - except json.JSONDecodeError: - continue - ev = translate_event(obj) - if ev is None: - continue - if ev.get("kind") == "batch": - for sub in ev["events"]: - yield sub - else: - yield ev - - async for ev in _with_timeout(_pump(), config.TURN_TIMEOUT_SECONDS): - yield ev - except asyncio.TimeoutError: - proc.kill() - await proc.wait() - yield {"kind": "error", "error": f"turn timed out after {config.TURN_TIMEOUT_SECONDS}s"} - return - - await proc.wait() - if proc.returncode not in (0, None): - err = (await proc.stderr.read()).decode(errors="replace") - yield {"kind": "error", "error": err.strip()[:500] or f"exit {proc.returncode}"} - - -async def _with_timeout(agen: AsyncIterator[dict], timeout: float) -> AsyncIterator[dict]: - """Yield from an async generator but raise TimeoutError if the WHOLE turn - exceeds ``timeout`` seconds (a wedged agent shouldn't stream forever).""" - loop = asyncio.get_event_loop() - deadline = loop.time() + timeout - it = agen.__aiter__() - while True: - remaining = deadline - loop.time() - if remaining <= 0: - raise asyncio.TimeoutError - try: - yield await asyncio.wait_for(it.__anext__(), timeout=remaining) - except StopAsyncIteration: - return diff --git a/app/breakglass/config.py b/app/breakglass/config.py index 785d17f..6f3e86a 100644 --- a/app/breakglass/config.py +++ b/app/breakglass/config.py @@ -25,6 +25,9 @@ MAX_CONCURRENT_TURNS = int(os.environ.get("BREAKGLASS_MAX_CONCURRENT_TURNS", "2" TURN_TIMEOUT_SECONDS = int(os.environ.get("BREAKGLASS_TURN_TIMEOUT_SECONDS", "1800")) # A single PVE power verb must return fast; a wedged host shouldn't hang the UI. PVE_VERB_TIMEOUT_SECONDS = int(os.environ.get("BREAKGLASS_PVE_VERB_TIMEOUT_SECONDS", "120")) +# How long an idle attach stream waits before emitting an SSE keepalive comment +# (keeps proxies/CDN from closing the long-lived connection). +SSE_KEEPALIVE_SECONDS = int(os.environ.get("BREAKGLASS_SSE_KEEPALIVE_SECONDS", "20")) # Auth. The app sits behind the ingress `auth = "required"` resilience proxy # (Authentik SSO, basic-auth fallback when Authentik is down). We additionally diff --git a/app/breakglass/server.py b/app/breakglass/server.py index 9a7201f..215b5a2 100644 --- a/app/breakglass/server.py +++ b/app/breakglass/server.py @@ -1,38 +1,44 @@ """Breakglass FastAPI app — the in-cluster emergency recovery UI. +The chat uses the tmux/attach model (see session.py): the server owns the +conversation; clients attach over SSE and the turn keeps running if they +disconnect. + Routes: - GET /health — liveness (no auth) - GET / — the single-page UI (static) - POST /api/session — open a chat session, returns {session_id} - POST /api/chat — run one turn, streams SSE events (text/tool/result) - POST /api/pve/{verb} — LLM-independent PVE power verb (manual buttons) - GET /api/pve/verbs — list allowed verbs + which mutate + GET /health — liveness (no auth) + GET / — the single-page UI (static) + POST /api/session — create a session, returns {session_id} + GET /api/session/{id}/stream — ATTACH (SSE): replay + live tail + POST /api/session/{id}/prompt — run a turn (detached; survives disconnect) + POST /api/session/{id}/cancel — stop the in-flight turn + GET /api/pve/verbs — list allowed verbs + which mutate + POST /api/pve/{verb} — LLM-independent PVE power verb (buttons) Everything under /api requires auth (edge Authentik header or bearer token). """ -import json import os -import uuid -from fastapi import Depends, FastAPI, HTTPException +from fastapi import Depends, FastAPI, Header, HTTPException from fastapi.responses import FileResponse, JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field -from . import agent_session, config, pve +from . import config, pve from .auth import require_auth +from .session import SessionManager, attach_stream app = FastAPI(title="Claude Breakglass") _STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") +manager = SessionManager() + class SessionResponse(BaseModel): session_id: str -class ChatRequest(BaseModel): - session_id: str +class PromptRequest(BaseModel): prompt: str = Field(..., min_length=1) model: str | None = None @@ -44,30 +50,53 @@ async def health(): @app.post("/api/session", response_model=SessionResponse) async def open_session(_identity: str = Depends(require_auth)): - # Claude wants a UUID for --session-id. - return SessionResponse(session_id=str(uuid.uuid4())) + return SessionResponse(session_id=manager.create().id) -@app.post("/api/chat") -async def chat(req: ChatRequest, _identity: str = Depends(require_auth)): - """Stream one chat turn as Server-Sent Events. The browser reads the - response body incrementally (fetch + ReadableStream).""" - - async def _sse(): - try: - async for ev in agent_session.run_turn(req.session_id, req.prompt, req.model): - yield f"data: {json.dumps(ev)}\n\n" - except Exception as exc: # noqa: BLE001 — surface any failure to the UI - yield f"data: {json.dumps({'kind': 'error', 'error': str(exc)[:500]})}\n\n" - yield f"data: {json.dumps({'kind': 'done'})}\n\n" - +@app.get("/api/session/{session_id}/stream") +async def attach( + session_id: str, + _identity: str = Depends(require_auth), + last_event_id: str | None = Header(default=None, alias="Last-Event-ID"), +): + """Attach to a session (SSE). Replays the conversation so far, then tails + live. On an EventSource auto-reconnect the browser sends Last-Event-ID, so we + replay only what was missed.""" + session = manager.get(session_id) + if session is None: + raise HTTPException(status_code=404, detail="session not found") + try: + leid = int(last_event_id) if last_event_id is not None else None + except ValueError: + leid = None return StreamingResponse( - _sse(), + attach_stream(session, leid), media_type="text/event-stream", - headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive"}, ) +@app.post("/api/session/{session_id}/prompt") +async def prompt(session_id: str, req: PromptRequest, _identity: str = Depends(require_auth)): + """Start a turn. It runs DETACHED (keeps going if the client disconnects); + output is delivered via the attach stream, not this response.""" + session = manager.get(session_id) + if session is None: + raise HTTPException(status_code=404, detail="session not found") + if not session.start_turn(req.prompt, req.model): + raise HTTPException(status_code=409, detail="a turn is already running") + return {"status": "started"} + + +@app.post("/api/session/{session_id}/cancel") +async def cancel(session_id: str, _identity: str = Depends(require_auth)): + session = manager.get(session_id) + if session is None: + raise HTTPException(status_code=404, detail="session not found") + cancelled = await session.cancel() + return {"cancelled": cancelled} + + @app.get("/api/pve/verbs") async def pve_verbs(_identity: str = Depends(require_auth)): return { diff --git a/app/breakglass/session.py b/app/breakglass/session.py new file mode 100644 index 0000000..c6558ed --- /dev/null +++ b/app/breakglass/session.py @@ -0,0 +1,201 @@ +"""Attachable server-side sessions — the tmux model for the breakglass chat. + +Instead of the client owning conversation state, the SERVER owns it and clients +*attach*. A turn runs as a detached task that keeps going if the client +disconnects (you can background the phone / hit a tunnel blip and the agent +keeps working); its output is appended to a per-session event log and broadcast +to every attached subscriber. A client attaches over SSE, gets the log replayed +(or only the part it missed, via Last-Event-ID), then tails live — exactly like +re-attaching to a tmux session. ``EventSource`` reconnects natively, so the +"re-attach" needs zero client logic. + +This module owns the lifecycle; ``agent_session`` still provides the claude +argv + the stream-json→UI-event translation (all subprocesses use the no-shell +list-argv form), and ``config`` the knobs. +""" +import asyncio +import json +import os +import uuid +from subprocess import PIPE +from typing import AsyncIterator + +from . import agent_session, config + + +class Session: + """One conversation. Owns the replay log + live subscribers + the in-flight + turn. The claude ``session_id`` is reused with ``--resume`` so the agent + keeps its own context across turns.""" + + def __init__(self, session_id: str): + self.id = session_id + # The replay log: every UI event, in order. Index in the list IS the + # SSE event id, so a reconnecting client replays only what it missed. + self.events: list[dict] = [] + self._subscribers: set[asyncio.Queue] = set() + self._turn: asyncio.Task | None = None + self._proc: asyncio.subprocess.Process | None = None + self._started = False # has claude opened this session id yet? + + # ── event log + fan-out ──────────────────────────────────────────────── + def add_event(self, event: dict) -> dict: + """Append an event to the log and broadcast it to attached clients.""" + stored = {**event, "id": len(self.events)} + self.events.append(stored) + for q in list(self._subscribers): + q.put_nowait(stored) + return stored + + def subscribe(self) -> asyncio.Queue: + q: asyncio.Queue = asyncio.Queue() + self._subscribers.add(q) + return q + + def unsubscribe(self, q: asyncio.Queue) -> None: + self._subscribers.discard(q) + + @property + def turn_active(self) -> bool: + return self._turn is not None and not self._turn.done() + + # ── running a turn (detached from any client) ────────────────────────── + def start_turn(self, prompt: str, model: str | None = None) -> bool: + """Kick off a turn as a background task. Returns False if one is already + running (one turn at a time per session).""" + if self.turn_active: + return False + self.add_event({"kind": "user", "text": prompt}) + self._turn = asyncio.create_task(self._run_turn(prompt, model)) + return True + + async def _run_turn(self, prompt: str, model: str | None) -> None: + model = model or config.DEFAULT_MODEL + resume = self._started + argv = agent_session._turn_argv(self.id, prompt, resume, model) + try: + self._proc = await asyncio.create_subprocess_exec( + *argv, cwd=_workspace_for(self.id), stdout=PIPE, stderr=PIPE, + ) + except Exception as exc: # noqa: BLE001 + self.add_event({"kind": "error", "error": f"could not start agent: {exc}"}) + self.add_event({"kind": "turn_end"}) + return + self._started = True + assert self._proc.stdout is not None and self._proc.stderr is not None + + try: + async def _pump(): + async for raw in self._proc.stdout: + line = raw.decode(errors="replace").strip() + if not line: + continue + try: + obj = json.loads(line) + except json.JSONDecodeError: + continue + ev = agent_session.translate_event(obj) + if ev is None: + continue + if ev.get("kind") == "batch": + for sub in ev["events"]: + self.add_event(sub) + else: + self.add_event(ev) + + await asyncio.wait_for(_pump(), timeout=config.TURN_TIMEOUT_SECONDS) + await self._proc.wait() + if self._proc.returncode not in (0, None): + err = (await self._proc.stderr.read()).decode(errors="replace") + self.add_event({"kind": "error", "error": err.strip()[:500] or f"exit {self._proc.returncode}"}) + except asyncio.TimeoutError: + await self._kill_proc() + self.add_event({"kind": "error", "error": f"turn timed out after {config.TURN_TIMEOUT_SECONDS}s"}) + except asyncio.CancelledError: + await self._kill_proc() + self.add_event({"kind": "cancelled"}) + raise + finally: + self._proc = None + self.add_event({"kind": "turn_end"}) + + async def _kill_proc(self) -> None: + if self._proc and self._proc.returncode is None: + try: + self._proc.kill() + await self._proc.wait() + except ProcessLookupError: + pass + + async def cancel(self) -> bool: + """Stop the in-flight turn. Returns True if a turn was cancelled.""" + if not self.turn_active: + return False + await self._kill_proc() + if self._turn: + self._turn.cancel() + try: + await self._turn + except (asyncio.CancelledError, Exception): # noqa: BLE001 + pass + return True + + +def _workspace_for(session_id: str) -> str: + path = os.path.join(config.SESSIONS_DIR, session_id) + os.makedirs(path, exist_ok=True) + return path + + +class SessionManager: + """Holds all live sessions. The breakglass is single-operator, so callers + typically reuse one persistent session; multiple are still supported.""" + + def __init__(self): + self.sessions: dict[str, Session] = {} + + def create(self) -> Session: + sid = str(uuid.uuid4()) + s = Session(sid) + self.sessions[sid] = s + return s + + def get(self, session_id: str) -> Session | None: + return self.sessions.get(session_id) + + def get_or_create(self, session_id: str | None) -> Session: + if session_id and session_id in self.sessions: + return self.sessions[session_id] + return self.create() + + +async def attach_stream(session: Session, last_event_id: int | None) -> AsyncIterator[str]: + """Yield SSE frames for an attached client: first the replay (everything, or + only events after ``last_event_id`` on a reconnect), then live events as they + arrive. Each frame carries an ``id:`` so EventSource resumes precisely.""" + q = session.subscribe() + try: + start = 0 if last_event_id is None else last_event_id + 1 + backlog = session.events[start:] + for ev in backlog: + yield _sse_frame(ev) + # Tell the client the replay is done and it's now live. + yield "event: caught-up\ndata: {}\n\n" + + seen = backlog[-1]["id"] if backlog else (last_event_id if last_event_id is not None else -1) + while True: + try: + ev = await asyncio.wait_for(q.get(), timeout=config.SSE_KEEPALIVE_SECONDS) + except asyncio.TimeoutError: + yield ": keepalive\n\n" # comment frame keeps the connection warm + continue + if ev["id"] <= seen: + continue + seen = ev["id"] + yield _sse_frame(ev) + finally: + session.unsubscribe(q) + + +def _sse_frame(event: dict) -> str: + return f"id: {event['id']}\ndata: {json.dumps(event)}\n\n" diff --git a/app/breakglass/static/apple-touch-icon.png b/app/breakglass/static/apple-touch-icon.png new file mode 100644 index 0000000..e5763f2 Binary files /dev/null and b/app/breakglass/static/apple-touch-icon.png differ diff --git a/app/breakglass/static/assets/index-BoWC1Onq.css b/app/breakglass/static/assets/index-BoWC1Onq.css new file mode 100644 index 0000000..0c9823f --- /dev/null +++ b/app/breakglass/static/assets/index-BoWC1Onq.css @@ -0,0 +1 @@ +:root{--bg-0:#06080b;--bg-1:#0b0f14;--bg-2:#10161d;--bg-3:#161e27;--bg-term:#05070a;--line:#1c2530;--line-strong:#2a3744;--line-bright:#3a4a5a;--ink:#e9eff5;--ink-dim:#9bb0c0;--ink-faint:#8499ab;--cyan:#3dd1d6;--cyan-bright:#62e3e7;--cyan-dim:#1f6f72;--cyan-deep:#0e3133;--amber:#f5b657;--amber-dim:#6a5226;--green:#5ddb8e;--green-dim:#1f5f3d;--danger:#ff4d4d;--danger-bright:#ff6363;--danger-deep:#7a1717;--danger-glow:#ff4d4d59;--radius:11px;--radius-sm:8px;--radius-lg:16px;--mono:"Berkeley Mono", ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", "Fira Code", "Source Code Pro", Menlo, Consolas, "Liberation Mono", monospace;--sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--shadow-panel:0 1px 0 #ffffff06 inset, 0 18px 44px -26px #000000f2;--shadow-sheet:0 -22px 48px -12px #000000b3;--safe-top:env(safe-area-inset-top,0px);--safe-bottom:env(safe-area-inset-bottom,0px);--safe-left:env(safe-area-inset-left,0px);--safe-right:env(safe-area-inset-right,0px);--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body{overscroll-behavior:none;height:100%;margin:0;overflow:hidden}body{background-color:var(--bg-0);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background-image:radial-gradient(120% 78% at 86% -12%,#3dd1d614,#0000 55%),radial-gradient(90% 70% at 8% 112%,#f5b6570b,#0000 52%),repeating-linear-gradient(0deg,#ffffff03 0 1px,#0000 1px 3px);background-attachment:fixed}#app{height:100dvh}button{font-family:var(--mono);cursor:pointer}button:disabled{cursor:not-allowed}::selection{background:#3dd1d647}*{scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line-strong);background-clip:content-box;border:2px solid #0000;border-radius:99px}::-webkit-scrollbar-thumb:hover{background:var(--line-bright);background-clip:content-box}@keyframes rise-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.rise-in{animation:.5s cubic-bezier(.22,.61,.36,1) both rise-in;animation-delay:var(--d,0s)}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.chip.svelte-2zgsrv{background:var(--bg-3);border:1px solid var(--line-strong);border-left:2px solid var(--cyan-dim);max-width:100%;font-family:var(--mono);vertical-align:baseline;border-radius:6px;align-items:baseline;gap:6px;margin:3px 4px 3px 0;padding:3px 9px;font-size:12px;line-height:1.45;display:inline-flex}.cog.svelte-2zgsrv{color:var(--cyan);font-size:11px;transform:translateY(1px)}.name.svelte-2zgsrv{color:var(--ink);font-weight:600}.sep.svelte-2zgsrv{color:var(--ink-faint)}.cmd.svelte-2zgsrv{color:var(--amber);font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.chat.svelte-1bi93vx{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow:hidden}.chat-head.svelte-1bi93vx{border-bottom:1px solid var(--line);background:linear-gradient(#ffffff05,#0000);flex:none;align-items:baseline;gap:12px;padding:12px 18px;display:flex}.chat-head-label.svelte-1bi93vx{font-family:var(--mono);text-transform:uppercase;letter-spacing:.2em;color:var(--cyan);font-size:11px}.chat-head-hint.svelte-1bi93vx{color:var(--ink-faint);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.stream.svelte-1bi93vx{scroll-behavior:smooth;flex-direction:column;flex:1;gap:14px;min-height:0;padding:20px 16px 10px;display:flex;overflow-y:auto}.empty.svelte-1bi93vx{text-align:center;max-width:470px;color:var(--ink-dim);margin:auto;padding:24px 14px}.empty.dim.svelte-1bi93vx{opacity:.8}.empty-mark.svelte-1bi93vx{color:var(--cyan-dim);text-shadow:0 0 26px #3dd1d64d;margin-bottom:14px;font-size:42px;line-height:1;animation:3.6s ease-in-out infinite svelte-1bi93vx-lamp-breathe}@keyframes svelte-1bi93vx-lamp-breathe{0%,to{opacity:.7}50%{opacity:1}}.empty-title.svelte-1bi93vx{font-family:var(--mono);color:var(--ink);letter-spacing:.01em;margin:0 0 8px;font-size:15px}.empty-sub.svelte-1bi93vx{color:var(--ink-faint);margin:0;font-size:13px;line-height:1.6}.empty-sub.svelte-1bi93vx strong:where(.svelte-1bi93vx){color:var(--ink-dim);font-weight:600}.row.svelte-1bi93vx{display:flex}.row--user.svelte-1bi93vx{justify-content:flex-end}.row--assistant.svelte-1bi93vx{justify-content:flex-start}.bubble.svelte-1bi93vx{word-wrap:break-word;overflow-wrap:anywhere;border-radius:13px;max-width:88%;padding:11px 14px;font-size:14px;line-height:1.62}.bubble--user.svelte-1bi93vx{border:1px solid var(--cyan-dim);color:#d8f6f7;white-space:pre-wrap;font-family:var(--sans);background:linear-gradient(#123036,#0d2329);border-bottom-right-radius:4px}.bubble--assistant.svelte-1bi93vx{background:var(--bg-2);border:1px solid var(--line-strong);color:var(--ink);border-bottom-left-radius:4px}.prose.svelte-1bi93vx{white-space:pre-wrap}.thinking.svelte-1bi93vx,.working-dots.svelte-1bi93vx{align-items:center;gap:4px;display:inline-flex}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx){background:var(--amber);opacity:.4;border-radius:50%;width:6px;height:6px;animation:1.2s ease-in-out infinite svelte-1bi93vx-blink}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2){animation-delay:.18s}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3){animation-delay:.36s}@keyframes svelte-1bi93vx-blink{0%,80%,to{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}.turn-note.svelte-1bi93vx{border-radius:var(--radius-sm);font-family:var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;flex-wrap:wrap;align-items:baseline;gap:8px;margin-top:10px;padding:7px 10px;font-size:12px;line-height:1.5;display:flex}.turn-note--ok.svelte-1bi93vx{border:1px solid var(--green-dim);color:#bff5d3;background:#5ddb8e12}.turn-note--error.svelte-1bi93vx{border:1px solid var(--amber-dim);color:#f7d49a;background:#f5b6570f}.turn-note--muted.svelte-1bi93vx{border:1px solid var(--line-strong);color:var(--ink-faint);background:#ffffff05}.turn-note-tag.svelte-1bi93vx{text-transform:uppercase;letter-spacing:.14em;opacity:.85;border:1px solid;border-radius:4px;padding:1px 6px;font-size:10px}.turn-note-body.svelte-1bi93vx{flex:1;min-width:0}.turn-note-time.svelte-1bi93vx{color:var(--ink-faint);margin-left:auto}.dock.svelte-1bi93vx{border-top:1px solid var(--line);background:linear-gradient(#0000,#ffffff04);flex:none}.presets.svelte-1bi93vx{scrollbar-width:none;-webkit-overflow-scrolling:touch;gap:8px;padding:11px 12px 4px;display:flex;overflow-x:auto;-webkit-mask-image:linear-gradient(90deg,#0000 0,#000 14px calc(100% - 18px),#0000 100%);mask-image:linear-gradient(90deg,#0000 0,#000 14px calc(100% - 18px),#0000 100%)}.presets.svelte-1bi93vx::-webkit-scrollbar{display:none}.preset.svelte-1bi93vx{border:1px solid var(--line-strong);background:var(--bg-2);min-height:38px;color:var(--ink-dim);font-family:var(--mono);letter-spacing:.02em;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:7px;padding:0 13px;font-size:12.5px;transition:border-color .15s,color .15s,background .15s,transform 60ms;display:inline-flex}.preset.svelte-1bi93vx:hover:not(:disabled){border-color:var(--cyan-dim);color:var(--ink);background:var(--bg-3)}.preset.svelte-1bi93vx:active:not(:disabled){transform:translateY(1px)}.preset.svelte-1bi93vx:disabled{opacity:.4}.preset-icon.svelte-1bi93vx{color:var(--cyan);font-size:12px}.composer.svelte-1bi93vx{padding:8px 12px calc(12px + var(--safe-bottom))}.working-bar.svelte-1bi93vx{font-family:var(--mono);color:var(--amber);letter-spacing:.02em;align-items:center;gap:10px;padding:2px 4px 9px;font-size:12px;display:flex}.composer-row.svelte-1bi93vx{align-items:flex-end;gap:10px;display:flex}textarea.svelte-1bi93vx{resize:none;background:var(--bg-2);min-height:48px;max-height:160px;color:var(--ink);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font-family:var(--sans);field-sizing:content;outline:none;flex:1;padding:13px;font-size:16px;line-height:1.5;transition:border-color .15s,box-shadow .15s}textarea.svelte-1bi93vx::placeholder{color:var(--ink-faint)}textarea.svelte-1bi93vx:focus{border-color:var(--cyan-dim);box-shadow:0 0 0 3px #3dd1d61f}textarea.svelte-1bi93vx:disabled{opacity:.55}.send.svelte-1bi93vx,.stop.svelte-1bi93vx{border-radius:var(--radius-sm);letter-spacing:.05em;flex:none;align-self:stretch;min-width:82px;min-height:48px;padding:0 18px;font-size:13px;font-weight:600;transition:filter .15s,border-color .15s,opacity .15s,background .15s}.send.svelte-1bi93vx{border:1px solid var(--cyan-dim);color:#d8f6f7;background:linear-gradient(#16464a,#0e3438)}.send.svelte-1bi93vx:hover:not(:disabled){filter:brightness(1.24);border-color:var(--cyan)}.send.svelte-1bi93vx:disabled{opacity:.4;background:var(--bg-2);border-color:var(--line-strong);color:var(--ink-faint)}.stop.svelte-1bi93vx{border:1px solid var(--line-bright);background:var(--bg-3);color:var(--ink);justify-content:center;align-items:center;gap:8px;display:inline-flex}.stop.svelte-1bi93vx:hover{border-color:var(--ink-faint);filter:brightness(1.1)}.stop-glyph.svelte-1bi93vx{background:var(--amber);border-radius:2px;width:10px;height:10px;animation:1s ease-in-out infinite svelte-1bi93vx-lamp-pulse;box-shadow:0 0 8px #f5b6578c}@keyframes svelte-1bi93vx-lamp-pulse{0%,to{opacity:.8;transform:scale(.85)}50%{opacity:1;transform:scale(1.08)}}.panel.svelte-1qihpg4{background:var(--bg-1);border:1px solid var(--line);border-top:2px solid var(--danger-deep);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow-y:auto}.panel-head.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px 12px}.panel-head-row.svelte-1qihpg4{align-items:center;gap:9px;display:flex}.hazard.svelte-1qihpg4{color:var(--danger);filter:drop-shadow(0 0 8px var(--danger-glow));font-size:15px}h2.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.12em;color:var(--ink);margin:0;font-size:13px}.panel-sub.svelte-1qihpg4{color:var(--ink-faint);margin:9px 0 0;font-size:11.5px;line-height:1.55}.loading.svelte-1qihpg4{font-family:var(--mono);color:var(--ink-faint);padding:22px 16px;font-size:12px}.group.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px}.group-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.18em;color:var(--ink-faint);align-items:center;gap:8px;margin-bottom:11px;font-size:10.5px;display:flex}.group-label--danger.svelte-1qihpg4{color:var(--danger-bright)}.group-tag.svelte-1qihpg4{letter-spacing:.1em;border:1px solid var(--line-strong);color:var(--ink-faint);border-radius:4px;padding:2px 6px;font-size:9.5px}.group-tag--danger.svelte-1qihpg4{border-color:var(--danger-deep);color:var(--danger-bright);background:#ff4d4d0f}.btn-row.svelte-1qihpg4{flex-wrap:wrap;gap:9px;display:flex}.vbtn.svelte-1qihpg4{border-radius:var(--radius-sm);letter-spacing:.05em;text-transform:lowercase;justify-content:center;align-items:center;gap:8px;min-height:44px;padding:10px 16px;font-size:13px;font-weight:600;transition:filter .14s,border-color .14s,background .14s,transform 60ms;display:inline-flex}.vbtn.svelte-1qihpg4:active:not(:disabled){transform:translateY(1px)}.vbtn.svelte-1qihpg4:disabled{opacity:.4}.vbtn-label.svelte-1qihpg4{line-height:1}.vbtn--safe.svelte-1qihpg4{background:var(--bg-2);color:var(--ink);border:1px solid var(--line-strong)}.vbtn--safe.svelte-1qihpg4:hover:not(:disabled){border-color:var(--cyan-dim);background:var(--bg-3)}.danger-list.svelte-1qihpg4{flex-direction:column;gap:12px;display:flex}.danger-item.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid #0000}.danger-item--headline.svelte-1qihpg4{border-color:var(--danger-deep);background:#ff4d4d0b;padding:11px}.vbtn--danger.svelte-1qihpg4{width:100%;color:var(--danger-bright);border:1px solid var(--danger-deep);border-left:3px solid var(--danger);text-shadow:0 0 12px var(--danger-glow);background:linear-gradient(#ff4d4d29,#ff4d4d12)}.vbtn--danger.svelte-1qihpg4:hover:not(:disabled){background:linear-gradient(180deg, var(--danger), var(--danger-bright));color:#1a0606;border-color:var(--danger-bright);text-shadow:none;filter:drop-shadow(0 4px 14px var(--danger-glow))}.vbtn--headline.svelte-1qihpg4{padding:12px 15px;font-size:14px}.headline-badge.svelte-1qihpg4{text-transform:uppercase;letter-spacing:.14em;background:var(--danger);color:#1a0606;border-radius:999px;padding:2px 7px;font-size:9px;font-weight:700}.danger-blurb.svelte-1qihpg4{color:var(--ink-faint);margin:7px 2px 0;font-size:11.5px;line-height:1.5}.danger-item--headline.svelte-1qihpg4 .danger-blurb:where(.svelte-1qihpg4){color:#f0b0b0}.confirm.svelte-1qihpg4{border:1px solid var(--danger);border-radius:var(--radius-sm);background:#ff4d4d1a;margin-top:10px;padding:11px 12px;animation:.16s ease-out svelte-1qihpg4-confirm-in}@keyframes svelte-1qihpg4-confirm-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.confirm-text.svelte-1qihpg4{color:#ffe0e0;margin-bottom:10px;font-size:12.5px;line-height:1.5;display:block}.confirm-text.svelte-1qihpg4 strong:where(.svelte-1qihpg4){color:#fff;font-family:var(--mono);text-transform:uppercase;letter-spacing:.04em}.confirm-actions.svelte-1qihpg4{gap:9px;display:flex}.confirm-yes.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--danger-bright);background:var(--danger);color:#1a0606;letter-spacing:.06em;text-transform:uppercase;flex:1;min-height:44px;padding:10px;font-size:13px;font-weight:700;transition:filter .14s}.confirm-yes.svelte-1qihpg4:hover:not(:disabled){filter:brightness(1.12)}.confirm-no.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);min-height:44px;color:var(--ink-dim);letter-spacing:.04em;text-transform:uppercase;flex:1;padding:10px;font-size:13px;transition:border-color .14s,color .14s}.confirm-no.svelte-1qihpg4:hover:not(:disabled){border-color:var(--ink-faint);color:var(--ink)}.confirm-yes.svelte-1qihpg4:disabled,.confirm-no.svelte-1qihpg4:disabled{opacity:.5}.spin.svelte-1qihpg4{border:2px solid #e6edf340;border-top-color:var(--cyan);border-radius:50%;flex:none;width:13px;height:13px;animation:.7s linear infinite svelte-1qihpg4-spin}.spin--danger.svelte-1qihpg4{border-color:#ff4d4d4d;border-top-color:var(--danger-bright)}@keyframes svelte-1qihpg4-spin{to{transform:rotate(360deg)}}.out.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-term);margin:14px 16px 16px;overflow:hidden}.out--ok.svelte-1qihpg4{border-color:var(--green-dim)}.out--fail.svelte-1qihpg4{border-color:var(--danger-deep)}.out-head.svelte-1qihpg4{border-bottom:1px solid var(--line);background:#ffffff05;justify-content:space-between;align-items:center;padding:8px 11px;display:flex}.out-verb.svelte-1qihpg4{font-family:var(--mono);color:var(--ink);letter-spacing:.04em;font-size:12px}.out-verb.svelte-1qihpg4:before{content:"$ pve ";color:var(--ink-faint)}.out-status.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em;border:1px solid;border-radius:4px;padding:2px 7px;font-size:10.5px}.out-status--ok.svelte-1qihpg4{color:var(--green)}.out-status--fail.svelte-1qihpg4{color:var(--danger-bright)}.out-pre.svelte-1qihpg4{font-family:var(--mono);color:#c7d6e2;white-space:pre-wrap;overflow-wrap:anywhere;max-height:320px;margin:0;padding:11px 12px;font-size:12px;line-height:1.55;overflow-y:auto}.out-stderr-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.16em;color:var(--danger-bright);padding:6px 12px 0;font-size:10px}.out-pre--stderr.svelte-1qihpg4{color:#f3b6b6}.out-pre--empty.svelte-1qihpg4{color:var(--ink-faint);font-style:italic}.block-error.svelte-1qihpg4{border:1px solid var(--danger-deep);border-left:3px solid var(--danger);border-radius:var(--radius-sm);color:#ffd5d5;background:#ff4d4d12;margin:14px 16px;padding:11px 13px;font-size:12.5px;line-height:1.5}.retry.svelte-1qihpg4{border:1px solid var(--danger-deep);color:var(--danger-bright);background:0 0;border-radius:5px;margin-left:8px;padding:3px 9px;font-size:11px}.retry.svelte-1qihpg4:hover{background:#ff4d4d1f}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4),details.out.svelte-1qihpg4>summary:where(.svelte-1qihpg4){cursor:pointer;-webkit-user-select:none;user-select:none;list-style:none}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4)::-webkit-details-marker{display:none}details.out.svelte-1qihpg4>summary:where(.svelte-1qihpg4)::-webkit-details-marker{display:none}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4):before,details.out.svelte-1qihpg4>summary:where(.svelte-1qihpg4):before{content:"▾";width:11px;color:var(--ink-faint);margin-right:4px;font-size:9px;transition:transform .15s;display:inline-block}details.group.svelte-1qihpg4:not([open])>summary:where(.svelte-1qihpg4):before,details.out.svelte-1qihpg4:not([open])>summary:where(.svelte-1qihpg4):before{transform:rotate(-90deg)}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4){padding:3px 0}.out-head.svelte-1qihpg4 .out-status:where(.svelte-1qihpg4){margin-left:auto}.out-pre.svelte-1qihpg4{max-height:46vh;overflow:auto}.shell.svelte-1n46o8q{max-width:1520px;height:100%;padding-left:var(--safe-left);padding-right:var(--safe-right);flex-direction:column;margin:0 auto;display:flex}.rail.svelte-1n46o8q{padding:max(10px, var(--safe-top)) 14px 10px;border-bottom:1px solid var(--line);background:linear-gradient(#3dd1d608,#0000 60%),linear-gradient(#ffffff04,#0000);flex:none;justify-content:space-between;align-items:center;gap:10px;display:flex}.rail-title.svelte-1n46o8q{align-items:center;gap:10px;min-width:0;display:flex}.brand-mark.svelte-1n46o8q{color:var(--cyan);filter:drop-shadow(0 0 10px #3dd1d659);flex:none;display:inline-flex}.brand-mark.svelte-1n46o8q .frac:where(.svelte-1n46o8q){color:var(--amber);stroke:var(--amber);opacity:.85}h1.svelte-1n46o8q{font-family:var(--mono);letter-spacing:.04em;color:var(--ink);white-space:nowrap;margin:0;font-size:16px;font-weight:600}.accent.svelte-1n46o8q{color:var(--cyan);text-shadow:0 0 18px #3dd1d666}.rail-right.svelte-1n46o8q{flex:none;align-items:center;gap:8px;display:flex}.lamp-wrap.svelte-1n46o8q{font-family:var(--mono);align-items:center;gap:8px;padding:0 4px;font-size:12px;display:inline-flex}.lamp.svelte-1n46o8q{background:var(--ink-faint);border-radius:50%;flex:none;width:10px;height:10px;position:relative}.lamp.svelte-1n46o8q:after{content:"";opacity:0;border:1px solid;border-radius:50%;position:absolute;inset:-4px}.lamp--live.svelte-1n46o8q{background:var(--cyan);color:var(--cyan);animation:3.6s ease-in-out infinite svelte-1n46o8q-lamp-breathe;box-shadow:0 0 10px 1px #3dd1d6a6}.lamp--live.svelte-1n46o8q:after{animation:3.6s ease-out infinite svelte-1n46o8q-lamp-ring}.lamp--connecting.svelte-1n46o8q{background:var(--cyan-dim);color:var(--cyan);animation:1.4s ease-in-out infinite svelte-1n46o8q-lamp-blink}.lamp--working.svelte-1n46o8q{background:var(--amber);color:var(--amber);animation:1s ease-in-out infinite svelte-1n46o8q-lamp-pulse;box-shadow:0 0 10px 1px #f5b657b3}.lamp--working.svelte-1n46o8q:after{animation:1s ease-out infinite svelte-1n46o8q-lamp-ring}.lamp--error.svelte-1n46o8q{background:var(--danger);color:var(--danger);box-shadow:0 0 10px 1px var(--danger-glow);animation:1.2s ease-in-out infinite svelte-1n46o8q-lamp-pulse}@keyframes svelte-1n46o8q-lamp-breathe{0%,to{opacity:.6}50%{opacity:1}}@keyframes svelte-1n46o8q-lamp-blink{0%,to{opacity:.35}50%{opacity:.9}}@keyframes svelte-1n46o8q-lamp-pulse{0%,to{opacity:.75;transform:scale(.82)}50%{opacity:1;transform:scale(1.12)}}@keyframes svelte-1n46o8q-lamp-ring{0%{opacity:.5;transform:scale(.6)}70%{opacity:0;transform:scale(1.8)}to{opacity:0;transform:scale(1.8)}}.lamp-text.svelte-1n46o8q{letter-spacing:.04em;color:var(--ink-dim);text-overflow:ellipsis;white-space:nowrap;max-width:88px;overflow:hidden}.lamp-text--live.svelte-1n46o8q .sid:where(.svelte-1n46o8q){color:var(--cyan);letter-spacing:.06em}.lamp-text--working.svelte-1n46o8q{color:var(--amber)}.lamp-text--error.svelte-1n46o8q{color:var(--danger-bright)}.lamp-text--connecting.svelte-1n46o8q{color:var(--ink-faint)}.sid.svelte-1n46o8q{font-family:var(--mono)}@media (width<=439px){.lamp-text.svelte-1n46o8q{display:none}.lamp-wrap.svelte-1n46o8q{padding:0}}.rail-btn.svelte-1n46o8q{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);min-height:44px;color:var(--ink-dim);letter-spacing:.03em;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:border-color .15s,background .15s,color .15s;display:inline-flex}.rail-btn.svelte-1n46o8q:hover:not(:disabled){border-color:var(--line-bright);color:var(--ink)}.rail-btn.svelte-1n46o8q:active:not(:disabled){background:var(--bg-3)}.rail-btn.svelte-1n46o8q:disabled{opacity:.42}.rail-btn--vm.svelte-1n46o8q{border-color:var(--amber-dim);color:var(--amber)}.rail-btn--vm.svelte-1n46o8q:hover:not(:disabled){border-color:var(--amber);color:var(--amber)}.bolt.svelte-1n46o8q{font-size:13px;line-height:1}.rail-note.svelte-1n46o8q{border:1px solid var(--danger-deep);color:#ffd9d9;border-radius:var(--radius-sm);background:#ff4d4d12;border-left-width:3px;flex-wrap:wrap;flex:none;align-items:center;gap:6px 12px;margin:10px 12px 0;padding:10px 13px;font-size:13px;line-height:1.5;display:flex}.rail-note-aside.svelte-1n46o8q{color:#f0b8b8}.rail-note-aside.svelte-1n46o8q strong:where(.svelte-1n46o8q){color:#fff;font-family:var(--mono)}.rail-note-retry.svelte-1n46o8q{border:1px solid var(--danger-deep);color:var(--danger-bright);background:0 0;border-radius:6px;min-height:36px;margin-left:auto;padding:6px 12px;font-size:12px}.rail-note-retry.svelte-1n46o8q:hover{background:#ff4d4d1f}.toast.svelte-1n46o8q{border:1px solid var(--line-strong);border-left:3px solid var(--amber);background:var(--bg-2);color:var(--amber);border-radius:var(--radius-sm);font-family:var(--mono);flex:none;margin:10px 12px 0;padding:9px 13px;font-size:12.5px;line-height:1.45;animation:.28s ease-out both rise-in}.stage.svelte-1n46o8q{flex:1;min-width:0;min-height:0;padding:10px;display:flex}.chat-pane.svelte-1n46o8q{flex:1;min-width:0;min-height:0;display:flex}.controls-pane.svelte-1n46o8q{z-index:40;background:var(--bg-1);border-top:1px solid var(--line-strong);border-radius:var(--radius-lg) var(--radius-lg) 0 0;max-height:88dvh;box-shadow:var(--shadow-sheet);padding:8px 14px calc(14px + var(--safe-bottom));flex-direction:column;transition:transform .3s cubic-bezier(.32,.72,0,1);display:flex;position:fixed;bottom:0;left:0;right:0;transform:translateY(102%);animation:none!important}.controls-pane.open.svelte-1n46o8q{transform:translateY(0)}.sheet-grip.svelte-1n46o8q{background:var(--line-bright);border-radius:99px;flex:none;width:40px;height:4px;margin:4px auto 10px}.controls-head.svelte-1n46o8q{flex:none;justify-content:space-between;align-items:center;margin-bottom:10px;display:flex}.controls-head-title.svelte-1n46o8q{font-family:var(--mono);text-transform:uppercase;letter-spacing:.2em;color:var(--amber);font-size:11px}.sheet-close.svelte-1n46o8q{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);width:40px;height:40px;color:var(--ink-dim);font-size:14px}.sheet-close.svelte-1n46o8q:active{background:var(--bg-3)}.sheet-backdrop.svelte-1n46o8q{z-index:30;-webkit-backdrop-filter:blur(1.5px);backdrop-filter:blur(1.5px);opacity:0;pointer-events:none;background:#0204079e;border:0;padding:0;transition:opacity .24s;position:fixed;inset:0}.sheet-backdrop.show.svelte-1n46o8q{opacity:1;pointer-events:auto}@media (width>=900px){.rail.svelte-1n46o8q{padding:14px 18px}h1.svelte-1n46o8q{font-size:19px}.stage.svelte-1n46o8q{grid-template-columns:minmax(0,1fr) 384px;gap:16px;padding:16px 18px 18px;display:grid}.chat-pane.svelte-1n46o8q{display:flex}.rail-btn--vm.svelte-1n46o8q{display:none}.controls-pane.svelte-1n46o8q{max-height:none;box-shadow:none;z-index:auto;border:none;border-radius:0;padding:0;position:static;transform:none;animation:.5s cubic-bezier(.22,.61,.36,1) both rise-in!important;animation-delay:var(--d,0s)!important}.sheet-grip.svelte-1n46o8q,.controls-head.svelte-1n46o8q,.sheet-backdrop.svelte-1n46o8q{display:none}} diff --git a/app/breakglass/static/assets/index-CLbKo1Yx.js b/app/breakglass/static/assets/index-CLbKo1Yx.js new file mode 100644 index 0000000..b6d571b --- /dev/null +++ b/app/breakglass/static/assets/index-CLbKo1Yx.js @@ -0,0 +1,6 @@ +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})(),typeof window<`u`&&((window.__svelte??={}).v??=new Set).add(`5`);var e={},t=Symbol(`uninitialized`),n=`http://www.w3.org/1999/xhtml`,r=Array.isArray,i=Array.prototype.indexOf,a=Array.prototype.includes,o=Array.from,s=Object.defineProperty,c=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyDescriptors,u=Object.prototype,d=Array.prototype,f=Object.getPrototypeOf,p=Object.isExtensible,m=()=>{};function h(e){for(var t=0;t{e=n,t=r}),resolve:e,reject:t}}var _=1024,v=2048,y=4096,b=8192,x=16384,S=32768,C=1<<25,w=65536,T=1<<19,ee=1<<20,te=1<<25,ne=65536,re=1<<21,ie=1<<22,ae=1<<23,oe=Symbol(`$state`),se=Symbol(`legacy props`),ce=Symbol(``),le=Symbol(`attributes`),ue=Symbol(`class`),de=Symbol(`style`),fe=Symbol(`text`),pe=Symbol(`form reset`),me=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},he=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function ge(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function _e(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function ve(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function ye(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function be(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function xe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function Se(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ce(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function we(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Te(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Ee(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function De(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}function Oe(){console.warn(`https://svelte.dev/e/derived_inert`)}function ke(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ae(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var E=!1;function je(e){E=e}var D;function O(t){if(t===null)throw ke(),e;return D=t}function Me(){return O(on(D))}function k(t){if(E){if(on(D)!==null)throw ke(),e;D=t}}function Ne(e=1){if(E){for(var t=e,n=D;t--;)n=on(n);D=n}}function Pe(e=!0){for(var t=0,n=D;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else (r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=on(n);e&&n.remove(),n=i}}function Fe(t){if(!t||t.nodeType!==8)throw ke(),e;return t.data}function Ie(e){return e===this.v}function Le(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function Re(e){return!Le(e,this.v)}var ze=!1,Be=!1,A=null;function Ve(e){A=e}function He(e,t=!1,n){A={p:A,i:!1,c:null,e:null,s:e,x:null,r:W,l:Be&&!t?{s:null,u:null,$:[]}:null}}function Ue(e){var t=A,n=t.e;if(n!==null){t.e=null;for(var r of n)Cn(r)}return e!==void 0&&(t.x=e),t.i=!0,A=t.p,e??{}}function We(){return!Be||A!==null&&A.l===null}var Ge=[];function Ke(){var e=Ge;Ge=[],h(e)}function qe(e){if(Ge.length===0&&!Et){var t=Ge;queueMicrotask(()=>{t===Ge&&Ke()})}Ge.push(e)}function Je(){for(;Ge.length>0;)Ke()}function Ye(e){var t=W;if(t===null)return H.f|=ae,e;if(!(t.f&32768)&&!(t.f&4))throw e;Xe(e,t)}function Xe(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}var Ze=~(v|y|_);function j(e,t){e.f=e.f&Ze|t}function Qe(e){e.f&512||e.deps===null?j(e,_):j(e,y)}function $e(e){if(e!==null)for(let t of e)!(t.f&2)||!(t.f&65536)||(t.f^=ne,$e(t.deps))}function et(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),$e(e.deps),j(e,_)}var tt=!1,nt=!1;function rt(e){var t=nt;try{return nt=!1,[e(),nt]}finally{nt=t}}function it(e){let t=0,n=Gt(0),r;return()=>{bn()&&(J(n),Dn(()=>(t===0&&(r=cr(()=>e(()=>Xt(n)))),t+=1,()=>{qe(()=>{--t,t===0&&(r?.(),r=void 0,Xt(n))})})))}}var at=w|T;function ot(e,t,n,r){new st(e,t,n,r)}var st=class{parent;is_pending=!1;transform_error;#e;#t=E?D:null;#n;#r;#i;#a=null;#o=null;#s=null;#c=null;#l=0;#u=0;#d=!1;#f=new Set;#p=new Set;#m=null;#h=it(()=>(this.#m=Gt(this.#l),()=>{this.#m=null}));constructor(e,t,n,r){this.#e=e,this.#n=t,this.#r=e=>{var t=W;t.b=this,t.f|=128,n(e)},this.parent=W.b,this.transform_error=r??this.parent?.transform_error??(e=>e),this.#i=On(()=>{if(E){let e=this.#t;Me();let t=e.data===`[!`;if(e.data.startsWith(`[?`)){let t=JSON.parse(e.data.slice(2));this.#_(t)}else t?this.#v():this.#g()}else this.#y()},at),E&&(this.#e=D)}#g(){try{this.#a=B(()=>this.#r(this.#e))}catch(e){this.error(e)}}#_(e){let t=this.#n.failed;t&&(this.#s=B(()=>{t(this.#e,()=>e,()=>()=>{})}))}#v(){let e=this.#n.pending;e&&(this.is_pending=!0,this.#o=B(()=>e(this.#e)),qe(()=>{var e=this.#c=document.createDocumentFragment(),t=I();e.append(t),this.#a=this.#x(()=>B(()=>this.#r(t))),this.#u===0&&(this.#e.before(e),this.#c=null,Pn(this.#o,()=>{this.#o=null}),this.#b(M))}))}#y(){try{if(this.is_pending=this.has_pending_snippet(),this.#u=0,this.#l=0,this.#a=B(()=>{this.#r(this.#e)}),this.#u>0){var e=this.#c=document.createDocumentFragment();Rn(this.#a,e);let t=this.#n.pending;this.#o=B(()=>t(this.#e))}else this.#b(M)}catch(e){this.error(e)}}#b(e){this.is_pending=!1,e.transfer_effects(this.#f,this.#p)}defer_effect(e){et(e,this.#f,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#n.pending}#x(e){var t=W,n=H,r=A;Wn(this.#i),U(this.#i),Ve(this.#i.ctx);try{return Mt.ensure(),e()}catch(e){return Ye(e),null}finally{Wn(t),U(n),Ve(r)}}#S(e,t){if(!this.has_pending_snippet()){this.parent&&this.parent.#S(e,t);return}this.#u+=e,this.#u===0&&(this.#b(t),this.#o&&Pn(this.#o,()=>{this.#o=null}),this.#c&&=(this.#e.before(this.#c),null))}update_pending_count(e,t){this.#S(e,t),this.#l+=e,!(!this.#m||this.#d)&&(this.#d=!0,qe(()=>{this.#d=!1,this.#m&&qt(this.#m,this.#l)}))}get_effect_pending(){return this.#h(),J(this.#m)}error(e){if(!this.#n.onerror&&!this.#n.failed)throw e;M?.is_fork?(this.#a&&M.skip_effect(this.#a),this.#o&&M.skip_effect(this.#o),this.#s&&M.skip_effect(this.#s),M.oncommit(()=>{this.#C(e)})):this.#C(e)}#C(e){this.#a&&=(V(this.#a),null),this.#o&&=(V(this.#o),null),this.#s&&=(V(this.#s),null),E&&(O(this.#t),Ne(),O(Pe()));var t=this.#n.onerror;let n=this.#n.failed;var r=!1,i=!1;let a=()=>{if(r){Ae();return}r=!0,i&&De(),this.#s!==null&&Pn(this.#s,()=>{this.#s=null}),this.#x(()=>{this.#y()})},o=e=>{try{i=!0,t?.(e,a),i=!1}catch(e){Xe(e,this.#i&&this.#i.parent)}n&&(this.#s=this.#x(()=>{try{return B(()=>{var t=W;t.b=this,t.f|=128,n(this.#e,()=>e,()=>a)})}catch(e){return Xe(e,this.#i.parent),null}}))};qe(()=>{var t;try{t=this.transform_error(e)}catch(e){Xe(e,this.#i&&this.#i.parent);return}typeof t==`object`&&t&&typeof t.then==`function`?t.then(o,e=>Xe(e,this.#i&&this.#i.parent)):o(t)})}};function ct(e,t,n,r){let i=We()?ft:gt;var a=e.filter(e=>!e.settled),o=t.map(i);if(n.length===0&&a.length===0){r(o);return}var s=W,c=lt(),l=a.length===1?a[0].promise:a.length>1?Promise.all(a.map(e=>e.promise)):null;function u(e){if(!(s.f&16384)){c();try{r([...o,...e])}catch(e){Xe(e,s)}ut()}}var d=dt();if(n.length===0){l.then(()=>u([])).finally(d);return}function f(){Promise.all(n.map(e=>mt(e))).then(u).catch(e=>Xe(e,s)).finally(d)}l?l.then(()=>{c(),f(),ut()}):f()}function lt(){var e=W,t=H,n=A,r=M;return function(i=!0){Wn(e),U(t),Ve(n),i&&!(e.f&16384)&&(r?.activate(),r?.apply())}}function ut(e=!0){Wn(null),U(null),Ve(null),e&&M?.deactivate()}function dt(){var e=W,t=e.b,n=M,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ft(e){var n=2|v;return W!==null&&(W.f|=T),{ctx:A,deps:null,effects:null,equals:Ie,f:n,fn:e,reactions:null,rv:0,v:t,wv:0,parent:W,ac:null}}var pt=Symbol(`obsolete`);function mt(e,n,r){let i=W;i===null&&_e();var a=void 0,o=Gt(t),s=!H,c=new Set;return En(()=>{var t=W,n=g();a=n.promise;try{Promise.resolve(e()).then(n.resolve,e=>{e!==me&&n.reject(e)}).finally(ut)}catch(e){n.reject(e),ut()}var r=M;if(s){if(t.f&32768)var l=dt();if(i.b?.is_rendered())r.async_deriveds.get(t)?.reject(pt);else for(let e of c.values())e.reject(pt);c.add(n),r.async_deriveds.set(t,n)}let u=(e,t=void 0)=>{l?.(),c.delete(n),t!==pt&&(r.activate(),t?(o.f|=ae,qt(o,t)):(o.f&8388608&&(o.f^=ae),qt(o,e)),r.deactivate())};n.promise.then(u,e=>u(null,e||`unknown`))}),xn(()=>{for(let e of c)e.reject(pt)}),new Promise(e=>{function t(n){function r(){n===a?e(o):t(a)}n.then(r,r)}t(a)})}function ht(e){let t=ft(e);return ze||Kn(t),t}function gt(e){let t=ft(e);return t.equals=Re,t}function _t(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;nthis.schedule(e)){var n=this.#f.get(e);if(n){this.#f.delete(e);for(var r of n.d)j(r,v),t(r);for(r of n.m)j(r,y),t(r)}this.#p.add(e)}#g(){this.#e=!0,At++>1e3&&(this.#S(),Pt());for(let e of this.#u)this.#d.delete(e),j(e,v),this.schedule(e);for(let e of this.#d)j(e,y),this.schedule(e);let t=this.#c;this.#c=[],this.apply();var n=Ot=[],r=[],i=kt=[];for(let e of t)try{this.#_(e,n,r)}catch(t){throw Vt(e),this.#h()||this.discard(),t}if(M=null,i.length>0){var a=e.ensure();for(let e of i)a.schedule(e)}if(Ot=null,kt=null,this.#h()){this.#b(r),this.#b(n);for(let[e,t]of this.#f)Bt(e,t);i.length>0&&M.#g();return}let o=this.#v();if(o){this.#b(r),this.#b(n),o.#y(this);return}this.#u.clear(),this.#d.clear();for(let e of this.#r)e(this);this.#r.clear(),wt=this,It(r),It(n),wt=null,this.#s?.resolve();var s=M;if(this.#a===0&&(this.#c.length===0||s!==null)&&(this.#S(),ze&&(this.#x(),M=s)),this.#c.length>0)if(s!==null){let e=s;e.#c.push(...this.#c.filter(t=>!e.#c.includes(t)))}else s=this;s!==null&&s.#g()}#_(e,t,n){e.f^=_;for(var r=e.first;r!==null;){var i=r.f,a=(i&96)!=0;if(!(a&&i&1024||i&8192||this.#f.has(r))&&r.fn!==null){a?r.f^=_:i&4?t.push(r):ze&&i&16777224?n.push(r):$n(r)&&(i&16&&this.#d.add(r),ir(r));var o=r.first;if(o!==null){r=o;continue}}for(;r!==null;){var s=r.next;if(s!==null){r=s;break}r=r.parent}}}#v(){for(var e=this.#t;e!==null;){if(!e.is_fork){for(let[t,[,n]]of this.current)if(e.current.has(t)&&!n)return e}e=e.#t}return null}#y(e){for(let[t,n]of e.current)!this.previous.has(t)&&e.previous.has(t)&&this.previous.set(t,e.previous.get(t)),this.current.set(t,n);for(let[t,n]of e.async_deriveds){let e=this.async_deriveds.get(t);e&&n.promise.then(e.resolve).catch(e.reject)}e.async_deriveds.clear(),this.transfer_effects(e.#u,e.#d);let t=e=>{var n=e.reactions;if(n!==null)for(let e of n){var r=e.f;if(r&2)t(e);else{var i=e;r&4194320&&!this.async_deriveds.has(i)&&(this.#d.delete(i),j(i,v),this.schedule(i))}}};for(let e of this.current.keys())t(e);this.oncommit(()=>e.discard()),e.#S(),M=this,this.#g()}#b(e){for(var t=0;t!u.current.get(e)[1]);if(!(!u.#e||r.length===0)){var i=r.filter(e=>!this.current.has(e));if(i.length===0)e&&u.discard();else if(t.length>0){if(e)for(let e of this.#p)u.unskip_effect(e,e=>{e.f&4194320?u.schedule(e):u.#b([e])});u.activate();var a=new Set,o=new Map;for(var s of t)Lt(s,i,a,o);o=new Map;var c=[...u.current].filter(([e,t])=>{let n=this.current.get(e);return n?n[0]!==t[0]||n[1]!==t[1]:!0}).map(([e])=>e);if(c.length>0)for(let e of this.#l)!(e.f&155648)&&Rt(e,c,o)&&(e.f&4194320?(j(e,v),u.schedule(e)):u.#u.add(e));if(u.#c.length>0&&!u.#m){u.apply();for(var l of u.#c)u.#_(l,[],[]);u.#c=[]}u.deactivate()}}}}increment(e,t){if(this.#a+=1,e){let e=this.#o.get(t)??0;this.#o.set(t,e+1)}}decrement(e,t){if(--this.#a,e){let e=this.#o.get(t)??0;e===1?this.#o.delete(t):this.#o.set(t,e-1)}this.#m||(this.#m=!0,qe(()=>{this.#m=!1,this.linked&&this.flush()}))}transfer_effects(e,t){for(let t of e)this.#u.add(t);for(let e of t)this.#d.add(e);e.clear(),t.clear()}oncommit(e){this.#r.add(e)}ondiscard(e){this.#i.add(e)}settled(){return(this.#s??=g()).promise}static ensure(){if(M===null){let t=M=new e;!Dt&&!Et&&qe(()=>{t.#e||t.flush()})}return M}apply(){if(!ze||!this.is_fork&&this.#t===null&&this.#n===null){N=null;return}N=new Map;for(let[e,[t]]of this.current)N.set(e,t);for(let t=St;t!==null;t=t.#n)if(!(t===this||t.is_fork)){var e=!1;if(t.id0)){Ut.clear();for(let e of Ft){if(e.f&24576)continue;let t=[e],n=e.parent;for(;n!==null;)Ft.has(n)&&(Ft.delete(n),t.push(n)),n=n.parent;for(let e=t.length-1;e>=0;e--){let n=t[e];n.f&24576||ir(n)}}Ft.clear()}}Ft=null}}function Lt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(let i of e.reactions){let e=i.f;e&2?Lt(i,t,n,r):e&4194320&&!(e&2048)&&Rt(i,t,r)&&(j(i,v),zt(i))}}function Rt(e,t,n){let r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(let r of e.deps){if(a.call(t,r))return!0;if(r.f&2&&Rt(r,t,n))return n.set(r,!0),!0}return n.set(e,!1),!1}function zt(e){M.schedule(e)}function Bt(e,t){if(!(e.f&32&&e.f&1024)){e.f&2048?t.d.push(e):e.f&4096&&t.m.push(e),j(e,_);for(var n=e.first;n!==null;)Bt(n,t),n=n.next}}function Vt(e){j(e,_);for(var t=e.first;t!==null;)Vt(t),t=t.next}var Ht=new Set,Ut=new Map,Wt=!1;function Gt(e,t){return{f:0,v:e,reactions:null,equals:Ie,rv:0,wv:0}}function P(e,t){let n=Gt(e,t);return Kn(n),n}function Kt(e,t=!1,n=!0){let r=Gt(e);return t||(r.equals=Re),Be&&n&&A!==null&&A.l!==null&&(A.l.s??=[]).push(r),r}function F(e,t,n=!1){return H!==null&&(!Un||H.f&131072)&&We()&&H.f&4325394&&(Gn===null||!Gn.has(e))&&Ee(),qt(e,n?Qt(t):t,kt)}function qt(e,t,n=null){if(!e.equals(t)){Ut.set(e,Vn?t:e.v);var r=Mt.ensure();if(r.capture(e,t),e.f&2){let t=e;e.f&2048&&vt(t),N===null&&Qe(t)}e.wv=Qn(),Zt(e,v,n),We()&&W!==null&&W.f&1024&&!(W.f&96)&&(q===null?qn([e]):q.push(e)),!r.is_fork&&Ht.size>0&&!Wt&&Jt()}return t}function Jt(){Wt=!1;for(let e of Ht){e.f&1024&&j(e,y);let t;try{t=$n(e)}catch{t=!0}t&&ir(e)}Ht.clear()}function Yt(e,t=1){var n=J(e),r=t===1?n++:n--;return F(e,n),r}function Xt(e){F(e,e.v+1)}function Zt(e,t,n){var r=e.reactions;if(r!==null)for(var i=We(),a=r.length,o=0;o{if(Xn===l)return e();var t=H,n=Xn;U(null),Zn(l);var r=e();return U(t),Zn(n),r};return a&&i.set(`length`,P(e.length,s)),new Proxy(e,{defineProperty(e,t,n){(!(`value`in n)||n.configurable===!1||n.enumerable===!1||n.writable===!1)&&we();var r=i.get(t);return r===void 0?p(()=>{var e=P(n.value,s);return i.set(t,e),e}):F(r,n.value,!0),!0},deleteProperty(e,n){var r=i.get(n);if(r===void 0){if(n in e){let e=p(()=>P(t,s));i.set(n,e),Xt(o)}}else F(r,t),Xt(o);return!0},get(n,r,a){if(r===oe)return e;var o=i.get(r),l=r in n;if(o===void 0&&(!l||c(n,r)?.writable)&&(o=p(()=>P(Qt(l?n[r]:t),s)),i.set(r,o)),o!==void 0){var u=J(o);return u===t?void 0:u}return Reflect.get(n,r,a)},getOwnPropertyDescriptor(e,n){var r=Reflect.getOwnPropertyDescriptor(e,n);if(r&&`value`in r){var a=i.get(n);a&&(r.value=J(a))}else if(r===void 0){var o=i.get(n),s=o?.v;if(o!==void 0&&s!==t)return{enumerable:!0,configurable:!0,value:s,writable:!0}}return r},has(e,n){if(n===oe)return!0;var r=i.get(n),a=r!==void 0&&r.v!==t||Reflect.has(e,n);return(r!==void 0||W!==null&&(!a||c(e,n)?.writable))&&(r===void 0&&(r=p(()=>P(a?Qt(e[n]):t,s)),i.set(n,r)),J(r)===t)?!1:a},set(e,n,r,l){var u=i.get(n),d=n in e;if(a&&n===`length`)for(var f=r;fP(t,s)),i.set(f+``,m)):F(m,t)}if(u===void 0)(!d||c(e,n)?.writable)&&(u=p(()=>P(void 0,s)),F(u,Qt(r)),i.set(n,u));else{d=u.v!==t;var h=p(()=>Qt(r));F(u,h)}var g=Reflect.getOwnPropertyDescriptor(e,n);if(g?.set&&g.set.call(l,r),!d){if(a&&typeof n==`string`){var _=i.get(`length`),v=Number(n);Number.isInteger(v)&&v>=_.v&&F(_,v+1)}Xt(o)}return!0},ownKeys(e){J(o);var n=Reflect.ownKeys(e).filter(e=>{var n=i.get(e);return n===void 0||n.v!==t});for(var[r,a]of i)a.v!==t&&!(r in e)&&n.push(r);return n},setPrototypeOf(){Te()}})}new Set([`copyWithin`,`fill`,`pop`,`push`,`reverse`,`shift`,`sort`,`splice`,`unshift`]);var $t,en,tn,nn;function rn(){if($t===void 0){$t=window,en=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;tn=c(t,`firstChild`).get,nn=c(t,`nextSibling`).get,p(e)&&(e[ue]=void 0,e[le]=null,e[de]=void 0,e.__e=void 0),p(n)&&(n[fe]=void 0)}}function I(e=``){return document.createTextNode(e)}function an(e){return tn.call(e)}function on(e){return nn.call(e)}function L(e,t){if(!E)return an(e);var n=an(D);if(n===null)n=D.appendChild(I());else if(t&&n.nodeType!==3){var r=I();return n?.before(r),O(r),r}return t&&dn(n),O(n),n}function sn(e,t=!1){if(!E){var n=an(e);return n instanceof Comment&&n.data===``?on(n):n}if(t){if(D?.nodeType!==3){var r=I();return D?.before(r),O(r),r}dn(D)}return D}function R(e,t=1,n=!1){let r=E?D:e;for(var i;t--;)i=r,r=on(r);if(!E)return r;if(n){if(r?.nodeType!==3){var a=I();return r===null?i?.after(a):r.before(a),O(a),a}dn(r)}return O(r),r}function cn(e){e.textContent=``}function ln(){return!ze||Ft!==null?!1:(W.f&S)!==0}function un(e,t,n){return t==null||t===`http://www.w3.org/1999/xhtml`?n?document.createElement(e,{is:n}):document.createElement(e):n?document.createElementNS(t,e,{is:n}):document.createElementNS(t,e)}function dn(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===3;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function fn(e){E&&an(e)!==null&&cn(e)}var pn=!1;function mn(){pn||(pn=!0,document.addEventListener(`reset`,e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(let t of e.target.elements)t[pe]?.()})},{capture:!0}))}function hn(e){var t=H,n=W;U(null),Wn(null);try{return e()}finally{U(t),Wn(n)}}function gn(e,t,n,r=n){e.addEventListener(t,()=>hn(n));let i=e[pe];i?e[pe]=()=>{i(),r(!0)}:e[pe]=()=>r(!0),mn()}function _n(e){W===null&&(H===null&&xe(e),be()),Vn&&ye(e)}function vn(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function yn(e,t){var n=W;n!==null&&n.f&8192&&(e|=b);var r={ctx:A,deps:null,nodes:null,f:e|v|512,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};M?.register_created_effect(r);var i=r;if(e&4)Ot===null?Mt.ensure().schedule(r):Ot.push(r);else if(t!==null){try{ir(r)}catch(e){throw V(r),e}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&!(i.f&524288)&&(i=i.first,e&16&&e&65536&&i!==null&&(i.f|=w))}if(i!==null&&(i.parent=n,n!==null&&vn(i,n),H!==null&&H.f&2&&!(e&64))){var a=H;(a.effects??=[]).push(i)}return r}function bn(){return H!==null&&!Un}function xn(e){let t=yn(8,null);return j(t,_),t.teardown=e,t}function Sn(e){_n(`$effect`);var t=W.f;if(!H&&t&32&&A!==null&&!A.i){var n=A;(n.e??=[]).push(e)}else return Cn(e)}function Cn(e){return yn(4|ee,e)}function wn(e){Mt.ensure();let t=yn(64|T,e);return(e={})=>new Promise(n=>{e.outro?Pn(t,()=>{V(t),n(void 0)}):(V(t),n(void 0))})}function Tn(e){return yn(4,e)}function En(e){return yn(ie|T,e)}function Dn(e,t=0){return yn(8|t,e)}function z(e,t=[],n=[],r=[]){ct(r,t,n,t=>{yn(8,()=>{e(...t.map(J))})})}function On(e,t=0){return yn(16|t,e)}function B(e){return yn(32|T,e)}function kn(e){var t=e.teardown;if(t!==null){let e=Vn,n=H;Hn(!0),U(null);try{t.call(null)}finally{Hn(e),U(n)}}}function An(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){let e=n.ac;e!==null&&hn(()=>{e.abort(me)});var r=n.next;n.f&64?n.parent=null:V(n,t),n=r}}function jn(e){for(var t=e.first;t!==null;){var n=t.next;t.f&32||V(t),t=n}}function V(e,t=!0){var n=!1;(t||e.f&262144)&&e.nodes!==null&&e.nodes.end!==null&&(Mn(e.nodes.start,e.nodes.end),n=!0),e.f|=C,An(e,t&&!n),rr(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)e.stop();kn(e),e.f^=C,e.f|=x;var i=e.parent;i!==null&&i.first!==null&&Nn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function Mn(e,t){for(;e!==null;){var n=e===t?null:on(e);e.remove(),e=n}}function Nn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Pn(e,t,n=!0){var r=[];Fn(e,r,!0);var i=()=>{n&&V(e),t&&t()},a=r.length;if(a>0){var o=()=>--a||i();for(var s of r)s.out(o)}else i()}function Fn(e,t,n){if(!(e.f&8192)){e.f^=b;var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)(e.is_global||n)&&t.push(e);for(var i=e.first;i!==null;){var a=i.next;if(!(i.f&64)){var o=(i.f&65536)!=0||(i.f&32)!=0&&(e.f&16)!=0;Fn(i,t,o?n:!1)}i=a}}}function In(e){Ln(e,!0)}function Ln(e,t){if(e.f&8192){e.f^=b,e.f&1024||(j(e,v),Mt.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=(n.f&65536)!=0||(n.f&32)!=0;Ln(n,i?t:!1),n=r}var a=e.nodes&&e.nodes.t;if(a!==null)for(let e of a)(e.is_global||t)&&e.in()}}function Rn(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:on(n);t.append(n),n=i}}var zn=null,Bn=!1,Vn=!1;function Hn(e){Vn=e}var H=null,Un=!1;function U(e){H=e}var W=null;function Wn(e){W=e}var Gn=null;function Kn(e){H!==null&&(!ze||H.f&2)&&(Gn??=new Set).add(e)}var G=null,K=0,q=null;function qn(e){q=e}var Jn=1,Yn=0,Xn=Yn;function Zn(e){Xn=e}function Qn(){return++Jn}function $n(e){var t=e.f;if(t&2048)return!0;if(t&2&&(e.f&=~ne),t&4096){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}t&512&&N===null&&j(e,_)}return!1}function er(e,t,n=!0){var r=e.reactions;if(r!==null&&!(!ze&&Gn!==null&&Gn.has(e)))for(var i=0;i{e.ac.abort(me)}),e.ac=null);try{e.f|=re;var u=e.fn,d=u();e.f|=S;var f=e.deps,p=M?.is_fork;if(G!==null){var m;if(p||rr(e,K),f!==null&&K>0)for(f.length=K+G.length,m=0;m{requestAnimationFrame(()=>e()),setTimeout(()=>e())});await Promise.resolve(),Nt()}function J(e){var t=(e.f&2)!=0;if(zn?.add(e),H!==null&&!Un&&!(W!==null&&W.f&16384)&&(Gn===null||!Gn.has(e))){var n=H.deps;if(H.f&2097152)e.rvn?.call(this,e))}return e.startsWith(`pointer`)||e.startsWith(`touch`)||e===`wheel`?qe(()=>{t.addEventListener(e,i,r)}):t.addEventListener(e,i,r),i}function pr(e,t,n,r,i){var a={capture:r,passive:i},o=fr(e,t,n,a);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&xn(()=>{t.removeEventListener(e,o,a)})}function Y(e,t,n){(t[lr]??={})[e]=n}function mr(e){for(var t=0;t{throw e});throw p}}finally{e[lr]=t,delete e.currentTarget,U(d),Wn(f)}}}var _r=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy(`svelte-trusted-html`,{createHTML:e=>e});function vr(e){return _r?.createHTML(e)??e}function yr(e){var t=un(`template`);return t.innerHTML=vr(e.replaceAll(``,``)),t.content}function br(e,t){var n=W;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function X(e,t){var n=(t&1)!=0,r=(t&2)!=0,i,a=!e.startsWith(``);return()=>{if(E)return br(D,null),D;i===void 0&&(i=yr(a?e:``+e),n||(i=an(i)));var t=r||en?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=an(t),s=t.lastChild;br(o,s)}else br(t,t);return t}}function xr(e=``){if(!E){var t=I(e+``);return br(t,t),t}var n=D;return n.nodeType===3?dn(n):(n.before(n=I()),O(n)),br(n,n),n}function Sr(){if(E)return br(D,null),D;var e=document.createDocumentFragment(),t=document.createComment(``),n=I();return e.append(t,n),br(t,n),e}function Z(e,t){if(E){var n=W;(!(n.f&32768)||n.nodes.end===null)&&(n.nodes.end=D),Me();return}e!==null&&e.before(t)}[...`allowfullscreen.async.autofocus.autoplay.checked.controls.default.disabled.formnovalidate.indeterminate.inert.ismap.loop.multiple.muted.nomodule.novalidate.open.playsinline.readonly.required.reversed.seamless.selected.webkitdirectory.defer.disablepictureinpicture.disableremoteplayback`.split(`.`)];var Cr=[`touchstart`,`touchmove`];function wr(e){return Cr.includes(e)}function Q(e,t){var n=t==null?``:typeof t==`object`?`${t}`:t;n!==(e[fe]??=e.nodeValue)&&(e[fe]=n,e.nodeValue=`${n}`)}function Tr(e,t){return Dr(e,t)}var Er=new Map;function Dr(t,{target:n,anchor:r,props:i={},events:a,context:s,intro:c=!0,transformError:l}){rn();var u=void 0,d=wn(()=>{var c=r??n.appendChild(I());ot(c,{pending:()=>{}},n=>{He({});var r=A;if(s&&(r.c=s),a&&(i.$$events=a),E&&br(n,null),u=t(n,i)||{},E&&(W.nodes.end=D,D===null||D.nodeType!==8||D.data!==`]`))throw ke(),e;Ue()},l);var d=new Set,f=e=>{for(var t=0;t{for(var e of d)for(let r of[n,document]){var t=Er.get(r),i=t.get(e);--i==0?(r.removeEventListener(e,gr),t.delete(e),t.size===0&&Er.delete(r)):t.set(e,i)}dr.delete(f),c!==r&&c.parentNode?.removeChild(c)}});return Or.set(u,d),u}var Or=new WeakMap,kr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)In(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(In(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(V(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();Rn(r,t),t.append(I()),this.#n.set(e,{effect:r,fragment:t})}else V(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),Pn(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(V(n.effect),this.#n.delete(e))};ensure(e,t){var n=M,r=ln();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=I();i.append(a),this.#n.set(e,{effect:B(()=>t(a)),fragment:i})}else this.#t.set(e,B(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else E&&(this.anchor=D),this.#a(n)}};function Ar(e){A===null&&ge(`onMount`),Be&&A.l!==null?Mr(A).m.push(e):Sn(()=>{let t=cr(e);if(typeof t==`function`)return t})}function jr(e){A===null&&ge(`onDestroy`),Ar(()=>()=>cr(e))}function Mr(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}function $(e,t,n=!1){var r;E&&(r=D,Me());var i=new kr(e),a=n?w:0;function o(e,t){if(E){var n=Fe(r);if(e!==parseInt(n.substring(1))){var a=Pe();O(a),i.anchor=a,je(!1),i.ensure(e,t),je(!0);return}}i.ensure(e,t)}On(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}function Nr(e,t){return t}function Pr(e,t,n){for(var r=[],i=t.length,a,s=t.length,c=0;c{if(a){if(a.pending.delete(n),a.done.add(n),a.pending.size===0){var t=e.outrogroups;Fr(e,o(a.done)),t.delete(a),t.size===0&&(e.outrogroups=null)}}else --s},!1)}if(s===0){var l=r.length===0&&n!==null;if(l){var u=n,d=u.parentNode;cn(d),d.append(u),e.items.clear()}Fr(e,t,!l)}else a={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(a)}function Fr(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i{var e=n();return r(e)?e:e==null?[]:o(e)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,zr(v,p,c,t,i),d!==null&&(p.length===0?d.f&33554432?(d.f^=te,Vr(d,null,c)):In(d):Pn(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:On(()=>{p=J(f);var e=p.length;let r=!1;E&&Fe(c)===`[!`!=(e===0)&&(c=Pe(),O(c),je(!1),r=!0);for(var o=new Set,u=M,v=ln(),y=0;ys(c)):(d=B(()=>s(Ir??=I())),d.f|=te)),e>o.size&&ve(``,``,``),E&&e>0&&O(Pe()),!h)if(m.set(u,o),v){for(let[e,t]of l)o.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u);r&&je(!0),J(f)}),flags:t,items:l,pending:m,outrogroups:null,fallback:d};h=!1,E&&(c=D)}function Rr(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function zr(e,t,n,r,i){var a=(r&8)!=0,s=t.length,c=e.items,l=Rr(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(a)for(v=0;v0){var ee=r&4&&s===0?n:null;if(a){for(v=0;v{if(f!==void 0)for(_ of f)_.nodes?.a?.apply()})}function Br(e,t,n,r,i,a,o,s){var c=o&1?o&16?Gt(n):Kt(n,!1,!1):null,l=o&2?Gt(i):null;return{v:c,i:l,e:B(()=>(a(t,c??n,l??i,s),()=>{e.delete(r)}))}}function Vr(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,a=t&&!(t.f&33554432)?t.nodes.start:n;r!==null;){var o=on(r);if(a.before(r),r===i)return;r=o}}function Hr(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}var Ur=[...` +\r\f\xA0\v`];function Wr(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||Ur.includes(r[o-1]))&&(s===r.length||Ur.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function Gr(e,t=!1){var n=t?` !important;`:`;`,r=``;for(var i of Object.keys(e)){var a=e[i];a!=null&&a!==``&&(r+=` `+i+`: `+a+n)}return r}function Kr(e){return e[0]!==`-`||e[1]!==`-`?e.toLowerCase():e}function qr(e,t){if(t){var n=``,r,i;if(Array.isArray(t)?(r=t[0],i=t[1]):r=t,e){e=String(e).replaceAll(/\s*\/\*.*?\*\/\s*/g,``).trim();var a=!1,o=0,s=!1,c=[];r&&c.push(...Object.keys(r).map(Kr)),i&&c.push(...Object.keys(i).map(Kr));var l=0,u=-1;let t=e.length;for(var d=0;d{var a=i?e.defaultValue:e.value;if(a=ai(e)?oi(a):a,n(a),M!==null&&r.add(M),await ar(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(E&&e.defaultValue!==e.value||cr(t)==null&&e.value)&&(n(ai(e)?oi(e.value):e.value),M!==null&&r.add(M)),Dn(()=>{var n=t();if(e===document.activeElement){var i=ze?wt:M;if(r.has(i))return}ai(e)&&n===oi(e.value)||e.type===`date`&&!n&&!e.value||n!==e.value&&(e.value=n??``)})}function ai(e){var t=e.type;return t===`number`||t===`range`}function oi(e){return e===``?null:+e}function si(e,t){return e===t||e?.[oe]===t}function ci(e={},t,n,r){var i=A.r,a=W;return Tn(()=>{var o,s;return Dn(()=>{o=s,s=r?.()||[],cr(()=>{si(n(...s),e)||(t(e,...s),o&&si(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&si(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function li(e,t,n,r){var i=!Be||(n&2)!=0,a=(n&8)!=0,o=(n&16)!=0,s=r,l=!0,u=void 0,d=()=>o&&i?(u??=ft(r),J(u)):(l&&(l=!1,s=o?cr(r):r),s);let f;if(a){var p=oe in e||se in e;f=c(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;a?[m,h]=rt(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Ce(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(s=void 0),n===void 0?s:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?ft:gt)(()=>(v=!1,g()));a&&J(y);var b=W;return(function(e,t){if(arguments.length>0){let n=t?J(y):i&&a?Qt(e):e;return F(y,n),v=!0,s!==void 0&&(s=n),e}return Vn&&v||b.f&16384?y.v:J(y)})}var ui=`breakglass.session_id`;function di(){try{return localStorage.getItem(ui)||``}catch{return``}}function fi(e){try{e?localStorage.setItem(ui,e):localStorage.removeItem(ui)}catch{}}function pi(){fi(``)}async function mi(){let e=await fetch(`/api/session`,{method:`POST`,headers:{"content-type":`application/json`}});if(!e.ok)throw Error(`could not open a session (HTTP ${e.status})`);let t=await e.json();if(!t||typeof t.session_id!=`string`)throw Error(`session response missing session_id`);return t.session_id}function hi(e,{onEvent:t,onCaughtUp:n,onOpen:r,onError:i}){let a=new EventSource(`/api/session/${encodeURIComponent(e)}/stream`);return a.onopen=()=>r?.(),a.onmessage=e=>{if(!e||typeof e.data!=`string`||e.data===``)return;let n;try{n=JSON.parse(e.data)}catch{return}(n.id==null||n.id===``)&&e.lastEventId&&(n.id=e.lastEventId),t(n)},a.addEventListener(`caught-up`,()=>n?.()),a.onerror=e=>{i?.(e)},a}async function gi({session_id:e,prompt:t,model:n}){let r={prompt:t};n&&(r.model=n);let i=await fetch(`/api/session/${encodeURIComponent(e)}/prompt`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r)});if(i.status===409)return{status:`busy`};if(i.status===404)return{status:`gone`};if(!i.ok)throw Error(`could not start the turn (HTTP ${i.status})`);return{status:`started`}}async function _i(e){let t=await fetch(`/api/session/${encodeURIComponent(e)}/cancel`,{method:`POST`,headers:{"content-type":`application/json`}});if(!t.ok)throw Error(`could not stop the turn (HTTP ${t.status})`);return!!(await t.json().catch(()=>({}))).cancelled}async function vi(){let e=await fetch(`/api/pve/verbs`);if(!e.ok)throw Error(`could not load VM controls (HTTP ${e.status})`);let t=await e.json();return{verbs:Array.isArray(t.verbs)?t.verbs:[],mutating:Array.isArray(t.mutating)?t.mutating:[]}}async function yi(e){let t=await fetch(`/api/pve/${encodeURIComponent(e)}`,{method:`POST`,headers:{"content-type":`application/json`}}),n;try{n=await t.json()}catch{throw Error(`VM control '${e}' failed (HTTP ${t.status}, no body)`)}if(t.status===400)throw Error(n?.detail||`'${e}' was rejected by the server`);return{verb:n.verb??e,exit_code:n.exit_code??null,stdout:n.stdout??``,stderr:n.stderr??``,rejected:!!n.rejected}}function bi(e,t){let n=Number(e),r=Number(t);return Number.isFinite(n)&&Number.isFinite(r)&&`${e}`.trim()!==``&&`${t}`.trim()!==``?n>r:String(e)>String(t)}function xi(){return{messages:[],maxId:null,sawId:!1,openAssistant:null,activeUserSeen:!1}}function Si(e,t,n){return t!=null&&`${t}`.trim()!==``?`${e}:${t}`:`${e}:idx:${n}`}function Ci(e,t){return t==null||`${t}`.trim()===``?{apply:!0,maxId:e}:e==null||bi(t,e)?{apply:!0,maxId:t}:{apply:!1,maxId:e}}function wi(e,t){if(!t||typeof t!=`object`)return!1;let{apply:n,maxId:r}=Ci(e.maxId,t.id);if(e.maxId=r,!n)return!1;t.id!=null&&`${t.id}`.trim()!==``&&(e.sawId=!0);let i=()=>{if(!e.openAssistant){let n={role:`assistant`,key:Si(`a`,t.id,e.messages.length),parts:[],ended:!1};e.messages.push(n),e.openAssistant=n}return e.openAssistant};switch(t.kind){case`user`:return e.openAssistant=null,e.messages.push({role:`user`,key:Si(`u`,t.id,e.messages.length),text:typeof t.text==`string`?t.text:``}),e.activeUserSeen=!0,!0;case`session`:return i(),!0;case`text`:{if(typeof t.text!=`string`||t.text===``)return!1;let e=i(),n=e.parts[e.parts.length-1];return n&&n.type===`text`?n.text+=t.text:e.parts.push({type:`text`,text:t.text}),!0}case`tool`:{let e=i(),n=t.input&&typeof t.input.command==`string`?t.input.command:``;return e.parts.push({type:`tool`,name:typeof t.name==`string`&&t.name?t.name:`tool`,command:n,raw:t.input??null}),!0}case`result`:{let e=i();return e.result={is_error:!!t.is_error,text:typeof t.result==`string`?t.result:``,duration_ms:typeof t.duration_ms==`number`?t.duration_ms:null},!0}case`error`:{let e=i();return e.error=typeof t.error==`string`&&t.error?t.error:`unknown error`,!0}case`cancelled`:{let e=i();return e.cancelled=!0,!0}case`turn_end`:return e.openAssistant&&(e.openAssistant.ended=!0),e.openAssistant=null,e.activeUserSeen=!1,!0;default:return!1}}var Ti=X(` `,1),Ei=X(` `);function Di(e,t){let n=li(t,`name`,3,`tool`),r=li(t,`command`,3,``);var i=Ei(),a=R(L(i),2),o=L(a,!0);k(a);var s=R(a,2),c=e=>{var t=Ti(),n=R(sn(t),2),i=L(n,!0);k(n),z(()=>Q(i,r())),Z(e,t)};$(s,e=>{r()&&e(c)}),k(i),z(()=>{ei(i,`title`,r()?`${n()}: ${r()}`:n()),Q(o,n())}),Z(e,i)}var Oi=X(`The cluster or network may be down. You can still power-cycle the VM + with ⚡ Direct VM control — it needs no agent.`,1),ki=X(`Tap a preset below or describe the symptom — "devvm unreachable", + "disk full", "ssh hangs" — and it will connect over SSH, investigate, + and stream its work here. For a hard power action, use ⚡ Direct VM control.`,1),Ai=X(`

`),ji=X(`
`),Mi=X(``),Ni=X(` `),Pi=X(`
error
`),Fi=X(`
stopped turn cancelled
`),Ii=X(` `),Li=X(` `),Ri=X(`
`),zi=X(`
`),Bi=X(``),Vi=X(`
agent working — streaming live
`),Hi=X(``),Ui=X(``),Wi=X(`
Recovery agent SSHes into the devvm to diagnose & repair
`);function Gi(e,t){He(t,!0);let n=li(t,`rev`,3,0),r=li(t,`caughtUp`,3,!1),i=li(t,`turnActive`,3,!1),a=li(t,`sending`,3,!1),o=li(t,`linkState`,3,`connecting`),s=li(t,`onSubmit`,3,e=>{}),c=li(t,`onStop`,3,()=>{}),l=[{label:`Triage`,icon:`◑`,prompt:`Triage the devvm: uptime, load, memory, swap, disk usage, failed systemd units, and the last 30 lines of dmesg. Summarize what's wrong.`},{label:`Memory / OOM`,icon:`▦`,prompt:`Check devvm memory pressure: free -h, top memory consumers, any recent OOM-kills in dmesg/journal, and swap usage. Is it OOMing?`},{label:`Disk`,icon:`▤`,prompt:`What's filling the devvm disk? df -h, then the biggest directories/files under the fullest mount. Anything safe to clear?`},{label:`Services`,icon:`⚙`,prompt:`List failed or stuck systemd units on the devvm (systemctl --failed) and show the status + recent journal lines for any that are down.`},{label:`QEMU wedged?`,icon:`◫`,prompt:`Is the devvm's QEMU wedged (I/O stall)? Check guest responsiveness over SSH, then ssh pve forensics for VM 102's qm status/QMP/guest-agent. Tell me if a cycle is needed.`}],u=P(``),d,f,p=!0,m=ht(()=>n()>=0&&t.tx?t.tx.messages.map(e=>e.role===`assistant`?{...e,parts:e.parts.slice()}:{...e}):[]),h=ht(()=>J(m).length===0),g=ht(()=>o()!==`error`&&!i()&&J(u).trim().length>0),_=ht(()=>!i());function v(){d&&(p=d.scrollHeight-d.scrollTop-d.clientHeight<64)}async function y(e=!1){!e&&!p||(await ar(),d&&(d.scrollTop=d.scrollHeight))}Sn(()=>{n(),y()});function b(e){i()||(p=!0,s()(e),y(!0))}function x(){let e=J(u).trim();!e||i()||(F(u,``),b(e),ar().then(()=>f?.focus()))}function S(e){e.key===`Enter`&&!e.shiftKey&&(e.preventDefault(),x())}function C(e){return e==null?``:e<1e3?`${e} ms`:`${(e/1e3).toFixed(+(e<1e4))} s`}function w(e){return r()?Math.min(e,6)*45:0}var T=Wi(),ee=R(L(T),2),te=L(ee),ne=e=>{var t=Ai();let n;var r=R(L(t),2),i=L(r),a=e=>{Z(e,xr(`The agent is unreachable.`))},s=e=>{Z(e,xr(`Attaching to the session…`))},c=e=>{Z(e,xr(`The agent is standing by.`))};$(i,e=>{o()===`error`?e(a):o()===`connecting`?e(s,1):e(c,-1)}),k(r);var l=R(r,2),u=L(l),d=e=>{var t=Oi();Ne(2),Z(e,t)},f=e=>{var t=ki();Ne(2),Z(e,t)};$(u,e=>{o()===`error`?e(d):e(f,-1)}),k(l),k(t),z(()=>n=Jr(t,1,`empty svelte-1bi93vx`,null,n,{dim:o()===`connecting`})),Z(e,t)};$(te,e=>{J(h)&&e(ne)}),Lr(R(te,2),17,()=>J(m),e=>e.key,(e,t)=>{var n=Sr(),r=sn(n),i=e=>{var n=ji(),r=L(n),i=L(r,!0);k(r),k(n),z(e=>{Xr(n,`--d:${e??``}ms`),Q(i,J(t).text)},[()=>w(0)]),Z(e,n)},a=e=>{var n=zi(),r=L(n),i=L(r),a=e=>{Z(e,Mi())};$(i,e=>{J(t).parts.length===0&&!J(t).result&&!J(t).error&&!J(t).cancelled&&e(a)});var o=R(i,2);Lr(o,17,()=>J(t).parts,Nr,(e,t)=>{var n=Sr(),r=sn(n),i=e=>{var n=Ni(),r=L(n,!0);k(n),z(()=>Q(r,J(t).text)),Z(e,n)},a=e=>{Di(e,{get name(){return J(t).name},get command(){return J(t).command}})};$(r,e=>{J(t).type===`text`?e(i):e(a,-1)}),Z(e,n)});var s=R(o,2),c=e=>{var n=Pi(),r=R(L(n),2),i=L(r,!0);k(r),k(n),z(()=>Q(i,J(t).error)),Z(e,n)},l=e=>{Z(e,Fi())},u=e=>{var n=Ri(),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{var n=Ii(),r=L(n,!0);k(n),z(()=>Q(r,J(t).result.text)),Z(e,n)};$(a,e=>{J(t).result.text&&e(o)});var s=R(a,2),c=e=>{var n=Li(),r=L(n,!0);k(n),z(e=>Q(r,e),[()=>C(J(t).result.duration_ms)]),Z(e,n)};$(s,e=>{J(t).result.duration_ms!=null&&e(c)}),k(n),z(()=>{Jr(n,1,`turn-note ${J(t).result.is_error?`turn-note--error`:`turn-note--ok`}`,`svelte-1bi93vx`),Q(i,J(t).result.is_error?`failed`:`done`)}),Z(e,n)};$(s,e=>{J(t).error?e(c):J(t).cancelled?e(l,1):J(t).result&&e(u,2)}),k(r),k(n),z(e=>Xr(n,`--d:${e??``}ms`),[()=>w(0)]),Z(e,n)};$(r,e=>{J(t).role===`user`?e(i):e(a,-1)}),Z(e,n)}),k(ee),ci(ee,e=>d=e,()=>d);var re=R(ee,2),ie=L(re);Lr(ie,21,()=>l,e=>e.label,(e,t)=>{var n=Bi(),r=L(n),a=L(r,!0);k(r);var s=R(r,2),c=L(s,!0);k(s),k(n),z(()=>{n.disabled=i()||o()===`error`,ei(n,`title`,J(t).prompt),Q(a,J(t).icon),Q(c,J(t).label)}),Y(`click`,n,()=>b(J(t).prompt)),Z(e,n)}),k(ie);var ae=R(ie,2),oe=L(ae),se=e=>{Z(e,Vi())};$(oe,e=>{i()&&e(se)});var ce=R(oe,2),le=L(ce);fn(le),ci(le,e=>f=e,()=>f);var ue=R(le,2),de=e=>{var t=Hi();Y(`click`,t,function(...e){c()?.apply(this,e)}),Z(e,t)},fe=e=>{var t=Ui(),n=L(t,!0);k(t),z(()=>{t.disabled=!J(g),Q(n,a()?`···`:`Send`)}),Z(e,t)};$(ue,e=>{i()?e(de):e(fe,-1)}),k(ce),k(ae),k(re),k(T),z(()=>{ei(le,`placeholder`,J(_)?`Describe the problem… (Enter to send · Shift+Enter for a new line)`:`A turn is running — Stop it to type, or wait…`),le.disabled=!J(_)}),pr(`scroll`,ee,v),pr(`submit`,ae,e=>{e.preventDefault(),x()}),Y(`keydown`,le,S),ii(le,()=>J(u),e=>F(u,e)),Z(e,T),Ue()}mr([`click`,`keydown`]);var Ki=X(`
Loading controls…
`),qi=X(``),Ji=X(``),Yi=X(``),Xi=X(``),Zi=X(`recovery`),Qi=X(`
Confirm ? This will affect the running VM
`),$i=X(`

`),ea=X(``),ta=X(`rejected`),na=X(` `),ra=X(`
 
`),ia=X(`
stderr
 
`,1),aa=X(`
(no output)
`),oa=X(`
`),sa=X(`
Inspect read-only
Power affects the running VM
`,1),ca=X(`

Direct VM control

No AI in the path — these reach the Proxmox host over a + forced-command SSH key and work even when the agent is down.

`);function la(e,t){He(t,!0);let n={status:{label:`status`,blurb:`qm status — is the VM up?`},forensics:{label:`forensics`,blurb:`capture live diagnostic state`},start:{label:`start`,blurb:`power on a stopped VM`},stop:{label:`stop`,blurb:`hard power-off (pulls the plug)`},reset:{label:`reset`,blurb:`warm reboot — reuses the QEMU process`},cycle:{label:`cycle`,blurb:`stop → start; applies staged config; fixes a wedged QEMU`,headline:!0}},r=[`status`,`forensics`,`start`,`stop`,`reset`,`cycle`],i=P(`loading`),a=P(``),o=P(Qt([])),s=P(``),c=P(``),l=P(null),u=P(``),d=ht(()=>J(c)!==``);Ar(async()=>{try{let{verbs:e,mutating:t}=await vi(),a=new Set(t),s=e.filter(e=>n[e]);F(o,[...r.filter(e=>s.includes(e)),...s.filter(e=>!r.includes(e))].map(e=>({name:e,mutating:a.has(e),...n[e]})),!0),F(i,`ready`)}catch(e){F(i,`error`),F(a,e instanceof Error?e.message:String(e),!0)}});let f=ht(()=>J(o).filter(e=>!e.mutating)),p=ht(()=>J(o).filter(e=>e.mutating));function m(e){J(d)||(e.mutating?F(s,J(s)===e.name?``:e.name,!0):g(e.name))}function h(){F(s,``)}async function g(e){F(s,``),F(u,``),F(l,null),F(c,e,!0);try{F(l,await yi(e),!0)}catch(e){F(u,e instanceof Error?e.message:String(e),!0)}finally{F(c,``)}}let _=ht(()=>!!J(l)&&(J(l).rejected||J(l).exit_code!=null&&J(l).exit_code!==0));var v=ca(),y=R(L(v),2),b=e=>{Z(e,Ki())},x=e=>{var t=qi(),n=L(t),r=R(n);k(t),z(()=>Q(n,`Couldn't load the VM controls — ${J(a)??``}. `)),Y(`click`,r,()=>location.reload()),Z(e,t)},S=e=>{var t=sa(),n=sn(t),r=R(L(n),2);Lr(r,21,()=>J(f),e=>e.name,(e,t)=>{var n=Yi(),r=L(n),i=e=>{Z(e,Ji())};$(r,e=>{J(c)===J(t).name&&e(i)});var a=R(r,2),o=L(a,!0);k(a),k(n),z(()=>{n.disabled=J(d),ei(n,`title`,J(t).blurb),Q(o,J(t).label)}),Y(`click`,n,()=>m(J(t))),Z(e,n)}),k(r),k(n);var i=R(n,2),a=R(L(i),2);Lr(a,21,()=>J(p),e=>e.name,(e,t)=>{var n=$i(),r=L(n),i=L(r),a=e=>{Z(e,Xi())};$(i,e=>{J(c)===J(t).name&&e(a)});var o=R(i,2),l=L(o,!0);k(o);var u=R(o,2),f=e=>{Z(e,Zi())};$(u,e=>{J(t).headline&&e(f)}),k(r);var p=R(r,2),_=L(p,!0);k(p);var v=R(p,2),y=e=>{var n=Qi(),r=L(n),i=R(L(r)),a=L(i,!0);k(i),Ne(),k(r);var o=R(r,2),s=L(o),c=R(s,2);k(o),k(n),z(()=>{ei(n,`aria-label`,`Confirm ${J(t).name??``}`),Q(a,J(t).name),s.disabled=J(d),c.disabled=J(d)}),Y(`click`,s,()=>g(J(t).name)),Y(`click`,c,h),Z(e,n)};$(v,e=>{J(s)===J(t).name&&e(y)}),k(n),z(()=>{Jr(n,1,`danger-item ${J(t).headline?`danger-item--headline`:``}`,`svelte-1qihpg4`),Jr(r,1,`vbtn vbtn--danger ${J(t).headline?`vbtn--headline`:``}`,`svelte-1qihpg4`),r.disabled=J(d),ei(r,`aria-expanded`,J(s)===J(t).name),Q(l,J(t).label),Q(_,J(t).blurb)}),Y(`click`,r,()=>m(J(t))),Z(e,n)}),k(a),k(i);var o=R(i,2),v=e=>{var t=ea(),n=L(t);k(t),z(()=>Q(n,`⚠ Command failed to reach the host — ${J(u)??``}`)),Z(e,t)};$(o,e=>{J(u)&&e(v)});var y=R(o,2),b=e=>{var t=oa(),n=L(t),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{Z(e,ta())},s=e=>{var t=na(),n=L(t);k(t),z(()=>{Jr(t,1,`out-status ${J(_)?`out-status--fail`:`out-status--ok`}`,`svelte-1qihpg4`),Q(n,`exit ${J(l).exit_code??``}`)}),Z(e,t)};$(a,e=>{J(l).rejected?e(o):e(s,-1)}),k(n);var c=R(n,2),u=e=>{var t=ra(),n=L(t,!0);k(t),z(()=>Q(n,J(l).stdout)),Z(e,t)};$(c,e=>{J(l).stdout&&e(u)});var d=R(c,2),f=e=>{var t=ia(),n=R(sn(t),2),r=L(n,!0);k(n),z(()=>Q(r,J(l).stderr)),Z(e,t)};$(d,e=>{J(l).stderr&&e(f)});var p=R(d,2),m=e=>{Z(e,aa())};$(p,e=>{!J(l).stdout&&!J(l).stderr&&e(m)}),k(t),z(()=>{Jr(t,1,`out ${J(_)?`out--fail`:`out--ok`}`,`svelte-1qihpg4`),Q(i,J(l).verb)}),Z(e,t)};$(y,e=>{J(l)&&e(b)}),Z(e,t)};$(y,e=>{J(i)===`loading`?e(b):J(i)===`error`?e(x,1):e(S,-1)}),k(v),Z(e,v),Ue()}mr([`click`]);var ua=X(` `),da=X(``),fa=X(`
`),pa=X(`

devvmbreakglass

`);function ma(e,t){He(t,!0);let n=P(`connecting`),r=P(``),i=P(``),a=P(!1),o=P(!1),s=P(!1),c=P(xi()),l=P(0),u=null,d=P(!1);function f(){F(c,xi()),Yt(l)}function p(e){wi(J(c),e)&&(F(o,J(c).activeUserSeen,!0),Yt(l))}function m(){u&&=(u.close(),null)}function h(e){m(),F(i,e,!0),F(a,!1),F(n,`connecting`),F(r,``),u=hi(e,{onOpen:()=>{J(n)!==`attached`&&F(n,`attached`),F(r,``)},onCaughtUp:()=>{F(a,!0),F(n,`attached`)},onEvent:p,onError:()=>{u&&u.readyState===EventSource.CLOSED?(F(n,`error`),F(r,`lost the connection to the session — retrying…`),setTimeout(()=>{J(i)===e&&h(e)},1500)):F(n,`connecting`)}})}async function g(){F(n,`connecting`),F(r,``),f();let e=di();if(e){h(e);return}await _()}async function _(){try{F(n,`connecting`);let e=await mi();fi(e),h(e)}catch(e){F(n,`error`),F(r,e instanceof Error?e.message:String(e),!0)}}async function v(){J(o)||J(s)||(m(),pi(),f(),F(o,!1),await _())}async function y(e){let t=(e||``).trim();if(!(!t||J(o)||J(s))&&!(!J(i)&&(await _(),!J(i)))){F(s,!0),F(o,!0);try{let e=await gi({session_id:J(i),prompt:t});e.status===`busy`?F(x,`A turn is already running.`):e.status===`gone`&&(pi(),await _(),J(i)&&await gi({session_id:J(i),prompt:t}))}catch(e){F(x,e instanceof Error?e.message:String(e),!0),F(o,J(c).activeUserSeen,!0)}finally{F(s,!1)}}}async function b(){if(J(i))try{await _i(J(i))}catch(e){F(x,e instanceof Error?e.message:String(e),!0)}}let x=P(``),S;Sn(()=>{J(x)&&(clearTimeout(S),S=setTimeout(()=>F(x,``),4200))}),Ar(g),jr(m);let C=ht(()=>J(n)===`error`?`error`:J(o)?`working`:J(n)===`attached`?`live`:`connecting`),w=ht(()=>({error:`link down`,working:`agent working`,live:`attached`,connecting:`connecting`})[J(C)]),T=ht(()=>J(i)?J(i).slice(0,8):`········`);var ee=pa(),te=L(ee),ne=R(L(te),2),re=L(ne),ie=L(re),ae=R(ie,2),oe=L(ae),se=e=>{Z(e,xr(`link down`))},ce=e=>{Z(e,xr(`working`))},le=e=>{var t=ua(),n=L(t,!0);k(t),z(()=>Q(n,J(T))),Z(e,t)},ue=e=>{Z(e,xr(`connecting`))};$(oe,e=>{J(C)===`error`?e(se):J(C)===`working`?e(ce,1):J(C)===`live`?e(le,2):e(ue,-1)}),k(ae),k(re);var de=R(re,2),fe=R(de,2);k(ne),k(te);var pe=R(te,2),me=e=>{var t=da(),n=L(t),i=L(n,!0);k(n);var a=R(n,4);k(t),z(()=>Q(i,J(r)||`Can't reach the breakglass backend.`)),Y(`click`,a,g),Z(e,t)};$(pe,e=>{J(n)===`error`&&e(me)});var he=R(pe,2),ge=e=>{var t=fa(),n=L(t,!0);k(t),z(()=>Q(n,J(x))),Z(e,t)};$(he,e=>{J(x)&&e(ge)});var _e=R(he,2),ve=L(_e);Gi(L(ve),{get tx(){return J(c)},get rev(){return J(l)},get caughtUp(){return J(a)},get turnActive(){return J(o)},get sending(){return J(s)},get linkState(){return J(n)},onSubmit:y,onStop:b}),k(ve);var ye=R(ve,2);let be;var xe=R(L(ye),2),Se=R(L(xe),2);k(xe),la(R(xe,2),{}),k(ye),k(_e);var Ce=R(_e,2);let we;k(ee),z(()=>{ei(re,`title`,J(w)),Jr(ie,1,`lamp lamp--${J(C)??``}`,`svelte-1n46o8q`),Jr(ae,1,`lamp-text lamp-text--${J(C)??``}`,`svelte-1n46o8q`),fe.disabled=J(o)||J(s)||J(n)===`connecting`,ei(fe,`title`,J(o)?`wait for the current turn to finish`:`archive this session and start fresh`),be=Jr(ye,1,`controls-pane rise-in svelte-1n46o8q`,null,be,{open:J(d)}),we=Jr(Ce,1,`sheet-backdrop svelte-1n46o8q`,null,we,{show:J(d)})}),Y(`click`,de,()=>F(d,!0)),Y(`click`,fe,v),Y(`click`,Se,()=>F(d,!1)),Y(`click`,Ce,()=>F(d,!1)),Z(e,ee),Ue()}mr([`click`]),Tr(ma,{target:document.getElementById(`app`)}); \ No newline at end of file diff --git a/app/breakglass/static/assets/index-DWHIP1Zw.css b/app/breakglass/static/assets/index-DWHIP1Zw.css deleted file mode 100644 index 79c9110..0000000 --- a/app/breakglass/static/assets/index-DWHIP1Zw.css +++ /dev/null @@ -1 +0,0 @@ -:root{--bg-0:#07090c;--bg-1:#0c1015;--bg-2:#11171e;--bg-3:#161d26;--bg-term:#06080a;--line:#1d2630;--line-strong:#2a3744;--ink:#e6edf3;--ink-dim:#9bb0c0;--ink-faint:#5d7185;--cyan:#3dd1d6;--cyan-dim:#1f6f72;--amber:#f5b657;--green:#5ddb8e;--green-dim:#1f5f3d;--danger:#ff4d4d;--danger-bright:#ff6363;--danger-deep:#7a1717;--danger-glow:#ff4d4d59;--radius:10px;--radius-sm:7px;--mono:ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, "Liberation Mono", monospace;--sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--shadow-panel:0 1px 0 #ffffff05 inset, 0 16px 40px -24px #000000e6;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body{overscroll-behavior:none;height:100%;margin:0;overflow:hidden}body{background-color:var(--bg-0);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background-image:radial-gradient(120% 80% at 85% -10%,#3dd1d612,#0000 55%),radial-gradient(90% 70% at 10% 110%,#f5b6570a,#0000 50%),repeating-linear-gradient(0deg,#ffffff03 0 1px,#0000 1px 3px);background-attachment:fixed}#app{height:100dvh}button{font-family:var(--mono);cursor:pointer}button:disabled{cursor:not-allowed}::selection{background:#3dd1d647}*{scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line-strong);background-clip:content-box;border:2px solid #0000;border-radius:99px}::-webkit-scrollbar-thumb:hover{background:#3a4a5a padding-box content-box}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.chip.svelte-2zgsrv{background:var(--bg-3);border:1px solid var(--line-strong);border-left:2px solid var(--cyan-dim);max-width:100%;font-family:var(--mono);vertical-align:baseline;border-radius:6px;align-items:baseline;gap:6px;margin:3px 4px 3px 0;padding:3px 9px;font-size:12px;line-height:1.45;display:inline-flex}.cog.svelte-2zgsrv{color:var(--cyan);font-size:11px;transform:translateY(1px)}.name.svelte-2zgsrv{color:var(--ink);font-weight:600}.sep.svelte-2zgsrv{color:var(--ink-faint)}.cmd.svelte-2zgsrv{color:var(--amber);font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.chat.svelte-1bi93vx{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow:hidden}.chat-head.svelte-1bi93vx{border-bottom:1px solid var(--line);background:linear-gradient(#ffffff04,#0000);align-items:baseline;gap:12px;padding:13px 18px;display:flex}.chat-head-label.svelte-1bi93vx{font-family:var(--mono);text-transform:uppercase;letter-spacing:.2em;color:var(--cyan);font-size:11px}.chat-head-hint.svelte-1bi93vx{color:var(--ink-faint);font-size:12px}.stream.svelte-1bi93vx{scroll-behavior:smooth;flex-direction:column;flex:1;gap:14px;min-height:0;padding:20px 18px 8px;display:flex;overflow-y:auto}.empty.svelte-1bi93vx{text-align:center;max-width:460px;color:var(--ink-dim);margin:auto;padding:28px 12px}.empty-mark.svelte-1bi93vx{color:var(--cyan-dim);text-shadow:0 0 24px #3dd1d640;margin-bottom:14px;font-size:40px;line-height:1}.empty-title.svelte-1bi93vx{font-family:var(--mono);color:var(--ink);margin:0 0 8px;font-size:15px}.empty-sub.svelte-1bi93vx{color:var(--ink-faint);margin:0;font-size:13px;line-height:1.6}.empty-sub.svelte-1bi93vx strong:where(.svelte-1bi93vx){color:var(--ink-dim);font-weight:600}.row.svelte-1bi93vx{display:flex}.row--user.svelte-1bi93vx{justify-content:flex-end}.row--assistant.svelte-1bi93vx{justify-content:flex-start}.bubble.svelte-1bi93vx{word-wrap:break-word;overflow-wrap:anywhere;border-radius:13px;max-width:86%;padding:11px 14px;font-size:14px;line-height:1.6}.bubble--user.svelte-1bi93vx{border:1px solid var(--cyan-dim);color:#d8f6f7;white-space:pre-wrap;font-family:var(--sans);background:linear-gradient(#15333a,#0f262c);border-bottom-right-radius:4px}.bubble--assistant.svelte-1bi93vx{background:var(--bg-2);border:1px solid var(--line-strong);color:var(--ink);border-bottom-left-radius:4px}.prose.svelte-1bi93vx{white-space:pre-wrap}.thinking.svelte-1bi93vx,.working-dots.svelte-1bi93vx{align-items:center;gap:4px;display:inline-flex}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx){background:var(--amber);opacity:.4;border-radius:50%;width:6px;height:6px;animation:1.2s ease-in-out infinite svelte-1bi93vx-blink}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2){animation-delay:.18s}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3){animation-delay:.36s}@keyframes svelte-1bi93vx-blink{0%,80%,to{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}.turn-note.svelte-1bi93vx{border-radius:var(--radius-sm);font-family:var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;flex-wrap:wrap;align-items:baseline;gap:8px;margin-top:10px;padding:7px 10px;font-size:12px;line-height:1.5;display:flex}.turn-note--ok.svelte-1bi93vx{border:1px solid var(--green-dim);color:#bff5d3;background:#5ddb8e12}.turn-note--error.svelte-1bi93vx{border:1px solid var(--danger-deep);color:#ffd5d5;background:#ff4d4d14}.turn-note-tag.svelte-1bi93vx{text-transform:uppercase;letter-spacing:.14em;opacity:.85;border:1px solid;border-radius:4px;padding:1px 6px;font-size:10px}.turn-note-body.svelte-1bi93vx{flex:1;min-width:0}.turn-note-time.svelte-1bi93vx{color:var(--ink-faint);margin-left:auto}.composer.svelte-1bi93vx{border-top:1px solid var(--line);background:linear-gradient(#0000,#ffffff03);padding:12px}.working-bar.svelte-1bi93vx{font-family:var(--mono);color:var(--amber);letter-spacing:.02em;align-items:center;gap:10px;padding:0 4px 9px;font-size:12px;display:flex}.composer-row.svelte-1bi93vx{align-items:flex-end;gap:10px;display:flex}textarea.svelte-1bi93vx{resize:none;background:var(--bg-2);min-height:48px;max-height:168px;color:var(--ink);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font-family:var(--sans);field-sizing:content;outline:none;flex:1;padding:12px 13px;font-size:16px;line-height:1.5;transition:border-color .15s,box-shadow .15s}textarea.svelte-1bi93vx::placeholder{color:var(--ink-faint)}textarea.svelte-1bi93vx:focus{border-color:var(--cyan-dim);box-shadow:0 0 0 3px #3dd1d61f}textarea.svelte-1bi93vx:disabled{opacity:.55}.send.svelte-1bi93vx{border-radius:var(--radius-sm);border:1px solid var(--cyan-dim);color:#d8f6f7;letter-spacing:.04em;background:linear-gradient(#19474b,#103539);flex:none;align-self:stretch;min-width:78px;padding:0 18px;font-size:13px;font-weight:600;transition:filter .15s,border-color .15s,opacity .15s}.send.svelte-1bi93vx:hover:not(:disabled){filter:brightness(1.22);border-color:var(--cyan)}.send.svelte-1bi93vx:disabled{opacity:.4;background:var(--bg-2);border-color:var(--line-strong);color:var(--ink-faint)}.panel.svelte-1qihpg4{background:var(--bg-1);border:1px solid var(--line);border-top:2px solid var(--danger-deep);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow-y:auto}.panel-head.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px 12px}.panel-head-row.svelte-1qihpg4{align-items:center;gap:9px;display:flex}.hazard.svelte-1qihpg4{color:var(--danger);filter:drop-shadow(0 0 8px var(--danger-glow));font-size:15px}h2.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.12em;color:var(--ink);margin:0;font-size:13px}.panel-sub.svelte-1qihpg4{color:var(--ink-faint);margin:9px 0 0;font-size:11.5px;line-height:1.55}.loading.svelte-1qihpg4{font-family:var(--mono);color:var(--ink-faint);padding:22px 16px;font-size:12px}.group.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px}.group-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.18em;color:var(--ink-faint);align-items:center;gap:8px;margin-bottom:11px;font-size:10.5px;display:flex}.group-label--danger.svelte-1qihpg4{color:var(--danger-bright)}.group-tag.svelte-1qihpg4{letter-spacing:.1em;border:1px solid var(--line-strong);color:var(--ink-faint);border-radius:4px;padding:2px 6px;font-size:9.5px}.group-tag--danger.svelte-1qihpg4{border-color:var(--danger-deep);color:var(--danger-bright);background:#ff4d4d0f}.btn-row.svelte-1qihpg4{flex-wrap:wrap;gap:9px;display:flex}.vbtn.svelte-1qihpg4{border-radius:var(--radius-sm);letter-spacing:.05em;text-transform:lowercase;justify-content:center;align-items:center;gap:8px;padding:9px 15px;font-size:13px;font-weight:600;transition:filter .14s,border-color .14s,background .14s,transform 60ms;display:inline-flex}.vbtn.svelte-1qihpg4:active:not(:disabled){transform:translateY(1px)}.vbtn.svelte-1qihpg4:disabled{opacity:.4}.vbtn-label.svelte-1qihpg4{line-height:1}.vbtn--safe.svelte-1qihpg4{background:var(--bg-2);color:var(--ink);border:1px solid var(--line-strong)}.vbtn--safe.svelte-1qihpg4:hover:not(:disabled){border-color:var(--cyan-dim);background:var(--bg-3)}.danger-list.svelte-1qihpg4{flex-direction:column;gap:12px;display:flex}.danger-item.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid #0000}.danger-item--headline.svelte-1qihpg4{border-color:var(--danger-deep);background:#ff4d4d0b;padding:11px}.vbtn--danger.svelte-1qihpg4{width:100%;color:var(--danger-bright);border:1px solid var(--danger-deep);border-left:3px solid var(--danger);text-shadow:0 0 12px var(--danger-glow);background:linear-gradient(#ff4d4d29,#ff4d4d12)}.vbtn--danger.svelte-1qihpg4:hover:not(:disabled){background:linear-gradient(180deg, var(--danger), var(--danger-bright));color:#1a0606;border-color:var(--danger-bright);text-shadow:none;filter:drop-shadow(0 4px 14px var(--danger-glow))}.vbtn--headline.svelte-1qihpg4{padding:12px 15px;font-size:14px}.headline-badge.svelte-1qihpg4{text-transform:uppercase;letter-spacing:.14em;background:var(--danger);color:#1a0606;border-radius:999px;padding:2px 7px;font-size:9px;font-weight:700}.danger-blurb.svelte-1qihpg4{color:var(--ink-faint);margin:7px 2px 0;font-size:11.5px;line-height:1.5}.danger-item--headline.svelte-1qihpg4 .danger-blurb:where(.svelte-1qihpg4){color:#f0b0b0}.confirm.svelte-1qihpg4{border:1px solid var(--danger);border-radius:var(--radius-sm);background:#ff4d4d1a;margin-top:10px;padding:11px 12px;animation:.16s ease-out svelte-1qihpg4-confirm-in}@keyframes svelte-1qihpg4-confirm-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.confirm-text.svelte-1qihpg4{color:#ffe0e0;margin-bottom:10px;font-size:12.5px;line-height:1.5;display:block}.confirm-text.svelte-1qihpg4 strong:where(.svelte-1qihpg4){color:#fff;font-family:var(--mono);text-transform:uppercase;letter-spacing:.04em}.confirm-actions.svelte-1qihpg4{gap:9px;display:flex}.confirm-yes.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--danger-bright);background:var(--danger);color:#1a0606;letter-spacing:.06em;text-transform:uppercase;flex:1;padding:9px;font-size:13px;font-weight:700;transition:filter .14s}.confirm-yes.svelte-1qihpg4:hover:not(:disabled){filter:brightness(1.12)}.confirm-no.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);color:var(--ink-dim);letter-spacing:.04em;text-transform:uppercase;flex:1;padding:9px;font-size:13px;transition:border-color .14s,color .14s}.confirm-no.svelte-1qihpg4:hover:not(:disabled){border-color:var(--ink-faint);color:var(--ink)}.confirm-yes.svelte-1qihpg4:disabled,.confirm-no.svelte-1qihpg4:disabled{opacity:.5}.spin.svelte-1qihpg4{border:2px solid #e6edf340;border-top-color:var(--cyan);border-radius:50%;flex:none;width:13px;height:13px;animation:.7s linear infinite svelte-1qihpg4-spin}.spin--danger.svelte-1qihpg4{border-color:#ff4d4d4d;border-top-color:var(--danger-bright)}@keyframes svelte-1qihpg4-spin{to{transform:rotate(360deg)}}.out.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-term);margin:14px 16px 16px;overflow:hidden}.out--ok.svelte-1qihpg4{border-color:var(--green-dim)}.out--fail.svelte-1qihpg4{border-color:var(--danger-deep)}.out-head.svelte-1qihpg4{border-bottom:1px solid var(--line);background:#ffffff05;justify-content:space-between;align-items:center;padding:8px 11px;display:flex}.out-verb.svelte-1qihpg4{font-family:var(--mono);color:var(--ink);letter-spacing:.04em;font-size:12px}.out-verb.svelte-1qihpg4:before{content:"$ pve ";color:var(--ink-faint)}.out-status.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em;border:1px solid;border-radius:4px;padding:2px 7px;font-size:10.5px}.out-status--ok.svelte-1qihpg4{color:var(--green)}.out-status--fail.svelte-1qihpg4{color:var(--danger-bright)}.out-pre.svelte-1qihpg4{font-family:var(--mono);color:#c7d6e2;white-space:pre-wrap;overflow-wrap:anywhere;max-height:320px;margin:0;padding:11px 12px;font-size:12px;line-height:1.55;overflow-y:auto}.out-stderr-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.16em;color:var(--danger-bright);padding:6px 12px 0;font-size:10px}.out-pre--stderr.svelte-1qihpg4{color:#f3b6b6}.out-pre--empty.svelte-1qihpg4{color:var(--ink-faint);font-style:italic}.block-error.svelte-1qihpg4{border:1px solid var(--danger-deep);border-left:3px solid var(--danger);border-radius:var(--radius-sm);color:#ffd5d5;background:#ff4d4d12;margin:14px 16px;padding:11px 13px;font-size:12.5px;line-height:1.5}.retry.svelte-1qihpg4{border:1px solid var(--danger-deep);color:var(--danger-bright);background:0 0;border-radius:5px;margin-left:8px;padding:3px 9px;font-size:11px}.retry.svelte-1qihpg4:hover{background:#ff4d4d1f}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4),details.out.svelte-1qihpg4>summary:where(.svelte-1qihpg4){cursor:pointer;-webkit-user-select:none;user-select:none;list-style:none}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4)::-webkit-details-marker{display:none}details.out.svelte-1qihpg4>summary:where(.svelte-1qihpg4)::-webkit-details-marker{display:none}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4):before,details.out.svelte-1qihpg4>summary:where(.svelte-1qihpg4):before{content:"▾";width:11px;color:var(--ink-faint);margin-right:4px;font-size:9px;transition:transform .15s;display:inline-block}details.group.svelte-1qihpg4:not([open])>summary:where(.svelte-1qihpg4):before,details.out.svelte-1qihpg4:not([open])>summary:where(.svelte-1qihpg4):before{transform:rotate(-90deg)}details.group.svelte-1qihpg4>summary:where(.svelte-1qihpg4){padding:3px 0}.out-head.svelte-1qihpg4 .out-status:where(.svelte-1qihpg4){margin-left:auto}.out-pre.svelte-1qihpg4{max-height:46vh;overflow:auto}.shell.svelte-1n46o8q{flex-direction:column;max-width:1500px;height:100%;margin:0 auto;display:flex}.rail.svelte-1n46o8q{border-bottom:1px solid var(--line);flex:none;justify-content:space-between;align-items:center;gap:10px;padding:10px 14px;display:flex}.rail-title.svelte-1n46o8q{align-items:baseline;gap:9px;min-width:0;display:flex}.glyph.svelte-1n46o8q{filter:saturate(.85);font-size:17px;transform:translateY(2px)}h1.svelte-1n46o8q{font-family:var(--mono);letter-spacing:.02em;color:var(--ink);white-space:nowrap;margin:0;font-size:16px;font-weight:600}.accent.svelte-1n46o8q{color:var(--cyan);text-shadow:0 0 18px #3dd1d659}.rail-right.svelte-1n46o8q{flex:none;align-items:center;gap:8px;display:flex}.rail-status.svelte-1n46o8q{font-family:var(--mono);align-items:center;gap:7px;font-size:12px;display:inline-flex}.session-id.svelte-1n46o8q{color:var(--cyan);letter-spacing:.04em}.session-meta.svelte-1n46o8q{color:var(--amber)}.session-bad.svelte-1n46o8q{color:var(--danger-bright)}.dot.svelte-1n46o8q{background:var(--ink-faint);border-radius:50%;flex:none;width:9px;height:9px}.dot--ready.svelte-1n46o8q{background:var(--cyan);animation:3.4s ease-in-out infinite svelte-1n46o8q-breathe;box-shadow:0 0 10px 1px #3dd1d699}.dot--busy.svelte-1n46o8q{background:var(--amber);animation:1s ease-in-out infinite svelte-1n46o8q-pulse;box-shadow:0 0 10px 1px #f5b657b3}.dot--error.svelte-1n46o8q{background:var(--danger);box-shadow:0 0 10px 1px var(--danger-glow)}@keyframes svelte-1n46o8q-breathe{0%,to{opacity:.55}50%{opacity:1}}@keyframes svelte-1n46o8q-pulse{0%,to{opacity:.7;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}.controls-toggle.svelte-1n46o8q,.new-session.svelte-1n46o8q{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);min-height:40px;color:var(--ink-dim);letter-spacing:.02em;align-items:center;gap:5px;padding:0 13px;font-size:13px;display:inline-flex}.controls-toggle.svelte-1n46o8q{color:var(--amber);border-color:#5a4a2a}.controls-toggle.svelte-1n46o8q:active,.new-session.svelte-1n46o8q:active{background:var(--bg-3)}.new-session.svelte-1n46o8q:disabled{opacity:.45}.rail-error.svelte-1n46o8q{border:1px solid var(--danger-deep);color:#ffd5d5;border-radius:var(--radius-sm);background:#ff4d4d12;border-left-width:3px;flex:none;margin:10px 12px 0;padding:11px 14px;font-size:13px;line-height:1.5}.stage.svelte-1n46o8q{flex:1;min-width:0;min-height:0;padding:10px;display:flex}.chat-pane.svelte-1n46o8q{flex:1;min-width:0;min-height:0;display:flex}.controls-pane.svelte-1n46o8q{z-index:40;background:var(--bg-1);border-top:1px solid var(--line-strong);max-height:86dvh;padding:8px 14px calc(14px + env(safe-area-inset-bottom));border-radius:16px 16px 0 0;transition:transform .26s cubic-bezier(.32,.72,0,1);position:fixed;bottom:0;left:0;right:0;overflow-y:auto;transform:translateY(101%);box-shadow:0 -18px 40px #0000008c}.controls-pane.open.svelte-1n46o8q{transform:translateY(0)}.sheet-grip.svelte-1n46o8q{background:var(--line-strong);border-radius:99px;width:38px;height:4px;margin:4px auto 10px}.controls-head.svelte-1n46o8q{justify-content:space-between;align-items:center;margin-bottom:10px;display:flex}.controls-head-title.svelte-1n46o8q{font-family:var(--mono);text-transform:uppercase;letter-spacing:.2em;color:var(--amber);font-size:11px}.sheet-close.svelte-1n46o8q{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);width:34px;height:34px;color:var(--ink-dim);font-size:14px}.sheet-backdrop.svelte-1n46o8q{z-index:30;opacity:0;pointer-events:none;background:#0000008c;border:0;padding:0;transition:opacity .22s;position:fixed;inset:0}.sheet-backdrop.show.svelte-1n46o8q{opacity:1;pointer-events:auto}@media (width>=900px){.rail.svelte-1n46o8q{padding:14px 18px}h1.svelte-1n46o8q{font-size:19px}.stage.svelte-1n46o8q{grid-template-columns:minmax(0,1fr) 372px;gap:16px;padding:16px 18px 18px;display:grid}.chat-pane.svelte-1n46o8q{display:flex}.controls-toggle.svelte-1n46o8q{display:none}.controls-pane.svelte-1n46o8q{max-height:none;box-shadow:none;z-index:auto;border:none;border-radius:0;padding:0;position:static;overflow:visible;transform:none}.sheet-grip.svelte-1n46o8q,.controls-head.svelte-1n46o8q,.sheet-backdrop.svelte-1n46o8q{display:none}} diff --git a/app/breakglass/static/assets/index-DjaW81Sq.js b/app/breakglass/static/assets/index-DjaW81Sq.js deleted file mode 100644 index f829538..0000000 --- a/app/breakglass/static/assets/index-DjaW81Sq.js +++ /dev/null @@ -1,16 +0,0 @@ -(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})(),typeof window<`u`&&((window.__svelte??={}).v??=new Set).add(`5`);var e={},t=Symbol(`uninitialized`),n=`http://www.w3.org/1999/xhtml`,r=Array.isArray,i=Array.prototype.indexOf,a=Array.prototype.includes,o=Array.from,s=Object.defineProperty,c=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyDescriptors,u=Object.prototype,d=Array.prototype,f=Object.getPrototypeOf,p=Object.isExtensible,m=()=>{};function h(e){for(var t=0;t{e=n,t=r}),resolve:e,reject:t}}var _=1024,v=2048,y=4096,b=8192,x=16384,S=32768,C=1<<25,w=65536,T=1<<19,ee=1<<20,te=1<<25,ne=65536,re=1<<21,ie=1<<22,ae=1<<23,oe=Symbol(`$state`),se=Symbol(`legacy props`),ce=Symbol(``),le=Symbol(`attributes`),ue=Symbol(`class`),de=Symbol(`style`),fe=Symbol(`text`),pe=Symbol(`form reset`),me=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},he=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function ge(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function _e(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function ve(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function ye(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function be(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function xe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function Se(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ce(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function we(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Te(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Ee(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function De(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}function Oe(){console.warn(`https://svelte.dev/e/derived_inert`)}function ke(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ae(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var E=!1;function je(e){E=e}var D;function O(t){if(t===null)throw ke(),e;return D=t}function Me(){return O(an(D))}function k(t){if(E){if(an(D)!==null)throw ke(),e;D=t}}function Ne(e=1){if(E){for(var t=e,n=D;t--;)n=an(n);D=n}}function Pe(e=!0){for(var t=0,n=D;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else (r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=an(n);e&&n.remove(),n=i}}function Fe(t){if(!t||t.nodeType!==8)throw ke(),e;return t.data}function Ie(e){return e===this.v}function Le(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function Re(e){return!Le(e,this.v)}var ze=!1,Be=!1,A=null;function Ve(e){A=e}function He(e,t=!1,n){A={p:A,i:!1,c:null,e:null,s:e,x:null,r:G,l:Be&&!t?{s:null,u:null,$:[]}:null}}function Ue(e){var t=A,n=t.e;if(n!==null){t.e=null;for(var r of n)Sn(r)}return e!==void 0&&(t.x=e),t.i=!0,A=t.p,e??{}}function We(){return!Be||A!==null&&A.l===null}var Ge=[];function Ke(){var e=Ge;Ge=[],h(e)}function qe(e){if(Ge.length===0&&!Et){var t=Ge;queueMicrotask(()=>{t===Ge&&Ke()})}Ge.push(e)}function Je(){for(;Ge.length>0;)Ke()}function Ye(e){var t=G;if(t===null)return H.f|=ae,e;if(!(t.f&32768)&&!(t.f&4))throw e;Xe(e,t)}function Xe(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}var Ze=~(v|y|_);function j(e,t){e.f=e.f&Ze|t}function Qe(e){e.f&512||e.deps===null?j(e,_):j(e,y)}function $e(e){if(e!==null)for(let t of e)!(t.f&2)||!(t.f&65536)||(t.f^=ne,$e(t.deps))}function et(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),$e(e.deps),j(e,_)}var tt=!1,nt=!1;function rt(e){var t=nt;try{return nt=!1,[e(),nt]}finally{nt=t}}function it(e){let t=0,n=Gt(0),r;return()=>{yn()&&(Y(n),En(()=>(t===0&&(r=or(()=>e(()=>Yt(n)))),t+=1,()=>{qe(()=>{--t,t===0&&(r?.(),r=void 0,Yt(n))})})))}}var at=w|T;function ot(e,t,n,r){new st(e,t,n,r)}var st=class{parent;is_pending=!1;transform_error;#e;#t=E?D:null;#n;#r;#i;#a=null;#o=null;#s=null;#c=null;#l=0;#u=0;#d=!1;#f=new Set;#p=new Set;#m=null;#h=it(()=>(this.#m=Gt(this.#l),()=>{this.#m=null}));constructor(e,t,n,r){this.#e=e,this.#n=t,this.#r=e=>{var t=G;t.b=this,t.f|=128,n(e)},this.parent=G.b,this.transform_error=r??this.parent?.transform_error??(e=>e),this.#i=Dn(()=>{if(E){let e=this.#t;Me();let t=e.data===`[!`;if(e.data.startsWith(`[?`)){let t=JSON.parse(e.data.slice(2));this.#_(t)}else t?this.#v():this.#g()}else this.#y()},at),E&&(this.#e=D)}#g(){try{this.#a=B(()=>this.#r(this.#e))}catch(e){this.error(e)}}#_(e){let t=this.#n.failed;t&&(this.#s=B(()=>{t(this.#e,()=>e,()=>()=>{})}))}#v(){let e=this.#n.pending;e&&(this.is_pending=!0,this.#o=B(()=>e(this.#e)),qe(()=>{var e=this.#c=document.createDocumentFragment(),t=I();e.append(t),this.#a=this.#x(()=>B(()=>this.#r(t))),this.#u===0&&(this.#e.before(e),this.#c=null,Nn(this.#o,()=>{this.#o=null}),this.#b(M))}))}#y(){try{if(this.is_pending=this.has_pending_snippet(),this.#u=0,this.#l=0,this.#a=B(()=>{this.#r(this.#e)}),this.#u>0){var e=this.#c=document.createDocumentFragment();Ln(this.#a,e);let t=this.#n.pending;this.#o=B(()=>t(this.#e))}else this.#b(M)}catch(e){this.error(e)}}#b(e){this.is_pending=!1,e.transfer_effects(this.#f,this.#p)}defer_effect(e){et(e,this.#f,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#n.pending}#x(e){var t=G,n=H,r=A;Hn(this.#i),W(this.#i),Ve(this.#i.ctx);try{return Mt.ensure(),e()}catch(e){return Ye(e),null}finally{Hn(t),W(n),Ve(r)}}#S(e,t){if(!this.has_pending_snippet()){this.parent&&this.parent.#S(e,t);return}this.#u+=e,this.#u===0&&(this.#b(t),this.#o&&Nn(this.#o,()=>{this.#o=null}),this.#c&&=(this.#e.before(this.#c),null))}update_pending_count(e,t){this.#S(e,t),this.#l+=e,!(!this.#m||this.#d)&&(this.#d=!0,qe(()=>{this.#d=!1,this.#m&&qt(this.#m,this.#l)}))}get_effect_pending(){return this.#h(),Y(this.#m)}error(e){if(!this.#n.onerror&&!this.#n.failed)throw e;M?.is_fork?(this.#a&&M.skip_effect(this.#a),this.#o&&M.skip_effect(this.#o),this.#s&&M.skip_effect(this.#s),M.oncommit(()=>{this.#C(e)})):this.#C(e)}#C(e){this.#a&&=(V(this.#a),null),this.#o&&=(V(this.#o),null),this.#s&&=(V(this.#s),null),E&&(O(this.#t),Ne(),O(Pe()));var t=this.#n.onerror;let n=this.#n.failed;var r=!1,i=!1;let a=()=>{if(r){Ae();return}r=!0,i&&De(),this.#s!==null&&Nn(this.#s,()=>{this.#s=null}),this.#x(()=>{this.#y()})},o=e=>{try{i=!0,t?.(e,a),i=!1}catch(e){Xe(e,this.#i&&this.#i.parent)}n&&(this.#s=this.#x(()=>{try{return B(()=>{var t=G;t.b=this,t.f|=128,n(this.#e,()=>e,()=>a)})}catch(e){return Xe(e,this.#i.parent),null}}))};qe(()=>{var t;try{t=this.transform_error(e)}catch(e){Xe(e,this.#i&&this.#i.parent);return}typeof t==`object`&&t&&typeof t.then==`function`?t.then(o,e=>Xe(e,this.#i&&this.#i.parent)):o(t)})}};function ct(e,t,n,r){let i=We()?ft:gt;var a=e.filter(e=>!e.settled),o=t.map(i);if(n.length===0&&a.length===0){r(o);return}var s=G,c=lt(),l=a.length===1?a[0].promise:a.length>1?Promise.all(a.map(e=>e.promise)):null;function u(e){if(!(s.f&16384)){c();try{r([...o,...e])}catch(e){Xe(e,s)}ut()}}var d=dt();if(n.length===0){l.then(()=>u([])).finally(d);return}function f(){Promise.all(n.map(e=>mt(e))).then(u).catch(e=>Xe(e,s)).finally(d)}l?l.then(()=>{c(),f(),ut()}):f()}function lt(){var e=G,t=H,n=A,r=M;return function(i=!0){Hn(e),W(t),Ve(n),i&&!(e.f&16384)&&(r?.activate(),r?.apply())}}function ut(e=!0){Hn(null),W(null),Ve(null),e&&M?.deactivate()}function dt(){var e=G,t=e.b,n=M,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ft(e){var n=2|v;return G!==null&&(G.f|=T),{ctx:A,deps:null,effects:null,equals:Ie,f:n,fn:e,reactions:null,rv:0,v:t,wv:0,parent:G,ac:null}}var pt=Symbol(`obsolete`);function mt(e,n,r){let i=G;i===null&&_e();var a=void 0,o=Gt(t),s=!H,c=new Set;return Tn(()=>{var t=G,n=g();a=n.promise;try{Promise.resolve(e()).then(n.resolve,e=>{e!==me&&n.reject(e)}).finally(ut)}catch(e){n.reject(e),ut()}var r=M;if(s){if(t.f&32768)var l=dt();if(i.b?.is_rendered())r.async_deriveds.get(t)?.reject(pt);else for(let e of c.values())e.reject(pt);c.add(n),r.async_deriveds.set(t,n)}let u=(e,t=void 0)=>{l?.(),c.delete(n),t!==pt&&(r.activate(),t?(o.f|=ae,qt(o,t)):(o.f&8388608&&(o.f^=ae),qt(o,e)),r.deactivate())};n.promise.then(u,e=>u(null,e||`unknown`))}),bn(()=>{for(let e of c)e.reject(pt)}),new Promise(e=>{function t(n){function r(){n===a?e(o):t(a)}n.then(r,r)}t(a)})}function ht(e){let t=ft(e);return ze||Wn(t),t}function gt(e){let t=ft(e);return t.equals=Re,t}function _t(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;nthis.schedule(e)){var n=this.#f.get(e);if(n){this.#f.delete(e);for(var r of n.d)j(r,v),t(r);for(r of n.m)j(r,y),t(r)}this.#p.add(e)}#g(){this.#e=!0,At++>1e3&&(this.#S(),Pt());for(let e of this.#u)this.#d.delete(e),j(e,v),this.schedule(e);for(let e of this.#d)j(e,y),this.schedule(e);let t=this.#c;this.#c=[],this.apply();var n=Ot=[],r=[],i=kt=[];for(let e of t)try{this.#_(e,n,r)}catch(t){throw Vt(e),this.#h()||this.discard(),t}if(M=null,i.length>0){var a=e.ensure();for(let e of i)a.schedule(e)}if(Ot=null,kt=null,this.#h()){this.#b(r),this.#b(n);for(let[e,t]of this.#f)Bt(e,t);i.length>0&&M.#g();return}let o=this.#v();if(o){this.#b(r),this.#b(n),o.#y(this);return}this.#u.clear(),this.#d.clear();for(let e of this.#r)e(this);this.#r.clear(),wt=this,It(r),It(n),wt=null,this.#s?.resolve();var s=M;if(this.#a===0&&(this.#c.length===0||s!==null)&&(this.#S(),ze&&(this.#x(),M=s)),this.#c.length>0)if(s!==null){let e=s;e.#c.push(...this.#c.filter(t=>!e.#c.includes(t)))}else s=this;s!==null&&s.#g()}#_(e,t,n){e.f^=_;for(var r=e.first;r!==null;){var i=r.f,a=(i&96)!=0;if(!(a&&i&1024||i&8192||this.#f.has(r))&&r.fn!==null){a?r.f^=_:i&4?t.push(r):ze&&i&16777224?n.push(r):Zn(r)&&(i&16&&this.#d.add(r),nr(r));var o=r.first;if(o!==null){r=o;continue}}for(;r!==null;){var s=r.next;if(s!==null){r=s;break}r=r.parent}}}#v(){for(var e=this.#t;e!==null;){if(!e.is_fork){for(let[t,[,n]]of this.current)if(e.current.has(t)&&!n)return e}e=e.#t}return null}#y(e){for(let[t,n]of e.current)!this.previous.has(t)&&e.previous.has(t)&&this.previous.set(t,e.previous.get(t)),this.current.set(t,n);for(let[t,n]of e.async_deriveds){let e=this.async_deriveds.get(t);e&&n.promise.then(e.resolve).catch(e.reject)}e.async_deriveds.clear(),this.transfer_effects(e.#u,e.#d);let t=e=>{var n=e.reactions;if(n!==null)for(let e of n){var r=e.f;if(r&2)t(e);else{var i=e;r&4194320&&!this.async_deriveds.has(i)&&(this.#d.delete(i),j(i,v),this.schedule(i))}}};for(let e of this.current.keys())t(e);this.oncommit(()=>e.discard()),e.#S(),M=this,this.#g()}#b(e){for(var t=0;t!u.current.get(e)[1]);if(!(!u.#e||r.length===0)){var i=r.filter(e=>!this.current.has(e));if(i.length===0)e&&u.discard();else if(t.length>0){if(e)for(let e of this.#p)u.unskip_effect(e,e=>{e.f&4194320?u.schedule(e):u.#b([e])});u.activate();var a=new Set,o=new Map;for(var s of t)Lt(s,i,a,o);o=new Map;var c=[...u.current].filter(([e,t])=>{let n=this.current.get(e);return n?n[0]!==t[0]||n[1]!==t[1]:!0}).map(([e])=>e);if(c.length>0)for(let e of this.#l)!(e.f&155648)&&Rt(e,c,o)&&(e.f&4194320?(j(e,v),u.schedule(e)):u.#u.add(e));if(u.#c.length>0&&!u.#m){u.apply();for(var l of u.#c)u.#_(l,[],[]);u.#c=[]}u.deactivate()}}}}increment(e,t){if(this.#a+=1,e){let e=this.#o.get(t)??0;this.#o.set(t,e+1)}}decrement(e,t){if(--this.#a,e){let e=this.#o.get(t)??0;e===1?this.#o.delete(t):this.#o.set(t,e-1)}this.#m||(this.#m=!0,qe(()=>{this.#m=!1,this.linked&&this.flush()}))}transfer_effects(e,t){for(let t of e)this.#u.add(t);for(let e of t)this.#d.add(e);e.clear(),t.clear()}oncommit(e){this.#r.add(e)}ondiscard(e){this.#i.add(e)}settled(){return(this.#s??=g()).promise}static ensure(){if(M===null){let t=M=new e;!Dt&&!Et&&qe(()=>{t.#e||t.flush()})}return M}apply(){if(!ze||!this.is_fork&&this.#t===null&&this.#n===null){N=null;return}N=new Map;for(let[e,[t]]of this.current)N.set(e,t);for(let t=St;t!==null;t=t.#n)if(!(t===this||t.is_fork)){var e=!1;if(t.id0)){Ut.clear();for(let e of Ft){if(e.f&24576)continue;let t=[e],n=e.parent;for(;n!==null;)Ft.has(n)&&(Ft.delete(n),t.push(n)),n=n.parent;for(let e=t.length-1;e>=0;e--){let n=t[e];n.f&24576||nr(n)}}Ft.clear()}}Ft=null}}function Lt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(let i of e.reactions){let e=i.f;e&2?Lt(i,t,n,r):e&4194320&&!(e&2048)&&Rt(i,t,r)&&(j(i,v),zt(i))}}function Rt(e,t,n){let r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(let r of e.deps){if(a.call(t,r))return!0;if(r.f&2&&Rt(r,t,n))return n.set(r,!0),!0}return n.set(e,!1),!1}function zt(e){M.schedule(e)}function Bt(e,t){if(!(e.f&32&&e.f&1024)){e.f&2048?t.d.push(e):e.f&4096&&t.m.push(e),j(e,_);for(var n=e.first;n!==null;)Bt(n,t),n=n.next}}function Vt(e){j(e,_);for(var t=e.first;t!==null;)Vt(t),t=t.next}var Ht=new Set,Ut=new Map,Wt=!1;function Gt(e,t){return{f:0,v:e,reactions:null,equals:Ie,rv:0,wv:0}}function P(e,t){let n=Gt(e,t);return Wn(n),n}function Kt(e,t=!1,n=!0){let r=Gt(e);return t||(r.equals=Re),Be&&n&&A!==null&&A.l!==null&&(A.l.s??=[]).push(r),r}function F(e,t,n=!1){return H!==null&&(!U||H.f&131072)&&We()&&H.f&4325394&&(Un===null||!Un.has(e))&&Ee(),qt(e,n?Zt(t):t,kt)}function qt(e,t,n=null){if(!e.equals(t)){Ut.set(e,Bn?t:e.v);var r=Mt.ensure();if(r.capture(e,t),e.f&2){let t=e;e.f&2048&&vt(t),N===null&&Qe(t)}e.wv=Xn(),Xt(e,v,n),We()&&G!==null&&G.f&1024&&!(G.f&96)&&(J===null?Gn([e]):J.push(e)),!r.is_fork&&Ht.size>0&&!Wt&&Jt()}return t}function Jt(){Wt=!1;for(let e of Ht){e.f&1024&&j(e,y);let t;try{t=Zn(e)}catch{t=!0}t&&nr(e)}Ht.clear()}function Yt(e){F(e,e.v+1)}function Xt(e,t,n){var r=e.reactions;if(r!==null)for(var i=We(),a=r.length,o=0;o{if(Jn===l)return e();var t=H,n=Jn;W(null),Yn(l);var r=e();return W(t),Yn(n),r};return a&&i.set(`length`,P(e.length,s)),new Proxy(e,{defineProperty(e,t,n){(!(`value`in n)||n.configurable===!1||n.enumerable===!1||n.writable===!1)&&we();var r=i.get(t);return r===void 0?p(()=>{var e=P(n.value,s);return i.set(t,e),e}):F(r,n.value,!0),!0},deleteProperty(e,n){var r=i.get(n);if(r===void 0){if(n in e){let e=p(()=>P(t,s));i.set(n,e),Yt(o)}}else F(r,t),Yt(o);return!0},get(n,r,a){if(r===oe)return e;var o=i.get(r),l=r in n;if(o===void 0&&(!l||c(n,r)?.writable)&&(o=p(()=>P(Zt(l?n[r]:t),s)),i.set(r,o)),o!==void 0){var u=Y(o);return u===t?void 0:u}return Reflect.get(n,r,a)},getOwnPropertyDescriptor(e,n){var r=Reflect.getOwnPropertyDescriptor(e,n);if(r&&`value`in r){var a=i.get(n);a&&(r.value=Y(a))}else if(r===void 0){var o=i.get(n),s=o?.v;if(o!==void 0&&s!==t)return{enumerable:!0,configurable:!0,value:s,writable:!0}}return r},has(e,n){if(n===oe)return!0;var r=i.get(n),a=r!==void 0&&r.v!==t||Reflect.has(e,n);return(r!==void 0||G!==null&&(!a||c(e,n)?.writable))&&(r===void 0&&(r=p(()=>P(a?Zt(e[n]):t,s)),i.set(n,r)),Y(r)===t)?!1:a},set(e,n,r,l){var u=i.get(n),d=n in e;if(a&&n===`length`)for(var f=r;fP(t,s)),i.set(f+``,m)):F(m,t)}if(u===void 0)(!d||c(e,n)?.writable)&&(u=p(()=>P(void 0,s)),F(u,Zt(r)),i.set(n,u));else{d=u.v!==t;var h=p(()=>Zt(r));F(u,h)}var g=Reflect.getOwnPropertyDescriptor(e,n);if(g?.set&&g.set.call(l,r),!d){if(a&&typeof n==`string`){var _=i.get(`length`),v=Number(n);Number.isInteger(v)&&v>=_.v&&F(_,v+1)}Yt(o)}return!0},ownKeys(e){Y(o);var n=Reflect.ownKeys(e).filter(e=>{var n=i.get(e);return n===void 0||n.v!==t});for(var[r,a]of i)a.v!==t&&!(r in e)&&n.push(r);return n},setPrototypeOf(){Te()}})}new Set([`copyWithin`,`fill`,`pop`,`push`,`reverse`,`shift`,`sort`,`splice`,`unshift`]);var Qt,$t,en,tn;function nn(){if(Qt===void 0){Qt=window,$t=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;en=c(t,`firstChild`).get,tn=c(t,`nextSibling`).get,p(e)&&(e[ue]=void 0,e[le]=null,e[de]=void 0,e.__e=void 0),p(n)&&(n[fe]=void 0)}}function I(e=``){return document.createTextNode(e)}function rn(e){return en.call(e)}function an(e){return tn.call(e)}function L(e,t){if(!E)return rn(e);var n=rn(D);if(n===null)n=D.appendChild(I());else if(t&&n.nodeType!==3){var r=I();return n?.before(r),O(r),r}return t&&un(n),O(n),n}function on(e,t=!1){if(!E){var n=rn(e);return n instanceof Comment&&n.data===``?an(n):n}if(t){if(D?.nodeType!==3){var r=I();return D?.before(r),O(r),r}un(D)}return D}function R(e,t=1,n=!1){let r=E?D:e;for(var i;t--;)i=r,r=an(r);if(!E)return r;if(n){if(r?.nodeType!==3){var a=I();return r===null?i?.after(a):r.before(a),O(a),a}un(r)}return O(r),r}function sn(e){e.textContent=``}function cn(){return!ze||Ft!==null?!1:(G.f&S)!==0}function ln(e,t,n){return t==null||t===`http://www.w3.org/1999/xhtml`?n?document.createElement(e,{is:n}):document.createElement(e):n?document.createElementNS(t,e,{is:n}):document.createElementNS(t,e)}function un(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===3;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function dn(e){E&&rn(e)!==null&&sn(e)}var fn=!1;function pn(){fn||(fn=!0,document.addEventListener(`reset`,e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(let t of e.target.elements)t[pe]?.()})},{capture:!0}))}function mn(e){var t=H,n=G;W(null),Hn(null);try{return e()}finally{W(t),Hn(n)}}function hn(e,t,n,r=n){e.addEventListener(t,()=>mn(n));let i=e[pe];i?e[pe]=()=>{i(),r(!0)}:e[pe]=()=>r(!0),pn()}function gn(e){G===null&&(H===null&&xe(e),be()),Bn&&ye(e)}function _n(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function vn(e,t){var n=G;n!==null&&n.f&8192&&(e|=b);var r={ctx:A,deps:null,nodes:null,f:e|v|512,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};M?.register_created_effect(r);var i=r;if(e&4)Ot===null?Mt.ensure().schedule(r):Ot.push(r);else if(t!==null){try{nr(r)}catch(e){throw V(r),e}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&!(i.f&524288)&&(i=i.first,e&16&&e&65536&&i!==null&&(i.f|=w))}if(i!==null&&(i.parent=n,n!==null&&_n(i,n),H!==null&&H.f&2&&!(e&64))){var a=H;(a.effects??=[]).push(i)}return r}function yn(){return H!==null&&!U}function bn(e){let t=vn(8,null);return j(t,_),t.teardown=e,t}function xn(e){gn(`$effect`);var t=G.f;if(!H&&t&32&&A!==null&&!A.i){var n=A;(n.e??=[]).push(e)}else return Sn(e)}function Sn(e){return vn(4|ee,e)}function Cn(e){Mt.ensure();let t=vn(64|T,e);return(e={})=>new Promise(n=>{e.outro?Nn(t,()=>{V(t),n(void 0)}):(V(t),n(void 0))})}function wn(e){return vn(4,e)}function Tn(e){return vn(ie|T,e)}function En(e,t=0){return vn(8|t,e)}function z(e,t=[],n=[],r=[]){ct(r,t,n,t=>{vn(8,()=>{e(...t.map(Y))})})}function Dn(e,t=0){return vn(16|t,e)}function B(e){return vn(32|T,e)}function On(e){var t=e.teardown;if(t!==null){let e=Bn,n=H;Vn(!0),W(null);try{t.call(null)}finally{Vn(e),W(n)}}}function kn(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){let e=n.ac;e!==null&&mn(()=>{e.abort(me)});var r=n.next;n.f&64?n.parent=null:V(n,t),n=r}}function An(e){for(var t=e.first;t!==null;){var n=t.next;t.f&32||V(t),t=n}}function V(e,t=!0){var n=!1;(t||e.f&262144)&&e.nodes!==null&&e.nodes.end!==null&&(jn(e.nodes.start,e.nodes.end),n=!0),e.f|=C,kn(e,t&&!n),tr(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)e.stop();On(e),e.f^=C,e.f|=x;var i=e.parent;i!==null&&i.first!==null&&Mn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function jn(e,t){for(;e!==null;){var n=e===t?null:an(e);e.remove(),e=n}}function Mn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Nn(e,t,n=!0){var r=[];Pn(e,r,!0);var i=()=>{n&&V(e),t&&t()},a=r.length;if(a>0){var o=()=>--a||i();for(var s of r)s.out(o)}else i()}function Pn(e,t,n){if(!(e.f&8192)){e.f^=b;var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)(e.is_global||n)&&t.push(e);for(var i=e.first;i!==null;){var a=i.next;if(!(i.f&64)){var o=(i.f&65536)!=0||(i.f&32)!=0&&(e.f&16)!=0;Pn(i,t,o?n:!1)}i=a}}}function Fn(e){In(e,!0)}function In(e,t){if(e.f&8192){e.f^=b,e.f&1024||(j(e,v),Mt.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=(n.f&65536)!=0||(n.f&32)!=0;In(n,i?t:!1),n=r}var a=e.nodes&&e.nodes.t;if(a!==null)for(let e of a)(e.is_global||t)&&e.in()}}function Ln(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:an(n);t.append(n),n=i}}var Rn=null,zn=!1,Bn=!1;function Vn(e){Bn=e}var H=null,U=!1;function W(e){H=e}var G=null;function Hn(e){G=e}var Un=null;function Wn(e){H!==null&&(!ze||H.f&2)&&(Un??=new Set).add(e)}var K=null,q=0,J=null;function Gn(e){J=e}var Kn=1,qn=0,Jn=qn;function Yn(e){Jn=e}function Xn(){return++Kn}function Zn(e){var t=e.f;if(t&2048)return!0;if(t&2&&(e.f&=~ne),t&4096){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}t&512&&N===null&&j(e,_)}return!1}function Qn(e,t,n=!0){var r=e.reactions;if(r!==null&&!(!ze&&Un!==null&&Un.has(e)))for(var i=0;i{e.ac.abort(me)}),e.ac=null);try{e.f|=re;var u=e.fn,d=u();e.f|=S;var f=e.deps,p=M?.is_fork;if(K!==null){var m;if(p||tr(e,q),f!==null&&q>0)for(f.length=q+K.length,m=0;m{requestAnimationFrame(()=>e()),setTimeout(()=>e())});await Promise.resolve(),Nt()}function Y(e){var t=(e.f&2)!=0;if(Rn?.add(e),H!==null&&!U&&!(G!==null&&G.f&16384)&&(Un===null||!Un.has(e))){var n=H.deps;if(H.f&2097152)e.rvn?.call(this,e))}return e.startsWith(`pointer`)||e.startsWith(`touch`)||e===`wheel`?qe(()=>{t.addEventListener(e,i,r)}):t.addEventListener(e,i,r),i}function dr(e,t,n,r,i){var a={capture:r,passive:i},o=ur(e,t,n,a);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&bn(()=>{t.removeEventListener(e,o,a)})}function fr(e,t,n){(t[sr]??={})[e]=n}function pr(e){for(var t=0;t{throw e});throw p}}finally{e[sr]=t,delete e.currentTarget,W(d),Hn(f)}}}var gr=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy(`svelte-trusted-html`,{createHTML:e=>e});function _r(e){return gr?.createHTML(e)??e}function vr(e){var t=ln(`template`);return t.innerHTML=_r(e.replaceAll(``,``)),t.content}function yr(e,t){var n=G;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function X(e,t){var n=(t&1)!=0,r=(t&2)!=0,i,a=!e.startsWith(``);return()=>{if(E)return yr(D,null),D;i===void 0&&(i=vr(a?e:``+e),n||(i=rn(i)));var t=r||$t?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=rn(t),s=t.lastChild;yr(o,s)}else yr(t,t);return t}}function br(){if(E)return yr(D,null),D;var e=document.createDocumentFragment(),t=document.createComment(``),n=I();return e.append(t,n),yr(t,n),e}function Z(e,t){if(E){var n=G;(!(n.f&32768)||n.nodes.end===null)&&(n.nodes.end=D),Me();return}e!==null&&e.before(t)}[...`allowfullscreen.async.autofocus.autoplay.checked.controls.default.disabled.formnovalidate.indeterminate.inert.ismap.loop.multiple.muted.nomodule.novalidate.open.playsinline.readonly.required.reversed.seamless.selected.webkitdirectory.defer.disablepictureinpicture.disableremoteplayback`.split(`.`)];var xr=[`touchstart`,`touchmove`];function Sr(e){return xr.includes(e)}function Q(e,t){var n=t==null?``:typeof t==`object`?`${t}`:t;n!==(e[fe]??=e.nodeValue)&&(e[fe]=n,e.nodeValue=`${n}`)}function Cr(e,t){return Tr(e,t)}var wr=new Map;function Tr(t,{target:n,anchor:r,props:i={},events:a,context:s,intro:c=!0,transformError:l}){nn();var u=void 0,d=Cn(()=>{var c=r??n.appendChild(I());ot(c,{pending:()=>{}},n=>{He({});var r=A;if(s&&(r.c=s),a&&(i.$$events=a),E&&yr(n,null),u=t(n,i)||{},E&&(G.nodes.end=D,D===null||D.nodeType!==8||D.data!==`]`))throw ke(),e;Ue()},l);var d=new Set,f=e=>{for(var t=0;t{for(var e of d)for(let r of[n,document]){var t=wr.get(r),i=t.get(e);--i==0?(r.removeEventListener(e,hr),t.delete(e),t.size===0&&wr.delete(r)):t.set(e,i)}lr.delete(f),c!==r&&c.parentNode?.removeChild(c)}});return Er.set(u,d),u}var Er=new WeakMap,Dr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)Fn(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(Fn(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(V(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();Ln(r,t),t.append(I()),this.#n.set(e,{effect:r,fragment:t})}else V(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),Nn(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(V(n.effect),this.#n.delete(e))};ensure(e,t){var n=M,r=cn();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=I();i.append(a),this.#n.set(e,{effect:B(()=>t(a)),fragment:i})}else this.#t.set(e,B(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else E&&(this.anchor=D),this.#a(n)}};function Or(e){A===null&&ge(`onMount`),Be&&A.l!==null?kr(A).m.push(e):xn(()=>{let t=or(e);if(typeof t==`function`)return t})}function kr(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}function $(e,t,n=!1){var r;E&&(r=D,Me());var i=new Dr(e),a=n?w:0;function o(e,t){if(E){var n=Fe(r);if(e!==parseInt(n.substring(1))){var a=Pe();O(a),i.anchor=a,je(!1),i.ensure(e,t),je(!0);return}}i.ensure(e,t)}Dn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}function Ar(e,t){return t}function jr(e,t,n){for(var r=[],i=t.length,a,s=t.length,c=0;c{if(a){if(a.pending.delete(n),a.done.add(n),a.pending.size===0){var t=e.outrogroups;Mr(e,o(a.done)),t.delete(a),t.size===0&&(e.outrogroups=null)}}else --s},!1)}if(s===0){var l=r.length===0&&n!==null;if(l){var u=n,d=u.parentNode;sn(d),d.append(u),e.items.clear()}Mr(e,t,!l)}else a={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(a)}function Mr(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i{var e=n();return r(e)?e:e==null?[]:o(e)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,Ir(v,p,c,t,i),d!==null&&(p.length===0?d.f&33554432?(d.f^=te,Rr(d,null,c)):Fn(d):Nn(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:Dn(()=>{p=Y(f);var e=p.length;let r=!1;E&&Fe(c)===`[!`!=(e===0)&&(c=Pe(),O(c),je(!1),r=!0);for(var o=new Set,u=M,v=cn(),y=0;ys(c)):(d=B(()=>s(Nr??=I())),d.f|=te)),e>o.size&&ve(``,``,``),E&&e>0&&O(Pe()),!h)if(m.set(u,o),v){for(let[e,t]of l)o.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u);r&&je(!0),Y(f)}),flags:t,items:l,pending:m,outrogroups:null,fallback:d};h=!1,E&&(c=D)}function Fr(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function Ir(e,t,n,r,i){var a=(r&8)!=0,s=t.length,c=e.items,l=Fr(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(a)for(v=0;v0){var ee=r&4&&s===0?n:null;if(a){for(v=0;v{if(f!==void 0)for(_ of f)_.nodes?.a?.apply()})}function Lr(e,t,n,r,i,a,o,s){var c=o&1?o&16?Gt(n):Kt(n,!1,!1):null,l=o&2?Gt(i):null;return{v:c,i:l,e:B(()=>(a(t,c??n,l??i,s),()=>{e.delete(r)}))}}function Rr(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,a=t&&!(t.f&33554432)?t.nodes.start:n;r!==null;){var o=an(r);if(a.before(r),r===i)return;r=o}}function zr(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}var Br=[...` -\r\f\xA0\v`];function Vr(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||Br.includes(r[o-1]))&&(s===r.length||Br.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function Hr(e,t,n,r,i,a){var o=e[ue];if(E||o!==n||o===void 0){var s=Vr(n,r,a);(!E||s!==e.getAttribute(`class`))&&(s==null?e.removeAttribute(`class`):t?e.className=s:e.setAttribute(`class`,s)),e[ue]=n}else if(a&&i!==a)for(var c in a){var l=!!a[c];(i==null||l!==!!i[c])&&e.classList.toggle(c,l)}return a}var Ur=Symbol(`is custom element`),Wr=Symbol(`is html`),Gr=he?`link`:`LINK`;function Kr(e,t,n,r){var i=qr(e);E&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===Gr)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[ce]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&Yr(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function qr(e){return e[le]??={[Ur]:e.nodeName.includes(`-`),[Wr]:e.namespaceURI===n}}var Jr=new Map;function Yr(e){var t=e.getAttribute(`is`)||e.nodeName,n=Jr.get(t);if(n)return n;Jr.set(t,n=[]);for(var r,i=e,a=Element.prototype;a!==i;){for(var o in r=l(i),r)r[o].set&&o!==`innerHTML`&&o!==`textContent`&&o!==`innerText`&&n.push(o);i=f(i)}return n}function Xr(e,t,n=t){var r=new WeakSet;hn(e,`input`,async i=>{var a=i?e.defaultValue:e.value;if(a=Zr(e)?Qr(a):a,n(a),M!==null&&r.add(M),await rr(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(E&&e.defaultValue!==e.value||or(t)==null&&e.value)&&(n(Zr(e)?Qr(e.value):e.value),M!==null&&r.add(M)),En(()=>{var n=t();if(e===document.activeElement){var i=ze?wt:M;if(r.has(i))return}Zr(e)&&n===Qr(e.value)||e.type===`date`&&!n&&!e.value||n!==e.value&&(e.value=n??``)})}function Zr(e){var t=e.type;return t===`number`||t===`range`}function Qr(e){return e===``?null:+e}function $r(e,t){return e===t||e?.[oe]===t}function ei(e={},t,n,r){var i=A.r,a=G;return wn(()=>{var o,s;return En(()=>{o=s,s=r?.()||[],or(()=>{$r(n(...s),e)||(t(e,...s),o&&$r(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&$r(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function ti(e,t,n,r){var i=!Be||(n&2)!=0,a=(n&8)!=0,o=(n&16)!=0,s=r,l=!0,u=void 0,d=()=>o&&i?(u??=ft(r),Y(u)):(l&&(l=!1,s=o?or(r):r),s);let f;if(a){var p=oe in e||se in e;f=c(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;a?[m,h]=rt(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Ce(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(s=void 0),n===void 0?s:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?ft:gt)(()=>(v=!1,g()));a&&Y(y);var b=G;return(function(e,t){if(arguments.length>0){let n=t?Y(y):i&&a?Zt(e):e;return F(y,n),v=!0,s!==void 0&&(s=n),e}return Bn&&v||b.f&16384?y.v:Y(y)})}function ni(e){let t=[];for(let n of e.split(` -`)){let e=n.replace(/\r$/,``);if(!e.startsWith(`:`)){if(e===`data:`||e===`data`)t.push(``);else if(e.startsWith(`data:`)){let n=e.slice(5);n.startsWith(` `)&&(n=n.slice(1)),t.push(n)}}}return t.length===0?null:t.join(` -`)}var ri=class{constructor(){this.buffer=``}push(e){this.buffer+=e;let t=[],n;for(;(n=this._nextDelimiter())!==-1;){let e=this.buffer.slice(0,n.start);this.buffer=this.buffer.slice(n.end),e.length>0&&t.push(e)}return t}flush(){let e=this.buffer.trim();return this.buffer=``,e?[e]:[]}_nextDelimiter(){let e=[{token:`\r -\r -`,i:this.buffer.indexOf(`\r -\r -`)},{token:` - -`,i:this.buffer.indexOf(` - -`)},{token:`\r\r`,i:this.buffer.indexOf(`\r\r`)}].filter(e=>e.i!==-1);if(e.length===0)return-1;e.sort((e,t)=>e.i-t.i);let{token:t,i:n}=e[0];return{start:n,end:n+t.length}}};async function ii(e,t){if(!e.ok)throw Error(`server returned ${e.status} ${e.statusText}`);if(!e.body)throw Error(`response has no readable body (streaming unsupported)`);let n=e.body.getReader(),r=new TextDecoder,i=new ri,a=e=>{let n=ni(e);if(n==null||n.trim()===``)return;let r;try{r=JSON.parse(n)}catch{return}t(r)};try{for(;;){let{value:e,done:t}=await n.read();if(t)break;let o=r.decode(e,{stream:!0});for(let e of i.push(o))a(e)}}finally{n.releaseLock?.()}let o=r.decode();if(o)for(let e of i.push(o))a(e);for(let e of i.flush())a(e)}async function ai(){let e=await fetch(`/api/session`,{method:`POST`,headers:{"content-type":`application/json`}});if(!e.ok)throw Error(`could not open a session (HTTP ${e.status})`);let t=await e.json();if(!t||typeof t.session_id!=`string`)throw Error(`session response missing session_id`);return t.session_id}async function oi({session_id:e,prompt:t,model:n,signal:r},i){let a={session_id:e,prompt:t};n&&(a.model=n),await ii(await fetch(`/api/chat`,{method:`POST`,headers:{"content-type":`application/json`,accept:`text/event-stream`},body:JSON.stringify(a),signal:r}),i)}async function si(){let e=await fetch(`/api/pve/verbs`);if(!e.ok)throw Error(`could not load VM controls (HTTP ${e.status})`);let t=await e.json();return{verbs:Array.isArray(t.verbs)?t.verbs:[],mutating:Array.isArray(t.mutating)?t.mutating:[]}}async function ci(e){let t=await fetch(`/api/pve/${encodeURIComponent(e)}`,{method:`POST`,headers:{"content-type":`application/json`}}),n;try{n=await t.json()}catch{throw Error(`VM control '${e}' failed (HTTP ${t.status}, no body)`)}if(t.status===400)throw Error(n?.detail||`'${e}' was rejected by the server`);return{verb:n.verb??e,exit_code:n.exit_code??null,stdout:n.stdout??``,stderr:n.stderr??``,rejected:!!n.rejected}}var li=X(` `,1),ui=X(` `);function di(e,t){let n=ti(t,`name`,3,`tool`),r=ti(t,`command`,3,``);var i=ui(),a=R(L(i),2),o=L(a,!0);k(a);var s=R(a,2),c=e=>{var t=li(),n=R(on(t),2),i=L(n,!0);k(n),z(()=>Q(i,r())),Z(e,t)};$(s,e=>{r()&&e(c)}),k(i),z(()=>{Kr(i,`title`,r()?`${n()}: ${r()}`:n()),Q(o,n())}),Z(e,i)}var fi=X(`

The agent is standing by.

Describe the symptom — "devvm is unreachable", "disk full", "ssh hangs" - — and it will connect over SSH, investigate, and stream its work here. - For a hard power action when the agent can't help, use Direct VM control.

`),pi=X(`
`),mi=X(``),hi=X(` `),gi=X(`
`),_i=X(` `),vi=X(` `),yi=X(`
`),bi=X(`
`),xi=X(`
agent working — streaming live
`),Si=X(`
Recovery agent SSHes into the devvm to diagnose & repair
`);function Ci(e,t){He(t,!0);let n=ti(t,`sessionId`,3,``),r=ti(t,`sessionReady`,3,!1),i=ti(t,`onLiveSession`,3,e=>{}),a=ti(t,`onStreamingChange`,3,e=>{}),o=P(Zt([])),s=P(``),c=P(!1),l,u,d=!0,f=ht(()=>r()&&!Y(c)&&Y(s).trim().length>0);function p(){l&&(d=l.scrollHeight-l.scrollTop-l.clientHeight<60)}async function m(e=!1){!e&&!d||(await rr(),l&&(l.scrollTop=l.scrollHeight))}function h(){return Y(o)[Y(o).length-1]}function g(e){let t=h().parts,n=t[t.length-1];n&&n.type===`text`?n.text+=e:t.push({type:`text`,text:e}),F(o,Y(o),!0)}function _(e){switch(e?.kind){case`session`:i()(e.session_id);break;case`text`:e.text&&g(e.text);break;case`tool`:{let t=e.input&&typeof e.input.command==`string`?e.input.command:``;h().parts.push({type:`tool`,name:e.name||`tool`,command:t}),F(o,Y(o),!0);break}case`result`:h().result={is_error:!!e.is_error,text:typeof e.result==`string`?e.result:``,duration_ms:typeof e.duration_ms==`number`?e.duration_ms:null},F(o,Y(o),!0);break;case`error`:h().error=e.error||`unknown error`,F(o,Y(o),!0);break;case`done`:break;default:break}m()}async function v(){let e=Y(s).trim();if(!(!e||Y(c)||!r())){Y(o).push({role:`user`,text:e}),Y(o).push({role:`assistant`,parts:[]}),F(o,Y(o),!0),F(s,``),F(c,!0),a()(!0),d=!0,await m(!0);try{await oi({session_id:n(),prompt:e},_)}catch(e){let t=h();t&&t.role===`assistant`&&!t.error&&(t.error=(e instanceof Error?e.message:String(e))+` — the connection to the agent failed.`,F(o,Y(o),!0))}finally{F(c,!1),a()(!1),await m(),u?.focus()}}}function y(e){e.key===`Enter`&&!e.shiftKey&&(e.preventDefault(),v())}function b(e){return e==null?``:e<1e3?`${e} ms`:`${(e/1e3).toFixed(+(e<1e4))} s`}let x=ht(()=>Y(o).length===0);var S=Si(),C=R(L(S),2),w=L(C),T=e=>{Z(e,fi())};$(w,e=>{Y(x)&&e(T)}),Pr(R(w,2),17,()=>Y(o),Ar,(e,t)=>{var n=br(),r=on(n),i=e=>{var n=pi(),r=L(n),i=L(r,!0);k(r),k(n),z(()=>Q(i,Y(t).text)),Z(e,n)},a=e=>{var n=bi(),r=L(n),i=L(r),a=e=>{Z(e,mi())};$(i,e=>{Y(t).parts.length===0&&!Y(t).result&&!Y(t).error&&e(a)});var o=R(i,2);Pr(o,17,()=>Y(t).parts,Ar,(e,t)=>{var n=br(),r=on(n),i=e=>{var n=hi(),r=L(n,!0);k(n),z(()=>Q(r,Y(t).text)),Z(e,n)},a=e=>{di(e,{get name(){return Y(t).name},get command(){return Y(t).command}})};$(r,e=>{Y(t).type===`text`?e(i):e(a,-1)}),Z(e,n)});var s=R(o,2),c=e=>{var n=gi(),r=L(n);k(n),z(()=>Q(r,`⚠ ${Y(t).error??``}`)),Z(e,n)},l=e=>{var n=yi(),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{var n=_i(),r=L(n,!0);k(n),z(()=>Q(r,Y(t).result.text)),Z(e,n)};$(a,e=>{Y(t).result.text&&e(o)});var s=R(a,2),c=e=>{var n=vi(),r=L(n,!0);k(n),z(e=>Q(r,e),[()=>b(Y(t).result.duration_ms)]),Z(e,n)};$(s,e=>{Y(t).result.duration_ms!=null&&e(c)}),k(n),z(()=>{Hr(n,1,`turn-note ${Y(t).result.is_error?`turn-note--error`:`turn-note--ok`}`,`svelte-1bi93vx`),Q(i,Y(t).result.is_error?`failed`:`done`)}),Z(e,n)};$(s,e=>{Y(t).error?e(c):Y(t).result&&e(l,1)}),k(r),k(n),Z(e,n)};$(r,e=>{Y(t).role===`user`?e(i):e(a,-1)}),Z(e,n)}),k(C),ei(C,e=>l=e,()=>l);var ee=R(C,2),te=L(ee),ne=e=>{Z(e,xi())};$(te,e=>{Y(c)&&e(ne)});var re=R(te,2),ie=L(re);dn(ie),ei(ie,e=>u=e,()=>u);var ae=R(ie,2),oe=L(ae,!0);k(ae),k(re),k(ee),k(S),z(()=>{Kr(ie,`placeholder`,r()?`Describe the problem… (Enter to send · Shift+Enter for a new line)`:`Waiting for a session…`),ie.disabled=!r()||Y(c),ae.disabled=!Y(f),Q(oe,Y(c)?`…`:`Send`)}),dr(`scroll`,C,p),dr(`submit`,ee,e=>{e.preventDefault(),v()}),fr(`keydown`,ie,y),Xr(ie,()=>Y(s),e=>F(s,e)),Z(e,S),Ue()}pr([`keydown`]);var wi=X(`
Loading controls…
`),Ti=X(``),Ei=X(``),Di=X(``),Oi=X(``),ki=X(`recovery`),Ai=X(`
Confirm ? This will affect the running VM
`),ji=X(`

`),Mi=X(``),Ni=X(`rejected`),Pi=X(` `),Fi=X(`
 
`),Ii=X(`
stderr
 
`,1),Li=X(`
(no output)
`),Ri=X(`
`),zi=X(`
Inspect read-only
Power affects the running VM
`,1),Bi=X(`

Direct VM control

No AI in the path — these reach the Proxmox host over a - forced-command SSH key and work even when the agent is down.

`);function Vi(e,t){He(t,!0);let n={status:{label:`status`,blurb:`qm status — is the VM up?`},forensics:{label:`forensics`,blurb:`capture live diagnostic state`},start:{label:`start`,blurb:`power on a stopped VM`},stop:{label:`stop`,blurb:`hard power-off (pulls the plug)`},reset:{label:`reset`,blurb:`warm reboot — reuses the QEMU process`},cycle:{label:`cycle`,blurb:`stop → start; applies staged config; fixes a wedged QEMU`,headline:!0}},r=[`status`,`forensics`,`start`,`stop`,`reset`,`cycle`],i=P(`loading`),a=P(``),o=P(Zt([])),s=P(``),c=P(``),l=P(null),u=P(``),d=ht(()=>Y(c)!==``);Or(async()=>{try{let{verbs:e,mutating:t}=await si(),a=new Set(t),s=e.filter(e=>n[e]);F(o,[...r.filter(e=>s.includes(e)),...s.filter(e=>!r.includes(e))].map(e=>({name:e,mutating:a.has(e),...n[e]})),!0),F(i,`ready`)}catch(e){F(i,`error`),F(a,e instanceof Error?e.message:String(e),!0)}});let f=ht(()=>Y(o).filter(e=>!e.mutating)),p=ht(()=>Y(o).filter(e=>e.mutating));function m(e){Y(d)||(e.mutating?F(s,Y(s)===e.name?``:e.name,!0):g(e.name))}function h(){F(s,``)}async function g(e){F(s,``),F(u,``),F(l,null),F(c,e,!0);try{F(l,await ci(e),!0)}catch(e){F(u,e instanceof Error?e.message:String(e),!0)}finally{F(c,``)}}let _=ht(()=>!!Y(l)&&(Y(l).rejected||Y(l).exit_code!=null&&Y(l).exit_code!==0));var v=Bi(),y=R(L(v),2),b=e=>{Z(e,wi())},x=e=>{var t=Ti(),n=L(t),r=R(n);k(t),z(()=>Q(n,`Couldn't load the VM controls — ${Y(a)??``}. `)),fr(`click`,r,()=>location.reload()),Z(e,t)},S=e=>{var t=zi(),n=on(t),r=R(L(n),2);Pr(r,21,()=>Y(f),e=>e.name,(e,t)=>{var n=Di(),r=L(n),i=e=>{Z(e,Ei())};$(r,e=>{Y(c)===Y(t).name&&e(i)});var a=R(r,2),o=L(a,!0);k(a),k(n),z(()=>{n.disabled=Y(d),Kr(n,`title`,Y(t).blurb),Q(o,Y(t).label)}),fr(`click`,n,()=>m(Y(t))),Z(e,n)}),k(r),k(n);var i=R(n,2),a=R(L(i),2);Pr(a,21,()=>Y(p),e=>e.name,(e,t)=>{var n=ji(),r=L(n),i=L(r),a=e=>{Z(e,Oi())};$(i,e=>{Y(c)===Y(t).name&&e(a)});var o=R(i,2),l=L(o,!0);k(o);var u=R(o,2),f=e=>{Z(e,ki())};$(u,e=>{Y(t).headline&&e(f)}),k(r);var p=R(r,2),_=L(p,!0);k(p);var v=R(p,2),y=e=>{var n=Ai(),r=L(n),i=R(L(r)),a=L(i,!0);k(i),Ne(),k(r);var o=R(r,2),s=L(o),c=R(s,2);k(o),k(n),z(()=>{Kr(n,`aria-label`,`Confirm ${Y(t).name??``}`),Q(a,Y(t).name),s.disabled=Y(d),c.disabled=Y(d)}),fr(`click`,s,()=>g(Y(t).name)),fr(`click`,c,h),Z(e,n)};$(v,e=>{Y(s)===Y(t).name&&e(y)}),k(n),z(()=>{Hr(n,1,`danger-item ${Y(t).headline?`danger-item--headline`:``}`,`svelte-1qihpg4`),Hr(r,1,`vbtn vbtn--danger ${Y(t).headline?`vbtn--headline`:``}`,`svelte-1qihpg4`),r.disabled=Y(d),Kr(r,`aria-expanded`,Y(s)===Y(t).name),Q(l,Y(t).label),Q(_,Y(t).blurb)}),fr(`click`,r,()=>m(Y(t))),Z(e,n)}),k(a),k(i);var o=R(i,2),v=e=>{var t=Mi(),n=L(t);k(t),z(()=>Q(n,`⚠ Command failed to reach the host — ${Y(u)??``}`)),Z(e,t)};$(o,e=>{Y(u)&&e(v)});var y=R(o,2),b=e=>{var t=Ri(),n=L(t),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{Z(e,Ni())},s=e=>{var t=Pi(),n=L(t);k(t),z(()=>{Hr(t,1,`out-status ${Y(_)?`out-status--fail`:`out-status--ok`}`,`svelte-1qihpg4`),Q(n,`exit ${Y(l).exit_code??``}`)}),Z(e,t)};$(a,e=>{Y(l).rejected?e(o):e(s,-1)}),k(n);var c=R(n,2),u=e=>{var t=Fi(),n=L(t,!0);k(t),z(()=>Q(n,Y(l).stdout)),Z(e,t)};$(c,e=>{Y(l).stdout&&e(u)});var d=R(c,2),f=e=>{var t=Ii(),n=R(on(t),2),r=L(n,!0);k(n),z(()=>Q(r,Y(l).stderr)),Z(e,t)};$(d,e=>{Y(l).stderr&&e(f)});var p=R(d,2),m=e=>{Z(e,Li())};$(p,e=>{!Y(l).stdout&&!Y(l).stderr&&e(m)}),k(t),z(()=>{Hr(t,1,`out ${Y(_)?`out--fail`:`out--ok`}`,`svelte-1qihpg4`),Q(i,Y(l).verb)}),Z(e,t)};$(y,e=>{Y(l)&&e(b)}),Z(e,t)};$(y,e=>{Y(i)===`loading`?e(b):Y(i)===`error`?e(x,1):e(S,-1)}),k(v),Z(e,v),Ue()}pr([`click`]);var Hi=X(`offline`),Ui=X(`connecting…`),Wi=X(` `),Gi=X(``),Ki=X(`

devvm breakglass

`);function qi(e,t){He(t,!0);let n=P(``),r=P(`connecting`),i=P(``),a=P(!1),o=P(!1);async function s(){F(r,`connecting`),F(i,``);try{F(n,await ai(),!0),F(r,`ready`)}catch(e){F(r,`error`),F(i,e instanceof Error?e.message:String(e),!0)}}Or(s);function c(e){e&&F(n,e,!0)}let l=ht(()=>Y(n)?Y(n).slice(0,8):`────────`),u=ht(()=>Y(r)===`error`?`error`:Y(a)?`busy`:Y(r)===`ready`?`ready`:`idle`);var d=Ki(),f=L(d),p=R(L(f),2),m=L(p),h=L(m),g=R(h,2),_=e=>{Z(e,Hi())},v=e=>{Z(e,Ui())},y=e=>{var t=Wi(),r=L(t,!0);k(t),z(()=>{Kr(t,`title`,Y(n)),Q(r,Y(l))}),Z(e,t)};$(g,e=>{Y(r)===`error`?e(_):Y(r)===`connecting`?e(v,1):e(y,-1)}),k(m);var b=R(m,2),x=R(b,2);k(p),k(f);var S=R(f,2),C=e=>{var t=Gi(),n=L(t);Ne(2),k(t),z(()=>Q(n,`Can't reach the breakglass backend — ${Y(i)??``}. The cluster or network - may be down. The `)),Z(e,t)};$(S,e=>{Y(r)===`error`&&e(C)});var w=R(S,2),T=L(w),ee=L(T);{let e=ht(()=>Y(r)===`ready`);Ci(ee,{get sessionId(){return Y(n)},get sessionReady(){return Y(e)},onLiveSession:c,onStreamingChange:e=>F(a,e,!0)})}k(T);var te=R(T,2);let ne;var re=R(L(te),2),ie=R(L(re),2);k(re),Vi(R(re,2),{}),k(te),k(w);var ae=R(w,2);let oe;k(d),z(()=>{Hr(h,1,`dot dot--${Y(u)??``}`,`svelte-1n46o8q`),x.disabled=Y(a)||Y(r)===`connecting`,Kr(x,`title`,Y(a)?`wait for the current turn to finish`:`start a fresh session`),ne=Hr(te,1,`controls-pane svelte-1n46o8q`,null,ne,{open:Y(o)}),oe=Hr(ae,1,`sheet-backdrop svelte-1n46o8q`,null,oe,{show:Y(o)})}),fr(`click`,b,()=>F(o,!0)),fr(`click`,x,s),fr(`click`,ie,()=>F(o,!1)),fr(`click`,ae,()=>F(o,!1)),Z(e,d),Ue()}pr([`click`]),Cr(qi,{target:document.getElementById(`app`)}); \ No newline at end of file diff --git a/app/breakglass/static/icon-192.png b/app/breakglass/static/icon-192.png new file mode 100644 index 0000000..76162e2 Binary files /dev/null and b/app/breakglass/static/icon-192.png differ diff --git a/app/breakglass/static/icon-512.png b/app/breakglass/static/icon-512.png new file mode 100644 index 0000000..f3336c4 Binary files /dev/null and b/app/breakglass/static/icon-512.png differ diff --git a/app/breakglass/static/icon.svg b/app/breakglass/static/icon.svg new file mode 100644 index 0000000..536585a --- /dev/null +++ b/app/breakglass/static/icon.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/breakglass/static/index.html b/app/breakglass/static/index.html index 4010b4d..30a748e 100644 --- a/app/breakglass/static/index.html +++ b/app/breakglass/static/index.html @@ -2,12 +2,31 @@ - + + + + + + + + + + + + + + devvm breakglass - - + +
diff --git a/app/breakglass/static/manifest.webmanifest b/app/breakglass/static/manifest.webmanifest new file mode 100644 index 0000000..965ac11 --- /dev/null +++ b/app/breakglass/static/manifest.webmanifest @@ -0,0 +1,31 @@ +{ + "name": "devvm breakglass", + "short_name": "breakglass", + "description": "Emergency recovery console for the devvm — chat with a repair agent or power-cycle the VM directly.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "orientation": "portrait", + "background_color": "#06080b", + "theme_color": "#06080b", + "icons": [ + { + "src": "./icon.svg", + "type": "image/svg+xml", + "sizes": "any", + "purpose": "any maskable" + }, + { + "src": "./icon-192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "./icon-512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/index.html b/frontend/index.html index e04f111..7226e10 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,28 @@ - + + + + + + + + + + + + + + devvm breakglass diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..e5763f2 Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000..76162e2 Binary files /dev/null and b/frontend/public/icon-192.png differ diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png new file mode 100644 index 0000000..f3336c4 Binary files /dev/null and b/frontend/public/icon-512.png differ diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..536585a --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..965ac11 --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,31 @@ +{ + "name": "devvm breakglass", + "short_name": "breakglass", + "description": "Emergency recovery console for the devvm — chat with a repair agent or power-cycle the VM directly.", + "start_url": "./", + "scope": "./", + "display": "standalone", + "orientation": "portrait", + "background_color": "#06080b", + "theme_color": "#06080b", + "icons": [ + { + "src": "./icon.svg", + "type": "image/svg+xml", + "sizes": "any", + "purpose": "any maskable" + }, + { + "src": "./icon-192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any maskable" + }, + { + "src": "./icon-512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index e1376d5..0efd5d8 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,100 +1,294 @@
-
+
- -

devvm breakglass

+ +

devvm breakglass

- - - {#if sessionState === 'error'} - offline - {:else if sessionState === 'connecting'} - connecting… - {:else} - {shortId} - {/if} + + + + {#if lamp === 'error'} + link down + {:else if lamp === 'working'} + working + {:else if lamp === 'live'} + {shortId} + {:else} + connecting + {/if} +
- {#if sessionState === 'error'} - diff --git a/frontend/src/VmControls.svelte b/frontend/src/VmControls.svelte index 4db0019..8359e0c 100644 --- a/frontend/src/VmControls.svelte +++ b/frontend/src/VmControls.svelte @@ -293,7 +293,8 @@ align-items: center; justify-content: center; gap: 8px; - padding: 9px 15px; + min-height: 44px; /* touch target */ + padding: 10px 16px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 600; @@ -408,7 +409,8 @@ } .confirm-yes { flex: 1; - padding: 9px; + min-height: 44px; + padding: 10px; border-radius: var(--radius-sm); border: 1px solid var(--danger-bright); background: var(--danger); @@ -424,7 +426,8 @@ } .confirm-no { flex: 1; - padding: 9px; + min-height: 44px; + padding: 10px; border-radius: var(--radius-sm); border: 1px solid var(--line-strong); background: var(--bg-2); diff --git a/frontend/src/app.css b/frontend/src/app.css index 9e82129..03b9e3b 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,48 +1,70 @@ /* ─────────────────────────────────────────────────────────────────────────── devvm breakglass — global theme - A recovery console: dark, high-contrast, terminal-adjacent. Calm by default; - danger is the only loud thing on the screen. No external fonts/CDNs — system - monospace carries the identity, system sans carries readable prose. + Emergency recovery console / instrument panel. Dark, high-contrast, monospace + identity, calm by default. Danger (red) is reserved EXCLUSIVELY for the + destructive VM power actions — nothing else on the screen is ever red. No + external fonts/CDNs (air-gapped cluster): a refined system-monospace stack + carries the identity, system-sans carries readable prose. Distinctiveness is + earned through composition, the living "system pulse" lamp, motion, hairlines, + and the reserved danger treatment — not through a downloaded typeface. ─────────────────────────────────────────────────────────────────────────── */ :root { - /* Surfaces — a near-black slate with cool undertone, layered for depth. */ - --bg-0: #07090c; /* page base */ - --bg-1: #0c1015; /* panel */ - --bg-2: #11171e; /* raised panel / input */ - --bg-3: #161d26; /* chips, hover */ - --bg-term: #06080a; /* command-output panels */ + /* Surfaces — a near-black slate with a cool undertone, layered for depth. */ + --bg-0: #06080b; /* page base (darkened from #07090c for crisper AA) */ + --bg-1: #0b0f14; /* panel */ + --bg-2: #10161d; /* raised panel / input */ + --bg-3: #161e27; /* chips, hover */ + --bg-term: #05070a; /* command-output panels */ /* Hairlines & text */ - --line: #1d2630; + --line: #1c2530; --line-strong: #2a3744; - --ink: #e6edf3; /* primary text */ - --ink-dim: #9bb0c0; /* secondary text */ - --ink-faint: #5d7185; /* labels, meta */ + --line-bright: #3a4a5a; + --ink: #e9eff5; /* primary text */ + --ink-dim: #9bb0c0; /* secondary text — 8.0:1 on bg-2 */ + /* labels/meta — was #5d7185 (3.6:1, fails AA). Lifted to 6.1:1 on bg-2. */ + --ink-faint: #8499ab; - /* Accents */ - --cyan: #3dd1d6; /* "system alive" — links, focus, session dot */ + /* Accents — the "alive" cyan is the spine of the calm palette. */ + --cyan: #3dd1d6; /* "system alive" — links, focus, session pulse */ + --cyan-bright: #62e3e7; --cyan-dim: #1f6f72; + --cyan-deep: #0e3133; --amber: #f5b657; /* working / in-flight */ + --amber-dim: #6a5226; --green: #5ddb8e; /* healthy exit */ --green-dim: #1f5f3d; - /* Danger — reserved EXCLUSIVELY for mutating actions. Nothing else is red. */ + /* Danger — reserved EXCLUSIVELY for mutating power actions. Nothing else red. */ --danger: #ff4d4d; --danger-bright: #ff6363; --danger-deep: #7a1717; --danger-glow: rgba(255, 77, 77, 0.35); - --radius: 10px; - --radius-sm: 7px; + --radius: 11px; + --radius-sm: 8px; + --radius-lg: 16px; - --mono: ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", - "Fira Code", Menlo, Consolas, "Liberation Mono", monospace; + /* A refined, deliberately-ordered monospace stack. We lead with faces that + have real character (Berkeley Mono / JetBrains / Cascadia / SF Mono) and + fall back gracefully — but ship nothing; whatever the device has carries + the cockpit-readout identity. */ + --mono: "Berkeley Mono", ui-monospace, "JetBrains Mono", "SF Mono", + "Cascadia Code", "Fira Code", "Source Code Pro", Menlo, Consolas, + "Liberation Mono", monospace; --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - --shadow-panel: 0 1px 0 rgba(255, 255, 255, 0.02) inset, - 0 16px 40px -24px rgba(0, 0, 0, 0.9); + --shadow-panel: 0 1px 0 rgba(255, 255, 255, 0.025) inset, + 0 18px 44px -26px rgba(0, 0, 0, 0.95); + --shadow-sheet: 0 -22px 48px -12px rgba(0, 0, 0, 0.7); + + /* Safe-area shorthands (notch / home-indicator). 0px fallback off-device. */ + --safe-top: env(safe-area-inset-top, 0px); + --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-left: env(safe-area-inset-left, 0px); + --safe-right: env(safe-area-inset-right, 0px); color-scheme: dark; } @@ -55,23 +77,24 @@ html, body { margin: 0; height: 100%; - /* The page itself never scrolls — the chat stream scrolls internally. This - keeps the composer pinned and stops iOS rubber-banding the whole UI. */ + /* The page itself never scrolls — only the chat stream scrolls internally. + This keeps the composer pinned and stops iOS rubber-banding the whole UI. */ overflow: hidden; overscroll-behavior: none; } body { background-color: var(--bg-0); - /* Atmosphere: a soft cyan corner-glow over a faint scanline weave, so the - surface reads like backlit equipment rather than flat #000. */ + /* Atmosphere: a soft cyan corner-glow + a faint warm counter-glow over a + hairline scanline weave, so the surface reads as backlit equipment rather + than flat black. Fixed so it doesn't drift when the chat scrolls. */ background-image: - radial-gradient(120% 80% at 85% -10%, rgba(61, 209, 214, 0.07), transparent 55%), - radial-gradient(90% 70% at 10% 110%, rgba(245, 182, 87, 0.04), transparent 50%), + radial-gradient(120% 78% at 86% -12%, rgba(61, 209, 214, 0.08), transparent 55%), + radial-gradient(90% 70% at 8% 112%, rgba(245, 182, 87, 0.045), transparent 52%), repeating-linear-gradient( 0deg, - rgba(255, 255, 255, 0.012) 0px, - rgba(255, 255, 255, 0.012) 1px, + rgba(255, 255, 255, 0.013) 0px, + rgba(255, 255, 255, 0.013) 1px, transparent 1px, transparent 3px ); @@ -84,8 +107,8 @@ body { #app { /* 100dvh (dynamic viewport height) — NOT 100vh/100% — so the composer at the - bottom is never hidden behind a mobile browser's address/tool bar. Mobile is - the primary client for this tool. 100vh is the fallback for old engines. */ + bottom is never hidden behind a mobile browser's address/tool bar. 100vh is + the fallback for engines without dvh. Mobile is the primary client. */ height: 100vh; height: 100dvh; } @@ -94,7 +117,6 @@ button { font-family: var(--mono); cursor: pointer; } - button:disabled { cursor: not-allowed; } @@ -119,10 +141,26 @@ button:disabled { background-clip: content-box; } *::-webkit-scrollbar-thumb:hover { - background: #3a4a5a; + background: var(--line-bright); background-clip: content-box; } +/* ── Shared motion primitives ────────────────────────────────────────────── + One well-orchestrated entrance beats scattered micro-interactions: panels + and rows rise a few px with a soft fade, staggered via --d on each element. */ +@keyframes rise-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +.rise-in { + animation: rise-in 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) both; + animation-delay: var(--d, 0ms); +} + @media (prefers-reduced-motion: reduce) { *, *::before, diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 6d42dae..fdf6a6c 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,8 +1,41 @@ -// Same-origin API client. Auth is handled entirely by the edge proxy -// (Authentik / basic-auth / bearer) — this UI never sends or stores a token. -import { readEventStream } from './sse.js'; +// Same-origin API client for the breakglass UI. +// +// Auth is handled entirely by the edge proxy (Authentik / basic-auth / bearer): +// this UI never sends or stores a token, and builds no login screen. +// +// The chat uses the tmux/attach model. The conversation lives SERVER-SIDE; we +// only persist the session_id locally and ATTACH to it over an EventSource. The +// browser's native EventSource auto-reconnects and sends Last-Event-ID, and the +// server resumes from there — so there is ZERO reconnect logic here. We just +// render events idempotently by id (see transcript.js). -/** Open a fresh chat session. @returns {Promise} session_id */ +const SESSION_KEY = 'breakglass.session_id'; + +/** Read the persisted session id, or '' if none. */ +export function loadSessionId() { + try { + return localStorage.getItem(SESSION_KEY) || ''; + } catch { + return ''; + } +} + +/** Persist the session id (best-effort; private-mode storage may throw). */ +export function saveSessionId(id) { + try { + if (id) localStorage.setItem(SESSION_KEY, id); + else localStorage.removeItem(SESSION_KEY); + } catch { + /* ignore — storage is a convenience, not a requirement */ + } +} + +/** Forget the persisted session id (the "New session" archive step). */ +export function clearSessionId() { + saveSessionId(''); +} + +/** Open a fresh server-side session. @returns {Promise} session_id */ export async function openSession() { const res = await fetch('/api/session', { method: 'POST', @@ -19,30 +52,89 @@ export async function openSession() { } /** - * Run one chat turn. Streams events to onEvent until the backend sends - * {kind:"done"} and the connection closes. Pass an AbortSignal to cancel. + * Attach to a session's event stream. Returns the live EventSource so the + * caller can close() it. Events arrive as: + * - default `message` events: .data is JSON {kind, id, ...} + * - a named `caught-up` event once the replay is drained (.data is {}) + * - native `error` events while reconnecting (EventSource retries itself) * - * @param {{session_id: string, prompt: string, model?: string, signal?: AbortSignal}} opts - * @param {(event: object) => void} onEvent + * @param {string} sessionId + * @param {{ + * onEvent: (e: object) => void, + * onCaughtUp?: () => void, + * onOpen?: () => void, + * onError?: (e: Event) => void, + * }} handlers + * @returns {EventSource} */ -export async function streamChat({ session_id, prompt, model, signal }, onEvent) { - const payload = { session_id, prompt }; - if (model) payload.model = model; +export function attachStream(sessionId, { onEvent, onCaughtUp, onOpen, onError }) { + const es = new EventSource(`/api/session/${encodeURIComponent(sessionId)}/stream`); - const res = await fetch('/api/chat', { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: 'text/event-stream', - }, - body: JSON.stringify(payload), - signal, - }); - await readEventStream(res, onEvent); + es.onopen = () => onOpen?.(); + + es.onmessage = (e) => { + if (!e || typeof e.data !== 'string' || e.data === '') return; + let obj; + try { + obj = JSON.parse(e.data); + } catch { + // A malformed frame must not abort an in-progress recovery stream. + return; + } + // EventSource exposes the SSE `id:` line as e.lastEventId. The server also + // embeds id in the JSON; prefer the JSON id, fall back to lastEventId. + if ((obj.id == null || obj.id === '') && e.lastEventId) obj.id = e.lastEventId; + onEvent(obj); + }; + + es.addEventListener('caught-up', () => onCaughtUp?.()); + + es.onerror = (e) => { + // EventSource auto-reconnects on a transient drop (readyState CONNECTING); + // we only surface a hard, terminal failure (readyState CLOSED). + onError?.(e); + }; + + return es; } /** - * List the PVE power verbs and which of them mutate VM state. + * Start a turn. Output arrives via the attach stream, NOT this response. + * @param {{session_id: string, prompt: string, model?: string}} opts + * @returns {Promise<{status:'started'|'busy'|'gone'}>} + * started — accepted; busy — 409 (a turn already runs); gone — 404 (re-create). + */ +export async function sendPrompt({ session_id, prompt, model }) { + const payload = { prompt }; + if (model) payload.model = model; + const res = await fetch(`/api/session/${encodeURIComponent(session_id)}/prompt`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(payload), + }); + if (res.status === 409) return { status: 'busy' }; + if (res.status === 404) return { status: 'gone' }; + if (!res.ok) throw new Error(`could not start the turn (HTTP ${res.status})`); + return { status: 'started' }; +} + +/** + * Cancel the in-flight turn (the Stop button). + * @param {string} sessionId + * @returns {Promise} whether a turn was cancelled + */ +export async function cancelTurn(sessionId) { + const res = await fetch(`/api/session/${encodeURIComponent(sessionId)}/cancel`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + }); + if (!res.ok) throw new Error(`could not stop the turn (HTTP ${res.status})`); + const body = await res.json().catch(() => ({})); + return Boolean(body.cancelled); +} + +/** + * List the PVE power verbs and which mutate VM state. * @returns {Promise<{verbs: string[], mutating: string[]}>} */ export async function fetchVerbs() { @@ -58,27 +150,26 @@ export async function fetchVerbs() { } /** - * Run a PVE power verb directly (no AI in the path). The backend returns 200 - * on success and 502 when the verb's exit code is non-zero, but the JSON body - * carries {verb, exit_code, stdout, stderr, rejected} in BOTH cases — so we - * read the body regardless of HTTP status and let the caller style on - * exit_code / rejected. + * Run a PVE power verb directly (no AI in the path). The backend returns 200 on + * success and 502 when the verb's exit code is non-zero, but the JSON body + * carries {verb, exit_code, stdout, stderr, rejected} in BOTH cases — so we read + * the body regardless of HTTP status and let the caller style on exit_code. * * @param {string} verb - * @returns {Promise<{verb: string, exit_code: number|null, stdout: string, stderr: string, rejected: boolean}>} + * @returns {Promise<{verb:string, exit_code:number|null, stdout:string, stderr:string, rejected:boolean}>} */ export async function runVerb(verb) { const res = await fetch(`/api/pve/${encodeURIComponent(verb)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, }); - // 400 = unknown verb (FastAPI HTTPException) — has {detail}, not the verb shape. let body; try { body = await res.json(); } catch { throw new Error(`VM control '${verb}' failed (HTTP ${res.status}, no body)`); } + // 400 = unknown verb (FastAPI HTTPException) — has {detail}, not the verb shape. if (res.status === 400) { throw new Error(body?.detail || `'${verb}' was rejected by the server`); } diff --git a/frontend/src/lib/sse.js b/frontend/src/lib/sse.js deleted file mode 100644 index 8375612..0000000 --- a/frontend/src/lib/sse.js +++ /dev/null @@ -1,150 +0,0 @@ -// SSE frame parsing — the load-bearing core of the breakglass UI. -// -// The /api/chat endpoint returns a text/event-stream that we read with -// fetch() + response.body.getReader() (NOT EventSource, which cannot POST). -// The backend emits one frame per event as: -// -// data: {json}\n\n -// -// getReader() hands us bytes at arbitrary boundaries: a single frame can be -// split across reads, and one read can contain several frames. So we keep a -// rolling text buffer, split it on the blank-line frame delimiter, and only -// hand back the JSON payload of *complete* frames. Per the SSE spec a frame may -// carry multiple `data:` lines (joined with "\n"); the backend emits single -// line JSON today, but we handle the general case so a future multi-line -// payload can't silently corrupt the stream. - -/** - * Parse a single SSE event block (the text between blank lines) into its data - * payload string, or null if the block carries no `data:` field (e.g. a bare - * comment or a `:` heartbeat). - * @param {string} block - * @returns {string|null} - */ -export function dataFromEventBlock(block) { - const dataLines = []; - for (const rawLine of block.split('\n')) { - const line = rawLine.replace(/\r$/, ''); - if (line.startsWith(':')) continue; // SSE comment / heartbeat - if (line === 'data:' || line === 'data') { - dataLines.push(''); - } else if (line.startsWith('data:')) { - // Spec: a single leading space after the colon is stripped. - let v = line.slice('data:'.length); - if (v.startsWith(' ')) v = v.slice(1); - dataLines.push(v); - } - // field lines we don't care about (event:, id:, retry:) are ignored - } - if (dataLines.length === 0) return null; - return dataLines.join('\n'); -} - -/** - * A stateful splitter that turns an arbitrary sequence of decoded text chunks - * into a sequence of complete SSE event-block strings. Frames are delimited by - * a blank line; we tolerate both "\n\n" and "\r\n\r\n". - */ -export class SSEFrameSplitter { - constructor() { - this.buffer = ''; - } - - /** - * Feed a decoded text chunk; returns the event blocks that are now complete. - * Any trailing partial frame stays buffered for the next chunk. - * @param {string} chunk - * @returns {string[]} complete event blocks (text between delimiters) - */ - push(chunk) { - this.buffer += chunk; - const blocks = []; - // Normalise CRLF delimiters to LF so a single split rule covers both. - let idx; - // Process every complete frame currently in the buffer. - while ((idx = this._nextDelimiter()) !== -1) { - const block = this.buffer.slice(0, idx.start); - this.buffer = this.buffer.slice(idx.end); - if (block.length > 0) blocks.push(block); - } - return blocks; - } - - /** - * On stream end, return whatever complete-looking content remains. A - * well-behaved backend always terminates the last frame with a blank line, - * so this is usually empty — but if the connection closed mid-trailing-frame - * with a parseable block, surface it rather than dropping data. - * @returns {string[]} - */ - flush() { - const rest = this.buffer.trim(); - this.buffer = ''; - return rest ? [rest] : []; - } - - _nextDelimiter() { - // Find the earliest of "\n\n", "\r\n\r\n", "\r\r". - const candidates = [ - { token: '\r\n\r\n', i: this.buffer.indexOf('\r\n\r\n') }, - { token: '\n\n', i: this.buffer.indexOf('\n\n') }, - { token: '\r\r', i: this.buffer.indexOf('\r\r') }, - ].filter((c) => c.i !== -1); - if (candidates.length === 0) return -1; - candidates.sort((a, b) => a.i - b.i); - const { token, i } = candidates[0]; - return { start: i, end: i + token.length }; - } -} - -/** - * Read an SSE Response body to completion, invoking onEvent for every parsed - * JSON event object. Resolves when the stream ends. Throws if the response is - * not ok or has no readable body (caller shows the error inline). - * - * @param {Response} response a fetch() Response with a streaming body - * @param {(event: object) => void} onEvent called per parsed JSON event - */ -export async function readEventStream(response, onEvent) { - if (!response.ok) { - throw new Error(`server returned ${response.status} ${response.statusText}`); - } - if (!response.body) { - throw new Error('response has no readable body (streaming unsupported)'); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - const splitter = new SSEFrameSplitter(); - - const handleBlock = (block) => { - const payload = dataFromEventBlock(block); - if (payload == null || payload.trim() === '') return; - let obj; - try { - obj = JSON.parse(payload); - } catch { - // A malformed frame must not abort an in-progress recovery stream; - // skip it and keep reading. - return; - } - onEvent(obj); - }; - - try { - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - const text = decoder.decode(value, { stream: true }); - for (const block of splitter.push(text)) handleBlock(block); - } - } finally { - reader.releaseLock?.(); - } - // Drain any trailing bytes the decoder held, then any final frame. - const tail = decoder.decode(); - if (tail) { - for (const block of splitter.push(tail)) handleBlock(block); - } - for (const block of splitter.flush()) handleBlock(block); -} diff --git a/frontend/src/lib/sse.test.mjs b/frontend/src/lib/sse.test.mjs deleted file mode 100644 index 413433f..0000000 --- a/frontend/src/lib/sse.test.mjs +++ /dev/null @@ -1,152 +0,0 @@ -// Standalone test of the SSE frame parser — no test framework, just node. -// Run: node src/lib/sse.test.mjs (exits non-zero on any failure) -// -// These pin the protocol described in the API contract: frames are -// `data: {json}\n\n`, the event `kind` is one of session/text/tool/result/ -// error/done, and bytes arrive at arbitrary boundaries via getReader(). -import { SSEFrameSplitter, dataFromEventBlock, readEventStream } from './sse.js'; - -let failures = 0; -function ok(name, cond) { - if (cond) { - console.log(` ok ${name}`); - } else { - failures++; - console.error(`FAIL ${name}`); - } -} -function eq(name, got, want) { - const g = JSON.stringify(got); - const w = JSON.stringify(want); - ok(`${name} (got ${g})`, g === w); -} - -// --- dataFromEventBlock --------------------------------------------------- -eq( - 'extracts JSON payload from a data: line', - dataFromEventBlock('data: {"kind":"text","text":"hi"}'), - '{"kind":"text","text":"hi"}' -); -eq( - 'strips exactly one space after the colon', - dataFromEventBlock('data: leading-space-kept'), - ' leading-space-kept' -); -eq('ignores comment/heartbeat lines', dataFromEventBlock(': keep-alive'), null); -eq( - 'joins multi-line data fields with newline', - dataFromEventBlock('data: line1\ndata: line2'), - 'line1\nline2' -); - -// --- SSEFrameSplitter: whole frames -------------------------------------- -{ - const s = new SSEFrameSplitter(); - const blocks = s.push('data: {"kind":"session","session_id":"abc"}\n\n'); - eq('one complete frame yields one block', blocks, [ - 'data: {"kind":"session","session_id":"abc"}', - ]); -} - -// --- SSEFrameSplitter: multiple frames in one chunk ---------------------- -{ - const s = new SSEFrameSplitter(); - const blocks = s.push( - 'data: {"kind":"text","text":"a"}\n\ndata: {"kind":"text","text":"b"}\n\n' - ); - eq('two frames in one chunk yield two blocks', blocks.length, 2); - eq('first block', dataFromEventBlock(blocks[0]), '{"kind":"text","text":"a"}'); - eq('second block', dataFromEventBlock(blocks[1]), '{"kind":"text","text":"b"}'); -} - -// --- SSEFrameSplitter: frame split across chunks ------------------------- -{ - const s = new SSEFrameSplitter(); - let blocks = s.push('data: {"kind":"te'); - eq('partial frame yields nothing yet', blocks, []); - blocks = s.push('xt","text":"split"}\n\n'); - eq('completing the frame yields it whole', dataFromEventBlock(blocks[0]), '{"kind":"text","text":"split"}'); -} - -// --- SSEFrameSplitter: delimiter split across chunks --------------------- -{ - const s = new SSEFrameSplitter(); - let blocks = s.push('data: {"kind":"done"}\n'); - eq('frame held while delimiter incomplete', blocks, []); - blocks = s.push('\n'); - eq('frame released once blank line completes', dataFromEventBlock(blocks[0]), '{"kind":"done"}'); -} - -// --- SSEFrameSplitter: CRLF delimiters ----------------------------------- -{ - const s = new SSEFrameSplitter(); - const blocks = s.push('data: {"kind":"text","text":"crlf"}\r\n\r\n'); - eq('CRLF-delimited frame parses', dataFromEventBlock(blocks[0]), '{"kind":"text","text":"crlf"}'); -} - -// --- end-to-end via readEventStream over a mock streaming Response -------- -function mockResponse(chunks) { - const enc = new TextEncoder(); - let i = 0; - return { - ok: true, - status: 200, - body: { - getReader() { - return { - read() { - if (i < chunks.length) { - return Promise.resolve({ value: enc.encode(chunks[i++]), done: false }); - } - return Promise.resolve({ value: undefined, done: true }); - }, - releaseLock() {}, - }; - }, - }, - }; -} - -await (async () => { - // A realistic turn, deliberately chopped at ugly boundaries: - // - the session frame split mid-JSON - // - two text frames glued together - // - a tool frame - // - a result frame and the terminal done frame in one chunk - const chunks = [ - 'data: {"kind":"sess', - 'ion","session_id":"S1"}\n\n', - 'data: {"kind":"text","text":"checking "}\n\ndata: {"kind":"text","text":"disk"}\n\n', - 'data: {"kind":"tool","name":"Bash","input":{"command":"df -h"}}\n\n', - 'data: {"kind":"result","is_error":false,"result":"ok","duration_ms":12}\n\ndata: {"kind":"done"}\n\n', - ]; - const events = []; - await readEventStream(mockResponse(chunks), (e) => events.push(e)); - - eq('event count', events.length, 6); - eq('1: session id', events[0], { kind: 'session', session_id: 'S1' }); - eq('2: first text', events[1], { kind: 'text', text: 'checking ' }); - eq('3: second text', events[2], { kind: 'text', text: 'disk' }); - eq('4: tool kind+name', { kind: events[3].kind, name: events[3].name }, { kind: 'tool', name: 'Bash' }); - eq('4: tool command', events[3].input.command, 'df -h'); - eq('5: result', events[4], { kind: 'result', is_error: false, result: 'ok', duration_ms: 12 }); - eq('6: done terminal', events[5], { kind: 'done' }); -})(); - -// malformed frame in the middle must be skipped, not abort the stream -await (async () => { - const chunks = [ - 'data: {"kind":"text","text":"before"}\n\n', - 'data: {this is not json}\n\n', - 'data: {"kind":"done"}\n\n', - ]; - const events = []; - await readEventStream(mockResponse(chunks), (e) => events.push(e)); - eq('malformed frame skipped, stream continues', events.map((e) => e.kind), ['text', 'done']); -})(); - -if (failures) { - console.error(`\n${failures} assertion(s) FAILED`); - process.exit(1); -} -console.log('\nall SSE parser assertions passed'); diff --git a/frontend/src/lib/transcript.js b/frontend/src/lib/transcript.js new file mode 100644 index 0000000..39624c7 --- /dev/null +++ b/frontend/src/lib/transcript.js @@ -0,0 +1,196 @@ +// transcript.js — the load-bearing core of the breakglass UI. +// +// The attach stream (EventSource) replays the conversation-so-far and then +// tails live. Replayed events are byte-identical to live ones, and on a +// reconnect the server re-replays from Last-Event-ID — so the SAME event id can +// arrive more than once. This module folds a flat, possibly-duplicated event +// sequence into an ordered list of render-ready messages, idempotently. +// +// Contract (every default `message` event's .data is one of these JSON shapes): +// {kind:"user", text, id} → opens a USER bubble +// {kind:"session", session_id, id} → informational (agent's session id) +// {kind:"text", text, id} → assistant prose; concatenated +// {kind:"tool", name, input, id} → inline tool chip (Bash → command) +// {kind:"result", is_error, result, duration_ms, id} → closes the bubble +// {kind:"error", error, id} → error note on the bubble +// {kind:"cancelled", id} → muted "stopped" note +// {kind:"turn_end", id} → the turn finished +// +// Grouping: a `user` event opens a user message; the session/text/tool events +// that follow build ONE assistant message; result/error/cancelled annotate it; +// turn_end ends it. Assistant events with no preceding user (e.g. a session +// banner on a fresh attach) still get an assistant message so nothing is lost. +// +// Idempotency: every event carries a monotonic integer-ish id. We track the +// max id folded so far and DROP any event whose id we've already passed — a +// reconnect replay therefore never double-renders. Ids are compared +// numerically when both parse as numbers, else as strings (defensive). + +/** @typedef {{type:'text',text:string}|{type:'tool',name:string,command:string,raw:any}} Part */ +/** + * @typedef {Object} Message + * @property {'user'|'assistant'} role + * @property {string} key stable key for keyed {#each} + * @property {string} [text] user text + * @property {Part[]} [parts] assistant parts, in emit order + * @property {{is_error:boolean,text:string,duration_ms:number|null}} [result] + * @property {string} [error] + * @property {boolean} [cancelled] + * @property {boolean} [ended] turn_end seen for this message + */ + +/** Compare two ids; numeric when both look numeric, else lexicographic. */ +export function idGreater(a, b) { + const na = Number(a); + const nb = Number(b); + if (Number.isFinite(na) && Number.isFinite(nb) && `${a}`.trim() !== '' && `${b}`.trim() !== '') { + return na > nb; + } + return String(a) > String(b); +} + +/** + * Create an empty transcript-folding state. + * @returns {{messages: Message[], maxId: any, sawId: boolean, openAssistant: Message|null, activeUserSeen: boolean}} + */ +export function createTranscript() { + return { + messages: [], + maxId: null, + sawId: false, + openAssistant: null, + // a turn is "active" once a user event (or local prompt) has no following + // turn_end; the UI reads `active` from reduceEvent's return. + activeUserSeen: false, + }; +} + +function bubbleKey(prefix, id, fallbackIndex) { + if (id != null && `${id}`.trim() !== '') return `${prefix}:${id}`; + return `${prefix}:idx:${fallbackIndex}`; +} + +/** + * Should this event be applied, given the max id folded so far? Updates and + * returns the new max. Events WITHOUT an id are always applied (and don't move + * the watermark) — the protocol always carries ids, but we never drop data on a + * malformed frame. + * @returns {{apply:boolean, maxId:any}} + */ +export function admit(maxId, id) { + if (id == null || `${id}`.trim() === '') return { apply: true, maxId }; + if (maxId == null) return { apply: true, maxId: id }; + if (idGreater(id, maxId)) return { apply: true, maxId: id }; + return { apply: false, maxId }; // already seen — dedupe +} + +/** + * Fold one event into the transcript state, mutating `state` in place. + * Returns true if the state changed (so callers can trigger a re-render). + * + * @param {ReturnType} state + * @param {any} ev parsed event object ({kind, id, ...}) + * @returns {boolean} changed + */ +export function reduceEvent(state, ev) { + if (!ev || typeof ev !== 'object') return false; + const { apply, maxId } = admit(state.maxId, ev.id); + state.maxId = maxId; + if (!apply) return false; + if (ev.id != null && `${ev.id}`.trim() !== '') state.sawId = true; + + const ensureAssistant = () => { + if (!state.openAssistant) { + const msg = { + role: 'assistant', + key: bubbleKey('a', ev.id, state.messages.length), + parts: [], + ended: false, + }; + state.messages.push(msg); + state.openAssistant = msg; + } + return state.openAssistant; + }; + + switch (ev.kind) { + case 'user': { + // A new user turn. Close any dangling assistant bubble first. + state.openAssistant = null; + state.messages.push({ + role: 'user', + key: bubbleKey('u', ev.id, state.messages.length), + text: typeof ev.text === 'string' ? ev.text : '', + }); + state.activeUserSeen = true; + return true; + } + case 'session': { + // Informational — does not itself render a part, but it does open the + // assistant bubble for the turn so subsequent text lands in one place. + ensureAssistant(); + return true; + } + case 'text': { + if (typeof ev.text !== 'string' || ev.text === '') return false; + const msg = ensureAssistant(); + const tail = msg.parts[msg.parts.length - 1]; + if (tail && tail.type === 'text') { + tail.text += ev.text; // concatenate consecutive prose + } else { + msg.parts.push({ type: 'text', text: ev.text }); + } + return true; + } + case 'tool': { + const msg = ensureAssistant(); + const command = + ev.input && typeof ev.input.command === 'string' ? ev.input.command : ''; + msg.parts.push({ + type: 'tool', + name: typeof ev.name === 'string' && ev.name ? ev.name : 'tool', + command, + raw: ev.input ?? null, + }); + return true; + } + case 'result': { + const msg = ensureAssistant(); + msg.result = { + is_error: Boolean(ev.is_error), + text: typeof ev.result === 'string' ? ev.result : '', + duration_ms: typeof ev.duration_ms === 'number' ? ev.duration_ms : null, + }; + return true; + } + case 'error': { + const msg = ensureAssistant(); + msg.error = typeof ev.error === 'string' && ev.error ? ev.error : 'unknown error'; + return true; + } + case 'cancelled': { + const msg = ensureAssistant(); + msg.cancelled = true; + return true; + } + case 'turn_end': { + if (state.openAssistant) state.openAssistant.ended = true; + state.openAssistant = null; + state.activeUserSeen = false; + return true; + } + default: + return false; + } +} + +/** + * Convenience: fold an array of events into a fresh transcript (used by tests + * and by a from-scratch render). Returns the final state. + * @param {any[]} events + */ +export function foldAll(events) { + const state = createTranscript(); + for (const ev of events) reduceEvent(state, ev); + return state; +} diff --git a/frontend/src/lib/transcript.test.mjs b/frontend/src/lib/transcript.test.mjs new file mode 100644 index 0000000..93afeb4 --- /dev/null +++ b/frontend/src/lib/transcript.test.mjs @@ -0,0 +1,162 @@ +// Standalone test of the transcript folder — no test framework, just node. +// Run: node src/lib/transcript.test.mjs (exits non-zero on any failure) +// +// These pin the attach-model contract: events carry monotonic ids, a reconnect +// re-replays already-seen ids (which MUST be deduped), and events group into +// user/assistant messages with consecutive prose concatenated. +import { + admit, + idGreater, + reduceEvent, + createTranscript, + foldAll, +} from './transcript.js'; + +let failures = 0; +function ok(name, cond) { + if (cond) { + console.log(` ok ${name}`); + } else { + failures++; + console.error(`FAIL ${name}`); + } +} +function eq(name, got, want) { + const g = JSON.stringify(got); + const w = JSON.stringify(want); + ok(`${name} (got ${g})`, g === w); +} + +// --- id comparison -------------------------------------------------------- +ok('idGreater numeric', idGreater(10, 9) === true); +ok('idGreater numeric not', idGreater(2, 10) === false); // not string "2" > "10" +ok('idGreater string fallback', idGreater('b', 'a') === true); + +// --- admit / dedupe watermark -------------------------------------------- +{ + let { apply, maxId } = admit(null, 1); + eq('first id admitted', { apply, maxId }, { apply: true, maxId: 1 }); + ({ apply, maxId } = admit(5, 5)); + ok('equal id rejected (already seen)', apply === false && maxId === 5); + ({ apply, maxId } = admit(5, 3)); + ok('lower id rejected', apply === false && maxId === 5); + ({ apply, maxId } = admit(5, 6)); + ok('higher id admitted, watermark moves', apply === true && maxId === 6); + ({ apply, maxId } = admit(5, undefined)); + ok('id-less event always admitted, watermark held', apply === true && maxId === 5); +} + +// --- a full turn groups into user + one assistant bubble ------------------ +{ + const events = [ + { kind: 'user', text: 'triage it', id: 1 }, + { kind: 'session', session_id: 'S1', id: 2 }, + { kind: 'text', text: 'Checking ', id: 3 }, + { kind: 'text', text: 'disk usage.', id: 4 }, + { kind: 'tool', name: 'Bash', input: { command: 'df -h' }, id: 5 }, + { kind: 'result', is_error: false, result: 'ok', duration_ms: 1200, id: 6 }, + { kind: 'turn_end', id: 7 }, + ]; + const s = foldAll(events); + eq('two messages: user + assistant', s.messages.length, 2); + eq('first is user with text', { r: s.messages[0].role, t: s.messages[0].text }, { r: 'user', t: 'triage it' }); + const a = s.messages[1]; + eq('assistant role', a.role, 'assistant'); + // consecutive text concatenated into ONE part; tool is a separate part + eq('parts: one concatenated text + one tool', a.parts.map((p) => p.type), ['text', 'tool']); + eq('prose concatenated in order', a.parts[0].text, 'Checking disk usage.'); + eq('tool command captured', a.parts[1].command, 'df -h'); + eq('result attached', { e: a.result.is_error, ms: a.result.duration_ms }, { e: false, ms: 1200 }); + ok('turn ended', a.ended === true); + ok('no longer active after turn_end', s.activeUserSeen === false); +} + +// --- reconnect replay: re-feeding the SAME events must NOT double-render -- +{ + const events = [ + { kind: 'user', text: 'hi', id: 1 }, + { kind: 'text', text: 'hello', id: 2 }, + { kind: 'turn_end', id: 3 }, + ]; + const s = createTranscript(); + for (const e of events) reduceEvent(s, e); + // simulate an EventSource reconnect that re-replays everything from the top + for (const e of events) reduceEvent(s, e); + eq('still exactly two messages after replay', s.messages.length, 2); + eq('assistant prose not doubled', s.messages[1].parts[0].text, 'hello'); +} + +// --- a partial replay (Last-Event-ID resume) continues the same bubble ---- +{ + const s = createTranscript(); + reduceEvent(s, { kind: 'user', text: 'go', id: 1 }); + reduceEvent(s, { kind: 'text', text: 'part-A ', id: 2 }); + // reconnect: server resumes after id 2; we must drop id<=2 if re-sent and + // keep appending to the open assistant bubble. + reduceEvent(s, { kind: 'text', text: 'part-A ', id: 2 }); // dup, dropped + reduceEvent(s, { kind: 'text', text: 'part-B', id: 3 }); // new, appended + reduceEvent(s, { kind: 'turn_end', id: 4 }); + eq('resume appended to same bubble', s.messages[1].parts[0].text, 'part-A part-B'); + eq('still two messages', s.messages.length, 2); +} + +// --- error / cancelled annotate the open bubble --------------------------- +{ + const s = foldAll([ + { kind: 'user', text: 'x', id: 1 }, + { kind: 'text', text: 'working', id: 2 }, + { kind: 'error', error: 'ssh timeout', id: 3 }, + { kind: 'turn_end', id: 4 }, + ]); + eq('error note on assistant bubble', s.messages[1].error, 'ssh timeout'); +} +{ + const s = foldAll([ + { kind: 'user', text: 'x', id: 1 }, + { kind: 'cancelled', id: 2 }, + { kind: 'turn_end', id: 3 }, + ]); + ok('cancelled flag on assistant bubble', s.messages[1].cancelled === true); +} + +// --- active state: a user event with no turn_end means a turn is running --- +{ + const s = createTranscript(); + reduceEvent(s, { kind: 'user', text: 'go', id: 1 }); + reduceEvent(s, { kind: 'text', text: '...', id: 2 }); + ok('active while no turn_end', s.activeUserSeen === true); + reduceEvent(s, { kind: 'turn_end', id: 3 }); + ok('inactive after turn_end', s.activeUserSeen === false); +} + +// --- assistant-only stream (session banner on a fresh attach) still renders - +{ + const s = foldAll([ + { kind: 'session', session_id: 'S1', id: 1 }, + { kind: 'text', text: 'standing by', id: 2 }, + { kind: 'turn_end', id: 3 }, + ]); + eq('lone assistant message created', s.messages.length, 1); + eq('assistant prose present', s.messages[0].parts[0].text, 'standing by'); +} + +// --- two sequential turns produce two assistant bubbles ------------------- +{ + const s = foldAll([ + { kind: 'user', text: 'q1', id: 1 }, + { kind: 'text', text: 'a1', id: 2 }, + { kind: 'turn_end', id: 3 }, + { kind: 'user', text: 'q2', id: 4 }, + { kind: 'text', text: 'a2', id: 5 }, + { kind: 'turn_end', id: 6 }, + ]); + eq('four messages (u,a,u,a)', s.messages.map((m) => m.role), ['user', 'assistant', 'user', 'assistant']); + eq('second answer in its own bubble', s.messages[3].parts[0].text, 'a2'); + ok('message keys are unique', new Set(s.messages.map((m) => m.key)).size === 4); +} + +if (failures) { + console.error(`\n${failures} assertion(s) FAILED`); + process.exit(1); +} +console.log('\nall transcript assertions passed'); diff --git a/tests/test_breakglass.py b/tests/test_breakglass.py index 6f21c12..caa6b65 100644 --- a/tests/test_breakglass.py +++ b/tests/test_breakglass.py @@ -1,174 +1,251 @@ -"""Tests for the breakglass app: verb whitelist, SSE translation, auth, routes.""" +"""Tests for the breakglass app: session manager (attach model), verb whitelist, +SSE translation, auth, routes.""" import os os.environ.setdefault("API_BEARER_TOKEN", "test-token") +# Turns chdir into a per-session workspace; point it somewhere writable for tests +# (prod uses the /workspace emptyDir). Must be set before the app imports config. +os.environ.setdefault("BREAKGLASS_SESSIONS_DIR", "/tmp/bg-test-sessions") import pytest from fastapi.testclient import TestClient -from app.breakglass import agent_session, pve +from app.breakglass import agent_session, pve, session as sessionmod from app.breakglass.server import app # --------------------------------------------------------------------------- # -# PVE verb whitelist — the security boundary mirrored client-side. +# Fakes for the claude subprocess a turn spawns. # --------------------------------------------------------------------------- # +class _FakeStdout: + def __init__(self, lines): + self._lines = [(l + "\n").encode() for l in lines] + self._i = 0 + def __aiter__(self): + return self + + async def __anext__(self): + if self._i >= len(self._lines): + raise StopAsyncIteration + line = self._lines[self._i] + self._i += 1 + return line + + +class _FakeStderr: + async def read(self): + return b"" + + +class _FakeProc: + def __init__(self, lines, rc=0): + self.stdout = _FakeStdout(lines) + self.stderr = _FakeStderr() + self.returncode = None + self._rc = rc + + async def wait(self): + self.returncode = self._rc + return self._rc + + def kill(self): + self.returncode = -9 + + +def _patch_proc(monkeypatch, lines, rc=0): + async def _fake_spawn(*argv, **kwargs): + return _FakeProc(lines, rc) + monkeypatch.setattr(sessionmod.asyncio, "create_subprocess_exec", _fake_spawn) + + +_TURN_LINES = [ + '{"type":"system","subtype":"init","session_id":"s"}', + '{"type":"system","subtype":"thinking_tokens","estimated_tokens":5}', + '{"type":"assistant","message":{"content":[{"type":"text","text":"checking disk"}]}}', + '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Bash","input":{"command":"df -h"}}]}}', + '{"type":"result","is_error":false,"result":"done","duration_ms":12}', +] + + +# --------------------------------------------------------------------------- # +# Session: event log + broadcast + replay/Last-Event-ID. +# --------------------------------------------------------------------------- # +def test_add_event_assigns_sequential_ids(): + s = sessionmod.Session("s1") + a = s.add_event({"kind": "user", "text": "hi"}) + b = s.add_event({"kind": "text", "text": "yo"}) + assert a["id"] == 0 and b["id"] == 1 + assert [e["kind"] for e in s.events] == ["user", "text"] + + +def test_subscribe_receives_broadcast(): + s = sessionmod.Session("s1") + q = s.subscribe() + s.add_event({"kind": "text", "text": "live"}) + assert q.get_nowait()["text"] == "live" + s.unsubscribe(q) + s.add_event({"kind": "text", "text": "after"}) + assert q.empty() + + +@pytest.mark.asyncio +async def test_attach_replays_then_signals_caught_up(): + s = sessionmod.Session("s1") + s.add_event({"kind": "user", "text": "diagnose"}) + s.add_event({"kind": "text", "text": "looking"}) + frames = [] + async for frame in sessionmod.attach_stream(s, last_event_id=None): + frames.append(frame) + if "caught-up" in frame: + break + body = "".join(frames) + assert "diagnose" in body and "looking" in body + assert "id: 0" in body and "id: 1" in body + assert "event: caught-up" in frames[-1] + + +@pytest.mark.asyncio +async def test_attach_reconnect_replays_only_missed(): + s = sessionmod.Session("s1") + for i in range(3): + s.add_event({"kind": "text", "text": f"e{i}"}) # ids 0,1,2 + frames = [] + async for frame in sessionmod.attach_stream(s, last_event_id=0): # already saw id 0 + frames.append(frame) + if "caught-up" in frame: + break + body = "".join(frames) + assert "e0" not in body # not re-sent + assert "e1" in body and "e2" in body + + +# --------------------------------------------------------------------------- # +# Session: running a detached turn (mocked subprocess). +# --------------------------------------------------------------------------- # +@pytest.mark.asyncio +async def test_turn_streams_events_into_log(monkeypatch): + _patch_proc(monkeypatch, _TURN_LINES) + s = sessionmod.Session("s1") + assert s.start_turn("diagnose the devvm") is True + await s._turn # wait for the detached turn to finish + kinds = [e["kind"] for e in s.events] + assert kinds[0] == "user" + assert "session" in kinds and "text" in kinds and "tool" in kinds + assert "result" in kinds and kinds[-1] == "turn_end" + assert "thinking_tokens" not in kinds + + +@pytest.mark.asyncio +async def test_one_turn_at_a_time(monkeypatch): + _patch_proc(monkeypatch, _TURN_LINES) + s = sessionmod.Session("s1") + assert s.start_turn("first") is True + assert s.start_turn("second") is False # task not done yet + await s._turn + + +@pytest.mark.asyncio +async def test_resume_after_first_turn(monkeypatch): + captured = {"argvs": []} + + async def _fake_spawn(*argv, **kwargs): + captured["argvs"].append(argv) + return _FakeProc(_TURN_LINES) + + monkeypatch.setattr(sessionmod.asyncio, "create_subprocess_exec", _fake_spawn) + s = sessionmod.Session("s1") + s.start_turn("first"); await s._turn + s.start_turn("second"); await s._turn + assert "--session-id" in captured["argvs"][0] + assert "--resume" in captured["argvs"][1] + + +# --------------------------------------------------------------------------- # +# SessionManager. +# --------------------------------------------------------------------------- # +def test_manager_create_get(): + m = sessionmod.SessionManager() + s = m.create() + assert m.get(s.id) is s + assert m.get("nope") is None + assert m.get_or_create(s.id) is s + assert m.get_or_create(None).id != s.id + + +# --------------------------------------------------------------------------- # +# PVE verb whitelist (unchanged security boundary). +# --------------------------------------------------------------------------- # def test_allowed_verbs_match_host_script(): - assert pve.ALLOWED_VERBS == { - "status", "forensics", "reset", "stop", "start", "cycle" - } + assert pve.ALLOWED_VERBS == {"status", "forensics", "reset", "stop", "start", "cycle"} assert pve.MUTATING_VERBS == {"reset", "stop", "start", "cycle"} - assert pve.MUTATING_VERBS < pve.ALLOWED_VERBS -@pytest.mark.parametrize("bad", [ - "rm -rf /", "status; rm -rf /", "status 103", "shutdown", "", "STATUS", - "cycle 999", "$(reboot)", "../start", -]) +@pytest.mark.parametrize("bad", ["rm -rf /", "status; reboot", "status 103", "", "STATUS"]) @pytest.mark.asyncio async def test_run_verb_rejects_non_whitelisted_without_ssh(bad, monkeypatch): - """A bad verb must be rejected locally — never spawning a subprocess.""" - called = False - async def _boom(*a, **k): - nonlocal called - called = True raise AssertionError("ssh must not run for a rejected verb") - monkeypatch.setattr(pve.asyncio, "create_subprocess_exec", _boom) result = await pve.run_verb(bad) assert result["rejected"] is True - assert result["exit_code"] is None - assert called is False - - -@pytest.mark.asyncio -async def test_run_verb_allowed_invokes_ssh_with_bare_verb(monkeypatch): - captured = {} - - class _FakeProc: - returncode = 0 - - async def communicate(self): - return (b"status: running\n", b"") - - async def _fake_exec(*argv, **kwargs): - captured["argv"] = argv - return _FakeProc() - - monkeypatch.setattr(pve.asyncio, "create_subprocess_exec", _fake_exec) - result = await pve.run_verb("status") - assert result["rejected"] is False - assert result["exit_code"] == 0 - assert "running" in result["stdout"] - # The verb is the LAST argv element, passed as a single token (no shell). - assert captured["argv"][-1] == "status" - assert captured["argv"][0] == "ssh" # --------------------------------------------------------------------------- # -# stream-json -> UI event translation (pure function). +# translate_event (pure). # --------------------------------------------------------------------------- # - -def test_translate_init_to_session(): - ev = agent_session.translate_event( +def test_translate_init_and_noise_and_blocks(): + assert agent_session.translate_event( {"type": "system", "subtype": "init", "session_id": "abc"} + ) == {"kind": "session", "session_id": "abc"} + assert agent_session.translate_event({"type": "system", "subtype": "hook_started"}) is None + assert agent_session.translate_event( + {"type": "assistant", "message": {"content": [{"type": "text", "text": "hi"}]}} + ) == {"kind": "text", "text": "hi"} + tool = agent_session.translate_event( + {"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Bash", "input": {"command": "df -h"}}]}} ) - assert ev == {"kind": "session", "session_id": "abc"} - - -@pytest.mark.parametrize("noise", [ - {"type": "system", "subtype": "hook_started"}, - {"type": "system", "subtype": "thinking_tokens", "estimated_tokens": 5}, - {"type": "user", "message": {"content": []}}, - {"type": "unknown"}, -]) -def test_translate_drops_noise(noise): - assert agent_session.translate_event(noise) is None - - -def test_translate_assistant_text(): - ev = agent_session.translate_event({ - "type": "assistant", - "message": {"content": [{"type": "text", "text": "checking disk"}]}, - }) - assert ev == {"kind": "text", "text": "checking disk"} - - -def test_translate_assistant_tool_use(): - ev = agent_session.translate_event({ - "type": "assistant", - "message": {"content": [ - {"type": "tool_use", "name": "Bash", "input": {"command": "df -h"}} - ]}, - }) - assert ev["kind"] == "tool" - assert ev["name"] == "Bash" - assert ev["input"]["command"] == "df -h" - - -def test_translate_result(): - ev = agent_session.translate_event({ - "type": "result", "is_error": False, "result": "done", "duration_ms": 1234, - }) - assert ev == {"kind": "result", "is_error": False, "result": "done", "duration_ms": 1234} + assert tool["kind"] == "tool" and tool["input"]["command"] == "df -h" # --------------------------------------------------------------------------- # # Routes + auth. # --------------------------------------------------------------------------- # - client = TestClient(app) AUTH = {"Authorization": "Bearer test-token"} def test_health_no_auth(): - r = client.get("/health") - assert r.status_code == 200 - assert r.json()["service"] == "claude-breakglass" + assert client.get("/health").json()["service"] == "claude-breakglass" def test_api_requires_auth(): assert client.post("/api/session").status_code == 401 assert client.get("/api/pve/verbs").status_code == 401 + assert client.post("/api/session/x/prompt", json={"prompt": "hi"}).status_code == 401 -def test_api_accepts_bearer(): +def test_session_create_and_unknown_session_404(): r = client.post("/api/session", headers=AUTH) - assert r.status_code == 200 - assert "session_id" in r.json() + assert r.status_code == 200 and "session_id" in r.json() + assert client.post("/api/session/nope/prompt", headers=AUTH, json={"prompt": "x"}).status_code == 404 + assert client.post("/api/session/nope/cancel", headers=AUTH).status_code == 404 -def test_api_accepts_authentik_header(): - r = client.post("/api/session", headers={"X-authentik-username": "me@viktorbarzin.me"}) - assert r.status_code == 200 +def test_prompt_starts_turn(monkeypatch): + monkeypatch.setattr(sessionmod.Session, "start_turn", lambda self, *a, **k: True) + sid = client.post("/api/session", headers=AUTH).json()["session_id"] + r = client.post(f"/api/session/{sid}/prompt", headers=AUTH, json={"prompt": "diagnose"}) + assert r.status_code == 200 and r.json()["status"] == "started" -def test_pve_verb_route_rejects_unknown(): - r = client.post("/api/pve/destroy", headers=AUTH) - assert r.status_code == 400 +def test_prompt_409_when_turn_active(monkeypatch): + monkeypatch.setattr(sessionmod.Session, "start_turn", lambda self, *a, **k: False) + sid = client.post("/api/session", headers=AUTH).json()["session_id"] + r = client.post(f"/api/session/{sid}/prompt", headers=AUTH, json={"prompt": "x"}) + assert r.status_code == 409 -def test_pve_verbs_listing(): - r = client.get("/api/pve/verbs", headers=AUTH) - assert r.status_code == 200 - body = r.json() - assert set(body["verbs"]) == pve.ALLOWED_VERBS - assert set(body["mutating"]) == pve.MUTATING_VERBS - - -def test_chat_streams_sse(monkeypatch): - async def _fake_turn(session_id, prompt, model=None): - yield {"kind": "session", "session_id": session_id} - yield {"kind": "text", "text": "hello"} - yield {"kind": "result", "is_error": False, "result": "ok"} - - monkeypatch.setattr(agent_session, "run_turn", _fake_turn) - r = client.post("/api/chat", headers=AUTH, - json={"session_id": "s1", "prompt": "diagnose"}) - assert r.status_code == 200 - assert "text/event-stream" in r.headers["content-type"] - body = r.text - assert "hello" in body - assert '"kind": "done"' in body # terminal frame always emitted +def test_pve_verbs_listing_and_unknown_rejected(): + assert set(client.get("/api/pve/verbs", headers=AUTH).json()["verbs"]) == pve.ALLOWED_VERBS + assert client.post("/api/pve/destroy", headers=AUTH).status_code == 400