From 5a38045c246bdef77c2caf27291efd6fd2287fd0 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 00:56:17 -0700
Subject: [PATCH 001/190] Fix: instance-scope the per-agent theme.json (ADR
0004/0042) (#1294)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`_theme_path()` derived the theme file straight from `_live_config_dir()`,
skipping the `_config_scope` that config YAML, secrets, and the setup marker
all pass through. So co-located instances that differ only by
`PROTOAGENT_INSTANCE` (the default instance + the `scripts/dev.sh` sandbox)
shared a single `config/theme.json` and clobbered each other's theme —
breaking the ADR 0004 isolation contract for this one store.
Add `THEME_JSON_PATH = _config_scope(_LIVE_CONFIG_DIR / "theme.json")` next to
the sibling path constants and route `_theme_path()` through it. `_config_scope`
preserves the explicit-`PROTOAGENT_CONFIG_DIR` carve-out (desktop sidecar /
fleet member: the dir is already the isolated leaf, so don't double-scope), so
the only behavior change is that `PROTOAGENT_INSTANCE`-scoped instances now get
`config//theme.json`. Unscoped default is unchanged.
Closes #1293
Co-authored-by: Josh Mabry
Co-authored-by: Claude Opus 4.8 (1M context)
---
graph/config_io.py | 9 +++++++++
operator_api/theme_routes.py | 6 ++++--
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/graph/config_io.py b/graph/config_io.py
index 24f87109..4481b5c2 100644
--- a/graph/config_io.py
+++ b/graph/config_io.py
@@ -161,6 +161,15 @@ def secret_paths() -> tuple[tuple[str, str], ...]:
_BASE_SETUP_MARKER = _LIVE_CONFIG_DIR / ".setup-complete"
SETUP_MARKER_PATH = _config_scope(_BASE_SETUP_MARKER)
+# Per-agent console theme (ADR 0042). Like the config/secrets/setup-marker above,
+# it's a config-dir-relative store, so it MUST be instance-scoped too — otherwise
+# co-located instances that differ only by PROTOAGENT_INSTANCE (the default + the
+# scripts/dev.sh sandbox) share one theme.json and clobber each other's theme.
+# `_config_scope` keeps the explicit-PROTOAGENT_CONFIG_DIR carve-out (fleet member /
+# desktop sidecar: already the isolated leaf, don't double-scope).
+_BASE_THEME_JSON = _LIVE_CONFIG_DIR / "theme.json"
+THEME_JSON_PATH = _config_scope(_BASE_THEME_JSON)
+
# ---------------------------------------------------------------------------
# YAML round-trip
diff --git a/operator_api/theme_routes.py b/operator_api/theme_routes.py
index ed71fc18..137a8227 100644
--- a/operator_api/theme_routes.py
+++ b/operator_api/theme_routes.py
@@ -18,9 +18,11 @@
def _theme_path():
- from graph.config_io import _live_config_dir
+ # Instance-scoped (ADR 0004), same as config/secrets — co-located instances
+ # (default + scripts/dev.sh sandbox) must not share one theme.json.
+ from graph.config_io import THEME_JSON_PATH
- return _live_config_dir() / "theme.json"
+ return THEME_JSON_PATH
def register_theme_routes(app) -> None:
From f62e486110dde7800f68d8727b20f4f2da756cac Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 02:14:05 -0700
Subject: [PATCH 002/190] Sync the tab favicon + theme-color with the active
per-agent theme (#1297)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Sync the tab favicon + theme-color with the active per-agent theme
The per-agent theme (/api/theme, ADR 0042) repaints the whole console on an
agent switch, but the browser tab didn't follow: the favicon SVG and
` ` were both frozen at the brand default (#9b87f2),
so every agent looked identical in the tab strip and PWA/mobile chrome stayed
lavender regardless of theme.
Add `syncBrowserChrome()` in agentTheme.ts: it reads the resolved
`--pl-color-accent` and rebuilds the favicon as a recolored data-URI SVG and
updates the theme-color meta. Wire it into the existing `watchThemeChanges()`
MutationObserver — the single broadcast point that already catches agent
switches, saves/resets, and the ThemePanel's live picker edits — plus one
initial sync. A static favicon SVG can't read page CSS vars (browsers render it
in an isolated context where `currentColor` resolves to black), hence the
runtime data-URI.
Closes #1295
Co-Authored-By: Claude Opus 4.8 (1M context)
* Address review: gate favicon sync to active themes + validate the accent
Fixes the Web E2E failure and CodeRabbit's MAJOR finding on #1297.
- E2E (`e2e/assets.spec.ts` "favicon link resolves to the icon asset") fetched
the ` ` href and broke on `Protocol "data:" not supported`: the
unconditional initial sync swapped the static favicon to a data-URI even on a
default, un-themed load. Now `syncBrowserChrome()` only recolors when a theme
is actually active (`data-theme` / inline `--pl-*` overrides present), and
restores the shipped static favicon when a theme is cleared. Un-themed loads
keep index.html's real, fetchable asset — and the base-path regression guard
that test exists for. 124/125 passed before; this was the one.
- Harden the color before embedding it into SVG/markup (CodeRabbit): the accent
comes from the opaque, agent-supplied theme blob, so `safeColor()` validates
via `CSS.supports("color", …)` + a probe element and bails on anything that
isn't a real color, instead of interpolating it raw into `stroke="…"`.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Josh Mabry
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/src/lib/agentTheme.ts | 96 +++++++++++++++++++++++++++++++++-
1 file changed, 94 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/lib/agentTheme.ts b/apps/web/src/lib/agentTheme.ts
index 998ad740..ac0b8281 100644
--- a/apps/web/src/lib/agentTheme.ts
+++ b/apps/web/src/lib/agentTheme.ts
@@ -52,10 +52,98 @@ export function applyAgentTheme(theme: unknown, animate = true) {
}
}
+// The tab favicon — mirrors apps/web/public/protolabs-icon-outline.svg, with the stroke
+// color injected at runtime. A static favicon SVG can't read the page's CSS vars (browsers
+// render it in an isolated context where `currentColor` resolves to black), so when a theme
+// is active we swap in a recolored data-URI. Keep the geometry in sync with the source asset.
+const faviconSvg = (color: string) =>
+ `` +
+ `` +
+ ` ` +
+ ` ` +
+ ` `;
+
+// Validate + normalize a CSS color before it's interpolated into SVG/markup. The accent comes
+// from the opaque, agent-supplied theme blob (--pl-color-accent), so a malformed or hostile
+// token must never reach the favicon data-URI or the meta tag. Returns null if it isn't a real
+// color, so callers bail instead of emitting broken markup.
+function safeColor(value: string): string | null {
+ const v = value.trim();
+ if (!v) return null;
+ if (typeof CSS !== "undefined" && typeof CSS.supports === "function" && !CSS.supports("color", v)) return null;
+ const probe = document.createElement("span");
+ probe.style.color = v; // the browser drops anything it can't parse as a color
+ return probe.style.color || null;
+}
+
+// Is a per-agent theme currently applied (vs. the shipped DS defaults)? The theme machinery
+// sets `data-theme` and inline `--pl-*` overrides on ; absent both, we leave index.html's
+// static favicon/meta in place (and the base-path regression guard that e2e/assets.spec.ts owns).
+function isThemed(): boolean {
+ const r = root();
+ if (r.hasAttribute("data-theme")) return true;
+ const s = r.style;
+ for (let i = 0; i < s.length; i++) if (s[i].startsWith("--pl-")) return true;
+ return false;
+}
+
+// Shipped defaults from index.html, snapshotted (raw attributes) before we first mutate them,
+// so clearing a theme restores the exact static favicon + brand theme-color.
+let _chromeDefaults: { iconHref: string | null; themeColor: string | null } | null = null;
+let _chromeThemed = false;
+
+/** Point the tab favicon + ` ` at the active theme's accent
+ * (`--pl-color-accent`), so an agent/theme switch (ADR 0042) reaches the browser chrome too —
+ * otherwise the tab stays the frozen brand default while the rest of the console repaints.
+ * With no theme active it leaves (or restores) index.html's static favicon. Fail-safe: bails
+ * if the accent isn't a valid color. */
+export function syncBrowserChrome() {
+ if (typeof document === "undefined") return;
+ const icon = document.querySelector('link[rel~="icon"]');
+ const meta = document.querySelector('meta[name="theme-color"]');
+ if (!_chromeDefaults) {
+ _chromeDefaults = { iconHref: icon?.getAttribute("href") ?? null, themeColor: meta?.getAttribute("content") ?? null };
+ }
+
+ if (!isThemed()) {
+ // Restore the shipped static chrome only if WE themed it — otherwise don't touch the
+ // default favicon link at all (keeps it a real, fetchable asset, not a data-URI).
+ if (_chromeThemed) {
+ if (icon && _chromeDefaults.iconHref != null) icon.setAttribute("href", _chromeDefaults.iconHref);
+ if (meta && _chromeDefaults.themeColor != null) meta.setAttribute("content", _chromeDefaults.themeColor);
+ _chromeThemed = false;
+ }
+ return;
+ }
+
+ const accent = safeColor(getComputedStyle(root()).getPropertyValue("--pl-color-accent"));
+ if (!accent) return;
+
+ let link = icon;
+ if (!link) {
+ link = document.createElement("link");
+ link.rel = "icon";
+ document.head.appendChild(link);
+ }
+ link.type = "image/svg+xml";
+ link.setAttribute("href", `data:image/svg+xml,${encodeURIComponent(faviconSvg(accent))}`);
+
+ let mc = meta;
+ if (!mc) {
+ mc = document.createElement("meta");
+ mc.name = "theme-color";
+ document.head.appendChild(mc);
+ }
+ mc.setAttribute("content", accent);
+ _chromeThemed = true;
+}
+
// Broadcast a single `protoagent:theme` window event whenever the document's theme changes —
// applyAgentTheme (switch/save/reset) AND the ThemePanel's live picker edits both mutate the
// root's `style`/`data-theme`, so one MutationObserver catches everything. PluginView listens
-// and re-posts the theme to its iframe, so embedded plugin views repaint live too (ADR 0026/0042).
+// and re-posts the theme to its iframe, so embedded plugin views repaint live too (ADR 0026/0042);
+// the same hook keeps the tab favicon + theme-color on the active accent.
let _watching = false;
export function watchThemeChanges() {
if (_watching || typeof window === "undefined") return;
@@ -63,9 +151,13 @@ export function watchThemeChanges() {
let raf = 0;
const fire = () => {
cancelAnimationFrame(raf);
- raf = requestAnimationFrame(() => window.dispatchEvent(new Event("protoagent:theme")));
+ raf = requestAnimationFrame(() => {
+ syncBrowserChrome();
+ window.dispatchEvent(new Event("protoagent:theme"));
+ });
};
new MutationObserver(fire).observe(root(), { attributes: true, attributeFilter: ["style", "data-theme"] });
+ syncBrowserChrome(); // initial sync — covers a theme applied before the observer was wired
}
/** The panel's current blob (what "Save to this agent" PUTs), or null if untouched. */
From 2a402a38a5c6ec3865a9fc15e023d287ccab7e78 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 02:19:50 -0700
Subject: [PATCH 003/190] =?UTF-8?q?fix(acp):=20reap=20coding-agent=20subpr?=
=?UTF-8?q?ocesses=20=E2=80=94=20kill=20the=20process=20group,=20hard-stop?=
=?UTF-8?q?=20on=20cancel=20(#1298)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
delegate_to and the delegate health prober spawn CLI coding agents (codex-acp,
claude-agent-acp, …) over ACP. Two lifecycle bugs leaked their subprocesses until
hundreds of ppid-1 orphans piled up holding ~20 GB:
1. Teardown signalled only the DIRECT child. The backend each ACP adapter spawns
reparented to init and survived — close() did proc.terminate() on the node
adapter only, never its child tree.
2. dispatch() awaited a POOLED, long-lived client and never reaped it on cancel:
stopping the turn only sent a soft session/cancel and re-raised, leaving the
agent running ("I stopped the main thread and the delegate didn't stop").
3. The prober's probe spawns + handshakes with a 45s wait_for; a timeout cancelled
_start mid-initialize after the subprocess was already spawned, orphaning it.
Fixes:
- Spawn ACP agents with start_new_session=True (own process group), and TERM→KILL
the whole group in close() via os.killpg. Grandchildren die with the agent.
- Add AcpClient.kill_now() — a synchronous group SIGKILL safe to call from a
CancelledError handler (no awaits). dispatch() now drops the pooled client and
kill_now()s its tree on cancel before re-raising.
- _start() self-reaps (close()) if the handshake raises or is cancelled after the
subprocess exists.
- Add coding_agent.close_all() + _drop_client(); the delegate-health surface's
stop() drains the whole client pool on server shutdown.
Regression tests (tests/test_acp_reaping.py) spawn a real grandchild and prove it
dies with the group, that kill_now is synchronous, that a cancelled dispatch
hard-reaps + drops, and that close_all drains the pool.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 11 +++
plugins/coding_agent/__init__.py | 24 +++++
plugins/coding_agent/acp_client.py | 75 ++++++++++++---
plugins/delegates/adapters.py | 19 +++-
plugins/delegates/health.py | 9 ++
tests/test_acp_reaping.py | 148 +++++++++++++++++++++++++++++
6 files changed, 269 insertions(+), 17 deletions(-)
create mode 100644 tests/test_acp_reaping.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ab92859..a7a1f9e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
+- **ACP coding-agent subprocesses no longer leak as orphaned processes.** `delegate_to`
+ and the delegate health prober spawn CLI coding agents (`codex-acp`, `claude-agent-acp`,
+ …) over ACP, but teardown signalled only the direct child — the backend each adapter
+ spawns reparented to init and survived — and `dispatch` awaited a *pooled* client that
+ it never reaped on cancel, so stopping a turn left the agent running ("I stopped the
+ main thread and the delegate didn't stop"). Over days these piled up to hundreds of
+ `ppid 1` orphans holding ~20 GB. Now the agent is spawned in its own process group and
+ teardown SIGTERM→SIGKILLs the whole group; `dispatch` hard-kills + drops the pooled
+ client synchronously on cancel; the `_start` handshake self-reaps if it fails or is
+ cancelled mid-flight (the prober's probe-timeout path); and a shutdown hook drains every
+ pooled client so a server stop strands nothing.
- **Dialogs no longer render their content cramped flush to the body edge.** The shared
DS dialog defaulted to a tight 16px body padding, and roomier dialogs (MCP catalog,
New-skill) each hand-added a 24px override — so every newly-converted dialog (the
diff --git a/plugins/coding_agent/__init__.py b/plugins/coding_agent/__init__.py
index 4ffb4146..933990b9 100644
--- a/plugins/coding_agent/__init__.py
+++ b/plugins/coding_agent/__init__.py
@@ -121,6 +121,30 @@ def _client_for(spec: dict) -> AcpClient:
return client
+def _drop_client(spec: dict) -> AcpClient | None:
+ """Synchronously pop the cached client for ``spec`` (no await) and return it, so
+ a cancellation handler can ``kill_now()`` it and remove it from the pool without
+ risking that an awaited teardown is itself cancelled. Returns None if none cached."""
+ return _CLIENTS.pop(_cache_key(spec), None)
+
+
+async def close_all() -> bool:
+ """Reap EVERY cached ACP client + its subprocess tree — the shutdown hook so a
+ server stop doesn't strand pooled ``delegate_to`` agents as init-reparented
+ orphans (the leak that piled up to ~20 GB). Idempotent; returns True if any were
+ closed."""
+ clients = list(_CLIENTS.values())
+ _CLIENTS.clear()
+ closed = False
+ for client in clients:
+ try:
+ await client.close()
+ closed = True
+ except Exception: # noqa: BLE001 — shutdown reap is best-effort
+ log.warning("[coding_agent] close during close_all failed", exc_info=True)
+ return closed
+
+
async def evict_client(spec: dict) -> bool:
"""Drop the cached client for ``spec`` AND terminate its subprocess.
diff --git a/plugins/coding_agent/acp_client.py b/plugins/coding_agent/acp_client.py
index eebcdd74..a5860dbd 100644
--- a/plugins/coding_agent/acp_client.py
+++ b/plugins/coding_agent/acp_client.py
@@ -26,10 +26,12 @@
from __future__ import annotations
import asyncio
+import contextlib
import json
import logging
import os
import re
+import signal
from pathlib import Path
from typing import Awaitable, Callable
@@ -204,6 +206,12 @@ async def _start(self) -> None:
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env={**os.environ, **(self.env or {})},
+ # Put the agent in its OWN session/process group so teardown can kill
+ # the WHOLE tree (the adapter *and* the backend it spawns). Without
+ # this, terminate() signals only the adapter; its child reparents to
+ # init and leaks — the codex-acp / claude-agent-acp orphan pile that
+ # piled up to ~20 GB. POSIX-only (start_new_session ⇒ setsid()).
+ start_new_session=True,
# Raise the per-line buffer ceiling — ACP messages exceed the 64 KB
# default and would otherwise raise LimitOverrunError (kills the turn).
limit=_STDOUT_LINE_LIMIT,
@@ -211,10 +219,18 @@ async def _start(self) -> None:
except FileNotFoundError as exc:
raise AcpError(f"agent binary not found: {self.command!r} (is it installed and on PATH?)") from exc
- self._reader_task = asyncio.create_task(self._read_loop())
- self._stderr_task = asyncio.create_task(self._drain_stderr())
- await self._initialize()
- await self._open_session()
+ # The subprocess now exists. If the handshake raises OR the caller's wait_for
+ # cancels us mid-initialize (the health prober's 45s probe timeout), reap the
+ # tree we just spawned instead of leaking it — close() is idempotent.
+ try:
+ self._reader_task = asyncio.create_task(self._read_loop())
+ self._stderr_task = asyncio.create_task(self._drain_stderr())
+ await self._initialize()
+ await self._open_session()
+ except BaseException:
+ with contextlib.suppress(Exception):
+ await self.close()
+ raise
logger.info(
"[acp/%s] up (pid=%s, session=%s, cwd=%s)",
self.name,
@@ -223,32 +239,61 @@ async def _start(self) -> None:
self.cwd,
)
+ @staticmethod
+ def _signal_group(proc: asyncio.subprocess.Process, sig: int) -> None:
+ """Send ``sig`` to the subprocess's whole process GROUP (the agent plus the
+ backend it spawned), falling back to the bare process if the group is already
+ gone. Synchronous syscall + swallows ProcessLookup, so it's safe to call from
+ a teardown/cancel path where the event loop won't run our coroutines."""
+ try:
+ os.killpg(os.getpgid(proc.pid), sig)
+ except (ProcessLookupError, PermissionError, OSError):
+ with contextlib.suppress(ProcessLookupError, OSError):
+ proc.send_signal(sig)
+
+ def kill_now(self) -> None:
+ """Synchronously SIGKILL the agent's whole process group — no awaits, so it's
+ safe from a CancelledError handler where awaiting cleanup would itself be
+ cancelled. The zombie is reaped later by ``proc.wait()`` / the child watcher.
+ Use this on the hard-stop path (operator stops the turn); ``close()`` is the
+ graceful one."""
+ proc = self._proc
+ if proc and proc.returncode is None:
+ self._signal_group(proc, signal.SIGKILL)
+ for task in (self._reader_task, self._stderr_task):
+ if task and not task.done():
+ task.cancel()
+
async def close(self) -> None:
- """Cancel the I/O tasks and reap the subprocess. Crucially this ``await``s
+ """Cancel the I/O tasks and reap the subprocess TREE. Crucially this ``await``s
``proc.wait()`` so the child is reaped *while the loop is alive* — without
it the subprocess transport lingers and its ``__del__`` fires after the loop
closes ("Event loop is closed"), and the stderr-drain task leaks.
Sends a best-effort ``session/close`` first — the graceful, spec-aligned
- shutdown (and what matters if an agent ever serves multiple sessions per
- process) before the SIGTERM that actually frees this one-process-per-session."""
- await self._close_session()
+ shutdown — then SIGTERM→SIGKILL the agent's whole PROCESS GROUP, not just the
+ direct child, so the backend it spawned dies with it instead of reparenting to
+ init. The kill is a synchronous syscall, so even if this runs on a cancelled
+ task the tree still dies."""
+ with contextlib.suppress(Exception):
+ await self._close_session()
for task in (self._reader_task, self._stderr_task):
if task and not task.done():
task.cancel()
proc = self._proc
if proc and proc.returncode is None:
+ self._signal_group(proc, signal.SIGTERM)
try:
- proc.terminate()
await asyncio.wait_for(proc.wait(), timeout=5.0)
except asyncio.TimeoutError:
- try:
- proc.kill()
+ self._signal_group(proc, signal.SIGKILL)
+ with contextlib.suppress(Exception):
await proc.wait()
- except ProcessLookupError:
- pass
- except ProcessLookupError:
- pass
+ except asyncio.CancelledError:
+ # We're being torn down — guarantee the tree is dead, then let the
+ # cancellation propagate (don't swallow it).
+ self._signal_group(proc, signal.SIGKILL)
+ raise
# Close the subprocess transport too, so its pipe transports don't linger to
# a post-loop-close GC — reaping the process (above) leaves the stdin write-
# pipe transport open, whose __del__ then fires "Event loop is closed".
diff --git a/plugins/delegates/adapters.py b/plugins/delegates/adapters.py
index a6394797..7b8da9ce 100644
--- a/plugins/delegates/adapters.py
+++ b/plugins/delegates/adapters.py
@@ -362,7 +362,13 @@ def config_schema(self) -> list[FieldSpec]:
placeholder="proto",
help="Binary on PATH that speaks ACP (e.g. proto). For Claude Code use `claude-code` — an alias for the claude-agent-acp adapter.",
),
- FieldSpec("args", "Args", "args", placeholder="--acp", help="Launch args (e.g. --acp). Leave empty for claude-code."),
+ FieldSpec(
+ "args",
+ "Args",
+ "args",
+ placeholder="--acp",
+ help="Launch args (e.g. --acp). Leave empty for claude-code.",
+ ),
FieldSpec(
"workdir",
"Workdir",
@@ -438,7 +444,7 @@ def _spec(d: Delegate) -> dict:
async def dispatch(self, d: Delegate, query: str, *, timeout: float | None = None) -> str:
# Reuse the ADR 0024 ACP client + by-kind permission policy.
- from plugins.coding_agent import _client_for, _make_permission
+ from plugins.coding_agent import _client_for, _drop_client, _make_permission
from plugins.coding_agent.acp_client import AcpError
spec = self._spec(d)
@@ -446,6 +452,15 @@ async def dispatch(self, d: Delegate, query: str, *, timeout: float | None = Non
client._permission = _make_permission(spec)
try:
return await client.prompt(query, timeout=timeout or d.timeout_s)
+ except asyncio.CancelledError:
+ # The turn was stopped (operator hit stop, or an orchestrator watchdog
+ # fired). The client is POOLED, so without this its subprocess keeps
+ # running detached — exactly "I stopped the main thread and the delegate
+ # didn't stop". Drop it from the pool + SIGKILL the agent tree NOW
+ # (synchronous, no awaits — we're mid-cancellation) before re-raising.
+ _drop_client(spec)
+ client.kill_now()
+ raise
except AcpError as exc:
raise DelegateError(str(exc))
diff --git a/plugins/delegates/health.py b/plugins/delegates/health.py
index a5537e90..1e6362ca 100644
--- a/plugins/delegates/health.py
+++ b/plugins/delegates/health.py
@@ -79,3 +79,12 @@ async def stop() -> None:
if _task and not _task.done():
_task.cancel()
_task = None
+ # Server shutdown: reap every pooled ACP client so dispatch agents don't strand
+ # as init-reparented orphans. (This surface's stop() is the delegates plugin's
+ # process-scoped shutdown hook.)
+ try:
+ from plugins.coding_agent import close_all
+
+ await close_all()
+ except Exception: # noqa: BLE001 — shutdown reap is best-effort
+ log.exception("[delegates/health] reaping ACP clients on shutdown failed")
diff --git a/tests/test_acp_reaping.py b/tests/test_acp_reaping.py
new file mode 100644
index 00000000..0ff3ade0
--- /dev/null
+++ b/tests/test_acp_reaping.py
@@ -0,0 +1,148 @@
+"""Regression tests for ACP subprocess reaping — the orphan leak.
+
+`delegate_to` / the health prober spawn CLI coding agents (codex-acp, claude-agent-acp)
+over ACP. Two bugs leaked their processes until ~20 GB of `ppid 1` orphans piled up:
+
+ 1. teardown signalled only the DIRECT child, so the backend the adapter spawned
+ reparented to init and survived (no process-group kill);
+ 2. `dispatch` awaited a POOLED client and never reaped it on cancel, so stopping the
+ turn left the agent running ("I stopped the main thread and the delegate didn't
+ stop").
+
+These tests pin the fixes: a spawned grandchild dies with the group, the cancel path
+hard-kills synchronously, and the shutdown hook drains the whole pool.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import os
+import subprocess
+
+import pytest
+
+from plugins.coding_agent.acp_client import AcpClient
+
+
+def _alive(pid: int) -> bool:
+ """True if ``pid`` is a live (non-zombie) process."""
+ try:
+ os.kill(pid, 0)
+ except ProcessLookupError:
+ return False
+ except PermissionError:
+ return True
+ # Exists, but a zombie/defunct is effectively dead — distinguish via ps.
+ out = subprocess.run(["ps", "-o", "stat=", "-p", str(pid)], capture_output=True, text=True).stdout.strip()
+ return bool(out) and not out.startswith("Z")
+
+
+async def _wait_dead(*pids: int, timeout: float = 5.0) -> None:
+ for _ in range(int(timeout / 0.1)):
+ if not any(_alive(p) for p in pids):
+ return
+ await asyncio.sleep(0.1)
+
+
+async def _spawn_group_with_grandchild() -> tuple[asyncio.subprocess.Process, int]:
+ """A parent shell in its OWN process group that backgrounds a grandchild ``sleep``
+ (the stand-in for the adapter's backend), prints the grandchild pid, then waits."""
+ proc = await asyncio.create_subprocess_exec(
+ "sh",
+ "-c",
+ "sleep 300 & echo $! ; wait",
+ stdin=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ start_new_session=True, # same isolation _start() now uses
+ )
+ line = await asyncio.wait_for(proc.stdout.readline(), timeout=5)
+ return proc, int(line.decode().strip())
+
+
+def _client_holding(proc: asyncio.subprocess.Process) -> AcpClient:
+ client = AcpClient("sh", ["-c", "true"], cwd="/tmp", name="reap-test")
+ client._proc = proc # bypass the ACP handshake — we only exercise teardown
+ return client
+
+
+@pytest.mark.asyncio
+async def test_close_reaps_whole_process_group():
+ """close() must kill the backend the agent spawned, not just the agent."""
+ proc, grandchild = await _spawn_group_with_grandchild()
+ assert _alive(proc.pid) and _alive(grandchild)
+
+ await _client_holding(proc).close()
+
+ await _wait_dead(proc.pid, grandchild)
+ assert not _alive(grandchild), "grandchild leaked — process-group kill regressed"
+ assert not _alive(proc.pid)
+
+
+@pytest.mark.asyncio
+async def test_kill_now_is_synchronous_and_reaps_group():
+ """kill_now() is the cancel-path hard stop: no awaits, whole group dies."""
+ proc, grandchild = await _spawn_group_with_grandchild()
+ assert _alive(proc.pid) and _alive(grandchild)
+
+ _client_holding(proc).kill_now() # synchronous — no await
+
+ await _wait_dead(proc.pid, grandchild)
+ assert not _alive(grandchild), "grandchild survived kill_now — group SIGKILL regressed"
+ assert not _alive(proc.pid)
+
+
+@pytest.mark.asyncio
+async def test_dispatch_hard_reaps_on_cancel(monkeypatch):
+ """A cancelled dispatch must drop the pooled client AND kill its agent tree —
+ otherwise the subprocess keeps running detached after the turn is stopped."""
+ import plugins.coding_agent as ca
+ from plugins.delegates.adapters import ADAPTERS
+
+ killed = {"now": False}
+ dropped: list = []
+
+ class _FakeClient:
+ _permission = None
+
+ async def prompt(self, *a, **k):
+ raise asyncio.CancelledError()
+
+ def kill_now(self):
+ killed["now"] = True
+
+ fake = _FakeClient()
+ monkeypatch.setattr(ca, "_client_for", lambda spec: fake)
+ monkeypatch.setattr(ca, "_make_permission", lambda spec: None)
+ monkeypatch.setattr(ca, "_drop_client", lambda spec: dropped.append(spec))
+
+ d = ADAPTERS["acp"].parse({"name": "coder", "type": "acp", "command": "proto", "workdir": "/tmp"})
+ with pytest.raises(asyncio.CancelledError):
+ await ADAPTERS["acp"].dispatch(d, "do a thing")
+
+ assert killed["now"], "dispatch did not hard-kill the agent tree on cancel"
+ assert dropped, "dispatch did not drop the dead client from the pool on cancel"
+
+
+@pytest.mark.asyncio
+async def test_close_all_drains_the_pool():
+ """The shutdown hook reaps every pooled client and clears the cache."""
+ import plugins.coding_agent as ca
+
+ closed: list[str] = []
+
+ class _FakeClient:
+ def __init__(self, name):
+ self.name = name
+
+ async def close(self):
+ closed.append(self.name)
+
+ ca._CLIENTS.clear()
+ ca._CLIENTS[("a",)] = _FakeClient("a")
+ ca._CLIENTS[("b",)] = _FakeClient("b")
+
+ assert await ca.close_all() is True
+ assert sorted(closed) == ["a", "b"]
+ assert ca._CLIENTS == {}
+ assert await ca.close_all() is False # idempotent
From 6f9b0cd5e71e2fd6647e3b890cec6c499a4be772 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 10:58:38 -0700
Subject: [PATCH 004/190] fix(acp): initialize-only health probe + strip
nested-Claude env + round-trip logging (#1301)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #1300, #1296.
#1300 — the delegate health prober ran a real ACP session (session/new or
session/load) every 120s per acp delegate, despite the probe documenting itself
as "no session, side-effect-free". Add AcpClient.handshake() that runs ONLY the
ACP `initialize` round-trip (refactor _start(open_session=)), and point the probe
at it. A status probe is now genuinely cheap + inert — it no longer spawns a full
backend or touches persisted session state on a timer.
#1296 (mode 1, respawn loop) — the ACP launch env inherited CLAUDECODE /
CLAUDE_CODE_* from a protoAgent server itself started inside a Claude Code
session, so the spawned claude-agent-acp backend hit the "inside another Claude
Code session" guard and respawned every ~2 min with no surfaced error. Strip the
whole marker family in _launch_env() (an explicit delegate-env value still wins) —
no more partial-strip footgun.
#1296 (mode 2, idle freeze) — add round-trip logging (initialize OK →
session/prompt send → request_permission outcome) so a stalled prompt is
diagnosable from the server log instead of a silent hang.
Tests: handshake() opens no session (wire-level), launch env strips inherited
markers but keeps explicit ones (wire-level); existing probe tests repointed
_ensure_started → handshake. Docs: coding-agents + delegates guides, ADR 0024.
Co-authored-by: Claude Opus 4.8 (1M context)
---
docs/adr/0024-spawn-cli-coding-agents-acp.md | 9 +++
docs/guides/coding-agents.md | 23 +++---
docs/guides/delegates.md | 5 +-
plugins/coding_agent/acp_client.py | 80 +++++++++++++++++++-
plugins/delegates/adapters.py | 21 +++--
tests/test_coding_agent_plugin.py | 68 +++++++++++++++++
tests/test_delegates_api.py | 8 +-
tests/test_delegates_plugin.py | 4 +-
8 files changed, 190 insertions(+), 28 deletions(-)
diff --git a/docs/adr/0024-spawn-cli-coding-agents-acp.md b/docs/adr/0024-spawn-cli-coding-agents-acp.md
index d146c171..0be0dbe9 100644
--- a/docs/adr/0024-spawn-cli-coding-agents-acp.md
+++ b/docs/adr/0024-spawn-cli-coding-agents-acp.md
@@ -20,6 +20,15 @@ lifecycle:
launch signature), `session/close` on teardown, real `protocolVersion`
negotiation (closes if the agent counters with an unsupported version), and
`agent_thought_chunk` surfaced as the reasoning trace.
+- **#1296 / #1300** — launch + probe hardening. The ACP launch env now **strips the
+ nested-Claude markers** (`CLAUDECODE` / `CLAUDE_CODE_*`) so a `claude-agent-acp`
+ backend doesn't hit the "inside another Claude Code session" guard when protoAgent
+ itself runs inside a Claude session (the dogfooding case); the round-trip
+ (`initialize` → session → `session/prompt` → `request_permission`) is now logged so
+ an idle freeze is diagnosable. A new `AcpClient.handshake()` runs **`initialize`
+ only** (no `session/new`/`session/load`), and the delegate health prober uses it —
+ so a status probe is genuinely cheap + side-effect-free instead of opening a real
+ session every 120s.
Validated end-to-end against both `proto --acp` and Claude Code (via
`@zed-industries/claude-code-acp`) — the client is agent-agnostic. See the
diff --git a/docs/guides/coding-agents.md b/docs/guides/coding-agents.md
index ab542420..efcf946e 100644
--- a/docs/guides/coding-agents.md
+++ b/docs/guides/coding-agents.md
@@ -76,12 +76,16 @@ don't have to know the incantation. (The older `@zed-industries/claude-code-acp`
`command: claude` directly does **not** work — `claude` isn't an ACP server, and the
probe will tell you so.
-> **Caveat:** the adapter launches the `claude` binary, which **refuses to start
-> nested inside another Claude Code session** (`Error: Claude Code cannot be launched
-> inside another Claude Code session`). Run protoAgent from a normal shell / the
-> desktop app — not from within a `claude` session — when using the Claude Code agent.
-> (If you must run nested, strip `CLAUDECODE` + `CLAUDE_CODE_*` from protoAgent's
-> environment at launch — e.g. `env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT …`.)
+> **Nested Claude:** the adapter launches the `claude` binary, which **refuses to
+> start nested inside another Claude Code session** (`Error: Claude Code cannot be
+> launched inside another Claude Code session`). protoAgent now **strips the
+> nested-session markers** (`CLAUDECODE` and the whole `CLAUDE_CODE_*` family) from
+> the ACP launch env automatically ([#1296](https://github.com/protoLabsAI/protoAgent/issues/1296)),
+> so launching protoAgent from *within* a `claude` session — the dogfooding case —
+> works without the manual `env -u …` dance. (Partial strips were the footgun: missing
+> just one of `CLAUDE_CODE_SESSION_ID` / `CLAUDE_CODE_ENTRYPOINT` / … still tripped the
+> guard, so the agent respawned every ~2 min with no surfaced error.) A value you set
+> explicitly in the delegate `env` still wins.
**Codex has no native ACP mode either.** Recent `codex` CLI (≥ 0.13x) dropped the
`acp` subcommand — it speaks **MCP** natively, not ACP, so `command: codex, args:
@@ -141,9 +145,10 @@ Action kinds come from the ACP request (`toolCall.kind`: `read` / `edit` /
### Environment
-The subprocess **inherits protoAgent's environment** (plus any per-delegate `env`).
-Run protoAgent under an account whose ambient credentials you're willing to lend the
-coding agent, or scope the `workdir` to a throwaway checkout.
+The subprocess **inherits protoAgent's environment** (plus any per-delegate `env`),
+**minus** the nested-Claude markers (`CLAUDECODE` / `CLAUDE_CODE_*`) — see the caveat
+above. Run protoAgent under an account whose ambient credentials you're willing to lend
+the coding agent, or scope the `workdir` to a throwaway checkout.
## How it works
diff --git a/docs/guides/delegates.md b/docs/guides/delegates.md
index a8ed8f50..9706136a 100644
--- a/docs/guides/delegates.md
+++ b/docs/guides/delegates.md
@@ -26,7 +26,10 @@ MCP, and Subagents). The panel:
- **lists** your delegates with a type badge, a `secret set` / `⚠ unconfigured`
marker, a **live health dot** (a background prober probes each delegate
periodically — green reachable / red down / grey not-yet-checked), and a per-row
- **Test** button for an on-demand probe;
+ **Test** button for an on-demand probe. For an `acp` (coding-agent) delegate the
+ probe runs only the ACP `initialize` handshake — **not** a session — so it's cheap
+ and side-effect-free, never opening a thread against the agent on a timer
+ ([#1300](https://github.com/protoLabsAI/protoAgent/issues/1300));
- **adds** one via a **type picker** (A2A agent / Model endpoint / Coding agent)
and a form generated from each type's field schema;
- **edits / deletes** existing ones; secrets you enter are routed to
diff --git a/plugins/coding_agent/acp_client.py b/plugins/coding_agent/acp_client.py
index a5860dbd..b5090421 100644
--- a/plugins/coding_agent/acp_client.py
+++ b/plugins/coding_agent/acp_client.py
@@ -112,6 +112,31 @@ def _content_text(content) -> str:
# resolved auth (ACP `AUTH_REQUIRED`). The client surfaces an actionable message.
AUTH_REQUIRED = -32000
+# Env markers Claude Code sets so a nested `claude` can detect "am I already running
+# inside another Claude Code session?". When protoAgent's own server was launched from
+# within a Claude Code session (the dogfooding case), these are inherited — and the
+# claude-agent-acp backend we spawn then hits the *"cannot be launched inside another
+# Claude Code session"* guard, evicting + respawning every ~2 min with zero progress
+# and no surfaced error (#1296). Stripped from the ACP launch env: ``CLAUDECODE`` plus
+# the whole ``CLAUDE_CODE_*`` family (SESSION_ID / CHILD_SESSION / EXECPATH / ENTRYPOINT
+# / …). Harmless for non-Claude agents (proto/codex don't read them).
+_NESTED_CLAUDE_ENV_EXACT = frozenset({"CLAUDECODE"})
+_NESTED_CLAUDE_ENV_PREFIX = "CLAUDE_CODE_"
+
+
+def _launch_env(extra: dict[str, str] | None) -> dict[str, str]:
+ """Build the subprocess environment for an ACP agent: the server's own ``os.environ``
+ with the nested-Claude markers stripped (see above), then the delegate's ``env``
+ overlaid last — so an operator who *deliberately* sets one of these in the delegate
+ env still wins."""
+ env = {
+ k: v
+ for k, v in os.environ.items()
+ if k not in _NESTED_CLAUDE_ENV_EXACT and not k.startswith(_NESTED_CLAUDE_ENV_PREFIX)
+ }
+ env.update(extra or {})
+ return env
+
class AcpError(Exception):
"""Any ACP transport / protocol failure. The caller speaks the message.
@@ -189,12 +214,26 @@ def __init__(
# -- lifecycle -----------------------------------------------------------
async def _ensure_started(self) -> None:
+ """Start the agent for a real dispatch: spawn + ``initialize`` + open the
+ session (reattach or new). Idempotent — a no-op when already up."""
async with self._start_lock:
if self._proc is not None and self._proc.returncode is None:
return
await self._start()
- async def _start(self) -> None:
+ async def handshake(self) -> None:
+ """Start the agent for a *liveness check only*: spawn + run the ACP
+ ``initialize`` round-trip and STOP — no ``session/new``, no ``session/load``,
+ no session state touched. The genuinely cheap, side-effect-free probe the
+ health prober wants (#1300; the old probe went through ``_ensure_started``,
+ which also opened a real session every 120s). The caller ``close()``s it.
+ Idempotent — a no-op when already up."""
+ async with self._start_lock:
+ if self._proc is not None and self._proc.returncode is None:
+ return
+ await self._start(open_session=False)
+
+ async def _start(self, *, open_session: bool = True) -> None:
if not Path(self.cwd).is_dir():
raise AcpError(f"workdir does not exist: {self.cwd}")
try:
@@ -205,7 +244,10 @@ async def _start(self) -> None:
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
- env={**os.environ, **(self.env or {})},
+ # Strip the inherited nested-Claude markers (CLAUDECODE / CLAUDE_CODE_*)
+ # so a spawned Claude backend doesn't refuse to launch "inside another
+ # Claude Code session" (#1296); the delegate's env is overlaid last.
+ env=_launch_env(self.env),
# Put the agent in its OWN session/process group so teardown can kill
# the WHOLE tree (the adapter *and* the backend it spawns). Without
# this, terminate() signals only the adapter; its child reparents to
@@ -226,7 +268,10 @@ async def _start(self) -> None:
self._reader_task = asyncio.create_task(self._read_loop())
self._stderr_task = asyncio.create_task(self._drain_stderr())
await self._initialize()
- await self._open_session()
+ # The probe path (handshake) stops here — a liveness check must NOT open a
+ # session. A real dispatch opens (reattaches or news) the session.
+ if open_session:
+ await self._open_session()
except BaseException:
with contextlib.suppress(Exception):
await self.close()
@@ -434,8 +479,18 @@ async def _handle_request(self, msg: dict) -> None:
method = msg.get("method")
rid = msg.get("id")
if method == "session/request_permission":
+ params = msg.get("params") or {}
resolver = self._permission or self._auto_allow
- option_id = resolver(msg.get("params") or {})
+ option_id = resolver(params)
+ # Trace the decision — a permission the resolver can't answer (→ cancelled)
+ # can leave the agent blocked, a prime suspect for the idle freeze (#1296).
+ kind = str(((params.get("toolCall") or {}).get("kind") or "")).lower()
+ logger.info(
+ "[acp/%s] request_permission kind=%s → %s",
+ self.name,
+ kind or "?",
+ "selected" if option_id else "cancelled",
+ )
outcome = {"outcome": "selected", "optionId": option_id} if option_id else {"outcome": "cancelled"}
await self._respond(rid, {"outcome": outcome})
else:
@@ -580,6 +635,16 @@ async def _initialize(self) -> None:
f"client so their ACP versions match."
)
self._protocol_version = negotiated
+ # Log the handshake outcome so the ACP round-trip (initialize → session →
+ # prompt → permission → result) is traceable from the server log — an idle
+ # freeze with no diagnostics is the hard-to-debug half of #1296.
+ logger.info(
+ "[acp/%s] initialize OK (protocol v%s, loadSession=%s, authMethods=%d)",
+ self.name,
+ self._protocol_version,
+ bool(self._agent_capabilities.get("loadSession")),
+ len(self._auth_methods),
+ )
async def _open_session(self) -> None:
"""Reattach the persisted session when possible, else start a fresh one.
@@ -698,6 +763,13 @@ async def prompt(
self._on_tool = tool_callback
self._on_text = text_callback
self._on_thought = thought_callback
+ logger.info(
+ "[acp/%s] → session/prompt (session=%s, %d chars, timeout=%ss)",
+ self.name,
+ self._session_id,
+ len(text),
+ int(timeout),
+ )
try:
result = await self._request(
"session/prompt",
diff --git a/plugins/delegates/adapters.py b/plugins/delegates/adapters.py
index 7b8da9ce..d2cf0b7f 100644
--- a/plugins/delegates/adapters.py
+++ b/plugins/delegates/adapters.py
@@ -488,13 +488,17 @@ async def forget_session(self, d: Delegate) -> bool:
return await forget_session(self._spec(d))
async def probe(self, d: Delegate) -> dict:
- """Reachability check for the panel's Test button.
+ """Reachability check for the panel's Test button (also the periodic health
+ prober's per-delegate check).
Does a REAL ACP `initialize` handshake (spawn the command, complete the
- protocol handshake, close — no session/prompt), so a launch command that's
- on PATH and workdir-valid but doesn't actually speak ACP FAILS the probe
- instead of showing green (issue #1116 — the old PATH+workdir check passed
- `command: claude` while every dispatch failed with an opaque `agent exited`).
+ protocol handshake, close) — and NOTHING more: no `session/new`, no
+ `session/load`, no `session/prompt`. So a launch command that's on PATH and
+ workdir-valid but doesn't actually speak ACP FAILS the probe instead of
+ showing green (issue #1116 — the old PATH+workdir check passed `command:
+ claude` while every dispatch failed with an opaque `agent exited`), while a
+ probe stays genuinely cheap + side-effect-free: it never opens a session every
+ 120s the way the old `_ensure_started` path did (#1300).
"""
import asyncio
import os
@@ -522,13 +526,14 @@ async def probe(self, d: Delegate) -> dict:
if not os.path.isdir(wd):
return {"ok": False, "error": f"workdir does not exist: {wd}"}
- # Real handshake. `_ensure_started` spawns the agent and runs ACP `initialize`
- # (it does NOT open a session), so it's a cheap, side-effect-free liveness check.
+ # Real handshake — `handshake()` spawns the agent and runs ACP `initialize`
+ # ONLY (no session/new, no session/load), so it's a cheap, genuinely
+ # side-effect-free liveness check (#1300).
from plugins.coding_agent.acp_client import AcpClient
client = AcpClient(command=d.command, args=d.args, cwd=wd, env=(d.env or None), name=d.name)
try:
- await asyncio.wait_for(client._ensure_started(), timeout=45)
+ await asyncio.wait_for(client.handshake(), timeout=45)
except asyncio.TimeoutError:
return {"ok": False, "error": f"ACP handshake timed out — does {d.command!r} speak ACP?"}
except Exception as exc: # noqa: BLE001 — spawn/handshake failure → tool-visible string
diff --git a/tests/test_coding_agent_plugin.py b/tests/test_coding_agent_plugin.py
index e317a151..b1821019 100644
--- a/tests/test_coding_agent_plugin.py
+++ b/tests/test_coding_agent_plugin.py
@@ -131,6 +131,74 @@ async def test_close_reaps_the_subprocess(fake_agent, tmp_path):
assert client._stderr_task is not None and client._stderr_task.done() # not leaked
+# ── handshake-only liveness check + launch-env hygiene ────────────────────────
+
+# Records (to a marker) every CLAUDECODE / CLAUDE_CODE_* var visible in its env at
+# initialize time, so a test can prove the launch env stripped the inherited ones.
+_ENV_PROBE_AGENT = r"""
+import sys, json, os
+MARKER = os.environ.get("ENV_MARKER", "")
+def send(obj):
+ sys.stdout.write(json.dumps(obj) + "\n"); sys.stdout.flush()
+while True:
+ line = sys.stdin.readline()
+ if not line:
+ break
+ line = line.strip()
+ if not line:
+ continue
+ msg = json.loads(line)
+ if msg.get("method") == "initialize":
+ leaked = sorted(k for k in os.environ if k == "CLAUDECODE" or k.startswith("CLAUDE_CODE_"))
+ if MARKER:
+ with open(MARKER, "w") as fh:
+ fh.write(json.dumps(leaked))
+ send({"jsonrpc": "2.0", "id": msg.get("id"), "result": {"protocolVersion": 1}})
+"""
+
+
+async def test_handshake_runs_initialize_without_opening_a_session(fake_agent, tmp_path):
+ """handshake() runs ONLY the ACP initialize round-trip — no session/new or
+ session/load — so the health prober's liveness check is genuinely
+ side-effect-free (#1300). The process is up and initialize negotiated, but no
+ session id was ever assigned."""
+ client = AcpClient(sys.executable, [str(fake_agent)], cwd=str(tmp_path), name="probe")
+ try:
+ await client.handshake()
+ assert client._proc is not None and client._proc.returncode is None # up
+ assert client._protocol_version == 1 # initialize completed
+ assert client._session_id is None # but NO session was opened
+ finally:
+ await client.close()
+
+
+async def test_launch_env_strips_inherited_nested_claude_markers(tmp_path, monkeypatch):
+ """The ACP launch env strips the inherited CLAUDECODE / CLAUDE_CODE_* markers so a
+ spawned Claude backend doesn't refuse to launch "inside another Claude Code
+ session" (#1296) — but a value explicitly set in the delegate env still wins."""
+ script = tmp_path / "env_agent.py"
+ script.write_text(_ENV_PROBE_AGENT, encoding="utf-8")
+ marker = tmp_path / "env.marker"
+ monkeypatch.setenv("CLAUDECODE", "1")
+ monkeypatch.setenv("CLAUDE_CODE_SESSION_ID", "parent-sess")
+ monkeypatch.setenv("CLAUDE_CODE_ENTRYPOINT", "cli")
+ client = AcpClient(
+ sys.executable,
+ [str(script)],
+ cwd=str(tmp_path),
+ name="env",
+ # ENV_MARKER survives (not a CLAUDE_CODE_* var); the explicit CHILD_SESSION
+ # overlays the (stripped) inherited markers and must reach the child.
+ env={"ENV_MARKER": str(marker), "CLAUDE_CODE_CHILD_SESSION": "keepme"},
+ )
+ try:
+ await client.handshake()
+ finally:
+ await client.close()
+ leaked = json.loads(marker.read_text(encoding="utf-8"))
+ assert leaked == ["CLAUDE_CODE_CHILD_SESSION"] # inherited stripped, explicit kept
+
+
async def test_acp_client_readonly_policy_denies_edit(fake_agent, tmp_path):
# A readonly policy must reject the fake's `edit` permission request — the
# client picks the reject_once option, which the fake echoes back.
diff --git a/tests/test_delegates_api.py b/tests/test_delegates_api.py
index 29ab48b3..4f9fc6fe 100644
--- a/tests/test_delegates_api.py
+++ b/tests/test_delegates_api.py
@@ -128,15 +128,15 @@ def test_test_endpoint_acp_probe(client, monkeypatch):
from plugins.coding_agent.acp_client import AcpClient
- # The acp probe now does a real ACP `initialize` handshake (#1116), so mock it —
- # the python exe is on PATH + /tmp exists, but it doesn't speak ACP for real.
+ # The acp probe does a real ACP `initialize`-only handshake (#1116/#1300), so mock
+ # it — the python exe is on PATH + /tmp exists, but it doesn't speak ACP for real.
async def _ok(self):
self._protocol_version = 1
async def _noop(self):
pass
- monkeypatch.setattr(AcpClient, "_ensure_started", _ok)
+ monkeypatch.setattr(AcpClient, "handshake", _ok)
monkeypatch.setattr(AcpClient, "close", _noop)
r = client.post(
"/api/delegates/test", json={"name": "t", "type": "acp", "command": sys.executable, "workdir": "/tmp"}
@@ -170,7 +170,7 @@ async def _ok(self):
async def _noop(self):
pass
- monkeypatch.setattr(AcpClient, "_ensure_started", _ok)
+ monkeypatch.setattr(AcpClient, "handshake", _ok)
monkeypatch.setattr(AcpClient, "close", _noop)
client.post("/api/delegates", json={"name": "proto", "type": "acp", "command": sys.executable, "workdir": "/tmp"})
r = client.post("/api/delegates/test", json={"name": "proto", "type": "acp"})
diff --git a/tests/test_delegates_plugin.py b/tests/test_delegates_plugin.py
index 58400e4b..1dd87065 100644
--- a/tests/test_delegates_plugin.py
+++ b/tests/test_delegates_plugin.py
@@ -98,7 +98,7 @@ async def _boom(self):
async def _noop(self):
pass
- monkeypatch.setattr(AcpClient, "_ensure_started", _boom)
+ monkeypatch.setattr(AcpClient, "handshake", _boom)
monkeypatch.setattr(AcpClient, "close", _noop)
d = ADAPTERS["acp"].parse({"name": "x", "type": "acp", "command": sys.executable, "workdir": "/tmp"})
res = await ADAPTERS["acp"].probe(d)
@@ -116,7 +116,7 @@ async def _ok(self):
async def _noop(self):
pass
- monkeypatch.setattr(AcpClient, "_ensure_started", _ok)
+ monkeypatch.setattr(AcpClient, "handshake", _ok)
monkeypatch.setattr(AcpClient, "close", _noop)
d = ADAPTERS["acp"].parse({"name": "x", "type": "acp", "command": sys.executable, "workdir": "/tmp"})
res = await ADAPTERS["acp"].probe(d)
From 91f7a0d8c2d2980b972cc37f9932e17f1d44daad Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 11:02:26 -0700
Subject: [PATCH 005/190] fix(desktop): give the sidecar the user's real PATH;
probe honors delegate env PATH (#1302)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #1299.
Primary (Tauri): a macOS app launched from Finder/Dock/launchd inherits only
launchd's minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin), not the user's login-shell
PATH — so Homebrew (/opt/homebrew/bin), nvm, Volta, and asdf bin dirs (where npx,
node, and ACP coding-agent adapters live) are invisible to the bundled server, and a
`command: npx` delegate fails with "binary not on PATH". spawn_sidecar now augments
the sidecar's PATH (login-shell PATH via `$SHELL -ilc`, plus Homebrew/local fallbacks
and the already-inherited PATH) before .spawn(). macOS-only; web/terminal launches are
unaffected.
Secondary (probe consistency): AcpAdapter.probe resolved `shutil.which(d.command)`
against os.environ only, while the real ACP spawn merges the delegate's env — so a
delegate that supplies its own PATH spawned fine yet the Test button still red-X'd it.
The probe now resolves against the merged PATH (`shutil.which(..., path=…)`), so probe
and spawn agree.
Tests: probe resolves the command against the delegate env PATH (captures the path=
kwarg). Docs: coding-agents guide (macOS PATH warning + probe-vs-spawn note),
react-tauri-ui guide (sidecar PATH augmentation). cargo check ✓.
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/desktop/src-tauri/src/lib.rs | 64 ++++++++++++++++++++++++++++++-
docs/guides/coding-agents.md | 14 +++++++
docs/guides/react-tauri-ui.md | 6 +++
plugins/delegates/adapters.py | 9 ++++-
tests/test_delegates_plugin.py | 22 +++++++++++
5 files changed, 113 insertions(+), 2 deletions(-)
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index d092bb25..d5803aa1 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -33,6 +33,59 @@ fn pick_free_port() -> u16 {
#[derive(Default)]
struct SidecarProcess(Mutex>);
+/// Split a `:`-delimited PATH string and append each new, non-empty dir to `entries`,
+/// preserving order and skipping duplicates.
+#[cfg(target_os = "macos")]
+fn dedup_push_path(entries: &mut Vec, raw: &str) {
+ for dir in raw.split(':') {
+ if !dir.is_empty() && !entries.iter().any(|e| e == dir) {
+ entries.push(dir.to_string());
+ }
+ }
+}
+
+/// Ask the user's interactive login shell for its `PATH`
+/// (`$SHELL -ilc 'printf %s "$PATH"'`). `None` if `$SHELL` is unknown, the shell
+/// errors, or it returns nothing — callers fall back to a fixed prefix.
+#[cfg(target_os = "macos")]
+fn login_shell_path() -> Option {
+ let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
+ let output = std::process::Command::new(&shell)
+ .args(["-ilc", "printf %s \"$PATH\""])
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+ let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if path.is_empty() {
+ None
+ } else {
+ Some(path)
+ }
+}
+
+/// The PATH to hand the bundled sidecar on macOS. A GUI app launched from
+/// Finder/Dock/`launchd` only inherits `launchd`'s minimal PATH
+/// (`/usr/bin:/bin:/usr/sbin:/sbin`), so Homebrew (`/opt/homebrew/bin`), nvm, Volta,
+/// and asdf bin dirs — where `npx`, `node`, and ACP coding-agent adapters live — are
+/// invisible to the server, and a delegate launch command like `npx` fails with
+/// "binary not on PATH" (#1299). Compose: the login-shell PATH (covers nvm/Volta/asdf),
+/// then the common Homebrew/local dirs (belt-and-suspenders if shell resolution failed),
+/// then whatever the process already inherited (never drop a dir that already worked).
+#[cfg(target_os = "macos")]
+fn augmented_sidecar_path() -> String {
+ let mut entries: Vec = Vec::new();
+ if let Some(shell_path) = login_shell_path() {
+ dedup_push_path(&mut entries, &shell_path);
+ }
+ dedup_push_path(&mut entries, "/opt/homebrew/bin:/usr/local/bin");
+ if let Ok(existing) = std::env::var("PATH") {
+ dedup_push_path(&mut entries, &existing);
+ }
+ entries.join(":")
+}
+
/// Launch the bundled protoAgent server (console UI tier) as a sidecar.
///
/// The frozen binary is read-only, so its writable state (live config,
@@ -60,7 +113,8 @@ fn spawn_sidecar(app: &AppHandle, port: u16) {
}
};
let port_arg = port.to_string();
- let command = command
+ #[allow(unused_mut)] // `mut` is only used on the macOS PATH branch below.
+ let mut command = command
// The desktop renders the React operator console, so run the server in
// its 'console' UI tier (API + A2A + console, no Gradio) — ADR 0010.
// (Was the now-deprecated --headless / PROTOAGENT_HEADLESS alias.)
@@ -71,6 +125,14 @@ fn spawn_sidecar(app: &AppHandle, port: u16) {
.env("PROTOAGENT_PARENT_PID", std::process::id().to_string())
.env("PROTOAGENT_CONFIG_DIR", config_dir.to_string_lossy().to_string());
+ // A Finder/Dock/launchd launch strips PATH down to launchd's minimal set, hiding
+ // Homebrew/nvm/Volta/asdf — so delegate launch commands (`npx`, ACP adapters) fail
+ // with "binary not on PATH" (#1299). Hand the sidecar the user's real PATH.
+ #[cfg(target_os = "macos")]
+ {
+ command = command.env("PATH", augmented_sidecar_path());
+ }
+
let (mut rx, child) = match command.spawn() {
Ok(pair) => pair,
Err(e) => {
diff --git a/docs/guides/coding-agents.md b/docs/guides/coding-agents.md
index efcf946e..7e69968c 100644
--- a/docs/guides/coding-agents.md
+++ b/docs/guides/coding-agents.md
@@ -66,6 +66,20 @@ The binary must be installed and on the `PATH` of the process running protoAgent
The delegates panel's **Test** button performs a real ACP `initialize` handshake — so
a wrong launch command **fails the probe** instead of showing green (it's not just a
missing-binary check). A misconfigured delegate surfaces at Test, not at first dispatch.
+The probe resolves the command against the **same** PATH the spawn uses — the process
+PATH with the delegate's `env` PATH overlaid — so probe and dispatch never disagree.
+
+::: warning macOS desktop app & `PATH`
+A GUI app launched from Finder/Dock/`launchd` inherits only `launchd`'s minimal `PATH`
+(`/usr/bin:/bin:/usr/sbin:/sbin`), **not** your login-shell `PATH` — so Homebrew
+(`/opt/homebrew/bin`), nvm, Volta, and asdf installs (where `npx`/`node`/ACP adapters
+live) are invisible, and a `command: npx` delegate fails with `binary not on PATH`
+([#1299](https://github.com/protoLabsAI/protoAgent/issues/1299)). The desktop build now
+hands the bundled server your real login-shell `PATH`, so this works out of the box.
+If you still hit it (an unusual shell setup), either set an **absolute** `command`
+(`/opt/homebrew/bin/npx`) or add a `PATH` to the delegate **`env`** — both pass the
+probe too. The web app (terminal-launched server) is unaffected.
+:::
**Claude Code has no native ACP mode.** Drive it through the
[`claude-agent-acp`](https://www.npmjs.com/package/@agentclientprotocol/claude-agent-acp)
diff --git a/docs/guides/react-tauri-ui.md b/docs/guides/react-tauri-ui.md
index 6c315817..ed843644 100644
--- a/docs/guides/react-tauri-ui.md
+++ b/docs/guides/react-tauri-ui.md
@@ -128,6 +128,12 @@ frozen build bundles the `plugins/` tree and `--collect-all`s `tools`/`websocket
plugin, ADR 0058, can only import what's bundled). Signed macOS DMG / Linux AppImage+deb /
Windows NSIS artifacts + an in-app updater ship from the desktop-build CI on release tags.
+On macOS, `spawn_sidecar` augments the sidecar's `PATH` with the user's login-shell `PATH`
+(via `$SHELL -ilc`, plus the Homebrew/local fallbacks) before spawning. A Finder/Dock launch
+otherwise inherits only `launchd`'s minimal `PATH`, so `npx`/`node`/ACP coding-agent adapters
+would be invisible and a `delegate_to` ACP launch would fail with `binary not on PATH`
+([#1299](https://github.com/protoLabsAI/protoAgent/issues/1299)).
+
## Testing the console
A Playwright smoke suite (`apps/web/e2e/`) drives the **built** SPA against a deterministic
diff --git a/plugins/delegates/adapters.py b/plugins/delegates/adapters.py
index d2cf0b7f..61f3b364 100644
--- a/plugins/delegates/adapters.py
+++ b/plugins/delegates/adapters.py
@@ -515,7 +515,14 @@ async def probe(self, d: Delegate) -> dict:
"command: claude-code (an alias) or claude-agent-acp."
),
}
- if not shutil.which(d.command):
+ # Resolve the command against the SAME PATH the real spawn will use — the
+ # delegate's env PATH overlaid on the process PATH — not just os.environ.
+ # The actual ACP launch merges d.env (acp_client `_launch_env`/`env=…`), so a
+ # delegate that supplies its own PATH (or runs under the desktop app's
+ # augmented PATH) would spawn fine, yet the probe's bare `shutil.which` still
+ # red-X'd it. Probe and spawn now agree on where to look (#1299).
+ merged_path = (d.env or {}).get("PATH") or os.environ.get("PATH")
+ if not shutil.which(d.command, path=merged_path):
if os.path.basename(d.command) == "claude-agent-acp":
return {
"ok": False,
diff --git a/tests/test_delegates_plugin.py b/tests/test_delegates_plugin.py
index 1dd87065..f8ff3a7e 100644
--- a/tests/test_delegates_plugin.py
+++ b/tests/test_delegates_plugin.py
@@ -123,6 +123,28 @@ async def _noop(self):
assert res["ok"] is True and "handshake OK" in res["detail"]
+async def test_acp_probe_resolves_command_against_delegate_env_path(monkeypatch):
+ # The probe must resolve the command against the SAME PATH the real spawn uses —
+ # the delegate's env PATH overlaid on the process PATH — so a command reachable
+ # only via the delegate env doesn't red-X the Test button while the spawn would
+ # actually find it (#1299 probe-vs-spawn disagreement).
+ import shutil
+
+ seen: dict = {}
+
+ def fake_which(cmd, path=None):
+ seen["path"] = path
+ return None # force the not-on-PATH branch (so we never spawn a real process)
+
+ monkeypatch.setattr(shutil, "which", fake_which)
+ d = ADAPTERS["acp"].parse(
+ {"name": "x", "type": "acp", "command": "npx", "workdir": "/tmp", "env": {"PATH": "/custom/bin"}}
+ )
+ res = await ADAPTERS["acp"].probe(d)
+ assert res["ok"] is False and "not on PATH" in res["error"]
+ assert seen["path"] == "/custom/bin" # resolved against the delegate's env PATH
+
+
def test_secret_value_wins_then_env(monkeypatch):
assert _secret({"token": "explicit"}, "token", "credentialsEnv") == "explicit"
monkeypatch.setenv("MY_TOK", "fromenv")
From 2ad41318e8bb144659339e7c3081d3a8575e229e Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 11:02:45 -0700
Subject: [PATCH 006/190] =?UTF-8?q?test(chat-e2e):=20cover=20the=20?=
=?UTF-8?q?=E2=9C=95-cancel=20of=20a=20queued=20steer=20(closes=20#1103)?=
=?UTF-8?q?=20(#1303)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The mid-turn steering ✕-cancel was shipped in #1104 (backend dequeue + DELETE
/steer/{msgId}, api.cancelSteer, the wired onCancel with optimistic-drop +
turn-end reconcile) and is covered by unit tests (tests/test_steering.py,
tests/test_chat_routes.py). The one outstanding #1103 acceptance item was an e2e
that actually clicks the ✕ — this adds it.
- mock-server: a "hold the turn open" prompt streams the opening frames then holds
the SSE open (no terminal frame) so the surface stays "streaming" — the steering
state — deterministically, instead of racing the ~40ms-gapped frames; plus a
DELETE /steer/{msgId} handler returning {removed:true} (cancel-before-drain path).
- spec: start a held turn → queue a steer (dimmed `.pl-message--queued` bubble) →
click "Cancel queued message" → assert the DELETE fires and the bubble is gone.
Verified locally against a fresh build: the new spec + the chat e2e batch + web
unit tests all pass.
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/chat-steer-cancel.spec.ts | 39 ++++++++++++++++++++++++++
apps/web/e2e/mock-server.mjs | 17 +++++++++++
2 files changed, 56 insertions(+)
create mode 100644 apps/web/e2e/chat-steer-cancel.spec.ts
diff --git a/apps/web/e2e/chat-steer-cancel.spec.ts b/apps/web/e2e/chat-steer-cancel.spec.ts
new file mode 100644
index 00000000..4bfc059f
--- /dev/null
+++ b/apps/web/e2e/chat-steer-cancel.spec.ts
@@ -0,0 +1,39 @@
+import { expect, test } from "@playwright/test";
+
+// Mid-turn steering ✕-cancel (#1103, shipped in #1104). While a turn streams, a
+// message the user submits is QUEUED as a steer (folds into the agent's work at its
+// next model call) and shown as a dimmed pending bubble with a ✕. Clicking the ✕
+// must dequeue it server-side (DELETE /api/chat/sessions/{id}/steer/{msgId}) and drop
+// the bubble — so a cancelled steer never reaches the agent. The DS `Message queued`
+// renders `.pl-message--queued` + a "Cancel queued message" button (@protolabsai/ui).
+
+test("✕ on a queued steer dequeues it and removes the pending bubble", async ({ page }) => {
+ await page.goto("/app/", { waitUntil: "load" });
+ const composer = page.getByPlaceholder(/Message protoAgent/i);
+ await expect(composer).toBeVisible();
+
+ // Start a turn the mock HOLDS OPEN, so the surface stays "streaming" — the state
+ // where Enter queues a steer instead of sending a fresh turn.
+ await composer.fill("hold the turn open");
+ await composer.press("Enter");
+
+ // The composer flips to its steering placeholder once the turn is running.
+ const steerComposer = page.getByPlaceholder(/Steer the agent/i);
+ await expect(steerComposer).toBeVisible();
+
+ // Queue a steer → an optimistic dimmed bubble with a ✕ appears.
+ await steerComposer.fill("actually, do X instead");
+ await steerComposer.press("Enter");
+ const queued = page.locator(".pl-message--queued");
+ await expect(queued).toHaveText(/do X instead/);
+
+ // Clicking ✕ must hit the dequeue endpoint — prove it, not just the optimistic drop.
+ const deleted = page.waitForRequest(
+ (r) => r.method() === "DELETE" && /\/api\/chat\/sessions\/[^/]+\/steer\/[^/]+$/.test(r.url()),
+ );
+ await page.getByRole("button", { name: "Cancel queued message" }).click();
+ await deleted;
+
+ // The bubble is gone — the steer was cancelled before the agent saw it.
+ await expect(page.locator(".pl-message--queued")).toHaveCount(0);
+});
diff --git a/apps/web/e2e/mock-server.mjs b/apps/web/e2e/mock-server.mjs
index b53480d3..be7a3dc7 100644
--- a/apps/web/e2e/mock-server.mjs
+++ b/apps/web/e2e/mock-server.mjs
@@ -279,6 +279,18 @@ async function handleA2AStream(req, res, body) {
"cache-control": "no-cache",
connection: "keep-alive",
});
+ // A turn a spec can HOLD OPEN: stream only the opening frames (so the surface
+ // enters its "streaming" / steering state) and never the terminal frame, until
+ // the client disconnects. Lets the mid-turn steering ✕-cancel e2e (#1103) keep a
+ // turn running deterministically instead of racing the ~40ms-gapped frames.
+ if (/hold the turn open/i.test(prompt)) {
+ for (const frame of frames.slice(0, 2)) {
+ res.write(`data: ${JSON.stringify(frame)}\r\n\r\n`);
+ await new Promise((r) => setTimeout(r, 40));
+ }
+ await new Promise((resolve) => req.on("close", resolve));
+ return res.end();
+ }
for (const frame of frames) {
// CRLF frame separator — the a2a-sdk emits SSE with `\r\n\r\n`, not `\n\n`.
// The mock must mirror that so this e2e exercises the real wire shape: an
@@ -393,6 +405,11 @@ const server = createServer(async (req, res) => {
const body = await readBody(req);
return sendJson(res, { ok: true, id: body.id ?? null, pending: 0 });
}
+ // Mid-turn steering cancel (the ✕ on a queued bubble) — dequeue still-queued.
+ // `removed: true` is the happy path the #1103 e2e drives (cancel before drain).
+ if (/^\/api\/chat\/sessions\/[^/]+\/steer\/[^/]+$/.test(pathname) && req.method === "DELETE") {
+ return sendJson(res, { removed: true, pending: 0 });
+ }
if (pathname === "/api/knowledge/attach" && req.method === "POST") {
// Chat attachment upload (#1002) — multipart, so DON'T JSON-parse the body.
// Drain it, then return the small-file "inline" tier with a context block.
From 188b14cca13df7abb1e48e68f1336b0ae2056980 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 11:13:55 -0700
Subject: [PATCH 007/190] docs(changelog): backfill Unreleased entries for the
0.67 batch (#1304)
Several user-facing PRs merged since v0.66.0 never added a CHANGELOG bullet, so
the Unreleased section was incomplete ahead of cutting a release. Backfill them
so the rolled changelog + marketing /changelog are accurate:
- Added: per-agent ACP launch overrides (#1289)
- Fixed: ACP probe initialize-only + nested-Claude env strip + logging (#1301),
macOS desktop sidecar PATH (#1302), workspace _pick_port skips OS-occupied
ports (#1290), per-agent theme instance-scoped (#1294), tab favicon/theme-color
follow the active theme (#1297)
- Docs: codex needs the codex-acp adapter (#1287)
Test-only PRs (#1291, #1303) are intentionally omitted.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7a1f9e8..85494722 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- **Per-agent ACP launch overrides are honored.** `acp.agents..{command,args}` is
+ now parsed (it was silently dropped — `LangGraphConfig` had no `acp_agents` field), so an
+ `agent_runtime: acp:` turn launches the locally-installed adapter you configured
+ (`claude-agent-acp`, `codex-acp`) instead of always falling back to the `npx -y …` fetch
+ default — faster cold start, no per-spawn network dependency (ADR 0033). (#1289)
+
### Fixed
+- **ACP delegate health probe is `initialize`-only — it no longer opens a session every
+ 120s.** The prober ran a full `session/new`/`session/load` against every ACP delegate on
+ a timer despite documenting itself as side-effect-free; it now runs only the `initialize`
+ round-trip (`AcpClient.handshake()`). The ACP launch env also strips the inherited
+ `CLAUDECODE` / `CLAUDE_CODE_*` markers so a spawned Claude backend doesn't refuse to start
+ "inside another Claude Code session", and the round-trip (initialize → session/prompt →
+ request_permission) is now logged so an idle freeze is diagnosable. (#1301)
+- **macOS desktop app finds Homebrew/nvm/Volta/asdf-installed binaries.** A Finder/Dock
+ launch inherits only `launchd`'s minimal PATH, so `npx` and ACP adapters were invisible
+ and a `delegate_to` coding-agent launch failed with `binary not on PATH`. The bundled
+ server is now launched with the user's real login-shell PATH, and the delegate Test probe
+ resolves the command against the same merged PATH the spawn uses. (#1302)
+- **Workspace port assignment skips OS-occupied ports.** `_pick_port` now bind-probes
+ `127.0.0.1` and scans a bounded range, skipping ports held by *unrelated* processes (a dev
+ server, another fork on `:7871`) instead of handing out an already-bound port that killed
+ the spawned agent with `EADDRINUSE`. (#1290)
+- **The per-agent theme is instance-scoped.** `theme.json` now lands under
+ `config//` like config/secrets/setup, so a `PROTOAGENT_INSTANCE` sandbox no
+ longer shares — and clobbers — the default instance's theme (ADR 0042). (#1294)
+- **The browser tab favicon + `theme-color` follow the active per-agent theme.** Switching
+ agents recolors the tab favicon and PWA/mobile browser chrome to the agent's accent
+ instead of always showing the brand default (ADR 0042). (#1297)
- **ACP coding-agent subprocesses no longer leak as orphaned processes.** `delegate_to`
and the delegate health prober spawn CLI coding agents (`codex-acp`, `claude-agent-acp`,
…) over ACP, but teardown signalled only the direct child — the backend each adapter
@@ -31,6 +60,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
dropped the now-redundant per-dialog overrides. Surfaces that embed a full panel
(Settings, theme quick-pick) keep their intentional zero padding. (#1288)
+### Docs
+- **Coding-agents guide: Codex needs the `codex-acp` adapter.** Documented that recent
+ `codex` CLI dropped the native `acp` subcommand (it speaks MCP), so it must be driven
+ through `@zed-industries/codex-acp`. (#1287)
+
## [0.66.0] - 2026-06-21
### Changed
From d0026aa0e1b4906a1c453c6f1c68b9d4abd4405e Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 11:21:47 -0700
Subject: [PATCH 008/190] chore: release v0.67.0 (#1305)
Co-authored-by: github-actions[bot]
---
CHANGELOG.md | 2 ++
pyproject.toml | 2 +-
sites/marketing/data/changelog.json | 15 +++++++++++++++
3 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85494722..33ea5587 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.67.0] - 2026-06-22
+
### Added
- **Per-agent ACP launch overrides are honored.** `acp.agents..{command,args}` is
now parsed (it was silently dropped — `LangGraphConfig` had no `acp_agents` field), so an
diff --git a/pyproject.toml b/pyproject.toml
index aee1bcf3..49e0695c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "protoagent"
-version = "0.66.0"
+version = "0.67.0"
description = "protoAgent — LangGraph + A2A template for spawning protoLabs agents"
requires-python = ">=3.11"
diff --git a/sites/marketing/data/changelog.json b/sites/marketing/data/changelog.json
index e02705bd..7fd422ee 100644
--- a/sites/marketing/data/changelog.json
+++ b/sites/marketing/data/changelog.json
@@ -1,4 +1,19 @@
[
+ {
+ "version": "v0.67.0",
+ "date": "2026-06-22",
+ "changes": [
+ "Per-agent ACP launch overrides are honored.",
+ "ACP delegate health probe is initialize-only — it no longer opens a session every 120s.",
+ "macOS desktop app finds Homebrew/nvm/Volta/asdf-installed binaries.",
+ "Workspace port assignment skips OS-occupied ports.",
+ "The per-agent theme is instance-scoped.",
+ "The browser tab favicon + theme-color follow the active per-agent theme.",
+ "ACP coding-agent subprocesses no longer leak as orphaned processes.",
+ "Dialogs no longer render their content cramped flush to the body edge.",
+ "Coding-agents guide: Codex needs the codex-acp adapter."
+ ]
+ },
{
"version": "v0.66.0",
"date": "2026-06-21",
From 824e492ba596eb99b81e04a4823b68555c61ee03 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 11:54:31 -0700
Subject: [PATCH 009/190] ci(web-e2e): cache Playwright browsers +
timeout/retry the install (fixes #1167) (#1306)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Web E2E smoke job ran `playwright install --with-deps chromium` on every run
with no cache, no timeout, and no retry — so a stalled CDN download for the ~100MB
chromium binary hung the step ~10min until the job-level orphan-kill fired, a
recurring false-red that cost a manual `gh run rerun --failed` each hit.
- Cache `~/.cache/ms-playwright` keyed on the resolved Playwright version; on a hit
skip the binary download and run only the fast apt system-deps (`install-deps`).
- Hard-cap each attempt with `timeout 240` and retry up to 3× so a stalled download
fails fast and self-heals instead of hanging to the orphan-kill; step capped at
`timeout-minutes: 8`.
Co-authored-by: Claude Opus 4.8 (1M context)
---
.github/workflows/checks.yml | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 8ef74109..49cd84d0 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -151,8 +151,37 @@ jobs:
# the uiStore persist migration). Fast — runs before the heavier
# Playwright path so a reducer regression fails the job early.
run: npm run test:unit --workspace @protoagent/web
- - name: Install Playwright chromium
- run: npm --workspace @protoagent/web exec -- playwright install --with-deps chromium
+ - name: Resolve Playwright version
+ id: pw
+ run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
+ - name: Cache Playwright browsers
+ id: pw-cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ runner.os }}-${{ steps.pw.outputs.version }}
+ - name: Install Playwright browser + deps
+ # The chromium binary is the slow ~100MB CDN download that intermittently
+ # stalled ~10min until the job-level orphan-kill fired (#1167). Cache it keyed
+ # on the Playwright version (skips the download entirely on a hit — only the
+ # fast apt system-deps run then), and hard-cap each attempt with `timeout` so
+ # a stalled download fails fast and retries instead of hanging the whole job.
+ timeout-minutes: 8
+ run: |
+ if [ "${{ steps.pw-cache.outputs.cache-hit }}" = "true" ]; then
+ cmd="playwright install-deps chromium" # binary cached → apt deps only
+ else
+ cmd="playwright install --with-deps chromium"
+ fi
+ for attempt in 1 2 3; do
+ if timeout 240 npm --workspace @protoagent/web exec -- $cmd; then
+ exit 0
+ fi
+ echo "::warning::playwright install attempt $attempt stalled/failed; retrying…"
+ sleep 5
+ done
+ echo "::error::playwright install failed after 3 attempts"
+ exit 1
- name: Run E2E smoke (built SPA vs mock backend)
# Builds the web app, then drives it with Playwright against the
# deterministic mock backend (apps/web/e2e/mock-server.mjs) — no Python,
From c3b2a231aca685bf317fa02d99ce79d87d9f4750 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 12:00:16 -0700
Subject: [PATCH 010/190] fix(docker): multi-stage console build + dep-drift
guard (closes #874) (#1309)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Problem 1 — the Docker image never shipped the React console. apps/web/dist
is .gitignored, the Dockerfile had no Node stage, and .dockerignore dropped
the npm workspace manifests, so COPY shipped no built console and
mount_react_app silently returned False — `-e PROTOAGENT_UI=console` 404'd at
/app. Added a node:20-slim web-builder stage (npm ci + npm run build --workspace
@protoagent/web) and COPY --from into the runtime image; unblocked the package
manifests in .dockerignore (node_modules still excluded, so they never reach the
final image); and made the server warn LOUDLY when the console tier is requested
but the build is absent (was a single quiet log line).
Problem 2 — requirements-core.txt (what the image installs) drifted from the
pyproject [project.dependencies] source of truth: it had silently lost pypdf,
youtube-transcript-api, and markdown-it-py, so the prod image lacked the
ingestion deps while CI (which installs via pyproject) stayed green. Restored
the three deps and added tests/test_requirements_core_sync.py, which fails CI if
requirements-core.txt misses any core pyproject dependency.
Verified with a real `docker build` (web-builder + full image) on Docker
28.5.1: the running container serves /app (200 + HTML), /app/assets/*.js (200),
and /_ds/plugin-kit.css (200); the ingestion deps import; no node_modules leak
into the final image.
Co-authored-by: Claude Opus 4.8 (1M context)
---
.dockerignore | 10 ++-
CHANGELOG.md | 11 +++
Dockerfile | 46 +++++++++-
requirements-core.txt | 8 ++
server/__init__.py | 19 +++-
tests/test_requirements_core_sync.py | 126 +++++++++++++++++++++++++++
6 files changed, 212 insertions(+), 8 deletions(-)
create mode 100644 tests/test_requirements_core_sync.py
diff --git a/.dockerignore b/.dockerignore
index de3b6efd..34031308 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -41,9 +41,15 @@ tests/
# Docs site — VitePress builds to gh-pages, not into the container
docs/
+
+# node_modules NEVER enter the build context — the web-builder stage runs a fresh
+# `npm ci` and the python runtime installs via pip, so a host's node_modules would
+# only bloat the context and risk leaking platform-specific binaries.
node_modules/
-package.json
-package-lock.json
+# NOTE: package.json / package-lock.json are deliberately NOT excluded — the
+# web-builder stage COPYs them to run `npm ci` and build the React console
+# (apps/web/dist). Re-excluding them breaks that stage (the #874 console-never-
+# ships bug). They're harmless in the runtime image (Python ignores them).
# Seccomp profile — Docker reads this on the host at runtime
# (via docker-compose security_opt), NOT from inside the image
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33ea5587..8582b63d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Fixed
+- **The Docker image now serves the React console and stays in dep-lockstep with
+ pyproject.** A new node builder stage builds `apps/web/dist` and copies it into the
+ runtime image, so `-e PROTOAGENT_UI=console` actually mounts `/app` instead of silently
+ 404'ing (`.dockerignore` no longer drops the workspace manifests the build needs); the
+ server now warns loudly when the `console` tier is requested but the console build is
+ absent. A new `tests/test_requirements_core_sync.py` guard fails CI if
+ `requirements-core.txt` (what the image installs) misses any core `pyproject`
+ dependency — it had silently lost `pypdf`, `youtube-transcript-api`, and
+ `markdown-it-py`, now restored. (#874)
+
## [0.67.0] - 2026-06-22
### Added
diff --git a/Dockerfile b/Dockerfile
index d6d0d956..e507e1a6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,32 @@
+# ---------------------------------------------------------------------------
+# Stage 1 — node builder: build the React operator console (apps/web → dist/).
+#
+# `apps/web/dist` is .gitignored (build output, never committed), so the final
+# image can't just `COPY` it from the context — it has to be BUILT here. Without
+# this stage the `console` tier 404s at /app: mount_react_app finds no
+# dist/index.html and silently returns False (the #874 bug). We build only the
+# `@protoagent/web` workspace; `npm ci` at the root resolves the npm workspaces
+# (web + desktop), but the desktop workspace only pulls a JS CLI, so the install
+# stays light and these node_modules never reach the final image.
+# ---------------------------------------------------------------------------
+FROM node:20-slim AS web-builder
+WORKDIR /build
+# Copy only what the web build needs (lockfile-first so this layer caches across
+# source-only churn): the workspace manifests + lockfile, then the app sources.
+# The root + both workspace package.json files are required for `npm ci` to
+# reconstruct the workspace tree the committed package-lock.json describes.
+COPY package.json package-lock.json ./
+COPY apps/web/package.json apps/web/
+COPY apps/desktop/package.json apps/desktop/
+RUN npm ci
+# `prebuild` (check-css-comments + copy-plugin-kit) + `build` (tsc typecheck +
+# vite build) — emits apps/web/dist/, including dist/_ds/ from the plugin-kit.
+COPY apps/web/ apps/web/
+RUN npm run build --workspace @protoagent/web
+
+# ---------------------------------------------------------------------------
+# Stage 2 — python runtime: the agent core + the built console copied in.
+# ---------------------------------------------------------------------------
FROM python:3.12-slim
# System deps. iproute2 is required when running under NVIDIA OpenShell —
@@ -55,10 +84,12 @@ RUN useradd -m -s /bin/bash -u ${SANDBOX_UID} sandbox
# add them to requirements.txt.
# UI tier (ADR 0010): the build-arg bakes PROTOAGENT_UI so the image runs the
# matching tier — default 'none' (API + A2A + /metrics only, the lean headless
-# image) or 'console' (also serves the React console, which ships as COPY'd static
-# assets, not a pip dep). Either tier uses the same lean core deps, so the install
-# is unconditional; forks that need extras (the google/Discord MCP surfaces,
-# agent-browser, …) add them to requirements-core.txt (note above).
+# image) or 'console' (also serves the React console, built in the web-builder
+# stage above and copied in below as static assets, not a pip dep). Either tier
+# uses the same lean core deps — and the console dist always ships regardless of
+# UI — so the install is unconditional; forks that need extras (the
+# google/Discord MCP surfaces, agent-browser, …) add them to
+# requirements-core.txt (note above).
ARG UI=none
COPY requirements*.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/requirements-core.txt
@@ -69,6 +100,13 @@ RUN pip install --no-cache-dir -r /tmp/requirements-core.txt
COPY . /opt/protoagent/
RUN chmod +x /opt/protoagent/entrypoint.sh
+# The React console — copied from the node builder stage, NOT from the build
+# context (apps/web/dist is .gitignored and node_modules is .dockerignore'd, so
+# the source COPY above ships no built console). This is what makes the
+# `console` tier serve /app instead of 404'ing (the #874 bug). Only the static
+# dist/ lands here — the builder's node_modules stay in the discarded stage.
+COPY --from=web-builder /build/apps/web/dist /opt/protoagent/apps/web/dist
+
# Sandbox workspace + knowledge/audit dirs
RUN mkdir -p /sandbox /tmp/sandbox /sandbox/audit /sandbox/knowledge \
&& chown -R sandbox:sandbox /sandbox /tmp/sandbox
diff --git a/requirements-core.txt b/requirements-core.txt
index 3a9433cd..419ce791 100644
--- a/requirements-core.txt
+++ b/requirements-core.txt
@@ -63,3 +63,11 @@ croniter>=2.0
# via Gradio → huggingface_hub; the lean tiers must declare it (the #426 class).
filelock>=3.12
zeroconf>=0.131 # mDNS fleet discovery (ADR 0042 §I)
+
+# Document ingestion engine (ADR 0021) — light pure-Python extractors. Declared
+# in pyproject [project.dependencies] (the source of truth); mirrored here so the
+# Docker image (which installs from this file) matches. tests/test_requirements_core_sync.py
+# fails CI if these two lists drift again (the #874 dep-drift class).
+pypdf>=4.0 # PDF → text
+youtube-transcript-api>=1.0 # YouTube captions → text (1.x instance API)
+markdown-it-py>=3.0 # Markdown → HTML for the `docs` plugin reader view (server-side, offline)
diff --git a/server/__init__.py b/server/__init__.py
index 901a3680..c25c7caf 100644
--- a/server/__init__.py
+++ b/server/__init__.py
@@ -799,8 +799,23 @@ async def _prometheus_metrics():
mount_ds_plugin_kit(fastapi_app, web_dist_dir)
# The console SPA (/app) — console/full tiers only; 'none' (members/headless) skip it.
- if ui != "none" and mount_react_app(fastapi_app, web_dist_dir):
- log.info("React operator console mounted at /app")
+ if ui != "none":
+ if mount_react_app(fastapi_app, web_dist_dir):
+ log.info("React operator console mounted at /app")
+ else:
+ # The console tier was requested but the build output is missing — /app
+ # would silently 404 (the #874 footgun: a no-Node Docker image, or a
+ # source checkout that never ran `npm run build`). Warn LOUDLY with the
+ # exact fix instead of a single quiet log line.
+ log.warning(
+ "--ui %s requested but the React console build is missing at %s — "
+ "/app will 404. Build it with `npm ci && npm run build --workspace "
+ "@protoagent/web` (or use a Docker image built from the multi-stage "
+ "Dockerfile, which builds it for you). Use `--ui none` to run headless "
+ "without the console.",
+ ui,
+ web_dist_dir,
+ )
# --- Static + PWA assets (skipped in 'none') ---------------------------
if ui != "none" and static_dir.exists():
diff --git a/tests/test_requirements_core_sync.py b/tests/test_requirements_core_sync.py
new file mode 100644
index 00000000..383813c3
--- /dev/null
+++ b/tests/test_requirements_core_sync.py
@@ -0,0 +1,126 @@
+"""Drift guard: requirements-core.txt ⊇ the core pyproject [project.dependencies].
+
+``pyproject.toml [project.dependencies]`` is the single source of truth for the
+runtime deps (``pip install .`` / ``uv sync`` read it). But the Docker image
+installs from ``requirements-core.txt`` (``pip install -r requirements-core.txt``),
+which is hand-mirrored — so a dep added only to pyproject passes every CI gate
+(tests run against the pyproject install) yet silently MISSES the production
+image. That's the #874 dep-drift class: the runtime image quietly lacks a package
+the agent imports, and the failure only surfaces in production.
+
+This test makes that drift a CI failure: every package name declared in
+``[project.dependencies]`` must also appear in ``requirements-core.txt``. It
+compares by *normalized package name* (PEP 503) only — version pins and extras
+are formatting and intentionally NOT asserted, so a ``>=`` vs ``==`` or an extras
+list difference doesn't cause false failures.
+
+CORE-SUBSET DEFINITION: today the ENTIRE ``[project.dependencies]`` table IS the
+core tier — pyproject's own comment marks it "core (the `none`/`console` tiers) —
+mirrors requirements-core.txt", and there is no separate full-tier section. If a
+full-tier-only section is ever added to pyproject, mark those deps so they can be
+excluded from the core subset here (see ``_FULL_TIER_ONLY`` below).
+"""
+
+from __future__ import annotations
+
+import sys
+import tomllib
+from pathlib import Path
+
+from packaging.requirements import InvalidRequirement, Requirement
+from packaging.utils import canonicalize_name
+
+_REPO_ROOT = Path(__file__).resolve().parent.parent
+_PYPROJECT = _REPO_ROOT / "pyproject.toml"
+_REQUIREMENTS_CORE = _REPO_ROOT / "requirements-core.txt"
+
+# Full-tier-only deps to exclude from the "core subset". Empty today — the whole
+# [project.dependencies] table is the core tier (the lean none/console tiers).
+# If a full-only section is added to pyproject, list its package names here (PEP
+# 503 canonical form) with a comment, so this guard only enforces the core subset.
+_FULL_TIER_ONLY: set[str] = set()
+
+
+def _canonical_names(specifiers: list[str]) -> set[str]:
+ """Normalize a list of PEP 508 requirement strings to canonical package names.
+
+ Skips blank lines / comments and tolerates direct git references (``pkg @
+ git+https://…``), which ``packaging.requirements.Requirement`` parses fine.
+ """
+ names: set[str] = set()
+ for raw in specifiers:
+ line = raw.strip()
+ if not line or line.startswith("#"):
+ continue
+ # Strip trailing inline comments (`foo>=1 # why`) — not part of the spec.
+ line = line.split("#", 1)[0].strip()
+ if not line:
+ continue
+ try:
+ names.add(canonicalize_name(Requirement(line).name))
+ except InvalidRequirement:
+ # Non-requirement lines (e.g. `-e .`, `-r other.txt`) aren't package
+ # specs — ignore them; this guard is about named-package coverage.
+ continue
+ return names
+
+
+def _pyproject_core_deps() -> set[str]:
+ data = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8"))
+ deps = data["project"]["dependencies"]
+ return _canonical_names(deps) - _FULL_TIER_ONLY
+
+
+def _requirements_core_deps() -> set[str]:
+ lines = _REQUIREMENTS_CORE.read_text(encoding="utf-8").splitlines()
+ return _canonical_names(lines)
+
+
+def test_requirements_core_covers_pyproject_core_dependencies() -> None:
+ """Every core ``[project.dependencies]`` package must be in requirements-core.txt.
+
+ A package present in pyproject but absent here means the Docker image (which
+ installs from requirements-core.txt) ships without it — the silent prod gap.
+ """
+ pyproject = _pyproject_core_deps()
+ req_core = _requirements_core_deps()
+ missing = pyproject - req_core
+ assert not missing, (
+ "requirements-core.txt is missing core deps declared in pyproject.toml "
+ f"[project.dependencies]: {sorted(missing)}. Add them to requirements-core.txt "
+ "(the Docker image installs from that file) — or, if one is genuinely "
+ "full-tier-only, add it to _FULL_TIER_ONLY with a reason."
+ )
+
+
+def test_requirements_core_has_no_unknown_named_packages() -> None:
+ """Belt-and-suspenders: requirements-core.txt declares no NAMED package that
+ isn't in pyproject's core deps.
+
+ Keeps the mirror tight in both directions — a stray pin in requirements-core.txt
+ that no longer exists in the source of truth surfaces here rather than lingering.
+ (pyproject is authoritative; this catches the reverse drift.)
+ """
+ pyproject = _pyproject_core_deps()
+ req_core = _requirements_core_deps()
+ extra = req_core - pyproject - _FULL_TIER_ONLY
+ assert not extra, (
+ "requirements-core.txt declares package(s) not in pyproject.toml "
+ f"[project.dependencies]: {sorted(extra)}. pyproject is the source of truth — "
+ "add them there too, or remove the stale line from requirements-core.txt."
+ )
+
+
+def test_parser_skips_non_package_lines() -> None:
+ """The name extractor ignores comments, blanks, and pip directives (`-e .`)."""
+ assert _canonical_names(["", "# comment", "-e .", "-r other.txt"]) == set()
+ # canonicalize_name (PEP 503): lowercase + collapse runs of -_. to a single '-'.
+ assert _canonical_names(["Foo.Bar_Baz>=1.0 # inline"]) == {"foo-bar-baz"}
+ assert _canonical_names(["pkg @ git+https://example.com/pkg.git@v1"]) == {"pkg"}
+
+
+# Guard the floor: tomllib is stdlib on 3.11+, which is requires-python. If this
+# ever runs on an older interpreter the import above would already have failed —
+# this assertion documents the expectation explicitly.
+def test_running_on_supported_python() -> None:
+ assert sys.version_info >= (3, 11), "requires-python is >=3.11 (tomllib is stdlib there)"
From ae8632a1e22d6d0cfa513880ff785230d17f40a7 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 12:06:42 -0700
Subject: [PATCH 011/190] perf: off-loop console handlers + inbox/activity
retention (#875) (#1308)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* perf: off-loop console handlers + inbox/activity retention (#875)
Address the SAFE subset of the perf-audit tail (#875):
- Item 8: `_operator_runtime_status` now awaits `asyncio.to_thread` for the
per-poll co-location (`ps` shell-out) and fleet version-skew probes, matching
the startup-path co-location check. The `/api/runtime/status` route awaits a
coroutine accessor and still accepts a sync dict (test doubles / forks).
- Item 7: the inbox/activity console handlers (`_operator_activity_list`,
`_operator_inbox_add`, `_operator_inbox_list`, `_operator_inbox_deliver`)
offload their sync SQLite store calls via `asyncio.to_thread`, mirroring the
scheduler/goals neighbors. Return shapes unchanged.
Item 2 (inbox/activity retention) was already shipped on main (#1059); items 5
and 9 are deferred; items 1/3/4 were already done.
Co-Authored-By: Claude Opus 4.8 (1M context)
* chore(changelog): trim #875 bullet — retention pruning already shipped (v0.42.0/#1059)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 5 ++++
operator_api/console_handlers.py | 42 ++++++++++++++++++++++++-------
operator_api/routes.py | 8 ++++--
tests/test_operator_api_routes.py | 20 +++++++++++++++
4 files changed, 64 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8582b63d..0a9e6a15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
+- **Console-poll handlers no longer block the event loop.** `GET /api/runtime/status`
+ shelled out to `ps` (the per-poll co-location + fleet version-skew probes) and the
+ inbox/activity console handlers ran sync SQLite reads/writes directly on the loop; both
+ are now offloaded via `asyncio.to_thread`, matching the scheduler/goals handlers and the
+ startup-path co-location check. (#875)
- **The Docker image now serves the React console and stays in dep-lockstep with
pyproject.** A new node builder stage builds `apps/web/dist` and copies it into the
runtime image, so `-e PROTOAGENT_UI=console` actually mounts `/app` instead of silently
diff --git a/operator_api/console_handlers.py b/operator_api/console_handlers.py
index 9fd37bb9..e80dbba6 100644
--- a/operator_api/console_handlers.py
+++ b/operator_api/console_handlers.py
@@ -47,24 +47,30 @@ def _operator_allowed_dirs() -> list[str]:
return list(dict.fromkeys(roots))
-def _operator_runtime_status():
+async def _operator_runtime_status():
+ import asyncio
+
# Live co-location check (#706) — re-evaluated per poll so the shell banner
# appears/clears as siblings come and go. Quiet (empty `.instances/`) costs one
# is_dir(); the `ps` guard only runs when sibling heartbeats actually exist.
+ # The probe shells out to `ps` per sibling, so it's offloaded off the event
+ # loop (#875) — matching the startup-path co-location check in server._main.
from infra.paths import colocation_warning, instance_uid, package_version
try:
- warnings = [w for w in (colocation_warning(),) if w]
+ warn = await asyncio.to_thread(colocation_warning)
+ warnings = [warn] if warn else []
except Exception: # noqa: BLE001 — status must never raise
warnings = []
# Fleet version skew (version-coherence P2) — also live + self-clearing: a
# member that survived an app update keeps running the OLD binary until
# restarted; banner it the same way as a co-located sibling. Inside a member
- # the scoped fleet.json is empty, so this no-ops.
+ # the scoped fleet.json is empty, so this no-ops. It probes sibling liveness
+ # (a `ps` shell-out under the hood), so it's offloaded off the loop too (#875).
try:
from graph.fleet import supervisor as _sup
- skew = _sup.version_skew_warning()
+ skew = await asyncio.to_thread(_sup.version_skew_warning)
if skew:
warnings.append(skew)
except Exception: # noqa: BLE001 — status must never raise
@@ -341,7 +347,13 @@ async def _operator_activity_list() -> dict:
with origin/trigger/priority — plus the thread's message history from the
checkpointer (for the continue view). The console renders the feed and
opens the thread on demand."""
- entries = STATE.activity_log.recent(limit=100) if STATE.activity_log is not None else []
+ import asyncio
+
+ # recent() is sync sqlite — offload it off the loop (#875), mirroring the
+ # scheduler/goals handlers above.
+ entries = (
+ await asyncio.to_thread(STATE.activity_log.recent, 100) if STATE.activity_log is not None else []
+ )
messages: list[dict] = []
if STATE.checkpointer is not None:
thread_id = f"a2a:{ACTIVITY_CONTEXT}"
@@ -439,9 +451,13 @@ async def _fire_activity_from_inbox(item: dict) -> bool:
async def _operator_inbox_add(payload: dict) -> dict:
"""Ingest an inbound item (ADR 0003). now-priority fires an Activity turn;
others queue for check_inbox. Dedup is handled by the store."""
+ import asyncio
+
if STATE.inbox_store is None:
raise RuntimeError("inbox not loaded; finish setup first")
- item = STATE.inbox_store.add(
+ # add() is sync sqlite — offload it off the loop (#875).
+ item = await asyncio.to_thread(
+ STATE.inbox_store.add,
payload.get("text", ""),
priority=payload.get("priority", "next") or "next",
source=payload.get("source", "") or "",
@@ -465,16 +481,20 @@ async def _operator_inbox_add(payload: dict) -> dict:
# re-surfaced by the next check_inbox (bd-jus). A FAILED fire stays pending
# so check_inbox remains the fallback delivery path for it.
try:
- STATE.inbox_store.mark_delivered([item["id"]])
+ await asyncio.to_thread(STATE.inbox_store.mark_delivered, [item["id"]])
except Exception: # noqa: BLE001 — best-effort; a missed mark just re-surfaces
log.warning("[inbox] could not mark fired now-item %s delivered", item.get("id"))
return {"ok": True, "item": item, "fired": fired}
async def _operator_inbox_list(floor: str, include_delivered: bool) -> dict:
+ import asyncio
+
if STATE.inbox_store is None:
return {"items": []}
- items = STATE.inbox_store.list(
+ # list() is sync sqlite — offload it off the loop (#875).
+ items = await asyncio.to_thread(
+ STATE.inbox_store.list,
priority_floor=floor or "later",
include_delivered=include_delivered,
limit=200,
@@ -483,9 +503,13 @@ async def _operator_inbox_list(floor: str, include_delivered: bool) -> dict:
async def _operator_inbox_deliver(item_id: int) -> dict:
+ import asyncio
+
if STATE.inbox_store is None:
raise RuntimeError("inbox not loaded; finish setup first")
- return {"ok": True, "delivered": STATE.inbox_store.mark_delivered([item_id])}
+ # mark_delivered() is sync sqlite — offload it off the loop (#875).
+ delivered = await asyncio.to_thread(STATE.inbox_store.mark_delivered, [item_id])
+ return {"ok": True, "delivered": delivered}
def _operator_chat_commands() -> dict:
diff --git a/operator_api/routes.py b/operator_api/routes.py
index 209e0c9c..910fad0f 100644
--- a/operator_api/routes.py
+++ b/operator_api/routes.py
@@ -170,7 +170,7 @@ def delete(self, project_path: str, issue_id: str) -> dict[str, Any]:
def register_operator_routes(
app,
*,
- runtime_status: Callable[[], dict[str, Any]],
+ runtime_status: Callable[[], dict[str, Any] | Awaitable[dict[str, Any]]],
subagent_list: Callable[[], list[dict[str, Any]]],
tools_list: Callable[[], dict[str, Any]] = lambda: {"tools": [], "count": 0},
subagent_run: Callable[[dict[str, Any]], Awaitable[str]],
@@ -205,7 +205,11 @@ def register_operator_routes(
@app.get("/api/runtime/status")
async def _runtime_status():
- return runtime_status()
+ # The console handler is async (it offloads the per-poll `ps` co-location
+ # probe off the loop, #875); accept a plain dict too so sync test doubles
+ # and forks that wire a sync accessor keep working.
+ res = runtime_status()
+ return await res if asyncio.iscoroutine(res) else res
@app.get("/api/subagents")
async def _subagents():
diff --git a/tests/test_operator_api_routes.py b/tests/test_operator_api_routes.py
index db8b3e54..d4757bbb 100644
--- a/tests/test_operator_api_routes.py
+++ b/tests/test_operator_api_routes.py
@@ -64,6 +64,26 @@ def test_tasks_store_route_ignores_project_path() -> None:
assert client.get("/api/tasks/issues").status_code == 200
+def test_runtime_status_accepts_async_accessor() -> None:
+ """The real console handler is async (#875 offloads the per-poll `ps`
+ co-location probe off the loop); the route must await a coroutine accessor
+ while still accepting a plain-dict (sync) one for forks/test doubles."""
+ app = FastAPI()
+
+ async def _async_status():
+ return {"graph_loaded": True, "async": True}
+
+ register_operator_routes(
+ app,
+ runtime_status=_async_status,
+ subagent_list=lambda: [],
+ subagent_run=lambda req: "",
+ subagent_batch=lambda req: "",
+ tasks_store=_FakeTaskStore(),
+ )
+ assert TestClient(app).get("/api/runtime/status").json() == {"graph_loaded": True, "async": True}
+
+
def test_operator_routes_return_expected_shapes(tmp_path) -> None:
client = _client()
From 1320d7a44f9e90429781059ae74b828e82be4a50 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 12:17:17 -0700
Subject: [PATCH 012/190] fix(console): disable rail panel-toggle buttons when
the rail is empty (#1234) (#1307)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The utility-bar left/right panel-toggle buttons now disable (greyed out,
non-interactive, with a "No panels in the rail" title) when their rail
holds no views — matching the bottom-dock toggle's existing gate, instead of
appearing active but doing nothing when clicked.
- App.tsx: gate the left/right toggles on `leftMembers.length === 0` /
`rightMembers.length === 0` (the rail's resolved view set, already computed).
- theme.css: add a `.util-btn:disabled` rule (opacity + default cursor) and
guard `:hover` against disabled buttons, so a disabled toggle is visibly
greyed and shows no hover feedback (also tidies the bottom toggle).
- layout.spec.ts: assert the populated left/right toggles stay enabled and
interactive (the gate doesn't over-disable).
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 1 +
apps/web/e2e/layout.spec.ts | 24 ++++++++++++++++++++++++
apps/web/src/app/App.tsx | 18 ++++++++++++++++--
apps/web/src/app/theme.css | 9 ++++++++-
4 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a9e6a15..09789a44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
+- **Empty-rail panel toggles fully disable.** The utility-bar left/right panel-toggle buttons are now greyed out and non-interactive when their rail holds no views (matching the bottom-dock toggle), instead of appearing active but doing nothing when clicked. (#1234)
- **Console-poll handlers no longer block the event loop.** `GET /api/runtime/status`
shelled out to `ps` (the per-poll co-location + fleet version-skew probes) and the
inbox/activity console handlers ran sync SQLite reads/writes directly on the loop; both
diff --git a/apps/web/e2e/layout.spec.ts b/apps/web/e2e/layout.spec.ts
index 416c3395..46113ed0 100644
--- a/apps/web/e2e/layout.spec.ts
+++ b/apps/web/e2e/layout.spec.ts
@@ -89,3 +89,27 @@ test("the bottom-panel toggle sits with the layout buttons, gated until a surfac
// Disabled by default — nothing is docked at the bottom (railOrder.bottom is empty).
await expect(toggleBottom).toBeDisabled();
});
+
+// #1234: each rail toggle is gated on whether its rail holds any views — the same
+// gate the bottom toggle already had, extended to left/right. In the default layout
+// the left rail (chat/knowledge) and right rail (work + the boardy "scratch" right
+// panel) are populated, so their toggles stay ENABLED and interactive; only the
+// empty bottom dock is disabled. This pins that the gate doesn't over-disable a
+// populated rail. (An empty left/right rail can't be seeded in this harness — the
+// on-mount core self-heal re-adds chat/work and the mock's right-placed plugin view
+// re-seeds the right rail — so the disabled state is covered for the bottom dock
+// above and verified visually for left/right per the PR.)
+test("populated left/right rail toggles stay enabled and interactive (#1234)", async ({ page }) => {
+ await page.goto("/app/", { waitUntil: "load" });
+ const toggleLeft = page.getByTestId("toggle-left");
+ const toggleRight = page.getByTestId("toggle-right");
+ await expect(toggleLeft).toBeEnabled();
+ await expect(toggleRight).toBeEnabled();
+ // Still toggles the right column (proves the gate left an enabled rail interactive).
+ const right = page.locator(".pl-appshell__col--right");
+ await expect(right).toBeVisible();
+ await toggleRight.click();
+ await expect(right).toHaveCount(0);
+ await toggleRight.click();
+ await expect(right).toBeVisible();
+});
diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx
index a2d862de..032b88d1 100644
--- a/apps/web/src/app/App.tsx
+++ b/apps/web/src/app/App.tsx
@@ -955,7 +955,14 @@ export function App() {
type="button"
className={`util-btn ${leftCollapsed ? "is-off" : ""}`}
onClick={() => setLeftCollapsed(!leftCollapsed)}
- title={leftCollapsed ? "Show left panel" : "Hide left panel"}
+ disabled={leftMembers.length === 0}
+ title={
+ leftMembers.length === 0
+ ? "No panels in the left rail"
+ : leftCollapsed
+ ? "Show left panel"
+ : "Hide left panel"
+ }
aria-label="Toggle left panel"
data-testid="toggle-left"
>
@@ -965,7 +972,14 @@ export function App() {
type="button"
className={`util-btn ${rightCollapsed ? "is-off" : ""}`}
onClick={() => setRightCollapsed(!rightCollapsed)}
- title={rightCollapsed ? "Show side panel" : "Hide side panel"}
+ disabled={rightMembers.length === 0}
+ title={
+ rightMembers.length === 0
+ ? "No panels in the right rail"
+ : rightCollapsed
+ ? "Show side panel"
+ : "Hide side panel"
+ }
aria-label="Toggle side panel"
data-testid="toggle-right"
>
diff --git a/apps/web/src/app/theme.css b/apps/web/src/app/theme.css
index abc1d589..18ef562b 100644
--- a/apps/web/src/app/theme.css
+++ b/apps/web/src/app/theme.css
@@ -106,11 +106,18 @@
cursor: pointer;
}
-.util-btn:hover {
+.util-btn:not(:disabled):hover {
background: var(--bg-elevated, #1a1a1f);
color: var(--fg);
}
+/* Fully disabled toggle (#1234): the rail/dock it controls has no views, so the
+ button is greyed out and non-interactive (no hover feedback, no pointer). */
+.util-btn:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+
.util-btn svg {
flex: none;
}
From 3dc845d77c37d630982f7383ca904c37b204ec59 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 12:35:19 -0700
Subject: [PATCH 013/190] =?UTF-8?q?perf(chat):=20incremental=20mid-stream?=
=?UTF-8?q?=20output/reasoning=20views=20=E2=80=94=20kill=20the=20O(N?=
=?UTF-8?q?=C2=B2)=20rescan=20(#1310)=20(#1311)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`_chat_langgraph_stream` recomputed the visible `` and the live reasoning
view by re-running regexes over the ENTIRE accumulated text on every token chunk, so
a turn streaming N chars over ~N chunks cost O(N²) (`stream_visible_output` /
`stream_visible_reasoning` over `accumulated_raw` each step).
Add `StreamingOutputView` / `StreamingReasoningView` (graph/output_format.py): same
"full visible-so-far" contract, but they scan only the newly-appended tail —
- a cheap tail-only scan for the `` / `` opener (the scratch_pad
is never re-scanned), returning "" meanwhile;
- a fast-append in the steady answer-body stream (open, unclosed, no `<` in the region);
- a fall back to the authoritative pure function on any `<` (close / `` /
`` / partial tag) or the closed state.
The pure functions stay as the oracle: a parametrized equivalence test (curated cases
× chunk sizes 1..50) plus a 300-doc random fuzz test assert the views return EXACTLY
what the pure functions return at every growing prefix — so downstream streaming
behavior is byte-identical, just ~O(N).
Addresses the O(N²) item of #1310; the goals/beads polling→event-bus item remains
(it needs backend task-change events first).
ruff + lint-imports clean; full suite (2503) + live smoke green.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 7 +++
graph/output_format.py | 112 ++++++++++++++++++++++++++++++++++++
server/chat.py | 13 +++--
tests/test_output_format.py | 66 +++++++++++++++++++++
4 files changed, 194 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09789a44..68cc6d11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
+- **Mid-stream output rendering no longer rescans the whole response on every chunk.**
+ The chat stream recomputed the visible `` (and the live reasoning view) by
+ re-running regexes over the *entire* accumulated text per token chunk — O(N²) over a
+ turn. New incremental `StreamingOutputView` / `StreamingReasoningView` scan only the
+ newly-appended tail (a cheap pre-`` scan, then fast-append in the steady
+ answer body), falling back to the authoritative parser on any tag boundary; an
+ equivalence + fuzz test pins them byte-for-byte to the original functions. (#1310)
- **Empty-rail panel toggles fully disable.** The utility-bar left/right panel-toggle buttons are now greyed out and non-interactive when their rail holds no views (matching the bottom-dock toggle), instead of appearing active but doing nothing when clicked. (#1234)
- **Console-poll handlers no longer block the event loop.** `GET /api/runtime/status`
shelled out to `ps` (the per-poll co-location + fleet version-skew probes) and the
diff --git a/graph/output_format.py b/graph/output_format.py
index 1eaa7d20..59ed6536 100644
--- a/graph/output_format.py
+++ b/graph/output_format.py
@@ -249,6 +249,118 @@ def stream_visible_reasoning(raw: str) -> str:
return "\n\n".join(chunks)
+# Compiled openers for the incremental streaming views below. The lookbehind skips a
+# backticked mention (matching the pure functions); `Pattern.search(raw, pos)` keeps the
+# FULL string visible to the lookbehind, so backing the search start up by the tag length
+# catches a tag that straddles a chunk boundary without a slice false-positive.
+_OUTPUT_OPEN_RE = re.compile(r"(?", re.IGNORECASE)
+_REASONING_OPEN_RE = re.compile(r"(?", re.IGNORECASE)
+
+
+class StreamingOutputView:
+ """Incremental, amortized-O(total) equivalent of :func:`stream_visible_output`.
+
+ ``stream_visible_output`` re-scans the ENTIRE accumulated text every chunk, so a turn
+ streaming N chars over ~N chunks costs O(N²) (#1310). This keeps the same contract —
+ feed it the monotonically growing accumulated raw, get the full visible-so-far back —
+ but only looks at the newly-appended tail in the common cases:
+
+ * before ```` opens, it scans only the tail for the opener (the
+ ```` is never re-scanned), returning ``""`` meanwhile;
+ * once ```` is open with no ``<`` in the output region (the steady
+ answer-body stream), it just appends the delta.
+
+ Anything ambiguous — a ``<`` that may begin `` `` / ```` /
+ ```` / a partial tag, or the closed state — falls back to the
+ authoritative ``stream_visible_output``, the oracle the equivalence test pins this
+ against. Construct one per turn; call :meth:`update` per chunk.
+ """
+
+ __slots__ = ("_opened", "_fast", "_prev_len", "_visible")
+
+ def __init__(self) -> None:
+ self._opened = False
+ self._fast = False
+ self._prev_len = 0
+ self._visible = ""
+
+ def update(self, raw: str) -> str:
+ delta_start = self._prev_len
+ self._prev_len = len(raw)
+ # Steady answer-body stream: open, unclosed, no `<` in the region — the visible
+ # simply grows by a delta that adds no tag-significant character.
+ if self._fast and "<" not in raw[delta_start:]:
+ self._visible += raw[delta_start:]
+ return self._visible
+ # Pre-output: scan only the tail for the opener (never re-scan the scratch_pad).
+ if not self._opened:
+ start = max(0, delta_start - 8) # catch a "" split across the boundary
+ if _OUTPUT_OPEN_RE.search(raw, start) is None:
+ return "" # still in scratch_pad — nothing user-facing yet
+ self._opened = True
+ # Authoritative recompute (close / / partial-tag handling), then decide
+ # whether the next chunks can take the fast path.
+ self._visible = stream_visible_output(raw)
+ m = _OUTPUT_OPEN_RE.search(raw)
+ self._fast = m is not None and "<" not in raw[m.end() :]
+ return self._visible
+
+
+class StreamingReasoningView:
+ """Incremental, amortized-O(total) equivalent of :func:`stream_visible_reasoning`.
+
+ Same idea as :class:`StreamingOutputView` for the hidden "thinking" stream: only the
+ open trailing reasoning block grows on a plain delta (closed blocks are committed and
+ not re-joined; ``str.strip`` scans only end-whitespace, not the whole body). A ``<``
+ (a block open/close, a nested or partial tag) drops to the authoritative
+ ``stream_visible_reasoning`` — the oracle the equivalence test pins this against.
+ """
+
+ __slots__ = ("_opened", "_fast", "_prev_len", "_committed", "_open_raw", "_visible")
+
+ def __init__(self) -> None:
+ self._opened = False
+ self._fast = False
+ self._prev_len = 0
+ self._committed = "" # joined, stripped bodies of CLOSED reasoning blocks
+ self._open_raw = "" # raw body of the open trailing block (fast mode only)
+ self._visible = ""
+
+ def _join(self, body: str) -> str:
+ if not body:
+ return self._committed
+ if not self._committed:
+ return body
+ return self._committed + "\n\n" + body
+
+ def update(self, raw: str) -> str:
+ delta_start = self._prev_len
+ self._prev_len = len(raw)
+ # Steady deliberation: inside one open trailing block, delta adds no tag char —
+ # extend that block's body and re-join.
+ if self._fast and "<" not in raw[delta_start:]:
+ self._open_raw += raw[delta_start:]
+ self._visible = self._join(self._open_raw.strip())
+ return self._visible
+ # Pre-reasoning: cheap tail-scan for the first scratch_pad/think opener.
+ if not self._opened:
+ start = max(0, delta_start - 13) # catch a "" split across the boundary
+ if _REASONING_OPEN_RE.search(raw, start) is None:
+ return ""
+ self._opened = True
+ # Authoritative recompute, then refresh fast-mode state.
+ self._visible = stream_visible_reasoning(raw)
+ self._committed = "\n\n".join(b for m in _REASONING_BLOCK_RE.finditer(raw) if (b := m.group(2).strip()))
+ tail = _REASONING_OPEN_TAIL_RE.search(raw)
+ if tail is not None and "<" not in tail.group(2):
+ self._open_raw = tail.group(2)
+ self._fast = True
+ else:
+ self._open_raw = ""
+ self._fast = False
+ return self._visible
+
+
def extract_confidence(text: str) -> tuple[float | None, str | None]:
"""Parse an optional self-reported ```` (and explanation).
diff --git a/server/chat.py b/server/chat.py
index 077765d6..f86f9251 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -21,11 +21,11 @@
from graph.output_format import (
DROPPED_SCRATCH_KICKER,
+ StreamingOutputView,
+ StreamingReasoningView,
extract_confidence,
extract_output,
is_dropped_scratch_turn,
- stream_visible_output,
- stream_visible_reasoning,
)
from runtime.state import STATE
@@ -308,6 +308,11 @@ async def _run_turn_stream(
accumulated_raw = ""
streamed_len = 0 # chars of visible already emitted as text frames
reasoned_len = 0 # chars of scratch_pad reasoning already emitted (live thinking)
+ # Incremental views: same visible-so-far as stream_visible_output/_reasoning, but
+ # they scan only the new tail instead of re-running regexes over the whole
+ # accumulated text every chunk — turning the per-turn O(N²) rescan into ~O(N) (#1310).
+ out_view = StreamingOutputView()
+ reason_view = StreamingReasoningView()
_llm_started: dict[str, float] = {} # run_id → monotonic start (per-call latency)
announced_tools: set[str] = set() # tool_call ids already surfaced as a start frame
async for event in STATE.graph.astream_events(
@@ -377,13 +382,13 @@ async def _run_turn_stream(
# Stream only the user-facing region, token by token —
# never the scratch_pad. The terminal artifact (extract_output)
# reconciles any partial tail held back here.
- visible = stream_visible_output(accumulated_raw)
+ visible = out_view.update(accumulated_raw)
if len(visible) > streamed_len:
yield ("text", visible[streamed_len:])
streamed_len = len(visible)
# Stream the scratch_pad reasoning on its own channel — a collapsible
# "thinking" view in the console (never folded into the answer text).
- reasoning = stream_visible_reasoning(accumulated_raw)
+ reasoning = reason_view.update(accumulated_raw)
if len(reasoning) > reasoned_len:
yield ("reasoning", reasoning[reasoned_len:])
reasoned_len = len(reasoning)
diff --git a/tests/test_output_format.py b/tests/test_output_format.py
index e5d6e114..b9ed2400 100644
--- a/tests/test_output_format.py
+++ b/tests/test_output_format.py
@@ -15,8 +15,14 @@
from __future__ import annotations
+import random
+
+import pytest
+
from graph.output_format import (
OUTPUT_FORMAT_INSTRUCTIONS,
+ StreamingOutputView,
+ StreamingReasoningView,
_strip_reasoning,
extract_output,
stream_visible_output,
@@ -310,3 +316,63 @@ def test_stream_visible_is_monotonic_prefix():
assert seen == "Hello world"
# The terminal extractor agrees with the final streamed text.
assert extract_output(raw) == "Hello world"
+
+
+# ── incremental streaming views == the pure functions (oracle equivalence, #1310) ──
+# StreamingOutputView / StreamingReasoningView only scan the new tail per chunk, so they
+# must return EXACTLY what the (O(N²)) pure functions return at every growing prefix.
+# These pin the fast incremental paths to the proven pure implementations.
+
+_STREAM_CASES = [
+ "",
+ "no tags at all, just a bare answer",
+ "thinking Hello world ",
+ "plan the work answer with a < b and c > d comparison ",
+ "partial answer with no closing tag yet", # orphan open (max_tokens truncation)
+ "step 1 step 2 done ", # multi scratch
+ "beforeleaked provider reasoning after ", # think inside output
+ "x 0.9 ",
+ "reasoning that mentions `` in backticks the real answer ",
+ "provider think block ok ",
+ "```python\nxs: list[int] = []\nif a < b and c > d:\n pass\n```\ndone ", # code: many < >
+ "trailing partial close at the very end
+ "answer \n0.8 \nwhy it scored ",
+ "only reasoning, model never opened output and stopped ",
+ "a one more rawtwo ", # second (ignored) output
+]
+
+
+@pytest.mark.parametrize("text", _STREAM_CASES)
+@pytest.mark.parametrize("chunk", [1, 2, 3, 5, 7, 13, 50])
+def test_streaming_output_view_matches_pure(text, chunk):
+ view = StreamingOutputView()
+ for i in range(chunk, len(text) + chunk, chunk):
+ prefix = text[:i]
+ assert view.update(prefix) == stream_visible_output(prefix), f"mismatch at prefix {prefix!r}"
+
+
+@pytest.mark.parametrize("text", _STREAM_CASES)
+@pytest.mark.parametrize("chunk", [1, 2, 3, 5, 7, 13, 50])
+def test_streaming_reasoning_view_matches_pure(text, chunk):
+ view = StreamingReasoningView()
+ for i in range(chunk, len(text) + chunk, chunk):
+ prefix = text[:i]
+ assert view.update(prefix) == stream_visible_reasoning(prefix), f"mismatch at prefix {prefix!r}"
+
+
+def test_streaming_views_match_pure_under_random_fuzz():
+ """Random documents from a tag-heavy alphabet, chunked at random offsets — the
+ incremental views must equal the pure functions at every step (catches boundary /
+ holdback / whitespace edges the curated cases miss)."""
+ rng = random.Random(1310)
+ alphabet = ["", " ", "", " ", "", " ",
+ "", " ", "`", "<", ">", " ", "\n", "a", "b", "x", "word"]
+ for _ in range(300):
+ text = "".join(rng.choice(alphabet) for _ in range(rng.randint(0, 40)))
+ out_view, reason_view = StreamingOutputView(), StreamingReasoningView()
+ pos = 0
+ while pos < len(text):
+ pos = min(len(text), pos + rng.randint(1, 6))
+ prefix = text[:pos]
+ assert out_view.update(prefix) == stream_visible_output(prefix), f"output mismatch: {prefix!r}"
+ assert reason_view.update(prefix) == stream_visible_reasoning(prefix), f"reasoning mismatch: {prefix!r}"
From 6e2a9a1ff9080d0e84674bf045b6de21685e938a Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 12:50:44 -0700
Subject: [PATCH 014/190] perf(console): goals/tasks panels invalidate on the
event bus, drop the 5s poll (closes #1310) (#1312)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Goals and Tasks right-sidebar panels each held a `refetchInterval: 5_000`, so
the console polled goals + the tasks board every 5s per mounted panel forever. The
inbox panel already does this the right way — invalidate off an `/api/events` push.
Backend (the single store seam, so it fires for BOTH agent-tool and console-API writes):
- graph/goals/store.py: `set()` / `clear()` publish `goal.changed` (covers create,
each goal-loop iteration, finish, and clear — the controller routes them all through
the store) via the best-effort `HOST.publish` seam.
- tasks/store.py: `create` / `update` / `close` / `delete` publish `task.changed`
(delete only when a row was actually removed).
Both are no-ops when no publisher is wired (unit tests / standalone) and never break a
write on a bus hiccup.
Frontend:
- GoalsPanel / TasksPanel subscribe to `goal.changed` / `task.changed` and invalidate
their query (mirrors InboxPanel).
- queries.ts: drop the 5s `refetchInterval` from goalsQuery + tasksQuery.
Live updates are now immediate (agent files a task → it appears at once) and steady-
state polling is gone.
Tests: goal/task store publish on each mutation + no-op on a no-op clear/delete; the
goal-hooks bus test now finds goal.achieved among the new goal.changed pushes rather
than assuming it's first. ruff + lint-imports clean; full suite (2507) + live smoke +
web vitest (115) + the goals/tasks e2e batch (20) all green.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 8 ++++++++
apps/web/src/app/GoalsPanel.tsx | 10 +++++++++-
apps/web/src/app/TasksPanel.tsx | 14 ++++++++++----
apps/web/src/lib/queries.ts | 13 ++++++-------
graph/goals/store.py | 15 +++++++++++++++
tasks/store.py | 18 ++++++++++++++++++
tests/test_goal_hooks.py | 9 ++++++---
tests/test_goal_store.py | 23 +++++++++++++++++++++++
tests/test_tasks_store.py | 28 ++++++++++++++++++++++++++++
9 files changed, 123 insertions(+), 15 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68cc6d11..c38d4b97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+- **The Goals and Tasks panels refresh on a bus push instead of polling every 5s.** Both
+ panels held a 5s `refetchInterval`; now the goal store publishes `goal.changed` (on
+ set/advance/clear) and the task store publishes `task.changed` (on create/update/
+ close/delete), and the panels invalidate off those `/api/events` pushes — the same
+ pattern the inbox panel already used. Live updates are now immediate (the agent files a
+ task → it appears at once) and steady-state polling is gone. (#1310)
+
### Fixed
- **Mid-stream output rendering no longer rescans the whole response on every chunk.**
The chat stream recomputed the visible `` (and the live reasoning view) by
diff --git a/apps/web/src/app/GoalsPanel.tsx b/apps/web/src/app/GoalsPanel.tsx
index 5aea8762..b3c03282 100644
--- a/apps/web/src/app/GoalsPanel.tsx
+++ b/apps/web/src/app/GoalsPanel.tsx
@@ -9,10 +9,11 @@ import {
useSuspenseQuery,
} from "@tanstack/react-query";
import { Trash2 } from "lucide-react";
-import { Suspense } from "react";
+import { Suspense, useEffect } from "react";
import { api } from "../lib/api";
import { ago } from "../lib/format";
+import { onServerEvent } from "../lib/events";
import { PanelHeader } from "@protolabsai/ui/navigation";
import { goalsQuery, queryKeys } from "../lib/queries";
import { ErrorBoundary, PanelError, PanelSkeleton } from "./ErrorBoundary";
@@ -43,6 +44,13 @@ function GoalsList() {
onSettled: () => queryClient.invalidateQueries({ queryKey: queryKeys.goals }),
});
+ // Live: the agent set/advanced/cleared a goal mid-turn — refresh off the `goal.changed`
+ // bus push instead of polling every 5s (#1310), the same pattern as the inbox panel.
+ useEffect(
+ () => onServerEvent("goal.changed", () => void queryClient.invalidateQueries({ queryKey: queryKeys.goals })),
+ [queryClient],
+ );
+
if (!goals.length) {
return (
void }) {
const queryClient = useQueryClient();
const invalidate = () => queryClient.invalidateQueries({ queryKey: queryKeys.tasks });
+ // Live: the agent created/closed/updated an issue mid-turn — refresh off the
+ // `task.changed` bus push instead of polling every 5s (#1310), like the inbox panel.
+ useEffect(() => onServerEvent("task.changed", invalidate), [queryClient]);
+
const [dialogOpen, setDialogOpen] = useState(false);
const [collapsed, setCollapsed] = useState>(() => new Set(["closed"]));
diff --git a/apps/web/src/lib/queries.ts b/apps/web/src/lib/queries.ts
index 0517daf4..8968d8ab 100644
--- a/apps/web/src/lib/queries.ts
+++ b/apps/web/src/lib/queries.ts
@@ -41,23 +41,22 @@ export const archetypesQuery = () =>
queryFn: () => api.archetypes(),
});
-// Goals the agent works toward (goal mode). Lives in the right sidebar and
-// refetches every 5s while mounted — the agent advances/clears goals mid-turn,
-// so the panel should track that without a manual refresh.
+// Goals the agent works toward (goal mode). Lives in the right sidebar; the panel
+// invalidates this on the `goal.changed` bus push (the agent set/advances/clears goals
+// mid-turn) instead of a 5s poll (#1310) — same pattern as the inbox.
export const goalsQuery = () =>
queryOptions({
queryKey: queryKeys.goals,
queryFn: () => api.goals(),
- refetchInterval: 5_000,
});
-// The agent's task board (in-process tasks store — always available). Refetches
-// while mounted so the panel tracks issues the agent files/closes mid-turn.
+// The agent's task board (in-process tasks store — always available). The panel
+// invalidates this on the `task.changed` bus push (issues the agent files/closes
+// mid-turn) instead of a 5s poll (#1310).
export const tasksQuery = () =>
queryOptions({
queryKey: queryKeys.tasks,
queryFn: () => api.tasks(),
- refetchInterval: 5_000,
});
// Registered workflow recipes + the subagent registry — config, not live, so no
diff --git a/graph/goals/store.py b/graph/goals/store.py
index 48cfc889..ac2776b6 100644
--- a/graph/goals/store.py
+++ b/graph/goals/store.py
@@ -20,6 +20,19 @@
log = logging.getLogger(__name__)
+def _publish(topic: str, data: dict) -> None:
+ """Best-effort bus push so the console Goals panel can invalidate live on a change
+ instead of polling every 5s (#1310). No-op when the host hasn't wired a publisher
+ (unit tests / standalone use); a bus hiccup must never break a goal write."""
+ try:
+ from graph.plugins.host import HOST
+
+ if HOST.publish:
+ HOST.publish(topic, data)
+ except Exception: # noqa: BLE001
+ pass
+
+
def _resolve_base() -> Path:
# Per-instance scoping (ADR 0004): namespace by PROTOAGENT_INSTANCE so two
# agents on one machine don't share a goals dir — without this, scheduled /
@@ -116,6 +129,7 @@ def set(self, state: GoalState) -> None:
json.dump(state.to_dict(), fh, indent=2, default=str)
os.rename(tmp_path, path)
tmp_path = None
+ _publish("goal.changed", {"session_id": state.session_id})
except OSError as exc:
log.error("[goal] write failed for session %s: %s", state.session_id, exc)
finally:
@@ -133,6 +147,7 @@ def clear(self, session_id: str) -> bool:
path = self._path(session_id)
try:
path.unlink()
+ _publish("goal.changed", {"session_id": session_id})
return True
except FileNotFoundError:
return False
diff --git a/tasks/store.py b/tasks/store.py
index ea28a89c..62123978 100644
--- a/tasks/store.py
+++ b/tasks/store.py
@@ -21,6 +21,19 @@
from infra.paths import scope_leaf
+
+def _publish(topic: str, data: dict) -> None:
+ """Best-effort bus push so the console Tasks panel can invalidate live on a change
+ instead of polling every 5s (#1310). No-op when the host hasn't wired a publisher
+ (unit tests / standalone use); a bus hiccup must never break a task write."""
+ try:
+ from graph.plugins.host import HOST
+
+ if HOST.publish:
+ HOST.publish(topic, data)
+ except Exception: # noqa: BLE001
+ pass
+
DEFAULT_DB_PATH = "/sandbox/tasks/issues.db"
# Open lifecycle states (closed is terminal). Mirrors the console's
@@ -136,6 +149,7 @@ def create(
),
)
self._conn.commit()
+ _publish("task.changed", {"id": issue_id, "action": "created"})
return dict(self._row(issue_id))
def list(self, *, include_closed: bool = True) -> list[dict[str, Any]]:
@@ -176,6 +190,7 @@ def update(self, issue_id: str, **fields: Any) -> dict[str, Any]:
vals.append(_now())
self._conn.execute(f"UPDATE issues SET {', '.join(sets)} WHERE id = ?", (*vals, issue_id))
self._conn.commit()
+ _publish("task.changed", {"id": issue_id, "action": "updated"})
return dict(self._row(issue_id))
def close(self, issue_id: str, reason: str | None = None) -> dict[str, Any]:
@@ -190,10 +205,13 @@ def close(self, issue_id: str, reason: str | None = None) -> dict[str, Any]:
(now, reason or "", now, issue_id),
)
self._conn.commit()
+ _publish("task.changed", {"id": issue_id, "action": "closed"})
return dict(self._row(issue_id))
def delete(self, issue_id: str) -> bool:
with self._lock:
cur = self._conn.execute("DELETE FROM issues WHERE id = ?", (issue_id,))
self._conn.commit()
+ if cur.rowcount > 0:
+ _publish("task.changed", {"id": issue_id, "action": "deleted"})
return cur.rowcount > 0
diff --git a/tests/test_goal_hooks.py b/tests/test_goal_hooks.py
index fd845b30..ae38298b 100644
--- a/tests/test_goal_hooks.py
+++ b/tests/test_goal_hooks.py
@@ -98,9 +98,12 @@ async def _met(spec, ctx):
c = GoalController(config=None, store=GoalStore(base_dir=str(tmp_path)))
c.set_goal_safe("s", "reach target", {"type": "plugin", "check": "p:always"})
await c.evaluate("s", last_text="done")
- assert events and events[0][0] == "goal.achieved"
- assert events[0][1]["condition"] == "reach target"
- assert events[0][1]["status"] == "achieved"
+ # goal.achieved is broadcast on finish — alongside the lightweight goal.changed
+ # store pushes (#1310), so find it rather than assume it's first.
+ achieved = [d for t, d in events if t == "goal.achieved"]
+ assert achieved, f"goal.achieved not on the bus: {[t for t, _ in events]}"
+ assert achieved[0]["condition"] == "reach target"
+ assert achieved[0]["status"] == "achieved"
finally:
HOST.publish = orig
set_plugin_verifiers({})
diff --git a/tests/test_goal_store.py b/tests/test_goal_store.py
index e7d79b9d..fcb8271e 100644
--- a/tests/test_goal_store.py
+++ b/tests/test_goal_store.py
@@ -82,3 +82,26 @@ def test_resolve_base_is_instance_scoped(monkeypatch, tmp_path):
# No instance id → unscoped (single-instance back-compat).
monkeypatch.delenv("PROTOAGENT_INSTANCE", raising=False)
assert goal_store._resolve_base() == tmp_path
+
+
+def test_set_and_clear_publish_goal_changed(tmp_path, monkeypatch):
+ """A goal write/clear pushes `goal.changed` on the bus so the console Goals panel
+ invalidates live instead of polling every 5s (#1310)."""
+ from graph.plugins import host
+
+ events: list[tuple] = []
+ monkeypatch.setattr(host.HOST, "publish", lambda topic, data: events.append((topic, data)))
+ store = GoalStore(tmp_path)
+ store.set(GoalState(session_id="s1", condition="x"))
+ assert store.clear("s1") is True
+ assert [t for t, _ in events] == ["goal.changed", "goal.changed"]
+ assert events[0][1]["session_id"] == "s1"
+
+
+def test_clear_missing_does_not_publish(tmp_path, monkeypatch):
+ from graph.plugins import host
+
+ events: list[tuple] = []
+ monkeypatch.setattr(host.HOST, "publish", lambda topic, data: events.append((topic, data)))
+ assert GoalStore(tmp_path).clear("nope") is False
+ assert events == [] # nothing was cleared → no event
diff --git a/tests/test_tasks_store.py b/tests/test_tasks_store.py
index 8c07ae6d..229069cb 100644
--- a/tests/test_tasks_store.py
+++ b/tests/test_tasks_store.py
@@ -113,3 +113,31 @@ def test_operator_adapter_maps_to_store(store):
a.close("/x", "task-1", "shipped")
assert store.get("task-1")["status"] == "closed"
assert a.delete("/x", "task-1") == {"deleted": True}
+
+
+def test_mutations_publish_task_changed(store, monkeypatch):
+ """create/update/close/delete push `task.changed` so the console Tasks panel
+ invalidates live instead of polling every 5s (#1310)."""
+ from graph.plugins import host
+
+ events: list[tuple] = []
+ monkeypatch.setattr(host.HOST, "publish", lambda topic, data: events.append((topic, data)))
+ issue = store.create("do a thing")
+ store.update(issue["id"], status="in_progress")
+ store.close(issue["id"])
+ assert store.delete(issue["id"]) is True
+ assert [(t, d["action"]) for t, d in events] == [
+ ("task.changed", "created"),
+ ("task.changed", "updated"),
+ ("task.changed", "closed"),
+ ("task.changed", "deleted"),
+ ]
+
+
+def test_delete_missing_does_not_publish(store, monkeypatch):
+ from graph.plugins import host
+
+ events: list[tuple] = []
+ monkeypatch.setattr(host.HOST, "publish", lambda topic, data: events.append((topic, data)))
+ assert store.delete("bd-404") is False
+ assert events == [] # nothing deleted → no event
From a8972dd4e8a861fb8fee342ebed0fc6e31902d49 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 13:11:18 -0700
Subject: [PATCH 015/190] =?UTF-8?q?feat(tooling):=20scripts/reset.sh=20?=
=?UTF-8?q?=E2=80=94=20CLI=20factory-reset=20for=20the=20default=20instanc?=
=?UTF-8?q?e=20(closes=20#1159)=20(#1313)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A first-class factory-reset for the default ("prod") instance — wipe its data +
local config back to a clean slate so the next boot runs the setup wizard, for
testing the fresh-user flow. CLI-only by design; there is intentionally NO in-app
self-wipe (a running server deleting its own WAL-locked data then relaunching is
too risky — descoped per the issue discussion).
SAFE on a multi-instance machine (validated against a real ~/.protoagent with 5
sibling instance roots + dozens of scoped team leaves): it preserves EVERY other
instance — any `~/.protoagent/` carrying an `.instance-uid`/checkpoints.db
(the dev sandbox, fleet members, forks) is left untouched, and inside shared store
dirs every `/` leaf (a subdirectory) is preserved. Only prod's own
unscoped top-level DBs + the direct files in those store dirs are removed. Tracked
`config/` files are `git checkout`-restored to pristine; only gitignored local
config (langgraph-config.yaml, secrets.yaml, .setup-complete, plugins/) is deleted.
Flags: `--dry-run` (print the exact plan, delete nothing — ALWAYS run first),
`--yes`, `--backup` (timestamped tar.gz), `--keep-secrets` (preserve secrets.yaml +
langgraph-config.yaml so no gateway re-auth), `--include-dev` (also wipe the dev
sandbox), `--force` (SIGTERM a server still bound to :7870 — a live process holds
WAL handles). Refuses to wipe while a server is bound unless --force.
Tests: tests/test_reset_script.py drives `--dry-run` against a synthetic tree and
asserts the plan targets prod-only, preserves siblings + leaves, and deletes
NOTHING (plus --keep-secrets / --include-dev plan variants). Verified the real
deletion path end-to-end in an isolated tmp HOME. Docs: PROTO.md run-commands.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 11 +++
PROTO.md | 11 +++
scripts/reset.sh | 191 +++++++++++++++++++++++++++++++++++++
tests/test_reset_script.py | 114 ++++++++++++++++++++++
4 files changed, 327 insertions(+)
create mode 100755 scripts/reset.sh
create mode 100644 tests/test_reset_script.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c38d4b97..940a9bbe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- **`scripts/reset.sh` — factory-reset the default (prod) instance from the CLI.** Wipes
+ the prod instance's data + local config back to a clean slate so the next boot runs the
+ setup wizard (for testing the fresh-user flow). Safe on a multi-instance machine: every
+ *other* instance (any `~/.protoagent/` with an `.instance-uid`, the dev sandbox,
+ fleet members) and every scoped `/` leaf is preserved — only prod's
+ unscoped DBs + direct files are removed; tracked `config/` files are `git checkout`-
+ restored, gitignored local config deleted. `--dry-run` prints the exact plan;
+ `--keep-secrets` / `--include-dev` / `--backup` / `--force` / `--yes`. No in-app reset
+ (deliberately CLI-only). (#1159)
+
### Changed
- **The Goals and Tasks panels refresh on a bus push instead of polling every 5s.** Both
panels held a 5s `refetchInterval`; now the goal store publishes `goal.changed` (on
diff --git a/PROTO.md b/PROTO.md
index b0b3d92a..e90cd68f 100644
--- a/PROTO.md
+++ b/PROTO.md
@@ -21,6 +21,17 @@ TypeScript is the console.
chat/tasks/knowledge. The default instance (`config/` + `~/.protoagent`, `:7870`)
is untouched. `scripts/dev-reset.sh` wipes just the sandbox. Use this for feature
testing instead of the default instance.
+- **Factory-reset the default instance:** `scripts/reset.sh` wipes the **prod**
+ instance's data + local config back to a clean slate (next boot runs the setup
+ wizard) — for testing the fresh-user flow via CLI (there is no in-app reset).
+ **Always `scripts/reset.sh --dry-run` first** to read the plan. It's safe on a
+ multi-instance machine: every *other* instance (any `~/.protoagent/` with an
+ `.instance-uid`, the dev sandbox, fleet members) and every scoped
+ `/` leaf is preserved — only prod's unscoped DBs + the direct
+ files in shared store dirs are removed; tracked `config/` files are `git checkout`-
+ restored, gitignored local config deleted. Flags: `--yes`, `--backup`,
+ `--keep-secrets` (keep gateway creds), `--include-dev`, `--force` (stop a bound
+ server first).
- **Python deps:** managed with `uv` (`pyproject.toml [project.dependencies]` is
the source of truth; `uv.lock` is tracked). `uv sync` to install.
- **Console deps:** `npm ci` at the repo root (npm workspaces; the web app is
diff --git a/scripts/reset.sh b/scripts/reset.sh
new file mode 100755
index 00000000..57f532b8
--- /dev/null
+++ b/scripts/reset.sh
@@ -0,0 +1,191 @@
+#!/usr/bin/env bash
+#
+# Factory-reset the DEFAULT ("prod") protoAgent instance: wipe its data + local
+# config back to a clean slate so the next boot runs the setup wizard. For local
+# testing of the fresh-user flow. (Reset ONLY the dev sandbox instead with
+# scripts/dev-reset.sh; there is intentionally NO in-app factory reset — #1159.)
+#
+# SAFE ON A MULTI-INSTANCE MACHINE — it preserves EVERY OTHER instance:
+# * any ~/.protoagent/ that carries an instance marker (.instance-uid /
+# checkpoints.db) is left untouched (the dev sandbox, fleet members, forks);
+# * inside a shared store dir (knowledge/, memory/, tasks/, …) every
+# / leaf (a SUBDIRECTORY) is preserved — only prod's own
+# unscoped top-level DBs and the direct files in those store dirs are removed.
+# Shipped/tracked files in config/ are RESTORED to pristine (git checkout); only
+# gitignored local config is deleted.
+#
+# scripts/reset.sh --dry-run # print exactly what would change; delete nothing
+# scripts/reset.sh # confirm, then reset prod (keeps every other instance)
+# scripts/reset.sh --yes # skip the typed confirmation
+# scripts/reset.sh --backup # timestamped tar.gz of data + config first
+# scripts/reset.sh --keep-secrets # keep secrets.yaml + langgraph-config.yaml (no re-auth)
+# scripts/reset.sh --include-dev # ALSO wipe the `dev` sandbox (its data + config/dev)
+# scripts/reset.sh --force # stop a server still bound to the port first
+#
+# ALWAYS run --dry-run first on a busy machine and read the plan.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+REPO="$(pwd)"
+
+# ── flags ─────────────────────────────────────────────────────────────────────
+DRY_RUN=false; ASSUME_YES=false; DO_BACKUP=false
+KEEP_SECRETS=false; INCLUDE_DEV=false; FORCE=false
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run|-n) DRY_RUN=true ;;
+ --yes|-y) ASSUME_YES=true ;;
+ --backup) DO_BACKUP=true ;;
+ --keep-secrets) KEEP_SECRETS=true ;;
+ --include-dev) INCLUDE_DEV=true ;;
+ --force) FORCE=true ;;
+ -h|--help) sed -n '2,33p' "$0"; exit 0 ;;
+ *) echo "unknown option: $arg (try --help)" >&2; exit 2 ;;
+ esac
+done
+
+# ── paths ─────────────────────────────────────────────────────────────────────
+# Mirror infra.paths.data_home(): /sandbox in a container, else ~/.protoagent.
+if [ -d /sandbox ]; then DATA="/sandbox"; else DATA="${HOME}/.protoagent"; fi
+CONFIG="${PROTOAGENT_CONFIG_DIR:-${REPO}/config}"
+PORT="${PORT:-7870}"
+
+# Gitignored local config to DELETE (the prod instance's machine-local state).
+CONFIG_IGNORED=(langgraph-config.yaml secrets.yaml .setup-complete plugins)
+# Shipped/tracked config to RESTORE to pristine (git checkout, never delete).
+CONFIG_TRACKED=(config/SOUL.md config/skills config/plugin-catalog.json
+ config/mcp-catalog.json config/soul-presets config/langgraph-config.example.yaml
+ plugins.lock)
+
+# ── helpers ───────────────────────────────────────────────────────────────────
+say() { printf '%s\n' "$*"; }
+plan() { printf ' %s\n' "$*"; }
+run() { if $DRY_RUN; then printf ' [dry-run] %s\n' "$*"; else "$@"; fi; }
+
+is_instance_root() { [ -e "$1/.instance-uid" ] || [ -e "$1/checkpoints.db" ]; }
+
+# A server still bound to PORT holds WAL handles — a wipe is pointless until it exits.
+running_pids() { command -v lsof >/dev/null 2>&1 && lsof -ti "tcp:${PORT}" -sTCP:LISTEN 2>/dev/null || true; }
+
+# ── 0. running-server guard ───────────────────────────────────────────────────
+PIDS="$(running_pids)"
+if [ -n "$PIDS" ]; then
+ if $FORCE && ! $DRY_RUN; then
+ say "▶ stopping server on :${PORT} (pids: ${PIDS//$'\n'/ })"
+ # shellcheck disable=SC2086
+ kill $PIDS 2>/dev/null || true
+ for _ in 1 2 3 4 5 6 7 8 9 10; do [ -n "$(running_pids)" ] || break; sleep 0.5; done
+ # shellcheck disable=SC2046
+ [ -z "$(running_pids)" ] || kill -9 $(running_pids) 2>/dev/null || true
+ elif ! $DRY_RUN; then
+ say "✗ a server is bound to :${PORT} (pids: ${PIDS//$'\n'/ })."
+ say " Stop it first, or pass --force to SIGTERM it. (A live process holds WAL handles.)"
+ exit 1
+ else
+ plan "NOTE: a server is bound to :${PORT} (pids: ${PIDS//$'\n'/ }) — stop it (or --force) before a real run."
+ fi
+fi
+
+# ── 1. the plan ───────────────────────────────────────────────────────────────
+KEEP="dev"; $INCLUDE_DEV && KEEP=""
+say ""
+say "Factory reset — DEFAULT (prod) instance"
+say " data: ${DATA}"
+say " config: ${CONFIG}"
+$DRY_RUN && say " mode: DRY RUN (nothing will be deleted)"
+say ""
+
+if [ -d "$DATA" ]; then
+ say "Data home (${DATA}):"
+ PRESERVED=()
+ while IFS= read -r entry; do
+ name="$(basename "$entry")"
+ if [ -d "$entry" ] && is_instance_root "$entry"; then
+ if [ -n "$KEEP" ] || [ "$name" != "dev" ]; then PRESERVED+=("$name"); continue; fi
+ fi
+ if [ -f "$entry" ] || [ -L "$entry" ]; then
+ plan "delete prod file: ${name}"
+ elif [ -d "$entry" ]; then
+ # A store dir: prod's content is its DIRECT FILES; every subdir is a
+ # / leaf and is preserved (unless --include-dev → drop dev/).
+ files=$(find "$entry" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')
+ leaves=$(find "$entry" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
+ plan "store dir ${name}/: drop ${files} prod file(s); keep ${leaves} instance leaf(s)$( $INCLUDE_DEV && [ -d "$entry/dev" ] && echo ' (minus dev/)')"
+ fi
+ done < <(find "$DATA" -mindepth 1 -maxdepth 1 | sort)
+ [ ${#PRESERVED[@]} -gt 0 ] && plan "PRESERVE other instances: ${PRESERVED[*]}"
+else
+ plan "(no data home at ${DATA})"
+fi
+
+say ""
+say "Config (${CONFIG}):"
+for f in "${CONFIG_IGNORED[@]}"; do
+ if $KEEP_SECRETS && { [ "$f" = "secrets.yaml" ] || [ "$f" = "langgraph-config.yaml" ]; }; then
+ [ -e "${CONFIG}/${f}" ] && plan "keep (--keep-secrets): ${f}"
+ continue
+ fi
+ [ -e "${CONFIG}/${f}" ] && plan "delete local: ${f}"
+done
+$INCLUDE_DEV && [ -e "${CONFIG}/dev" ] && plan "delete dev config: dev/"
+for p in "${CONFIG_TRACKED[@]}"; do plan "restore tracked: ${p}"; done
+
+if $DRY_RUN; then
+ say ""
+ say "Dry run complete — nothing was changed."
+ exit 0
+fi
+
+# ── 2. confirm ────────────────────────────────────────────────────────────────
+if ! $ASSUME_YES; then
+ say ""
+ printf "Type 'reset' to wipe the prod instance (other instances are preserved): "
+ read -r reply
+ [ "$reply" = "reset" ] || { say "aborted."; exit 1; }
+fi
+
+# ── 3. backup ─────────────────────────────────────────────────────────────────
+if $DO_BACKUP; then
+ TS="$(date +%Y%m%d-%H%M%S)"
+ BK="${HOME}/protoagent-backup-${TS}.tar.gz"
+ say "▶ backing up data + config → ${BK}"
+ tar czf "$BK" -C "$HOME" "$(realpath --relative-to="$HOME" "$DATA" 2>/dev/null || echo .protoagent)" \
+ -C "$REPO" config 2>/dev/null || say " (backup best-effort; some paths skipped)"
+fi
+
+# ── 4. wipe prod data (preserving every other instance) ───────────────────────
+if [ -d "$DATA" ]; then
+ while IFS= read -r entry; do
+ name="$(basename "$entry")"
+ if [ -d "$entry" ] && is_instance_root "$entry"; then
+ if [ -n "$KEEP" ] || [ "$name" != "dev" ]; then continue; fi
+ fi
+ if [ -f "$entry" ] || [ -L "$entry" ]; then
+ run rm -f "$entry"
+ elif [ -d "$entry" ]; then
+ # delete prod's direct files; preserve every / subdir
+ while IFS= read -r f; do run rm -f "$f"; done < <(find "$entry" -mindepth 1 -maxdepth 1 -type f)
+ if $INCLUDE_DEV && [ -d "$entry/dev" ]; then run rm -rf "$entry/dev"; fi
+ rmdir "$entry" 2>/dev/null || true # remove if now empty (held no instance leaf)
+ fi
+ done < <(find "$DATA" -mindepth 1 -maxdepth 1 | sort)
+fi
+# --include-dev: also drop the dev instance root + its scoped data leaves.
+if $INCLUDE_DEV; then
+ run rm -rf "${DATA:?}/dev"
+ while IFS= read -r leaf; do run rm -rf "$leaf"; done \
+ < <(find "$DATA" -mindepth 2 -maxdepth 2 -type d -name dev 2>/dev/null)
+fi
+
+# ── 5. config: delete gitignored local state, restore tracked to pristine ─────
+for f in "${CONFIG_IGNORED[@]}"; do
+ if $KEEP_SECRETS && { [ "$f" = "secrets.yaml" ] || [ "$f" = "langgraph-config.yaml" ]; }; then continue; fi
+ [ -e "${CONFIG}/${f}" ] && run rm -rf "${CONFIG:?}/${f}"
+done
+$INCLUDE_DEV && [ -e "${CONFIG}/dev" ] && run rm -rf "${CONFIG:?}/dev"
+# Restore the shipped/tracked files only if they're tracked in this repo.
+for p in "${CONFIG_TRACKED[@]}"; do
+ git -C "$REPO" ls-files --error-unmatch "$p" >/dev/null 2>&1 && run git -C "$REPO" checkout -- "$p"
+done
+
+say ""
+say "✓ prod instance reset. Next boot (python -m server) runs the setup wizard."
diff --git a/tests/test_reset_script.py b/tests/test_reset_script.py
new file mode 100644
index 00000000..76fa23ee
--- /dev/null
+++ b/tests/test_reset_script.py
@@ -0,0 +1,114 @@
+"""`scripts/reset.sh` dry-run safety (#1159).
+
+The factory-reset script is destructive, so its dangerous logic — "target prod's
+unscoped data, preserve EVERY sibling instance + every scoped /
+leaf" — is pinned here by running it in `--dry-run` against a synthetic data tree
+and asserting (a) the plan targets the right things and (b) it deletes NOTHING.
+"""
+
+from __future__ import annotations
+
+import os
+import subprocess
+from pathlib import Path
+
+import pytest
+
+REPO = Path(__file__).resolve().parents[1]
+SCRIPT = REPO / "scripts" / "reset.sh"
+
+# data_home() prefers /sandbox over $HOME/.protoagent; if it exists the script
+# would target it instead of our test HOME, so skip there (CI has no /sandbox).
+_SANDBOX = pytest.mark.skipif(Path("/sandbox").is_dir(), reason="data_home() would target /sandbox")
+
+
+def _run(args: list[str], home: Path, cfg: Path):
+ env = {**os.environ, "HOME": str(home), "PROTOAGENT_CONFIG_DIR": str(cfg)}
+ return subprocess.run(
+ ["bash", str(SCRIPT), *args], capture_output=True, text=True, env=env, cwd=str(REPO)
+ )
+
+
+@_SANDBOX
+def test_dry_run_targets_prod_only_and_preserves_siblings(tmp_path):
+ home = tmp_path / "home"
+ data = home / ".protoagent"
+ data.mkdir(parents=True)
+ # prod (unscoped) state
+ (data / "checkpoints.db").write_text("x")
+ (data / "telemetry.db").write_text("x")
+ (data / ".instance-uid").write_text("x")
+ # a shared store dir: prod's direct file + two scoped instance leaves
+ (data / "knowledge" / "dev").mkdir(parents=True)
+ (data / "knowledge" / "roxy").mkdir(parents=True)
+ (data / "knowledge" / "agent.db").write_text("prod")
+ (data / "knowledge" / "dev" / "agent.db").write_text("dev")
+ (data / "knowledge" / "roxy" / "agent.db").write_text("roxy")
+ # a sibling instance ROOT (must be preserved wholesale)
+ (data / "sib").mkdir()
+ (data / "sib" / ".instance-uid").write_text("sib")
+
+ cfg = tmp_path / "config"
+ cfg.mkdir()
+ (cfg / "langgraph-config.yaml").write_text("x")
+ (cfg / "secrets.yaml").write_text("x")
+ (cfg / "plugins").mkdir()
+
+ out = _run(["--dry-run", "--yes"], home, cfg)
+ assert out.returncode == 0, out.stderr
+ plan = out.stdout
+
+ assert "delete prod file: checkpoints.db" in plan
+ assert "delete prod file: telemetry.db" in plan
+ assert "delete prod file: .instance-uid" in plan
+ # the shared store dir: drop prod's 1 file, KEEP the 2 instance leaves
+ assert "store dir knowledge/: drop 1 prod file(s); keep 2 instance leaf(s)" in plan
+ # sibling instance root preserved + dev (default keeps dev too)
+ assert "PRESERVE other instances:" in plan and "sib" in plan
+ assert "delete local: langgraph-config.yaml" in plan
+ assert "delete local: plugins" in plan
+ assert "restore tracked: config/SOUL.md" in plan
+
+ # CRITICAL: a dry run deletes NOTHING.
+ assert (data / "checkpoints.db").exists()
+ assert (data / "knowledge" / "agent.db").exists()
+ assert (data / "knowledge" / "dev" / "agent.db").exists()
+ assert (data / "sib" / ".instance-uid").exists()
+ assert (cfg / "langgraph-config.yaml").exists()
+ assert (cfg / "plugins").is_dir()
+
+
+@_SANDBOX
+def test_dry_run_keep_secrets_preserves_creds(tmp_path):
+ home = tmp_path / "home"
+ (home / ".protoagent").mkdir(parents=True)
+ cfg = tmp_path / "config"
+ cfg.mkdir()
+ (cfg / "secrets.yaml").write_text("x")
+ (cfg / "langgraph-config.yaml").write_text("x")
+ (cfg / "plugins").mkdir()
+
+ out = _run(["--dry-run", "--yes", "--keep-secrets"], home, cfg)
+ assert out.returncode == 0, out.stderr
+ plan = out.stdout
+ assert "keep (--keep-secrets): secrets.yaml" in plan
+ assert "keep (--keep-secrets): langgraph-config.yaml" in plan
+ assert "delete local: langgraph-config.yaml" not in plan
+ assert "delete local: plugins" in plan # plugins still wiped
+
+
+@_SANDBOX
+def test_dry_run_include_dev_wipes_dev(tmp_path):
+ home = tmp_path / "home"
+ data = home / ".protoagent"
+ (data / "dev").mkdir(parents=True)
+ (data / "dev" / ".instance-uid").write_text("dev")
+ cfg = tmp_path / "config"
+ (cfg / "dev").mkdir(parents=True)
+
+ out = _run(["--dry-run", "--yes", "--include-dev"], home, cfg)
+ assert out.returncode == 0, out.stderr
+ plan = out.stdout
+ # default keeps dev; --include-dev drops it from the preserve set + wipes its config
+ assert "PRESERVE other instances:" not in plan or "dev" not in plan.split("PRESERVE")[-1]
+ assert "delete dev config: dev/" in plan
From 014cc170d8130846d41dd3c17609e435fb37f00f Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 21:35:50 -0700
Subject: [PATCH 016/190] chore(issues): issue forms + silent validation gate
(#1314)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore(issues): issue forms + silent validation gate
Add a GitHub issue standard and enforce it without blocking authors.
- .github/ISSUE_TEMPLATE/{bug,enhancement}.yml — issue forms whose required
fields render as the exact headings the gate checks; auto-label bug/enhancement.
- .github/ISSUE_TEMPLATE/config.yml — keep blank issues enabled (maintainer
escape) + contact links (CONTRIBUTING, security advisory).
- .github/workflows/issue-gate.yml — runs on issues opened/edited/reopened;
flags non-conforming issues with `needs-info` and NOTHING else (silent, never
comments, never closes). Removes the label on edit once the issue conforms.
Skips issues labeled `gate-exempt`. Ensures both labels exist (idempotent).
- CONTRIBUTING.md — the issue standard, surfaced by GitHub on the New-issue page
(the discoverable home for the silent gate's rules).
- PROTO.md — short "Filing issues" note pointing at the gate + CONTRIBUTING.
Gate contract verified against #1159/#1300 (pass) and a thin issue (flag);
form-filed issues always pass by construction.
Co-Authored-By: Claude Opus 4.8 (1M context)
* ci(issue-gate): annotate runs-on for the workspace-config verifier
The Verify-workspace-config check requires every workflow's runs-on to carry the
'# workspace-config: allow-hosted-runner …' annotation; issue-gate.yml was missing
it (the 1 error blocking the PR). Mirrors the other workflows in checks.yml.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
.github/ISSUE_TEMPLATE/bug.yml | 54 +++++++++++++++
.github/ISSUE_TEMPLATE/config.yml | 11 +++
.github/ISSUE_TEMPLATE/enhancement.yml | 39 +++++++++++
.github/workflows/issue-gate.yml | 92 ++++++++++++++++++++++++++
CONTRIBUTING.md | 43 ++++++++++++
PROTO.md | 11 +++
6 files changed, 250 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug.yml
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
create mode 100644 .github/ISSUE_TEMPLATE/enhancement.yml
create mode 100644 .github/workflows/issue-gate.yml
create mode 100644 CONTRIBUTING.md
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 00000000..3a413886
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,54 @@
+name: Bug report
+description: Something is broken or behaves incorrectly.
+title: "bug: "
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for filing. The required fields below map 1:1 to the issue gate
+ (`.github/workflows/issue-gate.yml`) — fill them and the gate passes.
+ See #1300 / #1310 for the house style.
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem / what's wrong
+ description: What's broken, and where? Name the file / subsystem, and link the relevant ADR if any.
+ placeholder: "`server/chat.py` does X every chunk but the contract says Y…"
+ validations:
+ required: true
+ - type: textarea
+ id: repro
+ attributes:
+ label: Steps to reproduce / evidence
+ description: Minimal steps, logs, or a stack trace that demonstrate it.
+ validations:
+ required: true
+ - type: textarea
+ id: expected-actual
+ attributes:
+ label: Expected vs. actual
+ description: What you expected to happen, and what actually happened.
+ validations:
+ required: true
+ - type: textarea
+ id: acceptance
+ attributes:
+ label: Acceptance
+ description: How we'll know it's fixed — verifiable criteria.
+ validations:
+ required: true
+ - type: input
+ id: env
+ attributes:
+ label: Environment
+ description: Instance (default/dev), OS, app vs. CLI, version / commit.
+ validations:
+ required: false
+ - type: textarea
+ id: refs
+ attributes:
+ label: Refs
+ description: Related issues / PRs / ADRs (e.g. #1298, ADR 0047).
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..bb62b067
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+# Keep blank issues enabled: maintainers/agents may file intentional free-form
+# issues (tracking notes, quick split-outs). The gate flags those unless they
+# carry the `gate-exempt` label — see CONTRIBUTING.md.
+blank_issues_enabled: true
+contact_links:
+ - name: Issue standard & contributing
+ url: https://github.com/protoLabsAI/protoAgent/blob/main/CONTRIBUTING.md
+ about: What a good issue needs (the same checks the silent gate runs) and how to clear the needs-info label.
+ - name: Report a security vulnerability
+ url: https://github.com/protoLabsAI/protoAgent/security/advisories/new
+ about: Disclose vulnerabilities privately — do not open a public issue.
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
new file mode 100644
index 00000000..5420325c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -0,0 +1,39 @@
+name: Enhancement / feature
+description: A new capability, or an improvement to an existing one.
+title: "feat: "
+labels: ["enhancement"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ The required fields below map 1:1 to the issue gate
+ (`.github/workflows/issue-gate.yml`) — fill them and the gate passes.
+ See #1159 for the house style.
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem / motivation
+ description: What gap or pain motivates this? Who's affected, and where does it live today?
+ validations:
+ required: true
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposed direction
+ description: Sketch the approach. Multiple options are fine — note the trade-offs.
+ validations:
+ required: true
+ - type: textarea
+ id: acceptance
+ attributes:
+ label: Acceptance
+ description: Verifiable criteria for "done".
+ validations:
+ required: true
+ - type: textarea
+ id: refs
+ attributes:
+ label: Refs
+ description: Related issues / PRs / ADRs.
+ validations:
+ required: false
diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml
new file mode 100644
index 00000000..bb311c1a
--- /dev/null
+++ b/.github/workflows/issue-gate.yml
@@ -0,0 +1,92 @@
+name: Issue gate
+
+# Validates every opened/edited issue against the issue standard (CONTRIBUTING.md).
+# Non-conforming issues get the `needs-info` label and NOTHING else — the gate is
+# silent (never comments) and never closes. Editing an issue to conform removes the
+# label automatically. Issues labeled `gate-exempt` are skipped (maintainer escape).
+on:
+ issues:
+ types: [opened, edited, reopened]
+
+permissions:
+ issues: write
+
+concurrency:
+ group: issue-gate-${{ github.event.issue.number }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest # workspace-config: allow-hosted-runner public repo — GH-hosted is free
+ steps:
+ - uses: actions/github-script@v7
+ with:
+ script: |
+ const GATE_LABEL = 'needs-info';
+ const EXEMPT_LABEL = 'gate-exempt';
+
+ const issue = context.payload.issue;
+ const num = issue.number;
+ const body = issue.body || '';
+ const labels = (issue.labels || [])
+ .map(l => (typeof l === 'string' ? l : l.name).toLowerCase());
+
+ async function ensureLabel(name, color, description) {
+ try {
+ await github.rest.issues.getLabel({ ...context.repo, name });
+ } catch (e) {
+ if (e.status === 404) {
+ await github.rest.issues.createLabel({ ...context.repo, name, color, description });
+ } else { throw e; }
+ }
+ }
+
+ // Make both gate labels available in the picker (idempotent).
+ await ensureLabel(GATE_LABEL, 'fbca04',
+ 'Issue is missing required sections — see CONTRIBUTING.md (silent issue gate).');
+ await ensureLabel(EXEMPT_LABEL, 'c5def5',
+ 'Intentional free-form issue — skipped by the issue gate.');
+
+ if (labels.includes(EXEMPT_LABEL)) {
+ core.info(`#${num} is ${EXEMPT_LABEL}; skipping gate.`);
+ return;
+ }
+
+ // A "section" is a markdown heading (#..######) or a bold line (**…**).
+ const lines = body.split('\n');
+ const hasSection = (re) => lines.some(line => {
+ const m = line.match(/^\s*(?:#{1,6}\s+|\*\*\s*)(.+?)(?:\s*\*\*)?\s*$/);
+ return m && re.test(m[1]);
+ });
+
+ const text = body.replace(/\s+/g, ' ').trim();
+ const longEnough = text.length >= 80;
+ const hasProblem = hasSection(/problem|what'?s? wrong|motivation|background|context|summary/i);
+ const hasRepro = hasSection(/repro|reproduce|steps|evidence|expected|actual|observed/i);
+ const hasProposal = hasSection(/propos|solution|approach|direction|fix|design/i);
+ const hasAcceptance = hasSection(/acceptance|done when|success criteria|definition of done/i);
+
+ const missing = [];
+ if (!longEnough) missing.push('a substantive description (>= 80 chars)');
+ if (!hasProblem) missing.push('a Problem / What-is-wrong / Motivation section');
+ if (labels.includes('bug') && !hasRepro)
+ missing.push('Steps to reproduce / Evidence / Expected-vs-actual (bug)');
+ if (labels.includes('enhancement') && !hasProposal && !hasAcceptance)
+ missing.push('a Proposed-direction or Acceptance section (enhancement)');
+
+ const conforms = missing.length === 0;
+ const hasGate = labels.includes(GATE_LABEL);
+
+ if (conforms && hasGate) {
+ await github.rest.issues.removeLabel({
+ ...context.repo, issue_number: num, name: GATE_LABEL,
+ }).catch(() => {});
+ core.info(`#${num} now conforms; removed ${GATE_LABEL}.`);
+ } else if (!conforms && !hasGate) {
+ await github.rest.issues.addLabels({
+ ...context.repo, issue_number: num, labels: [GATE_LABEL],
+ });
+ core.warning(`#${num} missing: ${missing.join('; ')} -> labeled ${GATE_LABEL} (silent).`);
+ } else {
+ core.info(`#${num} unchanged (conforms=${conforms}, labeled=${hasGate}).`);
+ }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..dfb3da96
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contributing
+
+protoAgent's full contributor guide — run commands, the **must-pass-before-PR
+gates**, and the gotchas that recur — lives in **[PROTO.md](./PROTO.md)**. Read it
+before sending code.
+
+This file covers the one thing GitHub surfaces directly on the *New issue* page:
+**what a good issue needs.**
+
+## Filing an issue
+
+Use a template — **Bug report** or **Enhancement / feature** — whenever you can.
+A template's required fields are exactly what the gate checks, so a template-filed
+issue always passes.
+
+Every issue — however it's filed (web, `gh issue create`, or an agent) — should have:
+
+- **A substantive description.** Not just a title; state the actual problem.
+- **A Problem / What's-wrong / Motivation section** — *why* it matters, and
+ *where* (name the file / subsystem / ADR).
+- **Type-specific detail:**
+ - *Bug* (`bug` label): **Steps to reproduce / Evidence** and **Expected vs.
+ actual**.
+ - *Enhancement* (`enhancement` label): a **Proposed direction** and/or
+ **Acceptance** criteria.
+- **Refs** to related issues / PRs / ADRs where relevant (`#1300`, `ADR 0047`).
+
+See #1159, #1300, #1310 for the house style: Problem → (What's wrong / Proposed
+direction) → Acceptance → Refs.
+
+## The issue gate
+
+`.github/workflows/issue-gate.yml` runs on every opened/edited issue and checks
+the requirements above. It is **silent** — it never comments. An issue missing
+required sections just gets the **`needs-info`** label, nothing else.
+
+- **To clear it:** edit the issue to add the missing sections. The gate re-runs
+ on edit and **removes `needs-info`** automatically once the issue conforms.
+- **Intentional free-form** (a maintainer tracking note, a quick agent split-out):
+ add the **`gate-exempt`** label and the gate skips the issue.
+
+No required field blocks you from *opening* an issue — the gate only flags, it
+never closes.
diff --git a/PROTO.md b/PROTO.md
index e90cd68f..65e5a06a 100644
--- a/PROTO.md
+++ b/PROTO.md
@@ -54,6 +54,17 @@ before the PR, not after. CI is the merge gate; a red PR is wasted cycles.
If a change is genuinely test-free (docs, config, pure refactor), say so
explicitly in the PR description — but that is the exception, not the default.
+## Filing issues
+
+Issues are gated too — but only **flagged**, never blocked. The silent
+`issue-gate` workflow (`.github/workflows/issue-gate.yml`) labels any issue
+missing the required structure with **`needs-info`** (no comment) and removes it
+once you edit the issue to conform. Use the **Bug** / **Enhancement** issue forms
+— their required fields match the gate; a free-form issue needs at least a
+*Problem / What's-wrong* section, plus repro + evidence (bugs) or a
+proposed-direction / acceptance (enhancements). Intentional free-form → add the
+`gate-exempt` label. Full checklist: **[CONTRIBUTING.md](./CONTRIBUTING.md)**.
+
---
## House rules & gotchas that bite
From eb083c573134b7a34fee98505ad4b494ef3517ec Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 21:35:55 -0700
Subject: [PATCH 017/190] =?UTF-8?q?feat:=20/issue=20=E2=80=94=20user-only?=
=?UTF-8?q?=20GitHub=20issue=20command=20+=20console=20form=20dialog=20(#1?=
=?UTF-8?q?315)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(chat): /issue — user-only GitHub issue-creation control command
Add a server-handled `/issue` chat control command (like `/goal`): the user
types it, the dispatcher short-circuits the turn, and an issue is filed via the
`gh` CLI. It is deliberately NOT a LangChain tool — the read-only GitHub tools
(plugins/github) stay agent-facing; creating an issue is a write kept in the
user's hands, so the agent can't open issues autonomously.
- tools/gh_issue.py — parse + scaffold + gate-conformance check + `gh issue
create`. The body is validated against the SAME rules as the CI issue gate
(>=80-char body + Problem section; bugs need repro/expected; features need
proposed-direction/acceptance), so anything filed here clears the gate. Repo
resolution: --repo > github.default_repo > GITHUB_DEFAULT_REPO/GH_REPO env >
ask (no silent misrouting). --dry-run previews without calling gh.
- server/chat.py — dispatch in both the streaming + non-streaming paths.
- graph/slash_commands.py — reserve the `issue` token (can't be shadowed).
- operator_api/console_handlers.py — surface /issue in the palette.
- graph/config.py + settings_schema.py — github.default_repo (UI-editable,
Settings > GitHub) + the three roundtrip goldens.
- tests/test_gh_issue.py — 14 unit tests (parse, conformance, dry-run, create).
- docs/guides/file-github-issues.md (+ sidebar) + README feature-map row.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(console): New-issue form dialog for /issue + share file_issue
The console UX for the user-only /issue command. Picking /issue from the chat
composer's slash menu opens a form dialog (DS Dialog, like the add-task dialog);
inline `/issue …` still works.
- tools/gh_issue.py — refactor: extract file_issue() (validate + gh create,
returns a structured result) + labels_for()/resolve_repo() helpers, shared by
the chat command and the new route so they can't diverge.
- operator_api/github_routes.py (new) — GET /api/github/config (default repo +
gh availability) and POST /api/github/issue (→ file_issue). Operator-surface
only, not an agent tool. Wired in server/__init__.py.
- apps/web NewIssueDialog.tsx + issueBody.ts — the form; buildBody() assembles
the exact `##` headings the gate checks, so a dialog-filed issue always
conforms. ChatSurface opens it from runClientSlash("issue"); on success a
"✓ Filed: " note is posted to the thread.
- api.ts — githubConfig() + createIssue().
- tests: test_github_routes.py (6, HTTP contract + validation), issueBody.test.ts
(6, body assembly + completeness).
- plugins/docs/nav.json — regenerate for the file-github-issues guide added with
the command (fixes the docs-nav-sync test).
Co-Authored-By: Claude Opus 4.8 (1M context)
* docs: fix dead link in file-github-issues guide (github plugin has no docs page)
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
README.md | 1 +
apps/web/src/chat/ChatSurface.tsx | 15 ++
apps/web/src/chat/NewIssueDialog.tsx | 193 +++++++++++++++++++
apps/web/src/chat/issueBody.test.ts | 61 ++++++
apps/web/src/chat/issueBody.ts | 46 +++++
apps/web/src/lib/api.ts | 28 +++
docs/.vitepress/config.mts | 1 +
docs/guides/file-github-issues.md | 70 +++++++
graph/config.py | 6 +
graph/settings_schema.py | 10 +
graph/slash_commands.py | 4 +-
operator_api/console_handlers.py | 12 +-
operator_api/github_routes.py | 70 +++++++
plugins/docs/nav.json | 4 +
server/__init__.py | 6 +
server/chat.py | 22 +++
tests/test_config_io.py | 1 +
tests/test_config_roundtrip.py | 6 +
tests/test_gh_issue.py | 119 ++++++++++++
tests/test_github_routes.py | 76 ++++++++
tools/gh_issue.py | 274 +++++++++++++++++++++++++++
21 files changed, 1022 insertions(+), 3 deletions(-)
create mode 100644 apps/web/src/chat/NewIssueDialog.tsx
create mode 100644 apps/web/src/chat/issueBody.test.ts
create mode 100644 apps/web/src/chat/issueBody.ts
create mode 100644 docs/guides/file-github-issues.md
create mode 100644 operator_api/github_routes.py
create mode 100644 tests/test_gh_issue.py
create mode 100644 tests/test_github_routes.py
create mode 100644 tools/gh_issue.py
diff --git a/README.md b/README.md
index 4a78d55f..90b7e0e9 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ rename / release-pipeline wiring.
| Subagents | `graph/subagents/config.py` | DeerFlow-pattern delegation via a `task()` tool; one worked example ships — a `researcher` (web + memory, plan→search→synthesize→cite) |
| Delegate to other agents | `plugins/delegates/`, `plugins/coding_agent/` | **`delegate_to`** routes a sub-task to another agent or endpoint over **a2a / openai / acp** — a **built-in** registry, managed + hot-swappable from the console (**Workspace settings ▸ Delegates**), with a health prober. The **acp** type spawns a CLI coding agent (e.g. protoCLI) over the Agent Client Protocol. See [Delegates](./docs/guides/delegates.md), [Spawn CLI coding agents](./docs/guides/coding-agents.md), ADR [0024](./docs/adr/0024-spawn-cli-coding-agents-acp.md) / [0025](./docs/adr/0025-unified-delegate-registry-and-panel.md) |
| Starter tools | `tools/lg_tools.py` | Default-on set: 4 keyless general (`current_time`, `calculator` safe AST eval, `web_search` via DuckDuckGo, `fetch_url`) + 2 HITL (`ask_human`, `request_user_input`) + `show_component` + 3 curation (`recent_activity`/`list_skills`/`save_skill`) + `set_goal`, plus — when their store is present — 4 memory + 3 scheduler + 4 tasks + 1 inbox. **Not** in `get_all_tools`: notes/docs tools (on-by-default plugins), `delegate_to` (built-in `delegates` plugin), GitHub read tools (opt-in `github` plugin). Drop any via `tools.disabled`; add via a plugin. See [Starter tools](./docs/reference/starter-tools.md) |
+| File GitHub issues | `tools/gh_issue.py` | **`/issue`** — a user-only chat control command (like `/goal`) that files a GitHub issue via the `gh` CLI, scaffolding + enforcing the required sections so it clears the repo's issue gate. **Not** an agent tool — creating issues stays in your hands (the `github` plugin's GitHub tools are read-only). Target repo = `--repo` › Settings ▸ GitHub › `GITHUB_DEFAULT_REPO`. See [File GitHub issues](./docs/guides/file-github-issues.md) |
| Knowledge store | `knowledge/store.py`, `knowledge/hybrid_store.py`, `ingestion/` | sqlite + FTS5 keyword search by default; an optional **hybrid** store adds embeddings + RRF fusion, and the **ingestion pipeline** pulls in txt/md/html/pdf/web/YouTube/audio/video sources. One `chunks` table for operator notes and conversation findings. Default-on; turn off with `middleware.knowledge: false` |
| Extensibility | `graph/skills/`, `tools/mcp_tools.py`, `graph/plugins/`, `plugins/` | Opt-in ways to extend a running agent without forking: **`SKILL.md` skills** (AgentSkills format, loaded on demand), **MCP servers** (external tools over stdio/HTTP), and **plugins** — drop-in packages that add tools, skills, subagents, workflows, FastAPI routes, background surfaces, managed MCP servers, **console rail views**, and their own config/secrets/Settings. Plugins are **installable from a git URL** (`python -m server plugin install `, pinned in `plugins.lock`) and shareable as repos — a repo is a full bundle. The first-party **Telegram** (`plugins/telegram`) integration ships bundled; **Discord**, **Slack**, and **Google** Gmail/Calendar install as external plugins from their own repos. See [Skills](./docs/guides/skills.md), [MCP](./docs/guides/mcp.md), [Plugins](./docs/guides/plugins.md), [Plugin console views](./docs/guides/plugin-views.md), [Install & publish plugins](./docs/guides/plugin-registry.md), ADR [0001](./docs/adr/0001-extensibility-and-plugin-architecture.md) / [0018](./docs/adr/0018-plugin-surfaces-routes-subagents.md) / [0019](./docs/adr/0019-plugin-config-settings-secrets.md) / [0026](./docs/adr/0026-plugin-contributed-console-surfaces.md) / [0027](./docs/adr/0027-install-plugins-from-git-url.md) |
| Scheduler | `scheduler/` | `schedule_task` / `list_schedules` / `cancel_schedule` tools backed by a bundled sqlite scheduler. Multi-agent-safe — every job is namespaced by `AGENT_NAME`. See [Schedule future work](./docs/guides/scheduler.md) |
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index 49928de3..2ec1876b 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -38,6 +38,7 @@ import {
import { ChatComponent } from "./ChatComponent";
import { ComposerModelSelect } from "./ComposerModelSelect";
import { Markdown } from "./LazyMarkdown";
+import { NewIssueDialog } from "./NewIssueDialog";
import { filesFromTransfer, isLargePaste, pastedTextFile } from "./paste";
import { ToolCalls } from "./ToolCalls";
import { addToolRef, appendReasoning, appendText, toolsForGroup } from "./parts";
@@ -311,6 +312,9 @@ function ChatSessionSlot({
const [commands, setCommands] = useState([]);
const [slashIndex, setSlashIndex] = useState(0);
const [slashDismissed, setSlashDismissed] = useState(false);
+ // `/issue` opens the New-issue form dialog (the console UX for the user-only
+ // issue command) rather than sending the raw text — see runClientSlash.
+ const [issueDialogOpen, setIssueDialogOpen] = useState(false);
useEffect(() => {
api.chatCommands().then((r) => setCommands(r.commands)).catch(() => {});
@@ -385,6 +389,12 @@ function ChatSessionSlot({
textareaRef.current?.focus();
return true;
}
+ if (verb === "issue") {
+ // Open the form dialog instead of sending. Inline `/issue …` still
+ // works: it only reaches here when picked from the menu with no args.
+ setIssueDialogOpen(true);
+ return true;
+ }
return false;
}
@@ -1218,6 +1228,11 @@ function ChatSessionSlot({
}}
/>
+ setIssueDialogOpen(false)}
+ onFiled={(note) => noteToThread(note)}
+ />
);
}
diff --git a/apps/web/src/chat/NewIssueDialog.tsx b/apps/web/src/chat/NewIssueDialog.tsx
new file mode 100644
index 00000000..f8f0b144
--- /dev/null
+++ b/apps/web/src/chat/NewIssueDialog.tsx
@@ -0,0 +1,193 @@
+import { DropdownSelect, Input, Textarea } from "@protolabsai/ui/forms";
+import { Dialog } from "@protolabsai/ui/overlays";
+import { Button } from "@protolabsai/ui/primitives";
+import { Bug, Github, Loader2, Send } from "lucide-react";
+import { useEffect, useMemo, useState } from "react";
+
+import { api } from "../lib/api";
+import { buildBody, EMPTY_FIELDS, type Fields, isComplete, type Kind } from "./issueBody";
+
+/**
+ * The console UX for the user-only `/issue` command: a form that files a GitHub
+ * issue via POST /api/github/issue (which shares the server's gate-conformance +
+ * `gh` path with the chat command). Opened from the composer by picking `/issue`.
+ */
+export function NewIssueDialog({
+ open,
+ onClose,
+ onFiled,
+}: {
+ open: boolean;
+ onClose: () => void;
+ onFiled: (note: string) => void;
+}) {
+ const [kind, setKind] = useState("bug");
+ const [repo, setRepo] = useState("");
+ const [title, setTitle] = useState("");
+ const [fields, setFields] = useState(EMPTY_FIELDS);
+ const [defaultRepo, setDefaultRepo] = useState("");
+ const [ghAvailable, setGhAvailable] = useState(true);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Reset + prefill (default repo, gh availability) each time the dialog opens.
+ useEffect(() => {
+ if (!open) return;
+ setKind("bug");
+ setTitle("");
+ setFields(EMPTY_FIELDS);
+ setError(null);
+ setBusy(false);
+ api
+ .githubConfig()
+ .then((c) => {
+ setDefaultRepo(c.default_repo);
+ setRepo(c.default_repo);
+ setGhAvailable(c.gh_available);
+ })
+ .catch(() => {});
+ }, [open]);
+
+ const set = (k: keyof Fields) => (e: { target: { value: string } }) =>
+ setFields((f) => ({ ...f, [k]: e.target.value }));
+
+ // Enable submit only when the issue will clear the gate (server stays source
+ // of truth on submit). `isComplete` mirrors the gate's required sections.
+ const canSubmit = useMemo(
+ () => !busy && isComplete(kind, title, repo, fields),
+ [title, repo, busy, fields, kind],
+ );
+
+ async function submit() {
+ setBusy(true);
+ setError(null);
+ try {
+ const res = await api.createIssue({
+ title: title.trim(),
+ body: buildBody(kind, fields),
+ kind,
+ repo: repo.trim() || undefined,
+ });
+ if (!res.ok) {
+ setError(res.error || (res.missing ? `Missing: ${res.missing.join("; ")}` : "Couldn't file the issue."));
+ return;
+ }
+ onFiled(`✓ Filed in ${res.repo ?? repo}: ${res.url ?? "(created)"}`);
+ onClose();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Request failed.");
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ return (
+
+ New GitHub issue
+ >
+ }
+ width="min(560px, 94vw)"
+ footer={
+ <>
+
+ Cancel
+
+
+ {busy ? : } File issue
+
+ >
+ }
+ >
+
+ {error ?
{error}
: null}
+ {!ghAvailable ? (
+
+ The gh CLI isn't installed on the host — filing will fail until it is.
+
+ ) : null}
+
+
+ Type
+ setKind(v as Kind)}
+ aria-label="Issue type"
+ options={[
+ { value: "bug", label: "Bug" },
+ { value: "feature", label: "Enhancement" },
+ ]}
+ />
+
+
+ Repo (owner/name)
+ setRepo(e.target.value)}
+ placeholder={defaultRepo || "owner/name"}
+ data-testid="issue-create-repo"
+ />
+
+
+
+ Title
+ setTitle(e.target.value)}
+ placeholder={kind === "bug" ? "What's broken, in one line" : "The capability, in one line"}
+ data-testid="issue-create-title"
+ />
+
+
+ {kind === "bug" ? "Problem / what's wrong" : "Problem / motivation"}
+
+
+ {kind === "bug" ? (
+ <>
+
+ Steps to reproduce / evidence
+
+
+
+ Expected vs. actual
+
+
+ >
+ ) : (
+
+ Proposed direction
+
+
+ )}
+
+ Acceptance
+
+
+
+ Refs (optional)
+
+
+
+
+ );
+}
diff --git a/apps/web/src/chat/issueBody.test.ts b/apps/web/src/chat/issueBody.test.ts
new file mode 100644
index 00000000..8e59b116
--- /dev/null
+++ b/apps/web/src/chat/issueBody.test.ts
@@ -0,0 +1,61 @@
+import { describe, expect, it } from "vitest";
+
+import { buildBody, EMPTY_FIELDS, type Fields, isComplete } from "./issueBody";
+
+const FULL: Fields = {
+ problem: "the wheel does nothing in the modal",
+ repro: "open modal, scroll",
+ expected: "expected scroll; got nothing",
+ proposal: "wire onWheel to the scroll container",
+ acceptance: "modal body scrolls",
+ refs: "#1300",
+};
+
+describe("buildBody", () => {
+ it("emits the bug sections the gate requires", () => {
+ const body = buildBody("bug", FULL);
+ expect(body).toContain("## Problem");
+ expect(body).toContain("## Steps to reproduce / evidence");
+ expect(body).toContain("## Expected vs. actual");
+ expect(body).toContain("## Acceptance");
+ expect(body).not.toContain("## Proposed direction");
+ });
+
+ it("emits the feature sections the gate requires", () => {
+ const body = buildBody("feature", FULL);
+ expect(body).toContain("## Problem");
+ expect(body).toContain("## Proposed direction");
+ expect(body).toContain("## Acceptance");
+ expect(body).not.toContain("## Steps to reproduce");
+ });
+
+ it("includes Refs only when provided", () => {
+ expect(buildBody("bug", FULL)).toContain("## Refs");
+ expect(buildBody("bug", { ...FULL, refs: " " })).not.toContain("## Refs");
+ });
+
+ it("clears the gate's 80-char floor", () => {
+ const collapsed = buildBody("feature", FULL).replace(/\s+/g, " ").trim();
+ expect(collapsed.length).toBeGreaterThanOrEqual(80);
+ });
+});
+
+describe("isComplete", () => {
+ it("requires title, repo, problem and acceptance", () => {
+ expect(isComplete("bug", "", "o/r", FULL)).toBe(false);
+ expect(isComplete("bug", "t", "", FULL)).toBe(false);
+ expect(isComplete("bug", "t", "o/r", { ...FULL, problem: "" })).toBe(false);
+ expect(isComplete("bug", "t", "o/r", { ...FULL, acceptance: "" })).toBe(false);
+ });
+
+ it("requires repro + expected for a bug, proposal for a feature", () => {
+ expect(isComplete("bug", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a", repro: "r" })).toBe(false);
+ expect(
+ isComplete("bug", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a", repro: "r", expected: "e" }),
+ ).toBe(true);
+ expect(isComplete("feature", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a" })).toBe(false);
+ expect(isComplete("feature", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a", proposal: "x" })).toBe(
+ true,
+ );
+ });
+});
diff --git a/apps/web/src/chat/issueBody.ts b/apps/web/src/chat/issueBody.ts
new file mode 100644
index 00000000..032baf8d
--- /dev/null
+++ b/apps/web/src/chat/issueBody.ts
@@ -0,0 +1,46 @@
+// Pure helpers for the New-issue dialog — no React, so they're unit-testable
+// without a DOM. The body is assembled with the exact `##` headings the server
+// gate checks for, so a dialog-filed issue always conforms (the same rules
+// tools.gh_issue / .github/workflows/issue-gate.yml enforce).
+
+export type Kind = "bug" | "feature";
+
+export type Fields = {
+ problem: string;
+ repro: string;
+ expected: string;
+ proposal: string;
+ acceptance: string;
+ refs: string;
+};
+
+export const EMPTY_FIELDS: Fields = {
+ problem: "",
+ repro: "",
+ expected: "",
+ proposal: "",
+ acceptance: "",
+ refs: "",
+};
+
+export function buildBody(kind: Kind, f: Fields): string {
+ const parts = [`## Problem\n${f.problem.trim()}`];
+ if (kind === "bug") {
+ parts.push(`## Steps to reproduce / evidence\n${f.repro.trim()}`);
+ parts.push(`## Expected vs. actual\n${f.expected.trim()}`);
+ } else {
+ parts.push(`## Proposed direction\n${f.proposal.trim()}`);
+ }
+ parts.push(`## Acceptance\n${f.acceptance.trim()}`);
+ if (f.refs.trim()) parts.push(`## Refs\n${f.refs.trim()}`);
+ return parts.join("\n\n");
+}
+
+// Mirror the gate's required sections so the dialog can enable submit only when
+// the issue will pass (the server stays the source of truth on submit).
+export function isComplete(kind: Kind, title: string, repo: string, f: Fields): boolean {
+ if (!title.trim() || !repo.trim()) return false;
+ if (!f.problem.trim() || !f.acceptance.trim()) return false;
+ if (kind === "bug") return !!f.repro.trim() && !!f.expected.trim();
+ return !!f.proposal.trim();
+}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
index 82430339..c79a2791 100644
--- a/apps/web/src/lib/api.ts
+++ b/apps/web/src/lib/api.ts
@@ -876,6 +876,34 @@ export const api = {
return request<{ commands: SlashCommand[] }>("/api/chat/commands");
},
+ // GitHub issue creation (the /issue command's form dialog). `githubConfig`
+ // prefills the dialog (default repo + whether `gh` is installed); `createIssue`
+ // shares the server's tools.gh_issue path with the chat /issue command.
+ githubConfig() {
+ return request<{ default_repo: string; gh_available: boolean }>("/api/github/config");
+ },
+
+ createIssue(body: {
+ title: string;
+ body: string;
+ kind: "bug" | "feature" | "generic";
+ repo?: string;
+ labels?: string[];
+ dry_run?: boolean;
+ }) {
+ return request<{
+ ok: boolean;
+ url?: string;
+ missing?: string[];
+ error?: string;
+ dry_run?: boolean;
+ repo?: string;
+ title?: string;
+ body?: string;
+ labels?: string[];
+ }>("/api/github/issue", { method: "POST", body });
+ },
+
settingsSchema() {
return request<{ groups: SettingsGroup[] }>("/api/settings/schema");
},
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 177a7d94..4ff5be56 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -82,6 +82,7 @@ export default defineConfig({
collapsed: false,
items: [
{ text: "Goal mode", link: "/guides/goal-mode" },
+ { text: "File GitHub issues (/issue)", link: "/guides/file-github-issues" },
{ text: "Schedule future work", link: "/guides/scheduler" },
{ text: "Middleware", link: "/guides/middleware" },
{ text: "Run on a coding agent (ACP runtime)", link: "/guides/acp-runtime" },
diff --git a/docs/guides/file-github-issues.md b/docs/guides/file-github-issues.md
new file mode 100644
index 00000000..2f1c731e
--- /dev/null
+++ b/docs/guides/file-github-issues.md
@@ -0,0 +1,70 @@
+# File GitHub issues (`/issue`)
+
+Type `/issue` in chat to file a GitHub issue straight from the console. It's a
+**user-only control command** — like [`/goal`](/guides/goal-mode), it short-circuits
+the turn and is handled by the server. It is deliberately **not** an agent tool, so
+the agent can't open issues on its own (the read-only GitHub tools in the
+`github` plugin stay agent-facing; *creating* an issue is a write you keep in your
+own hands).
+
+## Syntax
+
+```
+/issue [--bug|--feature] [--repo owner/name] [--label a,b] [--dry-run]
+
+
+```
+
+- The **first line** carries the title plus flags; everything after the first newline
+ is the issue body (markdown).
+- `--bug` applies the `bug` label; `--feature` (alias `--feat`/`--enhancement`) applies
+ `enhancement`. `--label a,b` adds more.
+- `--dry-run` shows exactly what would be filed without calling GitHub — useful to
+ check the body before committing.
+
+### Example
+
+```
+/issue Touchpad scroll dead in the delegate modal --bug
+
+## Problem
+The scroll wheel does nothing inside the delegate setup modal.
+
+## Steps to reproduce
+1. Open delegate setup 2. hover the modal body 3. scroll
+
+## Expected vs. actual
+Expected the body to scroll; nothing moves.
+
+## Acceptance
+Wheel scrolls the modal body on macOS + Linux.
+```
+
+## It writes issues that pass the gate
+
+The body is checked against the **same requirements the repo's issue gate enforces**
+(`.github/workflows/issue-gate.yml`), so anything `/issue` files clears the gate:
+
+- always — a substantive body **and** a *Problem / What's-wrong / Motivation* section;
+- `--bug` — also *Steps to reproduce / Evidence / Expected-vs-actual*;
+- `--feature` — also a *Proposed direction* or *Acceptance* section.
+
+If a required section is missing, nothing is filed — the command replies with what's
+missing and a ready-to-fill scaffold. Run `/issue --bug` with no body to get the
+scaffold up front.
+
+## Which repo
+
+The target repo is resolved, in order:
+
+1. an explicit `--repo owner/name`;
+2. **Settings ▸ GitHub ▸ Default repo for /issue** (`github.default_repo`);
+3. the `GITHUB_DEFAULT_REPO` (or `GH_REPO`) environment variable.
+
+If none is set, the command asks for `--repo` rather than guess — no silent misrouting.
+
+## Auth
+
+Issue creation runs through the `gh` CLI. Set `GITHUB_TOKEN`/`GH_TOKEN` (needs **write**
+scope on the target repo) or sign in with `gh auth login` on the host. Without write
+auth `gh` returns a readable error, which `/issue` surfaces back to you.
diff --git a/graph/config.py b/graph/config.py
index 2476864d..e7ede450 100644
--- a/graph/config.py
+++ b/graph/config.py
@@ -571,6 +571,11 @@ class LangGraphConfig:
# YAML ⊕ secrets overlay). A plugin reads its own via plugin_config[""].
plugin_config: dict = field(default_factory=dict)
+ # Default GitHub repo (``owner/name``) for the user-only ``/issue`` command
+ # (tools/gh_issue.py). Blank = require an explicit ``--repo`` (or the
+ # GITHUB_DEFAULT_REPO / GH_REPO env). UI-editable under Settings ▸ GitHub.
+ github_default_repo: str = ""
+
# Identity — captured by the setup wizard, editable via the drawer.
# ``identity_name`` falls back to the AGENT_NAME env var at runtime;
# the YAML value wins when both are set so per-fork customization
@@ -825,6 +830,7 @@ def from_dict(
tools_disabled=list(data.get("tools", {}).get("disabled", []) or []),
routing_fallback_models=data.get("routing", {}).get("fallback_models", []),
aux_model=data.get("routing", {}).get("aux_model", cls.aux_model),
+ github_default_repo=data.get("github", {}).get("default_repo", cls.github_default_repo),
goal_enabled=data.get("goal", {}).get("enabled", cls.goal_enabled),
goal_max_iterations=data.get("goal", {}).get("max_iterations", cls.goal_max_iterations),
goal_no_progress_limit=data.get("goal", {}).get("no_progress_limit", cls.goal_no_progress_limit),
diff --git a/graph/settings_schema.py b/graph/settings_schema.py
index 7ca0e869..2a7e8af7 100644
--- a/graph/settings_schema.py
+++ b/graph/settings_schema.py
@@ -651,6 +651,16 @@ class Field:
minimum=0,
scope="host",
),
+ # ── GitHub ─────────────────────────────────────────────────────────────────
+ Field(
+ "github.default_repo",
+ "github_default_repo",
+ "Default repo for /issue",
+ "string",
+ "GitHub",
+ "Target repo (owner/name) the user-only /issue command files into when no "
+ "--repo is given. Blank = require --repo each time (or the GITHUB_DEFAULT_REPO env).",
+ ),
]
_BY_KEY = {f.key: f for f in FIELDS}
diff --git a/graph/slash_commands.py b/graph/slash_commands.py
index 4d842dc0..7421551a 100644
--- a/graph/slash_commands.py
+++ b/graph/slash_commands.py
@@ -54,13 +54,15 @@ def find_user_facing_skill(name: str):
def slash_kind(name: str) -> str | None:
"""The kind a ``/`` slash command resolves to — the SINGLE source of
precedence shared by the chat dispatcher and the console palette, so they can
- never disagree about what a token does. Reserved: ``goal``. Precedence:
+ never disagree about what a token does. Reserved: ``goal``, ``issue``. Precedence:
workflow > subagent > user-facing skill. Returns ``None`` for an unknown token.
(Workflows/subagents match the bare name; skills match a slug.)"""
if not name:
return None
if name == "goal" or slugify_slash(name) == "goal":
return "goal"
+ if name == "issue" or slugify_slash(name) == "issue":
+ return "issue"
if STATE.workflow_registry is not None and STATE.workflow_registry.get(name) is not None:
return "workflow"
try:
diff --git a/operator_api/console_handlers.py b/operator_api/console_handlers.py
index e80dbba6..a237379a 100644
--- a/operator_api/console_handlers.py
+++ b/operator_api/console_handlers.py
@@ -517,8 +517,8 @@ def _operator_chat_commands() -> dict:
The workflow/subagent/skill inventory + precedence comes from the SAME
resolver the chat dispatcher uses (``server.chat.resolve_slash_commands``), so
- the palette can't drift from what actually runs. ``/goal`` (a server-handled
- control command) is surfaced here when goal mode is loaded."""
+ the palette can't drift from what actually runs. ``/goal`` and ``/issue``
+ (server-handled control commands) are surfaced here."""
from graph.slash_commands import resolve_slash_commands
commands = []
@@ -531,5 +531,13 @@ def _operator_chat_commands() -> dict:
"usage": "/goal · /goal (status) · /goal clear",
}
)
+ commands.append(
+ {
+ "name": "issue",
+ "kind": "control",
+ "description": "File a GitHub issue (user-only — not an agent tool).",
+ "usage": "/issue --bug|--feature [--repo owner/name] [--dry-run] · then the body below",
+ }
+ )
commands.extend(resolve_slash_commands())
return {"commands": commands}
diff --git a/operator_api/github_routes.py b/operator_api/github_routes.py
new file mode 100644
index 00000000..7f87e0f2
--- /dev/null
+++ b/operator_api/github_routes.py
@@ -0,0 +1,70 @@
+"""GitHub issue creation for the console — the write counterpart to the read-only
+``github`` plugin tools.
+
+Backs the **New issue** form dialog (the console UX for the user-only ``/issue``
+command). ``POST /api/github/issue`` files an issue through
+``tools.gh_issue.file_issue`` — the *same* gate-conformance check + ``gh`` path the
+chat ``/issue`` command uses — so the dialog and the command can never diverge.
+``GET /api/github/config`` gives the dialog its prefilled defaults.
+
+This is operator-surface only; it is deliberately not exposed to the agent as a
+tool (creating issues stays user-driven).
+"""
+
+from __future__ import annotations
+
+import logging
+
+log = logging.getLogger("protoagent.server")
+
+
+def _default_repo() -> str:
+ """The configured default repo (``github.default_repo``), or ``""``."""
+ from runtime.state import STATE
+
+ cfg = getattr(STATE, "graph_config", None)
+ return getattr(cfg, "github_default_repo", "") or ""
+
+
+def register_github_routes(app) -> None:
+ import re
+ import shutil
+
+ from fastapi import Body
+
+ from tools.gh_issue import IssueRequest, file_issue, labels_for, resolve_repo
+
+ _REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
+
+ @app.get("/api/github/config")
+ async def _github_config():
+ """Defaults the New-issue dialog prefills: the effective default repo
+ (config ⊕ env) and whether the ``gh`` CLI is installed."""
+ return {
+ "default_repo": resolve_repo(None, _default_repo()) or "",
+ "gh_available": shutil.which("gh") is not None,
+ }
+
+ @app.post("/api/github/issue")
+ async def _github_create_issue(body: dict = Body(...)):
+ """File a GitHub issue from the console form. Returns the structured
+ ``file_issue`` result (``{ok, url|missing|error, ...}``)."""
+ kind = (body.get("kind") or "generic").lower()
+ if kind not in ("bug", "feature", "generic"):
+ kind = "generic"
+ title = (body.get("title") or "").strip()
+ issue_body = (body.get("body") or "").strip()
+ repo = resolve_repo(body.get("repo"), _default_repo())
+ labels = labels_for(kind, [str(x) for x in (body.get("labels") or [])])
+ dry_run = bool(body.get("dry_run"))
+
+ if not title:
+ return {"ok": False, "error": "Title is required."}
+ if not repo:
+ return {"ok": False, "error": "No target repo — set one in Settings ▸ GitHub, or enter one."}
+ if not _REPO_RE.match(repo):
+ return {"ok": False, "error": f"Repo must be 'owner/name' (got {repo!r})."}
+
+ return await file_issue(
+ IssueRequest(title=title, body=issue_body, kind=kind, repo=repo, labels=labels, dry_run=dry_run)
+ )
diff --git a/plugins/docs/nav.json b/plugins/docs/nav.json
index 28cec356..68ee006c 100644
--- a/plugins/docs/nav.json
+++ b/plugins/docs/nav.json
@@ -49,6 +49,10 @@
"path": "guides/goal-mode.md",
"title": "Goal mode"
},
+ {
+ "path": "guides/file-github-issues.md",
+ "title": "File GitHub issues (/issue)"
+ },
{
"path": "guides/scheduler.md",
"title": "Schedule future work"
diff --git a/server/__init__.py b/server/__init__.py
index c25c7caf..5a1154ab 100644
--- a/server/__init__.py
+++ b/server/__init__.py
@@ -669,6 +669,12 @@ async def _scheduler_shutdown() -> None:
register_theme_routes(fastapi_app)
register_mcp_routes(fastapi_app)
+ # GitHub issue creation (the /issue command's form dialog) — write counterpart
+ # to the read-only github plugin tools; operator-surface only.
+ from operator_api.github_routes import register_github_routes
+
+ register_github_routes(fastapi_app)
+
# --- Telemetry (ADR 0006 Slice 2) --------------------------------------
# Per-turn cost/latency + advise-only insights (ADR 0006). Extracted to
# operator_api/telemetry_routes.py (ADR 0023 phase 3).
diff --git a/server/chat.py b/server/chat.py
index f86f9251..1f56b3ba 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -685,6 +685,18 @@ async def _chat_langgraph_stream(
yield ("done", reply)
return
+ # Issue control command (/issue ...) short-circuits the turn: file a
+ # GitHub issue. User-only by design — it is NOT an agent tool, so the
+ # model can't create issues autonomously (mirrors /goal).
+ from tools.gh_issue import parse_issue_control
+
+ issue_reply = await parse_issue_control(
+ message, default_repo=getattr(STATE.graph_config, "github_default_repo", "")
+ )
+ if issue_reply is not None:
+ yield ("done", issue_reply)
+ return
+
# Workflow slash command (/ …) short-circuits the turn:
# run the recipe and return its output. Each step renders its own
# tool card (gather → angles → brief) so a multi-step workflow shows
@@ -1018,6 +1030,16 @@ async def _chat_langgraph(message: str, session_id: str, *, model: str | None =
if reply is not None:
return [{"role": "assistant", "content": reply}]
+ # Issue control command (/issue ...) short-circuits — file a GitHub
+ # issue (user-only; never an agent tool). See the streaming path.
+ from tools.gh_issue import parse_issue_control
+
+ issue_reply = await parse_issue_control(
+ message, default_repo=getattr(STATE.graph_config, "github_default_repo", "")
+ )
+ if issue_reply is not None:
+ return [{"role": "assistant", "content": issue_reply}]
+
# Workflow slash command (/ …) short-circuits the turn.
parsed = _parse_workflow_command(message)
if parsed is not None:
diff --git a/tests/test_config_io.py b/tests/test_config_io.py
index 705f8c24..e70d2b46 100644
--- a/tests/test_config_io.py
+++ b/tests/test_config_io.py
@@ -150,6 +150,7 @@ def test_config_to_dict_mirrors_yaml_shape() -> None:
"prompt_cache",
"routing",
"telemetry",
+ "github",
# Box runtime (Host layer, ADR 0047 D8).
"network",
"fleet",
diff --git a/tests/test_config_roundtrip.py b/tests/test_config_roundtrip.py
index c9676844..037a49f5 100644
--- a/tests/test_config_roundtrip.py
+++ b/tests/test_config_roundtrip.py
@@ -88,6 +88,7 @@ def _isolate_from_installed_plugins(monkeypatch):
"fleet_max_warm": 0,
"fleet_port_base": 7870,
"fleet_warm_grace_seconds": 0,
+ "github_default_repo": "",
"goal_enabled": True,
"goal_eval_model": "",
"goal_max_iterations": 8,
@@ -221,6 +222,9 @@ def _isolate_from_installed_plugins(monkeypatch):
"grace_seconds": 0,
},
},
+ "github": {
+ "default_repo": "",
+ },
"goal": {
"enabled": True,
"eval_model": "",
@@ -423,6 +427,8 @@ def _isolate_from_installed_plugins(monkeypatch):
"compaction_trigger",
"compaction_keep_messages",
"compaction_model",
+ # github.*
+ "github_default_repo",
# goal.*
"goal_enabled",
"goal_max_iterations",
diff --git a/tests/test_gh_issue.py b/tests/test_gh_issue.py
new file mode 100644
index 00000000..e71ef58a
--- /dev/null
+++ b/tests/test_gh_issue.py
@@ -0,0 +1,119 @@
+"""Unit tests for the user-only ``/issue`` control command (tools/gh_issue.py).
+
+Pure parse/validation logic + the create path with ``run_gh`` mocked — no
+network, no real ``gh``.
+"""
+
+from __future__ import annotations
+
+from unittest.mock import patch
+
+import pytest
+
+from tools.gh_issue import (
+ is_issue_command,
+ missing_sections,
+ parse_issue_control,
+)
+
+_GOOD_BUG = (
+ "/issue Scroll dead in delegate modal --bug --repo o/r\n"
+ "## Problem\nThe wheel does nothing inside the modal.\n\n"
+ "## Steps to reproduce\n1. open modal 2. scroll\n\n"
+ "## Expected vs actual\nExpected scroll; nothing happens.\n\n"
+ "## Acceptance\nWheel scrolls the modal body."
+)
+
+
+def test_is_issue_command():
+ assert is_issue_command("/issue foo")
+ assert is_issue_command("/issue")
+ assert is_issue_command("/issue\nbody")
+ assert not is_issue_command("/issues foo") # not a prefix match
+ assert not is_issue_command("hello /issue")
+ assert not is_issue_command(None) # type: ignore[arg-type]
+
+
+def test_missing_sections_by_kind():
+ body = "## Problem\nx" + " padding" * 20
+ assert missing_sections(body, "generic") == []
+ # bug needs repro/evidence/expected on top of Problem
+ assert any("reproduce" in m.lower() for m in missing_sections(body, "bug"))
+ # feature needs a proposed-direction or acceptance
+ assert any("proposed" in m.lower() for m in missing_sections(body, "feature"))
+ # a thin body is flagged regardless
+ assert any("substantive" in m for m in missing_sections("## Problem\nx", "generic"))
+
+
+async def test_non_issue_returns_none():
+ assert await parse_issue_control("just chatting") is None
+ assert await parse_issue_control("/goal win") is None
+
+
+async def test_no_title_returns_usage_and_scaffold():
+ out = await parse_issue_control("/issue", default_repo="o/r")
+ assert "Usage:" in out and "## Problem" in out
+
+
+async def test_no_repo_errors():
+ out = await parse_issue_control("/issue Title\n## Problem\n" + "x" * 80)
+ assert "No target repo" in out
+
+
+async def test_bad_repo_errors():
+ out = await parse_issue_control("/issue Title --repo not-a-repo\n## Problem\n" + "x" * 80)
+ assert "owner/name" in out
+
+
+async def test_missing_sections_block_creation():
+ out = await parse_issue_control("/issue Title --bug --repo o/r\nthis body has no headings at all here")
+ assert out.startswith("Not filed")
+ assert "Steps to reproduce" in out # scaffold for a bug
+
+
+async def test_dry_run_does_not_call_gh():
+ # --dry-run must be on the first line (the title+flags line), not the body.
+ msg = _GOOD_BUG.replace("--bug --repo o/r", "--bug --repo o/r --dry-run", 1)
+ with patch("tools.gh_issue.run_gh") as run:
+ out = await parse_issue_control(msg)
+ run.assert_not_called()
+ assert out.startswith("Dry run")
+ assert "o/r" in out and "bug" in out
+
+
+async def test_happy_path_creates_and_labels():
+ url = "https://github.com/o/r/issues/42"
+ with patch("tools.gh_issue.run_gh", return_value=(0, url, "")) as run:
+ out = await parse_issue_control(_GOOD_BUG)
+ assert url in out and "Filed in o/r" in out
+ args = run.call_args.args[0]
+ assert args[:2] == ["issue", "create"]
+ assert "--repo" in args and "o/r" in args
+ assert "--label" in args and "bug" in args
+
+
+async def test_default_repo_used_when_no_flag():
+ with patch("tools.gh_issue.run_gh", return_value=(0, "https://x/1", "")) as run:
+ out = await parse_issue_control(
+ "/issue Title --feature\n## Problem\nwhy this matters enough to file it here and now\n\n"
+ "## Proposed direction\nthe approach we would take to address it",
+ default_repo="acme/widgets",
+ )
+ assert "acme/widgets" in out
+ args = run.call_args.args[0]
+ assert "acme/widgets" in args
+ assert "enhancement" in args # feature → enhancement label
+
+
+async def test_gh_failure_surfaces_error():
+ with patch("tools.gh_issue.run_gh", return_value=(1, "", "label 'bug' not found")):
+ out = await parse_issue_control(_GOOD_BUG)
+ assert out.startswith("Error") or "Hint" in out
+
+
+@pytest.mark.parametrize("flag,label", [("--bug", "bug"), ("--feature", "enhancement"), ("--feat", "enhancement")])
+async def test_type_flags_map_to_labels(flag, label):
+ body = "## Problem\nwhy it matters here padding padding\n\n## Acceptance\ncriteria\n\n## Steps to reproduce\n1"
+ with patch("tools.gh_issue.run_gh", return_value=(0, "https://x/1", "")) as run:
+ await parse_issue_control(f"/issue Title {flag} --repo o/r\n{body}")
+ assert label in run.call_args.args[0]
diff --git a/tests/test_github_routes.py b/tests/test_github_routes.py
new file mode 100644
index 00000000..5b5b27c2
--- /dev/null
+++ b/tests/test_github_routes.py
@@ -0,0 +1,76 @@
+"""Console GitHub issue route (POST /api/github/issue, GET /api/github/config).
+
+Thin layer over tools.gh_issue (covered in test_gh_issue); here we check the
+HTTP contract + validation, with `gh` mocked.
+"""
+
+from __future__ import annotations
+
+from unittest.mock import patch
+
+import pytest
+
+
+@pytest.fixture
+def client():
+ from fastapi import FastAPI
+ from fastapi.testclient import TestClient
+
+ from operator_api.github_routes import register_github_routes
+
+ app = FastAPI()
+ register_github_routes(app)
+ return TestClient(app)
+
+
+_BODY = (
+ "## Problem\nthe scroll wheel does nothing inside the delegate modal here\n\n"
+ "## Steps to reproduce\nopen the modal and scroll the body region\n\n"
+ "## Expected vs actual\nexpected scroll; nothing happens\n\n"
+ "## Acceptance\nthe modal body scrolls on macOS and linux"
+)
+
+
+def test_config_reports_gh_availability(client):
+ with patch("shutil.which", return_value="/usr/bin/gh"):
+ r = client.get("/api/github/config")
+ assert r.status_code == 200
+ assert r.json()["gh_available"] is True
+
+
+def test_create_happy_path(client):
+ url = "https://github.com/o/r/issues/7"
+ with patch("tools.gh_issue.run_gh", return_value=(0, url, "")) as run:
+ r = client.post("/api/github/issue", json={"title": "Scroll dead", "body": _BODY, "kind": "bug", "repo": "o/r"})
+ j = r.json()
+ assert j["ok"] and j["url"] == url
+ assert "bug" in run.call_args.args[0] # bug → label applied
+
+
+def test_create_rejects_missing_sections(client):
+ with patch("tools.gh_issue.run_gh") as run:
+ r = client.post("/api/github/issue", json={"title": "x", "body": "too thin", "kind": "bug", "repo": "o/r"})
+ run.assert_not_called()
+ j = r.json()
+ assert j["ok"] is False and j["missing"]
+
+
+def test_create_requires_repo(client):
+ r = client.post("/api/github/issue", json={"title": "x", "body": _BODY, "kind": "bug"})
+ j = r.json()
+ assert j["ok"] is False and "repo" in j["error"].lower()
+
+
+def test_create_rejects_bad_repo(client):
+ r = client.post("/api/github/issue", json={"title": "x", "body": _BODY, "kind": "bug", "repo": "nope"})
+ assert r.json()["ok"] is False
+
+
+def test_dry_run_does_not_call_gh(client):
+ with patch("tools.gh_issue.run_gh") as run:
+ r = client.post(
+ "/api/github/issue",
+ json={"title": "x", "body": _BODY, "kind": "bug", "repo": "o/r", "dry_run": True},
+ )
+ run.assert_not_called()
+ assert r.json()["dry_run"] is True
diff --git a/tools/gh_issue.py b/tools/gh_issue.py
new file mode 100644
index 00000000..f4b74c9c
--- /dev/null
+++ b/tools/gh_issue.py
@@ -0,0 +1,274 @@
+"""``/issue`` — the user-only GitHub issue-creation control command.
+
+This is the **write** counterpart to the read-only GitHub *tools*
+(``tools/github_tools.py``). Those are agent-facing; creating an issue is a
+write the agent must NOT do autonomously, so this is deliberately **not** a
+LangChain tool. Instead it's a server-handled chat control command (like
+``/goal``): the user types ``/issue …`` and the dispatcher short-circuits the
+turn — the model never sees it as something it can call.
+
+Syntax (inline, one message — a console form dialog is a planned follow-up)::
+
+ /issue [--bug|--feature] [--repo owner/name] [--label a,b] [--dry-run]
+
+ ## Problem
+ …
+ ## Steps to reproduce / Acceptance
+ …
+
+The first line carries the title + flags; everything after the first newline is
+the body. The body is checked against the SAME requirements the CI issue gate
+enforces (`.github/workflows/issue-gate.yml`), so an issue filed here always
+passes the gate:
+
+- always: a non-trivial body + a Problem / What's-wrong / Motivation section;
+- ``--bug`` → label ``bug``, also needs repro / evidence / expected-vs-actual;
+- ``--feature`` → label ``enhancement``, also needs a proposed-direction or
+ acceptance section.
+
+Repo resolution (no silent misrouting): explicit ``--repo`` > the configured
+``github.default_repo`` > ``GITHUB_DEFAULT_REPO`` / ``GH_REPO`` env > an error
+asking for one. Auth rides on ``tools.gh_cli`` (``GITHUB_TOKEN``/``GH_TOKEN`` or
+ambient ``gh auth``); ``gh issue create`` needs write scope.
+"""
+
+from __future__ import annotations
+
+import os
+import re
+import shlex
+from dataclasses import dataclass, field
+
+from tools.gh_cli import check_gh_error, run_gh
+
+_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
+
+# Section detectors — kept in lockstep with the CI gate's regexes so the local
+# check and the server-side gate can never disagree about what "conforms" means.
+_SECTION_RES = {
+ "problem": re.compile(r"problem|what'?s? wrong|motivation|background|context|summary", re.I),
+ "repro": re.compile(r"repro|reproduce|steps|evidence|expected|actual|observed", re.I),
+ "proposal": re.compile(r"propos|solution|approach|direction|fix|design", re.I),
+ "acceptance": re.compile(r"acceptance|done when|success criteria|definition of done", re.I),
+}
+# A heading (#..######) or a bold line (**…**) — same shape the gate matches.
+_HEADING_RE = re.compile(r"^\s*(?:#{1,6}\s+|\*\*\s*)(.+?)(?:\s*\*\*)?\s*$")
+
+_BUG_SCAFFOLD = (
+ "## Problem / what's wrong\n\n\n"
+ "## Steps to reproduce / evidence\n\n\n"
+ "## Expected vs. actual\n\n\n"
+ "## Acceptance\n\n"
+)
+_FEATURE_SCAFFOLD = (
+ "## Problem / motivation\n\n\n"
+ "## Proposed direction\n\n\n"
+ "## Acceptance\n\n"
+)
+_GENERIC_SCAFFOLD = (
+ "## Problem\n\n\n"
+ "## Acceptance\n\n"
+)
+
+
+@dataclass
+class IssueRequest:
+ title: str
+ body: str
+ kind: str # "bug" | "feature" | "generic"
+ repo: str | None = None
+ labels: list[str] = field(default_factory=list)
+ dry_run: bool = False
+
+
+def _has_section(body: str, key: str) -> bool:
+ rx = _SECTION_RES[key]
+ for line in body.splitlines():
+ m = _HEADING_RE.match(line)
+ if m and rx.search(m.group(1)):
+ return True
+ return False
+
+
+def _scaffold(kind: str) -> str:
+ return {"bug": _BUG_SCAFFOLD, "feature": _FEATURE_SCAFFOLD}.get(kind, _GENERIC_SCAFFOLD)
+
+
+def missing_sections(body: str, kind: str) -> list[str]:
+ """The gate-required sections absent from ``body`` for this issue ``kind``."""
+ miss: list[str] = []
+ # Same metric as the CI gate (collapsed-whitespace length >= 80), so an issue
+ # that passes here is guaranteed to clear the server-side gate.
+ if len(" ".join(body.split())) < 80:
+ miss.append("a substantive description (>= 80 chars)")
+ if not _has_section(body, "problem"):
+ miss.append("a Problem / What's-wrong / Motivation section")
+ if kind == "bug" and not _has_section(body, "repro"):
+ miss.append("Steps to reproduce / Evidence / Expected-vs-actual")
+ if kind == "feature" and not (_has_section(body, "proposal") or _has_section(body, "acceptance")):
+ miss.append("a Proposed-direction or Acceptance section")
+ return miss
+
+
+def labels_for(kind: str, extra: list[str] | None = None) -> list[str]:
+ """Labels for an issue of this ``kind`` — the type label first (``bug`` /
+ ``enhancement``), then any extras, de-duped in order."""
+ out: list[str] = []
+ if kind == "bug":
+ out.append("bug")
+ elif kind == "feature":
+ out.append("enhancement")
+ for lbl in extra or []:
+ if lbl and lbl not in out:
+ out.append(lbl)
+ return out
+
+
+def resolve_repo(explicit: str | None, default_repo: str = "") -> str | None:
+ """Target repo: explicit ``--repo`` > configured default > GITHUB_DEFAULT_REPO
+ / GH_REPO env > ``None`` (caller errors — there is no silent default)."""
+ return (
+ (explicit or "").strip()
+ or (default_repo or "").strip()
+ or os.environ.get("GITHUB_DEFAULT_REPO")
+ or os.environ.get("GH_REPO")
+ or None
+ )
+
+
+async def file_issue(req: IssueRequest) -> dict:
+ """Validate ``req`` against the gate rules, then create the issue via ``gh``
+ (or, for ``dry_run``, report what would be filed). Returns a structured
+ result the chat command and the console dialog both render:
+
+ - ``{"ok": False, "missing": [...], "kind": ...}`` — body fails the gate;
+ - ``{"ok": False, "error": "..."}`` — ``gh`` failed (auth / label / repo);
+ - ``{"ok": True, "dry_run": True, ...}`` — preview only, nothing created;
+ - ``{"ok": True, "url": "...", "repo": ..., "labels": [...]}`` — created.
+
+ Assumes ``req.repo`` is already set + validated (the callers do that).
+ """
+ miss = missing_sections(req.body, req.kind)
+ if miss:
+ return {"ok": False, "missing": miss, "kind": req.kind}
+ if req.dry_run:
+ return {
+ "ok": True,
+ "dry_run": True,
+ "repo": req.repo,
+ "title": req.title,
+ "body": req.body,
+ "labels": req.labels,
+ }
+ args = ["issue", "create", "--repo", req.repo, "--title", req.title, "--body", req.body]
+ for lbl in req.labels:
+ args += ["--label", lbl]
+ rc, out, serr = await run_gh(args, timeout=45)
+ err = check_gh_error(rc, serr)
+ if err:
+ if "could not add label" in serr.lower() or "not found" in serr.lower():
+ err += f" (the label may not exist in {req.repo}; create it or drop the label.)"
+ return {"ok": False, "error": err}
+ url = out.strip().splitlines()[-1] if out.strip() else ""
+ return {"ok": True, "url": url, "repo": req.repo, "labels": req.labels}
+
+
+def _parse(rest: str, *, default_repo: str = "") -> IssueRequest | str:
+ """Parse the raw ``/issue`` argument string into a request, or an error string."""
+ first, _, body = rest.partition("\n")
+ body = body.strip()
+
+ kind = "generic"
+ labels: list[str] = []
+ repo: str | None = None
+ dry_run = False
+
+ # Tokenize only the first line (title + flags); the body is taken verbatim.
+ try:
+ tokens = shlex.split(first)
+ except ValueError:
+ tokens = first.split()
+
+ title_parts: list[str] = []
+ i = 0
+ while i < len(tokens):
+ tok = tokens[i]
+ low = tok.lower()
+ if low in ("--bug", "--fix"):
+ kind = "bug"
+ elif low in ("--feature", "--feat", "--enhancement"):
+ kind = "feature"
+ elif low in ("--dry-run", "--dryrun"):
+ dry_run = True
+ elif low.startswith("--repo"):
+ val = tok.split("=", 1)[1] if "=" in tok else (tokens[i + 1] if i + 1 < len(tokens) else "")
+ if "=" not in tok:
+ i += 1
+ repo = val.strip()
+ elif low.startswith("--label"):
+ val = tok.split("=", 1)[1] if "=" in tok else (tokens[i + 1] if i + 1 < len(tokens) else "")
+ if "=" not in tok:
+ i += 1
+ labels += [s.strip() for s in val.split(",") if s.strip()]
+ else:
+ title_parts.append(tok)
+ i += 1
+
+ title = " ".join(title_parts).strip()
+ labels = labels_for(kind, labels)
+ repo = resolve_repo(repo, default_repo)
+
+ if not title:
+ return (
+ "Usage: `/issue [--bug|--feature] [--repo owner/name] [--dry-run]` "
+ "then the body on the following lines.\n\n"
+ "Scaffold to fill in:\n```\n" + _scaffold(kind) + "```"
+ )
+ if not repo:
+ return (
+ "No target repo. Pass `--repo owner/name`, or set `github.default_repo` "
+ "in Settings (or the `GITHUB_DEFAULT_REPO` env var)."
+ )
+ if not _REPO_RE.match(repo):
+ return f"Error: --repo must be 'owner/name' (got {repo!r})."
+
+ return IssueRequest(title=title, body=body, kind=kind, repo=repo, labels=labels, dry_run=dry_run)
+
+
+def is_issue_command(message: str) -> bool:
+ """True when ``message`` is a ``/issue`` control command."""
+ if not isinstance(message, str):
+ return False
+ s = message.strip().lower()
+ return s == "/issue" or s.startswith("/issue ") or s.startswith("/issue\n")
+
+
+async def parse_issue_control(message: str, *, default_repo: str = "") -> str | None:
+ """Handle a ``/issue`` control message: validate, then create the issue (or
+ report what's missing / what a dry-run would do). Returns the reply string
+ when the message *was* an ``/issue`` command (caller short-circuits the
+ turn), else ``None``."""
+ if not is_issue_command(message):
+ return None
+ rest = message.strip()[len("/issue"):].strip()
+ parsed = _parse(rest, default_repo=default_repo)
+ if isinstance(parsed, str):
+ return parsed # usage / scaffold / validation error
+
+ result = await file_issue(parsed)
+ if not result["ok"] and result.get("missing"):
+ return (
+ "Not filed — this issue is missing " + "; ".join(result["missing"]) + ".\n\n"
+ "Add the section(s) and resend. Scaffold for a "
+ f"{parsed.kind} issue:\n```\n" + _scaffold(parsed.kind) + "```"
+ )
+ if not result["ok"]:
+ return result.get("error", "Error filing issue.")
+
+ label_note = f" · labels: {', '.join(parsed.labels)}" if parsed.labels else ""
+ if result.get("dry_run"):
+ return (
+ f"Dry run — would create in **{parsed.repo}**{label_note}:\n\n"
+ f"**{parsed.title}**\n\n{parsed.body}"
+ )
+ return f"Filed in {parsed.repo}{label_note}: {result.get('url') or '(created)'}"
From c8139989f3a7a4d48b9c5f11a7d30a24f4814e0d Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 22:46:00 -0700
Subject: [PATCH 018/190] feat(issue): repo picker list + util-bar bug action +
tooltip popovers (#1316)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Builds on the /issue command + dialog (#1315):
- Multi-repo picker. New `github.repos` config (list) drives a quick-toggle
dropdown in the New-issue dialog; `github.default_repo` is the preselected one
(and the command's default), falling back to the first repo when blank. A
"Custom…" option swaps in a free-text field (inline × to return to the list)
for one-off repos. Pairs with the portfolio manager's many-repo setup.
`effective_default_repo()` keeps the command + dialog agreeing on the default.
- Util-bar bug action. A 🐛 button next to Settings opens the same dialog,
store-driven (`uiStore.newIssueOpen`) so the button and the chat `/issue`
command share one mount; on success it notes "✓ Filed … " in the chat.
- Tooltip popovers. Settings, the new bug action, and the three panel-toggle
buttons now use the DS Tooltip (like the Inbox/Activity widgets) instead of a
native title.
- Settings fix. The GitHub section was orphaned (unmapped sections default to the
"Plugins" category, which only renders installed-plugin config). Route it to
System so Settings ▸ System ▸ GitHub renders the repos list + default editor.
Config roundtrip goldens + tests updated; docs (guide + README) refreshed.
Co-authored-by: Claude Opus 4.8 (1M context)
---
README.md | 2 +-
apps/web/src/app/App.tsx | 134 ++++++++++++++++++---------
apps/web/src/chat/ChatSurface.tsx | 17 +---
apps/web/src/chat/NewIssueDialog.tsx | 87 +++++++++++++++--
apps/web/src/lib/api.ts | 2 +-
apps/web/src/state/uiStore.ts | 11 ++-
docs/guides/file-github-issues.md | 41 +++++---
graph/config.py | 11 ++-
graph/settings_schema.py | 17 +++-
operator_api/github_routes.py | 21 ++++-
server/chat.py | 16 +++-
tests/test_config_roundtrip.py | 3 +
tests/test_gh_issue.py | 10 ++
tests/test_github_routes.py | 4 +-
tools/gh_issue.py | 13 +++
15 files changed, 297 insertions(+), 92 deletions(-)
diff --git a/README.md b/README.md
index 90b7e0e9..c6b53fb6 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ rename / release-pipeline wiring.
| Subagents | `graph/subagents/config.py` | DeerFlow-pattern delegation via a `task()` tool; one worked example ships — a `researcher` (web + memory, plan→search→synthesize→cite) |
| Delegate to other agents | `plugins/delegates/`, `plugins/coding_agent/` | **`delegate_to`** routes a sub-task to another agent or endpoint over **a2a / openai / acp** — a **built-in** registry, managed + hot-swappable from the console (**Workspace settings ▸ Delegates**), with a health prober. The **acp** type spawns a CLI coding agent (e.g. protoCLI) over the Agent Client Protocol. See [Delegates](./docs/guides/delegates.md), [Spawn CLI coding agents](./docs/guides/coding-agents.md), ADR [0024](./docs/adr/0024-spawn-cli-coding-agents-acp.md) / [0025](./docs/adr/0025-unified-delegate-registry-and-panel.md) |
| Starter tools | `tools/lg_tools.py` | Default-on set: 4 keyless general (`current_time`, `calculator` safe AST eval, `web_search` via DuckDuckGo, `fetch_url`) + 2 HITL (`ask_human`, `request_user_input`) + `show_component` + 3 curation (`recent_activity`/`list_skills`/`save_skill`) + `set_goal`, plus — when their store is present — 4 memory + 3 scheduler + 4 tasks + 1 inbox. **Not** in `get_all_tools`: notes/docs tools (on-by-default plugins), `delegate_to` (built-in `delegates` plugin), GitHub read tools (opt-in `github` plugin). Drop any via `tools.disabled`; add via a plugin. See [Starter tools](./docs/reference/starter-tools.md) |
-| File GitHub issues | `tools/gh_issue.py` | **`/issue`** — a user-only chat control command (like `/goal`) that files a GitHub issue via the `gh` CLI, scaffolding + enforcing the required sections so it clears the repo's issue gate. **Not** an agent tool — creating issues stays in your hands (the `github` plugin's GitHub tools are read-only). Target repo = `--repo` › Settings ▸ GitHub › `GITHUB_DEFAULT_REPO`. See [File GitHub issues](./docs/guides/file-github-issues.md) |
+| File GitHub issues | `tools/gh_issue.py` | **`/issue`** — a user-only chat command **and** a 🐛 utility-bar form dialog that file a GitHub issue via the `gh` CLI, scaffolding + enforcing the required sections so it clears the repo's issue gate. **Not** an agent tool — creating issues stays in your hands (the `github` plugin's GitHub tools are read-only). Repos are a quick-toggle list configured under **Settings ▸ System ▸ GitHub** (`github.repos` + `github.default_repo`), pairing with the portfolio manager's many-repo setup. See [File GitHub issues](./docs/guides/file-github-issues.md) |
| Knowledge store | `knowledge/store.py`, `knowledge/hybrid_store.py`, `ingestion/` | sqlite + FTS5 keyword search by default; an optional **hybrid** store adds embeddings + RRF fusion, and the **ingestion pipeline** pulls in txt/md/html/pdf/web/YouTube/audio/video sources. One `chunks` table for operator notes and conversation findings. Default-on; turn off with `middleware.knowledge: false` |
| Extensibility | `graph/skills/`, `tools/mcp_tools.py`, `graph/plugins/`, `plugins/` | Opt-in ways to extend a running agent without forking: **`SKILL.md` skills** (AgentSkills format, loaded on demand), **MCP servers** (external tools over stdio/HTTP), and **plugins** — drop-in packages that add tools, skills, subagents, workflows, FastAPI routes, background surfaces, managed MCP servers, **console rail views**, and their own config/secrets/Settings. Plugins are **installable from a git URL** (`python -m server plugin install `, pinned in `plugins.lock`) and shareable as repos — a repo is a full bundle. The first-party **Telegram** (`plugins/telegram`) integration ships bundled; **Discord**, **Slack**, and **Google** Gmail/Calendar install as external plugins from their own repos. See [Skills](./docs/guides/skills.md), [MCP](./docs/guides/mcp.md), [Plugins](./docs/guides/plugins.md), [Plugin console views](./docs/guides/plugin-views.md), [Install & publish plugins](./docs/guides/plugin-registry.md), ADR [0001](./docs/adr/0001-extensibility-and-plugin-architecture.md) / [0018](./docs/adr/0018-plugin-surfaces-routes-subagents.md) / [0019](./docs/adr/0019-plugin-config-settings-secrets.md) / [0026](./docs/adr/0026-plugin-contributed-console-surfaces.md) / [0027](./docs/adr/0027-install-plugins-from-git-url.md) |
| Scheduler | `scheduler/` | `schedule_task` / `list_schedules` / `cancel_schedule` tools backed by a bundled sqlite scheduler. Multi-agent-safe — every job is namespaced by `AGENT_NAME`. See [Schedule future work](./docs/guides/scheduler.md) |
diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx
index 032b88d1..b8bd5666 100644
--- a/apps/web/src/app/App.tsx
+++ b/apps/web/src/app/App.tsx
@@ -29,6 +29,7 @@ import {
// (dashboards, data, comms, dev, finance, space/fleet, AI) find a fitting glyph.
Bot,
Brain,
+ Bug,
Code,
Coins,
Compass,
@@ -73,10 +74,11 @@ import { Splash, BootGate } from "@protolabsai/ui/splash";
import { Button } from "@protolabsai/ui/primitives";
import { ActivityWidget } from "../activity/ActivityWidget";
-import { ConfirmDialog } from "@protolabsai/ui/overlays";
+import { ConfirmDialog, Tooltip } from "@protolabsai/ui/overlays";
import { InboxWidget } from "../inbox/InboxWidget";
import { ChatSlot } from "./ChatSlot";
-import { useAnyChatStreaming } from "../chat/chat-store";
+import { chatStore, useAnyChatStreaming } from "../chat/chat-store";
+import { NewIssueDialog } from "../chat/NewIssueDialog";
import { KnowledgeStore } from "../knowledge/KnowledgeStore";
import { SettingsOverlay } from "../settings/SettingsOverlay";
import { AppDrawer } from "./AppDrawer";
@@ -253,6 +255,9 @@ export function App() {
const globalSettingsSection = useUI((s) => s.globalSettingsSection);
const openGlobalSettings = useUI((s) => s.openGlobalSettings);
const closeGlobalSettings = useUI((s) => s.closeGlobalSettings);
+ const newIssueOpen = useUI((s) => s.newIssueOpen);
+ const openNewIssue = useUI((s) => s.openNewIssue);
+ const closeNewIssue = useUI((s) => s.closeNewIssue);
const [drawerOpen, setDrawerOpen] = useState(false);
const [projectPath, setProjectPath] = useLocalStorageState("protoagent.projectPath", "");
// Shell-level runtime read (ADR 0013): non-suspense useQuery so the topbar
@@ -897,16 +902,30 @@ export function App() {
{/* Settings (far left, 2026-06 consolidation) — opens the one settings dialog
(SettingsOverlay). A plain pill, not a UtilityWidget, so the drawer + ⌘K
deep-links can open it too via the store flag (openGlobalSettings). */}
- openGlobalSettings()}
- >
-
-
+
+ openGlobalSettings()}
+ >
+
+
+
+ {/* File a GitHub issue (the /issue command's form). Same store flag the
+ chat `/issue` slash command opens, so there's one dialog (mounted below). */}
+
+ openNewIssue()}
+ >
+
+
+
{/* Widgets (bottom-left): background subagents (ADR 0050 Phase 3), the
inbox, and the read-only Activity feed — each a pill with a hover info
popover + a click dialog. */}
@@ -934,57 +953,66 @@ export function App() {
}
end={
<>
- setBottomCollapsed(!bottomCollapsed)}
- disabled={!bottomActive}
- title={
+
-
-
- setLeftCollapsed(!leftCollapsed)}
- disabled={leftMembers.length === 0}
- title={
+ setBottomCollapsed(!bottomCollapsed)}
+ disabled={!bottomActive}
+ aria-label="Toggle bottom panel"
+ data-testid="toggle-bottom"
+ >
+
+
+
+
-
-
- setRightCollapsed(!rightCollapsed)}
- disabled={rightMembers.length === 0}
- title={
+ setLeftCollapsed(!leftCollapsed)}
+ disabled={leftMembers.length === 0}
+ aria-label="Toggle left panel"
+ data-testid="toggle-left"
+ >
+
+
+
+
-
-
+ setRightCollapsed(!rightCollapsed)}
+ disabled={rightMembers.length === 0}
+ aria-label="Toggle side panel"
+ data-testid="toggle-right"
+ >
+
+
+
>
}
/>
@@ -1039,6 +1067,28 @@ export function App() {
section={globalSettingsSection}
onClose={closeGlobalSettings}
/>
+ {/* New GitHub issue dialog — store-driven, so the util-bar bug action and the chat
+ `/issue` slash command share one mount. On success, drop a note in the current chat. */}
+ {
+ const snap = chatStore.getSnapshot();
+ const sid = snap.currentSessionId;
+ if (!sid) return;
+ const base = snap.sessions.find((s) => s.id === sid)?.messages ?? [];
+ chatStore.updateMessages(sid, [
+ ...base,
+ {
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
+ role: "assistant",
+ content: note,
+ createdAt: Date.now(),
+ status: "done",
+ },
+ ]);
+ }}
+ />
>
);
}
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index 2ec1876b..c73119e5 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -38,7 +38,7 @@ import {
import { ChatComponent } from "./ChatComponent";
import { ComposerModelSelect } from "./ComposerModelSelect";
import { Markdown } from "./LazyMarkdown";
-import { NewIssueDialog } from "./NewIssueDialog";
+import { useUI } from "../state/uiStore";
import { filesFromTransfer, isLargePaste, pastedTextFile } from "./paste";
import { ToolCalls } from "./ToolCalls";
import { addToolRef, appendReasoning, appendText, toolsForGroup } from "./parts";
@@ -312,9 +312,6 @@ function ChatSessionSlot({
const [commands, setCommands] = useState([]);
const [slashIndex, setSlashIndex] = useState(0);
const [slashDismissed, setSlashDismissed] = useState(false);
- // `/issue` opens the New-issue form dialog (the console UX for the user-only
- // issue command) rather than sending the raw text — see runClientSlash.
- const [issueDialogOpen, setIssueDialogOpen] = useState(false);
useEffect(() => {
api.chatCommands().then((r) => setCommands(r.commands)).catch(() => {});
@@ -390,9 +387,10 @@ function ChatSessionSlot({
return true;
}
if (verb === "issue") {
- // Open the form dialog instead of sending. Inline `/issue …` still
- // works: it only reaches here when picked from the menu with no args.
- setIssueDialogOpen(true);
+ // Open the New-issue form dialog (mounted once in App, store-driven) instead
+ // of sending. Inline `/issue …` still files directly — it only reaches
+ // here when picked from the menu with no args.
+ useUI.getState().openNewIssue();
return true;
}
return false;
@@ -1228,11 +1226,6 @@ function ChatSessionSlot({
}}
/>
- setIssueDialogOpen(false)}
- onFiled={(note) => noteToThread(note)}
- />
);
}
diff --git a/apps/web/src/chat/NewIssueDialog.tsx b/apps/web/src/chat/NewIssueDialog.tsx
index f8f0b144..3025502c 100644
--- a/apps/web/src/chat/NewIssueDialog.tsx
+++ b/apps/web/src/chat/NewIssueDialog.tsx
@@ -1,12 +1,16 @@
import { DropdownSelect, Input, Textarea } from "@protolabsai/ui/forms";
import { Dialog } from "@protolabsai/ui/overlays";
import { Button } from "@protolabsai/ui/primitives";
-import { Bug, Github, Loader2, Send } from "lucide-react";
+import { Bug, Github, Loader2, Send, X } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { api } from "../lib/api";
import { buildBody, EMPTY_FIELDS, type Fields, isComplete, type Kind } from "./issueBody";
+// Sentinel option that switches the repo picker to a free-text field for a
+// one-off repo not in the configured list.
+const CUSTOM_REPO = "__custom__";
+
/**
* The console UX for the user-only `/issue` command: a form that files a GitHub
* issue via POST /api/github/issue (which shares the server's gate-conformance +
@@ -25,12 +29,14 @@ export function NewIssueDialog({
const [repo, setRepo] = useState("");
const [title, setTitle] = useState("");
const [fields, setFields] = useState(EMPTY_FIELDS);
+ const [repos, setRepos] = useState([]);
const [defaultRepo, setDefaultRepo] = useState("");
+ const [customRepo, setCustomRepo] = useState(false);
const [ghAvailable, setGhAvailable] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
- // Reset + prefill (default repo, gh availability) each time the dialog opens.
+ // Reset + prefill (repo list, default, gh availability) each time it opens.
useEffect(() => {
if (!open) return;
setKind("bug");
@@ -41,13 +47,24 @@ export function NewIssueDialog({
api
.githubConfig()
.then((c) => {
+ setRepos(c.repos);
setDefaultRepo(c.default_repo);
- setRepo(c.default_repo);
+ setRepo(c.default_repo || c.repos[0] || "");
+ // No configured repos at all → start in free-text mode.
+ setCustomRepo(c.repos.length === 0 && !c.default_repo);
setGhAvailable(c.gh_available);
})
.catch(() => {});
}, [open]);
+ // Dropdown options: the default first, then the configured list, de-duped.
+ const repoOptions = useMemo(() => {
+ const out: string[] = [];
+ if (defaultRepo) out.push(defaultRepo);
+ for (const r of repos) if (r && !out.includes(r)) out.push(r);
+ return out;
+ }, [repos, defaultRepo]);
+
const set = (k: keyof Fields) => (e: { target: { value: string } }) =>
setFields((f) => ({ ...f, [k]: e.target.value }));
@@ -130,12 +147,64 @@ export function NewIssueDialog({
Repo (owner/name)
- setRepo(e.target.value)}
- placeholder={defaultRepo || "owner/name"}
- data-testid="issue-create-repo"
- />
+ {repoOptions.length > 0 && !customRepo ? (
+ {
+ if (v === CUSTOM_REPO) {
+ setCustomRepo(true);
+ setRepo("");
+ } else {
+ setRepo(v);
+ }
+ }}
+ aria-label="Repo"
+ options={[
+ ...repoOptions.map((r) => ({ value: r, label: r })),
+ { value: CUSTOM_REPO, label: "Custom…" },
+ ]}
+ />
+ ) : repoOptions.length > 0 ? (
+ // Custom mode (a list exists): free-text + an inline × to return to
+ // the dropdown — kept on the same row so the field doesn't shift.
+
+ setRepo(e.target.value)}
+ placeholder="owner/name"
+ data-testid="issue-create-repo"
+ style={{ flex: 1 }}
+ />
+ {
+ setCustomRepo(false);
+ setRepo(defaultRepo || repos[0] || "");
+ }}
+ style={{
+ display: "inline-flex",
+ background: "none",
+ border: "none",
+ padding: 4,
+ cursor: "pointer",
+ color: "var(--fg-muted)",
+ }}
+ >
+
+
+
+ ) : (
+ // No configured list at all → plain free-text (nothing to go back to).
+ setRepo(e.target.value)}
+ placeholder={defaultRepo || "owner/name"}
+ data-testid="issue-create-repo"
+ />
+ )}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
index c79a2791..39582597 100644
--- a/apps/web/src/lib/api.ts
+++ b/apps/web/src/lib/api.ts
@@ -880,7 +880,7 @@ export const api = {
// prefills the dialog (default repo + whether `gh` is installed); `createIssue`
// shares the server's tools.gh_issue path with the chat /issue command.
githubConfig() {
- return request<{ default_repo: string; gh_available: boolean }>("/api/github/config");
+ return request<{ repos: string[]; default_repo: string; gh_available: boolean }>("/api/github/config");
},
createIssue(body: {
diff --git a/apps/web/src/state/uiStore.ts b/apps/web/src/state/uiStore.ts
index 89e056ec..e70cef63 100644
--- a/apps/web/src/state/uiStore.ts
+++ b/apps/web/src/state/uiStore.ts
@@ -68,6 +68,12 @@ type UIState = {
globalSettingsSection?: string;
openGlobalSettings: (section?: string) => void;
closeGlobalSettings: () => void;
+ // New GitHub issue dialog (the /issue command's form). EPHEMERAL — partialized
+ // out of persistence so a refresh never reopens it. Opened from the util-bar
+ // bug action or the chat `/issue` slash command.
+ newIssueOpen: boolean;
+ openNewIssue: () => void;
+ closeNewIssue: () => void;
rightCollapsed: boolean;
leftCollapsed: boolean;
rightWidth: number;
@@ -259,6 +265,9 @@ export const useUI = create()(
globalSettingsSection: undefined,
openGlobalSettings: (section) => set({ globalSettingsOpen: true, globalSettingsSection: section }),
closeGlobalSettings: () => set({ globalSettingsOpen: false }),
+ newIssueOpen: false,
+ openNewIssue: () => set({ newIssueOpen: true }),
+ closeNewIssue: () => set({ newIssueOpen: false }),
rightCollapsed: false,
leftCollapsed: false,
rightWidth: 360,
@@ -355,7 +364,7 @@ export const useUI = create()(
migrate: (persisted: unknown) => migrateUiState(persisted) as never,
// The Global settings overlay is ephemeral UI state — drop it from persistence so a
// refresh never reopens it (everything else persists as before).
- partialize: ({ globalSettingsOpen: _o, globalSettingsSection: _s, ...rest }) => rest,
+ partialize: ({ globalSettingsOpen: _o, globalSettingsSection: _s, newIssueOpen: _ni, ...rest }) => rest,
},
),
);
diff --git a/docs/guides/file-github-issues.md b/docs/guides/file-github-issues.md
index 2f1c731e..be5c694d 100644
--- a/docs/guides/file-github-issues.md
+++ b/docs/guides/file-github-issues.md
@@ -1,11 +1,15 @@
# File GitHub issues (`/issue`)
-Type `/issue` in chat to file a GitHub issue straight from the console. It's a
-**user-only control command** — like [`/goal`](/guides/goal-mode), it short-circuits
-the turn and is handled by the server. It is deliberately **not** an agent tool, so
-the agent can't open issues on its own (the read-only GitHub tools in the
-`github` plugin stay agent-facing; *creating* an issue is a write you keep in your
-own hands).
+File a GitHub issue straight from the console — two ways, one backend path:
+
+- the **`/issue` chat command** (type it, or pick it from the slash menu), and
+- the **🐛 bug button** in the utility bar (bottom-left, next to Settings), which
+ opens a **form dialog**.
+
+It's **user-only** — like [`/goal`](/guides/goal-mode) the command short-circuits the
+turn and is handled by the server; it is deliberately **not** an agent tool, so the
+agent can't open issues on its own (the read-only GitHub tools in the `github` plugin
+stay agent-facing; *creating* an issue is a write you keep in your own hands).
## Syntax
@@ -53,15 +57,30 @@ If a required section is missing, nothing is filed — the command replies with
missing and a ready-to-fill scaffold. Run `/issue --bug` with no body to get the
scaffold up front.
+## The form dialog
+
+The 🐛 button (and picking `/issue` from the slash menu) opens a form: **Type**
+(Bug/Enhancement), **Repo**, **Title**, and the type-specific section fields. It
+assembles a body with the exact headings the gate checks, so a dialog-filed issue
+always conforms; on success it drops a `✓ Filed … ` note in the current chat.
+
+The **Repo** field is a quick-toggle dropdown of your configured repos (see below),
+with a **Custom…** option that swaps in a free-text box (an inline **×** returns you
+to the list) for a one-off repo.
+
## Which repo
-The target repo is resolved, in order:
+Configure the repos under **Settings ▸ System ▸ GitHub**:
-1. an explicit `--repo owner/name`;
-2. **Settings ▸ GitHub ▸ Default repo for /issue** (`github.default_repo`);
-3. the `GITHUB_DEFAULT_REPO` (or `GH_REPO`) environment variable.
+- **Repos for /issue** (`github.repos`) — the `owner/name` list shown in the dialog's
+ dropdown. Pairs with the [portfolio manager](/guides/portfolio)'s many-repo setup.
+- **Default repo for /issue** (`github.default_repo`) — the preselected one (and the
+ command's default). Blank = the first repo in the list.
-If none is set, the command asks for `--repo` rather than guess — no silent misrouting.
+For a single issue the target is resolved, in order: an explicit `--repo owner/name`
+(or the dialog's Repo field) › the default above › the first configured repo › the
+`GITHUB_DEFAULT_REPO` / `GH_REPO` env var. If none is set the command asks for `--repo`
+rather than guess — no silent misrouting.
## Auth
diff --git a/graph/config.py b/graph/config.py
index e7ede450..15913f98 100644
--- a/graph/config.py
+++ b/graph/config.py
@@ -571,9 +571,13 @@ class LangGraphConfig:
# YAML ⊕ secrets overlay). A plugin reads its own via plugin_config[""].
plugin_config: dict = field(default_factory=dict)
- # Default GitHub repo (``owner/name``) for the user-only ``/issue`` command
- # (tools/gh_issue.py). Blank = require an explicit ``--repo`` (or the
- # GITHUB_DEFAULT_REPO / GH_REPO env). UI-editable under Settings ▸ GitHub.
+ # GitHub repos for the user-only ``/issue`` command + its console dialog
+ # (tools/gh_issue.py). ``github_repos`` is the picker list (each ``owner/name``)
+ # shown as a quick-toggle dropdown; ``github_default_repo`` is the preselected
+ # one (and the command's default when no ``--repo`` is given). An empty default
+ # falls back to the first repo in the list. Pairs with the portfolio manager,
+ # which federates over many repos/boards. UI-editable under Settings ▸ GitHub.
+ github_repos: list[str] = field(default_factory=list)
github_default_repo: str = ""
# Identity — captured by the setup wizard, editable via the drawer.
@@ -830,6 +834,7 @@ def from_dict(
tools_disabled=list(data.get("tools", {}).get("disabled", []) or []),
routing_fallback_models=data.get("routing", {}).get("fallback_models", []),
aux_model=data.get("routing", {}).get("aux_model", cls.aux_model),
+ github_repos=list(data.get("github", {}).get("repos", []) or []),
github_default_repo=data.get("github", {}).get("default_repo", cls.github_default_repo),
goal_enabled=data.get("goal", {}).get("enabled", cls.goal_enabled),
goal_max_iterations=data.get("goal", {}).get("max_iterations", cls.goal_max_iterations),
diff --git a/graph/settings_schema.py b/graph/settings_schema.py
index 2a7e8af7..93159ad0 100644
--- a/graph/settings_schema.py
+++ b/graph/settings_schema.py
@@ -652,14 +652,23 @@ class Field:
scope="host",
),
# ── GitHub ─────────────────────────────────────────────────────────────────
+ Field(
+ "github.repos",
+ "github_repos",
+ "Repos for /issue",
+ "string_list",
+ "GitHub",
+ "Repos (owner/name, one per line) offered as a quick-toggle dropdown in the "
+ "New-issue dialog. Pairs with the portfolio manager's many-repo setup.",
+ ),
Field(
"github.default_repo",
"github_default_repo",
"Default repo for /issue",
"string",
"GitHub",
- "Target repo (owner/name) the user-only /issue command files into when no "
- "--repo is given. Blank = require --repo each time (or the GITHUB_DEFAULT_REPO env).",
+ "Preselected repo (owner/name) for the dialog + the /issue command's default when "
+ "no --repo is given. Blank = the first repo above (or require --repo / GITHUB_DEFAULT_REPO).",
),
]
@@ -742,6 +751,10 @@ def _plugin_group(sch, spec) -> str:
"Middleware": "System",
"Runtime": "System",
"Telemetry": "System",
+ # GitHub — the /issue command's repo list + default (Settings → System). Not a
+ # plugin section, so it must map to a category SettingsCategoryPanel renders
+ # (else it'd default to "Plugins", which only shows installed-plugin config).
+ "GitHub": "System",
# Host box-runtime knobs (ADR 0047 D8), regrouped (bd-2zb) from one "Fleet" lump
# into Network / Discovery / Keep-warm. All System category so the host-scoped
# fields surface in Settings ▸ Host / App ▸ Host config (which renders the host
diff --git a/operator_api/github_routes.py b/operator_api/github_routes.py
index 7f87e0f2..642959ec 100644
--- a/operator_api/github_routes.py
+++ b/operator_api/github_routes.py
@@ -26,22 +26,33 @@ def _default_repo() -> str:
return getattr(cfg, "github_default_repo", "") or ""
+def _repos() -> list[str]:
+ """The configured repo picker list (``github.repos``)."""
+ from runtime.state import STATE
+
+ cfg = getattr(STATE, "graph_config", None)
+ return [str(r).strip() for r in (getattr(cfg, "github_repos", []) or []) if str(r).strip()]
+
+
def register_github_routes(app) -> None:
import re
import shutil
from fastapi import Body
- from tools.gh_issue import IssueRequest, file_issue, labels_for, resolve_repo
+ from tools.gh_issue import IssueRequest, effective_default_repo, file_issue, labels_for, resolve_repo
_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
@app.get("/api/github/config")
async def _github_config():
- """Defaults the New-issue dialog prefills: the effective default repo
- (config ⊕ env) and whether the ``gh`` CLI is installed."""
+ """Defaults the New-issue dialog prefills: the repo picker list, the
+ effective default repo (default ⊕ first-in-list ⊕ env), and whether the
+ ``gh`` CLI is installed."""
+ repos = _repos()
return {
- "default_repo": resolve_repo(None, _default_repo()) or "",
+ "repos": repos,
+ "default_repo": resolve_repo(None, effective_default_repo(_default_repo(), repos)) or "",
"gh_available": shutil.which("gh") is not None,
}
@@ -54,7 +65,7 @@ async def _github_create_issue(body: dict = Body(...)):
kind = "generic"
title = (body.get("title") or "").strip()
issue_body = (body.get("body") or "").strip()
- repo = resolve_repo(body.get("repo"), _default_repo())
+ repo = resolve_repo(body.get("repo"), effective_default_repo(_default_repo(), _repos()))
labels = labels_for(kind, [str(x) for x in (body.get("labels") or [])])
dry_run = bool(body.get("dry_run"))
diff --git a/server/chat.py b/server/chat.py
index 1f56b3ba..ec93919e 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -688,10 +688,14 @@ async def _chat_langgraph_stream(
# Issue control command (/issue ...) short-circuits the turn: file a
# GitHub issue. User-only by design — it is NOT an agent tool, so the
# model can't create issues autonomously (mirrors /goal).
- from tools.gh_issue import parse_issue_control
+ from tools.gh_issue import effective_default_repo, parse_issue_control
issue_reply = await parse_issue_control(
- message, default_repo=getattr(STATE.graph_config, "github_default_repo", "")
+ message,
+ default_repo=effective_default_repo(
+ getattr(STATE.graph_config, "github_default_repo", ""),
+ getattr(STATE.graph_config, "github_repos", []),
+ ),
)
if issue_reply is not None:
yield ("done", issue_reply)
@@ -1032,10 +1036,14 @@ async def _chat_langgraph(message: str, session_id: str, *, model: str | None =
# Issue control command (/issue ...) short-circuits — file a GitHub
# issue (user-only; never an agent tool). See the streaming path.
- from tools.gh_issue import parse_issue_control
+ from tools.gh_issue import effective_default_repo, parse_issue_control
issue_reply = await parse_issue_control(
- message, default_repo=getattr(STATE.graph_config, "github_default_repo", "")
+ message,
+ default_repo=effective_default_repo(
+ getattr(STATE.graph_config, "github_default_repo", ""),
+ getattr(STATE.graph_config, "github_repos", []),
+ ),
)
if issue_reply is not None:
return [{"role": "assistant", "content": issue_reply}]
diff --git a/tests/test_config_roundtrip.py b/tests/test_config_roundtrip.py
index 037a49f5..3b4897b6 100644
--- a/tests/test_config_roundtrip.py
+++ b/tests/test_config_roundtrip.py
@@ -89,6 +89,7 @@ def _isolate_from_installed_plugins(monkeypatch):
"fleet_port_base": 7870,
"fleet_warm_grace_seconds": 0,
"github_default_repo": "",
+ "github_repos": [],
"goal_enabled": True,
"goal_eval_model": "",
"goal_max_iterations": 8,
@@ -223,6 +224,7 @@ def _isolate_from_installed_plugins(monkeypatch):
},
},
"github": {
+ "repos": [],
"default_repo": "",
},
"goal": {
@@ -428,6 +430,7 @@ def _isolate_from_installed_plugins(monkeypatch):
"compaction_keep_messages",
"compaction_model",
# github.*
+ "github_repos",
"github_default_repo",
# goal.*
"goal_enabled",
diff --git a/tests/test_gh_issue.py b/tests/test_gh_issue.py
index e71ef58a..0c3d1b2a 100644
--- a/tests/test_gh_issue.py
+++ b/tests/test_gh_issue.py
@@ -11,11 +11,21 @@
import pytest
from tools.gh_issue import (
+ effective_default_repo,
is_issue_command,
missing_sections,
parse_issue_control,
)
+
+def test_effective_default_repo():
+ # explicit default wins; else first non-blank in the list; else ""
+ assert effective_default_repo("o/r", ["a/b", "c/d"]) == "o/r"
+ assert effective_default_repo("", ["a/b", "c/d"]) == "a/b"
+ assert effective_default_repo("", [" ", "c/d"]) == "c/d"
+ assert effective_default_repo("", []) == ""
+ assert effective_default_repo(" ", None) == ""
+
_GOOD_BUG = (
"/issue Scroll dead in delegate modal --bug --repo o/r\n"
"## Problem\nThe wheel does nothing inside the modal.\n\n"
diff --git a/tests/test_github_routes.py b/tests/test_github_routes.py
index 5b5b27c2..f25e3607 100644
--- a/tests/test_github_routes.py
+++ b/tests/test_github_routes.py
@@ -35,7 +35,9 @@ def test_config_reports_gh_availability(client):
with patch("shutil.which", return_value="/usr/bin/gh"):
r = client.get("/api/github/config")
assert r.status_code == 200
- assert r.json()["gh_available"] is True
+ body = r.json()
+ assert body["gh_available"] is True
+ assert "repos" in body and isinstance(body["repos"], list)
def test_create_happy_path(client):
diff --git a/tools/gh_issue.py b/tools/gh_issue.py
index f4b74c9c..96ee6c94 100644
--- a/tools/gh_issue.py
+++ b/tools/gh_issue.py
@@ -136,6 +136,19 @@ def resolve_repo(explicit: str | None, default_repo: str = "") -> str | None:
)
+def effective_default_repo(default_repo: str, repos: list[str] | None = None) -> str:
+ """The preselected default repo for the dialog + the ``/issue`` command: the
+ explicit ``github.default_repo`` if set, else the first entry in the
+ ``github.repos`` picker list, else ``""`` (env still applies via
+ ``resolve_repo``). Keeps the command and the dialog agreeing on the default."""
+ if (default_repo or "").strip():
+ return default_repo.strip()
+ for r in repos or []:
+ if (r or "").strip():
+ return r.strip()
+ return ""
+
+
async def file_issue(req: IssueRequest) -> dict:
"""Validate ``req`` against the gate rules, then create the issue via ``gh``
(or, for ``dry_run``, report what would be filed). Returns a structured
From 7de0de422f895626f7aa7d1f3d63b9180aff6702 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Mon, 22 Jun 2026 22:52:15 -0700
Subject: [PATCH 019/190] fix(console): Background-agents util pill uses the DS
Tooltip popover (#1317)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The last util button still on a native `title` — give it the same hover popover
as the Inbox/Activity/Settings/issue actions, with a stateful label (N running /
N finished / idle).
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/src/app/BackgroundJobs.tsx | 43 +++++++++++++++++------------
1 file changed, 26 insertions(+), 17 deletions(-)
diff --git a/apps/web/src/app/BackgroundJobs.tsx b/apps/web/src/app/BackgroundJobs.tsx
index 161905fc..17a2424c 100644
--- a/apps/web/src/app/BackgroundJobs.tsx
+++ b/apps/web/src/app/BackgroundJobs.tsx
@@ -1,4 +1,4 @@
-import { Dialog } from "@protolabsai/ui/overlays";
+import { Dialog, Tooltip } from "@protolabsai/ui/overlays";
import { Bot, CheckCircle2, Loader2, Square, Trash2, XCircle } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -131,6 +131,14 @@ export function BackgroundJobs() {
const running = list.filter((j) => j.status === "running").length;
const finished = list.length - running;
+ // Hover popover copy (matches the Inbox/Activity widgets), reflecting live state.
+ const info =
+ running > 0
+ ? `${running} background agent${running === 1 ? "" : "s"} running`
+ : unread > 0
+ ? `${unread} finished — click to review`
+ : "Background agents — work running on its own";
+
// Tick the elapsed clock once a second while the dialog is open and work runs.
useEffect(() => {
if (!open || running === 0) return;
@@ -176,22 +184,23 @@ export function BackgroundJobs() {
return (
<>
- {
- setOpen(true);
- setUnread(0);
- hydrate(); // fetch the FULL results when the panel opens (replaces any live previews)
- }}
- title="Background agents"
- aria-label={`Background agents${running ? ` — ${running} running` : ""}`}
- data-testid="background-jobs-pill"
- >
- {running > 0 ? : }
- {running > 0 ? {running} : null}
- {unread > 0 ? : null}
-
+
+ {
+ setOpen(true);
+ setUnread(0);
+ hydrate(); // fetch the FULL results when the panel opens (replaces any live previews)
+ }}
+ aria-label={`Background agents${running ? ` — ${running} running` : ""}`}
+ data-testid="background-jobs-pill"
+ >
+ {running > 0 ? : }
+ {running > 0 ? {running} : null}
+ {unread > 0 ? : null}
+
+
{open ? (
setOpen(false)} title="Background agents" width="min(640px, 94vw)">
{list.length === 0 ? (
From c1006b10d11b85b56f4b1b47def3d2c2d5e7ae0c Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 00:37:32 -0700
Subject: [PATCH 020/190] =?UTF-8?q?chore(web):=20@protolabsai/ui=200.45=20?=
=?UTF-8?q?=E2=86=92=200.46.0=20(collision-aware=20Tooltip)=20(#1318)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adopts the DS fix (protoContent#290 → ui 0.46.0): Tooltip is now Radix-backed and
collision-aware, so the utility-bar tooltips (Settings, /issue, background-agents,
inbox, activity, panel toggles) auto-flip/shift to stay within the viewport instead
of clipping at the bottom corners — no per-button `side` workaround needed.
- apps/web/package.json: ^0.45.0 → ^0.46.0 (pulls @radix-ui/react-tooltip transitively).
- e2e: the DS Tooltip is now portaled + shown on hover, so the plugin-view utility
widget test hovers the pill and asserts the page-level `role=tooltip` (was a static
child of `.pl-tip-wrap`).
Verified: tsc clean · web unit 121 · web e2e 127 · build OK.
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/plugin-views.spec.ts | 7 +-
apps/web/package.json | 2 +-
package-lock.json | 262 +++++++++++++++++++++++++++++-
3 files changed, 260 insertions(+), 11 deletions(-)
diff --git a/apps/web/e2e/plugin-views.spec.ts b/apps/web/e2e/plugin-views.spec.ts
index 84b1b19f..55f0a02d 100644
--- a/apps/web/e2e/plugin-views.spec.ts
+++ b/apps/web/e2e/plugin-views.spec.ts
@@ -123,8 +123,11 @@ test("a plugin view with utility:{...} is a bottom-left widget (hover info + cli
await expect(pill).toBeVisible();
await expect(page.locator(".pl-rail").getByRole("button", { name: "Boardy Snapshot" })).toHaveCount(0);
- // The hover info popover carries the manifest's `utility.info`.
- await expect(page.locator(".pl-tip-wrap", { has: pill }).getByRole("tooltip")).toContainText("A quick board snapshot");
+ // The hover info popover carries the manifest's `utility.info`. The DS Tooltip is
+ // Radix-backed (ui 0.46.0): portaled + shown on hover, so assert the page-level
+ // tooltip role after hovering, not a static child of the trigger wrap.
+ await pill.hover();
+ await expect(page.getByRole("tooltip")).toContainText("A quick board snapshot");
// Click → the plugin opens in a dialog hosting its iframe at the declared path.
await pill.click();
diff --git a/apps/web/package.json b/apps/web/package.json
index 1a4100ab..fab84cae 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.45.0",
+ "@protolabsai/ui": "^0.46.0",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
diff --git a/package-lock.json b/package-lock.json
index 41d3d7b4..906bc823 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"version": "0.1.0",
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.45.0",
+ "@protolabsai/ui": "^0.46.0",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
@@ -68,17 +68,18 @@
}
},
"apps/web/node_modules/@protolabsai/ui": {
- "version": "0.45.1",
- "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.45.1.tgz",
- "integrity": "sha512-oR608P2N0kd7wrVV5Y/uCEJNnLuGJz4cMD6k3uHUJJWN7KMeK5UXujyckjd1lyunkI5FiJVKV5+tpB7MjWk/gg==",
+ "version": "0.46.0",
+ "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.46.0.tgz",
+ "integrity": "sha512-zX9tL57xlyxBAzAxJXBhc6LVRfmS8D284H67WEAVjQvB2kb4+6DeJZDLs5TIxpPqxvo0zokmbnxE6x7pi9ZnJA==",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
- "@protolabsai/design": "0.6.0",
+ "@protolabsai/design": "0.7.0",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@radix-ui/react-popover": "^1.1.16",
+ "@radix-ui/react-tooltip": "^1.2.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"culori": "^4.0.2",
@@ -90,9 +91,9 @@
}
},
"apps/web/node_modules/@protolabsai/ui/node_modules/@protolabsai/design": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@protolabsai/design/-/design-0.6.0.tgz",
- "integrity": "sha512-i8s1rk35yoGmLNl8c1B4NIVvigxneoh11XRfGvD++p5IZ7vsbifipNMbtGYIUAIJ0SQrporGHkhRtbIyu47ETg==",
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@protolabsai/design/-/design-0.7.0.tgz",
+ "integrity": "sha512-QF/SShqPhP9SFPFiBnJkjOyp8/bbP07xi6dVtqZDkPb4U8QwnUtL1ld88xEZMa3SvY6GX2R53fa+JCgSiz3HGw==",
"license": "MIT",
"bin": {
"protolabs-sync-assets": "bin/sync-assets.mjs"
@@ -438,6 +439,251 @@
}
}
},
+ "apps/web/node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz",
+ "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-visually-hidden": "1.2.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz",
+ "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz",
+ "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz",
+ "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-rect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz",
+ "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
+ "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
+ "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz",
+ "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
+ "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
+ "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"apps/web/node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
From 4975687e1a53ee000f9c5ddc4241e6eeb001533a Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 03:37:10 -0700
Subject: [PATCH 021/190] fix(task-tool): error-state cards + subagent label +
fold settled tools into a chip (#1319)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(graph): failed/cancelled task delegations close as error cards
A subagent crash returned an 'Error: …' string and a user-cancelled delegation
returned a '[cancelled…]' string — both rode the green 'done' card because the
tool-call frame's error flag is read from the ToolMessage status, which a plain
string never sets. Now _run_subagent raises SubagentError on a hard failure and
the task tool converts failure + cancellation into ToolMessage(status='error'),
so the card shows the X (the lead still gets a readable 'continue without it'
result). task_batch reports a failed sub-delegation inline and continues.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): fold settled tool cards into a summary chip + label subagents
Spotlight active work: running top-level tool cards render full; finished ones
fold into one expandable 'N tools' chip (aggregate status → 'N failed' when any
errored), so a fan-out turn no longer buries the answer under a wall of cards.
Once the turn ends nothing is running, so the whole run collapses into the chip.
A 'task' card now shows which subagent ran ('task → researcher', read from the
call args) instead of a bare 'task'. ToolCardSummary is a thin local mirror of
the proposed DS primitive (protoContent#292) — swaps to @protolabsai/ui on release.
Co-Authored-By: Claude Opus 4.8 (1M context)
* test(graph): document subagent tool frames don't nest live (audit #4)
A subagent's own tool calls DO propagate into the parent turn stream, but the
task tool's on_tool_end is emitted before them (the delegation is detached via
ensure_future for Tier-2 cancellation), so the console's 'last open task wins'
nesting can't attach them — subagent tools render as top-level cards, not nested.
Pins the ordering so a future parent-id linkage fix trips this test.
Co-Authored-By: Claude Opus 4.8 (1M context)
* fix(web): only fold at ≥2 settled tools; keep single tools inline
E2E caught two things: (1) a lone finished tool folding into a '1 tool' chip hid a
useful card and broke the single-tool specs — fold now engages only at ≥2 settled
cards; (2) splitting running-vs-settled into separate lists remounted a card when it
settled (losing its expanded state) — so the no-fan-out path now renders every card
inline in emission order, identical to before. Adds a FANOUT mock scenario + e2e
asserting the ≥2 fold collapses to a '2 tools' chip that expands to the cards.
Co-Authored-By: Claude Opus 4.8 (1M context)
* fix(web): summary chip shows a resting border + a running total of the block
The fold chip now reads as a card at rest (solid border + raised fill, not just on
hover), and its count is the running total of ALL tool calls in the block (running +
settled) so the tally ticks up live as tools fire rather than only counting what's
folded. DS chip border mirrored in protoContent#292.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): pinned-spotlight tool block — stable height, no bounce
The tool block bounced as cards popped in/out mid-turn (each running tool mounted a
row, then unmounted into the chip; the chip popped in at the 2nd tool) and the chat
auto-scroll reflowed on every change. Now, while the turn streams, the active card(s)
sit in a fixed-height '.tool-spotlight' slot that reserves one row and never collapses
between tools, and everything finished folds into the running-total chip below — so the
block holds a stable height for the whole turn and a finishing tool just crossfades from
the slot into the chip (reduced-motion respected). Settled state is unchanged: a lone
tool inline, a fan-out folded. Threads message streaming-status into ToolCalls.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/fixtures.mjs | 12 +++
apps/web/e2e/tool-fold.spec.ts | 23 ++++
apps/web/src/chat/ChatSurface.tsx | 2 +-
apps/web/src/chat/ToolCalls.tsx | 93 +++++++++++++---
apps/web/src/chat/ToolCardSummary.tsx | 59 ++++++++++
apps/web/src/chat/tool-calls.css | 78 ++++++++++++++
graph/agent.py | 62 ++++++++---
tests/test_subagent_nesting_stream.py | 150 ++++++++++++++++++++++++++
tests/test_task_tool_error.py | 67 ++++++++++++
9 files changed, 516 insertions(+), 30 deletions(-)
create mode 100644 apps/web/e2e/tool-fold.spec.ts
create mode 100644 apps/web/src/chat/ToolCardSummary.tsx
create mode 100644 tests/test_subagent_nesting_stream.py
create mode 100644 tests/test_task_tool_error.py
diff --git a/apps/web/e2e/fixtures.mjs b/apps/web/e2e/fixtures.mjs
index a99d52e2..99612c40 100644
--- a/apps/web/e2e/fixtures.mjs
+++ b/apps/web/e2e/fixtures.mjs
@@ -310,6 +310,18 @@ function scenarioFor(prompt) {
{ id: "task-1", name: "task", phase: "end", output: "Subagent finished: found 1 result." },
],
};
+ if (t.includes("FANOUT"))
+ // Two INDEPENDENT top-level tool calls (no task wrapping them) → both settle, so the
+ // console folds them behind a single "2 tools" summary chip (clutter cleanup).
+ return {
+ answer: "Ran a search and a calculation.",
+ events: [
+ { id: "fan-1", name: "web_search", phase: "start", input: JSON.stringify({ query: "coding agents" }) },
+ { id: "fan-1", name: "web_search", phase: "end", output: "1 result(s) for 'coding agents':\n1. Example — https://example.com/x\n A snippet." },
+ { id: "fan-2", name: "calculator", phase: "start", input: JSON.stringify({ expression: "2 + 2" }) },
+ { id: "fan-2", name: "calculator", phase: "end", output: "2 + 2 = 4" },
+ ],
+ };
if (t.includes("NOEND"))
// A tool whose `end` frame never arrives (e.g. a workflow card whose end
// races with the terminal `done`). The turn still completes — the client
diff --git a/apps/web/e2e/tool-fold.spec.ts b/apps/web/e2e/tool-fold.spec.ts
new file mode 100644
index 00000000..771991f5
--- /dev/null
+++ b/apps/web/e2e/tool-fold.spec.ts
@@ -0,0 +1,23 @@
+import { expect, test } from "@playwright/test";
+
+// Clutter cleanup: once a turn fans out (≥2 finished tool calls), the settled cards fold
+// into a single expandable "N tools" summary chip so the answer isn't buried under a wall
+// of cards. The chip is collapsed by default; expanding it reveals the folded cards.
+test("settled tool cards fold into a summary chip", async ({ page }) => {
+ await page.goto("/app/", { waitUntil: "load" });
+ const composer = page.getByPlaceholder(/Message protoAgent/i);
+ await composer.waitFor({ state: "visible" });
+ await composer.fill("FANOUT do two things");
+ await composer.press("Enter");
+
+ // Both finished tools collapse behind one chip — the cards aren't rendered while folded.
+ const chip = page.locator(".pl-toolcard-summary");
+ await expect(chip).toBeVisible();
+ await expect(chip.locator(".pl-toolcard-summary__text")).toHaveText("2 tools");
+ await expect(page.locator(".pl-toolcard")).toHaveCount(0);
+
+ // Expand → both folded cards appear.
+ await chip.locator(".pl-toolcard-summary__head").click();
+ await expect(page.locator(".pl-toolcard")).toHaveCount(2);
+ await expect(page.locator(".pl-toolcard__name").first()).toHaveText("web_search");
+});
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index c73119e5..f7329bfb 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -1020,7 +1020,7 @@ function ChatSessionSlot({
// "thinking" inline next to the step it precedes.
message.parts.map((part, i, arr) =>
part.kind === "tools" ? (
-
+
) : part.kind === "reasoning" ? (
part.text.trim() ? (
// Stream the animation only on the trailing run (thinking in progress).
diff --git a/apps/web/src/chat/ToolCalls.tsx b/apps/web/src/chat/ToolCalls.tsx
index b7e7adbd..6e03ea9c 100644
--- a/apps/web/src/chat/ToolCalls.tsx
+++ b/apps/web/src/chat/ToolCalls.tsx
@@ -10,10 +10,12 @@ import {
Wrench,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
+import type { ReactNode } from "react";
import { ToolCard, ToolCardList, ToolSection } from "@protolabsai/ui/tool-card";
import type { ToolCall } from "../lib/types";
+import { ToolCardSummary } from "./ToolCardSummary";
import { ToolValue } from "./tool-renderers";
/** Map a tool name to a recognizable icon; falls back to a generic wrench. */
@@ -27,6 +29,29 @@ function iconFor(name: string): LucideIcon {
return Wrench;
}
+/** Header label for a card. A `task` delegation surfaces WHICH subagent it ran
+ * (`task → researcher`), read from the call's args, so the roster is visible at a
+ * glance without expanding. The subagent type rides in `input` from the start frame,
+ * so it shows while the delegation is still running; falls back to the bare name until
+ * the args parse. */
+function cardLabel(call: ToolCall): ReactNode {
+ if (call.name !== "task" || !call.input) return call.name;
+ try {
+ const args = JSON.parse(call.input) as { subagent_type?: unknown };
+ const sub = args.subagent_type;
+ if (typeof sub === "string" && sub) {
+ return (
+ <>
+ task → {sub}
+ >
+ );
+ }
+ } catch {
+ /* args not valid JSON yet (mid-stream) — fall back to the bare name */
+ }
+ return call.name;
+}
+
/**
* Renders the agent's tool activity as collapsible cards inside an assistant
* message. Each card shows the tool name, a running→done/error state pill, and
@@ -37,9 +62,13 @@ function iconFor(name: string): LucideIcon {
*/
export function ToolCalls({
calls,
+ streaming = false,
onCancelDelegation,
}: {
calls: ToolCall[];
+ /** The turn is still live. Keeps the spotlight slot reserved for the whole turn so the
+ * layout doesn't bounce in the gap between one tool finishing and the next starting. */
+ streaming?: boolean;
/** Abort a running top-level `task` delegation by its tool-call id (Tier 2). When
* omitted, no Stop affordance renders (e.g. historical/finished messages). */
onCancelDelegation?: (id: string) => void;
@@ -56,21 +85,55 @@ export function ToolCalls({
top.push(call);
}
}
- return (
-
- {top.map((call) => (
- // Only TOP-LEVEL groups get the cancel callback — a delegation is always a
- // top-level `task`; its nested children (the subagent's own tools) aren't
- // independently cancellable, so recursion drops `onCancelDelegation`.
-
- ))}
-
+ const running = top.filter((c) => c.status === "running");
+ const settled = top.filter((c) => c.status !== "running");
+ const failedCount = settled.filter((c) => c.status === "error").length;
+
+ // Only TOP-LEVEL `task` groups get the cancel callback (the Stop affordance only shows
+ // for a running task); nested children and settled cards never need it.
+ const group = (call: ToolCall) => (
+
);
+
+ // The folded summary chip — a running total of the whole block (running + settled), with
+ // the finished cards inside it.
+ const chip = (count: number) => (
+ 0 ? "error" : "done"}
+ failedCount={failedCount || undefined}
+ >
+ {settled.map(group)}
+
+ );
+
+ // PINNED SPOTLIGHT (live turn): the active card(s) sit in a fixed-height slot that never
+ // collapses as tools cycle (`.tool-spotlight` reserves one row), and everything finished
+ // folds into the chip below. So the block holds a stable height for the whole turn — the
+ // column doesn't bounce as cards pop in and out; a finishing tool just crossfades from
+ // the slot into the chip. The chip is the running total, so it's present from the first
+ // finished tool onward.
+ if (streaming) {
+ return (
+
+ {running.map(group)}
+ {settled.length > 0 && chip(top.length)}
+
+ );
+ }
+
+ // SETTLED (turn done for this block): a lone finished tool renders inline (no pointless
+ // "1 tool" chip); a real fan-out (≥2) stays folded.
+ if (settled.length >= 2) {
+ return {chip(settled.length)} ;
+ }
+ return {top.map(group)} ;
}
/** A tool card plus, when it's a subagent `task`, its nested child tool cards.
@@ -136,7 +199,7 @@ function ToolGroup({
return (
}
duration={call.durationMs}
diff --git a/apps/web/src/chat/ToolCardSummary.tsx b/apps/web/src/chat/ToolCardSummary.tsx
new file mode 100644
index 00000000..cf1db1fa
--- /dev/null
+++ b/apps/web/src/chat/ToolCardSummary.tsx
@@ -0,0 +1,59 @@
+import { useState, type ReactNode } from "react";
+
+/**
+ * TEMPORARY local mirror of the proposed `@protolabsai/ui` `ToolCardSummary`
+ * (protoContent#292). Folds a run of SETTLED tool cards into one expandable chip
+ * ("6 tools" / "6 tools · 1 failed") so the active card stays prominent. The API + DS
+ * class names match the upstream primitive 1:1, so once `@protolabsai/ui` releases it
+ * this file is deleted and the import swaps to `@protolabsai/ui/tool-card`.
+ */
+export function ToolCardSummary({
+ count,
+ label = "tools",
+ status = "done",
+ failedCount,
+ defaultOpen = false,
+ children,
+}: {
+ count: number;
+ label?: string;
+ status?: "done" | "error";
+ failedCount?: number;
+ defaultOpen?: boolean;
+ children: ReactNode;
+}) {
+ const [open, setOpen] = useState(defaultOpen);
+ const text = failedCount ? `${count} ${label} · ${failedCount} failed` : `${count} ${label}`;
+ return (
+
+
setOpen((v) => !v)}
+ >
+
+
+
+
+
+
+ {status === "error" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {text}
+
+ {open &&
{children}
}
+
+ );
+}
diff --git a/apps/web/src/chat/tool-calls.css b/apps/web/src/chat/tool-calls.css
index 59a73dec..c2d143cf 100644
--- a/apps/web/src/chat/tool-calls.css
+++ b/apps/web/src/chat/tool-calls.css
@@ -17,6 +17,84 @@
white-space: normal;
}
+/* TEMPORARY — mirrors the proposed DS `.pl-toolcard-summary*` (protoContent#292).
+ The summary chip folds a run of SETTLED cards into one expandable row so the active
+ card stays prominent. Delete this block once `@protolabsai/ui` ships the primitive
+ (its styles.css bundle then supplies these same rules) and ToolCardSummary.tsx swaps
+ to the DS import. */
+.pl-toolcard-summary {
+ min-width: 0;
+}
+.pl-toolcard-summary__head {
+ width: 100%;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 5px 9px;
+ /* Reads as a card even at rest — solid border + raised fill always visible (#1319). */
+ background: var(--pl-color-bg-raised);
+ border: var(--pl-border-width) solid var(--pl-color-border);
+ border-radius: var(--pl-radius);
+ color: var(--pl-color-fg-muted);
+ font: inherit;
+ font-size: 12px;
+ text-align: left;
+ cursor: pointer;
+}
+.pl-toolcard-summary__head:hover {
+ color: var(--pl-color-fg);
+}
+.pl-toolcard-summary__text {
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.pl-toolcard-summary--error .pl-toolcard-summary__text {
+ color: var(--pl-color-status-error);
+}
+.pl-toolcard-summary__body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pl-space-2);
+ margin-top: 6px;
+}
+
+/* `task → researcher` — the subagent type in the delegation card header. */
+.tool-subagent {
+ color: var(--pl-color-fg-muted);
+}
+
+/* Pinned spotlight: while the turn streams, the active card(s) live in a slot that
+ reserves one card-row of height and never collapses as tools cycle (so the column
+ can't bounce). A finishing tool leaves the slot for the chip; the next crossfades in. */
+.tool-spotlight {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pl-space-2);
+ min-height: 30px; /* ~ one collapsed ToolCard head row */
+}
+.tool-spotlight > * {
+ animation: tool-spotlight-in var(--pl-motion-base, 160ms) var(--pl-motion-ease, ease) both;
+}
+@keyframes tool-spotlight-in {
+ from {
+ opacity: 0;
+ transform: translateY(-2px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .tool-spotlight > * {
+ animation: none;
+ }
+}
+
/* Tier 2: Stop a running subagent delegation. Rides the DS header `actions` slot
(.pl-toolcard__actions) as a compact, error-toned text button — quiet until
hover so a long delegation reads as "running" first, "abortable" on intent. */
diff --git a/graph/agent.py b/graph/agent.py
index 2964dccf..626fb31f 100644
--- a/graph/agent.py
+++ b/graph/agent.py
@@ -7,6 +7,7 @@
from typing import Annotated, Any
from langchain.agents import create_agent
+from langchain_core.messages import ToolMessage
from langchain_core.tools import BaseTool
from langgraph.prebuilt import InjectedState
@@ -194,6 +195,13 @@ def _parse_compaction_trigger(spec: str):
return ("fraction", 0.8)
+class SubagentError(RuntimeError):
+ """A subagent delegation failed hard (the subagent itself raised). The ``task``
+ tool converts this into a ``status="error"`` ToolMessage so the console renders
+ the delegation card as a failure (X) — not a green "done" wrapping an ``Error:``
+ string (which read as success). ``task_batch`` reports it inline and continues."""
+
+
async def _run_subagent(
*,
config,
@@ -270,7 +278,10 @@ async def _run_subagent(
return f"[{subagent_type} completed: {description}]\n\n{body}"
except Exception as e:
- return f"Error: Subagent '{subagent_type}' failed: {e}"
+ # Surface a hard subagent failure as a tool ERROR (X) rather than a green
+ # "done" wrapping an "Error:" string. Callers (``task``/``task_batch``)
+ # turn this into a status="error" ToolMessage or an inline batch error line.
+ raise SubagentError(f"Subagent '{subagent_type}' failed: {e}") from e
async def run_manual_subagent(
@@ -478,7 +489,14 @@ async def _spawn_bg() -> str:
)
done, _pending = await asyncio.wait({inline}, timeout=auto_s)
if inline in done:
- return inline.result()
+ try:
+ return inline.result()
+ except SubagentError as e:
+ return ToolMessage(
+ content=f"[{e}. Continue without its result.]",
+ tool_call_id=tool_call_id,
+ status="error",
+ )
inline.cancel()
try:
await inline
@@ -518,11 +536,23 @@ async def _spawn_bg() -> str:
# User-initiated delegation cancel → swallow and let the lead keep going;
# a turn-level cancel (flag unset) → re-raise so the whole turn unwinds.
if delegations.was_cancelled(session_id, tool_call_id):
- return (
- f"[delegation cancelled by the user before it finished: "
- f"{subagent_type} — {description}. Continue without its result.]"
+ return ToolMessage(
+ content=(
+ f"[delegation cancelled by the user before it finished: "
+ f"{subagent_type} — {description}. Continue without its result.]"
+ ),
+ tool_call_id=tool_call_id,
+ status="error", # cancelled → the card closes as an X, not green "done"
)
raise
+ except SubagentError as e:
+ # Subagent crashed → close the delegation card as a failure (X) while still
+ # handing the lead a readable result so it can continue without it.
+ return ToolMessage(
+ content=f"[{e}. Continue without its result.]",
+ tool_call_id=tool_call_id,
+ status="error",
+ )
finally:
delegations.unregister(session_id, tool_call_id)
@@ -562,15 +592,19 @@ async def _one(spec: dict) -> str:
if not prm:
return f"Error: task '{desc}' is missing 'prompt'."
async with sem:
- return await _run_subagent(
- config=config,
- tool_map=tool_map,
- available_subagents=available_subagents,
- description=desc,
- prompt=prm,
- subagent_type=spec.get("subagent_type", "researcher"),
- truncate=truncate,
- )
+ try:
+ return await _run_subagent(
+ config=config,
+ tool_map=tool_map,
+ available_subagents=available_subagents,
+ description=desc,
+ prompt=prm,
+ subagent_type=spec.get("subagent_type", "researcher"),
+ truncate=truncate,
+ )
+ except SubagentError as e:
+ # One failed delegation is reported inline; the batch goes on.
+ return f"Error: {e}"
results = await asyncio.gather(*(_one(s) for s in tasks), return_exceptions=True)
diff --git a/tests/test_subagent_nesting_stream.py b/tests/test_subagent_nesting_stream.py
new file mode 100644
index 00000000..64d49b1d
--- /dev/null
+++ b/tests/test_subagent_nesting_stream.py
@@ -0,0 +1,150 @@
+"""task-tool-rendering audit #4: does a subagent's OWN tool activity nest under the
+`task` card in the live stream? Drives the real `_run_turn_stream` frame emitter with a
+fake model scripting lead → task → subagent → current_time → done, and inspects the
+interleaved frame order.
+
+FINDING: the subagent's tool frames DO propagate into the parent stream (good), but the
+`task` tool's on_tool_end is emitted BEFORE them (because the delegation is detached via
+ensure_future for cancellation). The console nests by "last open task wins", which needs
+the task still running when the child starts — so the child arrives too late and renders
+as a top-level card, never nested. The nesting rail is effectively dead code for streamed
+delegations until child frames carry an explicit parent-task id. This test pins that real
+ordering so a future linkage fix trips it.
+"""
+
+from __future__ import annotations
+
+import itertools
+import json
+from unittest.mock import patch
+
+import pytest
+from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
+from langchain_core.messages import AIMessage, AIMessageChunk
+from langchain_core.outputs import ChatGenerationChunk
+
+
+class _ToolFake(GenericFakeChatModel):
+ """Replays preset AIMessages (incl. tool calls) over the STREAMING path so it drops
+ into create_agent for BOTH the lead and the subagent (same patched create_llm). The
+ stock GenericFakeChatModel chunks `content` and yields nothing for an empty-content
+ tool-call message — which breaks astream_events — so we emit one chunk carrying the
+ tool calls as `tool_call_chunks` (the wire shape the agent re-aggregates)."""
+
+ def bind_tools(self, tools, **kwargs):
+ return self
+
+ def _chunk(self):
+ msg = next(self.messages)
+ return ChatGenerationChunk(
+ message=AIMessageChunk(
+ content=msg.content,
+ tool_call_chunks=[
+ {"name": tc["name"], "args": json.dumps(tc["args"]), "id": tc["id"], "index": i}
+ for i, tc in enumerate(getattr(msg, "tool_calls", None) or [])
+ ],
+ )
+ )
+
+ def _stream(self, messages, stop=None, run_manager=None, **kwargs):
+ yield self._chunk()
+
+ async def _astream(self, messages, stop=None, run_manager=None, **kwargs):
+ # Yield control like a real model's network I/O would, so astream_events can
+ # flush the detached subagent's events in real-time order (not a sync burst).
+ import asyncio
+
+ await asyncio.sleep(0)
+ chunk = self._chunk()
+ await asyncio.sleep(0)
+ yield chunk
+
+
+def _install(monkeypatch, messages):
+ import runtime.state as rs
+ from graph.config import LangGraphConfig
+ from langgraph.checkpoint.memory import MemorySaver
+
+ # Pad with a no-tool-call finisher forever so an extra lead/subagent step (a retry,
+ # a structured-output kicker) ends cleanly instead of exhausting the script.
+ stream = itertools.chain(iter(messages), itertools.repeat(AIMessage(content="done ")))
+ fake = _ToolFake(messages=stream)
+ with patch("graph.agent.create_llm", lambda *a, **k: fake):
+ from graph.agent import create_agent_graph
+
+ g = create_agent_graph(LangGraphConfig(), include_subagents=True, checkpointer=MemorySaver())
+ monkeypatch.setattr(rs.STATE, "graph", g, raising=False)
+ monkeypatch.setattr(rs.STATE, "goal_controller", None, raising=False)
+ monkeypatch.setattr(rs.STATE, "graph_config", LangGraphConfig(), raising=False)
+ return g
+
+
+def _delegate(**args):
+ return AIMessage(
+ content="",
+ tool_calls=[{"name": "task", "args": args, "id": "t1", "type": "tool_call"}],
+ )
+
+
+@pytest.mark.asyncio
+async def test_subagent_tool_calls_surface_but_do_not_nest_live(monkeypatch):
+ from server.chat import _run_turn_stream
+
+ _install(
+ monkeypatch,
+ [
+ _delegate(description="check the time", prompt="what time is it", subagent_type="researcher"),
+ # subagent's turn 1 → call a nested tool (current_time is in researcher's allowlist)
+ AIMessage(content="", tool_calls=[{"name": "current_time", "args": {}, "id": "s1", "type": "tool_call"}]),
+ # subagent's turn 2 → finish
+ AIMessage(content="it is noon"),
+ # lead's turn 2 → finish
+ AIMessage(content="the subagent says it is noon "),
+ ],
+ )
+
+ # Track the interleaved frame order AND, per frame, which `task` cards are still
+ # open — replaying the console's "last open task wins" nesting (ChatSurface): a
+ # child nests only if a `task` is still running when its start arrives.
+ seq: list[tuple[str, str]] = []
+ open_tasks: set[str] = set()
+ nested_under_task = False
+ for_status: dict[str, str] = {} # id → running/closed (dedupe the twin start frames)
+ async for kind, payload in _run_turn_stream("ask the researcher the time", "s4", {"configurable": {"thread_id": "s4"}}):
+ if kind not in ("tool_start", "tool_end"):
+ continue
+ name, tid = payload.get("name"), payload.get("id")
+ if kind == "tool_start":
+ seq.append(("start", name))
+ if name == "task":
+ open_tasks.add(tid)
+ elif open_tasks and for_status.get(tid) is None:
+ nested_under_task = True # a non-task tool started while a task was open
+ for_status.setdefault(tid, "running")
+ elif kind == "tool_end":
+ seq.append(("end", name))
+ open_tasks.discard(tid)
+
+ print(f"\n[#4] interleaved frames: {seq}")
+ starts = [n for k, n in seq if k == "start"]
+ ends = [n for k, n in seq if k == "end"]
+ assert "task" in starts, f"the delegation itself should surface a card; saw {seq}"
+
+ # Propagation works: the subagent's OWN tool call surfaces in the parent stream.
+ assert "current_time" in starts, f"subagent's nested tool did not surface; saw {seq}"
+ assert "current_time" in ends, f"subagent's nested tool never closed; saw {seq}"
+
+ # KNOWN LIMITATION (audit #4) — the delegation runs its subagent via
+ # asyncio.ensure_future + await (for Tier-2 cancellation), which DETACHES it, so in
+ # astream_events the task's on_tool_end is emitted BEFORE the subagent's child
+ # frames. The child therefore arrives after the task card has already closed, and
+ # the console's "last open task wins" nesting can't attach it — subagent tools
+ # render as top-level cards, NOT nested under the delegation. The nesting rail
+ # (parentId / pl-toolcard__children) is effectively unreachable for streamed
+ # delegations. The real fix is to tag child frames with their parent task's
+ # tool_call_id (a delegation contextvar) so nesting is explicit, not timing-based.
+ # These assertions pin the current ordering so that fix trips this test.
+ task_end_i = seq.index(("end", "task"))
+ child_start_i = next(i for i, f in enumerate(seq) if f == ("start", "current_time"))
+ assert task_end_i < child_start_i, f"task should close before its child today; saw {seq}"
+ assert not nested_under_task, f"nesting unexpectedly worked — update this test + audit; saw {seq}"
diff --git a/tests/test_task_tool_error.py b/tests/test_task_tool_error.py
new file mode 100644
index 00000000..ce3137b2
--- /dev/null
+++ b/tests/test_task_tool_error.py
@@ -0,0 +1,67 @@
+"""A failed or user-cancelled `task` delegation must close its tool card as an ERROR
+(X), not a green "done" wrapping an "Error:" string. The fix: the tool returns a
+``ToolMessage(status="error")`` (which the on_tool_end → tool-call-v1 path reads via
+``status == "error"``), instead of a plain string. Invoking the tool with a tool-call
+envelope mirrors how the ToolNode runs it, so a preserved status here = a red card live.
+"""
+
+from __future__ import annotations
+
+import asyncio
+
+from langchain_core.messages import ToolMessage
+
+import graph.agent as agent_mod
+from graph.config import LangGraphConfig
+
+
+def _task_tool():
+ tools = agent_mod._build_task_tools(LangGraphConfig(), all_tools=[], background_mgr=None)
+ return next(t for t in tools if t.name == "task")
+
+
+def _invoke(task, **args):
+ call = {"name": "task", "args": args, "id": "tc-1", "type": "tool_call"}
+ return asyncio.run(task.ainvoke(call))
+
+
+def test_failed_delegation_returns_error_toolmessage(monkeypatch):
+ async def _boom(**kwargs):
+ raise agent_mod.SubagentError("Subagent 'researcher' failed: boom")
+
+ monkeypatch.setattr(agent_mod, "_run_subagent", _boom)
+ result = _invoke(_task_tool(), description="dig", prompt="go", subagent_type="researcher")
+
+ assert isinstance(result, ToolMessage)
+ assert result.status == "error"
+ assert result.tool_call_id == "tc-1"
+ assert "researcher" in result.content # the lead still gets a readable reason
+ assert "Continue without its result" in result.content
+
+
+def test_cancelled_delegation_returns_error_toolmessage(monkeypatch):
+ from graph import delegations
+
+ async def _cancel(**kwargs):
+ raise asyncio.CancelledError()
+
+ monkeypatch.setattr(agent_mod, "_run_subagent", _cancel)
+ monkeypatch.setattr(delegations, "was_cancelled", lambda *a, **k: True)
+ result = _invoke(_task_tool(), description="dig", prompt="go", subagent_type="researcher")
+
+ assert isinstance(result, ToolMessage)
+ assert result.status == "error"
+ assert "cancelled by the user" in result.content
+
+
+def test_successful_delegation_stays_plain_text(monkeypatch):
+ async def _ok(**kwargs):
+ return "[researcher completed: dig]\n\nfound it"
+
+ monkeypatch.setattr(agent_mod, "_run_subagent", _ok)
+ result = _invoke(_task_tool(), description="dig", prompt="go", subagent_type="researcher")
+
+ # Success path is unchanged: the string is wrapped (as any tool return is), but its
+ # status is NOT "error" — so the card renders a green "done", not the X.
+ assert result.status != "error"
+ assert "found it" in result.content
From eed6292c80aa3aa5a8d4265e822ec7d43007f111 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 12:39:54 -0700
Subject: [PATCH 022/190] chore(web): swap local ToolCardSummary shim for the
published DS primitive (#1320)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
@protolabsai/ui 0.46.0 → 0.47.1 (now ships ToolCardSummary). ToolCalls imports it
from @protolabsai/ui/tool-card; the local mirror (ToolCardSummary.tsx) and its
duplicate .pl-toolcard-summary* CSS are deleted — the DS styles bundle supplies the
chip frame (incl. the resting border). Host-only .tool-spotlight/.tool-subagent stay.
Completes the contribute-back loop; rendering is unchanged (e2e green).
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/package.json | 2 +-
apps/web/src/chat/ToolCalls.tsx | 3 +-
apps/web/src/chat/ToolCardSummary.tsx | 59 ---------------------------
apps/web/src/chat/tool-calls.css | 46 +--------------------
package-lock.json | 8 ++--
5 files changed, 8 insertions(+), 110 deletions(-)
delete mode 100644 apps/web/src/chat/ToolCardSummary.tsx
diff --git a/apps/web/package.json b/apps/web/package.json
index fab84cae..e7012d0a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.46.0",
+ "@protolabsai/ui": "^0.47.1",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
diff --git a/apps/web/src/chat/ToolCalls.tsx b/apps/web/src/chat/ToolCalls.tsx
index 6e03ea9c..712bc558 100644
--- a/apps/web/src/chat/ToolCalls.tsx
+++ b/apps/web/src/chat/ToolCalls.tsx
@@ -12,10 +12,9 @@ import {
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
-import { ToolCard, ToolCardList, ToolSection } from "@protolabsai/ui/tool-card";
+import { ToolCard, ToolCardList, ToolCardSummary, ToolSection } from "@protolabsai/ui/tool-card";
import type { ToolCall } from "../lib/types";
-import { ToolCardSummary } from "./ToolCardSummary";
import { ToolValue } from "./tool-renderers";
/** Map a tool name to a recognizable icon; falls back to a generic wrench. */
diff --git a/apps/web/src/chat/ToolCardSummary.tsx b/apps/web/src/chat/ToolCardSummary.tsx
deleted file mode 100644
index cf1db1fa..00000000
--- a/apps/web/src/chat/ToolCardSummary.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useState, type ReactNode } from "react";
-
-/**
- * TEMPORARY local mirror of the proposed `@protolabsai/ui` `ToolCardSummary`
- * (protoContent#292). Folds a run of SETTLED tool cards into one expandable chip
- * ("6 tools" / "6 tools · 1 failed") so the active card stays prominent. The API + DS
- * class names match the upstream primitive 1:1, so once `@protolabsai/ui` releases it
- * this file is deleted and the import swaps to `@protolabsai/ui/tool-card`.
- */
-export function ToolCardSummary({
- count,
- label = "tools",
- status = "done",
- failedCount,
- defaultOpen = false,
- children,
-}: {
- count: number;
- label?: string;
- status?: "done" | "error";
- failedCount?: number;
- defaultOpen?: boolean;
- children: ReactNode;
-}) {
- const [open, setOpen] = useState(defaultOpen);
- const text = failedCount ? `${count} ${label} · ${failedCount} failed` : `${count} ${label}`;
- return (
-
-
setOpen((v) => !v)}
- >
-
-
-
-
-
-
- {status === "error" ? (
-
-
-
- ) : (
-
-
-
- )}
-
- {text}
-
- {open &&
{children}
}
-
- );
-}
diff --git a/apps/web/src/chat/tool-calls.css b/apps/web/src/chat/tool-calls.css
index c2d143cf..c123dd19 100644
--- a/apps/web/src/chat/tool-calls.css
+++ b/apps/web/src/chat/tool-calls.css
@@ -17,50 +17,8 @@
white-space: normal;
}
-/* TEMPORARY — mirrors the proposed DS `.pl-toolcard-summary*` (protoContent#292).
- The summary chip folds a run of SETTLED cards into one expandable row so the active
- card stays prominent. Delete this block once `@protolabsai/ui` ships the primitive
- (its styles.css bundle then supplies these same rules) and ToolCardSummary.tsx swaps
- to the DS import. */
-.pl-toolcard-summary {
- min-width: 0;
-}
-.pl-toolcard-summary__head {
- width: 100%;
- min-width: 0;
- display: flex;
- align-items: center;
- gap: 7px;
- padding: 5px 9px;
- /* Reads as a card even at rest — solid border + raised fill always visible (#1319). */
- background: var(--pl-color-bg-raised);
- border: var(--pl-border-width) solid var(--pl-color-border);
- border-radius: var(--pl-radius);
- color: var(--pl-color-fg-muted);
- font: inherit;
- font-size: 12px;
- text-align: left;
- cursor: pointer;
-}
-.pl-toolcard-summary__head:hover {
- color: var(--pl-color-fg);
-}
-.pl-toolcard-summary__text {
- flex: 1 1 auto;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.pl-toolcard-summary--error .pl-toolcard-summary__text {
- color: var(--pl-color-status-error);
-}
-.pl-toolcard-summary__body {
- display: flex;
- flex-direction: column;
- gap: var(--pl-space-2);
- margin-top: 6px;
-}
+/* The summary-chip frame (`.pl-toolcard-summary*`) is the DS `ToolCardSummary` — styled
+ by the `@protolabsai/ui` styles bundle imported in main.tsx (ships ≥0.47.1). */
/* `task → researcher` — the subagent type in the delegation card header. */
.tool-subagent {
diff --git a/package-lock.json b/package-lock.json
index 906bc823..7b3e6053 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"version": "0.1.0",
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.46.0",
+ "@protolabsai/ui": "^0.47.1",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
@@ -68,9 +68,9 @@
}
},
"apps/web/node_modules/@protolabsai/ui": {
- "version": "0.46.0",
- "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.46.0.tgz",
- "integrity": "sha512-zX9tL57xlyxBAzAxJXBhc6LVRfmS8D284H67WEAVjQvB2kb4+6DeJZDLs5TIxpPqxvo0zokmbnxE6x7pi9ZnJA==",
+ "version": "0.47.1",
+ "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.47.1.tgz",
+ "integrity": "sha512-f6QetgfVXAClYDgN5QjX2VROgd5pCOKhWiSCC4oxAzRjh/qKAV/0xwph3mnIA6KyYoeE38NSmLOox1u72wyR7w==",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
From 8d6beecdac87b9701b334c0afdc034b082a21686 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 13:07:40 -0700
Subject: [PATCH 023/190] fix: nest subagent tool cards under the task by
explicit parent id (#1321)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The console nested a subagent's own tool cards under the `task` card via a timing
heuristic ('last open task wins'), which breaks when the delegation's on_tool_end races
ahead of the subagent's child frames (the delegation runs detached via ensure_future) and
mis-attributes concurrent task_batch delegations. Now the subagent's run is tagged with
the delegating tool-call id (`_run_subagent` sets it as run metadata → propagates to every
child event); server/chat.py emits it as `parentId`, the A2A executor rides it on the
tool-call DataPart as `parentToolCallId`, and the console nests by that id (heuristic kept
as a fallback for older servers). task_batch gains an InjectedToolCallId so its sub-
delegations nest too.
Tests: nesting integration test now asserts the wire linkage; new e2e proves the child
nests even when its frames arrive after the task closes; task_batch tests invoke via the
tool-call envelope.
Co-authored-by: Claude Opus 4.8 (1M context)
---
a2a_impl/executor.py | 5 ++
apps/web/e2e/fixtures.mjs | 15 ++++
apps/web/e2e/tool-nesting-explicit.spec.ts | 22 +++++
apps/web/src/chat/ChatSurface.tsx | 9 +-
apps/web/src/lib/api.ts | 12 ++-
apps/web/src/lib/types.ts | 3 +
graph/agent.py | 20 ++++-
server/chat.py | 18 +++-
tests/test_subagent_nesting_stream.py | 95 +++++++++-------------
tests/test_task_batch.py | 54 ++++++------
10 files changed, 163 insertions(+), 90 deletions(-)
create mode 100644 apps/web/e2e/tool-nesting-explicit.spec.ts
diff --git a/a2a_impl/executor.py b/a2a_impl/executor.py
index e89b0281..7b52e113 100644
--- a/a2a_impl/executor.py
+++ b/a2a_impl/executor.py
@@ -554,6 +554,11 @@ def _tool_call_part(event_type: str, payload: Any) -> Part | None:
phase,
**kwargs,
)
+ # A subagent's tool frame carries its parent delegation's tool-call id so the
+ # console nests it under the `task` card by id (the contract dict has no field
+ # for it, so ride it as an extra payload key the console reads).
+ if payload.get("parentId"):
+ emit["content"]["value"]["parentToolCallId"] = str(payload["parentId"])
return _ext_data_part(emit)
if payload:
return _text_part(str(payload))
diff --git a/apps/web/e2e/fixtures.mjs b/apps/web/e2e/fixtures.mjs
index 99612c40..25d4731f 100644
--- a/apps/web/e2e/fixtures.mjs
+++ b/apps/web/e2e/fixtures.mjs
@@ -310,6 +310,19 @@ function scenarioFor(prompt) {
{ id: "task-1", name: "task", phase: "end", output: "Subagent finished: found 1 result." },
],
};
+ if (t.includes("NESTLATE"))
+ // Out-of-order delegation: the `task` card CLOSES before the subagent's child frames
+ // arrive (the real detached-delegation ordering). Only the explicit parentToolCallId
+ // lets the console still nest the child under the task — the timing heuristic can't.
+ return {
+ answer: "Delegated; the child frame arrived after the task closed.",
+ events: [
+ { id: "task-late", name: "task", phase: "start", input: JSON.stringify({ description: "Research", prompt: "Find it." }) },
+ { id: "task-late", name: "task", phase: "end", output: "Subagent finished." },
+ { id: "kid-late", name: "web_search", phase: "start", input: JSON.stringify({ query: "late" }), parentId: "task-late" },
+ { id: "kid-late", name: "web_search", phase: "end", output: "1 result(s) for 'late':\n1. Ex — https://example.com/x\n snip.", parentId: "task-late" },
+ ],
+ };
if (t.includes("FANOUT"))
// Two INDEPENDENT top-level tool calls (no task wrapping them) → both settle, so the
// console folds them behind a single "2 tools" summary chip (clutter cleanup).
@@ -373,6 +386,8 @@ export function buildFrames({ rpcId, contextId, taskId, prompt }) {
phase: ev.phase === "start" ? "started" : "completed",
args: ev.input,
result: ev.output,
+ // A subagent's own tool frame carries its parent `task` id so the console nests it.
+ ...(ev.parentId ? { parentToolCallId: ev.parentId } : {}),
});
const statusFrame = (text, toolEvent) =>
wrap({
diff --git a/apps/web/e2e/tool-nesting-explicit.spec.ts b/apps/web/e2e/tool-nesting-explicit.spec.ts
new file mode 100644
index 00000000..0c4ca738
--- /dev/null
+++ b/apps/web/e2e/tool-nesting-explicit.spec.ts
@@ -0,0 +1,22 @@
+import { expect, test } from "@playwright/test";
+
+// The robust nesting fix: a subagent's tool frames carry their parent `task` id
+// (`parentToolCallId` on the wire), so the console nests them under the delegation card
+// even when those frames arrive AFTER the task card has closed — the detached-delegation
+// ordering the old "last open task wins" timing heuristic could not handle.
+test("a subagent tool nests under the task even when its frames arrive after the task closes", async ({ page }) => {
+ await page.goto("/app/", { waitUntil: "load" });
+ const composer = page.getByPlaceholder(/Message protoAgent/i);
+ await composer.waitFor({ state: "visible" });
+ await composer.fill("NESTLATE delegate this");
+ await composer.press("Enter");
+
+ // Final state: one top-level task group with web_search nested inside — NOT a stray
+ // top-level sibling, even though the child frames streamed after the task closed.
+ const group = page.locator(".tool-calls > .pl-toolcard-group");
+ await expect(group).toHaveCount(1);
+ await expect(group.locator("> .pl-toolcard .pl-toolcard__name")).toHaveText("task");
+ await expect(group.locator("> .pl-toolcard__children .pl-toolcard__name")).toHaveText("web_search");
+ // The child is nested, not rendered as a sibling top-level card.
+ await expect(page.locator(".tool-calls > .pl-toolcard")).toHaveCount(0);
+});
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index f7329bfb..2a78a0a9 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -872,9 +872,10 @@ function ChatSessionSlot({
// so they don't get their own block.
let nextParts = message.parts;
if (evt.phase === "start") {
- // A tool that starts while a `task` is still running is a child
- // of that subagent delegation — nest it. (Last open task wins,
- // so nested task() calls group correctly.)
+ // Nest a subagent's own tool under its `task` card. The server tags the
+ // child frame with the parent delegation's id (authoritative — works even
+ // though the task's end races AHEAD of the child); fall back to "last open
+ // task wins" only for older servers that don't send it.
const openTask = [...calls]
.reverse()
.find((c) => c.name === "task" && c.status === "running" && c.id !== evt.id);
@@ -884,7 +885,7 @@ function ChatSessionSlot({
input: evt.input,
status: "running",
startedAt: now,
- parentId: openTask?.id,
+ parentId: evt.parentId ?? openTask?.id,
};
if (idx >= 0) calls[idx] = { ...calls[idx], ...card };
else calls.push(card);
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
index 39582597..932384b0 100644
--- a/apps/web/src/lib/api.ts
+++ b/apps/web/src/lib/api.ts
@@ -399,7 +399,15 @@ function dataByMime(parts: RawPart[] | undefined, mime: string): unknown {
* single ever-overwriting card — the "only one tool at a time" symptom. */
function toolEventFromParts(parts?: RawPart[]): ToolEvent | null {
const d = dataByMime(parts, TOOL_CALL_MIME) as
- | { toolCallId?: string; name?: string; phase?: string; args?: string; result?: string; error?: string }
+ | {
+ toolCallId?: string;
+ name?: string;
+ phase?: string;
+ args?: string;
+ result?: string;
+ error?: string;
+ parentToolCallId?: string;
+ }
| null;
if (!d) return null;
return {
@@ -410,6 +418,8 @@ function toolEventFromParts(parts?: RawPart[]): ToolEvent | null {
// A "failed" end carries the error text in `error`; fall back to it for the body.
output: d.result ?? d.error,
error: d.phase === "failed" || Boolean(d.error),
+ // Set only for a subagent's own tool calls → nest under the `task` card by id.
+ ...(d.parentToolCallId ? { parentId: d.parentToolCallId } : {}),
};
}
diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts
index dc8ac966..c228682f 100644
--- a/apps/web/src/lib/types.ts
+++ b/apps/web/src/lib/types.ts
@@ -386,6 +386,9 @@ export type ToolEvent = {
input?: string;
output?: string;
error?: boolean; // an "end" that failed (phase "failed" on the wire) → card shows the X
+ /** id of the enclosing `task` delegation when this is a subagent's own tool call —
+ * set server-side so nesting is explicit (by id), not inferred from frame order. */
+ parentId?: string;
};
// A background subagent job (ADR 0050) as returned by GET /api/background and
diff --git a/graph/agent.py b/graph/agent.py
index 626fb31f..9c246b87 100644
--- a/graph/agent.py
+++ b/graph/agent.py
@@ -211,12 +211,16 @@ async def _run_subagent(
prompt: str,
subagent_type: str,
truncate: int | None = None,
+ parent_task_id: str | None = None,
) -> str:
"""Run a single subagent delegation and return its output text.
Shared by the single ``task`` tool and the concurrent ``task_batch`` tool.
``truncate`` (chars) bounds the returned body so a wide fan-out can't blow
the parent context; ``None`` means unbounded (single-task path).
+ ``parent_task_id`` is the delegating ``task``/``task_batch`` tool-call id; when
+ set, every event the subagent emits is tagged with it so the console can nest
+ the subagent's own tool cards under the delegation card.
"""
sub_config = SUBAGENT_REGISTRY.get(subagent_type)
if not sub_config:
@@ -254,10 +258,19 @@ async def _run_subagent(
system_prompt=build_subagent_prompt(subagent_type),
)
+ # Tag every event the subagent emits with the parent delegation's tool-call id.
+ # LangChain propagates config metadata to all child runs, so the subagent's own
+ # tool frames carry `parent_task_id` — letting the console nest them under the
+ # `task` card BY ID rather than by frame ordering (the delegation runs detached
+ # via ensure_future, so its on_tool_end races AHEAD of these child frames).
+ sub_run_config: dict[str, Any] = {"recursion_limit": sub_config.max_turns}
+ if parent_task_id:
+ sub_run_config["metadata"] = {"parent_task_id": parent_task_id}
+
try:
result = await subagent.ainvoke(
{"messages": [{"role": "user", "content": prompt}]},
- config={"recursion_limit": sub_config.max_turns},
+ config=sub_run_config,
)
messages = result.get("messages", [])
@@ -485,6 +498,7 @@ async def _spawn_bg() -> str:
prompt=prompt,
subagent_type=subagent_type,
truncate=None,
+ parent_task_id=tool_call_id,
)
)
done, _pending = await asyncio.wait({inline}, timeout=auto_s)
@@ -527,6 +541,7 @@ async def _spawn_bg() -> str:
prompt=prompt,
subagent_type=subagent_type,
truncate=None,
+ parent_task_id=tool_call_id,
)
)
delegations.register(session_id, tool_call_id, deleg, label=description)
@@ -557,7 +572,7 @@ async def _spawn_bg() -> str:
delegations.unregister(session_id, tool_call_id)
@tool
- async def task_batch(tasks: list[dict]) -> str:
+ async def task_batch(tasks: list[dict], tool_call_id: Annotated[str, InjectedToolCallId] = "") -> str:
"""Delegate several independent tasks to subagents concurrently.
Prefer this over multiple sequential ``task`` calls whenever the
@@ -601,6 +616,7 @@ async def _one(spec: dict) -> str:
prompt=prm,
subagent_type=spec.get("subagent_type", "researcher"),
truncate=truncate,
+ parent_task_id=tool_call_id,
)
except SubagentError as e:
# One failed delegation is reported inline; the batch goes on.
diff --git a/server/chat.py b/server/chat.py
index ec93919e..3d56baa3 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -322,6 +322,11 @@ async def _run_turn_stream(
):
kind = event.get("event", "")
name = event.get("name", "")
+ # A subagent's events carry the delegating `task`/`task_batch` tool-call id (set
+ # as run metadata in graph.agent._run_subagent), so the console can nest the
+ # subagent's own tool cards under the delegation card BY ID — not by frame order
+ # (the delegation runs detached, so the task's on_tool_end races ahead of these).
+ parent_tool_id = (event.get("metadata") or {}).get("parent_task_id")
# Skills are no longer auto-retrieved per turn (ADR 0060 — progressive
# disclosure); the model loads one on demand via the `load_skill` tool,
# which surfaces as an ordinary tool card. No `skills_loaded` event to forward.
@@ -362,6 +367,7 @@ async def _run_turn_stream(
"name": name,
"output": coerced,
"error": getattr(output, "status", None) == "error",
+ **({"parentId": parent_tool_id} if parent_tool_id else {}),
},
)
elif kind == "on_chat_model_stream":
@@ -376,7 +382,10 @@ async def _run_turn_stream(
tcid, tcname = tcc.get("id"), tcc.get("name")
if tcid and tcname and tcid not in announced_tools:
announced_tools.add(tcid)
- yield ("tool_start", {"id": tcid, "name": tcname, "input": ""})
+ yield (
+ "tool_start",
+ {"id": tcid, "name": tcname, "input": "", **({"parentId": parent_tool_id} if parent_tool_id else {})},
+ )
if hasattr(chunk, "content") and chunk.content:
accumulated_raw += chunk.content if isinstance(chunk.content, str) else str(chunk.content)
# Stream only the user-facing region, token by token —
@@ -404,7 +413,12 @@ async def _run_turn_stream(
announced_tools.add(tcid)
yield (
"tool_start",
- {"id": tcid, "name": tc.get("name", ""), "input": _coerce_tool_value(tc.get("args", ""))},
+ {
+ "id": tcid,
+ "name": tc.get("name", ""),
+ "input": _coerce_tool_value(tc.get("args", "")),
+ **({"parentId": parent_tool_id} if parent_tool_id else {}),
+ },
)
usage = getattr(output, "usage_metadata", None) if output else None
rid = event.get("run_id")
diff --git a/tests/test_subagent_nesting_stream.py b/tests/test_subagent_nesting_stream.py
index 64d49b1d..1570e413 100644
--- a/tests/test_subagent_nesting_stream.py
+++ b/tests/test_subagent_nesting_stream.py
@@ -1,22 +1,19 @@
-"""task-tool-rendering audit #4: does a subagent's OWN tool activity nest under the
-`task` card in the live stream? Drives the real `_run_turn_stream` frame emitter with a
-fake model scripting lead → task → subagent → current_time → done, and inspects the
-interleaved frame order.
-
-FINDING: the subagent's tool frames DO propagate into the parent stream (good), but the
-`task` tool's on_tool_end is emitted BEFORE them (because the delegation is detached via
-ensure_future for cancellation). The console nests by "last open task wins", which needs
-the task still running when the child starts — so the child arrives too late and renders
-as a top-level card, never nested. The nesting rail is effectively dead code for streamed
-delegations until child frames carry an explicit parent-task id. This test pins that real
-ordering so a future linkage fix trips it.
+"""task-tool-rendering audit #4 (FIXED): a subagent's OWN tool activity nests under the
+`task` card in the live stream. Drives the real `_run_turn_stream` frame emitter with a
+fake model scripting lead → task → subagent → current_time → done.
+
+The delegation runs detached (asyncio.ensure_future, for Tier-2 cancellation), so the
+task's on_tool_end can race ahead of the subagent's child frames — defeating the console's
+old "last open task wins" heuristic (which also mis-attributes concurrent task_batch
+delegations). The fix tags the subagent's run with the delegating task's tool-call id
+(`_run_subagent` sets it as run metadata), so every child frame carries `parentId` and the
+console nests by id, order-independent. This test asserts the linkage on the wire frames.
"""
from __future__ import annotations
import itertools
import json
-from unittest.mock import patch
import pytest
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
@@ -69,10 +66,13 @@ def _install(monkeypatch, messages):
# a structured-output kicker) ends cleanly instead of exhausting the script.
stream = itertools.chain(iter(messages), itertools.repeat(AIMessage(content="done ")))
fake = _ToolFake(messages=stream)
- with patch("graph.agent.create_llm", lambda *a, **k: fake):
- from graph.agent import create_agent_graph
+ # Persist the fake for the whole turn — the subagent builds ITS model lazily at
+ # runtime (in _run_subagent), so a patch that exits after construction would leave the
+ # subagent on the real gateway model (and miss the parent-id metadata propagation).
+ monkeypatch.setattr("graph.agent.create_llm", lambda *a, **k: fake)
+ from graph.agent import create_agent_graph
- g = create_agent_graph(LangGraphConfig(), include_subagents=True, checkpointer=MemorySaver())
+ g = create_agent_graph(LangGraphConfig(), include_subagents=True, checkpointer=MemorySaver())
monkeypatch.setattr(rs.STATE, "graph", g, raising=False)
monkeypatch.setattr(rs.STATE, "goal_controller", None, raising=False)
monkeypatch.setattr(rs.STATE, "graph_config", LangGraphConfig(), raising=False)
@@ -87,7 +87,7 @@ def _delegate(**args):
@pytest.mark.asyncio
-async def test_subagent_tool_calls_surface_but_do_not_nest_live(monkeypatch):
+async def test_subagent_tool_calls_nest_via_explicit_parent_id(monkeypatch):
from server.chat import _run_turn_stream
_install(
@@ -103,48 +103,31 @@ async def test_subagent_tool_calls_surface_but_do_not_nest_live(monkeypatch):
],
)
- # Track the interleaved frame order AND, per frame, which `task` cards are still
- # open — replaying the console's "last open task wins" nesting (ChatSurface): a
- # child nests only if a `task` is still running when its start arrives.
+ # Capture every tool frame with its parent linkage. The fix tags a subagent's own
+ # frames with the delegating task's tool-call id (server-side), so the console nests
+ # them BY ID rather than by timing.
+ frames: list[tuple[str, str, str | None]] = [] # (kind, name, parentId)
seq: list[tuple[str, str]] = []
- open_tasks: set[str] = set()
- nested_under_task = False
- for_status: dict[str, str] = {} # id → running/closed (dedupe the twin start frames)
async for kind, payload in _run_turn_stream("ask the researcher the time", "s4", {"configurable": {"thread_id": "s4"}}):
if kind not in ("tool_start", "tool_end"):
continue
- name, tid = payload.get("name"), payload.get("id")
- if kind == "tool_start":
- seq.append(("start", name))
- if name == "task":
- open_tasks.add(tid)
- elif open_tasks and for_status.get(tid) is None:
- nested_under_task = True # a non-task tool started while a task was open
- for_status.setdefault(tid, "running")
- elif kind == "tool_end":
- seq.append(("end", name))
- open_tasks.discard(tid)
-
- print(f"\n[#4] interleaved frames: {seq}")
+ frames.append((kind, payload.get("name"), payload.get("parentId")))
+ seq.append(("start" if kind == "tool_start" else "end", payload.get("name")))
+
+ print(f"\n[#4] frames: {frames}")
starts = [n for k, n in seq if k == "start"]
ends = [n for k, n in seq if k == "end"]
- assert "task" in starts, f"the delegation itself should surface a card; saw {seq}"
-
- # Propagation works: the subagent's OWN tool call surfaces in the parent stream.
- assert "current_time" in starts, f"subagent's nested tool did not surface; saw {seq}"
- assert "current_time" in ends, f"subagent's nested tool never closed; saw {seq}"
-
- # KNOWN LIMITATION (audit #4) — the delegation runs its subagent via
- # asyncio.ensure_future + await (for Tier-2 cancellation), which DETACHES it, so in
- # astream_events the task's on_tool_end is emitted BEFORE the subagent's child
- # frames. The child therefore arrives after the task card has already closed, and
- # the console's "last open task wins" nesting can't attach it — subagent tools
- # render as top-level cards, NOT nested under the delegation. The nesting rail
- # (parentId / pl-toolcard__children) is effectively unreachable for streamed
- # delegations. The real fix is to tag child frames with their parent task's
- # tool_call_id (a delegation contextvar) so nesting is explicit, not timing-based.
- # These assertions pin the current ordering so that fix trips this test.
- task_end_i = seq.index(("end", "task"))
- child_start_i = next(i for i, f in enumerate(seq) if f == ("start", "current_time"))
- assert task_end_i < child_start_i, f"task should close before its child today; saw {seq}"
- assert not nested_under_task, f"nesting unexpectedly worked — update this test + audit; saw {seq}"
+ assert "task" in starts, f"the delegation itself should surface a card; saw {frames}"
+ # The subagent's OWN tool call surfaces in the parent stream (propagation works)…
+ assert "current_time" in starts and "current_time" in ends, f"subagent tool didn't surface; {frames}"
+
+ # …and EVERY child frame carries the parent delegation's tool-call id ("t1"), so the
+ # console nests it under the `task` card BY ID — order-independent. (The delegation
+ # runs detached via ensure_future, so the task's on_tool_end can race ahead of the
+ # child frames; the old "last open task wins" heuristic broke on that and on
+ # concurrent task_batch delegations. Explicit linkage fixes both.)
+ child_parents = {p for k, n, p in frames if n == "current_time"}
+ assert child_parents == {"t1"}, f"subagent tool must carry parentId=t1; saw {child_parents} in {frames}"
+ # The delegation card itself stays top-level (no parent).
+ task_parents = {p for k, n, p in frames if n == "task"}
+ assert task_parents == {None}, f"the task card must not be nested; saw {task_parents}"
diff --git a/tests/test_task_batch.py b/tests/test_task_batch.py
index 5f57ed92..4b279593 100644
--- a/tests/test_task_batch.py
+++ b/tests/test_task_batch.py
@@ -34,6 +34,13 @@ async def fake_run(**kwargs):
return {t.name: t for t in tools}
+async def _batch(tool, tasks):
+ """Invoke task_batch the way the ToolNode does — a full model ToolCall (it carries
+ an InjectedToolCallId now, the parent id for nesting) — and return the string body."""
+ out = await tool.ainvoke({"name": "task_batch", "args": {"tasks": tasks}, "id": "tb-test", "type": "tool_call"})
+ return getattr(out, "content", out)
+
+
def test_build_returns_task_and_batch(monkeypatch):
tools = _build(monkeypatch)
assert set(tools) == {"task", "task_batch"}
@@ -62,14 +69,13 @@ async def test_single_task_is_unbounded(monkeypatch):
@pytest.mark.asyncio
async def test_batch_orders_results_by_index(monkeypatch):
tools = _build(monkeypatch)
- out = await tools["task_batch"].ainvoke(
- {
- "tasks": [
- {"description": "alpha", "prompt": "p1"},
- {"description": "beta", "prompt": "p2"},
- {"description": "gamma", "prompt": "p3"},
- ]
- }
+ out = await _batch(
+ tools["task_batch"],
+ [
+ {"description": "alpha", "prompt": "p1"},
+ {"description": "beta", "prompt": "p2"},
+ {"description": "gamma", "prompt": "p3"},
+ ],
)
# ordered 1..3 regardless of completion order
assert out.index("Task 1/3") < out.index("Task 2/3") < out.index("Task 3/3")
@@ -81,7 +87,7 @@ async def test_batch_passes_truncate_from_config(monkeypatch):
rec = []
cfg = LangGraphConfig(subagent_output_truncate=1234)
tools = _build(monkeypatch, config=cfg, recorder=rec)
- await tools["task_batch"].ainvoke({"tasks": [{"description": "d", "prompt": "p"}]})
+ await _batch(tools["task_batch"], [{"description": "d", "prompt": "p"}])
assert rec[0]["truncate"] == 1234
@@ -99,27 +105,26 @@ async def fake_run(**kwargs):
monkeypatch.setattr(agent_mod, "_run_subagent", fake_run)
tools = {t.name: t for t in agent_mod._build_task_tools(cfg, [])}
- await tools["task_batch"].ainvoke({"tasks": [{"description": f"t{i}", "prompt": "p"} for i in range(6)]})
+ await _batch(tools["task_batch"], [{"description": f"t{i}", "prompt": "p"} for i in range(6)])
assert state["peak"] <= 2
@pytest.mark.asyncio
async def test_batch_empty_list(monkeypatch):
tools = _build(monkeypatch)
- out = await tools["task_batch"].ainvoke({"tasks": []})
+ out = await _batch(tools["task_batch"], [])
assert "empty task list" in out
@pytest.mark.asyncio
async def test_batch_missing_prompt_isolated(monkeypatch):
tools = _build(monkeypatch)
- out = await tools["task_batch"].ainvoke(
- {
- "tasks": [
- {"description": "good", "prompt": "p"},
- {"description": "bad"}, # no prompt
- ]
- }
+ out = await _batch(
+ tools["task_batch"],
+ [
+ {"description": "good", "prompt": "p"},
+ {"description": "bad"}, # no prompt
+ ],
)
assert "OUT:good" in out
assert "missing 'prompt'" in out
@@ -134,13 +139,12 @@ async def fake_run(**kwargs):
monkeypatch.setattr(agent_mod, "_run_subagent", fake_run)
tools = {t.name: t for t in agent_mod._build_task_tools(LangGraphConfig(), [])}
- out = await tools["task_batch"].ainvoke(
- {
- "tasks": [
- {"description": "ok", "prompt": "p"},
- {"description": "boom", "prompt": "p"},
- ]
- }
+ out = await _batch(
+ tools["task_batch"],
+ [
+ {"description": "ok", "prompt": "p"},
+ {"description": "boom", "prompt": "p"},
+ ],
)
assert "OUT:ok" in out
assert "RuntimeError" in out and "kaboom" in out
From b98ad461dc47fdcd5947522140f305a83f0f096a Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 13:50:20 -0700
Subject: [PATCH 024/190] chore(tools): temporarily disable the show_component
tool (see #1323) (#1324)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Comments out the show_component tool (inline component rendering, ADR 0051) and removes
it from get_all_tools(), so the agent can't call it and it's gone from the console Tools
tab. The component-v1 pipeline (graph/components.py codec, the server/chat.py sentinel
extraction, the console component registry) is left intact — re-enabling is just
uncommenting the tool. Tracked by #1323. TestShowComponentTool skipped (codec tests stay).
Co-authored-by: Claude Opus 4.8 (1M context)
---
README.md | 2 +-
docs/reference/starter-tools.md | 2 +-
tests/test_components.py | 3 ++
tools/lg_tools.py | 66 ++++++++++++++++++---------------
4 files changed, 42 insertions(+), 31 deletions(-)
diff --git a/README.md b/README.md
index c6b53fb6..d1ffd9bd 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ rename / release-pipeline wiring.
| LLM gateway | `graph/llm.py` | OpenAI-compatible client pointed at LiteLLM — swap models by editing the gateway config, not the fork |
| Subagents | `graph/subagents/config.py` | DeerFlow-pattern delegation via a `task()` tool; one worked example ships — a `researcher` (web + memory, plan→search→synthesize→cite) |
| Delegate to other agents | `plugins/delegates/`, `plugins/coding_agent/` | **`delegate_to`** routes a sub-task to another agent or endpoint over **a2a / openai / acp** — a **built-in** registry, managed + hot-swappable from the console (**Workspace settings ▸ Delegates**), with a health prober. The **acp** type spawns a CLI coding agent (e.g. protoCLI) over the Agent Client Protocol. See [Delegates](./docs/guides/delegates.md), [Spawn CLI coding agents](./docs/guides/coding-agents.md), ADR [0024](./docs/adr/0024-spawn-cli-coding-agents-acp.md) / [0025](./docs/adr/0025-unified-delegate-registry-and-panel.md) |
-| Starter tools | `tools/lg_tools.py` | Default-on set: 4 keyless general (`current_time`, `calculator` safe AST eval, `web_search` via DuckDuckGo, `fetch_url`) + 2 HITL (`ask_human`, `request_user_input`) + `show_component` + 3 curation (`recent_activity`/`list_skills`/`save_skill`) + `set_goal`, plus — when their store is present — 4 memory + 3 scheduler + 4 tasks + 1 inbox. **Not** in `get_all_tools`: notes/docs tools (on-by-default plugins), `delegate_to` (built-in `delegates` plugin), GitHub read tools (opt-in `github` plugin). Drop any via `tools.disabled`; add via a plugin. See [Starter tools](./docs/reference/starter-tools.md) |
+| Starter tools | `tools/lg_tools.py` | Default-on set: 4 keyless general (`current_time`, `calculator` safe AST eval, `web_search` via DuckDuckGo, `fetch_url`) + 2 HITL (`ask_human`, `request_user_input`) + 3 curation (`recent_activity`/`list_skills`/`save_skill`) + `set_goal`, plus — when their store is present — 4 memory + 3 scheduler + 4 tasks + 1 inbox. **Not** in `get_all_tools`: notes/docs tools (on-by-default plugins), `delegate_to` (built-in `delegates` plugin), GitHub read tools (opt-in `github` plugin). Drop any via `tools.disabled`; add via a plugin. See [Starter tools](./docs/reference/starter-tools.md) |
| File GitHub issues | `tools/gh_issue.py` | **`/issue`** — a user-only chat command **and** a 🐛 utility-bar form dialog that file a GitHub issue via the `gh` CLI, scaffolding + enforcing the required sections so it clears the repo's issue gate. **Not** an agent tool — creating issues stays in your hands (the `github` plugin's GitHub tools are read-only). Repos are a quick-toggle list configured under **Settings ▸ System ▸ GitHub** (`github.repos` + `github.default_repo`), pairing with the portfolio manager's many-repo setup. See [File GitHub issues](./docs/guides/file-github-issues.md) |
| Knowledge store | `knowledge/store.py`, `knowledge/hybrid_store.py`, `ingestion/` | sqlite + FTS5 keyword search by default; an optional **hybrid** store adds embeddings + RRF fusion, and the **ingestion pipeline** pulls in txt/md/html/pdf/web/YouTube/audio/video sources. One `chunks` table for operator notes and conversation findings. Default-on; turn off with `middleware.knowledge: false` |
| Extensibility | `graph/skills/`, `tools/mcp_tools.py`, `graph/plugins/`, `plugins/` | Opt-in ways to extend a running agent without forking: **`SKILL.md` skills** (AgentSkills format, loaded on demand), **MCP servers** (external tools over stdio/HTTP), and **plugins** — drop-in packages that add tools, skills, subagents, workflows, FastAPI routes, background surfaces, managed MCP servers, **console rail views**, and their own config/secrets/Settings. Plugins are **installable from a git URL** (`python -m server plugin install `, pinned in `plugins.lock`) and shareable as repos — a repo is a full bundle. The first-party **Telegram** (`plugins/telegram`) integration ships bundled; **Discord**, **Slack**, and **Google** Gmail/Calendar install as external plugins from their own repos. See [Skills](./docs/guides/skills.md), [MCP](./docs/guides/mcp.md), [Plugins](./docs/guides/plugins.md), [Plugin console views](./docs/guides/plugin-views.md), [Install & publish plugins](./docs/guides/plugin-registry.md), ADR [0001](./docs/adr/0001-extensibility-and-plugin-architecture.md) / [0018](./docs/adr/0018-plugin-surfaces-routes-subagents.md) / [0019](./docs/adr/0019-plugin-config-settings-secrets.md) / [0026](./docs/adr/0026-plugin-contributed-console-surfaces.md) / [0027](./docs/adr/0027-install-plugins-from-git-url.md) |
diff --git a/docs/reference/starter-tools.md b/docs/reference/starter-tools.md
index 93c67363..5a65a179 100644
--- a/docs/reference/starter-tools.md
+++ b/docs/reference/starter-tools.md
@@ -4,7 +4,7 @@ The default tool set (from `tools/lg_tools.py::get_all_tools`):
- Four keyless general-purpose tools — `current_time`, `calculator`, `web_search`, `fetch_url` — that work without any state.
- Two **HITL tools** — `ask_human` (a free-text question) and `request_user_input` (a structured multi-step form) — pause the turn (A2A `input-required`) for the operator and resume with their answer (lead-agent only).
-- One **render tool** — `show_component` — emits a structured component (table, key-value, timeline, …) that the console renders inline in chat ([ADR 0051](/adr/0051-a2a-realtime-streaming-and-component-rendering)), instead of formatting it as prose.
+- _(The **render tool** `show_component` — inline structured components, [ADR 0051](/adr/0051-a2a-realtime-streaming-and-component-rendering) — is **temporarily disabled** pending a design pass; see [issue #1323](https://github.com/protoLabsAI/protoAgent/issues/1323). The codec + console renderer remain.)_
- Four **memory tools** — `memory_ingest`, `memory_recall`, `memory_list`, `memory_stats` — bound to the bundled `KnowledgeStore` (sqlite + FTS5, see [Configuration](/reference/configuration#knowledge)). Omitted when no store.
- Three **scheduler tools** — `schedule_task`, `list_schedules`, `cancel_schedule` — bound to the bundled scheduler backend (local sqlite, see [Schedule future work](/guides/scheduler)). Omitted when no scheduler.
- Four **tasks tools** — `task_create`, `task_list`, `task_update`, `task_close` — the agent's in-process planning board, bridged to the console Tasks panel. Bound when a tasks store is present (default in `server/agent_init.py`).
diff --git a/tests/test_components.py b/tests/test_components.py
index 9f82b542..3a78f8f1 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -3,6 +3,8 @@
from __future__ import annotations
+import pytest
+
from graph.components import (
COMPONENT_MIME,
COMPONENT_TYPES,
@@ -44,6 +46,7 @@ def test_mime_and_types(self):
assert set(COMPONENT_TYPES) == {"table", "keyvalue", "timeline"}
+@pytest.mark.skip(reason="show_component temporarily disabled — see issue #1323 (the codec above stays tested)")
class TestShowComponentTool:
def _tool(self):
from tools.lg_tools import get_all_tools
diff --git a/tools/lg_tools.py b/tools/lg_tools.py
index b2c16b6c..641977b0 100644
--- a/tools/lg_tools.py
+++ b/tools/lg_tools.py
@@ -105,34 +105,40 @@ def request_user_input(title: str, steps: list[dict], description: str = "") ->
return response if isinstance(response, str) else json.dumps(response)
-@tool
-def show_component(component: str, props: dict, title: str = "") -> str:
- """Render a structured UI component inline in the chat (ADR 0051).
-
- Use this to present structured data as a real widget instead of a markdown blob —
- a comparison table, a status/metrics block, a plan/timeline. Data-only and safe;
- for free-form generated HTML use an artifact instead.
-
- Args:
- component: one of ``"table"``, ``"keyvalue"``, ``"timeline"``.
- props: the component's data:
- - table: ``{"columns": ["A","B"], "rows": [["a1","b1"], ...]}``
- - keyvalue: ``{"items": [{"label": "Credits", "value": "183k"}, ...]}``
- - timeline: ``{"steps": [{"label": "Buy hauler", "state": "done|active|todo",
- "detail": "…"}, ...]}``
- title: optional heading shown above the component.
-
- Renders immediately for the user; also briefly summarize the data in your text reply
- (the component is a visual aid, not a substitute for your answer).
- """
- from graph.components import COMPONENT_TYPES, encode_component
-
- if component not in COMPONENT_TYPES:
- return f"Error: unknown component '{component}'. Use one of: {', '.join(COMPONENT_TYPES)}."
- payload = dict(props or {})
- if title and "title" not in payload:
- payload["title"] = title
- return f"Rendered a {component} component for the user. " + encode_component(component, payload)
+# show_component (inline component rendering, ADR 0051) is TEMPORARILY DISABLED — see
+# https://github.com/protoLabsAI/protoAgent/issues/1323. The component-v1 pipeline
+# (graph/components.py codec, the server/chat.py sentinel extraction, the console
+# component registry) is left intact; re-enable by uncommenting this tool AND its entry
+# in the get_all_tools() list below.
+#
+# @tool
+# def show_component(component: str, props: dict, title: str = "") -> str:
+# """Render a structured UI component inline in the chat (ADR 0051).
+#
+# Use this to present structured data as a real widget instead of a markdown blob —
+# a comparison table, a status/metrics block, a plan/timeline. Data-only and safe;
+# for free-form generated HTML use an artifact instead.
+#
+# Args:
+# component: one of ``"table"``, ``"keyvalue"``, ``"timeline"``.
+# props: the component's data:
+# - table: ``{"columns": ["A","B"], "rows": [["a1","b1"], ...]}``
+# - keyvalue: ``{"items": [{"label": "Credits", "value": "183k"}, ...]}``
+# - timeline: ``{"steps": [{"label": "Buy hauler", "state": "done|active|todo",
+# "detail": "…"}, ...]}``
+# title: optional heading shown above the component.
+#
+# Renders immediately for the user; also briefly summarize the data in your text reply
+# (the component is a visual aid, not a substitute for your answer).
+# """
+# from graph.components import COMPONENT_TYPES, encode_component
+#
+# if component not in COMPONENT_TYPES:
+# return f"Error: unknown component '{component}'. Use one of: {', '.join(COMPONENT_TYPES)}."
+# payload = dict(props or {})
+# if title and "title" not in payload:
+# payload["title"] = title
+# return f"Rendered a {component} component for the user. " + encode_component(component, payload)
@tool
@@ -1078,7 +1084,9 @@ def get_all_tools(knowledge_store=None, scheduler=None, inbox_store=None, tasks_
# LangGraph interrupt that only the lead turn's runner resumes. Subagents
# (run outside that runner) must not get it, so it's gated by allowlist:
# present in the full set for the lead agent, absent from subagent allowlists.
- tools = [current_time, calculator, web_search, fetch_url, ask_human, request_user_input, show_component, load_skill]
+ # show_component is temporarily disabled (inline component rendering, ADR 0051) — see
+ # issue #1323. Re-add it here (and uncomment its def above) to restore.
+ tools = [current_time, calculator, web_search, fetch_url, ask_human, request_user_input, load_skill]
# GitHub read tools (PRs/issues/commits) moved to the first-party `github`
# plugin (opt-in) — not everyone needs them. Enable with plugins.enabled: [github].
# Notes tools now ship with the first-party `notes` plugin (ADR 0034 S4):
From 56a1c9fb7b4274c815d362e12cbc2b9b10759af6 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 13:50:41 -0700
Subject: [PATCH 025/190] fix(web): collapse a delegation's nested tools
(stable height, no bounce) (#1322)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(web): collapse a delegation's nested tools (stable height, no bounce)
The subagent-nesting fix put the subagent's tools in the DS always-on `nested` rail, so
the task card GREW as each child streamed in and then COLLAPSED when it folded into the
chip — the up/down bounce the user reported. Now the nested tools live in the card's
COLLAPSIBLE body (revealed on expand) and the header shows a running count
('task → researcher · 3 tools'), so the card holds a stable one-row height while the
subagent works. Nesting is unchanged (still by explicit parent id); only its default
visibility moved from always-on to on-expand.
Co-Authored-By: Claude Opus 4.8 (1M context)
* fix(web): keep the most-recent tool in the slot until replaced (no blank gap)
The spotlight reserved an empty row whenever no tool was running — a blank gap between
tools and during the answer-streaming tail. Now the slot holds the LAST tool that ran
(running or just-finished) until a newer one replaces it, so it's never empty; the
previous tool drops into the running-total chip as the new one crossfades in.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/nesting.spec.ts | 31 +++++-----
apps/web/e2e/tool-nesting-explicit.spec.ts | 17 +++---
apps/web/src/chat/ToolCalls.tsx | 68 +++++++++++++---------
apps/web/src/chat/tool-calls.css | 12 +++-
4 files changed, 74 insertions(+), 54 deletions(-)
diff --git a/apps/web/e2e/nesting.spec.ts b/apps/web/e2e/nesting.spec.ts
index 769cee42..e04cd46b 100644
--- a/apps/web/e2e/nesting.spec.ts
+++ b/apps/web/e2e/nesting.spec.ts
@@ -1,28 +1,25 @@
import { expect, test } from "@playwright/test";
-// When the agent delegates with the `task` tool, the child tool calls that run
-// inside the subagent are nested under the parent task card instead of a flat
-// list. (A tool that starts while a `task` is still running is its child.)
+// When the agent delegates with the `task` tool, the subagent's own tool calls collapse
+// INSIDE the task card (revealed on expand) and the header shows a running count — so the
+// card holds a stable height as the subagent works instead of growing a nested rail.
-test("subagent child tools nest under the task card", async ({ page }) => {
+test("subagent child tools collapse inside the task card with a count", async ({ page }) => {
await page.goto("/app/", { waitUntil: "load" });
const composer = page.getByPlaceholder(/Message protoAgent/i);
await composer.waitFor({ state: "visible" });
await composer.fill("SUBAGENT delegate this");
await composer.press("Enter");
- // The parent task card is top-level inside a group. Frame = DS ToolCard (#832):
- // `.pl-toolcard-group` / `.pl-toolcard` / `.pl-toolcard__children`.
- const group = page.locator(".tool-calls > .pl-toolcard-group");
- await expect(group).toHaveCount(1);
- await expect(group.locator("> .pl-toolcard .pl-toolcard__name")).toHaveText("task");
+ // The task renders as a single card; its header carries the nested-tool count.
+ const card = page.locator(".tool-calls .pl-toolcard").first();
+ await expect(card).toBeVisible();
+ await expect(card.locator(".pl-toolcard__name")).toContainText("task");
+ await expect(card.locator(".pl-toolcard__name")).toContainText("1 tool");
+ // The child is NOT rendered until you expand — no always-on rail (that's the bounce fix).
+ await expect(page.locator(".pl-toolcard__children")).toHaveCount(0);
- // The web_search child renders inside the nested children container.
- const children = group.locator("> .pl-toolcard__children");
- await expect(children).toBeVisible();
- await expect(children.locator(".pl-toolcard__name")).toHaveText("web_search");
-
- // And it is NOT also rendered as a sibling top-level card.
- await expect(page.locator(".tool-calls > .pl-toolcard-group")).toHaveCount(1);
- await expect(page.locator(".tool-calls > .pl-toolcard")).toHaveCount(0);
+ // Expand → the subagent's web_search appears nested in the body.
+ await card.locator(".pl-toolcard__head").click();
+ await expect(card.locator(".pl-toolcard__children .pl-toolcard__name")).toHaveText("web_search");
});
diff --git a/apps/web/e2e/tool-nesting-explicit.spec.ts b/apps/web/e2e/tool-nesting-explicit.spec.ts
index 0c4ca738..6381e8f2 100644
--- a/apps/web/e2e/tool-nesting-explicit.spec.ts
+++ b/apps/web/e2e/tool-nesting-explicit.spec.ts
@@ -11,12 +11,13 @@ test("a subagent tool nests under the task even when its frames arrive after the
await composer.fill("NESTLATE delegate this");
await composer.press("Enter");
- // Final state: one top-level task group with web_search nested inside — NOT a stray
- // top-level sibling, even though the child frames streamed after the task closed.
- const group = page.locator(".tool-calls > .pl-toolcard-group");
- await expect(group).toHaveCount(1);
- await expect(group.locator("> .pl-toolcard .pl-toolcard__name")).toHaveText("task");
- await expect(group.locator("> .pl-toolcard__children .pl-toolcard__name")).toHaveText("web_search");
- // The child is nested, not rendered as a sibling top-level card.
- await expect(page.locator(".tool-calls > .pl-toolcard")).toHaveCount(0);
+ // The task card counts the child in its header even though that child's frames streamed
+ // in AFTER the task closed — proof the explicit parent-id linkage attached it.
+ const card = page.locator(".tool-calls .pl-toolcard").first();
+ await expect(card).toBeVisible();
+ await expect(card.locator(".pl-toolcard__name")).toContainText("task");
+ await expect(card.locator(".pl-toolcard__name")).toContainText("1 tool");
+ // It's nested in the body (revealed on expand), not a stray top-level sibling.
+ await card.locator(".pl-toolcard__head").click();
+ await expect(card.locator(".pl-toolcard__children .pl-toolcard__name")).toHaveText("web_search");
});
diff --git a/apps/web/src/chat/ToolCalls.tsx b/apps/web/src/chat/ToolCalls.tsx
index 712bc558..85230bfc 100644
--- a/apps/web/src/chat/ToolCalls.tsx
+++ b/apps/web/src/chat/ToolCalls.tsx
@@ -84,9 +84,8 @@ export function ToolCalls({
top.push(call);
}
}
- const running = top.filter((c) => c.status === "running");
const settled = top.filter((c) => c.status !== "running");
- const failedCount = settled.filter((c) => c.status === "error").length;
+ const failedCount = top.filter((c) => c.status === "error").length;
// Only TOP-LEVEL `task` groups get the cancel callback (the Stop affordance only shows
// for a running task); nested children and settled cards never need it.
@@ -99,30 +98,30 @@ export function ToolCalls({
/>
);
- // The folded summary chip — a running total of the whole block (running + settled), with
- // the finished cards inside it.
- const chip = (count: number) => (
+ // The folded summary chip — the block's running total, with the given finished cards inside.
+ const chip = (count: number, folded: ToolCall[]) => (
0 ? "error" : "done"}
failedCount={failedCount || undefined}
>
- {settled.map(group)}
+ {folded.map(group)}
);
- // PINNED SPOTLIGHT (live turn): the active card(s) sit in a fixed-height slot that never
- // collapses as tools cycle (`.tool-spotlight` reserves one row), and everything finished
- // folds into the chip below. So the block holds a stable height for the whole turn — the
- // column doesn't bounce as cards pop in and out; a finishing tool just crossfades from
- // the slot into the chip. The chip is the running total, so it's present from the first
- // finished tool onward.
+ // LIVE TURN: keep the MOST-RECENT tool in the spotlight slot until a newer one replaces
+ // it — so the slot is never empty (no blank gap between tools, or during the answer tail
+ // after the last tool finishes). Everything older folds into the running-total chip; a
+ // new tool crossfades into the slot and the previous one drops into the chip.
if (streaming) {
+ if (top.length === 0) return null;
+ const current = top[top.length - 1];
+ const folded = top.slice(0, -1);
return (
- {running.map(group)}
- {settled.length > 0 && chip(top.length)}
+ {group(current)}
+ {folded.length > 0 && chip(top.length, folded)}
);
}
@@ -130,13 +129,16 @@ export function ToolCalls({
// SETTLED (turn done for this block): a lone finished tool renders inline (no pointless
// "1 tool" chip); a real fan-out (≥2) stays folded.
if (settled.length >= 2) {
- return {chip(settled.length)} ;
+ return {chip(settled.length, settled)} ;
}
return {top.map(group)} ;
}
-/** A tool card plus, when it's a subagent `task`, its nested child tool cards.
- * Subagent nesting rides the DS `ToolCard` `nested` prop (indented child rail). */
+/** A tool card. For a subagent `task`, its child tool cards collapse INSIDE the card's
+ * body (revealed on expand) and the header shows a running count
+ * ("task → researcher · 3 tools"). Keeping them in the collapsible body — not the DS
+ * always-on `nested` rail — is what lets the card hold a STABLE one-row height while the
+ * subagent works, instead of growing a rail and then collapsing when it folds. */
function ToolGroup({
call,
childrenByParent,
@@ -147,21 +149,19 @@ function ToolGroup({
onCancelDelegation?: (id: string) => void;
}) {
const kids = childrenByParent.get(call.id);
- const nested = kids?.length
+ const nestedCards = kids?.length
? kids.map((kid) => (
// Children inherit no cancel callback — they aren't independent delegations.
))
: undefined;
- // Collapsed by default and stays put — the header row (icon, name, status) is
- // the stable at-a-glance view; expanding is an explicit, sticky choice so the
- // message doesn't reflow as tools start and finish. Pass `children` only when
- // there's detail so the DS gives us the disabled-caret behavior for empty cards
- // (the DS gates `hasBody` on `children != null`, so a no-detail card omits it).
+ // Collapsed by default; expanding reveals the args/result AND the subagent's nested
+ // tools (the `.pl-toolcard__children` indented rail, but here gated by the card's open
+ // state instead of always-on — so the header row stays a stable height as kids stream in).
const Icon = iconFor(call.name);
const body =
- call.input || call.output ? (
+ call.input || call.output || nestedCards ? (
<>
{call.input ? (
@@ -173,6 +173,7 @@ function ToolGroup({
) : null}
+ {nestedCards ? {nestedCards}
: null}
>
) : undefined;
@@ -196,13 +197,28 @@ function ToolGroup({
) : undefined;
+ // A running count of the subagent's tools, in the header — so a collapsed delegation
+ // reads "task → researcher · 3 tools" at a glance without expanding.
+ const kidCount = kids?.length ?? 0;
+ const name =
+ kidCount > 0 ? (
+ <>
+ {cardLabel(call)}
+
+ {" · "}
+ {kidCount} {kidCount === 1 ? "tool" : "tools"}
+
+ >
+ ) : (
+ cardLabel(call)
+ );
+
return (
}
duration={call.durationMs}
- nested={nested}
actions={actions}
>
{body}
diff --git a/apps/web/src/chat/tool-calls.css b/apps/web/src/chat/tool-calls.css
index c123dd19..fdc7cd3d 100644
--- a/apps/web/src/chat/tool-calls.css
+++ b/apps/web/src/chat/tool-calls.css
@@ -25,9 +25,15 @@
color: var(--pl-color-fg-muted);
}
-/* Pinned spotlight: while the turn streams, the active card(s) live in a slot that
- reserves one card-row of height and never collapses as tools cycle (so the column
- can't bounce). A finishing tool leaves the slot for the chip; the next crossfades in. */
+/* "· 3 tools" — running count of a delegation's nested subagent tools, in the header. */
+.tool-nested-count {
+ color: var(--pl-color-fg-subtle);
+ font-variant-numeric: tabular-nums;
+}
+
+/* Spotlight: while the turn streams, the slot holds the MOST-RECENT tool — kept up until a
+ newer one replaces it (so it's never empty: no blank gap between tools or during the
+ answer tail). The next tool crossfades in; the previous one drops into the chip below. */
.tool-spotlight {
display: flex;
flex-direction: column;
From 84afcc1956e17438527351a4d4f04def5d8ff466 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 13:54:29 -0700
Subject: [PATCH 026/190] docs(changelog): tool-call rendering overhaul +
show_component disable (#1325)
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 940a9bbe..f8c61016 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(deliberately CLI-only). (#1159)
### Changed
+- **Chat tool-call rendering overhaul.** A `task` delegation card now shows which subagent
+ ran (`task → researcher`); the subagent's own tools nest inside that card with a running
+ count (expand to see them); a turn's finished tools fold into one expandable "N tools"
+ summary chip; and the live tool block holds a stable height — the column no longer grows
+ and shrinks as tools stream in and out. The summary chip is the new `@protolabsai/ui`
+ `ToolCardSummary` primitive. (#1319, #1320, #1321, #1322)
+- **`show_component` (inline component rendering, ADR 0051) is temporarily disabled** — not
+ in the agent's tool roster or the console Tools tab. The component-v1 pipeline (codec,
+ wire extraction, console renderer) is left intact; tracked by #1323. (#1324)
- **The Goals and Tasks panels refresh on a bus push instead of polling every 5s.** Both
panels held a 5s `refetchInterval`; now the goal store publishes `goal.changed` (on
set/advance/clear) and the task store publishes `task.changed` (on create/update/
@@ -31,6 +40,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
task → it appears at once) and steady-state polling is gone. (#1310)
### Fixed
+- **Failed or user-cancelled `task` delegations now close as error cards (the X).** They
+ returned a plain `Error:` / `[cancelled]` string that rode the green "done" card (the
+ card's error flag is read from the tool-message status, which a string never set); the
+ tool now returns a `status="error"` ToolMessage so the card matches the red body. (#1319)
+- **A subagent's own tool calls reliably nest under the delegation card.** Nesting was
+ inferred from frame timing ("last open task wins"), which broke when the detached
+ delegation's end raced ahead of its child frames (and mis-attributed concurrent
+ `task_batch` delegations); the child frames now carry the parent delegation's tool-call
+ id so nesting is explicit and order-independent. (#1321)
- **Mid-stream output rendering no longer rescans the whole response on every chunk.**
The chat stream recomputed the visible `` (and the live reasoning view) by
re-running regexes over the *entire* accumulated text per token chunk — O(N²) over a
From d0fc7154624109f3f7d07f34bd08ec423876a188 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 13:57:40 -0700
Subject: [PATCH 027/190] chore: release v0.68.0 (#1326)
Co-authored-by: github-actions[bot]
---
CHANGELOG.md | 2 ++
pyproject.toml | 2 +-
sites/marketing/data/changelog.json | 16 ++++++++++++++++
3 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8c61016..ceab04f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.68.0] - 2026-06-23
+
### Added
- **`scripts/reset.sh` — factory-reset the default (prod) instance from the CLI.** Wipes
the prod instance's data + local config back to a clean slate so the next boot runs the
diff --git a/pyproject.toml b/pyproject.toml
index 49e0695c..d89ead42 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "protoagent"
-version = "0.67.0"
+version = "0.68.0"
description = "protoAgent — LangGraph + A2A template for spawning protoLabs agents"
requires-python = ">=3.11"
diff --git a/sites/marketing/data/changelog.json b/sites/marketing/data/changelog.json
index 7fd422ee..f3194ddd 100644
--- a/sites/marketing/data/changelog.json
+++ b/sites/marketing/data/changelog.json
@@ -1,4 +1,20 @@
[
+ {
+ "version": "v0.68.0",
+ "date": "2026-06-23",
+ "changes": [
+ "scripts/reset.sh — factory-reset the default (prod) instance from the CLI.",
+ "Chat tool-call rendering overhaul.",
+ "show_component (inline component rendering, ADR 0051) is temporarily disabled",
+ "The Goals and Tasks panels refresh on a bus push instead of polling every 5s.",
+ "Failed or user-cancelled task delegations now close as error cards (the X).",
+ "A subagent's own tool calls reliably nest under the delegation card.",
+ "Mid-stream output rendering no longer rescans the whole response on every chunk.",
+ "Empty-rail panel toggles fully disable.",
+ "Console-poll handlers no longer block the event loop.",
+ "The Docker image now serves the React console and stays in dep-lockstep with pyproject."
+ ]
+ },
{
"version": "v0.67.0",
"date": "2026-06-22",
From c980963271479809727e0b565d6bf1098f71ad01 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 16:35:47 -0700
Subject: [PATCH 028/190] feat: native reasoning + folded work block +
streamdown chat markdown (#1328)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: native reasoning — drop the scratch_pad/output protocol
Stream the gateway's native reasoning instead of a prompted :
- _ReasoningChatOpenAI (graph/llm.py) lifts the gateway's reasoning_content delta into
the message's additional_kwargs (langchain-openai drops it by design — 'use a
provider-specific subclass'). The model reasons natively; nothing is forced.
- server/chat.py streams that reasoning_content on the reasoning channel and the model's
content directly as the answer — the / parsing + StreamingViews
are gone. extract_output stays as a terminal fallback (still strips any stray tag).
- graph/prompts.py drops the protocol from the lead + subagent prompts.
The model now behaves natively (reasons, answers, calls tools) with no text protocol.
extract_output's pass-through fallback keeps the non-streaming path backward-compatible.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): render reasoning as a collapsed tool-style card
The native reasoning blocks rendered as big always-expanded boxes (the DS Reasoning
component auto-opens and locks open while streaming) — cramped and distracting between
the tool cards. ReasoningCard now renders each reasoning run as a DS ToolCard: same
chrome, COLLAPSED by default, a spinner while the model is still thinking (done-glyph
hidden — reasoning isn't pass/fail), stacking evenly with the tool cards. Expand to read.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): fold an agentic turn's reason→tool timeline into one WorkBlock
With native reasoning interleaved between every tool step, a turn exploded into a
forever-stack (reasoning → tool → reasoning → tool …) burying the answer, with bogus
per-fragment 'N tools' chips. WorkBlock now folds the whole intermediate timeline behind
ONE collapsed disclosure ('Working… / Worked · N tools') and renders the trailing answer
prominently below it — expand to replay the timeline (collapsed reasoning + flat tool
cards). Only engages when a turn interleaves reasoning WITH tools; tool-only /
reasoning-only / plain turns keep their inline rendering (so the e2e mocks are unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): keep the latest tool exposed below the WorkBlock while streaming
The WorkBlock folded the WHOLE timeline even mid-turn, so you couldn't see what the agent
was doing. Now while streaming it spotlights the most-recent tool below the 'Working… · N
tools' summary (kept until a newer tool replaces it) — the block isn't fully collapsed
while it works. On completion it all folds into the one block, answer below.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): render chat markdown with streamdown (streaming-hardened)
Swap the assistant-message markdown renderer from react-markdown + remark-gfm +
rehype-highlight to **streamdown** — a renderer built for streaming AI output. It
HARDENS incomplete markdown (a half-written code block / table / link no longer flashes
broken mid-stream) and memoizes blocks instead of re-parsing the whole answer on every
streamed token. XSS-safe (rehype-sanitize/harden); Shiki highlighting + a code-block
copy button; mermaid/katex support.
- Markdown.tsx: render via ; LazyMarkdown still code-splits it out of the
initial chunk (streamdown pulls Shiki/mermaid/katex — heavy).
- tailwind.config.cjs: scan node_modules/streamdown/dist so streamdown's utility classes
(code-block chrome, emphasis) generate; the existing shadcn→token bridge themes them
on-brand (ADR 0037 already maps --muted/--border/--primary/etc. to --pl-* tokens).
- main.tsx: import streamdown/styles.css (its stream-in animations) before the app theme.
- theme.css: drop the dead rehype-highlight `.hljs-*` token rules — Shiki emits inline
colors. The `.markdown` typography stays, so common text is visually unchanged.
- chat.spec.ts: streamdown renders inline emphasis as `[data-streamdown="strong"]`
(styled span) rather than ; align the selector. Block tags (h2/li/pre/code)
stay semantic and assert unchanged.
DEPENDENCY RESOLUTION (.npmrc + root @types/react pin):
A transitive dep (@docsearch/react, VitePress docs site) pins react@18 while the console
runs react@19. npm's strict peer resolver ERESOLVE-rejects that valid nested split when
re-resolving the tree to add streamdown, so add `.npmrc legacy-peer-deps=true` (so
`npm install` and CI's `npm ci` agree). Legacy mode would otherwise drop the root
@types/react@18 the console's tsc resolves for the global `JSX` namespace, so pin it
explicitly in the root package.json devDependencies. streamdown deduped onto react@19;
react/react-dom/vite/ui versions unchanged (lockfile churn is legacy-mode reserialization).
Co-Authored-By: Claude Opus 4.8 (1M context)
* fix(web): stop the WorkBlock spotlight strobing on rapid tool fan-outs
The spotlight slot rendered its current tool via `group(call)`, which keys by tool id —
so every new tool (e.g. each of task_batch's 100+ children) REMOUNTED the card, replaying
its mount animation and flashing the previous tool's output for a frame before the next
took over. On a big fan-out this strobed.
Add a `spotlight` mode that renders only the most-recent tool under a STABLE key
(`__spotlight__`), so React updates one card in place — name/status/output swap smoothly,
the entrance animation fires once. WorkBlock's spotlight uses it; the inline streaming
spotlight is switched to the same stable key.
Co-Authored-By: Claude Opus 4.8 (1M context)
* perf(tools): offload blocking web_search/fetch_url work off the event loop
A fan-out research turn (task_batch → N parallel researchers) pegged the server at ~99%
CPU and made it UNRESPONSIVE — even the cancel/tasks API couldn't answer. Root cause: two
tools did synchronous work directly on the asyncio event loop, so under concurrency they
serialised on the loop and starved everything else:
- web_search: `ddgs.text()` is a blocking network call — wrap it in `asyncio.to_thread`.
- fetch_url: `_extract_text_from_html` (BeautifulSoup over up to 2MB) is CPU-heavy and
synchronous — offload it too. The httpx GET was already async.
Now the loop stays free to service other subagents' IO, SSE streaming, and cancellation
while a search/parse runs in a worker thread. No change to research depth or output — the
detailed reports are unaffected; the server just stops wedging under load.
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): tally reasoning + tool + skill work in the WorkBlock header
The "Working…/Worked" card counted only tools ("· N tools"). Replace that with an
icon-and-count tally of the turn's actual work, in the DS ToolCard name slot:
- 🧠 reasoning steps (reasoning parts in the timeline)
- 🔧 tool calls (timeline tool ids, minus skill loads)
- 📚 skill loads (`load_skill` calls, broken out; the skill name rides its JSON input)
Each kind shows only when present. Hovering the header opens a DS Tooltip breakdown —
"2 reasoning steps", "5 tool calls · web_search ×3, fetch_url ×2", "1 skill load ·
deep-research" — so the collapsed card still tells you what happened without expanding.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
.npmrc | 7 +
apps/web/e2e/chat.spec.ts | 6 +-
apps/web/package.json | 1 +
apps/web/src/app/theme.css | 26 +-
apps/web/src/chat/ChatSurface.tsx | 76 +-
apps/web/src/chat/LazyMarkdown.tsx | 9 +-
apps/web/src/chat/Markdown.tsx | 32 +-
apps/web/src/chat/ReasoningCard.tsx | 23 +
apps/web/src/chat/ToolCalls.tsx | 46 +-
apps/web/src/chat/WorkBlock.tsx | 141 +
apps/web/src/chat/tool-calls.css | 85 +
apps/web/src/main.tsx | 1 +
apps/web/tailwind.config.cjs | 5 +-
graph/llm.py | 22 +-
graph/prompts.py | 23 +-
package-lock.json | 7276 +++++++++++++++------------
package.json | 1 +
server/chat.py | 38 +-
tests/test_reasoning_order.py | 67 +
tools/lg_tools.py | 16 +-
20 files changed, 4543 insertions(+), 3358 deletions(-)
create mode 100644 .npmrc
create mode 100644 apps/web/src/chat/ReasoningCard.tsx
create mode 100644 apps/web/src/chat/WorkBlock.tsx
create mode 100644 tests/test_reasoning_order.py
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000..050b0d77
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,7 @@
+# A transitive dep (@docsearch/react via the VitePress docs site) pins react/react-dom@18
+# while the console (apps/web) runs react@19. npm's strict peer resolver ERESOLVE-rejects that
+# valid nested split whenever the tree is re-resolved (e.g. adding a dep). Use legacy peer
+# resolution so `npm install` and CI's `npm ci` agree. NOTE: legacy mode would otherwise drop
+# the root @types/react@18 the console's tsc resolves for the global `JSX` namespace — it's
+# pinned explicitly in the root package.json devDependencies to survive.
+legacy-peer-deps=true
diff --git a/apps/web/e2e/chat.spec.ts b/apps/web/e2e/chat.spec.ts
index 6c24ad5a..2fafec16 100644
--- a/apps/web/e2e/chat.spec.ts
+++ b/apps/web/e2e/chat.spec.ts
@@ -67,10 +67,12 @@ test("tool-call card is collapsed by default and renders structured components",
test("expanded state is sticky and the assistant answer renders as markdown", async ({ page }) => {
await send(page, "MARKDOWN: summarize");
- // Final answer renders through the markdown pipeline.
+ // Final answer renders through the streamdown markdown pipeline. streamdown emits real
+ // semantic tags for block elements (h2/li/pre/code) but renders inline emphasis as a
+ // styled span (`[data-streamdown="strong"]`, class `font-semibold`) rather than .
const md = page.locator(".pl-message--assistant .markdown");
await expect(md.locator("h2")).toHaveText("Summary");
- await expect(md.locator("strong")).toHaveText("key");
+ await expect(md.locator('[data-streamdown="strong"]')).toHaveText("key");
await expect(md.locator("li")).toHaveCount(2);
await expect(md.locator("pre code")).toContainText("const x = 1;");
});
diff --git a/apps/web/package.json b/apps/web/package.json
index e7012d0a..9e11df10 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -27,6 +27,7 @@
"react-markdown": "^9.0.1",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.0",
+ "streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.14"
diff --git a/apps/web/src/app/theme.css b/apps/web/src/app/theme.css
index 18ef562b..f849a80b 100644
--- a/apps/web/src/app/theme.css
+++ b/apps/web/src/app/theme.css
@@ -915,29 +915,9 @@ select:disabled {
.markdown th { background: var(--bg-raised); }
.markdown hr { border: 0; border-top: 1px solid var(--border); margin: 0.9em 0; }
-/* highlight.js tokens (compact dark theme for rehype-highlight output) */
-.markdown .hljs-comment,
-.markdown .hljs-quote { color: var(--fg-muted); font-style: italic; }
-.markdown .hljs-keyword,
-.markdown .hljs-selector-tag,
-.markdown .hljs-built_in,
-.markdown .hljs-name { color: #c792ea; }
-.markdown .hljs-string,
-.markdown .hljs-attr,
-.markdown .hljs-template-tag { color: #c3e88d; }
-.markdown .hljs-number,
-.markdown .hljs-literal,
-.markdown .hljs-type { color: #f78c6c; }
-.markdown .hljs-title,
-.markdown .hljs-function .hljs-title,
-.markdown .hljs-section { color: #82aaff; }
-.markdown .hljs-attribute,
-.markdown .hljs-variable,
-.markdown .hljs-symbol { color: #ffcb6b; }
-.markdown .hljs-deletion { color: #ff5370; }
-.markdown .hljs-addition { color: #c3e88d; }
-.markdown .hljs-emphasis { font-style: italic; }
-.markdown .hljs-strong { font-weight: 600; }
+/* Code highlighting is now Shiki (via streamdown), which emits inline token colors — the
+ old rehype-highlight `.hljs-*` token rules are gone. streamdown's code-block chrome
+ (header / copy button) is styled by its own Tailwind classes (scanned in tailwind.config). */
/* Chat composer + slash-command autocomplete — moved to src/chat/chat.css (#832). */
/* HITL request card — moved to src/chat/hitl.css (#832). */
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index 2a78a0a9..2abeb410 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -7,7 +7,6 @@ import {
MessageAction,
MessageActions,
PromptInput,
- Reasoning,
} from "@protolabsai/ui/ai";
import { TabBar } from "@protolabsai/ui/navigation";
import {
@@ -25,7 +24,7 @@ import { api } from "../lib/api";
import { errMsg } from "../lib/format";
import { runtimeStatusQuery } from "../lib/queries";
import { ConfirmDialog } from "@protolabsai/ui/overlays";
-import type { ChatMessage, HitlPayload, SlashCommand, ToolCall } from "../lib/types";
+import type { ChatMessage, ChatPart, HitlPayload, SlashCommand, ToolCall } from "../lib/types";
import { HitlForm } from "./HitlForm";
import { notifyIfHidden } from "../lib/notify";
import {
@@ -38,6 +37,8 @@ import {
import { ChatComponent } from "./ChatComponent";
import { ComposerModelSelect } from "./ComposerModelSelect";
import { Markdown } from "./LazyMarkdown";
+import { ReasoningCard } from "./ReasoningCard";
+import { WorkBlock } from "./WorkBlock";
import { useUI } from "../state/uiStore";
import { filesFromTransfer, isLargePaste, pastedTextFile } from "./paste";
import { ToolCalls } from "./ToolCalls";
@@ -1009,35 +1010,54 @@ function ChatSessionSlot({
>
{message.reasoning && !(message.parts && message.parts.length) ? (
// History-loaded turns have no ordered parts — fall back to the flat
- // collapsible "thinking" block (open while still reasoning, auto-collapses
- // once the answer starts). Live turns render reasoning inline via parts.
-
- {message.reasoning}
-
+ // collapsed reasoning card. Live turns render reasoning inline via parts.
+
) : null}
{message.parts && message.parts.length ? (
- // Live turns: reasoning, text runs and tool groups in emission order, so a
- // pre-tool preamble renders above the cards, the answer below them, and
- // "thinking" inline next to the step it precedes.
- message.parts.map((part, i, arr) =>
- part.kind === "tools" ? (
-
- ) : part.kind === "reasoning" ? (
- part.text.trim() ? (
- // Stream the animation only on the trailing run (thinking in progress).
-
- {part.text}
-
- ) : null
- ) : part.text.trim() ? (
- message.role === "user" ? (
- {part.text}
+ (() => {
+ // Fold the intermediate reason→tool timeline behind ONE WorkBlock so the
+ // answer leads — but ONLY when the turn interleaves reasoning WITH tools (the
+ // forever-stack case). Tool-only / reasoning-only turns keep their inline
+ // cards; a plain turn is just the answer. The "answer" is the trailing run of
+ // text parts; everything before it is the work. User messages carry only text.
+ const parts = message.parts!;
+ let split = parts.length;
+ while (split > 0 && parts[split - 1].kind === "text") split--;
+ const workParts = parts.slice(0, split);
+ const answerParts = parts.slice(split);
+ const hasTools = workParts.some((p) => p.kind === "tools");
+ const hasReasoning = workParts.some((p) => p.kind === "reasoning");
+ const renderText = (part: ChatPart, key: string) =>
+ part.kind !== "text" || !part.text.trim() ? null : message.role === "user" ? (
+ {part.text}
) : (
- // assistant + system carry markdown; only the user's own input stays literal.
- {part.text}
- )
- ) : null,
- )
+ {part.text}
+ );
+ const renderInline = (part: ChatPart, i: number) =>
+ part.kind === "tools" ? (
+
+ ) : part.kind === "reasoning" ? (
+ part.text.trim() ? (
+
+ ) : null
+ ) : (
+ renderText(part, `w${i}`)
+ );
+ return (
+ <>
+ {hasTools && hasReasoning ? (
+
+ ) : (
+ workParts.map(renderInline)
+ )}
+ {answerParts.map((part, i) => renderText(part, `a${i}`))}
+ >
+ );
+ })()
) : (
// History-loaded messages have no ordered parts — keep the grouped layout
// (tool cards above the text; order isn't recoverable from storage).
diff --git a/apps/web/src/chat/LazyMarkdown.tsx b/apps/web/src/chat/LazyMarkdown.tsx
index f626ddb1..9dcf3fb9 100644
--- a/apps/web/src/chat/LazyMarkdown.tsx
+++ b/apps/web/src/chat/LazyMarkdown.tsx
@@ -1,10 +1,9 @@
import { lazy, Suspense } from "react";
-// The markdown pipeline (react-markdown + remark-gfm + rehype-highlight, which
-// bundles highlight.js) is the heaviest dependency in the app. Load it lazily
-// so it isn't in the initial chunk — it only matters once an assistant message
-// renders. Until the chunk arrives, fall back to the raw text (a blink at most;
-// then the same `.markdown` container takes over).
+// The markdown pipeline (streamdown — which bundles Shiki + mermaid + katex) is the
+// heaviest dependency in the app. Load it lazily so it isn't in the initial chunk — it
+// only matters once an assistant message renders. Until the chunk arrives, fall back to
+// the raw text (a blink at most; then the same `.markdown` container takes over).
const MarkdownImpl = lazy(() => import("./Markdown").then((m) => ({ default: m.Markdown })));
export function Markdown({ children }: { children: string }) {
diff --git a/apps/web/src/chat/Markdown.tsx b/apps/web/src/chat/Markdown.tsx
index 764eb8c9..6a2cdc16 100644
--- a/apps/web/src/chat/Markdown.tsx
+++ b/apps/web/src/chat/Markdown.tsx
@@ -1,29 +1,17 @@
-import type { ComponentPropsWithoutRef } from "react";
-import ReactMarkdown, { type Components } from "react-markdown";
-import rehypeHighlight from "rehype-highlight";
-import remarkGfm from "remark-gfm";
+import { Streamdown } from "streamdown";
-// GitHub-flavored markdown (tables, strikethrough, task lists, autolinks) +
-// syntax-highlighted code blocks. No raw-HTML plugin — LLM output is untrusted,
-// so we never render embedded HTML (XSS guard).
-const REMARK = [remarkGfm];
-const REHYPE = [rehypeHighlight];
-
-const components: Components = {
- // External links open in a new tab; never let markdown navigate the app. Strip react-markdown's
- // `node` prop before spreading so it doesn't reach the DOM .
- a: ({ node: _node, ...props }) => (
- )} target="_blank" rel="noreferrer noopener" />
- ),
-};
-
-/** Render assistant message text as markdown. Wrapped in `.markdown` for theming. */
+/**
+ * Assistant message text via **streamdown** — a streaming-markdown renderer built for AI
+ * output. It HARDENS incomplete markdown (a half-written code block / table / link doesn't
+ * flash broken mid-stream) and memoizes blocks, instead of re-parsing the whole answer on
+ * every streamed token (the old react-markdown path was O(N²) per turn). XSS-safe
+ * (rehype-sanitize/harden, no raw HTML); Shiki code highlighting. Wrapped in `.markdown`
+ * for the chat theme.
+ */
export function Markdown({ children }: { children: string }) {
return (
-
- {children}
-
+ {children}
);
}
diff --git a/apps/web/src/chat/ReasoningCard.tsx b/apps/web/src/chat/ReasoningCard.tsx
new file mode 100644
index 00000000..18c663f9
--- /dev/null
+++ b/apps/web/src/chat/ReasoningCard.tsx
@@ -0,0 +1,23 @@
+import { Brain } from "lucide-react";
+
+import { ToolCard } from "@protolabsai/ui/tool-card";
+
+/**
+ * A reasoning ("thinking") block rendered as a tool-style card: the SAME DS `ToolCard`
+ * chrome as a tool call, COLLAPSED by default, so the model's native reasoning stacks
+ * consistently with the tool cards in the thread instead of dominating it as big expanded
+ * boxes. Expand to read the reasoning. A spinner shows while the model is still thinking;
+ * the done-glyph is hidden (reasoning isn't a pass/fail result — see tool-calls.css).
+ */
+export function ReasoningCard({ text, streaming = false }: { text: string; streaming?: boolean }) {
+ return (
+ }
+ status={streaming ? "running" : "done"}
+ className="reasoning-card"
+ >
+ {text}
+
+ );
+}
diff --git a/apps/web/src/chat/ToolCalls.tsx b/apps/web/src/chat/ToolCalls.tsx
index 85230bfc..fde0b061 100644
--- a/apps/web/src/chat/ToolCalls.tsx
+++ b/apps/web/src/chat/ToolCalls.tsx
@@ -62,12 +62,22 @@ function cardLabel(call: ToolCall): ReactNode {
export function ToolCalls({
calls,
streaming = false,
+ flat = false,
+ spotlight = false,
onCancelDelegation,
}: {
calls: ToolCall[];
/** The turn is still live. Keeps the spotlight slot reserved for the whole turn so the
* layout doesn't bounce in the gap between one tool finishing and the next starting. */
streaming?: boolean;
+ /** Render every card plainly — no spotlight, no fold chip. For use INSIDE the WorkBlock,
+ * where the whole reason→tool timeline is already folded behind one disclosure. */
+ flat?: boolean;
+ /** Spotlight ONLY the most-recent tool, in a single slot with a STABLE identity — the
+ * card updates in place (name/status/output swap) as tools advance instead of remounting
+ * per tool. Without this, a rapid fan-out (e.g. task_batch's many children) strobes: each
+ * new id remounts the card, replaying its mount animation and flashing the prior output. */
+ spotlight?: boolean;
/** Abort a running top-level `task` delegation by its tool-call id (Tier 2). When
* omitted, no Stop affordance renders (e.g. historical/finished messages). */
onCancelDelegation?: (id: string) => void;
@@ -110,6 +120,31 @@ export function ToolCalls({
);
+ // Inside the WorkBlock: just the plain cards, in order (the timeline is already folded).
+ if (flat) {
+ return {top.map(group)} ;
+ }
+
+ // A single, identity-STABLE slot holding only the most-recent tool. The fixed key keeps
+ // React updating one card in place as the current tool changes, so a fast fan-out advances
+ // smoothly instead of remounting (and strobing) on every new tool id.
+ if (spotlight) {
+ if (top.length === 0) return null;
+ const current = top[top.length - 1];
+ return (
+
+
+
+
+
+ );
+ }
+
// LIVE TURN: keep the MOST-RECENT tool in the spotlight slot until a newer one replaces
// it — so the slot is never empty (no blank gap between tools, or during the answer tail
// after the last tool finishes). Everything older folds into the running-total chip; a
@@ -120,7 +155,16 @@ export function ToolCalls({
const folded = top.slice(0, -1);
return (
- {group(current)}
+ {/* Stable key: the slot updates in place as the current tool advances (no remount
+ strobe — see the `spotlight` prop note). */}
+
+
+
{folded.length > 0 && chip(top.length, folded)}
);
diff --git a/apps/web/src/chat/WorkBlock.tsx b/apps/web/src/chat/WorkBlock.tsx
new file mode 100644
index 00000000..2f039ad1
--- /dev/null
+++ b/apps/web/src/chat/WorkBlock.tsx
@@ -0,0 +1,141 @@
+import { BookOpen, Brain, Wrench } from "lucide-react";
+
+import { ToolCard } from "@protolabsai/ui/tool-card";
+import { Tooltip } from "@protolabsai/ui/overlays";
+
+import type { ChatPart, ToolCall } from "../lib/types";
+import { toolsForGroup } from "./parts";
+import { ReasoningCard } from "./ReasoningCard";
+import { ToolCalls } from "./ToolCalls";
+
+type ToolsPart = Extract;
+
+/** A skill load is a `load_skill` tool call; the skill name rides its JSON input. */
+function skillName(input?: string): string {
+ if (!input) return "skill";
+ try {
+ const parsed = JSON.parse(input) as { name?: unknown };
+ return typeof parsed.name === "string" ? parsed.name : "skill";
+ } catch {
+ return "skill";
+ }
+}
+
+function plural(n: number, one: string): string {
+ return `${n} ${one}${n === 1 ? "" : "s"}`;
+}
+
+/**
+ * Folds an agentic turn's intermediate reason→tool timeline behind ONE collapsed disclosure
+ * so the final answer leads. The header tallies the WORK done this turn — reasoning steps,
+ * tool calls, and skill loads — each as an icon + count, with a hover breakdown. WHILE
+ * STREAMING it keeps the most-recent tool exposed below the summary (kept until a newer tool
+ * replaces it) so the block isn't fully collapsed while the agent works. Expand to replay
+ * the full timeline.
+ */
+export function WorkBlock({
+ parts,
+ toolCalls,
+ streaming,
+}: {
+ parts: ChatPart[];
+ toolCalls?: ToolCall[];
+ streaming: boolean;
+}) {
+ // Tally the turn's work. Tool ids come from the timeline; resolve each to its call so we
+ // can split plain tool calls from `load_skill` (skill loads get their own count).
+ const callById = new Map((toolCalls ?? []).map((c) => [c.id, c]));
+ const toolIds = new Set();
+ for (const p of parts) if (p.kind === "tools") for (const id of p.ids) toolIds.add(id);
+
+ const toolTally = new Map();
+ const skillNames: string[] = [];
+ for (const id of toolIds) {
+ const call = callById.get(id);
+ if (!call) continue;
+ if (call.name === "load_skill") skillNames.push(skillName(call.input));
+ else toolTally.set(call.name, (toolTally.get(call.name) ?? 0) + 1);
+ }
+ const toolCount = [...toolTally.values()].reduce((a, b) => a + b, 0);
+ const skillCount = skillNames.length;
+ const reasoningCount = parts.filter((p) => p.kind === "reasoning" && p.text.trim()).length;
+
+ const label = streaming ? "Working…" : "Worked";
+ const toolList = [...toolTally.entries()].map(([n, c]) => (c > 1 ? `${n} ×${c}` : n)).join(", ");
+
+ // Hover breakdown — the lines behind the icon counts.
+ const breakdown = (
+
+ {reasoningCount > 0 &&
{plural(reasoningCount, "reasoning step")}
}
+ {toolCount > 0 && (
+
+ {plural(toolCount, "tool call")}
+ {toolList && · {toolList} }
+
+ )}
+ {skillCount > 0 && (
+
+ {plural(skillCount, "skill load")}
+ · {skillNames.join(", ")}
+
+ )}
+
+ );
+
+ const header = (
+
+
+ {label}
+ {reasoningCount > 0 && (
+
+
+ {reasoningCount}
+
+ )}
+ {toolCount > 0 && (
+
+
+ {toolCount}
+
+ )}
+ {skillCount > 0 && (
+
+
+ {skillCount}
+
+ )}
+
+
+ );
+
+ // While streaming, spotlight the most-recent tool below the summary.
+ let spotlightIds: string[] = [];
+ if (streaming) {
+ const toolsParts = parts.filter((p): p is ToolsPart => p.kind === "tools");
+ const last = toolsParts[toolsParts.length - 1];
+ if (last && last.ids.length) spotlightIds = [last.ids[last.ids.length - 1]];
+ }
+
+ return (
+
+
+
+ {parts.map((part, i) =>
+ part.kind === "reasoning" ? (
+ part.text.trim() ?
: null
+ ) : part.kind === "tools" ? (
+
+ ) : part.text.trim() ? (
+
{part.text}
+ ) : null,
+ )}
+
+
+ {spotlightIds.length > 0 ? (
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/web/src/chat/tool-calls.css b/apps/web/src/chat/tool-calls.css
index fdc7cd3d..3be0d23e 100644
--- a/apps/web/src/chat/tool-calls.css
+++ b/apps/web/src/chat/tool-calls.css
@@ -31,6 +31,91 @@
font-variant-numeric: tabular-nums;
}
+/* Reasoning rendered as a tool-style card (ReasoningCard) — same DS ToolCard chrome,
+ collapsed by default, so the model's native thinking stacks evenly with the tool cards
+ instead of dominating the thread. */
+.reasoning-card {
+ margin: 0 0 0.6em; /* match the tool-call column's bottom margin */
+}
+.reasoning-card .pl-toolcard__status--done {
+ display: none; /* reasoning isn't a pass/fail result — keep only the streaming spinner */
+}
+.reasoning-text {
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.82rem;
+ line-height: 1.55;
+ color: var(--pl-color-fg-muted);
+ max-height: 340px;
+ overflow: auto;
+}
+
+/* WorkBlock — folds an agentic turn's whole reason→tool timeline behind ONE disclosure so
+ the final answer leads. While streaming, the most-recent tool stays exposed below the
+ summary (`.work-spotlight`); both sit tightly in the `.work` container. */
+.work {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pl-space-2);
+ margin: 0 0 0.6em;
+}
+.work-block,
+.work-spotlight .tool-calls {
+ margin: 0;
+}
+.work-timeline {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pl-space-2);
+}
+/* Inside the timeline the nested tool lists + reasoning cards stack on the flex gap, not
+ their own bottom margins. */
+.work-timeline .tool-calls,
+.work-timeline .reasoning-card {
+ margin: 0;
+}
+.work-text {
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.85rem;
+ line-height: 1.5;
+ color: var(--pl-color-fg-muted);
+}
+
+/* WorkBlock header: the turn's work tallied as icon + count chips (reasoning / tools /
+ skill loads), in the DS ToolCard name slot. Hover surfaces the breakdown tooltip. */
+.work-stats {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+}
+.work-stat-label {
+ font-weight: 500;
+}
+.work-stat {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ color: var(--pl-color-fg-muted);
+ font-variant-numeric: tabular-nums;
+}
+.work-stat svg {
+ flex: none;
+ opacity: 0.85;
+}
+/* Tooltip breakdown — one line per work kind, with the tool/skill names dimmed. */
+.work-breakdown {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 0.78rem;
+ line-height: 1.45;
+ max-width: 280px;
+}
+.work-breakdown-detail {
+ color: var(--pl-color-fg-subtle);
+}
+
/* Spotlight: while the turn streams, the slot holds the MOST-RECENT tool — kept up until a
newer one replaces it (so it's never empty: no blank gap between tools or during the
answer tail). The next tool crossfades in; the previous one drops into the chip below. */
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index 1638b430..129e8ba0 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -13,6 +13,7 @@ import { isLauncherWindow } from "./lib/desktop";
import "@protolabsai/design/css/tokens";
import "./app/tailwind.css";
import "@protolabsai/ui/styles.css";
+import "streamdown/styles.css"; // streaming-markdown base styles (before the app theme so it can override)
import "./app/theme-base.css"; // shared token bridge + resets — must load before the rest
import "./app/theme.css";
import { activateSlugAgent } from "./lib/api";
diff --git a/apps/web/tailwind.config.cjs b/apps/web/tailwind.config.cjs
index 35ce6ac2..a9144ce7 100644
--- a/apps/web/tailwind.config.cjs
+++ b/apps/web/tailwind.config.cjs
@@ -5,7 +5,10 @@
// legacy theme.css without resetting base styles (incremental migration, ADR 0037 D4).
module.exports = {
presets: [require("@protolabsai/design/tailwind")],
- content: ["./index.html", "./src/**/*.{ts,tsx}"],
+ // streamdown (chat markdown renderer) ships pre-built JS that uses Tailwind utility
+ // classes for its chrome (code-block header/copy button, tables, emphasis). Scan its dist
+ // so those classes generate; the shadcn→token vars below theme them on-brand.
+ content: ["./index.html", "./src/**/*.{ts,tsx}", "../../node_modules/streamdown/dist/*.js"],
corePlugins: { preflight: false },
theme: {
extend: {
diff --git a/graph/llm.py b/graph/llm.py
index 8aab6b44..2bf89648 100644
--- a/graph/llm.py
+++ b/graph/llm.py
@@ -18,6 +18,26 @@
_GATEWAY_UA = "protoAgent/0.1 (+https://github.com/protoLabsAI/protoAgent)"
+class _ReasoningChatOpenAI(ChatOpenAI):
+ """ChatOpenAI that surfaces the gateway's NATIVE reasoning stream.
+
+ The base class deliberately drops the non-OpenAI ``reasoning_content`` delta field
+ ("Use a provider-specific subclass" — its own docstring); DeepSeek and most reasoning
+ models routed through our LiteLLM gateway emit it token-by-token. We lift it into the
+ message's ``additional_kwargs`` so the chat stream can render the model's REAL thinking
+ in real time, instead of a prompted ```` narration.
+ """
+
+ def _convert_chunk_to_generation_chunk(self, chunk, default_chunk_class, base_generation_info):
+ gen = super()._convert_chunk_to_generation_chunk(chunk, default_chunk_class, base_generation_info)
+ if gen is not None:
+ choices = chunk.get("choices") or []
+ reasoning = (choices[0].get("delta") or {}).get("reasoning_content") if choices else None
+ if reasoning:
+ gen.message.additional_kwargs["reasoning_content"] = reasoning
+ return gen
+
+
def _build_llm_kwargs(config: LangGraphConfig) -> dict:
"""Assemble the ChatOpenAI kwargs from config (extracted for testing)."""
api_key = config.api_key or os.environ.get("OPENAI_API_KEY", "")
@@ -134,7 +154,7 @@ def create_llm(
# caches per (model, effort) so the rebuild is paid once.
if reasoning_effort:
kwargs["reasoning_effort"] = reasoning_effort
- return ChatOpenAI(**kwargs)
+ return _ReasoningChatOpenAI(**kwargs)
def _build_embeddings(config: LangGraphConfig) -> "OpenAIEmbeddings | None":
diff --git a/graph/prompts.py b/graph/prompts.py
index 5dc196a3..633b300c 100644
--- a/graph/prompts.py
+++ b/graph/prompts.py
@@ -13,9 +13,9 @@
5. Operator guidelines (the template ships neutral defaults — override
in your fork to encode domain behavior like "verify, don't trust"
or "always end with a PASS/WARN/FAIL verdict").
-6. Response format (```` / ```` protocol, parsed
- by ``graph/output_format.py`` and routed server-side so scratch
- content never reaches consumers).
+
+The model answers naturally; its reasoning streams natively (the gateway's
+``reasoning_content``), so there is no ````/```` text protocol.
When forking, the main thing to edit is the operator guidelines block
— that's where you encode how the agent behaves in its specific
@@ -24,7 +24,6 @@
from pathlib import Path
-from graph.output_format import OUTPUT_FORMAT_INSTRUCTIONS
from graph.subagents.config import SUBAGENT_REGISTRY
@@ -101,14 +100,10 @@ def build_system_prompt(
do NOT call a status tool in a loop to wait it out; that burns the whole
turn. Call `wait(seconds, then=…)` to yield and be re-triggered when it's
ready (it ends the turn and resumes you with `then`).
-- Keep internal deliberation in ``; put only the
- user-facing answer in `` — the handler parses these tags and
- never forwards scratch content to consumers.
+- Answer directly and naturally. Your reasoning is streamed separately (native
+ reasoning), so think freely — don't narrate your deliberation in the answer.
""")
- # 5. Response format (scratch_pad + output tags — parsed server-side)
- parts.append(OUTPUT_FORMAT_INSTRUCTIONS)
-
return "\n\n".join(parts)
@@ -190,11 +185,9 @@ def _build_subagent_section() -> str:
def build_subagent_prompt(agent_name: str, workspace: str = "/sandbox") -> str:
"""Build system prompt for a specific subagent.
- Subagents' return values are parsed by the lead agent, so they use
- the same ```` / ```` protocol — keeps the
- pipeline uniform and lets ``OutputFilter`` handle subagent output
- identically to top-level streaming.
+ Subagents answer naturally (no ``/`` protocol); their final
+ message content is the result the lead agent reads. Reasoning streams natively.
"""
config = SUBAGENT_REGISTRY.get(agent_name)
base = config.system_prompt if config else "You are a subagent. Complete the delegated task efficiently."
- return f"{base}\n\n{OUTPUT_FORMAT_INSTRUCTIONS}"
+ return base
diff --git a/package-lock.json b/package-lock.json
index 7b3e6053..955e8b5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"devDependencies": {
"@protolabsai/design": "^0.3.0",
"@protolabsai/vitepress-theme": "^0.3.6",
+ "@types/react": "18.3.29",
"vitepress": "^1.6.4"
}
},
@@ -40,6 +41,7 @@
"react-markdown": "^9.0.1",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.0",
+ "streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.14"
@@ -982,6 +984,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -990,6 +993,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@asamuzakjp/css-color": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
@@ -1323,6 +1339,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
+ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
+ "license": "MIT"
+ },
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
@@ -1336,6 +1358,12 @@
"specificity": "bin/cli.js"
}
},
+ "node_modules/@chevrotain/types": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz",
+ "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
+ "license": "Apache-2.0"
+ },
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
@@ -1580,21 +1608,6 @@
}
}
},
- "node_modules/@docsearch/js/node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -1918,24 +1931,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@@ -1953,24 +1948,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
- "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
@@ -1988,24 +1965,6 @@
"node": ">=12"
}
},
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
- "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
@@ -2144,13 +2103,24 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@iconify/utils": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz",
+ "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@iconify/types": "^2.0.0",
+ "import-meta-resolve": "^4.2.0"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -2172,6 +2142,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -2181,18 +2152,29 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mermaid-js/parser": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz",
+ "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@chevrotain/types": "~11.1.1"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
@@ -2216,6 +2198,7 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -2229,6 +2212,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -2238,6 +2222,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -3561,6 +3546,259 @@
"assertion-error": "^2.0.1"
}
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
@@ -3592,6 +3830,12 @@
"@types/estree": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -3645,20 +3889,27 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
- "license": "MIT",
- "peer": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.29",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz",
"integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -3678,6 +3929,16 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
+ "node_modules/@upsetjs/venn.js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
+ "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
+ "license": "MIT",
+ "optionalDependencies": {
+ "d3-selection": "^3.0.0",
+ "d3-transition": "^3.0.1"
+ }
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -4080,12 +4341,14 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -4099,6 +4362,7 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/aria-hidden": {
@@ -4197,6 +4461,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -4219,6 +4484,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -4265,6 +4531,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -4355,6 +4622,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -4379,6 +4647,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -4422,6 +4691,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -4450,6 +4720,15 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -4468,6 +4747,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -4491,1701 +4771,2251 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
- "node_modules/data-urls": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
- "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
- "dev": true,
+ "node_modules/cytoscape": {
+ "version": "3.34.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz",
+ "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==",
"license": "MIT",
- "dependencies": {
- "whatwg-mimetype": "^5.0.0",
- "whatwg-url": "^16.0.0"
- },
"engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ "node": ">=0.10"
}
},
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
"license": "MIT",
"dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
+ "cose-base": "^1.0.0"
},
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
}
},
- "node_modules/decimal.js": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
- "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/decode-named-character-reference": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
- "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
"license": "MIT",
"dependencies": {
- "character-entities": "^2.0.0"
+ "cose-base": "^2.2.0"
},
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
}
},
- "node_modules/dequal": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
"license": "MIT",
- "engines": {
- "node": ">=6"
+ "dependencies": {
+ "layout-base": "^2.0.0"
}
},
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/detect-node-es": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
- "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
"license": "MIT"
},
- "node_modules/devlop": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
- "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
- "license": "MIT",
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
"dependencies": {
- "dequal": "^2.0.0"
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
},
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/didyoumean": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "license": "Apache-2.0"
- },
- "node_modules/dlv": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "license": "MIT"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.364",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz",
- "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/emoji-regex-xs": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
- "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
- "dev": true,
- "license": "MIT"
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
- "dev": true,
- "license": "BSD-2-Clause",
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
"engines": {
- "node": ">=0.12"
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
},
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
}
},
- "node_modules/es-module-lexer": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
- "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
- "dev": true,
- "license": "MIT"
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
},
"engines": {
"node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
}
},
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
"engines": {
- "node": ">=6"
+ "node": ">=12"
}
},
- "node_modules/escape-string-regexp": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
- "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
- "license": "MIT",
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
"engines": {
"node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/estree-util-is-identifier-name": {
+ "node_modules/d3-drag": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
- "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "dev": true,
- "license": "MIT"
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/expect-type": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
- "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
- "dev": true,
- "license": "Apache-2.0",
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
"engines": {
- "node": ">=12.0.0"
+ "node": ">= 10"
}
},
- "node_modules/extend": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "license": "MIT"
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
},
- "node_modules/fast-glob": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
- "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "license": "MIT",
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
"dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.8"
+ "d3-dsv": "1 - 3"
},
"engines": {
- "node": ">=8.6.0"
+ "node": ">=12"
}
},
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"license": "ISC",
"dependencies": {
- "is-glob": "^4.0.1"
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
},
"engines": {
- "node": ">= 6"
+ "node": ">=12"
}
},
- "node_modules/fastq": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
- "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "license": "MIT",
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
"dependencies": {
- "to-regex-range": "^5.0.1"
+ "d3-array": "2.5.0 - 3"
},
"engines": {
- "node": ">=8"
+ "node": ">=12"
}
},
- "node_modules/focus-trap": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz",
- "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tabbable": "^6.4.0"
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/fraction.js": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
- "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/rawify"
- }
- },
- "node_modules/framer-motion": {
- "version": "11.18.2",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
- "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
- "license": "MIT",
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
"dependencies": {
- "motion-dom": "^11.18.1",
- "motion-utils": "^11.18.1",
- "tslib": "^2.4.0"
- },
- "peerDependencies": {
- "@emotion/is-prop-valid": "*",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
+ "d3-color": "1 - 3"
},
- "peerDependenciesMeta": {
- "@emotion/is-prop-valid": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
"engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ "node": ">=12"
}
},
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/get-nonce": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
- "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
- "license": "MIT",
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
"engines": {
- "node": ">=6"
+ "node": ">=12"
}
},
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+ "license": "ISC"
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
- "is-glob": "^4.0.3"
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=12"
}
},
- "node_modules/hasown": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "license": "MIT",
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
"dependencies": {
- "function-bind": "^1.1.2"
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=12"
}
},
- "node_modules/hast-util-is-element": {
+ "node_modules/d3-selection": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
- "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
"dependencies": {
- "@types/hast": "^3.0.0"
+ "d3-path": "^3.1.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/hast-util-to-html": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
- "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
- "dev": true,
- "license": "MIT",
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
"dependencies": {
- "@types/hast": "^3.0.0",
- "@types/unist": "^3.0.0",
- "ccount": "^2.0.0",
- "comma-separated-tokens": "^2.0.0",
- "hast-util-whitespace": "^3.0.0",
- "html-void-elements": "^3.0.0",
- "mdast-util-to-hast": "^13.0.0",
- "property-information": "^7.0.0",
- "space-separated-tokens": "^2.0.0",
- "stringify-entities": "^4.0.0",
- "zwitch": "^2.0.4"
+ "d3-array": "2 - 3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/hast-util-to-jsx-runtime": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
- "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
- "license": "MIT",
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
"dependencies": {
- "@types/estree": "^1.0.0",
- "@types/hast": "^3.0.0",
- "@types/unist": "^3.0.0",
- "comma-separated-tokens": "^2.0.0",
- "devlop": "^1.0.0",
- "estree-util-is-identifier-name": "^3.0.0",
- "hast-util-whitespace": "^3.0.0",
- "mdast-util-mdx-expression": "^2.0.0",
- "mdast-util-mdx-jsx": "^3.0.0",
- "mdast-util-mdxjs-esm": "^2.0.0",
- "property-information": "^7.0.0",
- "space-separated-tokens": "^2.0.0",
- "style-to-js": "^1.0.0",
- "unist-util-position": "^5.0.0",
- "vfile-message": "^4.0.0"
+ "d3-time": "1 - 3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/hast-util-to-text": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
- "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
- "license": "MIT",
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
"dependencies": {
- "@types/hast": "^3.0.0",
- "@types/unist": "^3.0.0",
- "hast-util-is-element": "^3.0.0",
- "unist-util-find-after": "^5.0.0"
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
}
},
- "node_modules/hast-util-whitespace": {
+ "node_modules/d3-zoom": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
- "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
- "license": "MIT",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
"dependencies": {
- "@types/hast": "^3.0.0"
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
- },
- "node_modules/highlight.js": {
- "version": "11.11.1",
- "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
- "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
- "license": "BSD-3-Clause",
"engines": {
- "node": ">=12.0.0"
+ "node": ">=12"
}
},
- "node_modules/hookable": {
- "version": "5.5.3",
- "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
- "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
- "dev": true,
- "license": "MIT"
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
+ "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
+ "license": "MIT",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
},
- "node_modules/html-encoding-sniffer": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
- "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@exodus/bytes": "^1.6.0"
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "node_modules/html-url-attributes": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
- "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
- }
+ "node_modules/dayjs": {
+ "version": "1.11.21",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+ "license": "MIT"
},
- "node_modules/html-void-elements": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
- "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
- "dev": true,
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
- "node_modules/inline-style-parser": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
- "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/is-alphabetical": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
- "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/is-alphanumerical": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
- "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
"license": "MIT",
"dependencies": {
- "is-alphabetical": "^2.0.0",
- "is-decimal": "^2.0.0"
+ "character-entities": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "license": "MIT",
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
"dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
+ "robust-predicates": "^3.0.2"
}
},
- "node_modules/is-core-module": {
- "version": "2.16.2",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
- "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
- "dependencies": {
- "hasown": "^2.0.3"
- },
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-decimal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
- "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "node": ">=6"
}
},
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "license": "MIT",
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"license": "MIT",
"dependencies": {
- "is-extglob": "^2.1.1"
+ "dequal": "^2.0.0"
},
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-hexadecimal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
- "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
- "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
},
- "node_modules/is-plain-obj": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
- "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dompurify": {
+ "version": "3.4.11",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
+ "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
}
},
- "node_modules/is-potential-custom-element-name": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
- "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.364",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz",
+ "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex-xs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
+ "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
"dev": true,
"license": "MIT"
},
- "node_modules/is-what": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
- "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=18"
+ "node": ">=0.12"
},
"funding": {
- "url": "https://github.com/sponsors/mesqueeb"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/jiti": {
- "version": "1.21.7",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
- "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "jiti": "bin/jiti.js"
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/jsdom": {
- "version": "29.1.1",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
- "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "node_modules/es-toolkit": {
+ "version": "1.48.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.48.1.tgz",
+ "integrity": "sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
+ "hasInstallScript": true,
"license": "MIT",
- "dependencies": {
- "@asamuzakjp/css-color": "^5.1.11",
- "@asamuzakjp/dom-selector": "^7.1.1",
- "@bramus/specificity": "^2.4.2",
- "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
- "@exodus/bytes": "^1.15.0",
- "css-tree": "^3.2.1",
- "data-urls": "^7.0.0",
- "decimal.js": "^10.6.0",
- "html-encoding-sniffer": "^6.0.0",
- "is-potential-custom-element-name": "^1.0.1",
- "lru-cache": "^11.3.5",
- "parse5": "^8.0.1",
- "saxes": "^6.0.0",
- "symbol-tree": "^3.2.4",
- "tough-cookie": "^6.0.1",
- "undici": "^7.25.0",
- "w3c-xmlserializer": "^5.0.0",
- "webidl-conversions": "^8.0.1",
- "whatwg-mimetype": "^5.0.0",
- "whatwg-url": "^16.0.1",
- "xml-name-validator": "^5.0.0"
+ "bin": {
+ "esbuild": "bin/esbuild"
},
"engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
- },
- "peerDependencies": {
- "canvas": "^3.0.0"
+ "node": ">=12"
},
- "peerDependenciesMeta": {
- "canvas": {
- "optional": true
- }
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
}
},
- "node_modules/jsdom/node_modules/lru-cache": {
- "version": "11.5.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
- "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
- "license": "BlueOak-1.0.0",
+ "license": "MIT",
"engines": {
- "node": "20 || >=22"
+ "node": ">=6"
}
},
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
+ "engines": {
+ "node": ">=12"
},
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
"engines": {
- "node": ">=6"
+ "node": ">=12.0.0"
}
},
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
},
"engines": {
- "node": ">=6"
+ "node": ">=8.6.0"
}
},
- "node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
- "license": "MPL-2.0",
+ "license": "ISC",
"dependencies": {
- "detect-libc": "^2.0.3"
+ "is-glob": "^4.0.1"
},
"engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
+ "node": ">= 6"
}
},
- "node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/focus-trap": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz",
+ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
"dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.4.0"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">= 12.0.0"
+ "node": "*"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
}
},
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
- "cpu": [
- "x64"
- ],
+ "node_modules/framer-motion": {
+ "version": "11.18.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
+ "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^11.18.1",
+ "motion-utils": "^11.18.1",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
- "license": "MPL-2.0",
+ "hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
],
- "peer": true,
"engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
- "cpu": [
- "x64"
- ],
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
- },
+ "license": "MIT",
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
- "cpu": [
- "arm"
- ],
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
+ "license": "MIT",
"engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "node": ">=6.9.0"
}
},
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
"engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "node": ">=6"
}
},
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "engines": {
+ "node": ">=10.13.0"
}
},
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
+ "license": "MIT"
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
"engines": {
- "node": ">= 12.0.0"
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-from-parse5": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
+ "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "property-information": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-location": "^5.0.0",
+ "web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
+ "node_modules/hast-util-is-element": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">= 12.0.0"
+ "node_modules/hast-util-raw": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
+ "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "hast-util-to-parse5": "^8.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "parse5": "^7.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "license": "MIT",
+ "node_modules/hast-util-raw/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=14"
+ "node": ">=0.12"
},
"funding": {
- "url": "https://github.com/sponsors/antonk52"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/lines-and-columns": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "license": "MIT"
- },
- "node_modules/longest-streak": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
- "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "node_modules/hast-util-raw/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "node_modules/hast-util-sanitize": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
+ "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
"license": "MIT",
- "peer": true,
"dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "unist-util-position": "^5.0.0"
},
- "bin": {
- "loose-envify": "cli.js"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/lowlight": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
- "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "node_modules/hast-util-to-html": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
- "devlop": "^1.0.0",
- "highlight.js": "~11.11.0"
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "stringify-entities": "^4.0.0",
+ "zwitch": "^2.0.4"
},
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lucide-react": {
- "version": "0.468.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
- "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/mark.js": {
- "version": "8.11.1",
- "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
- "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/markdown-table": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
- "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
- "license": "MIT",
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-find-and-replace": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
- "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "node_modules/hast-util-to-parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
+ "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
- "escape-string-regexp": "^5.0.0",
- "unist-util-is": "^6.0.0",
- "unist-util-visit-parents": "^6.0.0"
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-from-markdown": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
- "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "node_modules/hast-util-to-text": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
+ "@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
- "decode-named-character-reference": "^1.0.0",
- "devlop": "^1.0.0",
- "mdast-util-to-string": "^4.0.0",
- "micromark": "^4.0.0",
- "micromark-util-decode-numeric-character-reference": "^2.0.0",
- "micromark-util-decode-string": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0",
- "unist-util-stringify-position": "^4.0.0"
+ "hast-util-is-element": "^3.0.0",
+ "unist-util-find-after": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-gfm": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
- "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
"dependencies": {
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-gfm-autolink-literal": "^2.0.0",
- "mdast-util-gfm-footnote": "^2.0.0",
- "mdast-util-gfm-strikethrough": "^2.0.0",
- "mdast-util-gfm-table": "^2.0.0",
- "mdast-util-gfm-task-list-item": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
+ "@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-gfm-autolink-literal": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
- "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "node_modules/hastscript": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
+ "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
- "ccount": "^2.0.0",
- "devlop": "^1.0.0",
- "mdast-util-find-and-replace": "^3.0.0",
- "micromark-util-character": "^2.0.0"
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-gfm-footnote": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
- "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
- "devlop": "^1.1.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0"
+ "@exodus/bytes": "^1.6.0"
},
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-gfm-strikethrough": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
- "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/mdast-util-gfm-table": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
- "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
- "devlop": "^1.0.0",
- "markdown-table": "^3.0.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "license": "MIT",
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/mdast-util-gfm-task-list-item": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
- "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "devlop": "^1.0.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/mdast-util-mdx-expression": {
+ "node_modules/is-alphanumerical": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
- "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"license": "MIT",
"dependencies": {
- "@types/estree-jsx": "^1.0.0",
- "@types/hast": "^3.0.0",
- "@types/mdast": "^4.0.0",
- "devlop": "^1.0.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/mdast-util-mdx-jsx": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
- "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
- "license": "MIT",
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@types/estree-jsx": "^1.0.0",
- "@types/hast": "^3.0.0",
- "@types/mdast": "^4.0.0",
- "@types/unist": "^3.0.0",
- "ccount": "^2.0.0",
- "devlop": "^1.1.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0",
- "parse-entities": "^4.0.0",
- "stringify-entities": "^4.0.0",
- "unist-util-stringify-position": "^4.0.0",
- "vfile-message": "^4.0.0"
+ "binary-extensions": "^2.0.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/mdast-util-mdxjs-esm": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
- "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree-jsx": "^1.0.0",
- "@types/hast": "^3.0.0",
- "@types/mdast": "^4.0.0",
- "devlop": "^1.0.0",
- "mdast-util-from-markdown": "^2.0.0",
- "mdast-util-to-markdown": "^2.0.0"
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/mdast-util-phrasing": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
- "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "unist-util-is": "^6.0.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/mdast-util-to-hast": {
- "version": "13.2.1",
- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
- "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/hast": "^3.0.0",
- "@types/mdast": "^4.0.0",
- "@ungap/structured-clone": "^1.0.0",
- "devlop": "^1.0.0",
- "micromark-util-sanitize-uri": "^2.0.0",
- "trim-lines": "^3.0.0",
- "unist-util-position": "^5.0.0",
- "unist-util-visit": "^5.0.0",
- "vfile": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/mdast-util-to-markdown": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
- "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
- "@types/unist": "^3.0.0",
- "longest-streak": "^3.0.0",
- "mdast-util-phrasing": "^4.0.0",
- "mdast-util-to-string": "^4.0.0",
- "micromark-util-classify-character": "^2.0.0",
- "micromark-util-decode-string": "^2.0.0",
- "unist-util-visit": "^5.0.0",
- "zwitch": "^2.0.0"
+ "is-extglob": "^2.1.1"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/mdast-util-to-string": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
- "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/mdn-data": {
- "version": "2.27.1",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
- "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
- "license": "CC0-1.0"
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"license": "MIT",
"engines": {
- "node": ">= 8"
+ "node": ">=0.12.0"
}
},
- "node_modules/micromark": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
- "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"license": "MIT",
- "dependencies": {
- "@types/debug": "^4.0.0",
- "debug": "^4.0.0",
- "decode-named-character-reference": "^1.0.0",
- "devlop": "^1.0.0",
- "micromark-core-commonmark": "^2.0.0",
- "micromark-factory-space": "^2.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-chunked": "^2.0.0",
- "micromark-util-combine-extensions": "^2.0.0",
- "micromark-util-decode-numeric-character-reference": "^2.0.0",
- "micromark-util-encode": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0",
- "micromark-util-resolve-all": "^2.0.0",
- "micromark-util-sanitize-uri": "^2.0.0",
- "micromark-util-subtokenize": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/micromark-core-commonmark": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
- "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "decode-named-character-reference": "^1.0.0",
- "devlop": "^1.0.0",
- "micromark-factory-destination": "^2.0.0",
- "micromark-factory-label": "^2.0.0",
- "micromark-factory-space": "^2.0.0",
- "micromark-factory-title": "^2.0.0",
- "micromark-factory-whitespace": "^2.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-chunked": "^2.0.0",
- "micromark-util-classify-character": "^2.0.0",
- "micromark-util-html-tag-name": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0",
- "micromark-util-resolve-all": "^2.0.0",
- "micromark-util-subtokenize": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
- }
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/micromark-extension-gfm": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
- "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "node_modules/is-what": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+ "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "micromark-extension-gfm-autolink-literal": "^2.0.0",
- "micromark-extension-gfm-footnote": "^2.0.0",
- "micromark-extension-gfm-strikethrough": "^2.0.0",
- "micromark-extension-gfm-table": "^2.0.0",
- "micromark-extension-gfm-tagfilter": "^2.0.0",
- "micromark-extension-gfm-task-list-item": "^2.0.0",
- "micromark-util-combine-extensions": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "engines": {
+ "node": ">=18"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/sponsors/mesqueeb"
}
},
- "node_modules/micromark-extension-gfm-autolink-literal": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
- "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "micromark-util-character": "^2.0.0",
- "micromark-util-sanitize-uri": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "bin": {
+ "jiti": "bin/jiti.js"
}
},
- "node_modules/micromark-extension-gfm-footnote": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
- "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "devlop": "^1.0.0",
- "micromark-core-commonmark": "^2.0.0",
- "micromark-factory-space": "^2.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-normalize-identifier": "^2.0.0",
- "micromark-util-sanitize-uri": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
}
},
- "node_modules/micromark-extension-gfm-strikethrough": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
- "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.5.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+ "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "devlop": "^1.0.0",
- "micromark-util-chunked": "^2.0.0",
- "micromark-util-classify-character": "^2.0.0",
- "micromark-util-resolve-all": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "bin": {
+ "jsesc": "bin/jsesc"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=6"
}
},
- "node_modules/micromark-extension-gfm-table": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
- "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "devlop": "^1.0.0",
- "micromark-factory-space": "^2.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "bin": {
+ "json5": "lib/cli.js"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "engines": {
+ "node": ">=6"
}
},
- "node_modules/micromark-extension-gfm-tagfilter": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
- "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "node_modules/katex": {
+ "version": "0.16.47",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
+ "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
"license": "MIT",
"dependencies": {
- "micromark-util-types": "^2.0.0"
+ "commander": "^8.3.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "bin": {
+ "katex": "cli.js"
}
},
- "node_modules/micromark-extension-gfm-task-list-item": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
- "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
+ "license": "MIT"
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
"dependencies": {
- "devlop": "^1.0.0",
- "micromark-factory-space": "^2.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
}
},
- "node_modules/micromark-factory-destination": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
- "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
],
- "license": "MIT",
- "dependencies": {
- "micromark-util-character": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/micromark-factory-label": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
- "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
],
- "license": "MIT",
- "dependencies": {
- "devlop": "^1.0.0",
- "micromark-util-character": "^2.0.0",
- "micromark-util-symbol": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
- "node_modules/micromark-factory-space": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
- "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.468.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
+ "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mark.js": {
+ "version": "8.11.1",
+ "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
+ "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/marked": {
+ "version": "17.0.6",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz",
+ "integrity": "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
+ "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
"license": "MIT",
"dependencies": {
- "micromark-util-character": "^2.0.0",
- "micromark-util-types": "^2.0.0"
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-factory-title": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
- "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mermaid": {
+ "version": "11.15.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz",
+ "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.1.1",
+ "@iconify/utils": "^3.0.2",
+ "@mermaid-js/parser": "^1.1.1",
+ "@types/d3": "^7.4.3",
+ "@upsetjs/venn.js": "^2.0.0",
+ "cytoscape": "^3.33.1",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "cytoscape-fcose": "^2.2.0",
+ "d3": "^7.9.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.14",
+ "dayjs": "^1.11.19",
+ "dompurify": "^3.3.1",
+ "es-toolkit": "^1.45.1",
+ "katex": "^0.16.25",
+ "khroma": "^2.1.0",
+ "marked": "^16.3.0",
+ "roughjs": "^4.6.6",
+ "stylis": "^4.3.6",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/marked": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6198,16 +7028,29 @@
],
"license": "MIT",
"dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-factory-whitespace": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
- "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6220,115 +7063,149 @@
],
"license": "MIT",
"dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
"micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
"micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-util-character": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
- "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
"license": "MIT",
"dependencies": {
- "micromark-util-symbol": "^2.0.0",
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
"micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-util-chunked": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
- "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
"license": "MIT",
"dependencies": {
- "micromark-util-symbol": "^2.0.0"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-util-classify-character": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
- "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
"license": "MIT",
"dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-util-combine-extensions": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
- "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
"license": "MIT",
"dependencies": {
+ "devlop": "^1.0.0",
"micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-util-decode-numeric-character-reference": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
- "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
"license": "MIT",
"dependencies": {
- "micromark-util-symbol": "^2.0.0"
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/micromark-util-decode-string": {
+ "node_modules/micromark-factory-destination": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
- "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6341,48 +7218,15 @@
],
"license": "MIT",
"dependencies": {
- "decode-named-character-reference": "^1.0.0",
"micromark-util-character": "^2.0.0",
- "micromark-util-decode-numeric-character-reference": "^2.0.0",
- "micromark-util-symbol": "^2.0.0"
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-util-encode": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
- "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
- "license": "MIT"
- },
- "node_modules/micromark-util-html-tag-name": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
- "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
- "license": "MIT"
- },
- "node_modules/micromark-util-normalize-identifier": {
+ "node_modules/micromark-factory-label": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
- "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6395,13 +7239,16 @@
],
"license": "MIT",
"dependencies": {
- "micromark-util-symbol": "^2.0.0"
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-util-resolve-all": {
+ "node_modules/micromark-factory-space": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
- "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6414,13 +7261,14 @@
],
"license": "MIT",
"dependencies": {
+ "micromark-util-character": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-util-sanitize-uri": {
+ "node_modules/micromark-factory-title": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
- "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6433,15 +7281,16 @@
],
"license": "MIT",
"dependencies": {
+ "micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
- "micromark-util-encode": "^2.0.0",
- "micromark-util-symbol": "^2.0.0"
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-util-subtokenize": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
- "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6454,32 +7303,16 @@
],
"license": "MIT",
"dependencies": {
- "devlop": "^1.0.0",
- "micromark-util-chunked": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
- "node_modules/micromark-util-symbol": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
- "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
- "funding": [
- {
- "type": "GitHub Sponsors",
- "url": "https://github.com/sponsors/unifiedjs"
- },
- {
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
- "license": "MIT"
- },
- "node_modules/micromark-util-types": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
- "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"funding": [
{
"type": "GitHub Sponsors",
@@ -6488,2205 +7321,2216 @@
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
- }
- ],
- "license": "MIT"
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minisearch": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz",
- "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/mitt": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
- "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/motion-dom": {
- "version": "11.18.1",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
- "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
- "license": "MIT",
- "dependencies": {
- "motion-utils": "^11.18.1"
- }
- },
- "node_modules/motion-utils": {
- "version": "11.18.1",
- "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
- "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/mz": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
- "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "license": "MIT",
- "dependencies": {
- "any-promise": "^1.0.0",
- "object-assign": "^4.0.1",
- "thenify-all": "^1.0.0"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.46",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
- "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-hash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
- "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/obug": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
- "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
- "node_modules/oniguruma-to-es": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz",
- "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==",
- "dev": true,
+ }
+ ],
"license": "MIT",
"dependencies": {
- "emoji-regex-xs": "^1.0.0",
- "regex": "^6.0.1",
- "regex-recursion": "^6.0.2"
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/parse-entities": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
- "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@types/unist": "^2.0.0",
- "character-entities-legacy": "^3.0.0",
- "character-reference-invalid": "^2.0.0",
- "decode-named-character-reference": "^1.0.0",
- "is-alphanumerical": "^2.0.0",
- "is-decimal": "^2.0.0",
- "is-hexadecimal": "^2.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/parse-entities/node_modules/@types/unist": {
- "version": "2.0.11",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
- "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
- "license": "MIT"
- },
- "node_modules/parse5": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
- "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
- "dev": true,
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "entities": "^8.0.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
- "node_modules/parse5/node_modules/entities": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
- "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "license": "MIT"
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/perfect-debounce": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
- "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/pify": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">=0.10.0"
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/pirates": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
- "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
"license": "MIT",
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/playwright": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
- "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
- "dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.60.0"
- },
- "bin": {
- "playwright": "cli.js"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "fsevents": "2.3.2"
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/playwright-core": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
- "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "playwright-core": "cli.js"
- },
- "engines": {
- "node": ">=18"
- }
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
},
- "node_modules/playwright/node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
+ "license": "MIT"
},
- "node_modules/postcss": {
- "version": "8.5.15",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
"funding": [
{
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "github",
- "url": "https://github.com/sponsors/ai"
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.12",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/postcss-import": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
- "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "license": "MIT",
- "dependencies": {
- "postcss-value-parser": "^4.0.0",
- "read-cache": "^1.0.0",
- "resolve": "^1.1.7"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "postcss": "^8.0.0"
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/postcss-js": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
- "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
"funding": [
{
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "github",
- "url": "https://github.com/sponsors/ai"
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
- "camelcase-css": "^2.0.1"
- },
- "engines": {
- "node": "^12 || ^14 || >= 16"
- },
- "peerDependencies": {
- "postcss": "^8.4.21"
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/postcss-load-config": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
- "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"funding": [
{
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "github",
- "url": "https://github.com/sponsors/ai"
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
- "lilconfig": "^3.1.1"
- },
- "engines": {
- "node": ">= 18"
- },
- "peerDependencies": {
- "jiti": ">=1.21.0",
- "postcss": ">=8.0.9",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- },
- "postcss": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
}
},
- "node_modules/postcss-nested": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
- "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
"funding": [
{
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "github",
- "url": "https://github.com/sponsors/ai"
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
- "postcss-selector-parser": "^6.1.1"
- },
- "engines": {
- "node": ">=12.0"
- },
- "peerDependencies": {
- "postcss": "^8.2.14"
- }
- },
- "node_modules/postcss-selector-parser": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postcss-value-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "license": "MIT"
- },
- "node_modules/preact": {
- "version": "10.29.1",
- "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
- "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/preact"
- }
- },
- "node_modules/property-information": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
- "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
}
},
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"funding": [
{
- "type": "github",
- "url": "https://github.com/sponsors/feross"
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "consulting",
- "url": "https://feross.org/support"
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
- "node_modules/react": {
- "version": "19.2.7",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
- "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
- "node_modules/react-markdown": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
- "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/hast": "^3.0.0",
- "@types/mdast": "^4.0.0",
- "devlop": "^1.0.0",
- "hast-util-to-jsx-runtime": "^2.0.0",
- "html-url-attributes": "^3.0.0",
- "mdast-util-to-hast": "^13.0.0",
- "remark-parse": "^11.0.0",
- "remark-rehype": "^11.0.0",
- "unified": "^11.0.0",
- "unist-util-visit": "^5.0.0",
- "vfile": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
},
- "peerDependencies": {
- "@types/react": ">=18",
- "react": ">=18"
+ "engines": {
+ "node": ">=8.6"
}
},
- "node_modules/react-refresh": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
- "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "node_modules/minisearch": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz",
+ "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true,
+ "license": "MIT"
+ },
+ "node_modules/motion-dom": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
+ "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
"license": "MIT",
- "engines": {
- "node": ">=0.10.0"
+ "dependencies": {
+ "motion-utils": "^11.18.1"
}
},
- "node_modules/react-remove-scroll": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
- "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "node_modules/motion-utils": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
+ "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "react-remove-scroll-bar": "^2.3.7",
- "react-style-singleton": "^2.2.3",
- "tslib": "^2.1.0",
- "use-callback-ref": "^1.3.3",
- "use-sidecar": "^1.1.3"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
}
},
- "node_modules/react-remove-scroll-bar": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
- "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
- "dependencies": {
- "react-style-singleton": "^2.2.2",
- "tslib": "^2.0.0"
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
},
"engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/react-style-singleton": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
- "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "node_modules/node-releases": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
+ "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "get-nonce": "^1.0.0",
- "tslib": "^2.0.0"
- },
"engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "node": ">=18"
}
},
- "node_modules/read-cache": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
- "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "pify": "^2.3.0"
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
"engines": {
- "node": ">=8.10.0"
+ "node": ">=0.10.0"
}
},
- "node_modules/regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
- "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "regex-utilities": "^2.3.0"
+ "engines": {
+ "node": ">= 6"
}
},
- "node_modules/regex-recursion": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
- "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
+ "node_modules/obug": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
+ "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
"dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
"license": "MIT",
- "dependencies": {
- "regex-utilities": "^2.3.0"
+ "engines": {
+ "node": ">=12.20.0"
}
},
- "node_modules/regex-utilities": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
- "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
+ "node_modules/oniguruma-to-es": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz",
+ "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/rehype-highlight": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
- "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
"license": "MIT",
"dependencies": {
- "@types/hast": "^3.0.0",
- "hast-util-to-text": "^4.0.0",
- "lowlight": "^3.0.0",
- "unist-util-visit": "^5.0.0",
- "vfile": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "emoji-regex-xs": "^1.0.0",
+ "regex": "^6.0.1",
+ "regex-recursion": "^6.0.2"
}
},
- "node_modules/remark-gfm": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
- "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
+ "node_modules/package-manager-detector": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
+ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
+ "license": "MIT"
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
"license": "MIT",
"dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-gfm": "^3.0.0",
- "micromark-extension-gfm": "^3.0.0",
- "remark-parse": "^11.0.0",
- "remark-stringify": "^11.0.0",
- "unified": "^11.0.0"
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/remark-parse": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
- "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-from-markdown": "^2.0.0",
- "micromark-util-types": "^2.0.0",
- "unified": "^11.0.0"
+ "dependencies": {
+ "entities": "^8.0.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/remark-rehype": {
- "version": "11.1.2",
- "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
- "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
- "license": "MIT",
- "dependencies": {
- "@types/hast": "^3.0.0",
- "@types/mdast": "^4.0.0",
- "mdast-util-to-hast": "^13.0.0",
- "unified": "^11.0.0",
- "vfile": "^6.0.0"
+ "node_modules/parse5/node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/remark-stringify": {
- "version": "11.0.0",
- "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
- "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
+ "license": "MIT"
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/mdast": "^4.0.0",
- "mdast-util-to-markdown": "^2.0.0",
- "unified": "^11.0.0"
+ "engines": {
+ "node": ">=8.6"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/resolve": {
- "version": "1.22.12",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
- "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "es-errors": "^1.3.0",
- "is-core-module": "^2.16.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
+ "playwright-core": "1.60.0"
},
"bin": {
- "resolve": "bin/resolve"
+ "playwright": "cli.js"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=18"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
}
},
- "node_modules/reusify": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
- "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "license": "MIT",
+ "node_modules/playwright-core": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
"engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
+ "node": ">=18"
}
},
- "node_modules/rfdc": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
- "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
"license": "MIT"
},
- "node_modules/rolldown": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
- "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.133.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
},
"engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.3",
- "@rolldown/binding-darwin-arm64": "1.0.3",
- "@rolldown/binding-darwin-x64": "1.0.3",
- "@rolldown/binding-freebsd-x64": "1.0.3",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
- "@rolldown/binding-linux-arm64-gnu": "1.0.3",
- "@rolldown/binding-linux-arm64-musl": "1.0.3",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
- "@rolldown/binding-linux-s390x-gnu": "1.0.3",
- "@rolldown/binding-linux-x64-gnu": "1.0.3",
- "@rolldown/binding-linux-x64-musl": "1.0.3",
- "@rolldown/binding-openharmony-arm64": "1.0.3",
- "@rolldown/binding-wasm32-wasi": "1.0.3",
- "@rolldown/binding-win32-arm64-msvc": "1.0.3",
- "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ "node": "^10 || ^12 || >=14"
}
},
- "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
- "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/rollup": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
- "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.8"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
},
"engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
+ "node": ">=14.0.0"
},
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.1",
- "@rollup/rollup-android-arm64": "4.60.1",
- "@rollup/rollup-darwin-arm64": "4.60.1",
- "@rollup/rollup-darwin-x64": "4.60.1",
- "@rollup/rollup-freebsd-arm64": "4.60.1",
- "@rollup/rollup-freebsd-x64": "4.60.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
- "@rollup/rollup-linux-arm64-gnu": "4.60.1",
- "@rollup/rollup-linux-arm64-musl": "4.60.1",
- "@rollup/rollup-linux-loong64-gnu": "4.60.1",
- "@rollup/rollup-linux-loong64-musl": "4.60.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
- "@rollup/rollup-linux-ppc64-musl": "4.60.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
- "@rollup/rollup-linux-riscv64-musl": "4.60.1",
- "@rollup/rollup-linux-s390x-gnu": "4.60.1",
- "@rollup/rollup-linux-x64-gnu": "4.60.1",
- "@rollup/rollup-linux-x64-musl": "4.60.1",
- "@rollup/rollup-openbsd-x64": "4.60.1",
- "@rollup/rollup-openharmony-arm64": "4.60.1",
- "@rollup/rollup-win32-arm64-msvc": "4.60.1",
- "@rollup/rollup-win32-ia32-msvc": "4.60.1",
- "@rollup/rollup-win32-x64-gnu": "4.60.1",
- "@rollup/rollup-win32-x64-msvc": "4.60.1",
- "fsevents": "~2.3.2"
+ "peerDependencies": {
+ "postcss": "^8.0.0"
}
},
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
"funding": [
{
- "type": "github",
- "url": "https://github.com/sponsors/feross"
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
},
{
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
},
{
- "type": "consulting",
- "url": "https://feross.org/support"
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
- "queue-microtask": "^1.2.2"
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
}
},
- "node_modules/saxes": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
- "license": "ISC",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
"dependencies": {
- "xmlchars": "^2.2.0"
+ "postcss-selector-parser": "^6.1.1"
},
"engines": {
- "node": ">=v12.22.7"
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
}
},
- "node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
- "loose-envify": "^1.1.0"
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
}
},
- "node_modules/search-insights": {
- "version": "2.17.3",
- "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
- "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==",
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "node_modules/preact": {
+ "version": "10.29.1",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
+ "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
}
},
- "node_modules/shiki": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz",
- "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==",
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@shikijs/core": "2.5.0",
- "@shikijs/engine-javascript": "2.5.0",
- "@shikijs/engine-oniguruma": "2.5.0",
- "@shikijs/langs": "2.5.0",
- "@shikijs/themes": "2.5.0",
- "@shikijs/types": "2.5.0",
- "@shikijs/vscode-textmate": "^10.0.2",
- "@types/hast": "^3.0.4"
+ "engines": {
+ "node": ">=6"
}
},
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
- "license": "ISC"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
},
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "license": "BSD-3-Clause",
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/space-separated-tokens": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
- "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "node_modules/react-markdown": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
+ "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
"license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
}
},
- "node_modules/speakingurl": {
- "version": "14.0.1",
- "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
- "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
- "license": "BSD-3-Clause",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
- "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
- "dev": true,
- "license": "MIT"
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
},
- "node_modules/stringify-entities": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
- "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
- "character-entities-html4": "^2.0.0",
- "character-entities-legacy": "^3.0.0"
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
},
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/style-to-js": {
- "version": "1.1.21",
- "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
- "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
- "style-to-object": "1.0.14"
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/style-to-object": {
- "version": "1.0.14",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
- "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "inline-style-parser": "0.2.7"
+ "pify": "^2.3.0"
}
},
- "node_modules/sucrase": {
- "version": "3.35.1",
- "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
- "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/gen-mapping": "^0.3.2",
- "commander": "^4.0.0",
- "lines-and-columns": "^1.1.6",
- "mz": "^2.7.0",
- "pirates": "^4.0.1",
- "tinyglobby": "^0.2.11",
- "ts-interface-checker": "^0.1.9"
- },
- "bin": {
- "sucrase": "bin/sucrase",
- "sucrase-node": "bin/sucrase-node"
+ "picomatch": "^2.2.1"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": ">=8.10.0"
}
},
- "node_modules/superjson": {
- "version": "2.2.6",
- "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
- "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+ "node_modules/regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "copy-anything": "^4"
- },
- "engines": {
- "node": ">=16"
+ "regex-utilities": "^2.3.0"
}
},
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "node_modules/regex-recursion": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
}
},
- "node_modules/symbol-tree": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tabbable": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
- "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
+ "node_modules/regex-utilities": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"dev": true,
"license": "MIT"
},
- "node_modules/tailwind-merge": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
- "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "node_modules/rehype-harden": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/rehype-harden/-/rehype-harden-1.1.8.tgz",
+ "integrity": "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==",
"license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/dcastil"
+ "dependencies": {
+ "unist-util-visit": "^5.0.0"
}
},
- "node_modules/tailwindcss": {
- "version": "3.4.19",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
- "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "node_modules/rehype-highlight": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
+ "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
"license": "MIT",
"dependencies": {
- "@alloc/quick-lru": "^5.2.0",
- "arg": "^5.0.2",
- "chokidar": "^3.6.0",
- "didyoumean": "^1.2.2",
- "dlv": "^1.1.3",
- "fast-glob": "^3.3.2",
- "glob-parent": "^6.0.2",
- "is-glob": "^4.0.3",
- "jiti": "^1.21.7",
- "lilconfig": "^3.1.3",
- "micromatch": "^4.0.8",
- "normalize-path": "^3.0.0",
- "object-hash": "^3.0.0",
- "picocolors": "^1.1.1",
- "postcss": "^8.4.47",
- "postcss-import": "^15.1.0",
- "postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
- "postcss-nested": "^6.2.0",
- "postcss-selector-parser": "^6.1.2",
- "resolve": "^1.22.8",
- "sucrase": "^3.35.0"
- },
- "bin": {
- "tailwind": "lib/cli.js",
- "tailwindcss": "lib/cli.js"
+ "@types/hast": "^3.0.0",
+ "hast-util-to-text": "^4.0.0",
+ "lowlight": "^3.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
},
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tailwindcss-animate": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
- "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
- "license": "MIT",
- "peerDependencies": {
- "tailwindcss": ">=3.0.0 || insiders"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/thenify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
- "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "node_modules/rehype-raw": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
+ "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
"license": "MIT",
"dependencies": {
- "any-promise": "^1.0.0"
+ "@types/hast": "^3.0.0",
+ "hast-util-raw": "^9.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/thenify-all": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
- "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "node_modules/rehype-sanitize": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
+ "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==",
"license": "MIT",
"dependencies": {
- "thenify": ">= 3.1.0 < 4"
+ "@types/hast": "^3.0.0",
+ "hast-util-sanitize": "^5.0.0"
},
- "engines": {
- "node": ">=0.8"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
- "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
- "dev": true,
+ "node_modules/remark-gfm": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
"license": "MIT",
"dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
"license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
},
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
"license": "MIT",
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/tinyrainbow": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
- "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "node_modules/remend": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/remend/-/remend-1.3.0.tgz",
+ "integrity": "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=14.0.0"
+ "node": ">=0.10.0"
}
},
- "node_modules/tldts": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz",
- "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==",
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tldts-core": "^7.4.2"
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
- "tldts": "bin/cli.js"
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/tldts-core": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz",
- "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==",
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
"engines": {
- "node": ">=8.0"
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
}
},
- "node_modules/tough-cookie": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
- "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
- "license": "BSD-3-Clause",
+ "license": "MIT"
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "tldts": "^7.0.5"
+ "@oxc-project/types": "=0.133.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
},
"engines": {
- "node": ">=16"
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
- "node_modules/tr46": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
- "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "punycode": "^2.3.1"
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
},
"engines": {
- "node": ">=20"
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
}
},
- "node_modules/trim-lines": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
- "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
"license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
}
},
- "node_modules/trough": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
- "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
"license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
}
},
- "node_modules/ts-interface-checker": {
- "version": "0.1.13",
- "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "license": "Apache-2.0"
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
},
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=v12.22.7"
}
},
- "node_modules/undici": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
- "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20.18.1"
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
}
},
- "node_modules/unified": {
- "version": "11.0.5",
- "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
- "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "node_modules/shiki": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz",
+ "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/unist": "^3.0.0",
- "bail": "^2.0.0",
- "devlop": "^1.0.0",
- "extend": "^3.0.0",
- "is-plain-obj": "^4.0.0",
- "trough": "^2.0.0",
- "vfile": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "@shikijs/core": "2.5.0",
+ "@shikijs/engine-javascript": "2.5.0",
+ "@shikijs/engine-oniguruma": "2.5.0",
+ "@shikijs/langs": "2.5.0",
+ "@shikijs/themes": "2.5.0",
+ "@shikijs/types": "2.5.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4"
}
},
- "node_modules/unist-util-find-after": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
- "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
- "license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-is": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/unist-util-is": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
- "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/unist-util-position": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
- "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
- "license": "MIT",
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/streamdown": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/streamdown/-/streamdown-2.5.0.tgz",
+ "integrity": "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==",
+ "license": "Apache-2.0",
"dependencies": {
- "@types/unist": "^3.0.0"
+ "clsx": "^2.1.1",
+ "hast-util-to-jsx-runtime": "^2.3.6",
+ "html-url-attributes": "^3.0.1",
+ "marked": "^17.0.1",
+ "mermaid": "^11.12.2",
+ "rehype-harden": "^1.1.8",
+ "rehype-raw": "^7.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "remark-gfm": "^4.0.1",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.2",
+ "remend": "1.3.0",
+ "tailwind-merge": "^3.4.0",
+ "unified": "^11.0.5",
+ "unist-util-visit": "^5.0.0",
+ "unist-util-visit-parents": "^6.0.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
}
},
- "node_modules/unist-util-stringify-position": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
- "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
- "@types/unist": "^3.0.0"
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/unist-util-visit": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
- "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
"license": "MIT",
"dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-is": "^6.0.0",
- "unist-util-visit-parents": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "style-to-object": "1.0.14"
}
},
- "node_modules/unist-util-visit-parents": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
- "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
"license": "MIT",
"dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-is": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "inline-style-parser": "0.2.7"
}
},
- "node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "node_modules/stylis": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+ "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+ "license": "MIT"
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
"license": "MIT",
"dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
},
"bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/use-callback-ref": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
- "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
},
"engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "node": ">=16 || 14 >=14.17"
}
- },
- "node_modules/use-sidecar": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
- "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ },
+ "node_modules/superjson": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+ "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "detect-node-es": "^1.1.0",
- "tslib": "^2.0.0"
+ "copy-anything": "^4"
},
"engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "node": ">=16"
}
},
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT"
- },
- "node_modules/vfile": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
- "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0",
- "vfile-message": "^4.0.0"
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/vfile-message": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
- "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tabbable": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
+ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
"license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-stringify-position": "^4.0.0"
- },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
}
},
- "node_modules/vite": {
- "version": "5.4.21",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
- "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
},
"bin": {
- "vite": "bin/vite.js"
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
"peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
+ "tailwindcss": ">=3.0.0 || insiders"
}
},
- "node_modules/vitepress": {
- "version": "1.6.4",
- "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
- "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@docsearch/css": "3.8.2",
- "@docsearch/js": "3.8.2",
- "@iconify-json/simple-icons": "^1.2.21",
- "@shikijs/core": "^2.1.0",
- "@shikijs/transformers": "^2.1.0",
- "@shikijs/types": "^2.1.0",
- "@types/markdown-it": "^14.1.2",
- "@vitejs/plugin-vue": "^5.2.1",
- "@vue/devtools-api": "^7.7.0",
- "@vue/shared": "^3.5.13",
- "@vueuse/core": "^12.4.0",
- "@vueuse/integrations": "^12.4.0",
- "focus-trap": "^7.6.4",
- "mark.js": "8.11.1",
- "minisearch": "^7.1.1",
- "shiki": "^2.1.0",
- "vite": "^5.4.14",
- "vue": "^3.5.13"
- },
- "bin": {
- "vitepress": "bin/vitepress.js"
- },
- "peerDependencies": {
- "markdown-it-mathjax3": "^4",
- "postcss": "^8"
- },
- "peerDependenciesMeta": {
- "markdown-it-mathjax3": {
- "optional": true
- },
- "postcss": {
- "optional": true
- }
+ "any-promise": "^1.0.0"
}
},
- "node_modules/vitest": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
- "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.8",
- "@vitest/mocker": "4.1.8",
- "@vitest/pretty-format": "4.1.8",
- "@vitest/runner": "4.1.8",
- "@vitest/snapshot": "4.1.8",
- "@vitest/spy": "4.1.8",
- "@vitest/utils": "4.1.8",
- "es-module-lexer": "^2.0.0",
- "expect-type": "^1.3.0",
- "magic-string": "^0.30.21",
- "obug": "^2.1.1",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.3",
- "std-env": "^4.0.0-rc.1",
- "tinybench": "^2.9.0",
- "tinyexec": "^1.0.2",
- "tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.1.0",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
- "why-is-node-running": "^2.3.0"
+ "thenify": ">= 3.1.0 < 4"
},
- "bin": {
- "vitest": "vitest.mjs"
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
},
"engines": {
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ "node": ">=12.0.0"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
},
"peerDependencies": {
- "@edge-runtime/vm": "*",
- "@opentelemetry/api": "^1.9.0",
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.8",
- "@vitest/browser-preview": "4.1.8",
- "@vitest/browser-webdriverio": "4.1.8",
- "@vitest/coverage-istanbul": "4.1.8",
- "@vitest/coverage-v8": "4.1.8",
- "@vitest/ui": "4.1.8",
- "happy-dom": "*",
- "jsdom": "*",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser-playwright": {
- "optional": true
- },
- "@vitest/browser-preview": {
- "optional": true
- },
- "@vitest/browser-webdriverio": {
- "optional": true
- },
- "@vitest/coverage-istanbul": {
- "optional": true
- },
- "@vitest/coverage-v8": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
+ "picomatch": {
"optional": true
- },
- "vite": {
- "optional": false
}
}
},
- "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
- "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
- "cpu": [
- "ppc64"
- ],
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "peer": true,
"engines": {
- "node": ">=18"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/vitest/node_modules/@esbuild/android-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
- "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
- "cpu": [
- "arm"
- ],
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
"engines": {
- "node": ">=18"
+ "node": ">=14.0.0"
}
},
- "node_modules/vitest/node_modules/@esbuild/android-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
- "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/tldts": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz",
+ "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tldts-core": "^7.4.2"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
}
},
- "node_modules/vitest/node_modules/@esbuild/android-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
- "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/tldts-core": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz",
+ "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=8.0"
}
},
- "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
- "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=16"
}
},
- "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
- "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
- "cpu": [
- "x64"
- ],
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
},
- "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
- "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-dedent": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
+ "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==",
"license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
"engines": {
- "node": ">=18"
+ "node": ">=6.10"
}
},
- "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
- "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
- "cpu": [
- "x64"
- ],
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
+ "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
"engines": {
- "node": ">=18"
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-arm": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
- "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
+ "node_modules/unist-util-find-after": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
- "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
- "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
- "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
- "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
- "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
- "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
- "cpu": [
- "riscv64"
- ],
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
- "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/vitest/node_modules/@esbuild/linux-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
- "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
}
},
- "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
- "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
- "cpu": [
- "x64"
- ],
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
+ "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
}
},
- "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
- "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
- "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
+ "node_modules/vfile-location": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
+ "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
}
},
- "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
- "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
"engines": {
- "node": ">=18"
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
}
},
- "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
- "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/vitepress": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
+ "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@docsearch/css": "3.8.2",
+ "@docsearch/js": "3.8.2",
+ "@iconify-json/simple-icons": "^1.2.21",
+ "@shikijs/core": "^2.1.0",
+ "@shikijs/transformers": "^2.1.0",
+ "@shikijs/types": "^2.1.0",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/devtools-api": "^7.7.0",
+ "@vue/shared": "^3.5.13",
+ "@vueuse/core": "^12.4.0",
+ "@vueuse/integrations": "^12.4.0",
+ "focus-trap": "^7.6.4",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.1.1",
+ "shiki": "^2.1.0",
+ "vite": "^5.4.14",
+ "vue": "^3.5.13"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
}
},
- "node_modules/vitest/node_modules/@esbuild/win32-x64": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
- "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
- "cpu": [
- "x64"
- ],
+ "node_modules/vitest": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
+ "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
+ "dependencies": {
+ "@vitest/expect": "4.1.8",
+ "@vitest/mocker": "4.1.8",
+ "@vitest/pretty-format": "4.1.8",
+ "@vitest/runner": "4.1.8",
+ "@vitest/snapshot": "4.1.8",
+ "@vitest/spy": "4.1.8",
+ "@vitest/utils": "4.1.8",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
"engines": {
- "node": ">=18"
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.8",
+ "@vitest/browser-preview": "4.1.8",
+ "@vitest/browser-webdriverio": "4.1.8",
+ "@vitest/coverage-istanbul": "4.1.8",
+ "@vitest/coverage-v8": "4.1.8",
+ "@vitest/ui": "4.1.8",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
}
},
"node_modules/vitest/node_modules/@vitest/mocker": {
@@ -8716,50 +9560,6 @@
}
}
},
- "node_modules/vitest/node_modules/esbuild": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
- "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.0",
- "@esbuild/android-arm": "0.28.0",
- "@esbuild/android-arm64": "0.28.0",
- "@esbuild/android-x64": "0.28.0",
- "@esbuild/darwin-arm64": "0.28.0",
- "@esbuild/darwin-x64": "0.28.0",
- "@esbuild/freebsd-arm64": "0.28.0",
- "@esbuild/freebsd-x64": "0.28.0",
- "@esbuild/linux-arm": "0.28.0",
- "@esbuild/linux-arm64": "0.28.0",
- "@esbuild/linux-ia32": "0.28.0",
- "@esbuild/linux-loong64": "0.28.0",
- "@esbuild/linux-mips64el": "0.28.0",
- "@esbuild/linux-ppc64": "0.28.0",
- "@esbuild/linux-riscv64": "0.28.0",
- "@esbuild/linux-s390x": "0.28.0",
- "@esbuild/linux-x64": "0.28.0",
- "@esbuild/netbsd-arm64": "0.28.0",
- "@esbuild/netbsd-x64": "0.28.0",
- "@esbuild/openbsd-arm64": "0.28.0",
- "@esbuild/openbsd-x64": "0.28.0",
- "@esbuild/openharmony-arm64": "0.28.0",
- "@esbuild/sunos-x64": "0.28.0",
- "@esbuild/win32-arm64": "0.28.0",
- "@esbuild/win32-ia32": "0.28.0",
- "@esbuild/win32-x64": "0.28.0"
- }
- },
"node_modules/vitest/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -8896,6 +9696,16 @@
"node": ">=18"
}
},
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
diff --git a/package.json b/package.json
index 326d6e4c..24c64b46 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"devDependencies": {
"@protolabsai/design": "^0.3.0",
"@protolabsai/vitepress-theme": "^0.3.6",
+ "@types/react": "18.3.29",
"vitepress": "^1.6.4"
}
}
diff --git a/server/chat.py b/server/chat.py
index 3d56baa3..8b1fd6c3 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -21,8 +21,6 @@
from graph.output_format import (
DROPPED_SCRATCH_KICKER,
- StreamingOutputView,
- StreamingReasoningView,
extract_confidence,
extract_output,
is_dropped_scratch_turn,
@@ -305,14 +303,7 @@ async def _run_turn_stream(
from observability import metrics
from observability import pricing
- accumulated_raw = ""
- streamed_len = 0 # chars of visible already emitted as text frames
- reasoned_len = 0 # chars of scratch_pad reasoning already emitted (live thinking)
- # Incremental views: same visible-so-far as stream_visible_output/_reasoning, but
- # they scan only the new tail instead of re-running regexes over the whole
- # accumulated text every chunk — turning the per-turn O(N²) rescan into ~O(N) (#1310).
- out_view = StreamingOutputView()
- reason_view = StreamingReasoningView()
+ accumulated_raw = "" # the answer text so far (the model's content; no protocol tags)
_llm_started: dict[str, float] = {} # run_id → monotonic start (per-call latency)
announced_tools: set[str] = set() # tool_call ids already surfaced as a start frame
async for event in STATE.graph.astream_events(
@@ -386,21 +377,20 @@ async def _run_turn_stream(
"tool_start",
{"id": tcid, "name": tcname, "input": "", **({"parentId": parent_tool_id} if parent_tool_id else {})},
)
+ # Native reasoning: the model's REAL thinking, streamed on its own channel.
+ # `_ReasoningChatOpenAI` lifts the gateway's `reasoning_content` into
+ # additional_kwargs; reasoning chunks carry NO `content`, so this is checked
+ # independently of the answer below. Rendered live as a collapsible "thinking"
+ # view — it never enters the answer text (so it can't leak to storage either).
+ native_reasoning = (getattr(chunk, "additional_kwargs", None) or {}).get("reasoning_content")
+ if native_reasoning:
+ yield ("reasoning", native_reasoning if isinstance(native_reasoning, str) else str(native_reasoning))
+ # The answer is the model's content, streamed directly — no /
+ # protocol. (extract_output at the terminal still strips any stray legacy tag.)
if hasattr(chunk, "content") and chunk.content:
- accumulated_raw += chunk.content if isinstance(chunk.content, str) else str(chunk.content)
- # Stream only the user-facing region, token by token —
- # never the scratch_pad. The terminal artifact (extract_output)
- # reconciles any partial tail held back here.
- visible = out_view.update(accumulated_raw)
- if len(visible) > streamed_len:
- yield ("text", visible[streamed_len:])
- streamed_len = len(visible)
- # Stream the scratch_pad reasoning on its own channel — a collapsible
- # "thinking" view in the console (never folded into the answer text).
- reasoning = reason_view.update(accumulated_raw)
- if len(reasoning) > reasoned_len:
- yield ("reasoning", reasoning[reasoned_len:])
- reasoned_len = len(reasoning)
+ text = chunk.content if isinstance(chunk.content, str) else str(chunk.content)
+ accumulated_raw += text
+ yield ("text", text)
elif kind == "on_chat_model_end":
output = event.get("data", {}).get("output")
# Finalize each tool card with its full args, keyed by the tool_call id.
diff --git a/tests/test_reasoning_order.py b/tests/test_reasoning_order.py
new file mode 100644
index 00000000..ad7536e6
--- /dev/null
+++ b/tests/test_reasoning_order.py
@@ -0,0 +1,67 @@
+"""Native reasoning streams on its own channel, BEFORE the answer. The gateway emits
+``reasoning_content`` deltas (lifted into additional_kwargs by
+``graph.llm._ReasoningChatOpenAI``), and ``server.chat`` streams them as ``reasoning``
+frames ahead of the content ``text`` frames — so the console (which renders parts in
+emission order) shows the model's real thinking above the answer. No ```` /
+```` text protocol is involved anymore.
+"""
+
+from __future__ import annotations
+
+import itertools
+
+import pytest
+from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
+from langchain_core.messages import AIMessage, AIMessageChunk
+from langchain_core.outputs import ChatGenerationChunk
+
+
+class _ReasoningFake(GenericFakeChatModel):
+ """Streams a native-reasoning chunk (``reasoning_content``, no content) followed by the
+ answer chunk — the shape ``_ReasoningChatOpenAI`` surfaces from a reasoning gateway."""
+
+ def bind_tools(self, tools, **kwargs):
+ return self
+
+ async def _astream(self, messages, stop=None, run_manager=None, **kwargs):
+ msg = next(self.messages)
+ yield ChatGenerationChunk(
+ message=AIMessageChunk(content="", additional_kwargs={"reasoning_content": "Group the tools by domain."})
+ )
+ yield ChatGenerationChunk(message=AIMessageChunk(content=msg.content))
+
+
+@pytest.mark.asyncio
+async def test_native_reasoning_streams_before_the_answer(monkeypatch):
+ import runtime.state as rs
+ from graph.config import LangGraphConfig
+ from langgraph.checkpoint.memory import MemorySaver
+ from server.chat import _run_turn_stream
+
+ stream = itertools.chain(
+ iter([AIMessage(content="Here are the tools.")]),
+ itertools.repeat(AIMessage(content="done")),
+ )
+ fake = _ReasoningFake(messages=stream)
+ monkeypatch.setattr("graph.agent.create_llm", lambda *a, **k: fake)
+ from graph.agent import create_agent_graph
+
+ g = create_agent_graph(LangGraphConfig(), include_subagents=False, checkpointer=MemorySaver())
+ monkeypatch.setattr(rs.STATE, "graph", g, raising=False)
+ monkeypatch.setattr(rs.STATE, "goal_controller", None, raising=False)
+ monkeypatch.setattr(rs.STATE, "graph_config", LangGraphConfig(), raising=False)
+
+ frames: list[tuple[str, str]] = []
+ async for kind, payload in _run_turn_stream("what tools do you have", "r1", {"configurable": {"thread_id": "r1"}}):
+ if kind in ("reasoning", "text"):
+ frames.append((kind, payload if isinstance(payload, str) else str(payload)))
+
+ kinds = [k for k, _ in frames]
+ assert "reasoning" in kinds, f"native reasoning_content must surface as a reasoning frame; saw {frames}"
+ assert "text" in kinds, f"the answer must surface as a text frame; saw {frames}"
+ # Reasoning is emitted before the answer → renders above it.
+ assert kinds.index("reasoning") < kinds.index("text"), f"reasoning must precede the answer; saw {kinds}"
+ # The reasoning frame carries the model's NATIVE reasoning (not a parsed scratch_pad).
+ assert any(k == "reasoning" and "Group the tools" in v for k, v in frames), f"reasoning text missing; saw {frames}"
+ # The answer is the model's content, streamed directly (no wrapper).
+ assert any(k == "text" and "Here are the tools" in v for k, v in frames), f"answer text missing; saw {frames}"
diff --git a/tools/lg_tools.py b/tools/lg_tools.py
index 641977b0..bcd0cf8c 100644
--- a/tools/lg_tools.py
+++ b/tools/lg_tools.py
@@ -253,9 +253,16 @@ async def web_search(query: str, max_results: int = 5) -> str:
except ImportError:
return "Error: the 'ddgs' package is not installed. Add `ddgs>=9.0` to requirements.txt and rebuild the image."
- try:
+ # ddgs.text() is a SYNCHRONOUS network call — running it inline would block the asyncio
+ # event loop for the whole search. Under a fan-out (e.g. task_batch's parallel
+ # researchers) those blocking searches serialise on the loop and starve everything else,
+ # incl. the cancel/tasks API. Offload to a worker thread so the loop stays responsive.
+ def _run_search() -> list:
with DDGS() as ddgs:
- results = list(ddgs.text(query, max_results=max_results))
+ return list(ddgs.text(query, max_results=max_results))
+
+ try:
+ results = await asyncio.to_thread(_run_search)
except Exception as e:
return f"Error: DuckDuckGo search failed: {e}"
@@ -343,7 +350,10 @@ async def fetch_url(url: str, max_chars: int = _MAX_OUTPUT_CHARS) -> str:
ctype = (resp.headers.get("content-type") or "").lower()
if "html" in ctype or content.lstrip().startswith(b"<"):
- text = _extract_text_from_html(content)
+ # BeautifulSoup parsing of up to _MAX_FETCH_BYTES (2MB) is CPU-heavy and synchronous;
+ # inline it would block the event loop per fetch. Offload to a thread so concurrent
+ # fetches (and the rest of the server) keep making progress.
+ text = await asyncio.to_thread(_extract_text_from_html, content)
else:
try:
text = content.decode(resp.encoding or "utf-8", errors="replace")
From 6e0cddc6c52f220fa6417c7bc74550ac2087a091 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 16:59:05 -0700
Subject: [PATCH 029/190] test(web): full markdown-surface smoke test (chat
renderer audit) (#1329)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Renders a MARKDOWN_SMOKE fixture exercising every markdown construct (headings, inline
emphasis, nested lists, task lists, blockquotes, Shiki code, mermaid, KaTeX math, tables,
images, footnotes, hr) through the real streamdown pipeline. Hard-asserts the structure +
the interactive chrome the DS must theme (code-block copy button, table wrapper), and
annotates the harder constructs' current render state instead of flaking — plus saves a
screenshot of the rendered message for the brand/style audit.
Findings it pins (feeding protoContent#297/#298):
- table chrome: 3 off-brand action buttons render (#297)
- task-list checkboxes: render OK
- KaTeX math: 0 nodes — math is NOT wired in our streamdown setup (#298)
- mermaid: 0 SVG — diagrams fall back to a code block, NOT wired (#298)
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/fixtures.mjs | 52 ++++++++++++++++++++++++++
apps/web/e2e/markdown-smoke.spec.ts | 58 +++++++++++++++++++++++++++++
2 files changed, 110 insertions(+)
create mode 100644 apps/web/e2e/markdown-smoke.spec.ts
diff --git a/apps/web/e2e/fixtures.mjs b/apps/web/e2e/fixtures.mjs
index 25d4731f..00582e6c 100644
--- a/apps/web/e2e/fixtures.mjs
+++ b/apps/web/e2e/fixtures.mjs
@@ -259,6 +259,55 @@ const MARKDOWN_ANSWER = [
"```",
].join("\n");
+// A full-surface markdown smoke doc — exercises EVERY construct the chat renderer must
+// handle so we can eyeball the whole brand/style surface in one turn (and feed concrete
+// gaps to protoContent#297/#298). Kept in sync with scratch markdown-smoke.md.
+const MARKDOWN_SMOKE_ANSWER = [
+ "# H1 — Markdown smoke test",
+ "",
+ "## H2 heading",
+ "### H3 heading",
+ "",
+ "A paragraph with **bold**, *italic*, `inline code`, ~~strike~~, and a [link](https://example.com).",
+ "",
+ "> A blockquote.",
+ "> > Nested blockquote.",
+ "",
+ "- Unordered item",
+ " - Nested item",
+ "- Item with `code`",
+ "",
+ "1. Ordered item",
+ "2. Second item",
+ "",
+ "- [ ] Open task",
+ "- [x] Done task",
+ "",
+ "Inline math $E = mc^2$ and a block:",
+ "",
+ "$$a^2 + b^2 = c^2$$",
+ "",
+ "```ts",
+ "export const add = (a: number, b: number): number => a + b;",
+ "```",
+ "",
+ "```mermaid",
+ "flowchart LR",
+ " A[Start] --> B[End]",
+ "```",
+ "",
+ "| Column A | Column B | Numeric |",
+ "| -------- | -------- | ------: |",
+ "| alpha | first | 1024 |",
+ "| beta | second | 2048 |",
+ "",
+ "",
+ "",
+ "---",
+ "",
+ "Final paragraph after a horizontal rule.",
+].join("\n");
+
const DEFAULT_SEARCH_OUTPUT = [
"8 result(s) for 'AI coding agents latest news':",
"1. First Result — https://example.com/a",
@@ -359,6 +408,9 @@ function scenarioFor(prompt) {
streamChunks: ["Testing ", "catches bugs ", "before users do."],
answer: "Testing catches bugs before users do.",
};
+ if (t.includes("MARKDOWN_SMOKE"))
+ // Pure markdown render (no tool card — `events: []`) of the full-surface smoke doc.
+ return { events: [], answer: MARKDOWN_SMOKE_ANSWER };
if (t.includes("MARKDOWN"))
return { name: "web_search", input: { query: "md" }, output: DEFAULT_SEARCH_OUTPUT, answer: MARKDOWN_ANSWER };
return { name: "web_search", input: { max_results: 8, query: "AI coding agents latest news" }, output: DEFAULT_SEARCH_OUTPUT, answer: "Done — found 8 results." };
diff --git a/apps/web/e2e/markdown-smoke.spec.ts b/apps/web/e2e/markdown-smoke.spec.ts
new file mode 100644
index 00000000..74ed1d9a
--- /dev/null
+++ b/apps/web/e2e/markdown-smoke.spec.ts
@@ -0,0 +1,58 @@
+import { expect, test } from "@playwright/test";
+
+// Full-surface markdown smoke test. Renders the MARKDOWN_SMOKE fixture (every markdown
+// construct) through the real streamdown chat pipeline and asserts the structure + the
+// interactive chrome render. Doubles as the brand/style audit surface for the DS work in
+// protoContent#297 (chrome off-brand) and #298 (DS markdown renderer): a screenshot of the
+// rendered message is saved for visual review, and the elements that don't render
+// deterministically (task-list checkboxes, KaTeX math, mermaid) are annotated, not failed.
+
+test("full markdown surface renders (structure + chrome)", async ({ page }, testInfo) => {
+ await page.goto("/app/", { waitUntil: "load" });
+ const composer = page.getByPlaceholder(/Message protoAgent/i);
+ await composer.waitFor({ state: "visible" });
+ await composer.fill("MARKDOWN_SMOKE: render the full surface");
+ await composer.press("Enter");
+
+ const md = page.locator(".pl-message--assistant .markdown");
+ await expect(md).toBeVisible();
+
+ // ── Structure: streamdown emits these as real semantic tags ──────────────────
+ await expect(md.locator("h1")).toContainText("Markdown smoke test");
+ await expect(md.locator("h2")).toContainText("H2 heading");
+ await expect(md.locator('[data-streamdown="strong"]')).toContainText("bold");
+ await expect(md.locator("ul li").first()).toBeVisible();
+ await expect(md.locator("ol li")).toHaveCount(2);
+ await expect(md.locator('[data-streamdown="blockquote"]').first()).toBeVisible();
+ // NB: two code blocks render (the ts block + the mermaid block, which falls back to a code
+ // block because mermaid isn't wired — see the audit annotations), so target by text.
+ await expect(md.locator("pre code").filter({ hasText: "export const add" })).toBeVisible();
+ await expect(md.locator("table")).toBeVisible();
+ await expect(md.locator("table td").first()).toContainText("alpha");
+ await expect(md.locator('[data-streamdown="horizontal-rule"]')).toBeVisible();
+
+ // ── Interactive chrome (the protoContent#297 surface): these MUST render so the
+ // DS has something to theme; if streamdown stops emitting them the audit is stale ──
+ await expect(md.locator('[data-streamdown="code-block-copy-button"]').first()).toBeAttached();
+ await expect(md.locator('[data-streamdown="table-wrapper"]').first()).toBeAttached();
+
+ // ── Audit annotations (non-failing): the current render state of the constructs the DS
+ // must own/theme — captured so the report documents gaps without flaking the guard ──
+ const tableButtons = await md.locator('[data-streamdown="table-wrapper"] button').count();
+ const taskboxes = await md.locator('li input[type="checkbox"]').count();
+ const katex = await md.locator(".katex").count();
+ const mermaid = await md.locator('[data-streamdown="mermaid"] svg, [data-streamdown="mermaid-block"] svg').count();
+ testInfo.annotations.push(
+ { type: "audit", description: `table chrome buttons: ${tableButtons} (off-brand — protoContent#297)` },
+ { type: "audit", description: `task-list checkboxes: ${taskboxes} (expect 2)` },
+ { type: "audit", description: `KaTeX math nodes: ${katex} (0 ⇒ math NOT wired — protoContent#298)` },
+ { type: "audit", description: `mermaid SVG nodes: ${mermaid} (0 ⇒ mermaid NOT wired — protoContent#298)` },
+ );
+
+ // Visual artifact for the brand-style audit (attached to the Playwright report).
+ await md.screenshot({ path: testInfo.outputPath("markdown-surface.png") });
+ await testInfo.attach("markdown-surface", {
+ path: testInfo.outputPath("markdown-surface.png"),
+ contentType: "image/png",
+ });
+});
From 7966cabd51dde12d3ab717b6defedf92807665ed Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:08:06 -0700
Subject: [PATCH 030/190] =?UTF-8?q?feat(web):=20adopt=20the=20DS=20Markdow?=
=?UTF-8?q?n=20renderer=20(@protolabsai/ui=200.48)=20=E2=80=94=20closes=20?=
=?UTF-8?q?#1330=20(#1331)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The DS now ships a brand-styled streaming-markdown renderer (`@protolabsai/ui/markdown`,
0.48.1), the DS-owned version of what the console hand-rolled with streamdown (#1328). It
themes BOTH prose and streamdown's interactive chrome (code/table action buttons) to
`--pl-*` via the stable `[data-streamdown]` hooks, wires KaTeX math, and renders mermaid as
a themed code block. Closes the contribute-back from protoContent#297/#298.
Adoption:
- Markdown.tsx → `` from `@protolabsai/ui/markdown`; the
`.markdown` class rides the DS `.pl-markdown` element so existing selectors still match.
- theme.css: delete the hand-rolled `.markdown` element CSS — the DS owns prose + chrome.
- tailwind.config.cjs: drop the streamdown dist `@source` scan — the DS markdown.css themes
the chrome via `[data-streamdown]` in plain CSS, no Tailwind reliance.
- main.tsx: import `katex/dist/katex.min.css` (math glyphs); `streamdown/styles.css` stays
for the streaming token-fade. `@protolabsai/ui/styles.css` now carries `.pl-markdown`.
- Chrome defaults to copy-only (download/fullscreen off for a chat bubble — #297); mermaid
stays a themed code block (DS `renderMermaid` is opt-in — it's heavy).
- markdown-smoke.spec.ts: now asserts KaTeX math renders + the DS chrome; the prior gaps
(math unwired, 3 off-brand buttons) are resolved.
Dependency plumbing (the awkward part):
- 0.48.1 lists `@types/react@^19` as a dependency and ships .tsx SOURCE our tsc compiles,
so the tree must resolve a SINGLE @types/react or the DS source and our code disagree on
`ReactNode`. Align the app to @types/react@19 (the DS is React-19-native): bump apps/web
back to ^19, drop the root @18 pin, fix the 5 spots that relied on @18 (`JSX` global →
`import type { JSX }` in ChatComponent/PluginsSurface; type the react-error-boundary
`reset` param in ErrorBoundary/GoalsPanel/TasksPanel).
- Drop `.npmrc legacy-peer-deps=true` (it silently disables npm `overrides`) and instead
resolve the @docsearch/react react@18-vs-19 ERESOLVE with a targeted override pointing it
at react@19. `npm ci` validates cleanly.
Co-authored-by: Claude Opus 4.8 (1M context)
---
.npmrc | 7 -
apps/web/e2e/markdown-smoke.spec.ts | 25 +-
apps/web/package.json | 3 +-
apps/web/src/app/ErrorBoundary.tsx | 2 +-
apps/web/src/app/GoalsPanel.tsx | 2 +-
apps/web/src/app/TasksPanel.tsx | 2 +-
apps/web/src/app/theme.css | 68 +-
apps/web/src/chat/ChatComponent.tsx | 2 +
apps/web/src/chat/LazyMarkdown.tsx | 10 +-
apps/web/src/chat/Markdown.tsx | 23 +-
apps/web/src/main.tsx | 5 +-
apps/web/src/plugins/PluginsSurface.tsx | 2 +-
apps/web/tailwind.config.cjs | 7 +-
package-lock.json | 4984 ++++++++++++-----------
package.json | 8 +-
15 files changed, 2774 insertions(+), 2376 deletions(-)
delete mode 100644 .npmrc
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index 050b0d77..00000000
--- a/.npmrc
+++ /dev/null
@@ -1,7 +0,0 @@
-# A transitive dep (@docsearch/react via the VitePress docs site) pins react/react-dom@18
-# while the console (apps/web) runs react@19. npm's strict peer resolver ERESOLVE-rejects that
-# valid nested split whenever the tree is re-resolved (e.g. adding a dep). Use legacy peer
-# resolution so `npm install` and CI's `npm ci` agree. NOTE: legacy mode would otherwise drop
-# the root @types/react@18 the console's tsc resolves for the global `JSX` namespace — it's
-# pinned explicitly in the root package.json devDependencies to survive.
-legacy-peer-deps=true
diff --git a/apps/web/e2e/markdown-smoke.spec.ts b/apps/web/e2e/markdown-smoke.spec.ts
index 74ed1d9a..6db8b29c 100644
--- a/apps/web/e2e/markdown-smoke.spec.ts
+++ b/apps/web/e2e/markdown-smoke.spec.ts
@@ -1,11 +1,10 @@
import { expect, test } from "@playwright/test";
// Full-surface markdown smoke test. Renders the MARKDOWN_SMOKE fixture (every markdown
-// construct) through the real streamdown chat pipeline and asserts the structure + the
-// interactive chrome render. Doubles as the brand/style audit surface for the DS work in
-// protoContent#297 (chrome off-brand) and #298 (DS markdown renderer): a screenshot of the
-// rendered message is saved for visual review, and the elements that don't render
-// deterministically (task-list checkboxes, KaTeX math, mermaid) are annotated, not failed.
+// construct) through the DS `` renderer (@protolabsai/ui, adopted in #1330) and
+// asserts structure + chrome + KaTeX math render. A screenshot of the rendered message is
+// saved for visual brand review. Mermaid renders as a themed code block by default (DS
+// `renderMermaid` is opt-in — it's heavy), so SVG diagrams aren't expected here.
test("full markdown surface renders (structure + chrome)", async ({ page }, testInfo) => {
await page.goto("/app/", { waitUntil: "load" });
@@ -31,22 +30,24 @@ test("full markdown surface renders (structure + chrome)", async ({ page }, test
await expect(md.locator("table td").first()).toContainText("alpha");
await expect(md.locator('[data-streamdown="horizontal-rule"]')).toBeVisible();
- // ── Interactive chrome (the protoContent#297 surface): these MUST render so the
- // DS has something to theme; if streamdown stops emitting them the audit is stale ──
+ // ── DS-themed chrome + math: the DS renderer copy button + table wrapper are present
+ // (themed via [data-streamdown]); table download/fullscreen are off by default for a
+ // chat bubble, so the table carries a single (copy) control. KaTeX math now renders. ──
await expect(md.locator('[data-streamdown="code-block-copy-button"]').first()).toBeAttached();
await expect(md.locator('[data-streamdown="table-wrapper"]').first()).toBeAttached();
+ await expect(md.locator(".katex").first()).toBeVisible();
- // ── Audit annotations (non-failing): the current render state of the constructs the DS
- // must own/theme — captured so the report documents gaps without flaking the guard ──
+ // Audit annotations (non-failing) — the render state of the harder constructs, for the
+ // brand-review screenshot below.
const tableButtons = await md.locator('[data-streamdown="table-wrapper"] button').count();
const taskboxes = await md.locator('li input[type="checkbox"]').count();
const katex = await md.locator(".katex").count();
const mermaid = await md.locator('[data-streamdown="mermaid"] svg, [data-streamdown="mermaid-block"] svg').count();
testInfo.annotations.push(
- { type: "audit", description: `table chrome buttons: ${tableButtons} (off-brand — protoContent#297)` },
+ { type: "audit", description: `table chrome buttons: ${tableButtons} (copy-only — download/fullscreen off for chat)` },
{ type: "audit", description: `task-list checkboxes: ${taskboxes} (expect 2)` },
- { type: "audit", description: `KaTeX math nodes: ${katex} (0 ⇒ math NOT wired — protoContent#298)` },
- { type: "audit", description: `mermaid SVG nodes: ${mermaid} (0 ⇒ mermaid NOT wired — protoContent#298)` },
+ { type: "audit", description: `KaTeX math nodes: ${katex} (math wired via the DS renderer)` },
+ { type: "audit", description: `mermaid SVG nodes: ${mermaid} (0 = themed code block; DS renderMermaid is opt-in)` },
);
// Visual artifact for the brand-style audit (attached to the Playwright report).
diff --git a/apps/web/package.json b/apps/web/package.json
index 9e11df10..7ebbfc78 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -16,11 +16,12 @@
},
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.47.1",
+ "@protolabsai/ui": "^0.48.1",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "katex": "^0.16.47",
"lucide-react": "^0.468.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/apps/web/src/app/ErrorBoundary.tsx b/apps/web/src/app/ErrorBoundary.tsx
index 56f0ac32..7187ec99 100644
--- a/apps/web/src/app/ErrorBoundary.tsx
+++ b/apps/web/src/app/ErrorBoundary.tsx
@@ -115,7 +115,7 @@ export function StagePanel({
return (
- {({ reset }) => (
+ {({ reset }: { reset: () => void }) => (
}>
}>
{children}
diff --git a/apps/web/src/app/GoalsPanel.tsx b/apps/web/src/app/GoalsPanel.tsx
index b3c03282..cfe3997a 100644
--- a/apps/web/src/app/GoalsPanel.tsx
+++ b/apps/web/src/app/GoalsPanel.tsx
@@ -107,7 +107,7 @@ export function GoalsPanel() {
/>
- {({ reset }) => (
+ {({ reset }: { reset: () => void }) => (
}>
}>
diff --git a/apps/web/src/app/TasksPanel.tsx b/apps/web/src/app/TasksPanel.tsx
index e0843cc5..fa572823 100644
--- a/apps/web/src/app/TasksPanel.tsx
+++ b/apps/web/src/app/TasksPanel.tsx
@@ -334,7 +334,7 @@ export function TasksPanel({ confirm }: { confirm: (req: ConfirmRequest) => void
return (
- {({ reset }) => (
+ {({ reset }: { reset: () => void }) => (
}>
}>
diff --git a/apps/web/src/app/theme.css b/apps/web/src/app/theme.css
index f849a80b..c76662dd 100644
--- a/apps/web/src/app/theme.css
+++ b/apps/web/src/app/theme.css
@@ -856,68 +856,12 @@ select:disabled {
/* Tool-call cards + per-tool renderers — moved to src/chat/tool-calls.css (#832). */
-/* Rendered markdown (assistant messages). The parent keeps pre-wrap for the
- literal user text; markdown content reflows normally. */
-.markdown {
- white-space: normal;
- line-height: 1.55;
-}
-.markdown > :first-child { margin-top: 0; }
-.markdown > :last-child { margin-bottom: 0; }
-.markdown p { margin: 0 0 0.6em; }
-.markdown ul,
-.markdown ol { margin: 0 0 0.6em; padding-left: 1.4em; }
-.markdown li { margin: 0.15em 0; }
-.markdown h1,
-.markdown h2,
-.markdown h3,
-.markdown h4 { margin: 0.9em 0 0.4em; line-height: 1.25; font-weight: 600; }
-.markdown h1 { font-size: 1.3em; }
-.markdown h2 { font-size: 1.18em; }
-.markdown h3 { font-size: 1.06em; }
-.markdown a { color: var(--brand-indigo-bright); text-decoration: underline; }
-.markdown code {
- font-family: var(--font-mono);
- font-size: 0.9em;
- background: var(--bg-raised);
- border: 1px solid var(--border);
- border-radius: 4px;
- padding: 0.1em 0.35em;
-}
-.markdown pre {
- margin: 0 0 0.7em;
- padding: 12px 14px;
- background: var(--bg-raised);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- overflow-x: auto;
-}
-.markdown pre code {
- background: none;
- border: 0;
- padding: 0;
- font-size: 0.85em;
-}
-.markdown blockquote {
- margin: 0 0 0.6em;
- padding-left: 0.9em;
- border-left: 3px solid var(--border-strong);
- color: var(--fg-secondary);
-}
-.markdown table {
- border-collapse: collapse;
- margin: 0 0 0.7em;
- display: block;
- overflow-x: auto;
-}
-.markdown th,
-.markdown td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
-.markdown th { background: var(--bg-raised); }
-.markdown hr { border: 0; border-top: 1px solid var(--border); margin: 0.9em 0; }
-
-/* Code highlighting is now Shiki (via streamdown), which emits inline token colors — the
- old rehype-highlight `.hljs-*` token rules are gone. streamdown's code-block chrome
- (header / copy button) is styled by its own Tailwind classes (scanned in tailwind.config). */
+/* Rendered markdown (assistant messages) is now the DS `` (@protolabsai/ui
+ ≥0.48), which scopes its own `.pl-markdown` and owns the FULL visual layer — prose
+ typography, Shiki code, KaTeX math, GFM tables, AND streamdown's interactive chrome
+ (code/table action buttons, themed + re-pinned to the brand). The console's hand-rolled
+ `.markdown` rules + the streamdown Tailwind scan are gone (protoContent#297/#298). The
+ `.markdown` class still rides the same element as an e2e/selector alias. */
/* Chat composer + slash-command autocomplete — moved to src/chat/chat.css (#832). */
/* HITL request card — moved to src/chat/hitl.css (#832). */
diff --git a/apps/web/src/chat/ChatComponent.tsx b/apps/web/src/chat/ChatComponent.tsx
index 788a67c1..d77b4e91 100644
--- a/apps/web/src/chat/ChatComponent.tsx
+++ b/apps/web/src/chat/ChatComponent.tsx
@@ -1,5 +1,7 @@
import "./chat-component.css";
+import type { JSX } from "react";
+
import type { ComponentSpec } from "../lib/types";
// Curated, data-only chat component registry (ADR 0051 Slice 2). Renders typed
diff --git a/apps/web/src/chat/LazyMarkdown.tsx b/apps/web/src/chat/LazyMarkdown.tsx
index 9dcf3fb9..bb08854c 100644
--- a/apps/web/src/chat/LazyMarkdown.tsx
+++ b/apps/web/src/chat/LazyMarkdown.tsx
@@ -1,14 +1,14 @@
import { lazy, Suspense } from "react";
-// The markdown pipeline (streamdown — which bundles Shiki + mermaid + katex) is the
-// heaviest dependency in the app. Load it lazily so it isn't in the initial chunk — it
-// only matters once an assistant message renders. Until the chunk arrives, fall back to
-// the raw text (a blink at most; then the same `.markdown` container takes over).
+// The markdown pipeline (the DS `` over streamdown — Shiki + KaTeX + mermaid) is
+// the heaviest dependency in the app. Load it lazily so it isn't in the initial chunk — it
+// only matters once an assistant message renders. Until the chunk arrives, fall back to the
+// raw text in the DS `.pl-markdown` scope (a blink at most; the real renderer then takes over).
const MarkdownImpl = lazy(() => import("./Markdown").then((m) => ({ default: m.Markdown })));
export function Markdown({ children }: { children: string }) {
return (
- {children}}>
+ {children}}>
{children}
);
diff --git a/apps/web/src/chat/Markdown.tsx b/apps/web/src/chat/Markdown.tsx
index 6a2cdc16..65a7564d 100644
--- a/apps/web/src/chat/Markdown.tsx
+++ b/apps/web/src/chat/Markdown.tsx
@@ -1,17 +1,16 @@
-import { Streamdown } from "streamdown";
+import { Markdown as DSMarkdown } from "@protolabsai/ui/markdown";
/**
- * Assistant message text via **streamdown** — a streaming-markdown renderer built for AI
- * output. It HARDENS incomplete markdown (a half-written code block / table / link doesn't
- * flash broken mid-stream) and memoizes blocks, instead of re-parsing the whole answer on
- * every streamed token (the old react-markdown path was O(N²) per turn). XSS-safe
- * (rehype-sanitize/harden, no raw HTML); Shiki code highlighting. Wrapped in `.markdown`
- * for the chat theme.
+ * Assistant message markdown — the DS `` (`@protolabsai/ui/markdown`, ≥0.48),
+ * which owns the brand styling for streamdown's prose AND its interactive chrome (code /
+ * table action buttons, themed + re-pinned), wires KaTeX math + GFM, and renders ```mermaid
+ * as a themed code block (live diagrams are an opt-in `renderMermaid`). Chrome defaults to
+ * copy-only — download/fullscreen are off for a chat bubble. Replaces the console's
+ * hand-rolled streamdown usage (protoContent#298).
+ *
+ * `className="markdown"` rides the same element the DS scopes as `.pl-markdown`, so existing
+ * `.markdown` selectors (e2e + message-layout) keep matching.
*/
export function Markdown({ children }: { children: string }) {
- return (
-
- {children}
-
- );
+ return {children} ;
}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index 129e8ba0..a9400566 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -12,8 +12,9 @@ import { isLauncherWindow } from "./lib/desktop";
// Tailwind + the shadcn→token bridge, then the legacy theme.css (which may reference --pl-*).
import "@protolabsai/design/css/tokens";
import "./app/tailwind.css";
-import "@protolabsai/ui/styles.css";
-import "streamdown/styles.css"; // streaming-markdown base styles (before the app theme so it can override)
+import "@protolabsai/ui/styles.css"; // component styles, incl. the DS `.pl-markdown` renderer
+import "streamdown/styles.css"; // streaming per-token fade (opt-in; see DS docstring)
+import "katex/dist/katex.min.css"; // KaTeX glyph layout for math in the DS
import "./app/theme-base.css"; // shared token bridge + resets — must load before the rest
import "./app/theme.css";
import { activateSlugAgent } from "./lib/api";
diff --git a/apps/web/src/plugins/PluginsSurface.tsx b/apps/web/src/plugins/PluginsSurface.tsx
index 9dc9937d..31a29c6e 100644
--- a/apps/web/src/plugins/PluginsSurface.tsx
+++ b/apps/web/src/plugins/PluginsSurface.tsx
@@ -4,7 +4,7 @@ import { Button } from "@protolabsai/ui/primitives";
import { Alert } from "@protolabsai/ui/data";
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
-import { Suspense, useState } from "react";
+import { Suspense, useState, type JSX } from "react";
import { Download, DownloadCloud, ExternalLink, Github, Loader2, RefreshCw, Search, Settings2, Store, Trash2 } from "lucide-react";
import { PanelHeader } from "@protolabsai/ui/navigation";
diff --git a/apps/web/tailwind.config.cjs b/apps/web/tailwind.config.cjs
index a9144ce7..c7fc99c0 100644
--- a/apps/web/tailwind.config.cjs
+++ b/apps/web/tailwind.config.cjs
@@ -5,10 +5,9 @@
// legacy theme.css without resetting base styles (incremental migration, ADR 0037 D4).
module.exports = {
presets: [require("@protolabsai/design/tailwind")],
- // streamdown (chat markdown renderer) ships pre-built JS that uses Tailwind utility
- // classes for its chrome (code-block header/copy button, tables, emphasis). Scan its dist
- // so those classes generate; the shadcn→token vars below theme them on-brand.
- content: ["./index.html", "./src/**/*.{ts,tsx}", "../../node_modules/streamdown/dist/*.js"],
+ // (The streamdown dist scan is gone — the DS `` owns markdown chrome styling
+ // via its self-contained markdown.css; streamdown's Tailwind classes are inert here.)
+ content: ["./index.html", "./src/**/*.{ts,tsx}"],
corePlugins: { preflight: false },
theme: {
extend: {
diff --git a/package-lock.json b/package-lock.json
index 955e8b5e..094c1652 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,6 @@
"devDependencies": {
"@protolabsai/design": "^0.3.0",
"@protolabsai/vitepress-theme": "^0.3.6",
- "@types/react": "18.3.29",
"vitepress": "^1.6.4"
}
},
@@ -30,11 +29,12 @@
"version": "0.1.0",
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.47.1",
+ "@protolabsai/ui": "^0.48.1",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "katex": "^0.16.47",
"lucide-react": "^0.468.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -69,1617 +69,1231 @@
"protolabs-sync-assets": "bin/sync-assets.mjs"
}
},
- "apps/web/node_modules/@protolabsai/ui": {
- "version": "0.47.1",
- "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.47.1.tgz",
- "integrity": "sha512-f6QetgfVXAClYDgN5QjX2VROgd5pCOKhWiSCC4oxAzRjh/qKAV/0xwph3mnIA6KyYoeE38NSmLOox1u72wyR7w==",
+ "node_modules/@algolia/abtesting": {
+ "version": "1.21.1",
+ "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.21.1.tgz",
+ "integrity": "sha512-Wia5/mNTfiU0PIUN25UMfAGGdASkkwuCS9nBAdmhqrNPY/ff7U/6MgBVdwFDPsa3sA1msutPtO50gvOzx6MOXA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@dnd-kit/core": "^6.3.1",
- "@dnd-kit/sortable": "^10.0.0",
- "@dnd-kit/utilities": "^3.2.2",
- "@protolabsai/design": "0.7.0",
- "@radix-ui/react-dropdown-menu": "^2.1.17",
- "@radix-ui/react-popover": "^1.1.16",
- "@radix-ui/react-tooltip": "^1.2.10",
- "@types/react": "^19.0.0",
- "@types/react-dom": "^19.0.0",
- "culori": "^4.0.2",
- "framer-motion": "^11.18.0"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependencies": {
- "react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@protolabsai/ui/node_modules/@protolabsai/design": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/@protolabsai/design/-/design-0.7.0.tgz",
- "integrity": "sha512-QF/SShqPhP9SFPFiBnJkjOyp8/bbP07xi6dVtqZDkPb4U8QwnUtL1ld88xEZMa3SvY6GX2R53fa+JCgSiz3HGw==",
+ "node_modules/@algolia/autocomplete-core": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz",
+ "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==",
+ "dev": true,
"license": "MIT",
- "bin": {
- "protolabs-sync-assets": "bin/sync-assets.mjs"
+ "dependencies": {
+ "@algolia/autocomplete-plugin-algolia-insights": "1.17.7",
+ "@algolia/autocomplete-shared": "1.17.7"
}
},
- "apps/web/node_modules/@radix-ui/react-arrow": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.9.tgz",
- "integrity": "sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig==",
+ "node_modules/@algolia/autocomplete-plugin-algolia-insights": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz",
+ "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.5"
+ "@algolia/autocomplete-shared": "1.17.7"
},
"peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "search-insights": ">= 1 < 3"
}
},
- "apps/web/node_modules/@radix-ui/react-collection": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.9.tgz",
- "integrity": "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ==",
+ "node_modules/@algolia/autocomplete-preset-algolia": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz",
+ "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-slot": "1.2.5"
+ "@algolia/autocomplete-shared": "1.17.7"
},
"peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "@algolia/client-search": ">= 4.9.1 < 6",
+ "algoliasearch": ">= 4.9.1 < 6"
}
},
- "apps/web/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.12.tgz",
- "integrity": "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==",
+ "node_modules/@algolia/autocomplete-shared": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz",
+ "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-use-callback-ref": "1.1.2",
- "@radix-ui/react-use-escape-keydown": "1.1.2"
- },
"peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "@algolia/client-search": ">= 4.9.1 < 6",
+ "algoliasearch": ">= 4.9.1 < 6"
}
},
- "apps/web/node_modules/@radix-ui/react-dropdown-menu": {
- "version": "2.1.17",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.17.tgz",
- "integrity": "sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw==",
+ "node_modules/@algolia/client-abtesting": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.55.1.tgz",
+ "integrity": "sha512-miW8RzAtBgNiEJ9fGEhsOPgWUpekAe64YcVufqXrlykj0Jjmo5nj0a5f/HAzRVX5ZuU1GAVd7BkzFDx7q50P3A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-menu": "2.1.17",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-use-controllable-state": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-focus-scope": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.9.tgz",
- "integrity": "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==",
+ "node_modules/@algolia/client-analytics": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.55.1.tgz",
+ "integrity": "sha512-eR3J3kB9JX6DdCvDRi3I4KPfwO6fR9HWYRXhVke2TXIoOQafMKCRAneg33JRmIrb+DnnJ/eWApJLF1O1CLPERg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-use-callback-ref": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/client-common": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.55.1.tgz",
+ "integrity": "sha512-P5ak7EurwYqgAiDyb95mgA3WRR/Zu8CPMv36lWTISvL2AmlPyqQPy2nX/KEJRTcwaeTWwrk6wJV4/M93GfjOWw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-menu": {
- "version": "2.1.17",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.17.tgz",
- "integrity": "sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q==",
+ "node_modules/@algolia/client-insights": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.55.1.tgz",
+ "integrity": "sha512-OVtj9uA//+pjvKQI5INnzbyLrf3ClNv3XRbWswwJ2kHIStQNHtBfHo+LofNB/WhM9xjuXlW5ANn2aMj65UGx7w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-collection": "1.1.9",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-direction": "1.1.2",
- "@radix-ui/react-dismissable-layer": "1.1.12",
- "@radix-ui/react-focus-guards": "1.1.4",
- "@radix-ui/react-focus-scope": "1.1.9",
- "@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-popper": "1.3.0",
- "@radix-ui/react-portal": "1.1.11",
- "@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-roving-focus": "1.1.12",
- "@radix-ui/react-slot": "1.2.5",
- "@radix-ui/react-use-callback-ref": "1.1.2",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.7.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-popover": {
- "version": "1.1.16",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.16.tgz",
- "integrity": "sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==",
+ "node_modules/@algolia/client-personalization": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.55.1.tgz",
+ "integrity": "sha512-oKlVFlp+qbIEe4p7E54zSiP2gEV/vDu972Ykv8VDMFwEvreS7m0YKA3a8hGGHwc7yiBUGGiR3LlwzMLfnJmy6Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-dismissable-layer": "1.1.12",
- "@radix-ui/react-focus-guards": "1.1.4",
- "@radix-ui/react-focus-scope": "1.1.9",
- "@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-popper": "1.3.0",
- "@radix-ui/react-portal": "1.1.11",
- "@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-slot": "1.2.5",
- "@radix-ui/react-use-controllable-state": "1.2.3",
- "aria-hidden": "^1.2.4",
- "react-remove-scroll": "^2.7.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-popper": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.0.tgz",
- "integrity": "sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ==",
+ "node_modules/@algolia/client-query-suggestions": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.1.tgz",
+ "integrity": "sha512-BOVrld6vdtsFmotVDMTVQfYXwrVplJ+DUvy60JFi+tkWV698q2J9NNPKEO3dr5qxtSLKQP4vHF8n+3U5PDWhOQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.9",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-use-callback-ref": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.2",
- "@radix-ui/react-use-rect": "1.1.2",
- "@radix-ui/react-use-size": "1.1.2",
- "@radix-ui/rect": "1.1.2"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-portal": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.11.tgz",
- "integrity": "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==",
+ "node_modules/@algolia/client-search": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz",
+ "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-use-layout-effect": "1.1.2"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@algolia/ingestion": {
+ "version": "1.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.55.1.tgz",
+ "integrity": "sha512-BXZw+C+gsWL7pZvbnhJUnCXASiDLGcQxVV7h55Pyh2DmSzwdZIVccE5xc9RVD2trtrhIqk5smuODTxtaZqd0IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-presence": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz",
- "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==",
+ "node_modules/@algolia/monitoring": {
+ "version": "1.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.55.1.tgz",
+ "integrity": "sha512-9g/ceZrZTqA62FA3588Xj0onRPjDNfu0pVQqefK0rrHp9H6Wblph/YmzGjZ2g8uqbTh0ZGIvAGCzErU8f7MHpA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.2"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.5.tgz",
- "integrity": "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==",
+ "node_modules/@algolia/recommend": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.55.1.tgz",
+ "integrity": "sha512-cZTIrGyAP+W4A6jDVwvWM/JOaoJKQkD/2a5eLUEeNdKAD45jN7BCpsMDONyhZlosLa4UwL8uiINQzj4iFy9nqg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.2.5"
+ "@algolia/client-common": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-roving-focus": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.12.tgz",
- "integrity": "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg==",
+ "node_modules/@algolia/requester-browser-xhr": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.1.tgz",
+ "integrity": "sha512-N6I3leW0UO8Y9Zv90yo2UHgYGuxZO0mjbvzNxDIJDjO0qECEF7Z9XMvSNeUWXQh/iNDA9lr8MfEy3rmZGIcclw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-collection": "1.1.9",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-direction": "1.1.2",
- "@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-primitive": "2.1.5",
- "@radix-ui/react-use-callback-ref": "1.1.2",
- "@radix-ui/react-use-controllable-state": "1.2.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip": {
- "version": "1.2.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz",
- "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==",
+ "node_modules/@algolia/requester-fetch": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.55.1.tgz",
+ "integrity": "sha512-ukU5zeeFs44rQkzv+TRdYard+d+3lmPGs8lPZhHtWE8rfz+LlBSF6s9kP3VQ7LeOYL8Dz0u6tZfnyTrqrumbHQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-dismissable-layer": "1.1.13",
- "@radix-ui/react-id": "1.1.2",
- "@radix-ui/react-popper": "1.3.1",
- "@radix-ui/react-portal": "1.1.12",
- "@radix-ui/react-presence": "1.1.6",
- "@radix-ui/react-primitive": "2.1.6",
- "@radix-ui/react-slot": "1.3.0",
- "@radix-ui/react-use-controllable-state": "1.2.3",
- "@radix-ui/react-visually-hidden": "1.2.6"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz",
- "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==",
+ "node_modules/@algolia/requester-node-http": {
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.55.1.tgz",
+ "integrity": "sha512-lCwXyijwPm3vbYHpBXPRomMcD6mgiptmps27gnMCf4HK+u/AOeFPBnIFh4V3l4A5SnP9VRiKBZqwGBpUH0vaTg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.6"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@algolia/client-common": "5.55.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">= 14.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz",
- "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==",
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/primitive": "1.1.4",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-primitive": "2.1.6",
- "@radix-ui/react-use-callback-ref": "1.1.2",
- "@radix-ui/react-use-escape-keydown": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "engines": {
+ "node": ">=10"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz",
- "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==",
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
"license": "MIT",
"dependencies": {
- "@floating-ui/react-dom": "^2.0.0",
- "@radix-ui/react-arrow": "1.1.10",
- "@radix-ui/react-compose-refs": "1.1.3",
- "@radix-ui/react-context": "1.1.4",
- "@radix-ui/react-primitive": "2.1.6",
- "@radix-ui/react-use-callback-ref": "1.1.2",
- "@radix-ui/react-use-layout-effect": "1.1.2",
- "@radix-ui/react-use-rect": "1.1.2",
- "@radix-ui/react-use-size": "1.1.2",
- "@radix-ui/rect": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz",
- "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==",
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.6",
- "@radix-ui/react-use-layout-effect": "1.1.2"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
- "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-slot": "1.3.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
- "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.3"
- },
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "apps/web/node_modules/@radix-ui/react-visually-hidden": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz",
- "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==",
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.6"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "apps/web/node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
- "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.3.0"
- },
- "peerDependencies": {
- "@types/react": "*",
- "@types/react-dom": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
- "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "@types/react-dom": {
- "optional": true
- }
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "apps/web/node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
- "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@radix-ui/react-compose-refs": "1.1.3"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
},
- "peerDependencies": {
- "@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "engines": {
+ "node": ">=6.9.0"
},
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "apps/web/node_modules/@types/react": {
- "version": "19.2.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
- "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "apps/web/node_modules/@types/react-dom": {
- "version": "19.2.3",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
- "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.2.0"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
}
},
- "apps/web/node_modules/react-dom": {
- "version": "19.2.7",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
- "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "scheduler": "^0.27.0"
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
},
- "peerDependencies": {
- "react": "^19.2.7"
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "apps/web/node_modules/scheduler": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
- "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
- "license": "MIT"
- },
- "node_modules/@algolia/abtesting": {
- "version": "1.19.0",
- "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.19.0.tgz",
- "integrity": "sha512-Lhnez3hhXHk25lfxLAMxvkP4fmN3+1RgADhD2ssMDBYuAsDVReeyP+3SGRx+ntq8ijMrLqUyfvO72TB6jsTteQ==",
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/autocomplete-core": {
- "version": "1.17.7",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz",
- "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==",
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@algolia/autocomplete-plugin-algolia-insights": "1.17.7",
- "@algolia/autocomplete-shared": "1.17.7"
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/autocomplete-plugin-algolia-insights": {
- "version": "1.17.7",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz",
- "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==",
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/autocomplete-shared": "1.17.7"
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
- "peerDependencies": {
- "search-insights": ">= 1 < 3"
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/autocomplete-preset-algolia": {
- "version": "1.17.7",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz",
- "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==",
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/autocomplete-shared": "1.17.7"
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
},
"peerDependencies": {
- "@algolia/client-search": ">= 4.9.1 < 6",
- "algoliasearch": ">= 4.9.1 < 6"
+ "@babel/core": "^7.0.0"
}
},
- "node_modules/@algolia/autocomplete-shared": {
- "version": "1.17.7",
- "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz",
- "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==",
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "@algolia/client-search": ">= 4.9.1 < 6",
- "algoliasearch": ">= 4.9.1 < 6"
+ "engines": {
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/client-abtesting": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.53.0.tgz",
- "integrity": "sha512-0ZjA5Hcmaoz5Lj6OG0zhfIyeqzJZnLW2CRJA1W17UwMFGRtZAJ9yJKRvPEDA6gkpsIoQxORTSW6sWFiuYncPNQ==",
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
- },
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/client-analytics": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.53.0.tgz",
- "integrity": "sha512-kWNodP75iiEaOtemC9F/hlxNBG5E2QUjN1BusnE6m2b4l7Qh/BUO3fGCVsmKJI65VO4VKGGmT43ICvHtTcJ2JQ==",
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
- },
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/client-common": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.53.0.tgz",
- "integrity": "sha512-YPN45TXD9Wrse185t/Ta7nktZsqpv97oOjCzp2sblHnCL6rBc9TDeJAg1IGl2UpdwnSD05Zu/5wLB4watOUMyg==",
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/client-insights": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.53.0.tgz",
- "integrity": "sha512-qAcYTDJE6m924FDDUQvdD6vh7DYaqOeSpFS74IP37/JRV0v4cGBauyxTF2WzDnokUylQDbqreoFIJZfg0Fitmw==",
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/client-personalization": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.53.0.tgz",
- "integrity": "sha512-fQaY+DkSJOpuUVUe8MQTwrdiKAqkJGhpDarB08duBn/sUv7Bkib6MDRQauCcWTWTe4HIW+EbwQP9R4kci1V/Yw==",
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.0.0"
}
},
- "node_modules/@algolia/client-query-suggestions": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.53.0.tgz",
- "integrity": "sha512-o72tsiEZGfeS/dxL9IADfzcZWGEwKDEe5CvtrBuT//3JR+SHuTtHRI2ZTf7D7bcKagcbojvO8hnkHdfoakSlYg==",
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": ">= 14.0.0"
- }
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
},
- "node_modules/@algolia/client-search": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.53.0.tgz",
- "integrity": "sha512-Ds16IyPm/dNJPCU8OzApo2gwGrgWT5BYHhE3NFwZbpCveqyvPDB9sZDDkJ5DsdOGT2aC+R3i0/M1OVXF2qdgPg==",
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/helper-plugin-utils": "^7.29.7"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
}
},
- "node_modules/@algolia/ingestion": {
- "version": "1.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.53.0.tgz",
- "integrity": "sha512-oNbT6z4NwD8Pou9VPINGlN/tlG1afESh2EbxqnP6rwl95xKVD/Zlciis1PpNeO/9U/rrajc1+7DcfKi03tX1KQ==",
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/monitoring": {
- "version": "1.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.53.0.tgz",
- "integrity": "sha512-G+KZb/yd+qAOFn/cEvTGeLxQm8aP3a0od50l3z/ylccY+/o4YG3TNcjU1tFQHW4mXC137GPyR7W70R0kRQDLnA==",
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/recommend": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.53.0.tgz",
- "integrity": "sha512-6aVfYd55Un6IUgPLbo84WfgFZlS3L0vA1ttzXL5vahHewUJ8jYgd89TzlWRTeej7w70mb9RWsVlFYGmJ/diQww==",
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@algolia/requester-browser-xhr": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.53.0.tgz",
- "integrity": "sha512-ke27DqgzCOlt+RbeEdCxtXxMQOnAOi8ujr2wid0DmDKzR95Kw/f9sBsuhBxtjevCqJRJszfRTLY0B1pbO6IhkA==",
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
+ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
+ "license": "MIT"
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0"
+ "css-tree": "^3.0.0"
},
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz",
+ "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=20.19.0"
}
},
- "node_modules/@algolia/requester-fetch": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.53.0.tgz",
- "integrity": "sha512-GngiOqt2Gq4oLno6yXQVj9om+qSO9SWAoduoTOEg79dKZ62brB8OOIvSJG/vDNoanYi6a7Al9uDZwXvi+bcVTg==",
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
- "dependencies": {
- "@algolia/client-common": "5.53.0"
- },
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
}
},
- "node_modules/@algolia/requester-node-http": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.53.0.tgz",
- "integrity": "sha512-6mF9LZMUk0QqWvrnxkxBqhswwz6Xfiwy6/gmTzL5HrlhdVG3ITAqGV2k3XmVThP1h0Ulc3VQwiNCD7/Nr4JNlQ==",
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz",
+ "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@algolia/client-common": "5.53.0"
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.2.1"
},
"engines": {
- "node": ">= 14.0.0"
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
}
},
- "node_modules/@alloc/quick-lru": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
- "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">=20.19.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
}
},
- "node_modules/@antfu/install-pkg": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
- "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
- "license": "MIT",
- "dependencies": {
- "package-manager-detector": "^1.3.0",
- "tinyexec": "^1.0.1"
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
+ "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
},
- "funding": {
- "url": "https://github.com/sponsors/antfu"
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
}
},
- "node_modules/@asamuzakjp/css-color": {
- "version": "5.1.11",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
- "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
- "dependencies": {
- "@asamuzakjp/generational-cache": "^1.0.1",
- "@csstools/css-calc": "^3.2.0",
- "@csstools/css-color-parser": "^4.1.0",
- "@csstools/css-parser-algorithms": "^4.0.0",
- "@csstools/css-tokenizer": "^4.0.0"
- },
"engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ "node": ">=20.19.0"
}
},
- "node_modules/@asamuzakjp/dom-selector": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
- "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
- "dev": true,
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
- "@asamuzakjp/generational-cache": "^1.0.1",
- "@asamuzakjp/nwsapi": "^2.3.9",
- "bidi-js": "^1.0.3",
- "css-tree": "^3.2.1",
- "is-potential-custom-element-name": "^1.0.1"
+ "tslib": "^2.0.0"
},
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ "peerDependencies": {
+ "react": ">=16.8.0"
}
},
- "node_modules/@asamuzakjp/generational-cache": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
- "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
- "dev": true,
+ "node_modules/@dnd-kit/core": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.1",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
}
},
- "node_modules/@asamuzakjp/nwsapi": {
- "version": "2.3.9",
- "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
- "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
- "dev": true,
- "license": "MIT"
+ "node_modules/@dnd-kit/sortable": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+ "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.3.0",
+ "react": ">=16.8.0"
+ }
},
- "node_modules/@babel/code-frame": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
- "dev": true,
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.29.7",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
+ "tslib": "^2.0.0"
},
- "engines": {
- "node": ">=6.9.0"
+ "peerDependencies": {
+ "react": ">=16.8.0"
}
},
- "node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "node_modules/@docsearch/css": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz",
+ "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/js": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz",
+ "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=6.9.0"
+ "dependencies": {
+ "@docsearch/react": "3.8.2",
+ "preact": "^10.0.0"
}
},
- "node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "node_modules/@docsearch/js/node_modules/@docsearch/react": {
+ "version": "3.8.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz",
+ "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
+ "@algolia/autocomplete-core": "1.17.7",
+ "@algolia/autocomplete-preset-algolia": "1.17.7",
+ "@docsearch/css": "3.8.2",
+ "algoliasearch": "^5.14.2"
},
- "engines": {
- "node": ">=6.9.0"
+ "peerDependencies": {
+ "@types/react": ">= 16.8.0 < 19.0.0",
+ "react": ">= 16.8.0 < 19.0.0",
+ "react-dom": ">= 16.8.0 < 19.0.0",
+ "search-insights": ">= 1 < 3"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "search-insights": {
+ "optional": true
+ }
}
},
- "node_modules/@babel/generator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
- "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "node_modules/@docsearch/js/node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
"dev": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
}
},
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
}
},
- "node_modules/@babel/helper-globals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=6.9.0"
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
+ "tslib": "^2.4.0"
}
},
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
+ "optional": true,
+ "os": [
+ "aix"
+ ],
"engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/helper-plugin-utils": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
- "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.7"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
"engines": {
- "node": ">=6.0.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/plugin-transform-react-jsx-self": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
- "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
"engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
+ "node": ">=12"
}
},
- "node_modules/@babel/plugin-transform-react-jsx-source": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
- "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^7.29.7"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0-0"
+ "node": ">=12"
}
},
- "node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/traverse": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
- "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7",
- "debug": "^4.3.1"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@babel/types": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": ">=12"
}
},
- "node_modules/@braintree/sanitize-url": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
- "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
- "license": "MIT"
- },
- "node_modules/@bramus/specificity": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
- "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "css-tree": "^3.0.0"
- },
- "bin": {
- "specificity": "bin/cli.js"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/@chevrotain/types": {
- "version": "11.1.2",
- "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz",
- "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
- "license": "Apache-2.0"
- },
- "node_modules/@csstools/color-helpers": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
- "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
"dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
],
- "license": "MIT-0",
"engines": {
- "node": ">=20.19.0"
+ "node": ">=12"
}
},
- "node_modules/@csstools/css-calc": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
- "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
],
+ "dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@csstools/css-parser-algorithms": "^4.0.0",
- "@csstools/css-tokenizer": "^4.0.0"
+ "node": ">=12"
}
},
- "node_modules/@csstools/css-color-parser": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
- "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@csstools/color-helpers": "^6.0.2",
- "@csstools/css-calc": "^3.2.1"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@csstools/css-parser-algorithms": "^4.0.0",
- "@csstools/css-tokenizer": "^4.0.0"
- }
- },
- "node_modules/@csstools/css-parser-algorithms": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
- "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@csstools/css-tokenizer": "^4.0.0"
- }
- },
- "node_modules/@csstools/css-syntax-patches-for-csstree": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
- "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT-0",
- "peerDependencies": {
- "css-tree": "^3.2.1"
- },
- "peerDependenciesMeta": {
- "css-tree": {
- "optional": true
- }
- }
- },
- "node_modules/@csstools/css-tokenizer": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
- "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/csstools"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/csstools"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=20.19.0"
- }
- },
- "node_modules/@dnd-kit/accessibility": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
- "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- }
- },
- "node_modules/@dnd-kit/core": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
- "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
- "license": "MIT",
- "dependencies": {
- "@dnd-kit/accessibility": "^3.1.1",
- "@dnd-kit/utilities": "^3.2.2",
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
- }
- },
- "node_modules/@dnd-kit/sortable": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
- "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
- "license": "MIT",
- "dependencies": {
- "@dnd-kit/utilities": "^3.2.2",
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "@dnd-kit/core": "^6.3.0",
- "react": ">=16.8.0"
- }
- },
- "node_modules/@dnd-kit/utilities": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
- "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- }
- },
- "node_modules/@docsearch/css": {
- "version": "3.8.2",
- "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz",
- "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@docsearch/js": {
- "version": "3.8.2",
- "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz",
- "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@docsearch/react": "3.8.2",
- "preact": "^10.0.0"
- }
- },
- "node_modules/@docsearch/js/node_modules/@docsearch/react": {
- "version": "3.8.2",
- "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz",
- "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@algolia/autocomplete-core": "1.17.7",
- "@algolia/autocomplete-preset-algolia": "1.17.7",
- "@docsearch/css": "3.8.2",
- "algoliasearch": "^5.14.2"
- },
- "peerDependencies": {
- "@types/react": ">= 16.8.0 < 19.0.0",
- "react": ">= 16.8.0 < 19.0.0",
- "react-dom": ">= 16.8.0 < 19.0.0",
- "search-insights": ">= 1 < 3"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "search-insights": {
- "optional": true
- }
- }
- },
- "node_modules/@emnapi/core": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
- "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
- "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
+ "node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
- "ppc64"
+ "s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "aix"
+ "linux"
],
"engines": {
"node": ">=12"
}
},
- "node_modules/@esbuild/android-arm": {
+ "node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
- "arm"
+ "x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "android"
+ "linux"
],
"engines": {
"node": ">=12"
}
},
- "node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
@@ -1687,16 +1301,17 @@
"license": "MIT",
"optional": true,
"os": [
- "android"
+ "netbsd"
],
+ "peer": true,
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/@esbuild/android-x64": {
+ "node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
@@ -1704,16 +1319,16 @@
"license": "MIT",
"optional": true,
"os": [
- "android"
+ "netbsd"
],
"engines": {
"node": ">=12"
}
},
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
@@ -1721,50 +1336,17 @@
"license": "MIT",
"optional": true,
"os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
+ "openbsd"
],
+ "peer": true,
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/@esbuild/freebsd-x64": {
+ "node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
@@ -1772,33 +1354,16 @@
"license": "MIT",
"optional": true,
"os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
+ "openbsd"
],
"engines": {
"node": ">=12"
}
},
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
@@ -1806,163 +1371,11 @@
"license": "MIT",
"optional": true,
"os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
+ "openharmony"
],
+ "peer": true,
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
@@ -2090,9 +1503,9 @@
"license": "MIT"
},
"node_modules/@iconify-json/simple-icons": {
- "version": "1.2.78",
- "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.78.tgz",
- "integrity": "sha512-I3lkNp0Qu7q2iZWkdcf/I2hqGhzK6qxdILh9T7XqowQrnpmG/BayDsiCf6PktDoWlW0U971xA5g+panm+NFrfQ==",
+ "version": "1.2.87",
+ "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.87.tgz",
+ "integrity": "sha512-8YciStObhSji3OZFmWAWK6kBujyqO5bLCxeDwLxf3CR3F4PVelq7keC2LBvgTqviWzSTysj5/g4PCFLiAMVGsw==",
"dev": true,
"license": "CC0-1.0",
"dependencies": {
@@ -2120,7 +1533,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -2142,7 +1554,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -2152,14 +1563,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2198,7 +1607,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -2212,7 +1620,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -2222,7 +1629,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -2233,9 +1639,9 @@
}
},
"node_modules/@oxc-project/types": {
- "version": "0.133.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
- "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
+ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2243,13 +1649,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
- "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
+ "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.60.0"
+ "playwright": "1.61.1"
},
"bin": {
"playwright": "cli.js"
@@ -2276,24 +1682,64 @@
"protolabs-sync-assets": "bin/sync-assets.mjs"
}
},
- "node_modules/@protolabsai/vitepress-theme": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/@protolabsai/vitepress-theme/-/vitepress-theme-0.3.6.tgz",
- "integrity": "sha512-ggYOh7+e6lHWEaIVgZmFswzMpJGgdEXB7ZLkblev9gD4NhfCwr0qFUd/GBU7XGq+STxXegB1JKjwnp+N32AuuA==",
- "dev": true,
+ "node_modules/@protolabsai/ui": {
+ "version": "0.48.1",
+ "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.48.1.tgz",
+ "integrity": "sha512-ZjnYzurmN/vny/L7peKUDI/fSvAwSnHt14Jf4wGp5rVLbpbiNEdIuHRnVj6q3wZk4KNh777tEeNdxtbXOKRy2Q==",
"license": "MIT",
"dependencies": {
- "@protolabsai/design": "0.7.0"
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@protolabsai/design": "0.7.0",
+ "@radix-ui/react-dropdown-menu": "^2.1.17",
+ "@radix-ui/react-popover": "^1.1.16",
+ "@radix-ui/react-tooltip": "^1.2.10",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "culori": "^4.0.2",
+ "framer-motion": "^11.18.0",
+ "rehype-katex": "^7.0.1",
+ "remark-math": "^6.0.0"
},
"peerDependencies": {
- "vitepress": ">=1.0.0"
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "streamdown": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "streamdown": {
+ "optional": true
+ }
}
},
- "node_modules/@protolabsai/vitepress-theme/node_modules/@protolabsai/design": {
+ "node_modules/@protolabsai/ui/node_modules/@protolabsai/design": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@protolabsai/design/-/design-0.7.0.tgz",
"integrity": "sha512-QF/SShqPhP9SFPFiBnJkjOyp8/bbP07xi6dVtqZDkPb4U8QwnUtL1ld88xEZMa3SvY6GX2R53fa+JCgSiz3HGw==",
- "dev": true,
+ "license": "MIT",
+ "bin": {
+ "protolabs-sync-assets": "bin/sync-assets.mjs"
+ }
+ },
+ "node_modules/@protolabsai/vitepress-theme": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@protolabsai/vitepress-theme/-/vitepress-theme-0.3.6.tgz",
+ "integrity": "sha512-ggYOh7+e6lHWEaIVgZmFswzMpJGgdEXB7ZLkblev9gD4NhfCwr0qFUd/GBU7XGq+STxXegB1JKjwnp+N32AuuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@protolabsai/design": "0.7.0"
+ },
+ "peerDependencies": {
+ "vitepress": ">=1.0.0"
+ }
+ },
+ "node_modules/@protolabsai/vitepress-theme/node_modules/@protolabsai/design": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@protolabsai/design/-/design-0.7.0.tgz",
+ "integrity": "sha512-QF/SShqPhP9SFPFiBnJkjOyp8/bbP07xi6dVtqZDkPb4U8QwnUtL1ld88xEZMa3SvY6GX2R53fa+JCgSiz3HGw==",
+ "dev": true,
"license": "MIT",
"bin": {
"protolabs-sync-assets": "bin/sync-assets.mjs"
@@ -2305,6 +1751,55 @@
"integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==",
"license": "MIT"
},
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz",
+ "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz",
+ "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz",
@@ -2350,47 +1845,67 @@
}
}
},
- "node_modules/@radix-ui/react-focus-guards": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
- "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==",
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz",
+ "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-id": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
- "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==",
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.18.tgz",
+ "integrity": "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.2"
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.18",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-slot": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.5.tgz",
- "integrity": "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==",
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz",
+ "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==",
"license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "1.1.3"
- },
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@@ -2401,28 +1916,37 @@
}
}
},
- "node_modules/@radix-ui/react-use-callback-ref": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
- "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==",
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz",
+ "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==",
"license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-use-controllable-state": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz",
- "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==",
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz",
+ "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-effect-event": "0.0.3",
"@radix-ui/react-use-layout-effect": "1.1.2"
},
"peerDependencies": {
@@ -2435,103 +1959,422 @@
}
}
},
- "node_modules/@radix-ui/react-use-effect-event": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz",
- "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==",
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.18.tgz",
+ "integrity": "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-layout-effect": "1.1.2"
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-use-escape-keydown": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz",
- "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==",
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz",
+ "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-use-callback-ref": "1.1.2"
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-use-layout-effect": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
- "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==",
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz",
+ "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==",
"license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-rect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/rect": "1.1.2"
+ },
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-use-rect": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz",
- "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==",
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz",
+ "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==",
"license": "MIT",
"dependencies": {
- "@radix-ui/rect": "1.1.2"
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/react-use-size": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz",
- "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==",
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz",
+ "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
- "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
}
}
},
- "node_modules/@radix-ui/rect": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz",
- "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
- "license": "MIT"
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz",
+ "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz",
+ "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz",
+ "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz",
+ "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-visually-hidden": "1.2.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz",
+ "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz",
+ "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz",
+ "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz",
+ "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz",
+ "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz",
+ "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz",
+ "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz",
+ "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz",
+ "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
+ "license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
- "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.2.tgz",
+ "integrity": "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==",
"cpu": [
"arm64"
],
@@ -2546,9 +2389,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
- "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.2.tgz",
+ "integrity": "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==",
"cpu": [
"arm64"
],
@@ -2563,9 +2406,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
- "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.2.tgz",
+ "integrity": "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==",
"cpu": [
"x64"
],
@@ -2580,9 +2423,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
- "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.2.tgz",
+ "integrity": "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==",
"cpu": [
"x64"
],
@@ -2597,9 +2440,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
- "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.2.tgz",
+ "integrity": "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==",
"cpu": [
"arm"
],
@@ -2614,9 +2457,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
- "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.2.tgz",
+ "integrity": "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==",
"cpu": [
"arm64"
],
@@ -2631,9 +2474,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
- "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.2.tgz",
+ "integrity": "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==",
"cpu": [
"arm64"
],
@@ -2648,9 +2491,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
- "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.2.tgz",
+ "integrity": "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==",
"cpu": [
"ppc64"
],
@@ -2665,9 +2508,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
- "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.2.tgz",
+ "integrity": "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==",
"cpu": [
"s390x"
],
@@ -2682,9 +2525,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
- "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.2.tgz",
+ "integrity": "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==",
"cpu": [
"x64"
],
@@ -2699,9 +2542,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
- "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.2.tgz",
+ "integrity": "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==",
"cpu": [
"x64"
],
@@ -2716,9 +2559,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
- "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.2.tgz",
+ "integrity": "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==",
"cpu": [
"arm64"
],
@@ -2733,9 +2576,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
- "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.2.tgz",
+ "integrity": "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==",
"cpu": [
"wasm32"
],
@@ -2743,18 +2586,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/core": "1.10.0",
- "@emnapi/runtime": "1.10.0",
- "@napi-rs/wasm-runtime": "^1.1.4"
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
- "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.2.tgz",
+ "integrity": "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==",
"cpu": [
"arm64"
],
@@ -2769,9 +2612,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
- "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.2.tgz",
+ "integrity": "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==",
"cpu": [
"x64"
],
@@ -2793,9 +2636,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
- "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
"cpu": [
"arm"
],
@@ -2807,9 +2650,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
- "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
"cpu": [
"arm64"
],
@@ -2821,9 +2664,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
- "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
"cpu": [
"arm64"
],
@@ -2835,9 +2678,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
- "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
"cpu": [
"x64"
],
@@ -2849,9 +2692,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
- "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
"cpu": [
"arm64"
],
@@ -2863,9 +2706,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
- "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
"cpu": [
"x64"
],
@@ -2877,9 +2720,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
- "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
"cpu": [
"arm"
],
@@ -2891,9 +2734,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
- "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
"cpu": [
"arm"
],
@@ -2905,9 +2748,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
- "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
"cpu": [
"arm64"
],
@@ -2919,9 +2762,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
- "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
"cpu": [
"arm64"
],
@@ -2933,9 +2776,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
- "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
"cpu": [
"loong64"
],
@@ -2947,9 +2790,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
- "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
"cpu": [
"loong64"
],
@@ -2961,9 +2804,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
- "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
"cpu": [
"ppc64"
],
@@ -2975,9 +2818,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
- "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
"cpu": [
"ppc64"
],
@@ -2989,9 +2832,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
- "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
"cpu": [
"riscv64"
],
@@ -3003,9 +2846,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
- "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
"cpu": [
"riscv64"
],
@@ -3017,9 +2860,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
- "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
"cpu": [
"s390x"
],
@@ -3031,9 +2874,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
- "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
"cpu": [
"x64"
],
@@ -3045,9 +2888,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
- "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
"cpu": [
"x64"
],
@@ -3059,9 +2902,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
- "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
"cpu": [
"x64"
],
@@ -3073,9 +2916,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
- "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
"cpu": [
"arm64"
],
@@ -3087,9 +2930,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
- "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
"cpu": [
"arm64"
],
@@ -3101,9 +2944,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
- "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
"cpu": [
"ia32"
],
@@ -3115,9 +2958,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
- "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
"cpu": [
"x64"
],
@@ -3129,9 +2972,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
- "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
"cpu": [
"x64"
],
@@ -3237,9 +3080,9 @@
"license": "MIT"
},
"node_modules/@tanstack/query-core": {
- "version": "5.100.14",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
- "integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
+ "version": "5.101.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.1.tgz",
+ "integrity": "sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3247,12 +3090,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.100.14",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
- "integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
+ "version": "5.101.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.1.tgz",
+ "integrity": "sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.100.14"
+ "@tanstack/query-core": "5.101.1"
},
"funding": {
"type": "github",
@@ -3263,9 +3106,9 @@
}
},
"node_modules/@tauri-apps/cli": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz",
- "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.3.tgz",
+ "integrity": "sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
@@ -3279,23 +3122,23 @@
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
- "@tauri-apps/cli-darwin-arm64": "2.11.2",
- "@tauri-apps/cli-darwin-x64": "2.11.2",
- "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2",
- "@tauri-apps/cli-linux-arm64-gnu": "2.11.2",
- "@tauri-apps/cli-linux-arm64-musl": "2.11.2",
- "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2",
- "@tauri-apps/cli-linux-x64-gnu": "2.11.2",
- "@tauri-apps/cli-linux-x64-musl": "2.11.2",
- "@tauri-apps/cli-win32-arm64-msvc": "2.11.2",
- "@tauri-apps/cli-win32-ia32-msvc": "2.11.2",
- "@tauri-apps/cli-win32-x64-msvc": "2.11.2"
+ "@tauri-apps/cli-darwin-arm64": "2.11.3",
+ "@tauri-apps/cli-darwin-x64": "2.11.3",
+ "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.3",
+ "@tauri-apps/cli-linux-arm64-gnu": "2.11.3",
+ "@tauri-apps/cli-linux-arm64-musl": "2.11.3",
+ "@tauri-apps/cli-linux-riscv64-gnu": "2.11.3",
+ "@tauri-apps/cli-linux-x64-gnu": "2.11.3",
+ "@tauri-apps/cli-linux-x64-musl": "2.11.3",
+ "@tauri-apps/cli-win32-arm64-msvc": "2.11.3",
+ "@tauri-apps/cli-win32-ia32-msvc": "2.11.3",
+ "@tauri-apps/cli-win32-x64-msvc": "2.11.3"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz",
- "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.3.tgz",
+ "integrity": "sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew==",
"cpu": [
"arm64"
],
@@ -3310,9 +3153,9 @@
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz",
- "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.3.tgz",
+ "integrity": "sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg==",
"cpu": [
"x64"
],
@@ -3327,9 +3170,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz",
- "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.3.tgz",
+ "integrity": "sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA==",
"cpu": [
"arm"
],
@@ -3344,9 +3187,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz",
- "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.3.tgz",
+ "integrity": "sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA==",
"cpu": [
"arm64"
],
@@ -3361,9 +3204,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz",
- "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.3.tgz",
+ "integrity": "sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA==",
"cpu": [
"arm64"
],
@@ -3378,9 +3221,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz",
- "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.3.tgz",
+ "integrity": "sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw==",
"cpu": [
"riscv64"
],
@@ -3395,9 +3238,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz",
- "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.3.tgz",
+ "integrity": "sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA==",
"cpu": [
"x64"
],
@@ -3412,9 +3255,9 @@
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz",
- "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.3.tgz",
+ "integrity": "sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g==",
"cpu": [
"x64"
],
@@ -3429,9 +3272,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz",
- "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.3.tgz",
+ "integrity": "sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w==",
"cpu": [
"arm64"
],
@@ -3446,9 +3289,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz",
- "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.3.tgz",
+ "integrity": "sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg==",
"cpu": [
"ia32"
],
@@ -3463,9 +3306,9 @@
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz",
- "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==",
+ "version": "2.11.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.3.tgz",
+ "integrity": "sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww==",
"cpu": [
"x64"
],
@@ -3480,9 +3323,9 @@
}
},
"node_modules/@tybys/wasm-util": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
- "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -3816,9 +3659,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
"node_modules/@types/estree-jsx": {
@@ -3845,6 +3688,12 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/katex": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz",
+ "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==",
+ "license": "MIT"
+ },
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
@@ -3890,19 +3739,28 @@
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
"node_modules/@types/react": {
- "version": "18.3.29",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz",
- "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==",
- "dev": true,
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"license": "MIT",
"dependencies": {
- "@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -3924,9 +3782,9 @@
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
- "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz",
+ "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==",
"license": "ISC"
},
"node_modules/@upsetjs/venn.js": {
@@ -3975,16 +3833,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
- "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.8",
- "@vitest/utils": "4.1.8",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -3993,9 +3851,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
- "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4006,13 +3864,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
- "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.8",
+ "@vitest/utils": "4.1.9",
"pathe": "^2.0.3"
},
"funding": {
@@ -4020,14 +3878,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
- "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.8",
- "@vitest/utils": "4.1.8",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -4036,9 +3894,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
- "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -4046,13 +3904,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
- "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.8",
+ "@vitest/pretty-format": "4.1.9",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -4061,57 +3919,70 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
- "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
+ "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.2",
- "@vue/shared": "3.5.32",
+ "@babel/parser": "^7.29.7",
+ "@vue/shared": "3.5.38",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
+ "node_modules/@vue/compiler-core/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/@vue/compiler-dom": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
- "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
+ "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-core": "3.5.38",
+ "@vue/shared": "3.5.38"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
- "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
+ "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.2",
- "@vue/compiler-core": "3.5.32",
- "@vue/compiler-dom": "3.5.32",
- "@vue/compiler-ssr": "3.5.32",
- "@vue/shared": "3.5.32",
+ "@babel/parser": "^7.29.7",
+ "@vue/compiler-core": "3.5.38",
+ "@vue/compiler-dom": "3.5.38",
+ "@vue/compiler-ssr": "3.5.38",
+ "@vue/shared": "3.5.38",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.15",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
- "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
+ "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-dom": "3.5.38",
+ "@vue/shared": "3.5.38"
}
},
"node_modules/@vue/devtools-api": {
@@ -4151,57 +4022,57 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
- "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
+ "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.32"
+ "@vue/shared": "3.5.38"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
- "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
+ "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/reactivity": "3.5.38",
+ "@vue/shared": "3.5.38"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
- "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
+ "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.32",
- "@vue/runtime-core": "3.5.32",
- "@vue/shared": "3.5.32",
+ "@vue/reactivity": "3.5.38",
+ "@vue/runtime-core": "3.5.38",
+ "@vue/shared": "3.5.38",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
- "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
+ "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-ssr": "3.5.38",
+ "@vue/shared": "3.5.38"
},
"peerDependencies": {
- "vue": "3.5.32"
+ "vue": "3.5.38"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
- "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
+ "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
"dev": true,
"license": "MIT"
},
@@ -4312,26 +4183,26 @@
}
},
"node_modules/algoliasearch": {
- "version": "5.53.0",
- "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.53.0.tgz",
- "integrity": "sha512-OGW1q6b91CRSSeiOnM8LxuR5NYJ2esvw66jUZ4IIvdv+ItNkx3pwLuyR+jaCdbGee4ov5WgUnyPryyh11xvByQ==",
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz",
+ "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@algolia/abtesting": "1.19.0",
- "@algolia/client-abtesting": "5.53.0",
- "@algolia/client-analytics": "5.53.0",
- "@algolia/client-common": "5.53.0",
- "@algolia/client-insights": "5.53.0",
- "@algolia/client-personalization": "5.53.0",
- "@algolia/client-query-suggestions": "5.53.0",
- "@algolia/client-search": "5.53.0",
- "@algolia/ingestion": "1.53.0",
- "@algolia/monitoring": "1.53.0",
- "@algolia/recommend": "5.53.0",
- "@algolia/requester-browser-xhr": "5.53.0",
- "@algolia/requester-fetch": "5.53.0",
- "@algolia/requester-node-http": "5.53.0"
+ "@algolia/abtesting": "1.21.1",
+ "@algolia/client-abtesting": "5.55.1",
+ "@algolia/client-analytics": "5.55.1",
+ "@algolia/client-common": "5.55.1",
+ "@algolia/client-insights": "5.55.1",
+ "@algolia/client-personalization": "5.55.1",
+ "@algolia/client-query-suggestions": "5.55.1",
+ "@algolia/client-search": "5.55.1",
+ "@algolia/ingestion": "1.55.1",
+ "@algolia/monitoring": "1.55.1",
+ "@algolia/recommend": "5.55.1",
+ "@algolia/requester-browser-xhr": "5.55.1",
+ "@algolia/requester-fetch": "5.55.1",
+ "@algolia/requester-node-http": "5.55.1"
},
"engines": {
"node": ">= 14.0.0"
@@ -4341,14 +4212,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -4362,7 +4231,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true,
"license": "MIT"
},
"node_modules/aria-hidden": {
@@ -4388,9 +4256,9 @@
}
},
"node_modules/autoprefixer": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
- "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "version": "10.5.1",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.1.tgz",
+ "integrity": "sha512-jwM2pcTuCWUoN70FEvf5XrXyDbUgRURK4FnU8v0jWZZYU/KkVvN9T33mu1sVLFY9JW3kTWzKheEpn6xYLRc/VA==",
"dev": true,
"funding": [
{
@@ -4408,8 +4276,8 @@
],
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.2",
- "caniuse-lite": "^1.0.30001787",
+ "browserslist": "^4.28.4",
+ "caniuse-lite": "^1.0.30001799",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
@@ -4435,9 +4303,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.32",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
- "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
+ "version": "2.10.38",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
+ "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4461,7 +4329,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -4484,7 +4351,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -4494,9 +4360,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
- "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "version": "4.28.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
+ "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
"dev": true,
"funding": [
{
@@ -4514,10 +4380,10 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.12",
- "caniuse-lite": "^1.0.30001782",
- "electron-to-chromium": "^1.5.328",
- "node-releases": "^2.0.36",
+ "baseline-browser-mapping": "^2.10.38",
+ "caniuse-lite": "^1.0.30001799",
+ "electron-to-chromium": "^1.5.376",
+ "node-releases": "^2.0.48",
"update-browserslist-db": "^1.2.3"
},
"bin": {
@@ -4531,16 +4397,15 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001793",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
- "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"dev": true,
"funding": [
{
@@ -4622,7 +4487,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -4647,7 +4511,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -4688,13 +4551,12 @@
}
},
"node_modules/commander": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
- "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
- "node": ">= 6"
+ "node": ">= 12"
}
},
"node_modules/convert-source-map": {
@@ -4747,7 +4609,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -5387,14 +5248,12 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true,
"license": "MIT"
},
"node_modules/dompurify": {
@@ -5407,9 +5266,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.364",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz",
- "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==",
+ "version": "1.5.378",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz",
+ "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==",
"dev": true,
"license": "ISC"
},
@@ -5421,13 +5280,13 @@
"license": "MIT"
},
"node_modules/entities": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
- "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
- "node": ">=0.12"
+ "node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
@@ -5437,7 +5296,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5558,7 +5416,6 @@
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -5575,7 +5432,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -5588,7 +5444,6 @@
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
- "dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -5598,7 +5453,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -5659,10 +5513,9 @@
}
},
"node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -5677,7 +5530,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -5706,7 +5558,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -5725,7 +5576,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5734,6 +5584,79 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-from-dom": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
+ "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
+ "license": "ISC",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hastscript": "^9.0.0",
+ "web-namespaces": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-html": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
+ "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.1.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "parse5": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-html-isomorphic": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
+ "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-from-dom": "^5.0.0",
+ "hast-util-from-html": "^2.0.0",
+ "unist-util-remove-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-html/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/hast-util-from-html/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/hast-util-from-parse5": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
@@ -6074,7 +5997,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -6087,7 +6009,6 @@
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.3"
@@ -6113,7 +6034,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6123,7 +6043,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -6146,7 +6065,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -6188,7 +6106,6 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
- "dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -6294,15 +6211,6 @@
"katex": "cli.js"
}
},
- "node_modules/katex/node_modules/commander": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/khroma": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
@@ -6357,6 +6265,7 @@
"os": [
"android"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6378,6 +6287,7 @@
"os": [
"darwin"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6399,6 +6309,7 @@
"os": [
"darwin"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6420,6 +6331,7 @@
"os": [
"freebsd"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6441,6 +6353,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6462,6 +6375,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6483,6 +6397,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6504,6 +6419,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6525,6 +6441,7 @@
"os": [
"linux"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6546,6 +6463,7 @@
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6567,6 +6485,7 @@
"os": [
"win32"
],
+ "peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -6579,7 +6498,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -6592,7 +6510,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash-es": {
@@ -6825,6 +6742,25 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdast-util-math": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
+ "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.1.0",
+ "unist-util-remove-position": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@@ -6965,7 +6901,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -7202,6 +7137,25 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/micromark-extension-math": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
+ "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/katex": "^0.16.0",
+ "devlop": "^1.0.0",
+ "katex": "^0.16.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@@ -7579,7 +7533,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -7628,7 +7581,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -7637,10 +7589,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
- "dev": true,
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"funding": [
{
"type": "github",
@@ -7656,9 +7607,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.46",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
- "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
+ "version": "2.0.48",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
+ "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7669,7 +7620,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -7679,7 +7629,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -7689,16 +7638,15 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/obug": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
- "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -7765,19 +7713,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/parse5/node_modules/entities": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
- "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=20.19.0"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
"node_modules/path-data-parser": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
@@ -7788,7 +7723,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true,
"license": "MIT"
},
"node_modules/pathe": {
@@ -7809,14 +7743,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -7829,7 +7761,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -7839,20 +7770,19 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/playwright": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
- "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.60.0"
+ "playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
@@ -7865,9 +7795,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.60.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
- "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -7877,21 +7807,6 @@
"node": ">=18"
}
},
- "node_modules/playwright/node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/points-on-curve": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
@@ -7912,7 +7827,6 @@
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -7941,7 +7855,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -7959,7 +7872,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -7985,7 +7897,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -8028,7 +7939,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -8051,10 +7961,9 @@
}
},
"node_modules/postcss-selector-parser": {
- "version": "6.1.2",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+ "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -8068,13 +7977,12 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/preact": {
- "version": "10.29.1",
- "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
- "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
+ "version": "10.29.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
+ "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -8083,9 +7991,9 @@
}
},
"node_modules/property-information": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
- "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -8106,7 +8014,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -8132,6 +8039,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
"node_modules/react-markdown": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
@@ -8242,7 +8161,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -8252,7 +8170,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -8314,6 +8231,25 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/rehype-katex": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
+ "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/katex": "^0.16.0",
+ "hast-util-from-html-isomorphic": "^2.0.0",
+ "hast-util-to-text": "^4.0.0",
+ "katex": "^0.16.0",
+ "unist-util-visit-parents": "^6.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/rehype-raw": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
@@ -8361,6 +8297,22 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remark-math": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
+ "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-math": "^3.0.0",
+ "micromark-extension-math": "^3.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -8429,7 +8381,6 @@
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -8451,7 +8402,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@@ -8472,13 +8422,13 @@
"license": "Unlicense"
},
"node_modules/rolldown": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
- "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.2.tgz",
+ "integrity": "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.133.0",
+ "@oxc-project/types": "=0.137.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -8488,21 +8438,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.3",
- "@rolldown/binding-darwin-arm64": "1.0.3",
- "@rolldown/binding-darwin-x64": "1.0.3",
- "@rolldown/binding-freebsd-x64": "1.0.3",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
- "@rolldown/binding-linux-arm64-gnu": "1.0.3",
- "@rolldown/binding-linux-arm64-musl": "1.0.3",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
- "@rolldown/binding-linux-s390x-gnu": "1.0.3",
- "@rolldown/binding-linux-x64-gnu": "1.0.3",
- "@rolldown/binding-linux-x64-musl": "1.0.3",
- "@rolldown/binding-openharmony-arm64": "1.0.3",
- "@rolldown/binding-wasm32-wasi": "1.0.3",
- "@rolldown/binding-win32-arm64-msvc": "1.0.3",
- "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ "@rolldown/binding-android-arm64": "1.1.2",
+ "@rolldown/binding-darwin-arm64": "1.1.2",
+ "@rolldown/binding-darwin-x64": "1.1.2",
+ "@rolldown/binding-freebsd-x64": "1.1.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.2",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.2",
+ "@rolldown/binding-linux-arm64-musl": "1.1.2",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.2",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.2",
+ "@rolldown/binding-linux-x64-gnu": "1.1.2",
+ "@rolldown/binding-linux-x64-musl": "1.1.2",
+ "@rolldown/binding-openharmony-arm64": "1.1.2",
+ "@rolldown/binding-wasm32-wasi": "1.1.2",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.2",
+ "@rolldown/binding-win32-x64-msvc": "1.1.2"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
@@ -8513,13 +8463,13 @@
"license": "MIT"
},
"node_modules/rollup": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
- "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.8"
+ "@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -8529,31 +8479,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.1",
- "@rollup/rollup-android-arm64": "4.60.1",
- "@rollup/rollup-darwin-arm64": "4.60.1",
- "@rollup/rollup-darwin-x64": "4.60.1",
- "@rollup/rollup-freebsd-arm64": "4.60.1",
- "@rollup/rollup-freebsd-x64": "4.60.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
- "@rollup/rollup-linux-arm64-gnu": "4.60.1",
- "@rollup/rollup-linux-arm64-musl": "4.60.1",
- "@rollup/rollup-linux-loong64-gnu": "4.60.1",
- "@rollup/rollup-linux-loong64-musl": "4.60.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
- "@rollup/rollup-linux-ppc64-musl": "4.60.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
- "@rollup/rollup-linux-riscv64-musl": "4.60.1",
- "@rollup/rollup-linux-s390x-gnu": "4.60.1",
- "@rollup/rollup-linux-x64-gnu": "4.60.1",
- "@rollup/rollup-linux-x64-musl": "4.60.1",
- "@rollup/rollup-openbsd-x64": "4.60.1",
- "@rollup/rollup-openharmony-arm64": "4.60.1",
- "@rollup/rollup-win32-arm64-msvc": "4.60.1",
- "@rollup/rollup-win32-ia32-msvc": "4.60.1",
- "@rollup/rollup-win32-x64-gnu": "4.60.1",
- "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
"fsevents": "~2.3.2"
}
},
@@ -8573,7 +8523,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -8618,6 +8567,20 @@
"node": ">=v12.22.7"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/search-insights": {
+ "version": "2.17.3",
+ "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
+ "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -8656,7 +8619,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -8766,7 +8728,6 @@
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -8785,6 +8746,15 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/superjson": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
@@ -8802,7 +8772,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -8819,9 +8788,9 @@
"license": "MIT"
},
"node_modules/tabbable": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
- "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
+ "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
"dev": true,
"license": "MIT"
},
@@ -8839,7 +8808,6 @@
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -8886,7 +8854,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -8896,7 +8863,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -8925,7 +8891,6 @@
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -8942,7 +8907,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -8960,7 +8924,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -8980,22 +8943,22 @@
}
},
"node_modules/tldts": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz",
- "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==",
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz",
+ "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tldts-core": "^7.4.2"
+ "tldts-core": "^7.4.4"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz",
- "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==",
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz",
+ "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==",
"dev": true,
"license": "MIT"
},
@@ -9003,7 +8966,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -9071,7 +9033,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
@@ -9095,9 +9056,9 @@
}
},
"node_modules/undici": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
- "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -9163,6 +9124,20 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/unist-util-remove-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
+ "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-visit": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@@ -9283,7 +9258,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
"license": "MIT"
},
"node_modules/uuid": {
@@ -9318,229 +9292,658 @@
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0",
- "vfile": "^6.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/vitepress": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
+ "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@docsearch/css": "3.8.2",
+ "@docsearch/js": "3.8.2",
+ "@iconify-json/simple-icons": "^1.2.21",
+ "@shikijs/core": "^2.1.0",
+ "@shikijs/transformers": "^2.1.0",
+ "@shikijs/types": "^2.1.0",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "@vue/devtools-api": "^7.7.0",
+ "@vue/shared": "^3.5.13",
+ "@vueuse/core": "^12.4.0",
+ "@vueuse/integrations": "^12.4.0",
+ "focus-trap": "^7.6.4",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.1.1",
+ "shiki": "^2.1.0",
+ "vite": "^5.4.14",
+ "vue": "^3.5.13"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/vfile-message": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
- "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "node_modules/vitest/node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@types/unist": "^3.0.0",
- "unist-util-stringify-position": "^4.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/unified"
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/vite": {
- "version": "5.4.21",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
- "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "node_modules/vitest/node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
"engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
- "lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- }
+ "node": ">=18"
}
},
- "node_modules/vitepress": {
- "version": "1.6.4",
- "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz",
- "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==",
+ "node_modules/vitest/node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@docsearch/css": "3.8.2",
- "@docsearch/js": "3.8.2",
- "@iconify-json/simple-icons": "^1.2.21",
- "@shikijs/core": "^2.1.0",
- "@shikijs/transformers": "^2.1.0",
- "@shikijs/types": "^2.1.0",
- "@types/markdown-it": "^14.1.2",
- "@vitejs/plugin-vue": "^5.2.1",
- "@vue/devtools-api": "^7.7.0",
- "@vue/shared": "^3.5.13",
- "@vueuse/core": "^12.4.0",
- "@vueuse/integrations": "^12.4.0",
- "focus-trap": "^7.6.4",
- "mark.js": "8.11.1",
- "minisearch": "^7.1.1",
- "shiki": "^2.1.0",
- "vite": "^5.4.14",
- "vue": "^3.5.13"
- },
- "bin": {
- "vitepress": "bin/vitepress.js"
- },
- "peerDependencies": {
- "markdown-it-mathjax3": "^4",
- "postcss": "^8"
- },
- "peerDependenciesMeta": {
- "markdown-it-mathjax3": {
- "optional": true
- },
- "postcss": {
- "optional": true
- }
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/vitest": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
- "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
+ "node_modules/vitest/node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@vitest/expect": "4.1.8",
- "@vitest/mocker": "4.1.8",
- "@vitest/pretty-format": "4.1.8",
- "@vitest/runner": "4.1.8",
- "@vitest/snapshot": "4.1.8",
- "@vitest/spy": "4.1.8",
- "@vitest/utils": "4.1.8",
- "es-module-lexer": "^2.0.0",
- "expect-type": "^1.3.0",
- "magic-string": "^0.30.21",
- "obug": "^2.1.1",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.3",
- "std-env": "^4.0.0-rc.1",
- "tinybench": "^2.9.0",
- "tinyexec": "^1.0.2",
- "tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.1.0",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
"engines": {
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@opentelemetry/api": "^1.9.0",
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.8",
- "@vitest/browser-preview": "4.1.8",
- "@vitest/browser-webdriverio": "4.1.8",
- "@vitest/coverage-istanbul": "4.1.8",
- "@vitest/coverage-v8": "4.1.8",
- "@vitest/ui": "4.1.8",
- "happy-dom": "*",
- "jsdom": "*",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser-playwright": {
- "optional": true
- },
- "@vitest/browser-preview": {
- "optional": true
- },
- "@vitest/browser-webdriverio": {
- "optional": true
- },
- "@vitest/coverage-istanbul": {
- "optional": true
- },
- "@vitest/coverage-v8": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- },
- "vite": {
- "optional": false
- }
+ "node": ">=18"
}
},
"node_modules/vitest/node_modules/@vitest/mocker": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
- "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.8",
+ "@vitest/spy": "4.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -9560,6 +9963,50 @@
}
}
},
+ "node_modules/vitest/node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
"node_modules/vitest/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -9570,6 +10017,21 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/vitest/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
@@ -9584,16 +10046,16 @@
}
},
"node_modules/vitest/node_modules/vite": {
- "version": "8.0.16",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
- "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
+ "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.15",
- "rolldown": "1.0.3",
+ "rolldown": "~1.1.2",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -9610,7 +10072,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.1.18",
+ "@vitejs/devtools": "^0.3.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
@@ -9662,17 +10124,17 @@
}
},
"node_modules/vue": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
- "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
+ "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.32",
- "@vue/compiler-sfc": "3.5.32",
- "@vue/runtime-dom": "3.5.32",
- "@vue/server-renderer": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-dom": "3.5.38",
+ "@vue/compiler-sfc": "3.5.38",
+ "@vue/runtime-dom": "3.5.38",
+ "@vue/server-renderer": "3.5.38",
+ "@vue/shared": "3.5.38"
},
"peerDependencies": {
"typescript": "*"
@@ -9820,16 +10282,6 @@
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
- },
- "packages/plugin-ui": {
- "name": "@protoagent/plugin-ui",
- "version": "0.1.0",
- "extraneous": true,
- "license": "MIT",
- "peerDependencies": {
- "react": "^19",
- "zustand": "^5"
- }
}
}
}
diff --git a/package.json b/package.json
index 24c64b46..0d49bfd8 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,13 @@
"devDependencies": {
"@protolabsai/design": "^0.3.0",
"@protolabsai/vitepress-theme": "^0.3.6",
- "@types/react": "18.3.29",
"vitepress": "^1.6.4"
+ },
+ "//overrides": "@docsearch/react (VitePress docs search) peers on react@18, which ERESOLVEs against the console's react@19; point it at the root react so we don't need `legacy-peer-deps` (which silently disables overrides). With this, the whole tree resolves a single @types/react@19 — matching @protolabsai/ui (React-19-native, ships .tsx source our tsc compiles).",
+ "overrides": {
+ "@docsearch/react": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
}
}
From ed8f94134dad5b7612160967a28df6c7977eb090 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:34:18 -0700
Subject: [PATCH 031/190] docs(changelog): native reasoning + DS markdown
renderer under [Unreleased] (#1332)
Backfill the [Unreleased] entries for #1328 (native reasoning + WorkBlock + event-loop
perf) and #1329/#1331 (DS Markdown renderer) so the next release rolls a real changelog.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ceab04f8..cccf2710 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+- **Native reasoning — the agent's thinking now streams from the model, not a forced text
+ protocol.** Dropped the ``/`` convention; the chat renders the model's
+ native `reasoning_content`, tool calls, and answer as they actually stream. An agentic
+ turn's reason→tool steps fold into one "Working… / Worked" block that tallies reasoning
+ steps, tool calls, and skill loads (hover for the breakdown) so the final answer leads;
+ the most-recent tool stays spotlighted while the turn runs. (#1328)
+- **Chat markdown now renders through the design system's `Markdown` renderer**
+ (`@protolabsai/ui`), replacing the hand-rolled pipeline. Assistant answers are
+ streaming-hardened (partial markdown never flashes broken mid-stream), with on-brand
+ code/table chrome (copy button), KaTeX math, GFM tables/task-lists, and themed mermaid
+ code blocks. (#1329, #1331)
+
+### Fixed
+- **Heavy research turns no longer wedge the server.** `web_search` and `fetch_url` ran their
+ blocking work (DuckDuckGo search, HTML parsing) directly on the event loop, so a parallel
+ `task_batch` fan-out could peg CPU and make the server unresponsive — even to cancellation.
+ Both now run off the loop, keeping the server responsive under load. (#1328)
+
## [0.68.0] - 2026-06-23
### Added
From b139f848db69f0f1b670e1e0b9fd25897f877056 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 20:37:00 -0700
Subject: [PATCH 032/190] chore: release v0.69.0 (#1333)
Co-authored-by: github-actions[bot]
---
CHANGELOG.md | 2 ++
pyproject.toml | 2 +-
sites/marketing/data/changelog.json | 9 +++++++++
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cccf2710..458cab15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.69.0] - 2026-06-24
+
### Changed
- **Native reasoning — the agent's thinking now streams from the model, not a forced text
protocol.** Dropped the ``/`` convention; the chat renders the model's
diff --git a/pyproject.toml b/pyproject.toml
index d89ead42..a0de2181 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "protoagent"
-version = "0.68.0"
+version = "0.69.0"
description = "protoAgent — LangGraph + A2A template for spawning protoLabs agents"
requires-python = ">=3.11"
diff --git a/sites/marketing/data/changelog.json b/sites/marketing/data/changelog.json
index f3194ddd..ebeb6bbd 100644
--- a/sites/marketing/data/changelog.json
+++ b/sites/marketing/data/changelog.json
@@ -1,4 +1,13 @@
[
+ {
+ "version": "v0.69.0",
+ "date": "2026-06-24",
+ "changes": [
+ "Native reasoning — the agent's thinking now streams from the model, not a forced text protocol.",
+ "Chat markdown now renders through the design system's Markdown renderer",
+ "Heavy research turns no longer wedge the server."
+ ]
+ },
{
"version": "v0.68.0",
"date": "2026-06-23",
From 79e408803d6ec8e5d2c92091d8218ad02ddd8f36 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 21:37:04 -0700
Subject: [PATCH 033/190] feat(plugins): seam for plugin-owned / chat
commands (#1334)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add registry.register_chat_command(name, handler) so a plugin can own a
user-only / control command that short-circuits the turn with a reply
— the generalized form of the core /goal. handler is
async (rest, session_id) -> str | None: a string short-circuits (model never
runs), None passes through. User-only by design (not an agent tool), so a
plugin can expose a write action the model can't trigger autonomously.
- runtime/state.py: plugin_chat_commands dict on STATE (init + reload paths).
- graph/plugins/{registry,loader}.py: register_chat_command + collection with
first-wins de-dupe; meta surfaces the tokens for the console.
- graph/slash_commands.py: find_plugin_chat_command + run_plugin_chat_command
(errors swallowed to a ⚠️ reply); slash_kind gains a "plugin_command" band
(goal > plugin command > workflow > subagent > skill); palette emits them
and the workflow loop is precedence-guarded. Resolved once here so the
dispatcher and the console palette can't drift, and import-layering holds.
- server/chat.py: both dispatch paths consult the seam after /goal, before the
still-hardcoded /issue (additive — /issue moves onto this seam in a follow-up).
- tests + plugin-registry guide + CHANGELOG.
Groundwork for extracting GitHub (incl. /issue) into a standalone plugin.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 9 ++
docs/guides/plugin-registry.md | 10 ++
graph/plugins/loader.py | 7 ++
graph/plugins/registry.py | 36 ++++++
graph/slash_commands.py | 49 ++++++++-
runtime/state.py | 1 +
server/agent_init.py | 4 +
server/chat.py | 21 ++++
tests/test_plugin_chat_commands.py | 169 +++++++++++++++++++++++++++++
9 files changed, 303 insertions(+), 3 deletions(-)
create mode 100644 tests/test_plugin_chat_commands.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 458cab15..107d2452 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- **Plugins can own `/` chat control commands** via `registry.register_chat_command(name, handler)`
+ — the generalized form of the core `/goal`. The handler is `async (rest, session_id) -> str | None`:
+ a reply string short-circuits the turn (the model never runs), `None` passes through. It is
+ **user-only by design** (not an agent tool), so a plugin can expose a write action the model can't
+ trigger autonomously. Precedence is `goal` > plugin command > workflow > subagent > skill, resolved
+ once in `graph/slash_commands.py` so the chat dispatcher and the console palette can't drift. This is
+ the seam that lets the GitHub `/issue` command move into a plugin.
+
## [0.69.0] - 2026-06-24
### Changed
diff --git a/docs/guides/plugin-registry.md b/docs/guides/plugin-registry.md
index 940e334d..7cf20a65 100644
--- a/docs/guides/plugin-registry.md
+++ b/docs/guides/plugin-registry.md
@@ -132,10 +132,20 @@ def register(registry):
registry.register_subagent(my_subagent) # a SubagentConfig
registry.register_router(my_router) # FastAPI routes at /plugins/
registry.register_mcp_server(my_factory) # a managed MCP server
+ registry.register_chat_command("issue", h) # a user-only / control command
# skills/ and workflows/ are auto-discovered — no call needed. For a
# non-standard location: registry.register_workflow_dir("recipes")
```
+`register_chat_command(name, handler)` lets a plugin **own a `/` chat
+control command** — the generalized form of the core `/goal`. The handler is
+`async (rest, session_id) -> str | None`: return a reply string to short-circuit
+the turn (the model never runs), or `None` to pass the message through. It is
+**user-only by design** — not an agent tool — so a plugin can expose a write
+action (file an issue, open a PR) that the model can't trigger autonomously. Close
+over `registry.config` to read your own settings. Precedence is `goal` > plugin
+command > workflow > subagent > skill; `goal` is reserved.
+
`skills/` and `workflows/` are **data**, so they're auto-discovered from those
conventional subdirs — no boilerplate. **Console views** (a rail icon + page) are
declared in the manifest — see [Building a plugin view](/guides/building-react-plugin-views).
diff --git a/graph/plugins/loader.py b/graph/plugins/loader.py
index 5f53468b..7ae7f5c7 100644
--- a/graph/plugins/loader.py
+++ b/graph/plugins/loader.py
@@ -41,6 +41,7 @@ class PluginLoadResult:
late_tool_factories: list = field(default_factory=list) # (all_tools, config) -> tool|list (late seam)
mcp_servers: list = field(default_factory=list) # factories: config -> entry|None (ADR 0019)
thread_id_resolver: object = None # (request_metadata, session_id) -> str (#571); last plugin wins
+ chat_commands: dict = field(default_factory=dict) # token -> handler; user-only chat control commands
meta: list[dict] = field(default_factory=list)
@@ -386,6 +387,11 @@ def load_plugins(config, *, core_tool_names: set[str] | None = None) -> PluginLo
result.embedders[name] = factory
for f in registry.mcp_servers:
result.mcp_servers.append({"plugin_id": manifest.id, "factory": f})
+ for token, handler in registry.chat_commands.items(): # user-only chat control commands
+ if token in result.chat_commands:
+ log.warning("[plugins] %s: chat command /%s collides — skipped", manifest.id, token)
+ continue
+ result.chat_commands[token] = handler
entry["loaded"] = True
entry["tools"] = [t.name for t in kept]
entry["skills"] = len(registry.skill_dirs)
@@ -393,6 +399,7 @@ def load_plugins(config, *, core_tool_names: set[str] | None = None) -> PluginLo
entry["surfaces"] = len(registry.surfaces)
entry["subagents"] = [getattr(c, "name", "?") for c in registry.subagents]
entry["mcp_servers"] = len(registry.mcp_servers)
+ entry["chat_commands"] = [f"/{t}" for t in registry.chat_commands]
result.meta.append(entry)
log.info(
"[plugins] loaded %s: %d tool(s), %d skill dir(s), %d route(s), "
diff --git a/graph/plugins/registry.py b/graph/plugins/registry.py
index 607bf7cb..fd5d7bcd 100644
--- a/graph/plugins/registry.py
+++ b/graph/plugins/registry.py
@@ -28,6 +28,8 @@ class PluginRegistry:
(``register_subagent``).
- ``mcp_servers`` — managed MCP server factories ``config -> entry|None``
injected into MCP discovery (``register_mcp_server``).
+ - ``chat_commands`` — user-only ``/`` control commands that short-circuit
+ the turn, like ``/goal`` (``register_chat_command``).
Routes mount + surfaces start **once** at process init; a config reload reuses
them — changing ``plugins.enabled`` needs a restart (ADR 0018).
@@ -60,6 +62,7 @@ def __init__(self, plugin_id: str, plugin_dir: Path, config: dict | None = None)
self.goal_hooks: list = [] # {on_achieved, on_failed} terminal reactions (ADR 0028)
self.knowledge_stores: dict = {} # name -> (config) -> KnowledgeBackend (ADR 0031)
self.embedders: dict = {} # name -> (config) -> (text -> vector) embed_fn (ADR 0031)
+ self.chat_commands: dict = {} # token -> async (rest, session_id) -> str|None (user-only control commands)
def register_tool(self, tool) -> None:
"""Expose a LangChain tool to the agent."""
@@ -73,6 +76,39 @@ def register_tools(self, tools) -> None:
for tool in tools or []:
self.register_tool(tool)
+ def register_chat_command(self, name: str, handler) -> None:
+ """Own a user-only chat control command — ``/ …`` short-circuits the
+ turn with the handler's reply, like the core ``/goal`` (and the old, now
+ plugin-owned, ``/issue``).
+
+ ``handler`` is ``async (rest: str, session_id: str) -> str | None``: ``rest``
+ is everything after the token; return the reply string to send (the turn is
+ NOT run through the agent), or ``None`` to pass the message through as a
+ normal turn. This is **user-only by design** — it is NOT an agent tool, so a
+ plugin can expose a write action (file an issue, open a PR) that the model
+ can't invoke autonomously. Read your own config in ``register()`` and close
+ over it, e.g. ``repo = self.config.get("default_repo")``.
+
+ The token is slugified + lowercased (``/Issue`` == ``/issue``). The reserved
+ core token ``goal`` is refused; a collision with a token another enabled
+ plugin already registered keeps the first and warns (resolved in the loader).
+ """
+ from graph.slash_commands import slugify_slash # intra-graph, import-safe
+
+ token = slugify_slash(name)
+ if not token or not callable(handler):
+ log.warning(
+ "[plugins] %s: register_chat_command needs a name + callable: %r / %r", self.plugin_id, name, handler
+ )
+ return
+ if token == "goal":
+ log.warning("[plugins] %s: chat command /%s is reserved — skipped", self.plugin_id, token)
+ return
+ if token in self.chat_commands:
+ log.warning("[plugins] %s: chat command /%s registered twice — keeping the first", self.plugin_id, token)
+ return
+ self.chat_commands[token] = handler
+
def emit(self, topic: str, data: dict | None = None) -> None:
"""Broadcast an event on the bus (ADR 0039) — fire-and-forget.
diff --git a/graph/slash_commands.py b/graph/slash_commands.py
index 7421551a..02be0cc8 100644
--- a/graph/slash_commands.py
+++ b/graph/slash_commands.py
@@ -51,16 +51,50 @@ def find_user_facing_skill(name: str):
return None
+def find_plugin_chat_command(name: str):
+ """The plugin-registered chat command handler whose token matches ``/``,
+ or ``None``. Tokens are slugified+lowercased at registration, so we match the
+ lowercased name (exact) and its slug (so ``/Issue`` and ``/foo_bar`` resolve a
+ ``foo-bar`` token). User-only control commands (``register_chat_command``)."""
+ commands = getattr(STATE, "plugin_chat_commands", None) or {}
+ if not name or not commands:
+ return None
+ return commands.get(name.strip().lower()) or commands.get(slugify_slash(name))
+
+
+async def run_plugin_chat_command(name: str, rest: str, session_id: str) -> str | None:
+ """Invoke the plugin chat command matching ``/`` and return its reply (the
+ dispatcher short-circuits the turn with it), or ``None`` to fall through. A
+ handler that itself returns ``None`` falls through too (it decided not to handle
+ the message); precedence still excludes a same-named workflow/skill from firing
+ because ``slash_kind`` reports ``plugin_command`` for the token. A raising handler
+ is logged + swallowed into a ``⚠️`` reply so one bad plugin can't 500 the turn."""
+ handler = find_plugin_chat_command(name)
+ if handler is None:
+ return None
+ try:
+ return await handler(rest, session_id)
+ except Exception as exc: # noqa: BLE001 — a bad plugin command must not break the turn
+ log.warning("[slash] plugin chat command /%s failed: %s", name, exc)
+ return f"⚠️ /{name} failed: {exc}"
+
+
def slash_kind(name: str) -> str | None:
"""The kind a ``/`` slash command resolves to — the SINGLE source of
precedence shared by the chat dispatcher and the console palette, so they can
- never disagree about what a token does. Reserved: ``goal``, ``issue``. Precedence:
- workflow > subagent > user-facing skill. Returns ``None`` for an unknown token.
- (Workflows/subagents match the bare name; skills match a slug.)"""
+ never disagree about what a token does. Reserved: ``goal``, ``issue`` (``issue``
+ moves to the github plugin). Precedence:
+ goal > plugin chat command > workflow > subagent > user-facing skill. Returns
+ ``None`` for an unknown token. (Plugin commands/workflows/subagents match the
+ bare name or its slug; skills match a slug.)"""
if not name:
return None
if name == "goal" or slugify_slash(name) == "goal":
return "goal"
+ if find_plugin_chat_command(name) is not None:
+ return "plugin_command"
+ # ``issue`` is a core-reserved control command today; it moves to the github
+ # plugin (resolving as ``plugin_command`` above) and this branch is removed then.
if name == "issue" or slugify_slash(name) == "issue":
return "issue"
if STATE.workflow_registry is not None and STATE.workflow_registry.get(name) is not None:
@@ -92,8 +126,17 @@ def _add(name, kind, description, usage):
seen.add(name)
cmds.append({"name": name, "kind": kind, "description": description, "usage": usage})
+ # Plugin chat commands first — they sit just below ``goal`` in precedence, so a
+ # workflow/skill of the same token must not shadow them in the palette.
+ for token, handler in (getattr(STATE, "plugin_chat_commands", None) or {}).items():
+ doc = (getattr(handler, "__doc__", "") or "").strip()
+ desc = doc.splitlines()[0] if doc else f"Run the /{token} command."
+ _add(token, "plugin_command", desc, f"/{token} …")
+
if STATE.workflow_registry is not None:
for wf in STATE.workflow_registry.list():
+ if slash_kind(wf["name"]) != "workflow": # a goal/plugin-command/issue of the same token wins
+ continue
declared = wf.get("inputs", []) or []
req = "".join(f" <{i['name']}>" for i in declared if i.get("required"))
opt = "".join(f" [{i['name']}]" for i in declared if not i.get("required"))
diff --git a/runtime/state.py b/runtime/state.py
index ba013799..5b57e112 100644
--- a/runtime/state.py
+++ b/runtime/state.py
@@ -48,6 +48,7 @@ class AppState:
plugin_late_tool_factories: list = field(default_factory=list) # (all_tools, config) -> tool|list (late seam)
plugin_workflow_dirs: list = field(default_factory=list) # *.yaml recipe dirs (ADR 0027)
plugin_a2a_skills: list = field(default_factory=list) # A2A card skills from plugins (#570)
+ plugin_chat_commands: dict = field(default_factory=dict) # token -> handler; user-only / control commands
thread_id_resolver: object = None # (request_metadata, session_id) -> str (#571)
plugin_routers: list = field(default_factory=list)
# The live FastAPI app + the (plugin_id, prefix) keys already mounted on it —
diff --git a/server/agent_init.py b/server/agent_init.py
index 9ecf9e96..790c9384 100644
--- a/server/agent_init.py
+++ b/server/agent_init.py
@@ -193,6 +193,7 @@ def _init_langgraph_agent(headless_setup: bool = False):
)
STATE.plugin_workflow_dirs = _plugins.workflow_dirs
STATE.plugin_a2a_skills = _plugins.a2a_skills # A2A card skills (#570)
+ STATE.plugin_chat_commands = _plugins.chat_commands # user-only / control commands
STATE.thread_id_resolver = _plugins.thread_id_resolver # thread_id seam (#571)
# A plugin may provide the knowledge backend (ADR 0031) — swap it in now (the
# graph compiles below with STATE.knowledge_store). Default built-in store stays
@@ -1285,6 +1286,7 @@ def _reload_langgraph_agent() -> tuple[bool, str]:
new_skills = None
new_mcp_clients, new_mcp_tools, new_mcp_meta = [], [], []
new_plugin_tools, new_plugin_skill_dirs, new_plugin_meta = [], [], []
+ new_plugin_chat_commands: dict = {} # user-only / control commands
if is_setup_complete():
try:
new_store = _build_knowledge_store(new_config)
@@ -1305,6 +1307,7 @@ def _reload_langgraph_agent() -> tuple[bool, str]:
new_plugin_tools = new_plugins.tools
new_plugin_skill_dirs = new_plugins.skill_dirs
new_plugin_meta = new_plugins.meta
+ new_plugin_chat_commands = new_plugins.chat_commands # user-only / control commands
# Plugin knowledge backend (ADR 0031) — swap before the graph rebuild.
new_store = _apply_plugin_knowledge_backend(new_config, new_store, new_plugins)
_register_plugin_subagents(new_plugins.subagents)
@@ -1372,6 +1375,7 @@ def _reload_langgraph_agent() -> tuple[bool, str]:
STATE.graph = new_graph
STATE.plugin_middleware = new_middleware # ADR 0032
STATE.plugin_late_tool_factories = new_late_tool_factories # late-tools seam
+ STATE.plugin_chat_commands = new_plugin_chat_commands # user-only / control commands
# STATE.workflow_registry / workflow_run were (re)set by the workflows plugin above.
STATE.inbox_store = new_inbox_store
# Commit the scheduler swap. start/stop are async — fire-and-forget
diff --git a/server/chat.py b/server/chat.py
index 8b1fd6c3..8cbf04f3 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -599,6 +599,7 @@ async def _run_parsed_subagent(subagent_type: str, prompt: str) -> str:
# the two can't drift (operator_api may not import server, so it can't live here).
from graph.slash_commands import ( # noqa: E402
find_user_facing_skill as _find_user_facing_skill,
+ run_plugin_chat_command as _run_plugin_chat_command,
slash_kind as _slash_kind,
)
@@ -689,6 +690,17 @@ async def _chat_langgraph_stream(
yield ("done", reply)
return
+ # Plugin-registered chat control command (/ …) short-circuits the
+ # turn with the plugin's reply — user-only, like /goal. Sits above the
+ # core /issue handler below, which moves onto this same seam in the github
+ # plugin (PR2). No plugin claims a token by default ⇒ falls through.
+ name, rest = _parse_slash_command(message)
+ if name:
+ cmd_reply = await _run_plugin_chat_command(name, rest, session_id)
+ if cmd_reply is not None:
+ yield ("done", cmd_reply)
+ return
+
# Issue control command (/issue ...) short-circuits the turn: file a
# GitHub issue. User-only by design — it is NOT an agent tool, so the
# model can't create issues autonomously (mirrors /goal).
@@ -1038,6 +1050,15 @@ async def _chat_langgraph(message: str, session_id: str, *, model: str | None =
if reply is not None:
return [{"role": "assistant", "content": reply}]
+ # Plugin-registered chat control command (/ …) short-circuits —
+ # user-only, like /goal. Above the core /issue handler (PR2 moves /issue
+ # onto this seam). No plugin claims a token by default ⇒ falls through.
+ name, rest = _parse_slash_command(message)
+ if name:
+ cmd_reply = await _run_plugin_chat_command(name, rest, session_id)
+ if cmd_reply is not None:
+ return [{"role": "assistant", "content": cmd_reply}]
+
# Issue control command (/issue ...) short-circuits — file a GitHub
# issue (user-only; never an agent tool). See the streaming path.
from tools.gh_issue import effective_default_repo, parse_issue_control
diff --git a/tests/test_plugin_chat_commands.py b/tests/test_plugin_chat_commands.py
new file mode 100644
index 00000000..01ca9e4a
--- /dev/null
+++ b/tests/test_plugin_chat_commands.py
@@ -0,0 +1,169 @@
+"""Tests for the plugin chat-command seam (register_chat_command).
+
+A plugin can own a user-only ``/`` control command that short-circuits the
+turn with a reply — the generalized form of the core ``/goal`` (and the soon
+plugin-owned ``/issue``). The seam spans the registry (collect), the loader
+(``PluginLoadResult.chat_commands`` + first-wins de-dupe), ``runtime.state`` and
+``graph.slash_commands`` (resolve + precedence + palette).
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+from graph.config import LangGraphConfig
+from graph.plugins import loader as plugin_loader
+from graph.plugins.loader import load_plugins
+from graph.plugins.registry import PluginRegistry
+
+# A plugin whose register() owns a chat command. The token is mixed-case to prove
+# it is slugified+lowercased ("Issue" -> "issue"). The handler passes through
+# (returns None) on an empty rest, else replies — mirroring a real control command.
+_CMD_PLUGIN = '''
+def register(registry):
+ async def _handler(rest, session_id):
+ """File a GitHub issue."""
+ if not rest.strip():
+ return None
+ return f"handled:{rest}:{session_id}"
+ registry.register_chat_command("Issue", _handler)
+'''
+
+
+def _make_plugin(root: Path, pid: str, *, body: str, enabled: bool = True) -> Path:
+ d = root / pid
+ d.mkdir(parents=True, exist_ok=True)
+ (d / "protoagent.plugin.yaml").write_text(
+ f"id: {pid}\nname: {pid} plugin\nversion: 0.1.0\nenabled: {'true' if enabled else 'false'}\n",
+ encoding="utf-8",
+ )
+ (d / "__init__.py").write_text(body, encoding="utf-8")
+ return d
+
+
+def _cfg(**kw):
+ return LangGraphConfig(**kw)
+
+
+# --- registry-level surface --------------------------------------------------
+
+
+def test_register_chat_command_slugifies_reserves_goal_and_dedupes() -> None:
+ reg = PluginRegistry("p", Path("/tmp"))
+
+ async def h(rest, session_id):
+ return "first"
+
+ async def h2(rest, session_id):
+ return "second"
+
+ reg.register_chat_command("My Cmd", h)
+ assert "my-cmd" in reg.chat_commands # slugified + lowercased
+
+ reg.register_chat_command("goal", h) # reserved core token — refused
+ assert "goal" not in reg.chat_commands
+
+ reg.register_chat_command("my-cmd", h2) # same token again — keep the first
+ assert reg.chat_commands["my-cmd"] is h
+
+ reg.register_chat_command("", h) # empty / no token — ignored
+ reg.register_chat_command("bad", None) # non-callable — ignored
+ assert set(reg.chat_commands) == {"my-cmd"}
+
+
+# --- loader collection -------------------------------------------------------
+
+
+def test_loader_collects_chat_command(tmp_path, monkeypatch) -> None:
+ root = tmp_path / "plugins"
+ _make_plugin(root, "cmdp", body=_CMD_PLUGIN)
+ monkeypatch.setattr(plugin_loader, "_plugin_roots", lambda config: [root])
+
+ res = load_plugins(_cfg())
+ assert set(res.chat_commands) == {"issue"} # slugified token landed
+ assert res.meta[0]["chat_commands"] == ["/issue"] # surfaced for the console
+
+
+async def test_loader_first_wins_on_collision(tmp_path, monkeypatch) -> None:
+ root = tmp_path / "plugins"
+ # Discovery is sorted by dir name, so "ap" is collected before "bp".
+ _make_plugin(root, "ap", body=_dup_plugin("A"))
+ _make_plugin(root, "bp", body=_dup_plugin("B"))
+ monkeypatch.setattr(plugin_loader, "_plugin_roots", lambda config: [root])
+
+ res = load_plugins(_cfg())
+ assert set(res.chat_commands) == {"dup"}
+ # The first plugin's handler wins; the second is dropped.
+ assert await res.chat_commands["dup"]("x", "s") == "A:x"
+
+
+def _dup_plugin(tag: str) -> str:
+ return (
+ "def register(registry):\n"
+ " async def _h(rest, session_id):\n"
+ f" return '{tag}:' + rest\n"
+ " registry.register_chat_command('dup', _h)\n"
+ )
+
+
+# --- resolution + precedence + palette (graph.slash_commands) ----------------
+
+
+async def test_resolve_run_and_passthrough(monkeypatch) -> None:
+ from graph import slash_commands as sc
+ from runtime.state import STATE
+
+ async def handler(rest, session_id):
+ """File a GitHub issue."""
+ return None if rest == "skip" else f"ok:{rest}:{session_id}"
+
+ monkeypatch.setattr(STATE, "plugin_chat_commands", {"issue": handler})
+
+ assert sc.find_plugin_chat_command("issue") is handler
+ assert sc.find_plugin_chat_command("Issue") is handler # case-insensitive
+ assert sc.slash_kind("issue") == "plugin_command"
+
+ assert await sc.run_plugin_chat_command("issue", "hello", "sess1") == "ok:hello:sess1"
+ assert await sc.run_plugin_chat_command("issue", "skip", "s") is None # handler passes through
+ assert await sc.run_plugin_chat_command("nope", "x", "s") is None # no such command
+
+
+async def test_raising_handler_is_swallowed(monkeypatch) -> None:
+ from graph import slash_commands as sc
+ from runtime.state import STATE
+
+ async def boom(rest, session_id):
+ raise RuntimeError("kaboom")
+
+ monkeypatch.setattr(STATE, "plugin_chat_commands", {"boom": boom})
+ out = await sc.run_plugin_chat_command("boom", "", "s")
+ assert out.startswith("⚠️") and "boom" in out # turn still short-circuits, no 500
+
+
+def test_precedence_over_workflow_and_palette(monkeypatch) -> None:
+ from graph import slash_commands as sc
+ from runtime.state import STATE
+
+ async def handler(rest, session_id):
+ """File a GitHub issue."""
+ return "ok"
+
+ class _WF:
+ def get(self, name):
+ return {"name": name} if name in ("issue", "deploy") else None
+
+ def list(self):
+ return [{"name": "issue", "description": "wf"}, {"name": "deploy", "description": "Deploy it"}]
+
+ monkeypatch.setattr(STATE, "plugin_chat_commands", {"issue": handler})
+ monkeypatch.setattr(STATE, "workflow_registry", _WF())
+ monkeypatch.setattr(STATE, "skills_index", None)
+
+ # A plugin command outranks a same-named workflow; an unclaimed token stays a workflow.
+ assert sc.slash_kind("issue") == "plugin_command"
+ assert sc.slash_kind("deploy") == "workflow"
+
+ inv = {c["name"]: c for c in sc.resolve_slash_commands()}
+ assert inv["issue"]["kind"] == "plugin_command" # not double-listed as a workflow
+ assert inv["deploy"]["kind"] == "workflow"
+ assert inv["issue"]["description"] == "File a GitHub issue." # from the handler docstring
From 3caba9b1c29f9bdd58748821583a17d5b79517d3 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 22:15:07 -0700
Subject: [PATCH 034/190] feat(web): "Report a bug" link in the hamburger menu
(#1335)
Adds a "Report a bug" item to the header side panel (AppDrawer), next to
Docs / Changelog / GitHub, opening the repo's new-issue chooser
(/issues/new/choose) in a new tab. A lightweight, always-present bug-report
entry point that doesn't depend on any GitHub plugin being installed.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 3 +++
apps/web/src/app/AppDrawer.tsx | 12 +++++++++++-
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 107d2452..25b4d4d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
trigger autonomously. Precedence is `goal` > plugin command > workflow > subagent > skill, resolved
once in `graph/slash_commands.py` so the chat dispatcher and the console palette can't drift. This is
the seam that lets the GitHub `/issue` command move into a plugin.
+- **"Report a bug" link in the hamburger menu** (the header side panel), next to Docs /
+ Changelog / GitHub — opens the repo's new-issue chooser in a new tab. A lightweight,
+ always-present way to file a bug, independent of any GitHub plugin.
## [0.69.0] - 2026-06-24
diff --git a/apps/web/src/app/AppDrawer.tsx b/apps/web/src/app/AppDrawer.tsx
index f3c005a7..ae3b6d38 100644
--- a/apps/web/src/app/AppDrawer.tsx
+++ b/apps/web/src/app/AppDrawer.tsx
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import type { ReactNode } from "react";
-import { BarChart3, BookOpen, Github, ScrollText, Settings2, X } from "lucide-react";
+import { BarChart3, BookOpen, Bug, Github, ScrollText, Settings2, X } from "lucide-react";
import { Button } from "@protolabsai/ui/primitives";
@@ -121,6 +121,16 @@ export function AppDrawer({
GitHub
+
+
+ Report a bug
+
From c5aee7e37f31efa5ed0d1477886db1efb1e176db Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 23:07:46 -0700
Subject: [PATCH 035/190] =?UTF-8?q?feat:=20remove=20GitHub=20from=20core?=
=?UTF-8?q?=20=E2=80=94=20it's=20a=20standalone=20plugin=20now=20(PR2)=20(?=
=?UTF-8?q?#1336)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
GitHub's tools/UI/command surface moves out of core into the standalone
protoLabsAI/github-plugin (installable, enabled per-agent). Core keeps only the
generic seams + goal-system infra.
Removed:
- tools/github_tools.py (read tools), tools/gh_issue.py (/issue logic),
operator_api/github_routes.py (+ its mount), the in-tree plugins/github shim.
- server/chat.py: the two hardcoded /issue dispatch blocks (the plugin owns /issue
via the register_chat_command seam now); graph/slash_commands.py: the reserved
`issue` token; operator_api/console_handlers.py: the hardcoded /issue palette
entry (it now arrives from the plugin as a plugin_command — fixes the duplicate
/issue in the composer's slash menu).
- graph/config.py + graph/settings_schema.py: github_repos / github_default_repo
(the github.* config section is now the plugin's, same keys).
- console: the util-bar "New issue" bug button + NewIssueDialog + issueBody, the
uiStore newIssue* state, the ChatSurface `/issue` verb branch, and the
api.ts githubConfig/createIssue methods.
- the 4 github-specific test files (they move with the plugin).
Kept in core (NOT the GitHub toolset): the `ci` goal verifier and its
tools/gh_cli.py runner — goal-system infra (ADR 0028), used to check PR/branch CI.
Gates green: ruff, lint-imports (3 contracts kept), pytest (2506 passed),
web tsc + vitest (115 passed). The console GitHub surfaces now come from the plugin.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 11 +
apps/web/src/app/App.tsx | 40 ----
apps/web/src/chat/ChatSurface.tsx | 8 -
apps/web/src/chat/NewIssueDialog.tsx | 262 -----------------------
apps/web/src/chat/issueBody.test.ts | 61 ------
apps/web/src/chat/issueBody.ts | 46 ----
apps/web/src/lib/api.ts | 28 ---
apps/web/src/state/uiStore.ts | 11 +-
graph/config.py | 11 -
graph/settings_schema.py | 19 --
graph/slash_commands.py | 10 +-
operator_api/console_handlers.py | 17 +-
operator_api/github_routes.py | 81 -------
plugins/github/__init__.py | 21 --
plugins/github/protoagent.plugin.yaml | 14 --
server/__init__.py | 6 -
server/chat.py | 39 +---
tests/test_config_io.py | 1 -
tests/test_config_roundtrip.py | 9 -
tests/test_gh_issue.py | 129 -----------
tests/test_github_plugin.py | 41 ----
tests/test_github_routes.py | 78 -------
tests/test_github_tools.py | 162 --------------
tools/gh_issue.py | 287 -------------------------
tools/github_tools.py | 294 --------------------------
25 files changed, 24 insertions(+), 1662 deletions(-)
delete mode 100644 apps/web/src/chat/NewIssueDialog.tsx
delete mode 100644 apps/web/src/chat/issueBody.test.ts
delete mode 100644 apps/web/src/chat/issueBody.ts
delete mode 100644 operator_api/github_routes.py
delete mode 100644 plugins/github/__init__.py
delete mode 100644 plugins/github/protoagent.plugin.yaml
delete mode 100644 tests/test_gh_issue.py
delete mode 100644 tests/test_github_plugin.py
delete mode 100644 tests/test_github_routes.py
delete mode 100644 tests/test_github_tools.py
delete mode 100644 tools/gh_issue.py
delete mode 100644 tools/github_tools.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25b4d4d9..a70b115d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Changelog / GitHub — opens the repo's new-issue chooser in a new tab. A lightweight,
always-present way to file a bug, independent of any GitHub plugin.
+### Removed
+- **GitHub is no longer in core — it's a standalone plugin** ([`protoLabsAI/github-plugin`](https://github.com/protoLabsAI/github-plugin)).
+ Removed the read tools (`tools/github_tools.py`), the `/issue` command logic (`tools/gh_issue.py`),
+ its REST surface (`operator_api/github_routes.py`), the in-tree `plugins/github` shim, the core
+ `github.repos`/`github.default_repo` config + settings fields, and the console's util-bar
+ "New issue" button + dialog (`NewIssueDialog`/`issueBody`). The chat `/issue` command and the
+ console GitHub surfaces now come from the plugin (install it + `plugins.enabled: [github]`); the
+ same `github.*` config keys carry over. Kept in core: the generic `ci` goal verifier and its
+ `tools/gh_cli.py` runner (goal-system infra, not the GitHub toolset). Closes the lean-core audit's
+ "GitHub → plugin" item.
+
## [0.69.0] - 2026-06-24
### Changed
diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx
index b8bd5666..76dc1147 100644
--- a/apps/web/src/app/App.tsx
+++ b/apps/web/src/app/App.tsx
@@ -29,7 +29,6 @@ import {
// (dashboards, data, comms, dev, finance, space/fleet, AI) find a fitting glyph.
Bot,
Brain,
- Bug,
Code,
Coins,
Compass,
@@ -78,7 +77,6 @@ import { ConfirmDialog, Tooltip } from "@protolabsai/ui/overlays";
import { InboxWidget } from "../inbox/InboxWidget";
import { ChatSlot } from "./ChatSlot";
import { chatStore, useAnyChatStreaming } from "../chat/chat-store";
-import { NewIssueDialog } from "../chat/NewIssueDialog";
import { KnowledgeStore } from "../knowledge/KnowledgeStore";
import { SettingsOverlay } from "../settings/SettingsOverlay";
import { AppDrawer } from "./AppDrawer";
@@ -255,9 +253,6 @@ export function App() {
const globalSettingsSection = useUI((s) => s.globalSettingsSection);
const openGlobalSettings = useUI((s) => s.openGlobalSettings);
const closeGlobalSettings = useUI((s) => s.closeGlobalSettings);
- const newIssueOpen = useUI((s) => s.newIssueOpen);
- const openNewIssue = useUI((s) => s.openNewIssue);
- const closeNewIssue = useUI((s) => s.closeNewIssue);
const [drawerOpen, setDrawerOpen] = useState(false);
const [projectPath, setProjectPath] = useLocalStorageState("protoagent.projectPath", "");
// Shell-level runtime read (ADR 0013): non-suspense useQuery so the topbar
@@ -913,19 +908,6 @@ export function App() {
- {/* File a GitHub issue (the /issue command's form). Same store flag the
- chat `/issue` slash command opens, so there's one dialog (mounted below). */}
-
- openNewIssue()}
- >
-
-
-
{/* Widgets (bottom-left): background subagents (ADR 0050 Phase 3), the
inbox, and the read-only Activity feed — each a pill with a hover info
popover + a click dialog. */}
@@ -1067,28 +1049,6 @@ export function App() {
section={globalSettingsSection}
onClose={closeGlobalSettings}
/>
- {/* New GitHub issue dialog — store-driven, so the util-bar bug action and the chat
- `/issue` slash command share one mount. On success, drop a note in the current chat. */}
- {
- const snap = chatStore.getSnapshot();
- const sid = snap.currentSessionId;
- if (!sid) return;
- const base = snap.sessions.find((s) => s.id === sid)?.messages ?? [];
- chatStore.updateMessages(sid, [
- ...base,
- {
- id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
- role: "assistant",
- content: note,
- createdAt: Date.now(),
- status: "done",
- },
- ]);
- }}
- />
>
);
}
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index 2abeb410..e303a284 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -39,7 +39,6 @@ import { ComposerModelSelect } from "./ComposerModelSelect";
import { Markdown } from "./LazyMarkdown";
import { ReasoningCard } from "./ReasoningCard";
import { WorkBlock } from "./WorkBlock";
-import { useUI } from "../state/uiStore";
import { filesFromTransfer, isLargePaste, pastedTextFile } from "./paste";
import { ToolCalls } from "./ToolCalls";
import { addToolRef, appendReasoning, appendText, toolsForGroup } from "./parts";
@@ -387,13 +386,6 @@ function ChatSessionSlot({
textareaRef.current?.focus();
return true;
}
- if (verb === "issue") {
- // Open the New-issue form dialog (mounted once in App, store-driven) instead
- // of sending. Inline `/issue …` still files directly — it only reaches
- // here when picked from the menu with no args.
- useUI.getState().openNewIssue();
- return true;
- }
return false;
}
diff --git a/apps/web/src/chat/NewIssueDialog.tsx b/apps/web/src/chat/NewIssueDialog.tsx
deleted file mode 100644
index 3025502c..00000000
--- a/apps/web/src/chat/NewIssueDialog.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
-import { DropdownSelect, Input, Textarea } from "@protolabsai/ui/forms";
-import { Dialog } from "@protolabsai/ui/overlays";
-import { Button } from "@protolabsai/ui/primitives";
-import { Bug, Github, Loader2, Send, X } from "lucide-react";
-import { useEffect, useMemo, useState } from "react";
-
-import { api } from "../lib/api";
-import { buildBody, EMPTY_FIELDS, type Fields, isComplete, type Kind } from "./issueBody";
-
-// Sentinel option that switches the repo picker to a free-text field for a
-// one-off repo not in the configured list.
-const CUSTOM_REPO = "__custom__";
-
-/**
- * The console UX for the user-only `/issue` command: a form that files a GitHub
- * issue via POST /api/github/issue (which shares the server's gate-conformance +
- * `gh` path with the chat command). Opened from the composer by picking `/issue`.
- */
-export function NewIssueDialog({
- open,
- onClose,
- onFiled,
-}: {
- open: boolean;
- onClose: () => void;
- onFiled: (note: string) => void;
-}) {
- const [kind, setKind] = useState("bug");
- const [repo, setRepo] = useState("");
- const [title, setTitle] = useState("");
- const [fields, setFields] = useState(EMPTY_FIELDS);
- const [repos, setRepos] = useState([]);
- const [defaultRepo, setDefaultRepo] = useState("");
- const [customRepo, setCustomRepo] = useState(false);
- const [ghAvailable, setGhAvailable] = useState(true);
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState(null);
-
- // Reset + prefill (repo list, default, gh availability) each time it opens.
- useEffect(() => {
- if (!open) return;
- setKind("bug");
- setTitle("");
- setFields(EMPTY_FIELDS);
- setError(null);
- setBusy(false);
- api
- .githubConfig()
- .then((c) => {
- setRepos(c.repos);
- setDefaultRepo(c.default_repo);
- setRepo(c.default_repo || c.repos[0] || "");
- // No configured repos at all → start in free-text mode.
- setCustomRepo(c.repos.length === 0 && !c.default_repo);
- setGhAvailable(c.gh_available);
- })
- .catch(() => {});
- }, [open]);
-
- // Dropdown options: the default first, then the configured list, de-duped.
- const repoOptions = useMemo(() => {
- const out: string[] = [];
- if (defaultRepo) out.push(defaultRepo);
- for (const r of repos) if (r && !out.includes(r)) out.push(r);
- return out;
- }, [repos, defaultRepo]);
-
- const set = (k: keyof Fields) => (e: { target: { value: string } }) =>
- setFields((f) => ({ ...f, [k]: e.target.value }));
-
- // Enable submit only when the issue will clear the gate (server stays source
- // of truth on submit). `isComplete` mirrors the gate's required sections.
- const canSubmit = useMemo(
- () => !busy && isComplete(kind, title, repo, fields),
- [title, repo, busy, fields, kind],
- );
-
- async function submit() {
- setBusy(true);
- setError(null);
- try {
- const res = await api.createIssue({
- title: title.trim(),
- body: buildBody(kind, fields),
- kind,
- repo: repo.trim() || undefined,
- });
- if (!res.ok) {
- setError(res.error || (res.missing ? `Missing: ${res.missing.join("; ")}` : "Couldn't file the issue."));
- return;
- }
- onFiled(`✓ Filed in ${res.repo ?? repo}: ${res.url ?? "(created)"}`);
- onClose();
- } catch (e) {
- setError(e instanceof Error ? e.message : "Request failed.");
- } finally {
- setBusy(false);
- }
- }
-
- return (
-
- New GitHub issue
- >
- }
- width="min(560px, 94vw)"
- footer={
- <>
-
- Cancel
-
-
- {busy ? : } File issue
-
- >
- }
- >
-
- {error ?
{error}
: null}
- {!ghAvailable ? (
-
- The gh CLI isn't installed on the host — filing will fail until it is.
-
- ) : null}
-
-
- Type
- setKind(v as Kind)}
- aria-label="Issue type"
- options={[
- { value: "bug", label: "Bug" },
- { value: "feature", label: "Enhancement" },
- ]}
- />
-
-
- Repo (owner/name)
- {repoOptions.length > 0 && !customRepo ? (
- {
- if (v === CUSTOM_REPO) {
- setCustomRepo(true);
- setRepo("");
- } else {
- setRepo(v);
- }
- }}
- aria-label="Repo"
- options={[
- ...repoOptions.map((r) => ({ value: r, label: r })),
- { value: CUSTOM_REPO, label: "Custom…" },
- ]}
- />
- ) : repoOptions.length > 0 ? (
- // Custom mode (a list exists): free-text + an inline × to return to
- // the dropdown — kept on the same row so the field doesn't shift.
-
- setRepo(e.target.value)}
- placeholder="owner/name"
- data-testid="issue-create-repo"
- style={{ flex: 1 }}
- />
- {
- setCustomRepo(false);
- setRepo(defaultRepo || repos[0] || "");
- }}
- style={{
- display: "inline-flex",
- background: "none",
- border: "none",
- padding: 4,
- cursor: "pointer",
- color: "var(--fg-muted)",
- }}
- >
-
-
-
- ) : (
- // No configured list at all → plain free-text (nothing to go back to).
- setRepo(e.target.value)}
- placeholder={defaultRepo || "owner/name"}
- data-testid="issue-create-repo"
- />
- )}
-
-
-
- Title
- setTitle(e.target.value)}
- placeholder={kind === "bug" ? "What's broken, in one line" : "The capability, in one line"}
- data-testid="issue-create-title"
- />
-
-
- {kind === "bug" ? "Problem / what's wrong" : "Problem / motivation"}
-
-
- {kind === "bug" ? (
- <>
-
- Steps to reproduce / evidence
-
-
-
- Expected vs. actual
-
-
- >
- ) : (
-
- Proposed direction
-
-
- )}
-
- Acceptance
-
-
-
- Refs (optional)
-
-
-
-
- );
-}
diff --git a/apps/web/src/chat/issueBody.test.ts b/apps/web/src/chat/issueBody.test.ts
deleted file mode 100644
index 8e59b116..00000000
--- a/apps/web/src/chat/issueBody.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { buildBody, EMPTY_FIELDS, type Fields, isComplete } from "./issueBody";
-
-const FULL: Fields = {
- problem: "the wheel does nothing in the modal",
- repro: "open modal, scroll",
- expected: "expected scroll; got nothing",
- proposal: "wire onWheel to the scroll container",
- acceptance: "modal body scrolls",
- refs: "#1300",
-};
-
-describe("buildBody", () => {
- it("emits the bug sections the gate requires", () => {
- const body = buildBody("bug", FULL);
- expect(body).toContain("## Problem");
- expect(body).toContain("## Steps to reproduce / evidence");
- expect(body).toContain("## Expected vs. actual");
- expect(body).toContain("## Acceptance");
- expect(body).not.toContain("## Proposed direction");
- });
-
- it("emits the feature sections the gate requires", () => {
- const body = buildBody("feature", FULL);
- expect(body).toContain("## Problem");
- expect(body).toContain("## Proposed direction");
- expect(body).toContain("## Acceptance");
- expect(body).not.toContain("## Steps to reproduce");
- });
-
- it("includes Refs only when provided", () => {
- expect(buildBody("bug", FULL)).toContain("## Refs");
- expect(buildBody("bug", { ...FULL, refs: " " })).not.toContain("## Refs");
- });
-
- it("clears the gate's 80-char floor", () => {
- const collapsed = buildBody("feature", FULL).replace(/\s+/g, " ").trim();
- expect(collapsed.length).toBeGreaterThanOrEqual(80);
- });
-});
-
-describe("isComplete", () => {
- it("requires title, repo, problem and acceptance", () => {
- expect(isComplete("bug", "", "o/r", FULL)).toBe(false);
- expect(isComplete("bug", "t", "", FULL)).toBe(false);
- expect(isComplete("bug", "t", "o/r", { ...FULL, problem: "" })).toBe(false);
- expect(isComplete("bug", "t", "o/r", { ...FULL, acceptance: "" })).toBe(false);
- });
-
- it("requires repro + expected for a bug, proposal for a feature", () => {
- expect(isComplete("bug", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a", repro: "r" })).toBe(false);
- expect(
- isComplete("bug", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a", repro: "r", expected: "e" }),
- ).toBe(true);
- expect(isComplete("feature", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a" })).toBe(false);
- expect(isComplete("feature", "t", "o/r", { ...EMPTY_FIELDS, problem: "p", acceptance: "a", proposal: "x" })).toBe(
- true,
- );
- });
-});
diff --git a/apps/web/src/chat/issueBody.ts b/apps/web/src/chat/issueBody.ts
deleted file mode 100644
index 032baf8d..00000000
--- a/apps/web/src/chat/issueBody.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-// Pure helpers for the New-issue dialog — no React, so they're unit-testable
-// without a DOM. The body is assembled with the exact `##` headings the server
-// gate checks for, so a dialog-filed issue always conforms (the same rules
-// tools.gh_issue / .github/workflows/issue-gate.yml enforce).
-
-export type Kind = "bug" | "feature";
-
-export type Fields = {
- problem: string;
- repro: string;
- expected: string;
- proposal: string;
- acceptance: string;
- refs: string;
-};
-
-export const EMPTY_FIELDS: Fields = {
- problem: "",
- repro: "",
- expected: "",
- proposal: "",
- acceptance: "",
- refs: "",
-};
-
-export function buildBody(kind: Kind, f: Fields): string {
- const parts = [`## Problem\n${f.problem.trim()}`];
- if (kind === "bug") {
- parts.push(`## Steps to reproduce / evidence\n${f.repro.trim()}`);
- parts.push(`## Expected vs. actual\n${f.expected.trim()}`);
- } else {
- parts.push(`## Proposed direction\n${f.proposal.trim()}`);
- }
- parts.push(`## Acceptance\n${f.acceptance.trim()}`);
- if (f.refs.trim()) parts.push(`## Refs\n${f.refs.trim()}`);
- return parts.join("\n\n");
-}
-
-// Mirror the gate's required sections so the dialog can enable submit only when
-// the issue will pass (the server stays the source of truth on submit).
-export function isComplete(kind: Kind, title: string, repo: string, f: Fields): boolean {
- if (!title.trim() || !repo.trim()) return false;
- if (!f.problem.trim() || !f.acceptance.trim()) return false;
- if (kind === "bug") return !!f.repro.trim() && !!f.expected.trim();
- return !!f.proposal.trim();
-}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
index 932384b0..cbe4af12 100644
--- a/apps/web/src/lib/api.ts
+++ b/apps/web/src/lib/api.ts
@@ -886,34 +886,6 @@ export const api = {
return request<{ commands: SlashCommand[] }>("/api/chat/commands");
},
- // GitHub issue creation (the /issue command's form dialog). `githubConfig`
- // prefills the dialog (default repo + whether `gh` is installed); `createIssue`
- // shares the server's tools.gh_issue path with the chat /issue command.
- githubConfig() {
- return request<{ repos: string[]; default_repo: string; gh_available: boolean }>("/api/github/config");
- },
-
- createIssue(body: {
- title: string;
- body: string;
- kind: "bug" | "feature" | "generic";
- repo?: string;
- labels?: string[];
- dry_run?: boolean;
- }) {
- return request<{
- ok: boolean;
- url?: string;
- missing?: string[];
- error?: string;
- dry_run?: boolean;
- repo?: string;
- title?: string;
- body?: string;
- labels?: string[];
- }>("/api/github/issue", { method: "POST", body });
- },
-
settingsSchema() {
return request<{ groups: SettingsGroup[] }>("/api/settings/schema");
},
diff --git a/apps/web/src/state/uiStore.ts b/apps/web/src/state/uiStore.ts
index e70cef63..89e056ec 100644
--- a/apps/web/src/state/uiStore.ts
+++ b/apps/web/src/state/uiStore.ts
@@ -68,12 +68,6 @@ type UIState = {
globalSettingsSection?: string;
openGlobalSettings: (section?: string) => void;
closeGlobalSettings: () => void;
- // New GitHub issue dialog (the /issue command's form). EPHEMERAL — partialized
- // out of persistence so a refresh never reopens it. Opened from the util-bar
- // bug action or the chat `/issue` slash command.
- newIssueOpen: boolean;
- openNewIssue: () => void;
- closeNewIssue: () => void;
rightCollapsed: boolean;
leftCollapsed: boolean;
rightWidth: number;
@@ -265,9 +259,6 @@ export const useUI = create()(
globalSettingsSection: undefined,
openGlobalSettings: (section) => set({ globalSettingsOpen: true, globalSettingsSection: section }),
closeGlobalSettings: () => set({ globalSettingsOpen: false }),
- newIssueOpen: false,
- openNewIssue: () => set({ newIssueOpen: true }),
- closeNewIssue: () => set({ newIssueOpen: false }),
rightCollapsed: false,
leftCollapsed: false,
rightWidth: 360,
@@ -364,7 +355,7 @@ export const useUI = create()(
migrate: (persisted: unknown) => migrateUiState(persisted) as never,
// The Global settings overlay is ephemeral UI state — drop it from persistence so a
// refresh never reopens it (everything else persists as before).
- partialize: ({ globalSettingsOpen: _o, globalSettingsSection: _s, newIssueOpen: _ni, ...rest }) => rest,
+ partialize: ({ globalSettingsOpen: _o, globalSettingsSection: _s, ...rest }) => rest,
},
),
);
diff --git a/graph/config.py b/graph/config.py
index 15913f98..2476864d 100644
--- a/graph/config.py
+++ b/graph/config.py
@@ -571,15 +571,6 @@ class LangGraphConfig:
# YAML ⊕ secrets overlay). A plugin reads its own via plugin_config[""].
plugin_config: dict = field(default_factory=dict)
- # GitHub repos for the user-only ``/issue`` command + its console dialog
- # (tools/gh_issue.py). ``github_repos`` is the picker list (each ``owner/name``)
- # shown as a quick-toggle dropdown; ``github_default_repo`` is the preselected
- # one (and the command's default when no ``--repo`` is given). An empty default
- # falls back to the first repo in the list. Pairs with the portfolio manager,
- # which federates over many repos/boards. UI-editable under Settings ▸ GitHub.
- github_repos: list[str] = field(default_factory=list)
- github_default_repo: str = ""
-
# Identity — captured by the setup wizard, editable via the drawer.
# ``identity_name`` falls back to the AGENT_NAME env var at runtime;
# the YAML value wins when both are set so per-fork customization
@@ -834,8 +825,6 @@ def from_dict(
tools_disabled=list(data.get("tools", {}).get("disabled", []) or []),
routing_fallback_models=data.get("routing", {}).get("fallback_models", []),
aux_model=data.get("routing", {}).get("aux_model", cls.aux_model),
- github_repos=list(data.get("github", {}).get("repos", []) or []),
- github_default_repo=data.get("github", {}).get("default_repo", cls.github_default_repo),
goal_enabled=data.get("goal", {}).get("enabled", cls.goal_enabled),
goal_max_iterations=data.get("goal", {}).get("max_iterations", cls.goal_max_iterations),
goal_no_progress_limit=data.get("goal", {}).get("no_progress_limit", cls.goal_no_progress_limit),
diff --git a/graph/settings_schema.py b/graph/settings_schema.py
index 93159ad0..d96ed7f2 100644
--- a/graph/settings_schema.py
+++ b/graph/settings_schema.py
@@ -651,25 +651,6 @@ class Field:
minimum=0,
scope="host",
),
- # ── GitHub ─────────────────────────────────────────────────────────────────
- Field(
- "github.repos",
- "github_repos",
- "Repos for /issue",
- "string_list",
- "GitHub",
- "Repos (owner/name, one per line) offered as a quick-toggle dropdown in the "
- "New-issue dialog. Pairs with the portfolio manager's many-repo setup.",
- ),
- Field(
- "github.default_repo",
- "github_default_repo",
- "Default repo for /issue",
- "string",
- "GitHub",
- "Preselected repo (owner/name) for the dialog + the /issue command's default when "
- "no --repo is given. Blank = the first repo above (or require --repo / GITHUB_DEFAULT_REPO).",
- ),
]
_BY_KEY = {f.key: f for f in FIELDS}
diff --git a/graph/slash_commands.py b/graph/slash_commands.py
index 02be0cc8..4a620fea 100644
--- a/graph/slash_commands.py
+++ b/graph/slash_commands.py
@@ -82,21 +82,17 @@ async def run_plugin_chat_command(name: str, rest: str, session_id: str) -> str
def slash_kind(name: str) -> str | None:
"""The kind a ``/`` slash command resolves to — the SINGLE source of
precedence shared by the chat dispatcher and the console palette, so they can
- never disagree about what a token does. Reserved: ``goal``, ``issue`` (``issue``
- moves to the github plugin). Precedence:
+ never disagree about what a token does. Reserved: ``goal``. Precedence:
goal > plugin chat command > workflow > subagent > user-facing skill. Returns
``None`` for an unknown token. (Plugin commands/workflows/subagents match the
- bare name or its slug; skills match a slug.)"""
+ bare name or its slug; skills match a slug.) ``/issue`` is no longer core — the
+ github plugin owns it, so it resolves as a ``plugin_command``."""
if not name:
return None
if name == "goal" or slugify_slash(name) == "goal":
return "goal"
if find_plugin_chat_command(name) is not None:
return "plugin_command"
- # ``issue`` is a core-reserved control command today; it moves to the github
- # plugin (resolving as ``plugin_command`` above) and this branch is removed then.
- if name == "issue" or slugify_slash(name) == "issue":
- return "issue"
if STATE.workflow_registry is not None and STATE.workflow_registry.get(name) is not None:
return "workflow"
try:
diff --git a/operator_api/console_handlers.py b/operator_api/console_handlers.py
index a237379a..1ff433b8 100644
--- a/operator_api/console_handlers.py
+++ b/operator_api/console_handlers.py
@@ -515,10 +515,11 @@ async def _operator_inbox_deliver(item_id: int) -> dict:
def _operator_chat_commands() -> dict:
"""Slash commands the chat understands — drives the composer autocomplete.
- The workflow/subagent/skill inventory + precedence comes from the SAME
- resolver the chat dispatcher uses (``server.chat.resolve_slash_commands``), so
- the palette can't drift from what actually runs. ``/goal`` and ``/issue``
- (server-handled control commands) are surfaced here."""
+ The workflow/subagent/skill/plugin-command inventory + precedence comes from
+ the SAME resolver the chat dispatcher uses (``server.chat.resolve_slash_commands``),
+ so the palette can't drift from what actually runs. ``/goal`` (a core
+ server-handled control command) is surfaced here; ``/issue`` is now owned by the
+ github plugin and arrives via the resolver as a ``plugin_command``."""
from graph.slash_commands import resolve_slash_commands
commands = []
@@ -531,13 +532,5 @@ def _operator_chat_commands() -> dict:
"usage": "/goal · /goal (status) · /goal clear",
}
)
- commands.append(
- {
- "name": "issue",
- "kind": "control",
- "description": "File a GitHub issue (user-only — not an agent tool).",
- "usage": "/issue --bug|--feature [--repo owner/name] [--dry-run] · then the body below",
- }
- )
commands.extend(resolve_slash_commands())
return {"commands": commands}
diff --git a/operator_api/github_routes.py b/operator_api/github_routes.py
deleted file mode 100644
index 642959ec..00000000
--- a/operator_api/github_routes.py
+++ /dev/null
@@ -1,81 +0,0 @@
-"""GitHub issue creation for the console — the write counterpart to the read-only
-``github`` plugin tools.
-
-Backs the **New issue** form dialog (the console UX for the user-only ``/issue``
-command). ``POST /api/github/issue`` files an issue through
-``tools.gh_issue.file_issue`` — the *same* gate-conformance check + ``gh`` path the
-chat ``/issue`` command uses — so the dialog and the command can never diverge.
-``GET /api/github/config`` gives the dialog its prefilled defaults.
-
-This is operator-surface only; it is deliberately not exposed to the agent as a
-tool (creating issues stays user-driven).
-"""
-
-from __future__ import annotations
-
-import logging
-
-log = logging.getLogger("protoagent.server")
-
-
-def _default_repo() -> str:
- """The configured default repo (``github.default_repo``), or ``""``."""
- from runtime.state import STATE
-
- cfg = getattr(STATE, "graph_config", None)
- return getattr(cfg, "github_default_repo", "") or ""
-
-
-def _repos() -> list[str]:
- """The configured repo picker list (``github.repos``)."""
- from runtime.state import STATE
-
- cfg = getattr(STATE, "graph_config", None)
- return [str(r).strip() for r in (getattr(cfg, "github_repos", []) or []) if str(r).strip()]
-
-
-def register_github_routes(app) -> None:
- import re
- import shutil
-
- from fastapi import Body
-
- from tools.gh_issue import IssueRequest, effective_default_repo, file_issue, labels_for, resolve_repo
-
- _REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
-
- @app.get("/api/github/config")
- async def _github_config():
- """Defaults the New-issue dialog prefills: the repo picker list, the
- effective default repo (default ⊕ first-in-list ⊕ env), and whether the
- ``gh`` CLI is installed."""
- repos = _repos()
- return {
- "repos": repos,
- "default_repo": resolve_repo(None, effective_default_repo(_default_repo(), repos)) or "",
- "gh_available": shutil.which("gh") is not None,
- }
-
- @app.post("/api/github/issue")
- async def _github_create_issue(body: dict = Body(...)):
- """File a GitHub issue from the console form. Returns the structured
- ``file_issue`` result (``{ok, url|missing|error, ...}``)."""
- kind = (body.get("kind") or "generic").lower()
- if kind not in ("bug", "feature", "generic"):
- kind = "generic"
- title = (body.get("title") or "").strip()
- issue_body = (body.get("body") or "").strip()
- repo = resolve_repo(body.get("repo"), effective_default_repo(_default_repo(), _repos()))
- labels = labels_for(kind, [str(x) for x in (body.get("labels") or [])])
- dry_run = bool(body.get("dry_run"))
-
- if not title:
- return {"ok": False, "error": "Title is required."}
- if not repo:
- return {"ok": False, "error": "No target repo — set one in Settings ▸ GitHub, or enter one."}
- if not _REPO_RE.match(repo):
- return {"ok": False, "error": f"Repo must be 'owner/name' (got {repo!r})."}
-
- return await file_issue(
- IssueRequest(title=title, body=issue_body, kind=kind, repo=repo, labels=labels, dry_run=dry_run)
- )
diff --git a/plugins/github/__init__.py b/plugins/github/__init__.py
deleted file mode 100644
index d99103cd..00000000
--- a/plugins/github/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""GitHub read tools as a first-party plugin (lean-core audit).
-
-The read-only GitHub tools (PR / issue / list-issues / commit-diff / CI runs + failures over the `gh`
-CLI) aren't universal, so they're opt-in rather than shipped in the default tool
-set. The implementation stays in ``tools/github_tools.py`` (a shared library that
-uses ``tools/gh_cli.py``); this plugin just registers it. Enable with
-``plugins: { enabled: [github] }``.
-"""
-
-from __future__ import annotations
-
-import logging
-
-log = logging.getLogger(__name__)
-
-
-def register(registry) -> None:
- from tools.github_tools import get_github_tools
-
- registry.register_tools(get_github_tools())
- log.info("[plugins] github: registered %d read tools", 6)
diff --git a/plugins/github/protoagent.plugin.yaml b/plugins/github/protoagent.plugin.yaml
deleted file mode 100644
index 4be440d2..00000000
--- a/plugins/github/protoagent.plugin.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-id: github
-name: GitHub (read tools)
-version: 0.1.0
-description: >-
- Read-only GitHub tools over the `gh` CLI — fetch a PR, an issue, list issues,
- and a commit diff. Opt-in: not every agent needs GitHub. Enable with
- `plugins: { enabled: [github] }`. Tools degrade to a readable error if `gh`
- isn't installed / authenticated.
-enabled: false
-# Declarative (surfaced in the console for transparency) — shells out to `gh`,
-# which talks to api.github.com.
-capabilities:
- network: ["api.github.com"]
- filesystem: none
diff --git a/server/__init__.py b/server/__init__.py
index 5a1154ab..c25c7caf 100644
--- a/server/__init__.py
+++ b/server/__init__.py
@@ -669,12 +669,6 @@ async def _scheduler_shutdown() -> None:
register_theme_routes(fastapi_app)
register_mcp_routes(fastapi_app)
- # GitHub issue creation (the /issue command's form dialog) — write counterpart
- # to the read-only github plugin tools; operator-surface only.
- from operator_api.github_routes import register_github_routes
-
- register_github_routes(fastapi_app)
-
# --- Telemetry (ADR 0006 Slice 2) --------------------------------------
# Per-turn cost/latency + advise-only insights (ADR 0006). Extracted to
# operator_api/telemetry_routes.py (ADR 0023 phase 3).
diff --git a/server/chat.py b/server/chat.py
index 8cbf04f3..6fdc03a3 100644
--- a/server/chat.py
+++ b/server/chat.py
@@ -691,9 +691,8 @@ async def _chat_langgraph_stream(
return
# Plugin-registered chat control command (/ …) short-circuits the
- # turn with the plugin's reply — user-only, like /goal. Sits above the
- # core /issue handler below, which moves onto this same seam in the github
- # plugin (PR2). No plugin claims a token by default ⇒ falls through.
+ # turn with the plugin's reply — user-only, like /goal (e.g. the github
+ # plugin's /issue). No plugin claims a token by default ⇒ falls through.
name, rest = _parse_slash_command(message)
if name:
cmd_reply = await _run_plugin_chat_command(name, rest, session_id)
@@ -701,22 +700,6 @@ async def _chat_langgraph_stream(
yield ("done", cmd_reply)
return
- # Issue control command (/issue ...) short-circuits the turn: file a
- # GitHub issue. User-only by design — it is NOT an agent tool, so the
- # model can't create issues autonomously (mirrors /goal).
- from tools.gh_issue import effective_default_repo, parse_issue_control
-
- issue_reply = await parse_issue_control(
- message,
- default_repo=effective_default_repo(
- getattr(STATE.graph_config, "github_default_repo", ""),
- getattr(STATE.graph_config, "github_repos", []),
- ),
- )
- if issue_reply is not None:
- yield ("done", issue_reply)
- return
-
# Workflow slash command (/ …) short-circuits the turn:
# run the recipe and return its output. Each step renders its own
# tool card (gather → angles → brief) so a multi-step workflow shows
@@ -1051,28 +1034,14 @@ async def _chat_langgraph(message: str, session_id: str, *, model: str | None =
return [{"role": "assistant", "content": reply}]
# Plugin-registered chat control command (/ …) short-circuits —
- # user-only, like /goal. Above the core /issue handler (PR2 moves /issue
- # onto this seam). No plugin claims a token by default ⇒ falls through.
+ # user-only, like /goal (e.g. the github plugin's /issue). No plugin
+ # claims a token by default ⇒ falls through to normal dispatch.
name, rest = _parse_slash_command(message)
if name:
cmd_reply = await _run_plugin_chat_command(name, rest, session_id)
if cmd_reply is not None:
return [{"role": "assistant", "content": cmd_reply}]
- # Issue control command (/issue ...) short-circuits — file a GitHub
- # issue (user-only; never an agent tool). See the streaming path.
- from tools.gh_issue import effective_default_repo, parse_issue_control
-
- issue_reply = await parse_issue_control(
- message,
- default_repo=effective_default_repo(
- getattr(STATE.graph_config, "github_default_repo", ""),
- getattr(STATE.graph_config, "github_repos", []),
- ),
- )
- if issue_reply is not None:
- return [{"role": "assistant", "content": issue_reply}]
-
# Workflow slash command (/ …) short-circuits the turn.
parsed = _parse_workflow_command(message)
if parsed is not None:
diff --git a/tests/test_config_io.py b/tests/test_config_io.py
index e70d2b46..705f8c24 100644
--- a/tests/test_config_io.py
+++ b/tests/test_config_io.py
@@ -150,7 +150,6 @@ def test_config_to_dict_mirrors_yaml_shape() -> None:
"prompt_cache",
"routing",
"telemetry",
- "github",
# Box runtime (Host layer, ADR 0047 D8).
"network",
"fleet",
diff --git a/tests/test_config_roundtrip.py b/tests/test_config_roundtrip.py
index 3b4897b6..c9676844 100644
--- a/tests/test_config_roundtrip.py
+++ b/tests/test_config_roundtrip.py
@@ -88,8 +88,6 @@ def _isolate_from_installed_plugins(monkeypatch):
"fleet_max_warm": 0,
"fleet_port_base": 7870,
"fleet_warm_grace_seconds": 0,
- "github_default_repo": "",
- "github_repos": [],
"goal_enabled": True,
"goal_eval_model": "",
"goal_max_iterations": 8,
@@ -223,10 +221,6 @@ def _isolate_from_installed_plugins(monkeypatch):
"grace_seconds": 0,
},
},
- "github": {
- "repos": [],
- "default_repo": "",
- },
"goal": {
"enabled": True,
"eval_model": "",
@@ -429,9 +423,6 @@ def _isolate_from_installed_plugins(monkeypatch):
"compaction_trigger",
"compaction_keep_messages",
"compaction_model",
- # github.*
- "github_repos",
- "github_default_repo",
# goal.*
"goal_enabled",
"goal_max_iterations",
diff --git a/tests/test_gh_issue.py b/tests/test_gh_issue.py
deleted file mode 100644
index 0c3d1b2a..00000000
--- a/tests/test_gh_issue.py
+++ /dev/null
@@ -1,129 +0,0 @@
-"""Unit tests for the user-only ``/issue`` control command (tools/gh_issue.py).
-
-Pure parse/validation logic + the create path with ``run_gh`` mocked — no
-network, no real ``gh``.
-"""
-
-from __future__ import annotations
-
-from unittest.mock import patch
-
-import pytest
-
-from tools.gh_issue import (
- effective_default_repo,
- is_issue_command,
- missing_sections,
- parse_issue_control,
-)
-
-
-def test_effective_default_repo():
- # explicit default wins; else first non-blank in the list; else ""
- assert effective_default_repo("o/r", ["a/b", "c/d"]) == "o/r"
- assert effective_default_repo("", ["a/b", "c/d"]) == "a/b"
- assert effective_default_repo("", [" ", "c/d"]) == "c/d"
- assert effective_default_repo("", []) == ""
- assert effective_default_repo(" ", None) == ""
-
-_GOOD_BUG = (
- "/issue Scroll dead in delegate modal --bug --repo o/r\n"
- "## Problem\nThe wheel does nothing inside the modal.\n\n"
- "## Steps to reproduce\n1. open modal 2. scroll\n\n"
- "## Expected vs actual\nExpected scroll; nothing happens.\n\n"
- "## Acceptance\nWheel scrolls the modal body."
-)
-
-
-def test_is_issue_command():
- assert is_issue_command("/issue foo")
- assert is_issue_command("/issue")
- assert is_issue_command("/issue\nbody")
- assert not is_issue_command("/issues foo") # not a prefix match
- assert not is_issue_command("hello /issue")
- assert not is_issue_command(None) # type: ignore[arg-type]
-
-
-def test_missing_sections_by_kind():
- body = "## Problem\nx" + " padding" * 20
- assert missing_sections(body, "generic") == []
- # bug needs repro/evidence/expected on top of Problem
- assert any("reproduce" in m.lower() for m in missing_sections(body, "bug"))
- # feature needs a proposed-direction or acceptance
- assert any("proposed" in m.lower() for m in missing_sections(body, "feature"))
- # a thin body is flagged regardless
- assert any("substantive" in m for m in missing_sections("## Problem\nx", "generic"))
-
-
-async def test_non_issue_returns_none():
- assert await parse_issue_control("just chatting") is None
- assert await parse_issue_control("/goal win") is None
-
-
-async def test_no_title_returns_usage_and_scaffold():
- out = await parse_issue_control("/issue", default_repo="o/r")
- assert "Usage:" in out and "## Problem" in out
-
-
-async def test_no_repo_errors():
- out = await parse_issue_control("/issue Title\n## Problem\n" + "x" * 80)
- assert "No target repo" in out
-
-
-async def test_bad_repo_errors():
- out = await parse_issue_control("/issue Title --repo not-a-repo\n## Problem\n" + "x" * 80)
- assert "owner/name" in out
-
-
-async def test_missing_sections_block_creation():
- out = await parse_issue_control("/issue Title --bug --repo o/r\nthis body has no headings at all here")
- assert out.startswith("Not filed")
- assert "Steps to reproduce" in out # scaffold for a bug
-
-
-async def test_dry_run_does_not_call_gh():
- # --dry-run must be on the first line (the title+flags line), not the body.
- msg = _GOOD_BUG.replace("--bug --repo o/r", "--bug --repo o/r --dry-run", 1)
- with patch("tools.gh_issue.run_gh") as run:
- out = await parse_issue_control(msg)
- run.assert_not_called()
- assert out.startswith("Dry run")
- assert "o/r" in out and "bug" in out
-
-
-async def test_happy_path_creates_and_labels():
- url = "https://github.com/o/r/issues/42"
- with patch("tools.gh_issue.run_gh", return_value=(0, url, "")) as run:
- out = await parse_issue_control(_GOOD_BUG)
- assert url in out and "Filed in o/r" in out
- args = run.call_args.args[0]
- assert args[:2] == ["issue", "create"]
- assert "--repo" in args and "o/r" in args
- assert "--label" in args and "bug" in args
-
-
-async def test_default_repo_used_when_no_flag():
- with patch("tools.gh_issue.run_gh", return_value=(0, "https://x/1", "")) as run:
- out = await parse_issue_control(
- "/issue Title --feature\n## Problem\nwhy this matters enough to file it here and now\n\n"
- "## Proposed direction\nthe approach we would take to address it",
- default_repo="acme/widgets",
- )
- assert "acme/widgets" in out
- args = run.call_args.args[0]
- assert "acme/widgets" in args
- assert "enhancement" in args # feature → enhancement label
-
-
-async def test_gh_failure_surfaces_error():
- with patch("tools.gh_issue.run_gh", return_value=(1, "", "label 'bug' not found")):
- out = await parse_issue_control(_GOOD_BUG)
- assert out.startswith("Error") or "Hint" in out
-
-
-@pytest.mark.parametrize("flag,label", [("--bug", "bug"), ("--feature", "enhancement"), ("--feat", "enhancement")])
-async def test_type_flags_map_to_labels(flag, label):
- body = "## Problem\nwhy it matters here padding padding\n\n## Acceptance\ncriteria\n\n## Steps to reproduce\n1"
- with patch("tools.gh_issue.run_gh", return_value=(0, "https://x/1", "")) as run:
- await parse_issue_control(f"/issue Title {flag} --repo o/r\n{body}")
- assert label in run.call_args.args[0]
diff --git a/tests/test_github_plugin.py b/tests/test_github_plugin.py
deleted file mode 100644
index 20759fa1..00000000
--- a/tests/test_github_plugin.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""GitHub read tools are opt-in via the first-party `github` plugin (lean-core
-audit) — not in the default tool set, registered when the plugin is enabled."""
-
-from __future__ import annotations
-
-import importlib.util
-from pathlib import Path
-
-
-def test_github_tools_not_in_default_tool_set():
- from tools.lg_tools import get_all_tools
-
- names = {t.name for t in get_all_tools()}
- assert not any(n.startswith("github_") for n in names), names
-
-
-def test_github_plugin_registers_the_read_tools():
- # plugins/ isn't a package; load the entry module by path (like the loader).
- init = Path("plugins/github/__init__.py")
- spec = importlib.util.spec_from_file_location("_test_github_plugin", init)
- mod = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(mod)
-
- class FakeRegistry:
- def __init__(self):
- self.tools = []
-
- def register_tools(self, tools):
- self.tools.extend(tools)
-
- reg = FakeRegistry()
- mod.register(reg)
- names = {t.name for t in reg.tools}
- assert names == {
- "github_get_pr",
- "github_get_issue",
- "github_list_issues",
- "github_get_commit_diff",
- "github_ci_runs",
- "github_run_failure",
- }
diff --git a/tests/test_github_routes.py b/tests/test_github_routes.py
deleted file mode 100644
index f25e3607..00000000
--- a/tests/test_github_routes.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""Console GitHub issue route (POST /api/github/issue, GET /api/github/config).
-
-Thin layer over tools.gh_issue (covered in test_gh_issue); here we check the
-HTTP contract + validation, with `gh` mocked.
-"""
-
-from __future__ import annotations
-
-from unittest.mock import patch
-
-import pytest
-
-
-@pytest.fixture
-def client():
- from fastapi import FastAPI
- from fastapi.testclient import TestClient
-
- from operator_api.github_routes import register_github_routes
-
- app = FastAPI()
- register_github_routes(app)
- return TestClient(app)
-
-
-_BODY = (
- "## Problem\nthe scroll wheel does nothing inside the delegate modal here\n\n"
- "## Steps to reproduce\nopen the modal and scroll the body region\n\n"
- "## Expected vs actual\nexpected scroll; nothing happens\n\n"
- "## Acceptance\nthe modal body scrolls on macOS and linux"
-)
-
-
-def test_config_reports_gh_availability(client):
- with patch("shutil.which", return_value="/usr/bin/gh"):
- r = client.get("/api/github/config")
- assert r.status_code == 200
- body = r.json()
- assert body["gh_available"] is True
- assert "repos" in body and isinstance(body["repos"], list)
-
-
-def test_create_happy_path(client):
- url = "https://github.com/o/r/issues/7"
- with patch("tools.gh_issue.run_gh", return_value=(0, url, "")) as run:
- r = client.post("/api/github/issue", json={"title": "Scroll dead", "body": _BODY, "kind": "bug", "repo": "o/r"})
- j = r.json()
- assert j["ok"] and j["url"] == url
- assert "bug" in run.call_args.args[0] # bug → label applied
-
-
-def test_create_rejects_missing_sections(client):
- with patch("tools.gh_issue.run_gh") as run:
- r = client.post("/api/github/issue", json={"title": "x", "body": "too thin", "kind": "bug", "repo": "o/r"})
- run.assert_not_called()
- j = r.json()
- assert j["ok"] is False and j["missing"]
-
-
-def test_create_requires_repo(client):
- r = client.post("/api/github/issue", json={"title": "x", "body": _BODY, "kind": "bug"})
- j = r.json()
- assert j["ok"] is False and "repo" in j["error"].lower()
-
-
-def test_create_rejects_bad_repo(client):
- r = client.post("/api/github/issue", json={"title": "x", "body": _BODY, "kind": "bug", "repo": "nope"})
- assert r.json()["ok"] is False
-
-
-def test_dry_run_does_not_call_gh(client):
- with patch("tools.gh_issue.run_gh") as run:
- r = client.post(
- "/api/github/issue",
- json={"title": "x", "body": _BODY, "kind": "bug", "repo": "o/r", "dry_run": True},
- )
- run.assert_not_called()
- assert r.json()["dry_run"] is True
diff --git a/tests/test_github_tools.py b/tests/test_github_tools.py
deleted file mode 100644
index c134e869..00000000
--- a/tests/test_github_tools.py
+++ /dev/null
@@ -1,162 +0,0 @@
-"""Tests for the GitHub read tools (gh CLI wrapper + tool contracts)."""
-
-import json
-from unittest.mock import patch
-
-import pytest
-
-from tools.github_tools import get_github_tools
-
-
-def _tools():
- return {t.name: t for t in get_github_tools()}
-
-
-@pytest.mark.asyncio
-async def test_repo_required_no_silent_default():
- """Every tool rejects a missing/garbage repo without calling gh."""
- tools = _tools()
- with patch("tools.github_tools.run_gh") as run:
- for name, t in tools.items():
- arg = {"repo": "", "number": 1} if "number" in t.args else {"repo": ""}
- if name == "github_list_issues":
- arg = {"repo": ""}
- if name == "github_get_commit_diff":
- arg = {"repo": "", "ref": "abc"}
- if name == "github_run_failure":
- arg = {"repo": "", "run_id": 1}
- out = await t.ainvoke(arg)
- assert out.startswith("Error:"), name
- assert "no default" in out.lower() or "owner/name" in out.lower()
- run.assert_not_called()
-
-
-@pytest.mark.asyncio
-async def test_get_pr_happy_path():
- payload = json.dumps(
- {
- "number": 7,
- "title": "Add thing",
- "state": "OPEN",
- "author": {"login": "octocat"},
- "body": "does a thing",
- "additions": 10,
- "deletions": 2,
- "files": [{"path": "a.py"}, {"path": "b.py"}],
- "url": "http://x/7",
- }
- )
- with patch("tools.github_tools.run_gh", return_value=(0, payload, "")):
- out = await _tools()["github_get_pr"].ainvoke({"repo": "o/r", "number": 7})
- assert "PR #7" in out and "Add thing" in out
- assert "octocat" in out and "+10/-2" in out
- assert "a.py" in out
-
-
-@pytest.mark.asyncio
-async def test_list_issues_empty_and_populated():
- t = _tools()["github_list_issues"]
- with patch("tools.github_tools.run_gh", return_value=(0, "[]", "")):
- assert "No open issues" in await t.ainvoke({"repo": "o/r"})
- items = json.dumps(
- [
- {"number": 1, "title": "bug", "state": "OPEN", "labels": [{"name": "p0"}]},
- {"number": 2, "title": "feat", "state": "OPEN", "labels": []},
- ]
- )
- with patch("tools.github_tools.run_gh", return_value=(0, items, "")):
- out = await t.ainvoke({"repo": "o/r", "state": "open"})
- assert "#1" in out and "p0" in out and "#2" in out
-
-
-@pytest.mark.asyncio
-async def test_list_issues_bad_state():
- out = await _tools()["github_list_issues"].ainvoke({"repo": "o/r", "state": "weird"})
- assert out.startswith("Error:") and "open|closed|all" in out
-
-
-@pytest.mark.asyncio
-async def test_gh_failure_surfaces_error_string():
- with patch("tools.github_tools.run_gh", return_value=(1, "", "not found")):
- out = await _tools()["github_get_issue"].ainvoke({"repo": "o/r", "number": 99})
- assert out.startswith("Error (gh exit 1)") and "not found" in out
-
-
-@pytest.mark.asyncio
-async def test_commit_diff_truncates():
- big = "diff --git a b\n" + ("+x\n" * 5000)
- with patch("tools.github_tools.run_gh", return_value=(0, big, "")):
- out = await _tools()["github_get_commit_diff"].ainvoke({"repo": "o/r", "ref": "deadbeef", "max_chars": 100})
- assert "truncated at 100" in out
-
-
-@pytest.mark.asyncio
-async def test_run_gh_missing_binary():
- """run_gh reports a clean error when gh isn't installed (no raise)."""
- from tools.gh_cli import run_gh
-
- with patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError):
- rc, out, err = await run_gh(["pr", "view", "1"])
- assert rc == 1 and "not installed" in err
-
-
-@pytest.mark.asyncio
-async def test_ci_runs_happy_path():
- payload = json.dumps(
- [
- {
- "databaseId": 101,
- "name": "CI",
- "status": "completed",
- "conclusion": "failure",
- "headBranch": "main",
- "event": "push",
- "createdAt": "t",
- "url": "http://x/101",
- },
- {
- "databaseId": 102,
- "name": "CI",
- "status": "completed",
- "conclusion": "success",
- "headBranch": "main",
- "event": "push",
- "createdAt": "t",
- "url": "http://x/102",
- },
- ]
- )
- with patch("tools.github_tools.run_gh", return_value=(0, payload, "")):
- out = await _tools()["github_ci_runs"].ainvoke({"repo": "o/r"})
- assert "#101 [failure]" in out and "http://x/101" in out
- assert "2 recent run(s)" in out
-
-
-@pytest.mark.asyncio
-async def test_ci_runs_empty():
- with patch("tools.github_tools.run_gh", return_value=(0, "[]", "")):
- out = await _tools()["github_ci_runs"].ainvoke({"repo": "o/r", "branch": "main"})
- assert "No recent runs" in out and "main" in out
-
-
-@pytest.mark.asyncio
-async def test_run_failure_extracts_error_lines():
- log = (
- "build\tCompile\t2026-01-01T00:00:00Z all good here\n"
- "test\tRun tests\t2026-01-01T00:00:01Z AssertionError: expected 1 to equal 2\n"
- "test\tRun tests\t2026-01-01T00:00:02Z 1 passed, 1 failed\n"
- "test\tRun tests\t2026-01-01T00:00:03Z just a normal line\n"
- )
- with patch("tools.github_tools.run_gh", return_value=(0, log, "")):
- out = await _tools()["github_run_failure"].ainvoke({"repo": "o/r", "run_id": 101})
- assert "AssertionError: expected 1 to equal 2" in out
- assert "1 passed, 1 failed" in out # matched on "fail"
- assert "all good here" not in out # non-error line filtered out
-
-
-@pytest.mark.asyncio
-async def test_run_failure_falls_back_to_tail():
- log = "job\tstep\t2026Z benign line one\njob\tstep\t2026Z benign line two\n"
- with patch("tools.github_tools.run_gh", return_value=(0, log, "")):
- out = await _tools()["github_run_failure"].ainvoke({"repo": "o/r", "run_id": 5})
- assert "benign line two" in out # tail fallback when nothing matches
diff --git a/tools/gh_issue.py b/tools/gh_issue.py
deleted file mode 100644
index 96ee6c94..00000000
--- a/tools/gh_issue.py
+++ /dev/null
@@ -1,287 +0,0 @@
-"""``/issue`` — the user-only GitHub issue-creation control command.
-
-This is the **write** counterpart to the read-only GitHub *tools*
-(``tools/github_tools.py``). Those are agent-facing; creating an issue is a
-write the agent must NOT do autonomously, so this is deliberately **not** a
-LangChain tool. Instead it's a server-handled chat control command (like
-``/goal``): the user types ``/issue …`` and the dispatcher short-circuits the
-turn — the model never sees it as something it can call.
-
-Syntax (inline, one message — a console form dialog is a planned follow-up)::
-
- /issue [--bug|--feature] [--repo owner/name] [--label a,b] [--dry-run]
-
- ## Problem
- …
- ## Steps to reproduce / Acceptance
- …
-
-The first line carries the title + flags; everything after the first newline is
-the body. The body is checked against the SAME requirements the CI issue gate
-enforces (`.github/workflows/issue-gate.yml`), so an issue filed here always
-passes the gate:
-
-- always: a non-trivial body + a Problem / What's-wrong / Motivation section;
-- ``--bug`` → label ``bug``, also needs repro / evidence / expected-vs-actual;
-- ``--feature`` → label ``enhancement``, also needs a proposed-direction or
- acceptance section.
-
-Repo resolution (no silent misrouting): explicit ``--repo`` > the configured
-``github.default_repo`` > ``GITHUB_DEFAULT_REPO`` / ``GH_REPO`` env > an error
-asking for one. Auth rides on ``tools.gh_cli`` (``GITHUB_TOKEN``/``GH_TOKEN`` or
-ambient ``gh auth``); ``gh issue create`` needs write scope.
-"""
-
-from __future__ import annotations
-
-import os
-import re
-import shlex
-from dataclasses import dataclass, field
-
-from tools.gh_cli import check_gh_error, run_gh
-
-_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
-
-# Section detectors — kept in lockstep with the CI gate's regexes so the local
-# check and the server-side gate can never disagree about what "conforms" means.
-_SECTION_RES = {
- "problem": re.compile(r"problem|what'?s? wrong|motivation|background|context|summary", re.I),
- "repro": re.compile(r"repro|reproduce|steps|evidence|expected|actual|observed", re.I),
- "proposal": re.compile(r"propos|solution|approach|direction|fix|design", re.I),
- "acceptance": re.compile(r"acceptance|done when|success criteria|definition of done", re.I),
-}
-# A heading (#..######) or a bold line (**…**) — same shape the gate matches.
-_HEADING_RE = re.compile(r"^\s*(?:#{1,6}\s+|\*\*\s*)(.+?)(?:\s*\*\*)?\s*$")
-
-_BUG_SCAFFOLD = (
- "## Problem / what's wrong\n\n\n"
- "## Steps to reproduce / evidence\n\n\n"
- "## Expected vs. actual\n\n\n"
- "## Acceptance\n\n"
-)
-_FEATURE_SCAFFOLD = (
- "## Problem / motivation\n\n\n"
- "## Proposed direction\n\n\n"
- "## Acceptance\n\n"
-)
-_GENERIC_SCAFFOLD = (
- "## Problem\n\n\n"
- "## Acceptance\n\n"
-)
-
-
-@dataclass
-class IssueRequest:
- title: str
- body: str
- kind: str # "bug" | "feature" | "generic"
- repo: str | None = None
- labels: list[str] = field(default_factory=list)
- dry_run: bool = False
-
-
-def _has_section(body: str, key: str) -> bool:
- rx = _SECTION_RES[key]
- for line in body.splitlines():
- m = _HEADING_RE.match(line)
- if m and rx.search(m.group(1)):
- return True
- return False
-
-
-def _scaffold(kind: str) -> str:
- return {"bug": _BUG_SCAFFOLD, "feature": _FEATURE_SCAFFOLD}.get(kind, _GENERIC_SCAFFOLD)
-
-
-def missing_sections(body: str, kind: str) -> list[str]:
- """The gate-required sections absent from ``body`` for this issue ``kind``."""
- miss: list[str] = []
- # Same metric as the CI gate (collapsed-whitespace length >= 80), so an issue
- # that passes here is guaranteed to clear the server-side gate.
- if len(" ".join(body.split())) < 80:
- miss.append("a substantive description (>= 80 chars)")
- if not _has_section(body, "problem"):
- miss.append("a Problem / What's-wrong / Motivation section")
- if kind == "bug" and not _has_section(body, "repro"):
- miss.append("Steps to reproduce / Evidence / Expected-vs-actual")
- if kind == "feature" and not (_has_section(body, "proposal") or _has_section(body, "acceptance")):
- miss.append("a Proposed-direction or Acceptance section")
- return miss
-
-
-def labels_for(kind: str, extra: list[str] | None = None) -> list[str]:
- """Labels for an issue of this ``kind`` — the type label first (``bug`` /
- ``enhancement``), then any extras, de-duped in order."""
- out: list[str] = []
- if kind == "bug":
- out.append("bug")
- elif kind == "feature":
- out.append("enhancement")
- for lbl in extra or []:
- if lbl and lbl not in out:
- out.append(lbl)
- return out
-
-
-def resolve_repo(explicit: str | None, default_repo: str = "") -> str | None:
- """Target repo: explicit ``--repo`` > configured default > GITHUB_DEFAULT_REPO
- / GH_REPO env > ``None`` (caller errors — there is no silent default)."""
- return (
- (explicit or "").strip()
- or (default_repo or "").strip()
- or os.environ.get("GITHUB_DEFAULT_REPO")
- or os.environ.get("GH_REPO")
- or None
- )
-
-
-def effective_default_repo(default_repo: str, repos: list[str] | None = None) -> str:
- """The preselected default repo for the dialog + the ``/issue`` command: the
- explicit ``github.default_repo`` if set, else the first entry in the
- ``github.repos`` picker list, else ``""`` (env still applies via
- ``resolve_repo``). Keeps the command and the dialog agreeing on the default."""
- if (default_repo or "").strip():
- return default_repo.strip()
- for r in repos or []:
- if (r or "").strip():
- return r.strip()
- return ""
-
-
-async def file_issue(req: IssueRequest) -> dict:
- """Validate ``req`` against the gate rules, then create the issue via ``gh``
- (or, for ``dry_run``, report what would be filed). Returns a structured
- result the chat command and the console dialog both render:
-
- - ``{"ok": False, "missing": [...], "kind": ...}`` — body fails the gate;
- - ``{"ok": False, "error": "..."}`` — ``gh`` failed (auth / label / repo);
- - ``{"ok": True, "dry_run": True, ...}`` — preview only, nothing created;
- - ``{"ok": True, "url": "...", "repo": ..., "labels": [...]}`` — created.
-
- Assumes ``req.repo`` is already set + validated (the callers do that).
- """
- miss = missing_sections(req.body, req.kind)
- if miss:
- return {"ok": False, "missing": miss, "kind": req.kind}
- if req.dry_run:
- return {
- "ok": True,
- "dry_run": True,
- "repo": req.repo,
- "title": req.title,
- "body": req.body,
- "labels": req.labels,
- }
- args = ["issue", "create", "--repo", req.repo, "--title", req.title, "--body", req.body]
- for lbl in req.labels:
- args += ["--label", lbl]
- rc, out, serr = await run_gh(args, timeout=45)
- err = check_gh_error(rc, serr)
- if err:
- if "could not add label" in serr.lower() or "not found" in serr.lower():
- err += f" (the label may not exist in {req.repo}; create it or drop the label.)"
- return {"ok": False, "error": err}
- url = out.strip().splitlines()[-1] if out.strip() else ""
- return {"ok": True, "url": url, "repo": req.repo, "labels": req.labels}
-
-
-def _parse(rest: str, *, default_repo: str = "") -> IssueRequest | str:
- """Parse the raw ``/issue`` argument string into a request, or an error string."""
- first, _, body = rest.partition("\n")
- body = body.strip()
-
- kind = "generic"
- labels: list[str] = []
- repo: str | None = None
- dry_run = False
-
- # Tokenize only the first line (title + flags); the body is taken verbatim.
- try:
- tokens = shlex.split(first)
- except ValueError:
- tokens = first.split()
-
- title_parts: list[str] = []
- i = 0
- while i < len(tokens):
- tok = tokens[i]
- low = tok.lower()
- if low in ("--bug", "--fix"):
- kind = "bug"
- elif low in ("--feature", "--feat", "--enhancement"):
- kind = "feature"
- elif low in ("--dry-run", "--dryrun"):
- dry_run = True
- elif low.startswith("--repo"):
- val = tok.split("=", 1)[1] if "=" in tok else (tokens[i + 1] if i + 1 < len(tokens) else "")
- if "=" not in tok:
- i += 1
- repo = val.strip()
- elif low.startswith("--label"):
- val = tok.split("=", 1)[1] if "=" in tok else (tokens[i + 1] if i + 1 < len(tokens) else "")
- if "=" not in tok:
- i += 1
- labels += [s.strip() for s in val.split(",") if s.strip()]
- else:
- title_parts.append(tok)
- i += 1
-
- title = " ".join(title_parts).strip()
- labels = labels_for(kind, labels)
- repo = resolve_repo(repo, default_repo)
-
- if not title:
- return (
- "Usage: `/issue [--bug|--feature] [--repo owner/name] [--dry-run]` "
- "then the body on the following lines.\n\n"
- "Scaffold to fill in:\n```\n" + _scaffold(kind) + "```"
- )
- if not repo:
- return (
- "No target repo. Pass `--repo owner/name`, or set `github.default_repo` "
- "in Settings (or the `GITHUB_DEFAULT_REPO` env var)."
- )
- if not _REPO_RE.match(repo):
- return f"Error: --repo must be 'owner/name' (got {repo!r})."
-
- return IssueRequest(title=title, body=body, kind=kind, repo=repo, labels=labels, dry_run=dry_run)
-
-
-def is_issue_command(message: str) -> bool:
- """True when ``message`` is a ``/issue`` control command."""
- if not isinstance(message, str):
- return False
- s = message.strip().lower()
- return s == "/issue" or s.startswith("/issue ") or s.startswith("/issue\n")
-
-
-async def parse_issue_control(message: str, *, default_repo: str = "") -> str | None:
- """Handle a ``/issue`` control message: validate, then create the issue (or
- report what's missing / what a dry-run would do). Returns the reply string
- when the message *was* an ``/issue`` command (caller short-circuits the
- turn), else ``None``."""
- if not is_issue_command(message):
- return None
- rest = message.strip()[len("/issue"):].strip()
- parsed = _parse(rest, default_repo=default_repo)
- if isinstance(parsed, str):
- return parsed # usage / scaffold / validation error
-
- result = await file_issue(parsed)
- if not result["ok"] and result.get("missing"):
- return (
- "Not filed — this issue is missing " + "; ".join(result["missing"]) + ".\n\n"
- "Add the section(s) and resend. Scaffold for a "
- f"{parsed.kind} issue:\n```\n" + _scaffold(parsed.kind) + "```"
- )
- if not result["ok"]:
- return result.get("error", "Error filing issue.")
-
- label_note = f" · labels: {', '.join(parsed.labels)}" if parsed.labels else ""
- if result.get("dry_run"):
- return (
- f"Dry run — would create in **{parsed.repo}**{label_note}:\n\n"
- f"**{parsed.title}**\n\n{parsed.body}"
- )
- return f"Filed in {parsed.repo}{label_note}: {result.get('url') or '(created)'}"
diff --git a/tools/github_tools.py b/tools/github_tools.py
deleted file mode 100644
index 1df7a550..00000000
--- a/tools/github_tools.py
+++ /dev/null
@@ -1,294 +0,0 @@
-"""GitHub read tools — PRs, issues, commits, CI runs/failures — over the ``gh`` CLI.
-
-Closes the long-standing fleet requests for GitHub read access
-(protoAgent #158, #159). Each tool requires an explicit ``repo``
-(``owner/name``) — there is deliberately **no silent default**: an agent
-that forgets ``repo`` should get an error, not have the call quietly fire
-at the wrong repository (a real misrouting bug observed across the fleet).
-
-Tools degrade gracefully: if ``gh`` isn't installed or auth is missing,
-they return a readable ``Error: ...`` string the model can act on.
-
-Wire-in: ``get_github_tools()`` is registered by the GitHub plugin
-(``plugins/github/__init__.py``), not appended in ``tools/lg_tools.get_all_tools``.
-"""
-
-from __future__ import annotations
-
-import json
-import re
-
-from langchain_core.tools import tool
-
-from tools.fallbacks import with_fallback
-from tools.gh_cli import check_gh_error, run_gh
-
-_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
-
-# Error-relevant lines to surface from a failed CI log (github_run_failure) — the
-# deterministic version of hand-grepping a CI log for what actually broke.
-_CI_ERR_RE = re.compile(
- r"(error|fail|✕|✗|×|not ok|exit code|command not found|exception|traceback|"
- r"assertion|timeout|expected .* to|cannot |refused|unauthorized|forbidden|"
- r"panic|fatal)",
- re.IGNORECASE,
-)
-
-
-def _bad_repo(repo: str) -> str | None:
- if not repo or not _REPO_RE.match(repo):
- return (
- f"Error: 'repo' must be 'owner/name' (got {repo!r}). Pass it explicitly — there is no default repository."
- )
- return None
-
-
-def get_github_tools() -> list:
- """Return the GitHub read tools. Safe to include unconditionally — they
- return an error string when ``gh``/auth is unavailable."""
-
- @tool
- @with_fallback()
- async def github_get_pr(repo: str, number: int) -> str:
- """Fetch a GitHub pull request: title, state, author, body, and changed files.
-
- Args:
- repo: Repository as ``owner/name`` (required, no default).
- number: PR number.
- """
- err = _bad_repo(repo)
- if err:
- return err
- rc, out, serr = await run_gh(
- [
- "pr",
- "view",
- str(number),
- "--repo",
- repo,
- "--json",
- "number,title,state,author,body,additions,deletions,files,url",
- ]
- )
- gh_err = check_gh_error(rc, serr)
- if gh_err:
- return gh_err
- try:
- d = json.loads(out)
- except json.JSONDecodeError:
- return f"Error: could not parse gh output: {out[:200]}"
- files = ", ".join(f.get("path", "?") for f in (d.get("files") or [])[:20])
- return (
- f"PR #{d.get('number')} [{d.get('state')}] {d.get('title')}\n"
- f"by {(d.get('author') or {}).get('login', '?')} | "
- f"+{d.get('additions', 0)}/-{d.get('deletions', 0)} | {d.get('url')}\n"
- f"files: {files or '(none)'}\n\n{(d.get('body') or '').strip()[:2000]}"
- )
-
- @tool
- @with_fallback()
- async def github_get_issue(repo: str, number: int) -> str:
- """Fetch a GitHub issue: title, state, author, labels, and body.
-
- Args:
- repo: Repository as ``owner/name`` (required, no default).
- number: Issue number.
- """
- err = _bad_repo(repo)
- if err:
- return err
- rc, out, serr = await run_gh(
- [
- "issue",
- "view",
- str(number),
- "--repo",
- repo,
- "--json",
- "number,title,state,author,labels,body,url",
- ]
- )
- gh_err = check_gh_error(rc, serr)
- if gh_err:
- return gh_err
- try:
- d = json.loads(out)
- except json.JSONDecodeError:
- return f"Error: could not parse gh output: {out[:200]}"
- labels = ", ".join(lbl.get("name", "") for lbl in (d.get("labels") or []))
- return (
- f"Issue #{d.get('number')} [{d.get('state')}] {d.get('title')}\n"
- f"by {(d.get('author') or {}).get('login', '?')} | labels: {labels or '(none)'} | "
- f"{d.get('url')}\n\n{(d.get('body') or '').strip()[:2000]}"
- )
-
- @tool
- @with_fallback()
- async def github_list_issues(repo: str, state: str = "open", limit: int = 20) -> str:
- """List GitHub issues for a repo (closes #158).
-
- Args:
- repo: Repository as ``owner/name`` (required, no default).
- state: ``open`` | ``closed`` | ``all`` (default ``open``).
- limit: Max issues to return (1–50, default 20).
- """
- err = _bad_repo(repo)
- if err:
- return err
- if state not in ("open", "closed", "all"):
- return f"Error: state must be open|closed|all (got {state!r})."
- limit = max(1, min(int(limit), 50))
- rc, out, serr = await run_gh(
- [
- "issue",
- "list",
- "--repo",
- repo,
- "--state",
- state,
- "--limit",
- str(limit),
- "--json",
- "number,title,state,labels",
- ]
- )
- gh_err = check_gh_error(rc, serr)
- if gh_err:
- return gh_err
- try:
- items = json.loads(out)
- except json.JSONDecodeError:
- return f"Error: could not parse gh output: {out[:200]}"
- if not items:
- return f"No {state} issues in {repo}."
- lines = [f"{len(items)} {state} issue(s) in {repo}:"]
- for it in items:
- labels = ",".join(lbl.get("name", "") for lbl in (it.get("labels") or []))
- lines.append(
- f" #{it.get('number')} [{it.get('state')}] {it.get('title')}" + (f" ({labels})" if labels else "")
- )
- return "\n".join(lines)
-
- @tool
- @with_fallback()
- async def github_get_commit_diff(repo: str, ref: str, max_chars: int = 8000) -> str:
- """Fetch a commit's metadata + unified diff (closes #159).
-
- Args:
- repo: Repository as ``owner/name`` (required, no default).
- ref: Commit SHA (or ref) to inspect.
- max_chars: Truncate the diff at this many characters (default 8000).
- """
- err = _bad_repo(repo)
- if err:
- return err
- # The diff media type returns a raw unified diff via the REST API.
- rc, out, serr = await run_gh(
- [
- "api",
- f"repos/{repo}/commits/{ref}",
- "-H",
- "Accept: application/vnd.github.diff",
- ]
- )
- gh_err = check_gh_error(rc, serr)
- if gh_err:
- return gh_err
- diff = out.strip()
- if not diff:
- return f"No diff for {repo}@{ref} (empty or merge commit)."
- if len(diff) > max_chars:
- diff = diff[:max_chars] + f"\n… (truncated at {max_chars} chars)"
- return f"Commit {repo}@{ref}:\n\n{diff}"
-
- @tool
- @with_fallback()
- async def github_ci_runs(repo: str, branch: str = "", limit: int = 15) -> str:
- """List recent GitHub Actions runs for a repo — for CI triage.
-
- Args:
- repo: Repository as ``owner/name`` (required, no default).
- branch: Optional branch filter (e.g. ``main``).
- limit: Max runs to return (capped at 50).
-
- Feed a failing run's id to ``github_run_failure`` to see why it failed.
- """
- err = _bad_repo(repo)
- if err:
- return err
- args = [
- "run",
- "list",
- "--repo",
- repo,
- "--limit",
- str(max(1, min(int(limit), 50))),
- "--json",
- "databaseId,name,status,conclusion,headBranch,event,createdAt,url",
- ]
- if branch.strip():
- args += ["--branch", branch.strip()]
- rc, out, serr = await run_gh(args)
- gh_err = check_gh_error(rc, serr)
- if gh_err:
- return gh_err
- try:
- runs = json.loads(out)
- except json.JSONDecodeError:
- return f"Error: could not parse gh output: {out[:200]}"
- if not runs:
- return f"No recent runs for {repo}" + (f" on {branch}" if branch.strip() else "")
- lines = [
- f"#{r.get('databaseId')} [{r.get('conclusion') or r.get('status')}] "
- f"{r.get('name')} ({r.get('headBranch')} · {r.get('event')}) — {r.get('url')}"
- for r in runs
- ]
- return f"{repo} — {len(runs)} recent run(s):\n" + "\n".join(lines)
-
- @tool
- @with_fallback()
- async def github_run_failure(repo: str, run_id: int, max_lines: int = 40) -> str:
- """Explain why a GitHub Actions run failed — the error lines from its
- failed steps (the deterministic version of hand-grepping a CI log).
-
- Args:
- repo: Repository as ``owner/name`` (required, no default).
- run_id: The run id (``databaseId`` from ``github_ci_runs``).
- max_lines: Cap on error lines returned (capped at 80).
-
- Pulls only the failed steps' logs (``gh run view --log-failed``), keeps the
- error-relevant lines (matched, deduped), and falls back to the log tail
- when nothing matches.
- """
- err = _bad_repo(repo)
- if err:
- return err
- cap = max(5, min(int(max_lines), 80))
- rc, out, serr = await run_gh(["run", "view", str(run_id), "--repo", repo, "--log-failed"], timeout=60)
- gh_err = check_gh_error(rc, serr)
- if gh_err:
- return gh_err
- # gh prefixes each line "\t\t "; keep the tail.
- raw = [ln.rstrip() for ln in out.splitlines() if ln.strip()]
- seen: set = set()
- uniq: list[str] = []
- for ln in raw:
- msg = ln.split("\t")[-1]
- if _CI_ERR_RE.search(msg):
- key = msg[:120]
- if key not in seen:
- seen.add(key)
- uniq.append(msg[:200])
- picked = uniq[-cap:] if uniq else [ln.split("\t")[-1][:200] for ln in raw[-cap:]]
- if not picked:
- return f"Run {run_id} in {repo}: no failed-step log lines (run may not have failed, or its logs expired)."
- return f"{repo} run {run_id} — failure log ({len(picked)} line(s)):\n" + "\n".join(picked)
-
- return [
- github_get_pr,
- github_get_issue,
- github_list_issues,
- github_get_commit_diff,
- github_ci_runs,
- github_run_failure,
- ]
From 350333aa8fc7a10fc17b81207872cac7f92e279a Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 23:35:52 -0700
Subject: [PATCH 036/190] docs(marketing): point the GitHub plugin directory
entry at the standalone repo (#1338)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The GitHub plugin moved out of core (#1336) into protoLabsAI/github-plugin, but
the plugin-directory entry still described the old in-tree read-only shim. Update it:
read/write (not read-only), `bundled: false` + an `install` URL to the standalone
repo, `adds: [tool, view]`, and source/docs links to the repo. This is the
protoAgent→plugin half of the crosslink (the plugin's README already links back to
protoAgent); paired with the repo's new `protoagent-plugin` GitHub topic, the plugin
now surfaces in both the directory and the topic search.
Co-authored-by: Claude Opus 4.8 (1M context)
---
sites/marketing/data/plugins.json | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/sites/marketing/data/plugins.json b/sites/marketing/data/plugins.json
index 8f7b1c19..9aff0ea9 100644
--- a/sites/marketing/data/plugins.json
+++ b/sites/marketing/data/plugins.json
@@ -89,18 +89,20 @@
},
{
"id": "github",
- "name": "GitHub (read tools)",
+ "name": "GitHub (read/write tools)",
"category": "Integrations",
"official": true,
- "tagline": "Read-only GitHub over the gh CLI — fetch a PR, an issue, list issues, a commit diff. Opt-in; not every agent needs GitHub.",
+ "tagline": "Read AND write GitHub over the gh CLI — PRs, issues, diffs, CI, repo files; plus gated create/edit/merge/close, comments, labels (per-agent write gating). Ships a read-only Issues/PRs board, a util-bar New-issue widget, and the user-only /issue command.",
"adds": [
- "tool"
+ "tool",
+ "view"
],
- "bundled": true,
+ "bundled": false,
+ "install": "https://github.com/protoLabsAI/github-plugin",
"enable": "github",
"links": {
- "source": "https://github.com/protoLabsAI/protoAgent/tree/main/plugins/github",
- "docs": "/docs/guides/plugins"
+ "source": "https://github.com/protoLabsAI/github-plugin",
+ "docs": "https://github.com/protoLabsAI/github-plugin#readme"
}
},
{
From da8289883d5513bb2a4141f21ca6aa2046c05fc8 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Tue, 23 Jun 2026 23:53:05 -0700
Subject: [PATCH 037/190] chore: release v0.70.0 (#1339)
Co-authored-by: github-actions[bot]
---
CHANGELOG.md | 2 ++
pyproject.toml | 2 +-
sites/marketing/data/changelog.json | 9 +++++++++
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a70b115d..f37be39f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.70.0] - 2026-06-24
+
### Added
- **Plugins can own `/` chat control commands** via `registry.register_chat_command(name, handler)`
— the generalized form of the core `/goal`. The handler is `async (rest, session_id) -> str | None`:
diff --git a/pyproject.toml b/pyproject.toml
index a0de2181..ecd3589d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "protoagent"
-version = "0.69.0"
+version = "0.70.0"
description = "protoAgent — LangGraph + A2A template for spawning protoLabs agents"
requires-python = ">=3.11"
diff --git a/sites/marketing/data/changelog.json b/sites/marketing/data/changelog.json
index ebeb6bbd..9eedc13b 100644
--- a/sites/marketing/data/changelog.json
+++ b/sites/marketing/data/changelog.json
@@ -1,4 +1,13 @@
[
+ {
+ "version": "v0.70.0",
+ "date": "2026-06-24",
+ "changes": [
+ "Plugins can own / chat control commands",
+ "\"Report a bug\" link in the hamburger menu",
+ "GitHub is no longer in core — it's a standalone plugin"
+ ]
+ },
{
"version": "v0.69.0",
"date": "2026-06-24",
From ecb00b58bf061abdbf8b50c5eb4e491e342d471f Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Wed, 24 Jun 2026 00:34:23 -0700
Subject: [PATCH 038/190] =?UTF-8?q?feat(web):=20fork-safe=20console=20beha?=
=?UTF-8?q?vior=20seams=20=E2=80=94=20slash=20/=20composer=20/=20palette?=
=?UTF-8?q?=20registries=20(ADR=200061,=20#1337)=20(#1340)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(web): fork-safe client-side slash commands (ADR 0061, #1337)
Give the console the backend's extend-without-editing-core property for chat
behavior. Extends the src/ext/ fork seam (ADR 0038 D3) with a slash-command
registry mirroring registerSurface:
- apps/web/src/ext/slashRegistry.ts: registerSlashCommand({name, description, run})
+ registeredSlashCommands/findSlashCommand. First-wins (HMR-safe). A command's
run(ctx) returns true to claim/intercept the token (send short-circuited), false
to fall through. SlashContext gives it rest/sessionId/noteToThread/setDraft/
focusComposer. The frontend twin of the backend's register_chat_command (#1334).
- apps/web/src/chat/coreSlashCommands.ts: core /new, /clear, /effort move OUT of the
hardcoded runClientSlash switch and register through the SAME seam (dogfood — no
core bypass). ChatSurface builds the slash menu from registeredSlashCommands() +
the server list and dispatches via findSlashCommand; no hardcoded verbs remain.
- Tests: registry (register/find/first-wins/invalid/intercept) + core commands
(registered via the seam; /clear,/effort no-session fall-through). 122 web tests
pass; tsc clean.
- ADR 0061 (+ index) documents the seam, the dogfood rule, planned composer/palette
registries, and deferred uiStore slices. Surfaced by the GitHub extraction (#1336).
Co-Authored-By: Claude Opus 4.8 (1M context)
* feat(web): composer-action + palette-command registries (ADR 0061, #1337)
Finish the #1337 behavior-seam set — the two registries the slash-command seam
planned, same src/ext/ pattern (static, first-wins, HMR-safe):
- registerComposerAction (ext/composerRegistry.ts): add a control to the chat
composer's actions slot. ChatSurface renders registeredComposerActions() beside
the model picker. Additive seam (core composer controls are DS built-ins) — for
fork-added actions; ctx = {sessionId, setDraft, focusComposer, noteToThread}.
- registerPaletteCommand (ext/paletteRegistry.ts): add a root ⌘K command.
usePaletteRegistry maps these onto DS Commands. DOGFOODED — core's deep-links
(Plugins: Discover, Settings, Settings: Fleet/Telemetry) now register through the
seam; the deepLinkCommands() bypass is gone.
A fork now extends slash / composer / palette behavior by dropping a src/ext/
module — no core edits. ADR 0061 + CHANGELOG updated (all three shipped). Tests:
both registries (register/first-wins/invalid/context) + a check that the core
deep-links are registered through the seam. 129 web tests pass; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context)
* docs: regen nav.json for ADR 0061 (gen_docs_nav)
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 13 +++
apps/web/src/app/usePaletteRegistry.ts | 72 ++++++------
apps/web/src/chat/ChatSurface.tsx | 91 ++++++++-------
apps/web/src/chat/coreSlashCommands.test.ts | 32 ++++++
apps/web/src/chat/coreSlashCommands.ts | 56 ++++++++++
apps/web/src/ext/composerRegistry.test.ts | 41 +++++++
apps/web/src/ext/composerRegistry.ts | 47 ++++++++
apps/web/src/ext/index.ts | 6 +
apps/web/src/ext/paletteRegistry.test.ts | 34 ++++++
apps/web/src/ext/paletteRegistry.ts | 44 ++++++++
apps/web/src/ext/slashRegistry.test.ts | 47 ++++++++
apps/web/src/ext/slashRegistry.ts | 63 +++++++++++
.../adr/0061-frontend-extension-registries.md | 104 ++++++++++++++++++
docs/adr/index.md | 1 +
plugins/docs/nav.json | 4 +
15 files changed, 583 insertions(+), 72 deletions(-)
create mode 100644 apps/web/src/chat/coreSlashCommands.test.ts
create mode 100644 apps/web/src/chat/coreSlashCommands.ts
create mode 100644 apps/web/src/ext/composerRegistry.test.ts
create mode 100644 apps/web/src/ext/composerRegistry.ts
create mode 100644 apps/web/src/ext/paletteRegistry.test.ts
create mode 100644 apps/web/src/ext/paletteRegistry.ts
create mode 100644 apps/web/src/ext/slashRegistry.test.ts
create mode 100644 apps/web/src/ext/slashRegistry.ts
create mode 100644 docs/adr/0061-frontend-extension-registries.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f37be39f..592d05db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- **Fork-safe console behavior seams** (ADR 0061, #1337) — give the console the backend's
+ "extend-without-editing-core, update-safe" property. Extends the `src/ext/` fork pattern
+ with three registries mirroring `registerSurface` (static, first-wins, HMR-safe), so a fork
+ adds chat behavior by dropping a `src/ext/` module — no core edits, no upstream conflicts:
+ - **`registerSlashCommand`** — own a client-side `/` (registering claims the token;
+ the frontend twin of the backend's `register_chat_command`). Core's `/new`, `/clear`,
+ `/effort` now register through it — no hardcoded verbs remain.
+ - **`registerComposerAction`** — add a control to the chat composer's actions slot.
+ - **`registerPaletteCommand`** — add a root ⌘K command; core's deep-links (Plugins: Discover,
+ Settings, …) are dogfooded through it (no `deepLinkCommands()` bypass).
+ (uiStore slices deferred — a fork's `src/ext/` surface can own its own store.)
+
## [0.70.0] - 2026-06-24
### Added
diff --git a/apps/web/src/app/usePaletteRegistry.ts b/apps/web/src/app/usePaletteRegistry.ts
index 4232b6bc..f90af6a7 100644
--- a/apps/web/src/app/usePaletteRegistry.ts
+++ b/apps/web/src/app/usePaletteRegistry.ts
@@ -14,6 +14,8 @@ import { commandsView, createPaletteRegistry, pluginView } from "@protolabsai/ui
import type { Command, PaletteRegistry, PaletteView } from "@protolabsai/ui/command-palette";
import { useUI } from "../state/uiStore";
import type { View } from "../lib/viewRegistry";
+import { registerPaletteCommand, registeredPaletteCommands } from "../ext/paletteRegistry";
+import type { PaletteCommand } from "../ext/paletteRegistry";
/** Optional inline chat with the focused agent (ADR 0057). App builds the native chat
* PaletteView (it needs JSX + the focused agent name); the adapter registers it + a
@@ -102,43 +104,51 @@ function navigate(intent: NavIntent) {
navigator(intent);
}
-/** Deep-links into sub-tabbed surfaces. The sub-tab ids are the uiStore union types
- * (the source of truth), so these can't drift into a 404 section. */
-function deepLinkCommands(): Command[] {
- // Each deep-link is expressed as a serializable NavIntent routed through `navigate()`,
- // so it works identically in the console window (apply locally) and the desktop
- // launcher (forward to the main window).
- const link = (id: string, label: string, keywords: string[], intent: NavIntent): Command => ({
+// Core deep-link palette commands — DOGFOODED through the public `registerPaletteCommand`
+// seam (ADR 0061), so core uses the same path a fork does (no bypass). Each deep-link is a
+// serializable NavIntent routed through `navigate()`, so it works identically in the console
+// window (apply locally) and the desktop launcher (forward to the main window). The sub-tab
+// ids are the uiStore union types (source of truth), so they can't drift into a 404.
+// (Inbox moved to a utility-bar widget; Schedule is a top-level rail surface that
+// auto-registers as a "go to" nav command — so no Activity deep-links here.)
+const _link = (id: string, label: string, keywords: string[], intent: NavIntent) =>
+ registerPaletteCommand({
id,
label,
group: "Commands",
keywords,
- run: (c) => {
+ run: (ctx) => {
navigate(intent);
- c.close();
+ ctx.close();
},
});
- return [
- // (Inbox moved to a utility-bar widget; Schedule is a top-level rail surface that
- // auto-registers as a "go to" nav command — so no Activity deep-links here.)
- link("plug:market", "Plugins: Discover", ["plugins", "discover", "market", "directory", "browse"], {
- kind: "plugins",
- tab: "market",
- }),
- // Install-from-URL is the advanced action under Installed now (ADR 0059 D4) — land there.
- link("plug:download", "Plugins: Install from URL", ["plugins", "install", "url", "git"], {
- kind: "plugins",
- tab: "local",
- }),
- // Settings is the consolidated dialog now (2026-06) — opened from the utility-bar pill,
- // the drawer, or these ⌘K commands. A bare "Settings" command + Box-section deep-links.
- link("settings", "Settings", ["settings", "config", "preferences", "options"], { kind: "global" }),
- link("box:fleet", "Settings: Fleet", ["fleet", "agents", "box"], { kind: "global", section: "fleet" }),
- link("box:telemetry", "Settings: Telemetry", ["telemetry", "metrics", "box", "global"], {
- kind: "global",
- section: "telemetry",
- }),
- ];
+_link("plug:market", "Plugins: Discover", ["plugins", "discover", "market", "directory", "browse"], {
+ kind: "plugins",
+ tab: "market",
+});
+// Install-from-URL is the advanced action under Installed now (ADR 0059 D4) — land there.
+_link("plug:download", "Plugins: Install from URL", ["plugins", "install", "url", "git"], {
+ kind: "plugins",
+ tab: "local",
+});
+// Settings is the consolidated dialog now (2026-06) — opened from the utility-bar pill,
+// the drawer, or these ⌘K commands. A bare "Settings" command + Box-section deep-links.
+_link("settings", "Settings", ["settings", "config", "preferences", "options"], { kind: "global" });
+_link("box:fleet", "Settings: Fleet", ["fleet", "agents", "box"], { kind: "global", section: "fleet" });
+_link("box:telemetry", "Settings: Telemetry", ["telemetry", "metrics", "box", "global"], {
+ kind: "global",
+ section: "telemetry",
+});
+
+/** Map a registered (core or fork) PaletteCommand onto a DS palette `Command`. */
+function toDsCommand(pc: PaletteCommand): Command {
+ return {
+ id: pc.id,
+ label: pc.label,
+ group: pc.group ?? "Commands",
+ keywords: pc.keywords ?? [],
+ run: (c) => pc.run({ close: () => c.close() }),
+ };
}
/** Build the palette registry from the resolved view list + the inline plugin views.
@@ -246,7 +256,7 @@ export function usePaletteRegistry(
keywords: ["open", "go to", "surface", "view", "navigate", "switch", "panel"],
run: (c) => c.enter("open"),
};
- const offCommands = registry.registerCommands([openCommand, ...deepLinkCommands()]);
+ const offCommands = registry.registerCommands([openCommand, ...registeredPaletteCommands().map(toDsCommand)]);
return () => {
offChat?.();
offPlugins?.();
diff --git a/apps/web/src/chat/ChatSurface.tsx b/apps/web/src/chat/ChatSurface.tsx
index e303a284..1c3f335e 100644
--- a/apps/web/src/chat/ChatSurface.tsx
+++ b/apps/web/src/chat/ChatSurface.tsx
@@ -1,5 +1,5 @@
import "./chat.css";
-import { Empty } from "@protolabsai/ui/primitives";
+import { Button, Empty } from "@protolabsai/ui/primitives";
import { Switch } from "@protolabsai/ui/forms";
import {
Conversation,
@@ -30,10 +30,11 @@ import { notifyIfHidden } from "../lib/notify";
import {
chatStore,
useChatState,
- DEFAULT_REASONING_EFFORT,
- REASONING_EFFORTS,
effectiveReasoningEffort,
} from "./chat-store";
+import "./coreSlashCommands"; // registers /new, /clear, /effort via the slash-command seam (ADR 0061)
+import { findSlashCommand, registeredSlashCommands } from "../ext/slashRegistry";
+import { registeredComposerActions } from "../ext/composerRegistry";
import { ChatComponent } from "./ChatComponent";
import { ComposerModelSelect } from "./ComposerModelSelect";
import { Markdown } from "./LazyMarkdown";
@@ -326,11 +327,11 @@ function ChatSessionSlot({
const slashMatches = useMemo(() => {
if (slashQuery === null) return [];
const q = slashQuery.toLowerCase();
- // Deterministic client-side commands (ADR 0057) surface first, then server skills.
+ // Client-side commands (ADR 0061) surface first, then server skills. The client set
+ // comes from the slash-command registry — core (/new, /clear, /effort) AND any fork-
+ // registered commands — so neither is hardcoded here.
const all: SlashCommand[] = [
- { name: "new", description: "Open a new chat tab" },
- { name: "clear", description: "Clear this chat's history" },
- { name: "effort", description: "Reasoning effort: low | medium | high | max | off" },
+ ...registeredSlashCommands().map((c) => ({ name: c.name, description: c.description, usage: c.usage })),
...commands,
];
return all.filter(
@@ -352,41 +353,24 @@ function ChatSessionSlot({
]);
}
- // Deterministic client-side commands (ADR 0057) — run locally, never sent to the
- // agent. `/new` opens + focuses a fresh tab; `/clear` wipes THIS tab's history
- // (server checkpoint + transcript), keeping the tab; `/effort ` sets this
- // tab's reasoning effort (sent with each turn). `raw` is the command minus the
- // slash, e.g. "effort high". Returns true if handled.
+ // Dispatch a CLIENT-SIDE slash command through the registry (ADR 0061) — run locally,
+ // never sent to the agent. A registered `/` CLAIMS the token (the frontend twin of
+ // the backend's `register_chat_command`): we build the SlashContext from local state +
+ // invoke its handler. `raw` is the command minus the slash, e.g. "effort high". Returns
+ // true if a command handled it (caller clears the draft + skips the send); false ⇒ not a
+ // client command (fall through to the server / draft path). Core commands (/new, /clear,
+ // /effort) and any fork-registered commands flow through here identically.
function runClientSlash(raw: string): boolean {
const [verb, ...rest] = raw.split(/\s+/);
- const arg = rest.join(" ").trim().toLowerCase();
- if (verb === "new") {
- chatStore.createSession();
- textareaRef.current?.focus();
- return true;
- }
- if (verb === "clear" && session) {
- void api.deleteChatSession(session.id, false).catch(() => {});
- chatStore.updateMessages(session.id, []);
- textareaRef.current?.focus();
- return true;
- }
- if (verb === "effort" && session) {
- const opts = REASONING_EFFORTS.join(" · ");
- if (!arg) {
- const cur = session.reasoningEffort ?? `${DEFAULT_REASONING_EFFORT} (default)`;
- noteToThread(`⚙ Reasoning effort: **${cur}**. Set it with \`/effort ${REASONING_EFFORTS.join("|")}\`.`);
- } else if ((REASONING_EFFORTS as readonly string[]).includes(arg)) {
- chatStore.setSessionReasoningEffort(session.id, arg);
- const off = arg === "off" ? " — reasoning disabled for this tab" : "";
- noteToThread(`⚙ Reasoning effort set to **${arg}**${off}. Applies to the next message.`);
- } else {
- noteToThread(`⚠ Unknown effort \`${arg}\`. Options: ${opts}.`);
- }
- textareaRef.current?.focus();
- return true;
- }
- return false;
+ const cmd = findSlashCommand(verb);
+ if (!cmd) return false;
+ return cmd.run({
+ rest: rest.join(" ").trim(),
+ sessionId: session?.id ?? null,
+ noteToThread,
+ setDraft,
+ focusComposer: () => textareaRef.current?.focus(),
+ });
}
function completeCommand(cmd: SlashCommand) {
@@ -1191,7 +1175,32 @@ function ChatSessionSlot({
onAttach={() => fileInputRef.current?.click()}
// The model picker lives in the DS composer's actions slot (ADR 0048 / the
// ComposerWithAttachments DS pattern) — replaces the separate chip below.
- actions={ }
+ // Fork-registered composer actions (ADR 0061) render alongside it.
+ actions={
+ <>
+ {registeredComposerActions().map((a) => (
+
+ a.run({
+ sessionId: session?.id ?? null,
+ setDraft,
+ focusComposer: () => textareaRef.current?.focus(),
+ noteToThread,
+ })
+ }
+ >
+ {a.icon}
+
+ ))}
+
+ >
+ }
attachments={attachments.map((a) => ({
id: a.id,
name: a.name,
diff --git a/apps/web/src/chat/coreSlashCommands.test.ts b/apps/web/src/chat/coreSlashCommands.test.ts
new file mode 100644
index 00000000..24e00134
--- /dev/null
+++ b/apps/web/src/chat/coreSlashCommands.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+
+import { findSlashCommand } from "../ext/slashRegistry";
+import "./coreSlashCommands"; // side-effect: registers /new, /clear, /effort
+
+import type { SlashContext } from "../ext/slashRegistry";
+
+function ctx(over: Partial = {}): SlashContext {
+ return { rest: "", sessionId: null, noteToThread: () => {}, setDraft: () => {}, focusComposer: () => {}, ...over };
+}
+
+describe("core slash commands (dogfood the seam, ADR 0061)", () => {
+ it("registers /new, /clear, /effort through the same registry a fork uses", () => {
+ expect(findSlashCommand("new")).toBeTruthy();
+ expect(findSlashCommand("clear")).toBeTruthy();
+ expect(findSlashCommand("effort")).toBeTruthy();
+ });
+
+ it("/clear and /effort are no-ops (return false → fall through) without a session", () => {
+ expect(findSlashCommand("clear")!.run(ctx())).toBe(false);
+ expect(findSlashCommand("effort")!.run(ctx())).toBe(false);
+ });
+
+ it("/effort with an unknown level notes the error and still handles it", () => {
+ let noted = "";
+ const handled = findSlashCommand("effort")!.run(
+ ctx({ sessionId: "s1", rest: "turbo", noteToThread: (m) => (noted = m) }),
+ );
+ expect(handled).toBe(true);
+ expect(noted).toContain("Unknown effort");
+ });
+});
diff --git a/apps/web/src/chat/coreSlashCommands.ts b/apps/web/src/chat/coreSlashCommands.ts
new file mode 100644
index 00000000..97d0fc9a
--- /dev/null
+++ b/apps/web/src/chat/coreSlashCommands.ts
@@ -0,0 +1,56 @@
+// Core CLIENT-SIDE slash commands (ADR 0061) — registered through the SAME seam a fork
+// uses (`registerSlashCommand`), so the registry is the only path, never a special case
+// (the way the backend's `register_chat_command` has no core bypass). Imported for its
+// side effects by ChatSurface. `/new` opens a tab, `/clear` wipes this tab's history,
+// `/effort` sets this tab's reasoning effort. Behaviour ported verbatim from the old
+// hardcoded `runClientSlash` switch.
+
+import { registerSlashCommand } from "../ext/slashRegistry";
+import { api } from "../lib/api";
+import { chatStore, DEFAULT_REASONING_EFFORT, REASONING_EFFORTS } from "./chat-store";
+
+registerSlashCommand({
+ name: "new",
+ description: "Open a new chat tab",
+ run: (ctx) => {
+ chatStore.createSession();
+ ctx.focusComposer();
+ return true;
+ },
+});
+
+registerSlashCommand({
+ name: "clear",
+ description: "Clear this chat's history",
+ run: (ctx) => {
+ if (!ctx.sessionId) return false; // no session → not handled (falls through)
+ void api.deleteChatSession(ctx.sessionId, false).catch(() => {});
+ chatStore.updateMessages(ctx.sessionId, []);
+ ctx.focusComposer();
+ return true;
+ },
+});
+
+registerSlashCommand({
+ name: "effort",
+ description: "Reasoning effort: low | medium | high | max | off",
+ usage: "/effort low|medium|high|max|off",
+ run: (ctx) => {
+ if (!ctx.sessionId) return false;
+ const arg = ctx.rest.trim().toLowerCase();
+ const opts = REASONING_EFFORTS.join(" · ");
+ if (!arg) {
+ const session = chatStore.getSnapshot().sessions.find((s) => s.id === ctx.sessionId);
+ const cur = session?.reasoningEffort ?? `${DEFAULT_REASONING_EFFORT} (default)`;
+ ctx.noteToThread(`⚙ Reasoning effort: **${cur}**. Set it with \`/effort ${REASONING_EFFORTS.join("|")}\`.`);
+ } else if ((REASONING_EFFORTS as readonly string[]).includes(arg)) {
+ chatStore.setSessionReasoningEffort(ctx.sessionId, arg);
+ const off = arg === "off" ? " — reasoning disabled for this tab" : "";
+ ctx.noteToThread(`⚙ Reasoning effort set to **${arg}**${off}. Applies to the next message.`);
+ } else {
+ ctx.noteToThread(`⚠ Unknown effort \`${arg}\`. Options: ${opts}.`);
+ }
+ ctx.focusComposer();
+ return true;
+ },
+});
diff --git a/apps/web/src/ext/composerRegistry.test.ts b/apps/web/src/ext/composerRegistry.test.ts
new file mode 100644
index 00000000..6f100020
--- /dev/null
+++ b/apps/web/src/ext/composerRegistry.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from "vitest";
+
+import { registerComposerAction, registeredComposerActions } from "./composerRegistry";
+import type { ComposerActionContext } from "./composerRegistry";
+
+describe("composer-action registry (ADR 0061)", () => {
+ it("registers an action and exposes it", () => {
+ registerComposerAction({ id: "tmpl", label: "Insert template", icon: null, run: () => {} });
+ expect(registeredComposerActions().some((a) => a.id === "tmpl")).toBe(true);
+ });
+
+ it("first registration of an id wins (HMR-safe)", () => {
+ registerComposerAction({ id: "dup", label: "first", icon: null, run: () => {} });
+ registerComposerAction({ id: "dup", label: "second", icon: null, run: () => {} });
+ expect(registeredComposerActions().find((a) => a.id === "dup")?.label).toBe("first");
+ });
+
+ it("ignores invalid registrations (no id / no run)", () => {
+ registerComposerAction({ id: "", label: "x", icon: null, run: () => {} });
+ // @ts-expect-error — missing run
+ registerComposerAction({ id: "norun", label: "x", icon: null });
+ expect(registeredComposerActions().some((a) => a.id === "")).toBe(false);
+ expect(registeredComposerActions().some((a) => a.id === "norun")).toBe(false);
+ });
+
+ it("run receives the composer context", () => {
+ let gotSid: string | null = "unset";
+ registerComposerAction({
+ id: "ctx",
+ label: "c",
+ icon: null,
+ run: (ctx: ComposerActionContext) => {
+ gotSid = ctx.sessionId;
+ },
+ });
+ registeredComposerActions()
+ .find((a) => a.id === "ctx")!
+ .run({ sessionId: "s1", setDraft: () => {}, focusComposer: () => {}, noteToThread: () => {} });
+ expect(gotSid).toBe("s1");
+ });
+});
diff --git a/apps/web/src/ext/composerRegistry.ts b/apps/web/src/ext/composerRegistry.ts
new file mode 100644
index 00000000..cf4fce7d
--- /dev/null
+++ b/apps/web/src/ext/composerRegistry.ts
@@ -0,0 +1,47 @@
+import type { ReactNode } from "react";
+
+// Build-time fork seam for COMPOSER ACTIONS (ADR 0061, extends ADR 0038 D3). A fork drops
+// a `src/ext/.tsx` that calls `registerComposerAction()` to add a control to the chat
+// composer's actions slot (beside the model picker) — WITHOUT editing `ChatSurface.tsx`, so
+// `git pull upstream` stays conflict-free. Sibling of `registerSlashCommand` /
+// `registerSurface`: static registration at module load, first-wins (HMR-safe).
+//
+// This is an ADDITIVE seam — core's composer controls (attach, model select, send) are DS
+// PromptInput built-ins, not migrated; the registry is purely for fork-added actions.
+
+/** What a composer action's handler receives — the host (ChatSurface) builds it. */
+export type ComposerActionContext = {
+ /** The active chat session id, or null. */
+ sessionId: string | null;
+ /** Replace the composer draft text (e.g. insert a template). */
+ setDraft: (text: string) => void;
+ /** Return focus to the composer textarea. */
+ focusComposer: () => void;
+ /** Drop a LOCAL system note into the thread (shown to the operator, never sent). */
+ noteToThread: (markdown: string) => void;
+};
+
+export type ComposerAction = {
+ /** Stable id (dedup key). */
+ id: string;
+ /** aria-label + tooltip for the button. */
+ label: string;
+ /** The button glyph (e.g. a lucide icon element). */
+ icon: ReactNode;
+ /** Invoked on click. */
+ run: (ctx: ComposerActionContext) => void;
+};
+
+const _actions: ComposerAction[] = [];
+
+/** Register a composer action. First registration of an id wins (HMR-safe). */
+export function registerComposerAction(action: ComposerAction): void {
+ const id = (action?.id || "").trim();
+ if (!id || typeof action.run !== "function") return;
+ if (_actions.some((a) => a.id === id)) return; // first wins
+ _actions.push(action);
+}
+
+export function registeredComposerActions(): ComposerAction[] {
+ return _actions;
+}
diff --git a/apps/web/src/ext/index.ts b/apps/web/src/ext/index.ts
index e935af8d..efb8740e 100644
--- a/apps/web/src/ext/index.ts
+++ b/apps/web/src/ext/index.ts
@@ -8,6 +8,12 @@
// model doc: this is the fork path, NOT the sandboxed-plugin path.)
export { registerSurface, registeredSurfaces } from "./registry";
export type { ExtSurface } from "./registry";
+export { registerSlashCommand, registeredSlashCommands, findSlashCommand } from "./slashRegistry";
+export type { ClientSlashCommand, SlashContext } from "./slashRegistry";
+export { registerComposerAction, registeredComposerActions } from "./composerRegistry";
+export type { ComposerAction, ComposerActionContext } from "./composerRegistry";
+export { registerPaletteCommand, registeredPaletteCommands } from "./paletteRegistry";
+export type { PaletteCommand, PaletteCommandContext } from "./paletteRegistry";
export { registerContextMenu, openContextMenu } from "../contextMenu";
export type { MenuItem, MenuEntry, ContextType } from "../contextMenu";
diff --git a/apps/web/src/ext/paletteRegistry.test.ts b/apps/web/src/ext/paletteRegistry.test.ts
new file mode 100644
index 00000000..08437185
--- /dev/null
+++ b/apps/web/src/ext/paletteRegistry.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest";
+
+import { registerPaletteCommand, registeredPaletteCommands } from "./paletteRegistry";
+
+describe("palette-command registry (ADR 0061)", () => {
+ it("registers, first-wins, and ignores invalid", () => {
+ registerPaletteCommand({ id: "p1", label: "One", run: () => {} });
+ registerPaletteCommand({ id: "p1", label: "Two", run: () => {} });
+ expect(registeredPaletteCommands().find((c) => c.id === "p1")?.label).toBe("One");
+
+ registerPaletteCommand({ id: "", label: "x", run: () => {} });
+ // @ts-expect-error — missing run
+ registerPaletteCommand({ id: "norun", label: "x" });
+ expect(registeredPaletteCommands().some((c) => c.id === "")).toBe(false);
+ expect(registeredPaletteCommands().some((c) => c.id === "norun")).toBe(false);
+ });
+
+ it("run gets a close() context", () => {
+ let closed = false;
+ registerPaletteCommand({ id: "p2", label: "Two", run: (ctx) => ctx.close() });
+ registeredPaletteCommands()
+ .find((c) => c.id === "p2")!
+ .run({ close: () => (closed = true) });
+ expect(closed).toBe(true);
+ });
+
+ it("core deep-links are dogfooded through the same seam", async () => {
+ // Importing usePaletteRegistry runs its module-load registrations (the core deep-links).
+ await import("../app/usePaletteRegistry");
+ const ids = registeredPaletteCommands().map((c) => c.id);
+ expect(ids).toContain("settings");
+ expect(ids).toContain("plug:market");
+ });
+});
diff --git a/apps/web/src/ext/paletteRegistry.ts b/apps/web/src/ext/paletteRegistry.ts
new file mode 100644
index 00000000..e37a8911
--- /dev/null
+++ b/apps/web/src/ext/paletteRegistry.ts
@@ -0,0 +1,44 @@
+// Build-time fork seam for ROOT COMMAND-PALETTE commands (ADR 0061, extends ADR 0038 D3).
+// A fork (or core) calls `registerPaletteCommand()` to add a ⌘K command in the "Commands"
+// group — WITHOUT editing `usePaletteRegistry.ts`, so `git pull upstream` stays conflict-
+// free. Sibling of `registerSlashCommand` / `registerSurface`: static registration at module
+// load, first-wins (HMR-safe). usePaletteRegistry maps these onto DS palette `Command`s.
+//
+// Core dogfoods this: its deep-link commands (Plugins: Discover, Settings, …) register
+// through this seam (see usePaletteRegistry.ts), so the registry is the only path.
+//
+// Distinct from plugin manifest `palette` views (ADR 0057), which morph the palette body
+// into a plugin iframe; these are trusted in-process action commands that RUN code.
+
+/** What a palette command's handler receives. */
+export type PaletteCommandContext = {
+ /** Close the palette (call after navigating / running). */
+ close: () => void;
+};
+
+export type PaletteCommand = {
+ /** Stable id (dedup key). */
+ id: string;
+ /** Shown in the palette. */
+ label: string;
+ /** Palette group; defaults to "Commands". */
+ group?: string;
+ /** Fuzzy-match keywords. */
+ keywords?: string[];
+ /** Invoked when the command is run. */
+ run: (ctx: PaletteCommandContext) => void;
+};
+
+const _commands: PaletteCommand[] = [];
+
+/** Register a root ⌘K command. First registration of an id wins (HMR-safe). */
+export function registerPaletteCommand(cmd: PaletteCommand): void {
+ const id = (cmd?.id || "").trim();
+ if (!id || typeof cmd.run !== "function") return;
+ if (_commands.some((c) => c.id === id)) return; // first wins
+ _commands.push(cmd);
+}
+
+export function registeredPaletteCommands(): PaletteCommand[] {
+ return _commands;
+}
diff --git a/apps/web/src/ext/slashRegistry.test.ts b/apps/web/src/ext/slashRegistry.test.ts
new file mode 100644
index 00000000..c078b214
--- /dev/null
+++ b/apps/web/src/ext/slashRegistry.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+
+import { findSlashCommand, registerSlashCommand, registeredSlashCommands } from "./slashRegistry";
+
+describe("slash-command registry (ADR 0061)", () => {
+ it("registers and finds a command, case-insensitively", () => {
+ registerSlashCommand({ name: "Foo", description: "a foo", run: () => true });
+ expect(findSlashCommand("foo")?.description).toBe("a foo");
+ expect(findSlashCommand("FOO")).toBeTruthy(); // token matched case-insensitively
+ expect(registeredSlashCommands().some((c) => c.name === "foo")).toBe(true);
+ });
+
+ it("first registration of a token wins (HMR-safe)", () => {
+ registerSlashCommand({ name: "dup", description: "first", run: () => true });
+ registerSlashCommand({ name: "dup", description: "second", run: () => true });
+ expect(findSlashCommand("dup")?.description).toBe("first");
+ });
+
+ it("ignores invalid registrations (no name / no run)", () => {
+ registerSlashCommand({ name: "", description: "x", run: () => true });
+ // @ts-expect-error — missing run
+ registerSlashCommand({ name: "norun", description: "x" });
+ expect(findSlashCommand("")).toBeUndefined();
+ expect(findSlashCommand("norun")).toBeUndefined();
+ });
+
+ it("a command's run() controls interception via its boolean return", () => {
+ let got = "";
+ registerSlashCommand({
+ name: "echo",
+ description: "echo rest",
+ run: (ctx) => {
+ got = ctx.rest;
+ return true;
+ },
+ });
+ const handled = findSlashCommand("echo")!.run({
+ rest: "hello world",
+ sessionId: null,
+ noteToThread: () => {},
+ setDraft: () => {},
+ focusComposer: () => {},
+ });
+ expect(handled).toBe(true);
+ expect(got).toBe("hello world");
+ });
+});
diff --git a/apps/web/src/ext/slashRegistry.ts b/apps/web/src/ext/slashRegistry.ts
new file mode 100644
index 00000000..be243f13
--- /dev/null
+++ b/apps/web/src/ext/slashRegistry.ts
@@ -0,0 +1,63 @@
+// Build-time fork seam for CLIENT-SIDE slash commands (ADR 0061, extends ADR 0038 D3).
+//
+// A fork (or a core module) drops a `src/ext/.tsx` — or any module imported at
+// startup — that calls `registerSlashCommand()` to own a `/` chat command that runs
+// IN THE BROWSER, WITHOUT editing `ChatSurface.tsx`. So a `git pull upstream` stays
+// conflict-free. This is the frontend twin of the backend's `register_chat_command`
+// (graph/plugins/registry.py): registering a token CLAIMS it — typing or picking `/`
+// invokes the handler and short-circuits the send (the chat input never goes to the agent).
+//
+// Distinct from SERVER slash commands (`/api/chat/commands`, e.g. `/goal`, plugin `/issue`),
+// which fill the draft for the user to send. Client commands act locally on pick/submit.
+// Core itself registers `/new`, `/clear`, `/effort` through this seam (see
+// `chat/coreSlashCommands.ts`) — the seam is the only path, not a special case.
+
+/** What a client slash command's handler receives. The host (ChatSurface) builds this
+ * from its local state + the chat store when the command fires. */
+export type SlashContext = {
+ /** Everything after the token, trimmed (e.g. `/effort high` → `"high"`). */
+ rest: string;
+ /** The active chat session id, or null if none. */
+ sessionId: string | null;
+ /** Drop a LOCAL system note into the thread (shown to the operator, never sent). */
+ noteToThread: (markdown: string) => void;
+ /** Replace the composer draft text. */
+ setDraft: (text: string) => void;
+ /** Return focus to the composer textarea. */
+ focusComposer: () => void;
+};
+
+export type ClientSlashCommand = {
+ /** The `/` token (no leading slash), matched case-insensitively. */
+ name: string;
+ /** Shown in the slash-command menu. */
+ description: string;
+ /** Optional usage hint. */
+ usage?: string;
+ /** Run when the user picks or submits `/`. Return `true` if handled (the send is
+ * short-circuited + the draft cleared); return `false` to fall through to the default
+ * (insert `/ ` into the draft to edit + send). Fire async work and return `true`
+ * to intercept synchronously. */
+ run: (ctx: SlashContext) => boolean;
+};
+
+const _commands: ClientSlashCommand[] = [];
+
+/** Register a client-side slash command. First registration of a token wins (HMR-safe),
+ * mirroring `registerSurface`. */
+export function registerSlashCommand(cmd: ClientSlashCommand): void {
+ const name = (cmd?.name || "").trim().toLowerCase();
+ if (!name || typeof cmd.run !== "function") return;
+ if (_commands.some((c) => c.name === name)) return; // first wins (HMR-safe)
+ _commands.push({ ...cmd, name });
+}
+
+export function registeredSlashCommands(): ClientSlashCommand[] {
+ return _commands;
+}
+
+/** The command owning `/`, or undefined. Token matched case-insensitively. */
+export function findSlashCommand(token: string): ClientSlashCommand | undefined {
+ const t = (token || "").trim().toLowerCase();
+ return _commands.find((c) => c.name === t);
+}
diff --git a/docs/adr/0061-frontend-extension-registries.md b/docs/adr/0061-frontend-extension-registries.md
new file mode 100644
index 00000000..5f6350af
--- /dev/null
+++ b/docs/adr/0061-frontend-extension-registries.md
@@ -0,0 +1,104 @@
+# 0061 — Frontend extension registries (fork-safe console behavior seams)
+
+Status: **Accepted** (slash-command, composer-action, and palette-command registries shipped)
+
+## Context
+
+The **backend** is fork-safe: a fork adds tools, middleware, routes, subagents, goal
+verifiers, and chat-commands through `register_*` seams on the `PluginRegistry`
+(`graph/plugins/registry.py`) **without editing core**, so `git pull` from upstream
+never conflicts. The chat-command seam (ADR / PR #1334, `register_chat_command`) is the
+most recent: a plugin owns `/` and the core dispatcher consults the registry, no
+core edit.
+
+The **frontend has no equivalent for behavior**. The console's view layer *is* fork-safe
+— a fork drops `src/ext/.tsx` calling `registerSurface()` (ADR 0038 D3), and plugin
+manifest `views` render as sandboxed iframes (`placement`/`utility`/`palette`/`slot`, ADR
+0026/0057) — all without core edits. But anything that isn't a view-shaped iframe is
+hardcoded. The GitHub→plugin extraction (PR #1336, issue #1337) made this concrete:
+GitHub was wired straight into `apps/web/src/chat/ChatSurface.tsx` (a `verb === "issue"`
+branch that opened a dialog) and `apps/web/src/state/uiStore.ts` (`newIssue*` state). A
+fork that wants its own chat-input behavior must patch those core files — a permanent
+merge-conflict surface on every update.
+
+Concretely, today a fork **cannot** without editing core:
+- add a **client-side slash command** or **intercept `/x`** to do something other than
+ send (`ChatSurface.runClientSlash` was a closed `switch`; `completeCommand` had no hook);
+- add a **composer action** button (the PromptInput actions slot is hardcoded);
+- add a **root command-palette command** (`usePaletteRegistry.deepLinkCommands()` is a
+ static list);
+- add **UI-store state** (`uiStore` is a closed `UIState`, no slice system).
+
+## Decision
+
+Give the console the same *extend-without-editing-core, update-safe* property the backend
+has, by extending the existing `src/ext/` seam (ADR 0038 D3) with **behavior registries**
+that mirror `registerSurface`: static registration at module load, **first-registration-
+wins (HMR-safe)**, fork modules live only under `src/ext/` so upstream never touches them.
+
+**Core dogfoods every registry** — its own behavior registers through the same seam, with
+no bypass, exactly like the backend `register_*` (there is no "core slash command" special
+case). That guarantees the seam is real: if it works for core, it works for a fork.
+
+### This ADR's first seam — the slash-command registry (shipped)
+
+`apps/web/src/ext/slashRegistry.ts`:
+
+```ts
+registerSlashCommand({
+ name, // the / token (case-insensitive)
+ description, // shown in the slash menu
+ usage?,
+ run: (ctx: SlashContext) => boolean, // true ⇒ handled (send short-circuited, draft cleared);
+}) // false ⇒ fall through (insert "/name " to edit + send)
+```
+
+`SlashContext = { rest, sessionId, noteToThread, setDraft, focusComposer }` — the host
+(`ChatSurface`) builds it from local state + the chat store when the command fires.
+**Registering a token CLAIMS it** — the frontend twin of `register_chat_command`. Distinct
+from **server** slash commands (`/api/chat/commands`, e.g. `/goal`, plugin `/issue`), which
+fill the draft for the user to send; client commands act locally on pick/submit.
+
+Core's `/new`, `/clear`, `/effort` moved out of the hardcoded `runClientSlash` switch into
+`chat/coreSlashCommands.ts`, registered through this seam. `ChatSurface` builds the slash
+menu from `registeredSlashCommands()` + the server list, and `runClientSlash` dispatches via
+`findSlashCommand` — no hardcoded verbs remain.
+
+### The other two seams (also shipped, same pattern)
+
+- **`registerComposerAction`** (`apps/web/src/ext/composerRegistry.ts`) — adds a control to
+ the chat composer's actions slot (beside the model picker). `ChatSurface` renders
+ `registeredComposerActions()` there. An **additive** seam: core's composer controls
+ (attach, model select, send) are DS `PromptInput` built-ins, not migrated; the registry is
+ purely for fork-added actions (e.g. a templates or voice button). Handler context:
+ `{ sessionId, setDraft, focusComposer, noteToThread }`.
+- **`registerPaletteCommand`** (`apps/web/src/ext/paletteRegistry.ts`) — adds a root ⌘K
+ command in the "Commands" group; `usePaletteRegistry` maps these onto DS palette `Command`s.
+ **Dogfooded:** core's deep-links (Plugins: Discover, Settings, Settings: Fleet/Telemetry)
+ register through this seam, so the registry is the only path (no `deepLinkCommands()`
+ bypass). Handler context: `{ close }`. (Distinct from plugin manifest `palette` views,
+ ADR 0057, which morph the palette body into a plugin iframe — these RUN trusted in-process
+ code.)
+
+### Out of scope (deferred)
+
+- **UI-store slices.** Zustand has no runtime slice-merge, and a fork's `src/ext/` surface
+ can own its own store for its own state, so the need is weak. Revisit only if a real case
+ appears. (Issue #1337.)
+
+## Consequences
+
+- A fork adds chat-input behavior (and, once shipped, composer/palette actions) by adding a
+ `src/ext/` module — no core edits, no upstream merge conflicts. Same story as the backend.
+- The seam is **build-time + trusted/in-process** (the fork compiles its own bundle), NOT the
+ sandboxed-iframe plugin path (ADR 0026). Untrusted UI still goes through plugin iframe views.
+- Core behavior is now defined through the public seam, so the registry can't silently rot —
+ if core's `/new` works, a fork's command does too.
+
+## Alternatives considered
+
+- **A runtime (plugin-manifest) slash seam** like iframe views — rejected: client slash
+ behavior is trusted in-process code, not a sandboxed page; it belongs on the `src/ext/`
+ (fork, build-time) path, mirroring `registerSurface`.
+- **Leave it hardcoded, document the patch points** — rejected: that's exactly the
+ merge-conflict surface this ADR removes, and it contradicts the backend's fork-safety.
diff --git a/docs/adr/index.md b/docs/adr/index.md
index 74b1a2a6..dc2356c8 100644
--- a/docs/adr/index.md
+++ b/docs/adr/index.md
@@ -69,3 +69,4 @@ decision, numbered, never deleted (supersede instead).
| [0058](./0058-runtime-plugin-install-frozen-app.md) | Runtime plugin install in the frozen desktop app — a **git-less HTTPS archive fetch** + a **bundled-dep gate** (`requires_pip ⊆ bundled`, checked via `find_spec`) extend ADR 0027 so external plugins install at **runtime** inside the read-only PyInstaller bundle (the loader already discovers + `importlib`-loads live-root plugins in frozen mode; only `git`/`pip` were missing). First consumer: **Discord** becomes an external comms plugin — opt-in everywhere (removed from the default bundle), added via **Settings** (no onboarding step), behavior identical once enabled (same code relocated, frontend affordances stay in core). Gated cut: don't remove from the bundle until runtime-install is parity-verified on a signed dmg + the repo is pinned. Epic bd-3uh | Proposed |
| [0059](./0059-unified-plugin-manager.md) | Unified plugin manager — collapse the split (PluginsSurface's Local/Market/Download tabs + Settings▸Plugins config) into **one** surface with two sections: **Discover** (an in-app official-plugin directory rendered from a host-served `config/plugin-catalog.json` → `GET /api/plugins/catalog`, browse + **one-click install** that works on every surface incl. the frozen desktop app via ADR 0058) and **Installed** (each plugin row folds in enable/disable/update/uninstall **+ its config + Test/Connect inline**; Settings▸Plugins → a pointer). The curated index ADR 0027 D9 / 0040 gestured at, now thin because runtime-install is the primitive. Subsumes the comms-channel one-click install. Epic bd-23a | Proposed |
| [0060](./0060-skill-progressive-disclosure.md) | Skills: progressive disclosure — replace per-turn BM25 retrieval (which injected top-k **full skill bodies** every model call, built from a query that included the agent's own recent output → generic turns re-summoned every skill and pinned it in context) with **advertise cheaply, load deliberately**: an always-on `` **index** (name + description of up to `skills.top_k` discoverable skills, query-independent, `+N more → list_skills`) plus a lead-agent **`load_skill(name)`** tool returning one skill's full procedure on demand (visible as a tool card). Greenfield-deletes `load_skills`/`SkillRecord`/`_format_learned_skills`/`_build_skills_query` and the `skills_loaded` chip end-to-end; `skills.top_k` → "skills listed in index", `skills.announce`/`max_tokens` removed. Mirrors Anthropic Agent Skills / Cursor rule tiers. ACP external-brain feed lists the same index | Accepted |
+| [0061](./0061-frontend-extension-registries.md) | Frontend extension registries — give the console the backend's *extend-without-editing-core, update-safe* property. Extend the `src/ext/` fork seam (ADR 0038 D3) with **behavior registries** mirroring `registerSurface` (static, first-wins, HMR-safe): **`registerSlashCommand`** (shipped) lets a fork/core own a client-side `/` (claim-the-token, the frontend twin of `register_chat_command` #1334) — core's `/new`/`/clear`/`/effort` now register through it, no hardcoded verbs. Planned: `registerComposerAction`, `registerPaletteCommand`. Deferred: uiStore slices. Surfaced by the GitHub→plugin extraction (#1336/#1337). | Accepted |
diff --git a/plugins/docs/nav.json b/plugins/docs/nav.json
index 68ee006c..eab2c2d5 100644
--- a/plugins/docs/nav.json
+++ b/plugins/docs/nav.json
@@ -572,6 +572,10 @@
"path": "adr/0060-skill-progressive-disclosure.md",
"title": "0060 — Skills: progressive disclosure (always-on index + load on demand)"
},
+ {
+ "path": "adr/0061-frontend-extension-registries.md",
+ "title": "0061 — Frontend extension registries (fork-safe console behavior seams)"
+ },
{
"path": "adr/index.md",
"title": "Architecture Decision Records"
From 65f3878c5de4e69626a5b29d92d0c6f10a895590 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Wed, 24 Jun 2026 01:38:06 -0700
Subject: [PATCH 039/190] =?UTF-8?q?feat(web):=20createUISlice=20=E2=80=94?=
=?UTF-8?q?=20fork-owned=20persisted=20UI=20state=20(ADR=200061,=20closes?=
=?UTF-8?q?=20#1337=20set)=20(#1341)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The last #1337 behavior seam. `createUISlice(namespace, initial)` (ext/uiStateRegistry.ts)
gives a fork its OWN namespaced, persisted zustand store for UI state — WITHOUT editing core
uiStore.ts. It deliberately does NOT merge into core's closed UIState (zustand has no runtime
slice-merge, and fork state doesn't belong there); instead it standardizes the "own your own
store" path: same per-agent persistence as core layout (ADR 0042), first-wins per namespace
(re-call → same store, HMR-safe). Used like any zustand hook
(const useX = createUISlice("ns", {...}); useX((s)=>s.f); useX.setState(...)).
With this, the src/ext/ fork seam covers behavior end to end: registerSurface +
registerSlashCommand + registerComposerAction + registerPaletteCommand + createUISlice — a
fork extends the console without editing App.tsx/ChatSurface.tsx/uiStore.ts.
Docs (upserted): ADR 0061 (slices now shipped, not deferred) + index row; fork-the-template
gains a "customize the console without editing core" note AND drops the now-stale GitHub
core-tool / bundled-plugin references (GitHub is an external plugin now). CHANGELOG updated.
Tests: createUISlice (cache/first-wins, get/set, namespace isolation, empty-namespace guard).
133 web tests pass; tsc + docs tests clean.
Co-authored-by: Claude Opus 4.8 (1M context)
---
CHANGELOG.md | 4 +-
apps/web/src/ext/index.ts | 1 +
apps/web/src/ext/uiStateRegistry.test.ts | 32 ++++++++++++
apps/web/src/ext/uiStateRegistry.ts | 52 +++++++++++++++++++
.../adr/0061-frontend-extension-registries.md | 20 ++++---
docs/adr/index.md | 2 +-
docs/guides/fork-the-template.md | 17 ++++--
7 files changed, 114 insertions(+), 14 deletions(-)
create mode 100644 apps/web/src/ext/uiStateRegistry.test.ts
create mode 100644 apps/web/src/ext/uiStateRegistry.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 592d05db..181c32f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,7 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`registerComposerAction`** — add a control to the chat composer's actions slot.
- **`registerPaletteCommand`** — add a root ⌘K command; core's deep-links (Plugins: Discover,
Settings, …) are dogfooded through it (no `deepLinkCommands()` bypass).
- (uiStore slices deferred — a fork's `src/ext/` surface can own its own store.)
+ - **`createUISlice(namespace, initial)`** — own a namespaced, per-agent-persisted zustand
+ store for fork UI state, without editing core `uiStore.ts` (a standardized fork store, not
+ a merge into core's `UIState`).
## [0.70.0] - 2026-06-24
diff --git a/apps/web/src/ext/index.ts b/apps/web/src/ext/index.ts
index efb8740e..87c6248e 100644
--- a/apps/web/src/ext/index.ts
+++ b/apps/web/src/ext/index.ts
@@ -14,6 +14,7 @@ export { registerComposerAction, registeredComposerActions } from "./composerReg
export type { ComposerAction, ComposerActionContext } from "./composerRegistry";
export { registerPaletteCommand, registeredPaletteCommands } from "./paletteRegistry";
export type { PaletteCommand, PaletteCommandContext } from "./paletteRegistry";
+export { createUISlice, registeredUISlices } from "./uiStateRegistry";
export { registerContextMenu, openContextMenu } from "../contextMenu";
export type { MenuItem, MenuEntry, ContextType } from "../contextMenu";
diff --git a/apps/web/src/ext/uiStateRegistry.test.ts b/apps/web/src/ext/uiStateRegistry.test.ts
new file mode 100644
index 00000000..b3afc153
--- /dev/null
+++ b/apps/web/src/ext/uiStateRegistry.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+
+import { createUISlice, registeredUISlices } from "./uiStateRegistry";
+
+describe("UI-state slice registry (ADR 0061)", () => {
+ it("creates a namespaced store; re-using a namespace returns the SAME instance + state", () => {
+ const a = createUISlice("demo", { count: 0 });
+ a.setState({ count: 7 });
+ const b = createUISlice("demo", { count: 99 }); // same namespace → cached store, initial ignored
+ expect(b).toBe(a);
+ expect(b.getState().count).toBe(7);
+ });
+
+ it("supports the zustand store surface (getState / setState)", () => {
+ const s = createUISlice("counter", { n: 1 });
+ s.setState({ n: 5 });
+ expect(s.getState().n).toBe(5);
+ });
+
+ it("distinct namespaces are independent and registered", () => {
+ const alpha = createUISlice("alpha", { x: 1 });
+ const beta = createUISlice("beta", { y: 2 });
+ alpha.setState({ x: 10 });
+ expect(alpha.getState().x).toBe(10);
+ expect(beta.getState().y).toBe(2); // untouched
+ expect(registeredUISlices()).toEqual(expect.arrayContaining(["demo", "counter", "alpha", "beta"]));
+ });
+
+ it("rejects an empty namespace", () => {
+ expect(() => createUISlice("", { a: 1 })).toThrow();
+ });
+});
diff --git a/apps/web/src/ext/uiStateRegistry.ts b/apps/web/src/ext/uiStateRegistry.ts
new file mode 100644
index 00000000..afd9e2da
--- /dev/null
+++ b/apps/web/src/ext/uiStateRegistry.ts
@@ -0,0 +1,52 @@
+import { create, type StoreApi, type UseBoundStore } from "zustand";
+import { createJSONStorage, persist } from "zustand/middleware";
+
+// Build-time fork seam for UI-STATE SLICES (ADR 0061, extends ADR 0038 D3). A fork calls
+// `createUISlice(namespace, initial)` to own a namespaced, PERSISTED zustand store for its
+// own UI state — WITHOUT editing core `uiStore.ts`, so `git pull upstream` stays conflict-
+// free. This deliberately does NOT merge into the core `UIState` object (zustand has no
+// runtime slice-merge, and a fork's state doesn't belong in core's closed shape); it gives
+// the fork its OWN store, standardized: same per-agent persistence as core layout (ADR 0042),
+// first-registration-wins (HMR-safe — re-calling with a namespace returns the SAME store).
+//
+// A fork uses it like any zustand hook:
+// const useMyState = createUISlice("myplugin", { panelOpen: false });
+// const open = useMyState((s) => s.panelOpen); // in a component
+// useMyState.setState({ panelOpen: true }); // anywhere
+// Core UI/layout state stays in `state/uiStore.ts` (it's core's, not a fork slice).
+
+// Per-agent key (ADR 0042), mirroring uiStore: each fleet agent (URL slug) keeps its own
+// slice state; host window = the bare key.
+const _agent = (() => {
+ try {
+ const m = globalThis.location?.pathname?.match(/\/agent\/([^/?#]+)/);
+ return m ? decodeURIComponent(m[1]) : "";
+ } catch {
+ return "";
+ }
+})();
+const _storage = createJSONStorage(() => ({
+ getItem: (name: string) => globalThis.localStorage.getItem(_agent ? `${name}:${_agent}` : name),
+ setItem: (name: string, value: string) =>
+ globalThis.localStorage.setItem(_agent ? `${name}:${_agent}` : name, value),
+ removeItem: (name: string) => globalThis.localStorage.removeItem(_agent ? `${name}:${_agent}` : name),
+}));
+
+const _stores = new Map();
+
+/** Create (or, for an already-used namespace, return) a persisted, namespaced UI-state store.
+ * First-wins per namespace, so re-imports / HMR keep the same store instance + state. */
+export function createUISlice(namespace: string, initial: T): UseBoundStore> {
+ const ns = (namespace || "").trim();
+ if (!ns) throw new Error("createUISlice requires a non-empty namespace");
+ const cached = _stores.get(ns) as UseBoundStore> | undefined;
+ if (cached) return cached; // first-wins / HMR-safe: same instance + state per namespace
+ const store = create()(persist(() => ({ ...initial }), { name: `proto:uislice:${ns}`, storage: _storage }));
+ _stores.set(ns, store);
+ return store;
+}
+
+/** The namespaces with a created slice (for inspection / devtools). */
+export function registeredUISlices(): string[] {
+ return [..._stores.keys()];
+}
diff --git a/docs/adr/0061-frontend-extension-registries.md b/docs/adr/0061-frontend-extension-registries.md
index 5f6350af..2856b5f3 100644
--- a/docs/adr/0061-frontend-extension-registries.md
+++ b/docs/adr/0061-frontend-extension-registries.md
@@ -1,6 +1,6 @@
# 0061 — Frontend extension registries (fork-safe console behavior seams)
-Status: **Accepted** (slash-command, composer-action, and palette-command registries shipped)
+Status: **Accepted** (slash-command, composer-action, palette-command registries + UI-state slices shipped)
## Context
@@ -80,16 +80,22 @@ menu from `registeredSlashCommands()` + the server list, and `runClientSlash` di
ADR 0057, which morph the palette body into a plugin iframe — these RUN trusted in-process
code.)
-### Out of scope (deferred)
+### UI-state slices (shipped, `createUISlice`)
-- **UI-store slices.** Zustand has no runtime slice-merge, and a fork's `src/ext/` surface
- can own its own store for its own state, so the need is weak. Revisit only if a real case
- appears. (Issue #1337.)
+- **`createUISlice(namespace, initial)`** (`apps/web/src/ext/uiStateRegistry.ts`) — a fork
+ owns a namespaced, **persisted** zustand store for its own UI state. It deliberately does
+ **not** merge into the core `UIState` object (zustand has no runtime slice-merge, and a
+ fork's state doesn't belong in core's closed shape) — it gives the fork its OWN store,
+ *standardized*: the same per-agent persistence as core layout (ADR 0042) and first-wins per
+ namespace (re-calling returns the same store, HMR-safe). Used like any zustand hook
+ (`const useX = createUISlice("ns", {…}); useX((s) => s.field); useX.setState(…)`). Core
+ UI/layout state stays in `state/uiStore.ts` — it's core's, not a fork slice.
## Consequences
-- A fork adds chat-input behavior (and, once shipped, composer/palette actions) by adding a
- `src/ext/` module — no core edits, no upstream merge conflicts. Same story as the backend.
+- A fork adds chat-input behavior, composer/palette actions, and its own persisted UI-state
+ by adding a `src/ext/` module — no core edits, no upstream merge conflicts. Same story as
+ the backend's `register_*`.
- The seam is **build-time + trusted/in-process** (the fork compiles its own bundle), NOT the
sandboxed-iframe plugin path (ADR 0026). Untrusted UI still goes through plugin iframe views.
- Core behavior is now defined through the public seam, so the registry can't silently rot —
diff --git a/docs/adr/index.md b/docs/adr/index.md
index dc2356c8..2a98dde1 100644
--- a/docs/adr/index.md
+++ b/docs/adr/index.md
@@ -69,4 +69,4 @@ decision, numbered, never deleted (supersede instead).
| [0058](./0058-runtime-plugin-install-frozen-app.md) | Runtime plugin install in the frozen desktop app — a **git-less HTTPS archive fetch** + a **bundled-dep gate** (`requires_pip ⊆ bundled`, checked via `find_spec`) extend ADR 0027 so external plugins install at **runtime** inside the read-only PyInstaller bundle (the loader already discovers + `importlib`-loads live-root plugins in frozen mode; only `git`/`pip` were missing). First consumer: **Discord** becomes an external comms plugin — opt-in everywhere (removed from the default bundle), added via **Settings** (no onboarding step), behavior identical once enabled (same code relocated, frontend affordances stay in core). Gated cut: don't remove from the bundle until runtime-install is parity-verified on a signed dmg + the repo is pinned. Epic bd-3uh | Proposed |
| [0059](./0059-unified-plugin-manager.md) | Unified plugin manager — collapse the split (PluginsSurface's Local/Market/Download tabs + Settings▸Plugins config) into **one** surface with two sections: **Discover** (an in-app official-plugin directory rendered from a host-served `config/plugin-catalog.json` → `GET /api/plugins/catalog`, browse + **one-click install** that works on every surface incl. the frozen desktop app via ADR 0058) and **Installed** (each plugin row folds in enable/disable/update/uninstall **+ its config + Test/Connect inline**; Settings▸Plugins → a pointer). The curated index ADR 0027 D9 / 0040 gestured at, now thin because runtime-install is the primitive. Subsumes the comms-channel one-click install. Epic bd-23a | Proposed |
| [0060](./0060-skill-progressive-disclosure.md) | Skills: progressive disclosure — replace per-turn BM25 retrieval (which injected top-k **full skill bodies** every model call, built from a query that included the agent's own recent output → generic turns re-summoned every skill and pinned it in context) with **advertise cheaply, load deliberately**: an always-on `` **index** (name + description of up to `skills.top_k` discoverable skills, query-independent, `+N more → list_skills`) plus a lead-agent **`load_skill(name)`** tool returning one skill's full procedure on demand (visible as a tool card). Greenfield-deletes `load_skills`/`SkillRecord`/`_format_learned_skills`/`_build_skills_query` and the `skills_loaded` chip end-to-end; `skills.top_k` → "skills listed in index", `skills.announce`/`max_tokens` removed. Mirrors Anthropic Agent Skills / Cursor rule tiers. ACP external-brain feed lists the same index | Accepted |
-| [0061](./0061-frontend-extension-registries.md) | Frontend extension registries — give the console the backend's *extend-without-editing-core, update-safe* property. Extend the `src/ext/` fork seam (ADR 0038 D3) with **behavior registries** mirroring `registerSurface` (static, first-wins, HMR-safe): **`registerSlashCommand`** (shipped) lets a fork/core own a client-side `/` (claim-the-token, the frontend twin of `register_chat_command` #1334) — core's `/new`/`/clear`/`/effort` now register through it, no hardcoded verbs. Planned: `registerComposerAction`, `registerPaletteCommand`. Deferred: uiStore slices. Surfaced by the GitHub→plugin extraction (#1336/#1337). | Accepted |
+| [0061](./0061-frontend-extension-registries.md) | Frontend extension registries — give the console the backend's *extend-without-editing-core, update-safe* property. Extend the `src/ext/` fork seam (ADR 0038 D3) with **behavior registries** mirroring `registerSurface` (static, first-wins, HMR-safe): **`registerSlashCommand`** (shipped) lets a fork/core own a client-side `/` (claim-the-token, the frontend twin of `register_chat_command` #1334) — core's `/new`/`/clear`/`/effort` now register through it, no hardcoded verbs. Plus **`registerComposerAction`** (composer actions slot), **`registerPaletteCommand`** (root ⌘K — core deep-links dogfooded onto it), and **`createUISlice`** (a fork's own namespaced, per-agent-persisted store — not a merge into core `UIState`). A fork extends console behavior by dropping a `src/ext/` module — no core edit. Surfaced by the GitHub→plugin extraction (#1336/#1337). | Accepted |
diff --git a/docs/guides/fork-the-template.md b/docs/guides/fork-the-template.md
index db67b64d..6bab2d18 100644
--- a/docs/guides/fork-the-template.md
+++ b/docs/guides/fork-the-template.md
@@ -44,8 +44,7 @@ re-sync cleanly. Until the variable is set, releases won't fire (intentional).
## 2. Tools — keep / drop / add (config + plugins, no core edit)
The starter tools ship by default: `current_time`, `calculator`, `web_search`,
-`fetch_url` (keyless general) plus the memory, scheduler, notes, GitHub, and
-tasks tools.
+`fetch_url` (keyless general) plus the memory, scheduler, notes, and tasks tools.
- **Drop** the ones you don't want via config — list them under `tools.disabled`
in `config/langgraph-config.yaml` (live-reloadable). No `get_all_tools()` edit.
@@ -55,13 +54,21 @@ tasks tools.
(Editing `tools/lg_tools.py::get_all_tools()` directly still works, but it's a
core edit that conflicts on every upstream re-sync — prefer config + plugins.)
-Integrations are *plugins*. The bundled ones (e.g. `plugins/telegram`, `plugins/github`)
-turn off with `plugins: { disabled: [telegram] }` — no directory delete, no core edit.
-Integrations like **Discord**, **Google**, and **Slack** install as **external** plugins
+Integrations are *plugins*. The bundled ones (e.g. `plugins/telegram`) turn off with
+`plugins: { disabled: [telegram] }` — no directory delete, no core edit. Integrations
+like **GitHub**, **Discord**, **Google**, and **Slack** install as **external** plugins
from their own repos (browse + install in Settings ▸ Plugins ▸ Discover).
See the [starter tools reference](/reference/starter-tools) for the shapes of the shipped ones.
+**Customize the console without editing core, too.** The frontend has the same fork-safe
+seam as the backend (ADR 0061): drop a `src/ext/.tsx` that calls `registerSurface`
+(a rail panel), `registerSlashCommand` (a client-side `/`), `registerComposerAction`
+(a composer button), `registerPaletteCommand` (a ⌘K command), or `createUISlice` (its own
+persisted UI state) — no edit to `App.tsx` / `ChatSurface.tsx` / `uiStore.ts`, so upstream
+pulls stay conflict-free. (Untrusted UI still goes through sandboxed plugin iframe views —
+see [Building a plugin view](/guides/building-react-plugin-views).)
+
## 3. Configure subagents (optional)
`graph/subagents/config.py` ships with one `researcher`. Either:
From e2c6a049b10a35617b4048a98969dff1a326ecd6 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 11:05:21 -0700
Subject: [PATCH 040/190] fix(web): console SSE sends the sse-token the server
requires under a bearer (#1342)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The event-bus client opened `EventSource("/api/events")` with no token, assuming
the endpoint was server-side exempt. But the auth middleware requires a valid
short-lived HMAC token on `/api/events` whenever a bearer is configured — the
`/api/sse-token` endpoint exists for exactly this, but the console never called
it. With no bearer (default loopback/desktop) the server accepts an empty token,
so the gap was invisible; the moment A2A_AUTH_TOKEN is set (any LAN/exposed
bind), every `/api/events` connection 401s and the live event bus silently dies
(the console falls back to polling).
events.ts now fetches `/api/sse-token` before each connect and passes it as
`?token=`. Because the token is short-lived, EventSource's built-in reconnect
(which would reuse the stale token → permanent 401) is replaced with a manual
teardown + backoff reconnect that refreshes the token and passes `?since=`
so the server still replays missed events from its ring buffer (ADR 0039). Open
mode is unchanged: the token is "" and the stream stays tokenless.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/src/lib/api.ts | 9 +++
apps/web/src/lib/events.test.ts | 134 ++++++++++++++++++++++++++++++++
apps/web/src/lib/events.ts | 104 +++++++++++++++++++++----
3 files changed, 234 insertions(+), 13 deletions(-)
create mode 100644 apps/web/src/lib/events.test.ts
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
index cbe4af12..bfd4f8d2 100644
--- a/apps/web/src/lib/api.ts
+++ b/apps/web/src/lib/api.ts
@@ -545,6 +545,15 @@ export const api = {
return request("/api/runtime/status");
},
+ // Short-lived HMAC token for the SSE EventSource, which can't send an
+ // Authorization header. Bearer-gated; in open mode the server returns "" and
+ // accepts a tokenless /api/events. events.ts fetches this before each
+ // (re)connect. Same slug routing as /api/events so the token is signed by
+ // whichever server actually terminates the stream (host or a proxied member).
+ sseToken() {
+ return request<{ token: string }>("/api/sse-token");
+ },
+
// Gracefully restart the server process (POST /api/restart) — the server drains and
// re-execs; the console reconnects via the boot gate. Always targets the HOST (the
// process you're connected to), never a slug-routed agent.
diff --git a/apps/web/src/lib/events.test.ts b/apps/web/src/lib/events.test.ts
new file mode 100644
index 00000000..6e02e789
--- /dev/null
+++ b/apps/web/src/lib/events.test.ts
@@ -0,0 +1,134 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// The SSE event-bus client (ADR 0003/0039). Focus: topic matching, the `?token=`
+// auth handshake (#873 — the server requires an sse-token once a bearer is set),
+// and `?since=` replay continuity on reconnect.
+
+// apiUrl is identity-ish for "/api/events"; sseToken is mocked per-test.
+const sseToken = vi.fn(async () => ({ token: "" }) as { token: string });
+vi.mock("./api", () => ({
+ apiUrl: (p: string) => p,
+ api: {
+ sseToken: () => sseToken(),
+ },
+}));
+
+// Minimal EventSource fake: records the URL it was constructed with and lets a
+// test drive onopen/onmessage/onerror.
+class FakeEventSource {
+ static instances: FakeEventSource[] = [];
+ url: string;
+ onopen: ((ev: unknown) => void) | null = null;
+ onerror: ((ev: unknown) => void) | null = null;
+ onmessage: ((ev: unknown) => void) | null = null;
+ closed = false;
+ constructor(url: string) {
+ this.url = url;
+ FakeEventSource.instances.push(this);
+ }
+ close() {
+ this.closed = true;
+ }
+ emit(data: unknown) {
+ this.onmessage?.({ data: JSON.stringify(data) } as MessageEvent);
+ }
+}
+
+// Re-import the module fresh each test so its module-level singleton state resets.
+async function loadEvents() {
+ vi.resetModules();
+ FakeEventSource.instances = [];
+ (globalThis as unknown as { EventSource: unknown }).EventSource = FakeEventSource;
+ return import("./events");
+}
+
+beforeEach(() => {
+ sseToken.mockReset();
+ sseToken.mockResolvedValue({ token: "" });
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+describe("topicMatches", () => {
+ it("matches exact, single-segment *, and tail #", async () => {
+ const { topicMatches } = await loadEvents();
+ expect(topicMatches("a.b", "a.b")).toBe(true);
+ expect(topicMatches("a.*", "a.b")).toBe(true);
+ expect(topicMatches("a.*", "a.b.c")).toBe(false);
+ expect(topicMatches("a.#", "a.b.c")).toBe(true);
+ expect(topicMatches("#", "anything.here")).toBe(true);
+ expect(topicMatches("a.b", "a.c")).toBe(false);
+ });
+});
+
+describe("buildEventsUrl", () => {
+ it("returns the bare base when no token and no since", async () => {
+ const { buildEventsUrl } = await loadEvents();
+ expect(buildEventsUrl("/api/events", "", null)).toBe("/api/events");
+ });
+
+ it("appends token and since, joining with ? or &", async () => {
+ const { buildEventsUrl } = await loadEvents();
+ expect(buildEventsUrl("/api/events", "tok", null)).toBe("/api/events?token=tok");
+ expect(buildEventsUrl("/api/events", "", 7)).toBe("/api/events?since=7");
+ expect(buildEventsUrl("/api/events", "tok", 7)).toBe("/api/events?token=tok&since=7");
+ expect(buildEventsUrl("/api/events?x=1", "tok", null)).toBe("/api/events?x=1&token=tok");
+ });
+});
+
+describe("connect handshake", () => {
+ it("fetches an sse-token and opens EventSource with ?token=", async () => {
+ sseToken.mockResolvedValue({ token: "abc123" });
+ const { onTopic } = await loadEvents();
+ onTopic("x.*", () => {});
+ // connect() awaits the token fetch microtask before constructing EventSource.
+ await vi.waitFor(() => expect(FakeEventSource.instances.length).toBe(1));
+ expect(sseToken).toHaveBeenCalledTimes(1);
+ expect(FakeEventSource.instances[0].url).toBe("/api/events?token=abc123");
+ });
+
+ it("opens a tokenless stream in open mode (token \"\")", async () => {
+ sseToken.mockResolvedValue({ token: "" });
+ const { onTopic } = await loadEvents();
+ onTopic("#", () => {});
+ await vi.waitFor(() => expect(FakeEventSource.instances.length).toBe(1));
+ expect(FakeEventSource.instances[0].url).toBe("/api/events");
+ });
+
+ it("still connects (tokenless) when the sse-token fetch rejects", async () => {
+ sseToken.mockRejectedValue(new Error("401"));
+ const { onTopic } = await loadEvents();
+ onTopic("#", () => {});
+ await vi.waitFor(() => expect(FakeEventSource.instances.length).toBe(1));
+ expect(FakeEventSource.instances[0].url).toBe("/api/events");
+ });
+});
+
+describe("reconnect", () => {
+ it("on error, refreshes the token and replays via ?since=", async () => {
+ vi.useFakeTimers();
+ sseToken.mockResolvedValue({ token: "t1" });
+ const seen: number[] = [];
+ const { onTopic } = await loadEvents();
+ onTopic("job.*", (data) => seen.push((data as { n: number }).n));
+
+ await vi.waitFor(() => expect(FakeEventSource.instances.length).toBe(1));
+ const first = FakeEventSource.instances[0];
+ first.onopen?.({});
+ // Deliver an event carrying seq=42 so the next connect should resume after it.
+ first.emit({ topic: "job.start", data: { n: 1 }, seq: 42 });
+ expect(seen).toEqual([1]);
+
+ // Drop the connection; the token rotates server-side.
+ sseToken.mockResolvedValue({ token: "t2" });
+ first.onerror?.({});
+ expect(first.closed).toBe(true);
+
+ // Backoff timer (first attempt = 1s) → reconnect with fresh token + since.
+ await vi.advanceTimersByTimeAsync(1000);
+ await vi.waitFor(() => expect(FakeEventSource.instances.length).toBe(2));
+ expect(FakeEventSource.instances[1].url).toBe("/api/events?token=t2&since=42");
+ });
+});
diff --git a/apps/web/src/lib/events.ts b/apps/web/src/lib/events.ts
index df6805c8..610a9a37 100644
--- a/apps/web/src/lib/events.ts
+++ b/apps/web/src/lib/events.ts
@@ -1,11 +1,17 @@
-import { apiUrl } from "./api";
+import { api, apiUrl } from "./api";
// Client for the server→client event bus (ADR 0003, extended ADR 0039). One EventSource
// is shared for the app's lifetime. Every event arrives as an unnamed SSE frame carrying
// `{topic, data, seq}`, so we route in JS by topic with `*`/`#` wildcard matching — a
-// surface subscribes to an exact topic or a pattern. EventSource auto-reconnects and
-// (because frames carry `id:`) resends Last-Event-ID, so the server replays missed events
-// from its ring buffer.
+// surface subscribes to an exact topic or a pattern.
+//
+// Auth: when a bearer is configured the server requires a short-lived HMAC token on
+// `/api/events` (EventSource can't send an Authorization header). We fetch it from
+// `/api/sse-token` before each connect and pass it as `?token=`. Because that token
+// expires, we can't rely on EventSource's built-in reconnect (it would reuse the stale
+// token → a permanent 401); instead we tear down on error and reconnect with a fresh
+// token, passing `?since=` so the server still replays missed events from its
+// ring buffer. In open mode the token is "" and the server accepts a tokenless stream.
type Listener = (data: Record, topic: string) => void;
@@ -15,6 +21,11 @@ const subs = new Set();
const connListeners = new Set<(connected: boolean) => void>();
let source: EventSource | null = null;
let connected = false;
+let connecting = false;
+// Highest bus seq we've dispatched — replayed via `?since=` on reconnect.
+let lastSeq: number | null = null;
+let reconnectAttempts = 0;
+let reconnectTimer: ReturnType | null = null;
/** Topic matcher mirroring events/bus.py: `*` = one segment, `#` = tail. */
export function topicMatches(pattern: string, topic: string): boolean {
@@ -36,27 +47,94 @@ function setConnected(next: boolean) {
connListeners.forEach((fn) => fn(connected));
}
-function dispatch(raw: string) {
- let frame: { topic?: string; data?: Record } = {};
+/** Route a raw SSE frame to matching subscribers. Returns the frame's bus seq
+ * (for `?since=` replay) or null when the frame carries none. */
+function dispatch(raw: string): number | null {
+ let frame: { topic?: string; data?: Record; seq?: number } = {};
try {
frame = JSON.parse(raw || "{}");
} catch {
- return;
+ return null;
}
const topic = frame.topic;
- if (!topic) return;
+ if (!topic) return null;
const data = (frame.data as Record) || {};
for (const sub of subs) {
if (topicMatches(sub.pattern, topic)) sub.fn(data, topic);
}
+ return typeof frame.seq === "number" ? frame.seq : null;
+}
+
+/** Build the EventSource URL, appending `?token=` (auth) and `?since=` (replay)
+ * only when present. Exported for unit testing. */
+export function buildEventsUrl(base: string, token: string, since: number | null): string {
+ if (!token && since === null) return base;
+ const params = new URLSearchParams();
+ if (token) params.set("token", token);
+ if (since !== null) params.set("since", String(since));
+ return `${base}${base.includes("?") ? "&" : "?"}${params.toString()}`;
+}
+
+/** True while at least one consumer wants the stream open. */
+function wanted(): boolean {
+ return subs.size > 0 || connListeners.size > 0;
+}
+
+function teardownSource() {
+ if (source) {
+ source.onopen = null;
+ source.onerror = null;
+ source.onmessage = null;
+ source.close();
+ source = null;
+ }
+}
+
+function scheduleReconnect() {
+ if (reconnectTimer || !wanted()) return;
+ // Exponential backoff capped at 30s so a down server (or an operator who hasn't
+ // supplied a token yet) isn't hammered.
+ const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000);
+ reconnectAttempts += 1;
+ reconnectTimer = setTimeout(() => {
+ reconnectTimer = null;
+ void connect();
+ }, delay);
+}
+
+async function connect() {
+ if (source || connecting || typeof EventSource === "undefined") return;
+ connecting = true;
+ let token = "";
+ try {
+ token = (await api.sseToken()).token || "";
+ } catch {
+ // Bearer missing/invalid → request() already tripped the AuthGate (#873). Still
+ // attempt a tokenless connect: it succeeds in open mode, and in bearer mode the
+ // onerror path will retry once the operator supplies a token.
+ }
+ connecting = false;
+ // A consumer may have torn everything down while we awaited the token.
+ if (!wanted() || source) return;
+ const es = new EventSource(buildEventsUrl(apiUrl("/api/events"), token, lastSeq));
+ source = es;
+ es.onopen = () => {
+ reconnectAttempts = 0;
+ setConnected(true);
+ };
+ es.onerror = () => {
+ setConnected(false);
+ teardownSource();
+ scheduleReconnect();
+ };
+ es.onmessage = (event) => {
+ const seq = dispatch((event as MessageEvent).data);
+ if (seq !== null) lastSeq = seq;
+ };
}
function ensureOpen() {
- if (source || typeof EventSource === "undefined") return;
- source = new EventSource(apiUrl("/api/events"));
- source.onopen = () => setConnected(true);
- source.onerror = () => setConnected(false); // EventSource auto-reconnects (with Last-Event-ID)
- source.onmessage = (event) => dispatch((event as MessageEvent).data);
+ void connect();
}
/** Subscribe to a topic (exact, or a `*`/`#` pattern). Returns an unsubscribe function. */
From 3d70ec0b8aaec1f91a7c64ba9388a555998ac7a0 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 11:12:35 -0700
Subject: [PATCH 041/190] fix(a2a): drop legacy pre-SDK tasks table on boot so
the SDK can recreate it (#1343)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
An instance whose `a2a-tasks.db` predates the #443 migration from the bespoke
`a2a_task_store.py` to the a2a-sdk `DatabaseTaskStore` carries the old
`tasks(task_id, state, updated_at, data)` schema. The SDK store creates its table
via `Base.metadata.create_all`, which SKIPS a table that already exists — so the
legacy table survives and every task op 500s with `no such column: tasks.id`
(the console chat path goes through A2A, so chat is dead until the db is wiped
by hand).
Add `drop_legacy_task_table()` and call it from `initialize_a2a_stores()` before
`task_store.initialize()`: detect the legacy schema (`tasks` present but no `id`
column) and drop it — task rows are transient, regenerable state — so create_all
rebuilds the correct schema. No-op on a fresh db or one already on the SDK schema.
Tests: legacy table dropped; no-op on fresh + SDK schema; and an end-to-end
`initialize_a2a_stores` over a legacy db that then round-trips a task (regression
for `no such column: tasks.id`).
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
a2a_impl/stores.py | 38 ++++++++++++++++++
tests/test_a2a_stores.py | 87 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 125 insertions(+)
diff --git a/a2a_impl/stores.py b/a2a_impl/stores.py
index 1b29ca18..24ecd430 100644
--- a/a2a_impl/stores.py
+++ b/a2a_impl/stores.py
@@ -391,6 +391,37 @@ async def reconcile_interrupted_tasks(engine: AsyncEngine, *, now: datetime | No
return result.rowcount or 0
+async def drop_legacy_task_table(engine: AsyncEngine) -> bool:
+ """Drop a pre-SDK bespoke ``tasks`` table so the SDK store can recreate it.
+
+ The bespoke ``a2a_task_store.py`` (removed in the #443 SDK migration) created
+ ``tasks(task_id, state, updated_at, data)``. The SDK's ``DatabaseTaskStore``
+ expects ``tasks(id, context_id, status, …)`` and creates it via
+ ``Base.metadata.create_all`` — which **skips a table that already exists**. So
+ an instance upgraded across the migration keeps the legacy table, and every
+ task op 500s with ``no such column: tasks.id`` (the console chat path included).
+
+ Detect the legacy schema (``tasks`` present but no ``id`` column) and drop it
+ — task rows are transient, regenerable state — so the subsequent
+ ``create_all`` rebuilds the correct schema. No-op when the table is absent
+ (fresh db) or already on the SDK schema. Returns True when a drop occurred.
+ """
+ async with engine.begin() as conn:
+ cols = [r[1] for r in (await conn.execute(text("PRAGMA table_info('tasks')"))).fetchall()]
+ if not cols or "id" in cols:
+ return False # no table yet, or already the SDK schema — nothing to do
+ n = (await conn.execute(text("SELECT count(*) FROM tasks"))).scalar()
+ await conn.execute(text("DROP TABLE tasks"))
+ log.warning(
+ "[a2a] dropped legacy pre-SDK 'tasks' table (%s row(s), columns=%s) so the SDK "
+ "DatabaseTaskStore can recreate its schema — fixes 'no such column: tasks.id' on "
+ "instances upgraded across the #443 store migration",
+ n,
+ cols,
+ )
+ return True
+
+
async def initialize_a2a_stores(
task_store: DatabaseTaskStore,
push_store: ValidatingPushNotificationConfigStore,
@@ -405,6 +436,13 @@ async def initialize_a2a_stores(
``input_required`` / ``auth_required`` pauses are left alone (resumable from
the checkpoint).
"""
+ # Upgrade guard: a legacy bespoke ``tasks`` table would survive create_all and
+ # break every task op with "no such column: tasks.id". Drop it first so the
+ # SDK rebuilds the correct schema.
+ try:
+ await drop_legacy_task_table(task_store.engine)
+ except Exception:
+ log.exception("[a2a] legacy task-table migration check failed; continuing")
await task_store.initialize()
await push_store.initialize()
try:
diff --git a/tests/test_a2a_stores.py b/tests/test_a2a_stores.py
index 4a9ac914..e0081c85 100644
--- a/tests/test_a2a_stores.py
+++ b/tests/test_a2a_stores.py
@@ -364,3 +364,90 @@ def _recording_getaddrinfo(host, port, *args, **kwargs):
assert ok is False
assert resolver_threads and all(t != loop_thread for t in resolver_threads)
await engine.dispose()
+
+
+# ── (c) upgrade guard: legacy pre-SDK 'tasks' table is dropped + recreated ──────
+
+
+async def _make_legacy_tasks_db(db_path: str, rows: int = 2) -> None:
+ """Create the bespoke pre-#443 ``tasks`` schema (no ``id`` column) + some rows,
+ mimicking a db file left by the old a2a_task_store before the SDK migration."""
+ from sqlalchemy import text
+
+ engine = make_sqlite_engine(db_path)
+ async with engine.begin() as conn:
+ await conn.execute(
+ text(
+ "CREATE TABLE tasks ("
+ "task_id TEXT PRIMARY KEY, state TEXT NOT NULL, "
+ "updated_at TEXT NOT NULL, data TEXT NOT NULL)"
+ )
+ )
+ for i in range(rows):
+ await conn.execute(
+ text("INSERT INTO tasks (task_id, state, updated_at, data) VALUES (:i, 'working', '2026-01-01', '{}')"),
+ {"i": f"legacy-{i}"},
+ )
+ await engine.dispose()
+
+
+@pytest.mark.asyncio
+async def test_drop_legacy_task_table_drops_bespoke_schema(tmp_path):
+ """A legacy ``tasks`` table (no ``id``) is detected and dropped → True."""
+ from sqlalchemy import text
+
+ db = str(tmp_path / "a2a-tasks.db")
+ await _make_legacy_tasks_db(db)
+
+ engine = make_sqlite_engine(db)
+ dropped = await stores.drop_legacy_task_table(engine)
+ assert dropped is True
+ async with engine.begin() as conn:
+ cols = [r[1] for r in (await conn.execute(text("PRAGMA table_info('tasks')"))).fetchall()]
+ assert cols == [] # table gone; create_all will rebuild it
+ await engine.dispose()
+
+
+@pytest.mark.asyncio
+async def test_drop_legacy_task_table_noop_on_fresh_and_sdk_schema(tmp_path):
+ """No-op (False) when the table is absent, and again once the SDK schema exists."""
+ db = str(tmp_path / "a2a-tasks.db")
+
+ engine = make_sqlite_engine(db)
+ # Fresh db, no tasks table yet.
+ assert await stores.drop_legacy_task_table(engine) is False
+
+ # SDK schema present (has ``id``) → must be left untouched.
+ store = DatabaseTaskStore(engine)
+ await store.initialize()
+ assert await stores.drop_legacy_task_table(engine) is False
+ await engine.dispose()
+
+
+@pytest.mark.asyncio
+async def test_initialize_recreates_sdk_schema_over_legacy_db(tmp_path):
+ """End-to-end: a legacy db survives initialize_a2a_stores and a task round-trips
+ (regression for 'no such column: tasks.id')."""
+ from a2a.types import a2a_pb2
+
+ db = str(tmp_path / "a2a-tasks.db")
+ await _make_legacy_tasks_db(db)
+
+ task_engine = make_sqlite_engine(db)
+ task_store = DatabaseTaskStore(task_engine)
+ push_store, push_engine = await _fresh_push_store(str(tmp_path / "a2a-push.db"))
+
+ await initialize_a2a_stores(task_store, push_store)
+
+ ctx = _ctx()
+ task = a2a_pb2.Task(
+ id="t-new",
+ context_id="ctx-new",
+ status=a2a_pb2.TaskStatus(state=a2a_pb2.TASK_STATE_COMPLETED),
+ )
+ await task_store.save(task, ctx) # would 500 with the legacy schema
+ got = await task_store.get("t-new", ctx)
+ assert got is not None and got.id == "t-new"
+
+ await task_engine.dispose()
+ await push_engine.dispose()
From f17f5e6b802f7ac16074fe91c23821e5fcf92bf2 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 13:56:53 -0700
Subject: [PATCH 042/190] feat(delegates): add list_agents tool alongside
delegate_to (#1344)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`list_agents` belongs next to `delegate_to` — it's the agent's live roster of the
same delegate registry, with each delegate's type, description, and current
reachability (🟢/🔴/⚪ from the health prober). Registered alongside `delegate_to`
(only when delegates exist), it reads the registry's new `roster()` plus the
in-package health snapshot — no cross-plugin coupling.
Supersedes a standalone roster plugin: the capability is intrinsic to the
delegate registry, so it lives in the always-on delegates plugin.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
plugins/delegates/__init__.py | 33 ++++++++++++++++++++++++++++++++-
plugins/delegates/registry.py | 7 +++++++
tests/test_delegates_plugin.py | 25 +++++++++++++++++++++++--
3 files changed, 62 insertions(+), 3 deletions(-)
diff --git a/plugins/delegates/__init__.py b/plugins/delegates/__init__.py
index 1e435d16..b88e7660 100644
--- a/plugins/delegates/__init__.py
+++ b/plugins/delegates/__init__.py
@@ -55,6 +55,35 @@ async def delegate_to(target: str, query: str) -> str:
return delegate_to
+def _build_list_agents(registry: DelegateRegistry):
+ @tool
+ def list_agents() -> str:
+ """List the agents/delegates you can reach with `delegate_to`, with each one's
+ type, description, and current reachability (🟢 reachable · 🔴 down · ⚪ unknown).
+
+ Read this before assuming who's available — the roster is configuration, not a
+ fixed set, and it changes as delegates are added or removed."""
+ try:
+ from .health import health_snapshot
+
+ health = health_snapshot() or {}
+ except Exception: # noqa: BLE001 — prober not running; reachability stays unknown
+ health = {}
+ roster = registry.roster()
+ if not roster:
+ return "No delegates configured."
+ lines = []
+ for r in roster:
+ ok = (health.get(r["name"]) or {}).get("ok")
+ badge = "🟢" if ok is True else "🔴" if ok is False else "⚪"
+ typ = f" ({r['type']})" if r["type"] else ""
+ desc = f" — {r['description']}" if r["description"] else ""
+ lines.append(f"{badge} {r['name']}{typ}{desc}")
+ return "\n".join(lines)
+
+ return list_agents
+
+
def _load_delegates_config() -> list:
"""Read the top-level ``delegates: [...]`` list from the live config doc.
@@ -107,4 +136,6 @@ def register(registry) -> None:
)
return
registry.register_tool(_build_delegate_to(reg))
- log.info("[delegates] registered delegate_to for %d delegate(s): %s", len(reg.names()), ", ".join(reg.names()))
+ registry.register_tool(_build_list_agents(reg))
+ log.info("[delegates] registered delegate_to + list_agents for %d delegate(s): %s",
+ len(reg.names()), ", ".join(reg.names()))
diff --git a/plugins/delegates/registry.py b/plugins/delegates/registry.py
index 8832d3dc..7854c0fc 100644
--- a/plugins/delegates/registry.py
+++ b/plugins/delegates/registry.py
@@ -56,6 +56,13 @@ def listing(self) -> str:
f"`{d.name}` ({d.type}{' — ' + d.description if d.description else ''})" for d in self._items.values()
)
+ def roster(self) -> list[dict]:
+ """Structured one-entry-per-delegate roster (for the ``list_agents`` tool)."""
+ return [
+ {"name": d.name, "type": d.type, "description": d.description, "url": d.url}
+ for d in self._items.values()
+ ]
+
async def dispatch(self, name: str, query: str) -> str:
d = self._items.get(name)
if d is None:
diff --git a/tests/test_delegates_plugin.py b/tests/test_delegates_plugin.py
index f8ff3a7e..0d0b86b2 100644
--- a/tests/test_delegates_plugin.py
+++ b/tests/test_delegates_plugin.py
@@ -211,12 +211,33 @@ def test_register_no_delegates_registers_nothing(monkeypatch):
assert r.tools == []
-def test_register_exposes_delegate_to_with_listing(monkeypatch):
+def test_register_exposes_delegate_to_and_list_agents(monkeypatch):
r = _register([{"name": "opus", "type": "openai", "url": "https://g/v1", "model": "m"}], monkeypatch)
- assert [t.name for t in r.tools] == ["delegate_to"]
+ assert [t.name for t in r.tools] == ["delegate_to", "list_agents"]
assert "opus" in r.tools[0].description
+def test_registry_roster_shape():
+ reg = DelegateRegistry([{"name": "opus", "type": "openai", "url": "https://g/v1",
+ "model": "m", "description": "a model"}])
+ assert reg.roster() == [{"name": "opus", "type": "openai", "description": "a model", "url": "https://g/v1"}]
+
+
+def test_list_agents_lists_roster_with_health(monkeypatch):
+ r = _register([{"name": "opus", "type": "openai", "url": "https://g/v1",
+ "model": "m", "description": "a model"}], monkeypatch)
+ la = next(t for t in r.tools if t.name == "list_agents")
+ monkeypatch.setattr("plugins.delegates.health.health_snapshot", lambda: {"opus": {"ok": True}})
+ assert "🟢 opus (openai) — a model" in la.invoke({})
+
+
+def test_list_agents_unknown_health_is_neutral(monkeypatch):
+ r = _register([{"name": "opus", "type": "openai", "url": "https://g/v1", "model": "m"}], monkeypatch)
+ la = next(t for t in r.tools if t.name == "list_agents")
+ monkeypatch.setattr("plugins.delegates.health.health_snapshot", lambda: {})
+ assert "⚪ opus (openai)" in la.invoke({})
+
+
async def test_delegate_to_unknown_and_empty(monkeypatch):
r = _register([{"name": "opus", "type": "openai", "url": "https://g/v1", "model": "m"}], monkeypatch)
tool = r.tools[0]
From 8a114c186c6b5284b5017d492f8ff51cac2374f7 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 14:04:40 -0700
Subject: [PATCH 043/190] feat(plugins): let a plugin declare auth-exempt paths
in its manifest (#1345)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Under a configured bearer the default-deny auth middleware gates EVERY path —
including /plugins//*. That blocks two legitimate plugin needs: an inbound
webhook (the caller is a third party that sends no bearer — it signs the body
instead) and a public view PAGE (a browser iframe load can't carry a bearer).
Today there's no way to open a hole, so e.g. a Linear webhook 401s.
A plugin can now declare `public_paths` in protoagent.plugin.yaml. The manifest
parser keeps only paths under the plugin's OWN namespace (/plugins//… or
/api/plugins//…) — a plugin can never exempt a core route. The loader
collects them, the server hands them to the auth middleware
(`auth.set_public_prefixes`, replace-semantics so it's reload-safe), and
`_is_public` honours them alongside the static allowlist.
Security: namespace-scoped at parse time AND re-checked in set_public_prefixes
(drops anything not under /plugins/). A webhook still verifies its own signature;
this only gets the request past the bearer gate.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
a2a_impl/auth.py | 32 +++++++++++++++++++++++++++++++-
graph/plugins/loader.py | 5 +++++
graph/plugins/manifest.py | 32 ++++++++++++++++++++++++++++++++
runtime/state.py | 1 +
server/__init__.py | 4 ++++
server/agent_init.py | 2 ++
tests/test_a2a_auth.py | 19 +++++++++++++++++++
tests/test_plugins.py | 17 +++++++++++++++++
8 files changed, 111 insertions(+), 1 deletion(-)
diff --git a/a2a_impl/auth.py b/a2a_impl/auth.py
index 9e2fad2f..c934b254 100644
--- a/a2a_impl/auth.py
+++ b/a2a_impl/auth.py
@@ -61,13 +61,43 @@
"/_ds/",
)
+# Plugin-declared auth-exempt prefixes. Set once at startup (and on reload) from
+# enabled plugins' manifest ``public_paths`` — each already validated to the
+# plugin's own ``/plugins//`` namespace by the manifest parser. This lets a
+# plugin serve an inbound webhook (no bearer — verified by its own HMAC) or a
+# public view page even when a bearer gates everything else. A plugin can ONLY
+# exempt its own routes; ``set_public_prefixes`` rejects anything else as
+# defence-in-depth.
+_PLUGIN_PUBLIC: list[str] = []
+
# SSE token lifetime (seconds).
_SSE_TOKEN_LIFETIME = 30
+def set_public_prefixes(prefixes) -> None:
+ """Replace the plugin-declared public-prefix set (idempotent + reload-safe).
+
+ Each prefix must live under a ``/plugins//`` namespace — a plugin can
+ exempt its own routes, never a core path. Non-conforming entries are dropped
+ with a warning."""
+ cleaned: list[str] = []
+ for p in prefixes or []:
+ s = str(p).strip()
+ if not s:
+ continue
+ if s.startswith("/plugins/") or s.startswith("/api/plugins/"):
+ cleaned.append(s)
+ else:
+ logger.warning("[a2a] ignoring plugin public prefix %r — must live under /plugins//", s)
+ _PLUGIN_PUBLIC[:] = cleaned
+ if cleaned:
+ logger.info("[a2a] %d plugin-declared auth-exempt path(s): %s", len(cleaned), ", ".join(cleaned))
+
+
def _is_public(path: str) -> bool:
"""Return True when ``path`` is on the public allowlist (no auth needed)."""
- return any(path.startswith(p) for p in _PUBLIC_PREFIXES)
+ return (any(path.startswith(p) for p in _PUBLIC_PREFIXES)
+ or any(path.startswith(p) for p in _PLUGIN_PUBLIC))
def set_bearer_token(token: str | None) -> None:
diff --git a/graph/plugins/loader.py b/graph/plugins/loader.py
index 7ae7f5c7..443e513e 100644
--- a/graph/plugins/loader.py
+++ b/graph/plugins/loader.py
@@ -35,6 +35,7 @@ class PluginLoadResult:
embedders: dict = field(default_factory=dict) # name -> embed_fn factory (ADR 0031)
a2a_skills: list = field(default_factory=list) # A2A card skill specs (#570)
routers: list = field(default_factory=list) # {plugin_id, router, prefix} (ADR 0018)
+ public_paths: list = field(default_factory=list) # manifest-declared auth-exempt prefixes
surfaces: list = field(default_factory=list) # {plugin_id, name, start, stop}
subagents: list = field(default_factory=list) # SubagentConfig
middleware: list = field(default_factory=list) # factories: (config) -> AgentMiddleware|None (ADR 0032)
@@ -360,6 +361,10 @@ def load_plugins(config, *, core_tool_names: set[str] | None = None) -> PluginLo
# the server can namespace + report them.
for r in registry.routers:
result.routers.append({"plugin_id": manifest.id, **r})
+ # Manifest-declared auth-exempt prefixes (already namespace-scoped by the
+ # parser) — the server hands these to the auth middleware so an inbound
+ # webhook / public view page works under a token gate.
+ result.public_paths.extend(manifest.public_paths)
# Cross-check: every declared view must be served by one of this plugin's
# routers, else the iframe renders blank/404. Catches "declared a view but
# forgot register_router" / a path typo that fails silently today.
diff --git a/graph/plugins/manifest.py b/graph/plugins/manifest.py
index 8e04c946..35511071 100644
--- a/graph/plugins/manifest.py
+++ b/graph/plugins/manifest.py
@@ -66,6 +66,13 @@ class PluginManifest:
# postMessage token handshake. See `_parse_views` (warns on non-same-origin)
# and docs/guides/building-react-plugin-views.md.
views: list[dict] = field(default_factory=list)
+ # Auth-exempt paths — prefixes under THIS plugin's own /plugins// (or
+ # /api/plugins//) namespace that the default-deny auth middleware lets
+ # through WITHOUT a bearer. The escape hatch for an inbound webhook (no bearer
+ # — the plugin verifies its own signature) or a public view page that must load
+ # in a browser iframe under a token-gated deployment. Namespace-scoped by the
+ # parser so a plugin can never exempt a core route.
+ public_paths: list[str] = field(default_factory=list)
# Event contract (ADR 0039) — the topics this plugin broadcasts / listens for.
# Declarative for discoverability (surfaced in /api/runtime/status): a plugin
# "ships" its events as its public API so others subscribe by topic without
@@ -123,6 +130,29 @@ def _parse_views(views, plugin_id: str) -> list[dict]:
return kept
+def _parse_public_paths(paths, plugin_id: str) -> list[str]:
+ """Keep auth-exempt paths that live under THIS plugin's namespace
+ (``/plugins//…`` or ``/api/plugins//…``); drop + warn on anything else.
+
+ Namespace-scoping is the security boundary: a plugin can exempt only its own
+ routes from the auth gate, never a core path like ``/api/config``."""
+ if not isinstance(paths, (list, tuple)):
+ return []
+ roots = (f"/plugins/{plugin_id}", f"/api/plugins/{plugin_id}")
+ kept: list[str] = []
+ for p in paths:
+ s = str(p).strip()
+ if s.startswith(roots):
+ kept.append(s)
+ elif s:
+ log.warning(
+ "[plugins] %s: public_path %r is outside the plugin namespace "
+ "(/plugins/%s/… or /api/plugins/%s/…) — ignored",
+ plugin_id, s, plugin_id, plugin_id,
+ )
+ return kept
+
+
def load_manifest(plugin_dir: Path) -> PluginManifest | None:
"""Parse ``/protoagent.plugin.yaml`` → ``PluginManifest``.
@@ -156,6 +186,7 @@ def load_manifest(plugin_dir: Path) -> PluginManifest | None:
secrets = data.get("secrets")
settings = data.get("settings")
views = _parse_views(data.get("views"), pid)
+ public_paths = _parse_public_paths(data.get("public_paths"), pid)
emits = data.get("emits")
subscribes = data.get("subscribes")
requires_pip = data.get("requires_pip")
@@ -177,6 +208,7 @@ def load_manifest(plugin_dir: Path) -> PluginManifest | None:
test=bool(data.get("test", False)),
guide_url=str(data.get("guide_url", "") or "").strip(),
views=views,
+ public_paths=public_paths,
emits=[str(x) for x in emits] if isinstance(emits, (list, tuple)) else [],
subscribes=[str(x) for x in subscribes] if isinstance(subscribes, (list, tuple)) else [],
requires_pip=[str(x) for x in requires_pip] if isinstance(requires_pip, (list, tuple)) else [],
diff --git a/runtime/state.py b/runtime/state.py
index 5b57e112..e1a6b62b 100644
--- a/runtime/state.py
+++ b/runtime/state.py
@@ -51,6 +51,7 @@ class AppState:
plugin_chat_commands: dict = field(default_factory=dict) # token -> handler; user-only / control commands
thread_id_resolver: object = None # (request_metadata, session_id) -> str (#571)
plugin_routers: list = field(default_factory=list)
+ plugin_public_paths: list = field(default_factory=list) # manifest auth-exempt prefixes
# The live FastAPI app + the (plugin_id, prefix) keys already mounted on it —
# lets a config reload hot-mount a newly-enabled plugin's routes (no restart).
fastapi_app: object = None
diff --git a/server/__init__.py b/server/__init__.py
index c25c7caf..8766865e 100644
--- a/server/__init__.py
+++ b/server/__init__.py
@@ -716,6 +716,10 @@ async def _scheduler_shutdown() -> None:
# that to ``None`` so configure() applies the documented A2A_AUTH_TOKEN env
# fallback. (configure() treats an explicit "" as "bearer off, no fallback";
# protoAgent has no separate apiKey-only flag, so unset ⇒ env, not off.)
+ # Plugin-declared auth-exempt prefixes (namespace-scoped in the manifest parser)
+ # — registered before the gate is installed so an inbound webhook / public view
+ # page passes under a token-gated deployment.
+ auth.set_public_prefixes(getattr(STATE, "plugin_public_paths", []) or [])
auth.install(
fastapi_app,
bearer_token=((STATE.graph_config.auth_token if STATE.graph_config else "") or None),
diff --git a/server/agent_init.py b/server/agent_init.py
index 790c9384..a9faaadc 100644
--- a/server/agent_init.py
+++ b/server/agent_init.py
@@ -151,6 +151,7 @@ def _init_langgraph_agent(headless_setup: bool = False):
_pre.surfaces,
_pre.meta,
)
+ STATE.plugin_public_paths = _pre.public_paths
_register_plugin_subagents(_pre.subagents)
log.info(
"Setup wizard has not been completed — graph not compiled "
@@ -212,6 +213,7 @@ def _init_langgraph_agent(headless_setup: bool = False):
# build below so the first compile (and every reload) can delegate to them.
# (`global STATE.plugin_routers, STATE.plugin_surfaces` is declared at the top of the fn.)
STATE.plugin_routers, STATE.plugin_surfaces = _plugins.routers, _plugins.surfaces
+ STATE.plugin_public_paths = _plugins.public_paths
_register_plugin_subagents(_plugins.subagents)
_apply_config_subagents(STATE.graph_config) # YAML subagent overrides (tools/max_turns/model)
STATE.plugin_middleware = _resolve_plugin_middleware(STATE.graph_config, _plugins.middleware) # ADR 0032
diff --git a/tests/test_a2a_auth.py b/tests/test_a2a_auth.py
index 3a0b8d90..613d8039 100644
--- a/tests/test_a2a_auth.py
+++ b/tests/test_a2a_auth.py
@@ -169,6 +169,25 @@ def test_ac4_agent_card_public(monkeypatch):
assert _client_multi().get("/.well-known/agent-card.json").status_code == 200
+# A plugin-declared public prefix is exempted; a core path can never be, and the
+# set replaces cleanly (reload-safe).
+def test_plugin_public_prefix_exempts_only_namespaced(monkeypatch):
+ monkeypatch.delenv("A2A_AUTH_TOKEN", raising=False)
+ auth.configure(bearer_token="secret", api_key="", allowed_origins_raw="")
+ try:
+ auth.set_public_prefixes(["/plugins/example/status"])
+ c = _client_multi()
+ assert c.get("/plugins/example/status").status_code == 200 # now exempt (e.g. a webhook)
+ assert c.post("/plugins/example/status").status_code == 200
+ assert c.get("/api/config").status_code == 401 # core path untouched
+ # A plugin cannot exempt a core path — set_public_prefixes drops it.
+ auth.set_public_prefixes(["/api/config"])
+ assert _client_multi().get("/api/config").status_code == 401
+ finally:
+ auth.set_public_prefixes([]) # reset module state for other tests
+ assert _client_multi().get("/plugins/example/status").status_code == 401
+
+
# AC5: /app is public (SPA served without auth)
def test_ac5_app_public(monkeypatch):
monkeypatch.delenv("A2A_AUTH_TOKEN", raising=False)
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index c7dee4ac..7af473f3 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -57,6 +57,23 @@ def test_manifest_parse(tmp_path) -> None:
assert load_manifest(tmp_path / "bad") is None # missing id
+def test_public_paths_namespace_scoped(tmp_path) -> None:
+ # A plugin may declare auth-exempt paths only under its OWN namespace; anything
+ # else (a core path, another plugin's path) is dropped by the parser.
+ _make_plugin(
+ tmp_path, "wh", enabled=True,
+ manifest_extra=(
+ "public_paths:\n"
+ " - /plugins/wh/webhook\n"
+ " - /api/plugins/wh/data\n"
+ " - /api/config\n"
+ " - /plugins/other/x\n"
+ ),
+ )
+ m = load_manifest(tmp_path / "wh")
+ assert m.public_paths == ["/plugins/wh/webhook", "/api/plugins/wh/data"]
+
+
def test_discover_live_overrides_bundle(tmp_path, monkeypatch) -> None:
bundle = tmp_path / "bundle"
live = tmp_path / "live"
From edcb940c702c55f275293332a1a0f542b4ca560f Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 21:25:49 -0700
Subject: [PATCH 044/190] feat(fleet): auto-enable a bundle's plugins when
creating an agent from an archetype (#1346) (#1349)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Creating a fleet agent from a bundle archetype (e.g. Project Manager →
pm-stack) installed the bundle's plugins but left them disabled, so the new
agent booted with the tools installed-but-off and the operator had to flip
each one on in Settings ▸ Plugins. The console install path already
auto-enables on install (ADR 0027), but the fleet create path installs via a
`plugin install` CLI subprocess — which deliberately doesn't enable — and
never wrote `plugins.enabled`, leaving only the template's `[delegates]` on.
Fix:
- installer: persist the bundle's curated `enabled` subset into the lock
bundle record (additive, alongside the cached `archetype`). The live HTTP
install summary already carried it; now a lock-only consumer can read it.
- workspaces.create(): after installing a bundle, add its plugins to
`plugins.enabled` in the new workspace config — honoring the bundle's
curated `enabled` subset, falling back to every installed member (and the
lone plugin for a bare single-plugin install). Unions with the template's
`delegates`; idempotent; a malformed/missing lock leaves enablement
untouched.
Closes #1346
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
graph/plugins/installer.py | 5 +++
graph/workspaces/manager.py | 43 +++++++++++++++++++++
tests/test_plugin_installer.py | 3 ++
tests/test_workspaces.py | 68 ++++++++++++++++++++++++++++++++++
4 files changed, 119 insertions(+)
diff --git a/graph/plugins/installer.py b/graph/plugins/installer.py
index 6ad3ce24..54b3ba5c 100644
--- a/graph/plugins/installer.py
+++ b/graph/plugins/installer.py
@@ -466,6 +466,11 @@ def _install_bundle(
"requested_ref": ref or "",
"resolved_sha": bundle_sha,
"plugins": [s["id"] for s in installed],
+ # The bundle's curated turn-on list (a subset of `plugins`). Cached here so a
+ # consumer that only sees the lock — e.g. the fleet new-agent path, which
+ # installs via a CLI subprocess and never sees the live install summary — can
+ # auto-enable exactly what the bundle author intended. Empty = enable all members.
+ "enabled": list(bundle.get("enabled") or []),
# Archetype metadata (ADR 0042) cached here so the new-agent picker can offer
# this bundle as a starter type without re-reading its manifest.
"archetype": bundle.get("archetype") or {},
diff --git a/graph/workspaces/manager.py b/graph/workspaces/manager.py
index 63ff22ea..64b3ea24 100644
--- a/graph/workspaces/manager.py
+++ b/graph/workspaces/manager.py
@@ -280,12 +280,55 @@ def create(
try:
if bundle:
installed = _install_bundle_into(ws, bundle)
+ # Auto-enable the bundle's plugins so a new agent boots WITH its tools live —
+ # matching the console install path (which auto-enables on install, ADR 0027).
+ # The CLI installer deliberately doesn't enable, so without this the agent
+ # comes up with the bundle installed-but-off and the operator has to flip each
+ # one on in Settings ▸ Plugins (#1346).
+ _enable_installed_in_config(cfg, ws / "plugins.lock")
except Exception:
shutil.rmtree(ws, ignore_errors=True)
raise
return {**rec, "path": str(ws), "installed": installed}
+def _enable_installed_in_config(cfg: Path, lock: Path) -> list[str]:
+ """Add a freshly-installed bundle's plugins to ``plugins.enabled`` in the workspace
+ config, so the agent starts with them on. Honors each bundle's curated ``enabled``
+ subset (cached in the lock by ``_install_bundle``), falling back to every installed
+ member; for a bare single-plugin install with no bundle entry, enables that plugin.
+ Unions with whatever the template already enabled (``delegates``); returns the ids
+ newly added. Best-effort — a malformed lock/config leaves enablement untouched."""
+ import json
+
+ from graph.config_io import load_yaml_doc, save_yaml_doc
+
+ try:
+ data = json.loads(lock.read_text()) if lock.exists() else {}
+ except (json.JSONDecodeError, OSError):
+ return []
+ bundles = data.get("bundles") or []
+ want: list[str] = []
+ if bundles:
+ for b in bundles:
+ want += [str(x) for x in (b.get("enabled") or b.get("plugins") or [])]
+ else: # a bare plugin install (no bundle record) — enable what landed
+ want += [str(p["id"]) for p in (data.get("plugins") or []) if p.get("id")]
+ if not want:
+ return []
+
+ doc = load_yaml_doc(cfg)
+ if not isinstance(doc, dict):
+ return []
+ plugins = doc.setdefault("plugins", {})
+ enabled = list(plugins.get("enabled") or [])
+ added = [p for p in want if p not in enabled]
+ if added:
+ plugins["enabled"] = enabled + added
+ save_yaml_doc(doc, cfg)
+ return added
+
+
def _overlay_model(cfg: Path, ws: Path, src: str) -> None:
"""Pop only the ``model:`` section + secrets from another agent's config into this blank one
— the gateway (provider/api_base/key) carries over so the agent boots ready-to-chat, but its
diff --git a/tests/test_plugin_installer.py b/tests/test_plugin_installer.py
index 50b499a0..bed36d1d 100644
--- a/tests/test_plugin_installer.py
+++ b/tests/test_plugin_installer.py
@@ -267,6 +267,9 @@ def test_install_bundle_fans_out_and_records_provenance(env):
bundles = lock.get("bundles") or []
assert bundles and bundles[0]["id"] == "demo_stack"
assert set(bundles[0]["plugins"]) == {"demo_a", "demo_b"}
+ # the curated turn-on list is persisted in the lock (#1346) so a lock-only consumer
+ # (the fleet new-agent path) can auto-enable exactly what the author intended.
+ assert bundles[0]["enabled"] == ["delegates", "demo_a", "demo_b"]
def test_install_bundle_member_missing_url_errors(env):
diff --git a/tests/test_workspaces.py b/tests/test_workspaces.py
index 9e9d99c2..ca81ff62 100644
--- a/tests/test_workspaces.py
+++ b/tests/test_workspaces.py
@@ -115,3 +115,71 @@ def test_fleet_state_follows_scoped_root(root, monkeypatch):
monkeypatch.setenv("PROTOAGENT_INSTANCE", "roxy")
assert supervisor._state_path() == manager.workspaces_root() / "fleet.json"
assert "roxy" in supervisor._state_path().parts
+
+
+# ── bundle auto-enable on create (#1346) ──────────────────────────────────────
+def _seed_config(ws, enabled=("delegates",)):
+ """Write a minimal workspace config with the given plugins.enabled list."""
+ ws.mkdir(parents=True, exist_ok=True)
+ cfg = ws / "langgraph-config.yaml"
+ cfg.write_text(f"plugins:\n enabled: [{', '.join(enabled)}]\n")
+ return cfg
+
+
+def test_enable_installed_honors_bundle_curated_subset(root):
+ """A bundle's curated `enabled` subset is what gets turned on — not every member —
+ and `delegates` from the template is preserved."""
+ import json
+
+ ws = root / "agent"
+ cfg = _seed_config(ws)
+ (ws / "plugins.lock").write_text(
+ json.dumps(
+ {
+ "plugins": [{"id": "a"}, {"id": "b"}, {"id": "extra"}],
+ "bundles": [{"id": "stack", "plugins": ["a", "b", "extra"], "enabled": ["a", "b"]}],
+ }
+ )
+ )
+ added = manager._enable_installed_in_config(cfg, ws / "plugins.lock")
+ assert added == ["a", "b"]
+ enabled = yaml.safe_load(cfg.read_text())["plugins"]["enabled"]
+ assert enabled == ["delegates", "a", "b"] # delegates kept, curated subset added, `extra` left off
+
+
+def test_enable_installed_falls_back_to_all_members(root):
+ """A bundle with no curated `enabled` list enables every installed member."""
+ import json
+
+ ws = root / "agent"
+ cfg = _seed_config(ws)
+ (ws / "plugins.lock").write_text(
+ json.dumps({"plugins": [{"id": "a"}, {"id": "b"}], "bundles": [{"id": "stack", "plugins": ["a", "b"]}]})
+ )
+ added = manager._enable_installed_in_config(cfg, ws / "plugins.lock")
+ assert added == ["a", "b"]
+ assert yaml.safe_load(cfg.read_text())["plugins"]["enabled"] == ["delegates", "a", "b"]
+
+
+def test_enable_installed_bare_plugin_no_bundle(root):
+ """A single-plugin install (no bundle record) enables that plugin."""
+ import json
+
+ ws = root / "agent"
+ cfg = _seed_config(ws)
+ (ws / "plugins.lock").write_text(json.dumps({"plugins": [{"id": "solo"}]}))
+ added = manager._enable_installed_in_config(cfg, ws / "plugins.lock")
+ assert added == ["solo"]
+ assert yaml.safe_load(cfg.read_text())["plugins"]["enabled"] == ["delegates", "solo"]
+
+
+def test_enable_installed_idempotent_and_missing_lock(root):
+ """Already-enabled ids aren't duplicated; a missing lock is a no-op."""
+ import json
+
+ ws = root / "agent"
+ cfg = _seed_config(ws, enabled=("delegates", "a"))
+ assert manager._enable_installed_in_config(cfg, ws / "nope.lock") == [] # no lock → no change
+ (ws / "plugins.lock").write_text(json.dumps({"bundles": [{"id": "s", "enabled": ["a"]}]}))
+ assert manager._enable_installed_in_config(cfg, ws / "plugins.lock") == [] # already on
+ assert yaml.safe_load(cfg.read_text())["plugins"]["enabled"] == ["delegates", "a"]
From 0921a3799eff404faf759a7dc36011a333c55044 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 21:46:13 -0700
Subject: [PATCH 045/190] feat(plugins): apply a bundle's recommended config
defaults on install (#1350) (#1351)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both install paths now honor a bundle's `enabled:` list, but its `config:`
block (recommended per-plugin defaults — panel modes, endpoints, flags) was
surfaced in the install summary and then dropped, so a bundle came up enabled
but unconfigured and the operator re-entered defaults by hand.
Apply `config:` as DEFAULTS on both paths, never clobbering operator values:
- installer: persist the bundle's `config` into the lock record (additive,
beside `enabled`) so the lock-only fleet path can read it; add the shared
`bundle_config_overlay()` — reduces a bundle's config to only the keys not
already set in the live config (operator value always wins, present key
untouched, empty sections dropped).
- console install route: fold that overlay into the same enable write, under
the same trust gate (skipped when install≠enable opt-out is set).
- fleet create: seed the lock's bundle config into the new workspace config
after enabling.
Each plugin's config is a top-level section keyed by id (ADR 0019); the merge
is per-leaf so sibling operator keys survive. Precedence: operator config >
bundle defaults > manifest defaults.
Closes #1350
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
graph/plugins/installer.py | 24 ++++++++++++++++++++
graph/workspaces/manager.py | 41 +++++++++++++++++++++++++++++++++-
operator_api/plugin_routes.py | 19 +++++++++++++---
tests/test_plugin_installer.py | 26 +++++++++++++++++++++
tests/test_plugin_routes.py | 27 ++++++++++++++++++++++
tests/test_workspaces.py | 41 ++++++++++++++++++++++++++++++++++
6 files changed, 174 insertions(+), 4 deletions(-)
diff --git a/graph/plugins/installer.py b/graph/plugins/installer.py
index 54b3ba5c..05ebfcf3 100644
--- a/graph/plugins/installer.py
+++ b/graph/plugins/installer.py
@@ -435,6 +435,26 @@ def load_bundle(repo: Path) -> dict | None:
return doc
+def bundle_config_overlay(bundle_config: dict | None, current: dict | None) -> dict:
+ """Reduce a bundle's recommended ``config:`` ({section: {key: val}}) to a DEFAULTS
+ overlay: only the keys the operator hasn't already set in ``current`` (the live
+ config sections). Per-section, per-leaf — an operator value always wins and a
+ present key is left untouched, so applying this never clobbers existing settings.
+ Empty sections are dropped. Shared by the console install route and the fleet
+ create path so both apply bundle defaults identically (#1350)."""
+ overlay: dict = {}
+ cur = current if isinstance(current, dict) else {}
+ for section, values in (bundle_config or {}).items():
+ if not isinstance(values, dict):
+ continue
+ existing = cur.get(section)
+ existing = existing if isinstance(existing, dict) else {}
+ fill = {k: v for k, v in values.items() if k not in existing}
+ if fill:
+ overlay[str(section)] = fill
+ return overlay
+
+
def _install_bundle(
bundle: dict, bundle_url: str, bundle_sha: str, ref: str | None, *, force: bool, by: str, allow: list[str] | None
) -> dict:
@@ -471,6 +491,10 @@ def _install_bundle(
# installs via a CLI subprocess and never sees the live install summary — can
# auto-enable exactly what the bundle author intended. Empty = enable all members.
"enabled": list(bundle.get("enabled") or []),
+ # The bundle's recommended per-plugin config defaults ({section: {key: val}}).
+ # Cached for the same lock-only consumer; applied as DEFAULTS (operator values
+ # win, present keys are never clobbered — see `bundle_config_overlay`).
+ "config": dict(bundle.get("config") or {}),
# Archetype metadata (ADR 0042) cached here so the new-agent picker can offer
# this bundle as a starter type without re-reading its manifest.
"archetype": bundle.get("archetype") or {},
diff --git a/graph/workspaces/manager.py b/graph/workspaces/manager.py
index 64b3ea24..4522e6ce 100644
--- a/graph/workspaces/manager.py
+++ b/graph/workspaces/manager.py
@@ -284,8 +284,10 @@ def create(
# matching the console install path (which auto-enables on install, ADR 0027).
# The CLI installer deliberately doesn't enable, so without this the agent
# comes up with the bundle installed-but-off and the operator has to flip each
- # one on in Settings ▸ Plugins (#1346).
+ # one on in Settings ▸ Plugins (#1346). Then seed the bundle's recommended
+ # per-plugin config defaults (#1350) — a fresh workspace, so nothing to clobber.
_enable_installed_in_config(cfg, ws / "plugins.lock")
+ _apply_bundle_config_defaults(cfg, ws / "plugins.lock")
except Exception:
shutil.rmtree(ws, ignore_errors=True)
raise
@@ -329,6 +331,43 @@ def _enable_installed_in_config(cfg: Path, lock: Path) -> list[str]:
return added
+def _apply_bundle_config_defaults(cfg: Path, lock: Path) -> dict:
+ """Seed a freshly-installed bundle's recommended per-plugin ``config:`` defaults
+ into the workspace config (#1350). Defaults only — `bundle_config_overlay` drops any
+ key already set in the config, so an operator value is never clobbered. Each plugin's
+ config is a top-level section keyed by its id (ADR 0019). Returns the applied overlay.
+ Best-effort — a malformed lock/config is a no-op."""
+ import json
+
+ from graph.config_io import load_yaml_doc, save_yaml_doc
+ from graph.plugins.installer import bundle_config_overlay
+
+ try:
+ data = json.loads(lock.read_text()) if lock.exists() else {}
+ except (json.JSONDecodeError, OSError):
+ return {}
+ merged: dict = {}
+ for b in data.get("bundles") or []:
+ merged.update(b.get("config") or {})
+ if not merged:
+ return {}
+
+ doc = load_yaml_doc(cfg)
+ if not isinstance(doc, dict):
+ return {}
+ overlay = bundle_config_overlay(merged, doc)
+ if not overlay:
+ return {}
+ for section, fill in overlay.items():
+ dest = doc.setdefault(section, {})
+ if not isinstance(dest, dict):
+ continue
+ for k, v in fill.items():
+ dest[k] = v
+ save_yaml_doc(doc, cfg)
+ return overlay
+
+
def _overlay_model(cfg: Path, ws: Path, src: str) -> None:
"""Pop only the ``model:`` section + secrets from another agent's config into this blank one
— the gateway (provider/api_base/key) carries over so the agent boots ready-to-chat, but its
diff --git a/operator_api/plugin_routes.py b/operator_api/plugin_routes.py
index 1b0a8df4..51a793c0 100644
--- a/operator_api/plugin_routes.py
+++ b/operator_api/plugin_routes.py
@@ -233,9 +233,22 @@ async def _install(body: dict | None = None):
enabled.append(pid)
from server.agent_init import _apply_settings_changes
- ok, messages = _apply_settings_changes(
- config={"plugins": {"enabled": enabled, "disabled": disabled}},
- )
+ config_updates: dict = {"plugins": {"enabled": enabled, "disabled": disabled}}
+ # Seed the bundle's recommended per-plugin config defaults (#1350) — same trust
+ # gate as auto-enable. Defaults only: reduce against the current YAML config so an
+ # operator value (or a key they've already set) is never clobbered.
+ bundle_config = summary.get("config") if "bundle" in summary else None
+ if bundle_config:
+ from graph.config_io import load_yaml_doc
+ from graph.plugins.installer import bundle_config_overlay
+
+ import graph.config_io as _cio
+
+ current = load_yaml_doc(_cio.CONFIG_YAML_PATH)
+ overlay = bundle_config_overlay(bundle_config, current if isinstance(current, dict) else {})
+ config_updates.update(overlay)
+
+ ok, messages = _apply_settings_changes(config=config_updates)
if ok:
reloaded, enabled_now = True, ids
else:
diff --git a/tests/test_plugin_installer.py b/tests/test_plugin_installer.py
index bed36d1d..90e94861 100644
--- a/tests/test_plugin_installer.py
+++ b/tests/test_plugin_installer.py
@@ -270,6 +270,32 @@ def test_install_bundle_fans_out_and_records_provenance(env):
# the curated turn-on list is persisted in the lock (#1346) so a lock-only consumer
# (the fleet new-agent path) can auto-enable exactly what the author intended.
assert bundles[0]["enabled"] == ["delegates", "demo_a", "demo_b"]
+ # ...and the recommended config defaults too (#1350), for the same consumer.
+ assert bundles[0]["config"] == {"demo_a": {"k": "v"}}
+
+
+def test_bundle_config_overlay_fills_only_unset_keys():
+ """Defaults overlay: a key the operator already set is left untouched; only the
+ unset keys are filled, and a fully-set section is dropped (#1350)."""
+ bundle_config = {
+ "agent_browser": {"panel_mode": "full", "timeout": 30},
+ "board": {"theme": "dark"},
+ }
+ current = {
+ "agent_browser": {"panel_mode": "compact"}, # operator already chose this — keep it
+ "board": {"theme": "light"}, # fully set → section dropped
+ }
+ overlay = installer.bundle_config_overlay(bundle_config, current)
+ assert overlay == {"agent_browser": {"timeout": 30}} # only the unset key; operator value wins
+
+
+def test_bundle_config_overlay_empty_and_malformed():
+ assert installer.bundle_config_overlay(None, {}) == {}
+ assert installer.bundle_config_overlay({}, None) == {}
+ # a non-dict section value is skipped, not crashed on
+ assert installer.bundle_config_overlay({"x": "notadict"}, {}) == {}
+ # no current → every key is unset, so all fill
+ assert installer.bundle_config_overlay({"x": {"a": 1}}, {}) == {"x": {"a": 1}}
def test_install_bundle_member_missing_url_errors(env):
diff --git a/tests/test_plugin_routes.py b/tests/test_plugin_routes.py
index 4b062f06..a909e4eb 100644
--- a/tests/test_plugin_routes.py
+++ b/tests/test_plugin_routes.py
@@ -151,6 +151,33 @@ def test_install_bundle_without_declared_enable_enables_every_member(monkeypatch
assert body["enabled"] == ["a", "b"]
+def test_install_bundle_seeds_config_defaults_without_clobbering(monkeypatch):
+ """A bundle's recommended config defaults ride the same enable write (#1350) —
+ filling only keys the operator hasn't already set in the live config."""
+ from graph.plugins import installer
+
+ captured = _wire(monkeypatch, enabled=[], disabled=[], meta=[])
+ monkeypatch.setattr(
+ installer,
+ "install",
+ lambda url, ref=None, **k: {
+ "bundle": "pm-stack",
+ "installed": [{"id": "browser"}],
+ "enabled": ["browser"],
+ "config": {"browser": {"panel_mode": "full", "timeout": 30}},
+ },
+ )
+ # Operator already set browser.panel_mode — the default must NOT overwrite it.
+ import graph.config_io as _cio
+
+ monkeypatch.setattr(_cio, "load_yaml_doc", lambda p=None: {"browser": {"panel_mode": "compact"}})
+ body = _client().post("/api/plugins/install", json={"url": "https://x/pm-stack"}).json()
+ assert body["enabled"] == ["browser"]
+ # only the unset key is seeded; the operator's panel_mode is left for apply_updates_to_yaml to keep
+ assert captured["config"]["browser"] == {"timeout": 30}
+ assert captured["config"]["plugins"]["enabled"] == ["browser"]
+
+
def test_install_opt_out_stays_install_not_enable(monkeypatch):
from graph.plugins import installer
diff --git a/tests/test_workspaces.py b/tests/test_workspaces.py
index ca81ff62..08cc8fcd 100644
--- a/tests/test_workspaces.py
+++ b/tests/test_workspaces.py
@@ -183,3 +183,44 @@ def test_enable_installed_idempotent_and_missing_lock(root):
(ws / "plugins.lock").write_text(json.dumps({"bundles": [{"id": "s", "enabled": ["a"]}]}))
assert manager._enable_installed_in_config(cfg, ws / "plugins.lock") == [] # already on
assert yaml.safe_load(cfg.read_text())["plugins"]["enabled"] == ["delegates", "a"]
+
+
+# ── bundle config defaults on create (#1350) ──────────────────────────────────
+def test_apply_bundle_config_defaults_seeds_unset_keys(root):
+ """A bundle's recommended config defaults land in the workspace config, filling only
+ keys the operator hasn't set (a fresh workspace, so everything is unset)."""
+ import json
+
+ ws = root / "agent"
+ cfg = _seed_config(ws)
+ cfg.write_text(cfg.read_text() + "agent_browser:\n panel_mode: compact\n") # operator pre-set
+ (ws / "plugins.lock").write_text(
+ json.dumps(
+ {
+ "bundles": [
+ {
+ "id": "stack",
+ "config": {"agent_browser": {"panel_mode": "full", "timeout": 30}, "board": {"theme": "dark"}},
+ }
+ ]
+ }
+ )
+ )
+ overlay = manager._apply_bundle_config_defaults(cfg, ws / "plugins.lock")
+ assert overlay == {"agent_browser": {"timeout": 30}, "board": {"theme": "dark"}}
+ doc = yaml.safe_load(cfg.read_text())
+ assert doc["agent_browser"] == {"panel_mode": "compact", "timeout": 30} # operator value kept, default added
+ assert doc["board"] == {"theme": "dark"} # brand-new section seeded
+
+
+def test_apply_bundle_config_defaults_noop_without_config(root):
+ """A bundle with no `config:` block (or a missing lock) writes nothing."""
+ import json
+
+ ws = root / "agent"
+ cfg = _seed_config(ws)
+ before = cfg.read_text()
+ assert manager._apply_bundle_config_defaults(cfg, ws / "nope.lock") == {}
+ (ws / "plugins.lock").write_text(json.dumps({"bundles": [{"id": "stack", "plugins": ["a"]}]}))
+ assert manager._apply_bundle_config_defaults(cfg, ws / "plugins.lock") == {}
+ assert cfg.read_text() == before # untouched
From 92cad7ffeef224eaa4bcd28cc7add0f882bf2d37 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 22:00:24 -0700
Subject: [PATCH 046/190] feat(subagents): allow_skill_emission flag to opt a
subagent out of distillation (#1348)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(subagents): allow_skill_emission flag to opt a subagent out of distillation (#1347)
Add a per-subagent `allow_skill_emission: bool = True` field to
`SubagentConfig`. When False, a runtime suppresses skill-v1 emission for
that subagent's runs even if the caller requests it — so subagents whose
output is context-specific, sensitive, or non-deterministic (per-invocation
conformance/verdict reviewers, agents over private data) don't pollute the
distilled-skills index with one-off "skills" that don't generalize.
Default True preserves current behavior — purely additive. The flag is
read where a runtime wires per-task skill emission; the config overlay
(`_apply_config_subagents`) rebuilds entries via `replace(base, ...)` and
so never clobbers it. As a template repo, landing the field upstream lets
forks/plugins (e.g. the content-plugin port) declare non-distillable
subagents without dropping the field on construction.
Closes #1347
Co-Authored-By: Claude Opus 4.8 (1M context)
* test(subagents): strengthen allow_skill_emission preservation test (CodeRabbit #1348)
Seed the overlay base with allow_skill_emission=False (the non-default) so the
test fails if a refactor reconstructs SubagentConfig without carrying the flag
through — the prior assertion compared against the default True, so a
reset-to-default regression would have slipped past. Also pop the temp
researcher entry in cleanup when there was no original, so the test can't leave
the registry mutated for later tests.
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
graph/subagents/config.py | 8 +++++++
tests/test_subagent_model_config.py | 34 +++++++++++++++++++++++++++++
2 files changed, 42 insertions(+)
diff --git a/graph/subagents/config.py b/graph/subagents/config.py
index e3fef954..8389aba0 100644
--- a/graph/subagents/config.py
+++ b/graph/subagents/config.py
@@ -39,6 +39,14 @@ class SubagentConfig:
# the main model. Pin a subagent that needs heavy reasoning to the main
# model even when aux_model routes the others to a cheaper alias.
model: str = ""
+ # Whether this subagent's runs are eligible to become a reusable skill-v1
+ # artifact via the skill-emission path. When False, emission is suppressed for
+ # this subagent even if the caller requests it — set it on subagents whose
+ # output is context-specific, sensitive, or non-deterministic (per-invocation
+ # conformance/verdict reviewers, agents over private data) so one-off runs
+ # don't pollute the distilled-skills index. Default True preserves the
+ # current behavior; honored wherever a runtime wires per-task skill emission.
+ allow_skill_emission: bool = True
RESEARCHER_CONFIG = SubagentConfig(
diff --git a/tests/test_subagent_model_config.py b/tests/test_subagent_model_config.py
index a2bec3bd..31801776 100644
--- a/tests/test_subagent_model_config.py
+++ b/tests/test_subagent_model_config.py
@@ -84,6 +84,40 @@ def test_tools_and_max_turns_override_applies(tmp_path):
SUBAGENT_REGISTRY["researcher"] = original
+def test_allow_skill_emission_defaults_true_and_is_settable():
+ """The opt-out field (#1347) defaults True so stock subagents stay distillable,
+ and is settable so a plugin/fork can mark a subagent's runs non-emittable."""
+ from graph.subagents.config import SubagentConfig
+
+ assert RESEARCHER_CONFIG.allow_skill_emission is True
+ opted_out = SubagentConfig(name="verdict", description="d", system_prompt="p", allow_skill_emission=False)
+ assert opted_out.allow_skill_emission is False
+
+
+def test_config_overlay_preserves_allow_skill_emission(monkeypatch):
+ """A YAML model/tools override must not RESET allow_skill_emission to the dataclass
+ default — _apply_config_subagents rebuilds via replace(base, ...). Seed the base with
+ the flag OFF (the non-default) so this fails if a refactor reconstructs the config
+ without carrying it through, not just when it happens to match the default."""
+ import graph.subagents.config as sub_config
+
+ original = SUBAGENT_REGISTRY.get("researcher")
+ # _apply_config_subagents reads the module-level RESEARCHER_CONFIG as its base.
+ monkeypatch.setattr(sub_config, "RESEARCHER_CONFIG", dataclasses.replace(RESEARCHER_CONFIG, allow_skill_emission=False))
+ try:
+ cfg = LangGraphConfig()
+ cfg.researcher = dataclasses.replace(cfg.researcher, model="protolabs/reasoning")
+ _apply_config_subagents(cfg)
+ entry = SUBAGENT_REGISTRY["researcher"]
+ assert entry.model == "protolabs/reasoning" # overlay applied
+ assert entry.allow_skill_emission is False # base flag preserved, NOT reset to default True
+ finally:
+ if original is not None:
+ SUBAGENT_REGISTRY["researcher"] = original
+ else:
+ SUBAGENT_REGISTRY.pop("researcher", None) # don't leave a temp entry behind
+
+
def test_disabled_removes_subagent():
import dataclasses
From c25631f70350aa45a31cd29ea15bc13dbb0136e9 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 22:05:08 -0700
Subject: [PATCH 047/190] docs(fleet): note last-write-wins merge semantics in
_apply_bundle_config_defaults (#1351) (#1352)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Quinn's LOW review note on PR #1351: the per-bundle config merge uses dict.update(),
so two bundles shipping defaults for the same plugin section silently last-wins.
Document the expected behavior (a fresh workspace installs one archetype bundle, so
collisions don't arise in the create() path; lock order decides if they ever do).
Comment-only — no behavior change.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
graph/workspaces/manager.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/graph/workspaces/manager.py b/graph/workspaces/manager.py
index 4522e6ce..8b0d80a7 100644
--- a/graph/workspaces/manager.py
+++ b/graph/workspaces/manager.py
@@ -346,6 +346,11 @@ def _apply_bundle_config_defaults(cfg: Path, lock: Path) -> dict:
data = json.loads(lock.read_text()) if lock.exists() else {}
except (json.JSONDecodeError, OSError):
return {}
+ # Merge every bundle's `config` into one {section: {...}} map. Last-write-wins per
+ # section (`dict.update`): if two installed bundles ship defaults for the SAME plugin
+ # section, the later bundle's block replaces the earlier one's. That's acceptable —
+ # a fresh workspace installs a single archetype bundle, so collisions don't arise in
+ # the create() path; the order is lock order if they ever do.
merged: dict = {}
for b in data.get("bundles") or []:
merged.update(b.get("config") or {})
From a2c8ad4828871502aac25b8d6c99035258a31116 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 22:12:08 -0700
Subject: [PATCH 048/190] feat(web): compact plugin rows + Configure opens a
per-plugin settings dialog (#1353)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Settings ▸ Plugins list wasted vertical and horizontal space: each row's
action cluster stacked into a column (a latent `.subagent-row > div { display:
grid }` rule out-specified `.plugin-row-actions { display: flex }`), and every
action was a full text+icon button. Configure also grew the row with an inline
config form, pushing the list around as you edited.
- Action cluster is now a tight horizontal row (a two-class selector wins the
specificity fight that was stacking it). Secondary actions — Update, Configure,
Uninstall — are icon-only with tooltips + aria-labels; only the primary
Enable/Disable toggle keeps its label (now `size="sm"`). Uninstall icon goes
red on hover.
- Configure opens a new `PluginSettingsDialog` (reuses the DS `Dialog` like
InstallPluginDialog) instead of expanding the row. It wraps the SAME
pluginId-scoped `SettingsCategory`, so save / validate / restart behavior is
unchanged — only the container moved from an inline panel to a modal, giving
large schemas real room and keeping each row one line.
- Drops the inline `.plugin-row-config` expander state + CSS.
e2e: navigation spec updated to assert the dialog (role="dialog" named by the
plugin) instead of the inline `.plugin-row-config`. Unit (140) + plugin e2e
(22) green; tsc clean.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/navigation.spec.ts | 8 +++-
apps/web/src/plugins/PluginSettingsDialog.tsx | 30 ++++++++++++++
apps/web/src/plugins/PluginsSurface.tsx | 40 ++++++++++++-------
apps/web/src/settings/plugins.css | 10 ++++-
4 files changed, 69 insertions(+), 19 deletions(-)
create mode 100644 apps/web/src/plugins/PluginSettingsDialog.tsx
diff --git a/apps/web/e2e/navigation.spec.ts b/apps/web/e2e/navigation.spec.ts
index f213da5f..36f33a07 100644
--- a/apps/web/e2e/navigation.spec.ts
+++ b/apps/web/e2e/navigation.spec.ts
@@ -89,10 +89,14 @@ test("plugins section: Installed / Discover (config + advanced install folded in
.getByRole("button", { name: "Enable" }).click();
await expect(page.locator(".plugin-hint")).toContainText("Zzz Disabled enabled");
- // Config folded in (ADR 0059, bd-23a.3) — Demo Plugin's row exposes Configure → fields inline.
+ // Configure opens a per-plugin settings DIALOG now (2026-06) — not an inline row expander.
+ // The (icon-only) Configure button is labelled "Configure "; the dialog is
+ // role="dialog" named by the plugin.
const demoRow = page.locator(".plugin-row-wrap", { hasText: "Demo Plugin" });
await demoRow.getByRole("button", { name: "Configure" }).click();
- await expect(demoRow.locator('.plugin-row-config .setting-row[data-key="demo.greeting"]')).toBeVisible();
+ const configDialog = page.getByRole("dialog", { name: "Demo Plugin" });
+ await expect(configDialog.locator('.setting-row[data-key="demo.greeting"]')).toBeVisible();
+ await configDialog.locator(".pl-dialog__close").click(); // close so it doesn't overlay later steps
// Install-from-URL is a dialog opened from the Installed toolbar (2026-06 consolidation).
// The DS Dialog title is role="dialog" (not a heading); assert via the URL field, which
diff --git a/apps/web/src/plugins/PluginSettingsDialog.tsx b/apps/web/src/plugins/PluginSettingsDialog.tsx
new file mode 100644
index 00000000..42109974
--- /dev/null
+++ b/apps/web/src/plugins/PluginSettingsDialog.tsx
@@ -0,0 +1,30 @@
+import { Dialog } from "@protolabsai/ui/overlays";
+import { Suspense } from "react";
+
+import { SettingsCategory } from "../settings/SettingsCategory";
+
+// Per-plugin settings dialog (ADR 0059, supersedes the inline row expander). Configure
+// on a plugin row opens this instead of growing the row — the form gets real breathing
+// room for large schemas, and the row stays a single compact line. Reuses the same
+// `SettingsCategory` (pluginId-scoped) the inline form used, so save / validate / restart
+// behavior is unchanged; only the container moved from an inline panel to a modal.
+export function PluginSettingsDialog({
+ pluginId,
+ pluginName,
+ open,
+ onClose,
+}: {
+ pluginId: string;
+ pluginName: string;
+ open: boolean;
+ onClose: () => void;
+}) {
+ if (!open) return null;
+ return (
+
+ Loading settings…
}>
+
+
+
+ );
+}
diff --git a/apps/web/src/plugins/PluginsSurface.tsx b/apps/web/src/plugins/PluginsSurface.tsx
index 31a29c6e..a53b5373 100644
--- a/apps/web/src/plugins/PluginsSurface.tsx
+++ b/apps/web/src/plugins/PluginsSurface.tsx
@@ -4,7 +4,7 @@ import { Button } from "@protolabsai/ui/primitives";
import { Alert } from "@protolabsai/ui/data";
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
-import { Suspense, useState, type JSX } from "react";
+import { useState, type JSX } from "react";
import { Download, DownloadCloud, ExternalLink, Github, Loader2, RefreshCw, Search, Settings2, Store, Trash2 } from "lucide-react";
import { PanelHeader } from "@protolabsai/ui/navigation";
@@ -13,7 +13,7 @@ import { StagePanel } from "../app/ErrorBoundary";
import { errMsg } from "../lib/format";
import { StatusPill } from "../app/StatusPill";
import { InstallPluginDialog } from "./InstallPluginDialog";
-import { SettingsCategory } from "../settings/SettingsCategory";
+import { PluginSettingsDialog } from "./PluginSettingsDialog";
import { PluginFreshness } from "./PluginFreshness";
import { catalogCategories, filterCatalog } from "./catalog";
import { api } from "../lib/api";
@@ -59,7 +59,7 @@ function PluginRow({
removing: boolean;
}) {
const on = p.enabled;
- const [open, setOpen] = useState(false);
+ const [configOpen, setConfigOpen] = useState(false);
return (
@@ -71,6 +71,8 @@ function PluginRow({
{contributionsLabel(p)}
+ {/* Compact action cluster: secondary actions (update / configure / uninstall) are
+ icon-only with tooltips; only the primary Enable/Disable toggle keeps its label. */}
onUpdate(p)}
title={`Update ${p.name} to the latest commit`}
+ aria-label={`Update ${p.name}`}
>
- {updating ? : } Update
+ {updating ? : }
) : null}
- {/* Config folded in (ADR 0059, bd-23a.3) — expand to edit this plugin's settings inline. */}
+ {/* Configure opens a per-plugin settings dialog (ADR 0059) rather than expanding
+ the row, so the row stays one line and the form gets room. */}
{configurable ? (
setOpen((o) => !o)}
+ onClick={() => setConfigOpen(true)}
title={`Configure ${p.name}`}
+ aria-label={`Configure ${p.name}`}
>
- Configure
+
) : null}
onToggle(p)}
title={on ? `Disable ${p.name}` : `Enable ${p.name}`}
@@ -114,23 +121,26 @@ function PluginRow({
{removable ? (
onRemove(p)}
title={`Uninstall ${p.name}`}
aria-label={`uninstall ${p.id}`}
>
- {removing ? : } Uninstall
+ {removing ? : }
) : null}
- {configurable && open ? (
-
- Loading settings…}>
-
-
-
+ {configurable ? (
+ setConfigOpen(false)}
+ />
) : null}
);
diff --git a/apps/web/src/settings/plugins.css b/apps/web/src/settings/plugins.css
index b709f2f1..eccf74f8 100644
--- a/apps/web/src/settings/plugins.css
+++ b/apps/web/src/settings/plugins.css
@@ -74,8 +74,14 @@
.plugin-card-repo { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--fg-muted); text-decoration: none; }
.plugin-card-repo:hover { color: var(--fg-secondary); }
-/* Config folded into the Installed plugin rows (ADR 0059) — the Configure expander. */
+/* Installed plugin row. Configure now opens a per-plugin settings dialog (ADR 0059) —
+ the row is a single line; the icon action cluster shouldn't shrink under a long name. */
.plugin-row-wrap { display: flex; flex-direction: column; }
-.plugin-row-config { margin: 0 0 10px; padding: 4px 12px 10px; border: 1px solid var(--border); border-radius: 9px; background: color-mix(in srgb, var(--bg-raised) 55%, transparent); }
+/* Keep the action cluster a tight HORIZONTAL row. `.subagent-row > div { display: grid }`
+ (theme.css) otherwise out-specifies the base `.plugin-row-actions { display: flex }` and
+ stacks the buttons into a column — the bulk this rework is removing. This selector
+ (two classes) wins that specificity battle. */
+.subagent-row > .plugin-row-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
+.plugin-row-danger:hover:not(:disabled) { color: var(--danger); }
.settings-flat-group { display: flex; flex-direction: column; }
.plugin-install-advanced { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
From 5fe263e2f626cf2e1bd7fbffb6ef38aff84876dd Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 22:28:30 -0700
Subject: [PATCH 049/190] =?UTF-8?q?feat(web):=20declutter=20plugin=20rows?=
=?UTF-8?q?=20=E2=80=94=20drop=20redundant=20status=20badges,=20fix=20titl?=
=?UTF-8?q?e=20spacing=20(#1354)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Follow-up polish on the Plugins list. The rows carried too many indicators that
restated context the layout already gives:
- Drop the per-row status pill (loaded / enabled / disabled). Rows are already
grouped under "Loaded · N" / "Disabled · N" headers, so the pill just repeated
the section — and sat redundantly next to the Disable/Enable button. Only a
genuine load error now gets a badge (the detail still shows in the meta line).
- PluginFreshness no longer renders an "up to date" (or "pinned") badge — a badge
on every healthy plugin is pure noise. It shows only when actionable: "update
available" or "check failed". The Update action already only appears when behind.
- Give the title its own flex line (name · vX.Y.Z · badge) with real gaps — the
version and any badge were jammed straight against the name.
Net: most rows are now just "Name vX.Y.Z … Disable", which is the point.
Unit (140) + plugin e2e green; tsc clean.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/src/plugins/PluginFreshness.tsx | 23 +++++++++--------------
apps/web/src/plugins/PluginsSurface.tsx | 16 ++++++++--------
apps/web/src/settings/plugins.css | 5 +++++
3 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/apps/web/src/plugins/PluginFreshness.tsx b/apps/web/src/plugins/PluginFreshness.tsx
index ce97bf34..c95a2089 100644
--- a/apps/web/src/plugins/PluginFreshness.tsx
+++ b/apps/web/src/plugins/PluginFreshness.tsx
@@ -3,26 +3,21 @@ import { Badge } from "@protolabsai/ui/primitives";
import type { PluginUpdate } from "../lib/types";
// Shared freshness indicator (ADR 0027) — joins an installed plugin to its
-// update status (GET /api/plugins/updates) and renders a single DS Badge next to
-// the v{version}. Used by BOTH the Settings → Integrations list (PluginsSection)
-// and the Plugins rail Local tab (PluginsSurface) so the copy/tone stay identical.
+// update status (GET /api/plugins/updates) and renders a DS Badge next to the
+// v{version} ONLY when there's something to say. Used by BOTH the Settings →
+// Integrations list (PluginsSection) and the Plugins Local tab (PluginsSurface).
//
-// not behind → "up to date" (success)
// behind → "update available" (info)
-// pinned (SHA ref) → "pinned" (neutral, muted — never auto-updates)
// check errored → "check failed" (warning, error surfaced in the title)
+// up to date → nothing — the healthy default stays quiet (a badge on
+// every current plugin is just noise); the Update action
+// only appears when behind, so "up to date" needs no label
+// pinned (SHA ref) → nothing — it simply never offers an update
//
// `update` is undefined while the updates query is loading or unavailable (the
// query degrades gracefully) — render nothing then, the row is still complete.
export function PluginFreshness({ update }: { update?: PluginUpdate }) {
- if (!update) return null;
- if (update.pinned) {
- return (
-
- pinned
-
- );
- }
+ if (!update || update.pinned) return null;
if (update.error) {
return (
@@ -39,5 +34,5 @@ export function PluginFreshness({ update }: { update?: PluginUpdate }) {
);
}
- return up to date ;
+ return null; // up to date — no badge
}
diff --git a/apps/web/src/plugins/PluginsSurface.tsx b/apps/web/src/plugins/PluginsSurface.tsx
index a53b5373..5de5f908 100644
--- a/apps/web/src/plugins/PluginsSurface.tsx
+++ b/apps/web/src/plugins/PluginsSurface.tsx
@@ -64,20 +64,20 @@ function PluginRow({
-
- {p.name}
- {p.version ? v{p.version} : null}
+ {/* Name · version · (only-when-actionable) freshness/error badge. The loaded
+ vs disabled state is already the section this row sits under, so it carries
+ no per-row status pill — only a genuine error gets a badge. */}
+
+
{p.name}
+ {p.version ?
v{p.version} : null}
-
+ {p.error ?
: null}
+
{contributionsLabel(p)}
{/* Compact action cluster: secondary actions (update / configure / uninstall) are
icon-only with tooltips; only the primary Enable/Disable toggle keeps its label. */}
-
{update?.behind ? (
strong { min-width: 0; }
+.plugin-row-head .plugin-ver { font-weight: 400; }
/* Keep the action cluster a tight HORIZONTAL row. `.subagent-row > div { display: grid }`
(theme.css) otherwise out-specifies the base `.plugin-row-actions { display: flex }` and
stacks the buttons into a column — the bulk this rework is removing. This selector
From 3e32050654f40f835914091fefd1fcc13f288807 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Thu, 25 Jun 2026 22:41:23 -0700
Subject: [PATCH 050/190] fix(web): plugin views now pick up the theme on first
paint (handshake race) (#1355)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plugin-kit-hosted views often loaded with the kit's default theme instead of the
console's, fixed only by toggling the theme (which fires `protoagent:theme`). The
cause is a handshake race: the console posts `protoagent:init` (bearer + theme) on
the iframe's `load` event, but the plugin page registers its `message` listener
asynchronously (dynamic import of the plugin-kit), so the init post lands before the
kit is listening and is dropped — leaving the view unthemed until a manual switch.
Make the console robust to it:
- Re-post init on a short schedule after load ([100, 300, 700, 1500]ms). postInit is
idempotent (the kit just re-sets CSS vars), so the retry that lands once the kit is
listening themes it correctly. Timers are cleared on unmount / src change.
- Handle a `protoagent:ready` ping from the kit by re-sending init immediately — the
exact, race-free handshake. Older kits don't ping (the retry covers them); the
upstream @protolabsai/ui kit will ping (separate change), making this instant.
e2e: the mock plugin page now emits `protoagent:ready` like the real kit, exercising
the new console branch; the existing bearer+theme handshake test stays green.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/e2e/mock-server.mjs | 3 ++
apps/web/src/app/PluginView.tsx | 49 +++++++++++++++++++++++++--------
2 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/apps/web/e2e/mock-server.mjs b/apps/web/e2e/mock-server.mjs
index be7a3dc7..5b7e2399 100644
--- a/apps/web/e2e/mock-server.mjs
+++ b/apps/web/e2e/mock-server.mjs
@@ -660,6 +660,9 @@ const server = createServer(async (req, res) => {
`window.addEventListener("message",function(e){var m=e.data||{};` +
`if(m.type!=="protoagent:init")return;` +
`document.body.setAttribute("data-bridge",m.token?"authed":"anon");});` +
+ // Like the real plugin-kit: announce readiness so the console (re-)sends the
+ // bearer + theme, closing the race where load-time init beats our listener.
+ `try{parent&&parent!==window&&parent.postMessage({type:"protoagent:ready"},"*");}catch(_){}` +
``,
);
return;
diff --git a/apps/web/src/app/PluginView.tsx b/apps/web/src/app/PluginView.tsx
index 1c3bad05..f2bebcfe 100644
--- a/apps/web/src/app/PluginView.tsx
+++ b/apps/web/src/app/PluginView.tsx
@@ -47,8 +47,21 @@ export function PluginView({ view }: { view: PluginViewType }) {
// bare {"detail":"Not Found"} body as the "view".
const [reachable, setReachable] = useState(false);
const frameRef = useRef(null);
+ // Pending init re-post timers (see handleLoad) — cleared on unmount / src change.
+ const initTimers = useRef([]);
const pluginId = useMemo(() => pluginIdFromPath(view.path), [view.path]);
+ // Post the bearer + theme to the iframe. Idempotent on the kit side (applyTheme just
+ // re-sets CSS vars), so it's safe to call repeatedly — which the handshake relies on.
+ const postInit = (win: Window) => {
+ try {
+ const origin = new URL(apiUrl(src), window.location.href).origin;
+ win.postMessage({ type: "protoagent:init", token: authToken() || null, theme: consoleTheme() }, origin);
+ } catch {
+ /* cross-origin / detached — best effort */
+ }
+ };
+
// Probe the view URL before mounting the iframe. A same-origin HTTP error (a 404 from an
// unmounted /api/plugins//, FastAPI's {"detail":"Not Found"}) fires the iframe's
// onLoad — NOT onError — so trusting onLoad would render the raw 404 as a blank panel. We
@@ -127,7 +140,15 @@ export function PluginView({ view }: { view: PluginViewType }) {
// Only trust messages from THIS iframe's window.
if (!frameRef.current || e.source !== frameRef.current.contentWindow) return;
const m = e.data || {};
- if (m.type === "protoagent:subscribe" && Array.isArray(m.patterns)) {
+ if (m.type === "protoagent:ready") {
+ // The kit announced it's now listening. It registers its `message` handler
+ // asynchronously (dynamic import of the plugin-kit), so the load-time init post
+ // can race ahead of it and be dropped — leaving the view on the kit's default
+ // theme until a manual switch. Re-send the bearer + theme now that we know it's
+ // listening, so it themes immediately. (Older kits don't ping; handleLoad's
+ // retry covers those.)
+ if (frameRef.current?.contentWindow) postInit(frameRef.current.contentWindow);
+ } else if (m.type === "protoagent:subscribe" && Array.isArray(m.patterns)) {
patterns = m.patterns.filter((p: unknown) => typeof p === "string");
} else if (m.type === "protoagent:publish" && typeof m.topic === "string") {
// Force the plugin's namespace — a page can only publish under its own id.
@@ -156,6 +177,9 @@ export function PluginView({ view }: { view: PluginViewType }) {
return () => {
window.removeEventListener("message", onWindowMessage);
off();
+ // Drop any pending init re-posts — the iframe is being torn down / re-pointed.
+ initTimers.current.forEach(clearTimeout);
+ initTimers.current = [];
};
}, [src, pluginId]);
@@ -184,16 +208,19 @@ export function PluginView({ view }: { view: PluginViewType }) {
setLoaded(true);
const win = e.currentTarget.contentWindow;
if (!win) return;
- // Hand the page the bearer + theme AFTER load — same origin, targeted, not in
- // the URL. The plugin page listens for `message` and uses them.
- try {
- const origin = new URL(apiUrl(src), window.location.href).origin;
- win.postMessage(
- { type: "protoagent:init", token: authToken() || null, theme: consoleTheme() },
- origin,
- );
- } catch {
- /* cross-origin / detached — best effort */
+ // Hand the page the bearer + theme AFTER load — same origin, targeted, not in the URL.
+ // The plugin page registers its `message` listener asynchronously (dynamic import of the
+ // plugin-kit), so this first post can land BEFORE the kit is listening and be dropped —
+ // the view then renders with the kit's default theme until a manual theme switch (the
+ // "toggle around for it to load" bug). So re-post on a short schedule; the retry lands
+ // once the kit is ready, and postInit is idempotent so the extra posts are harmless. A
+ // newer kit that pings `protoagent:ready` makes this exact (handled above); the retry is
+ // the fallback for kits that only listen.
+ initTimers.current.forEach(clearTimeout);
+ initTimers.current = [];
+ postInit(win);
+ for (const ms of [100, 300, 700, 1500]) {
+ initTimers.current.push(window.setTimeout(() => postInit(win), ms));
}
}
From 8c65c403b8b31f761f2b0c216ff86a3fa9bc410f Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Fri, 26 Jun 2026 00:18:52 -0700
Subject: [PATCH 051/190] chore(web): bump @protolabsai/ui to 0.48.3
(plugin-kit ready handshake) (#1356)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
0.48.3 adds the `protoagent:ready` ping from the plugin-kit's initPluginView
(protoContent #337), which the console's PluginView already handles — so plugin
views now get an exact, race-free theme handshake instead of relying on the
post-load retry fallback. Surgical lockfile edit (deps unchanged across the patch);
`npm ci` validates clean. Unit (140) + plugin e2e green.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/package.json | 2 +-
package-lock.json | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/web/package.json b/apps/web/package.json
index 7ebbfc78..cf4f4b14 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -16,7 +16,7 @@
},
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.48.1",
+ "@protolabsai/ui": "^0.48.3",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
diff --git a/package-lock.json b/package-lock.json
index 094c1652..1c05fd2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"version": "0.1.0",
"dependencies": {
"@protolabsai/design": "^0.5.1",
- "@protolabsai/ui": "^0.48.1",
+ "@protolabsai/ui": "^0.48.3",
"@radix-ui/react-dropdown-menu": "^2.1.17",
"@tanstack/react-query": "^5.100.14",
"class-variance-authority": "^0.7.1",
@@ -1683,9 +1683,9 @@
}
},
"node_modules/@protolabsai/ui": {
- "version": "0.48.1",
- "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.48.1.tgz",
- "integrity": "sha512-ZjnYzurmN/vny/L7peKUDI/fSvAwSnHt14Jf4wGp5rVLbpbiNEdIuHRnVj6q3wZk4KNh777tEeNdxtbXOKRy2Q==",
+ "version": "0.48.3",
+ "resolved": "https://registry.npmjs.org/@protolabsai/ui/-/ui-0.48.3.tgz",
+ "integrity": "sha512-6sJ8Tcm1Ko+AJMjJxVW0fWOBQs/+hsF8D5g6Sn2P3g13qJ64afYchofQw3dnv5U/Qq9PLj56COKbTZATQ9JmxA==",
"license": "MIT",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
From ae053fb1b9361766f8e6743606d2554ffb320fc1 Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Fri, 26 Jun 2026 00:35:33 -0700
Subject: [PATCH 052/190] fix(coding_agent): find node/npx when the server runs
as a service (nvm/fnm PATH) (#1357)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The ACP runtime spawns node-based backends (the `claude` agent launches via `npx`),
resolving the binary against the server's inherited PATH. A protoAgent started as a
service — systemd, launchd, a bare nohup — gets a MINIMAL PATH that never picked up the
user's Node install: nvm/fnm/volta/asdf/nodenv all prepend their bin dir from the shell
rc, which a service never sources. So `npx` is "not found" even though it's installed —
the most common ACP launch failure ("agent binary not found: 'npx'").
`_launch_env` now augments the launch PATH: when `node` isn't already resolvable, it
discovers the version-manager node bin dirs (nvm/fnm versioned dirs newest-first, plus
volta/asdf/nodenv/homebrew shims) and APPENDS the ones that exist — so an explicitly-set
PATH still wins, and a service-launched server can still find npx. Discovery is cached
(one filesystem probe per process) and best-effort. The "binary not found" error also
now hints at the service-PATH gap for node tools.
Fixes the systemd+nvm case generally, so no per-host unit edit is needed.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
plugins/coding_agent/acp_client.py | 93 +++++++++++++++++++++++++++++-
tests/test_coding_agent_plugin.py | 75 +++++++++++++++++++++++-
2 files changed, 165 insertions(+), 3 deletions(-)
diff --git a/plugins/coding_agent/acp_client.py b/plugins/coding_agent/acp_client.py
index b5090421..2ded83b6 100644
--- a/plugins/coding_agent/acp_client.py
+++ b/plugins/coding_agent/acp_client.py
@@ -27,11 +27,14 @@
import asyncio
import contextlib
+import glob
import json
import logging
import os
import re
+import shutil
import signal
+from functools import lru_cache
from pathlib import Path
from typing import Awaitable, Callable
@@ -124,20 +127,106 @@ def _content_text(content) -> str:
_NESTED_CLAUDE_ENV_PREFIX = "CLAUDE_CODE_"
+# Node-based ACP backends (the `claude` agent launches via `npx`; codex-acp too) need
+# `node` on PATH. A protoAgent server started as a service — systemd, launchd, a bare
+# `nohup` — gets a MINIMAL PATH that never picked up the user's Node install: nvm / fnm /
+# volta / asdf / nodenv all prepend their bin dir from the *shell rc*, which a service
+# never sources. That's the #1 cause of "agent binary not found: 'npx'". So when `node`
+# isn't already resolvable on the launch PATH, we discover the version-manager bin dirs
+# and append them — making node tooling reachable regardless of how the server was started.
+_NODE_TOOL_BASENAMES = frozenset({"npx", "node", "npm", "pnpm", "yarn", "bun", "corepack"})
+
+
+def _version_sort_key(bin_dir: str) -> tuple[int, ...]:
+ """Sort key for a version-manager node `bin` dir, newest-first. The version is the
+ dir component holding the semver (``…/node/v22.22.0/bin`` → ``v22.22.0``; fnm's
+ ``…//installation/bin`` → the grandparent). Non-versioned dirs sort last."""
+ p = Path(bin_dir)
+ for name in (p.parent.name, p.parent.parent.name):
+ nums = re.findall(r"\d+", name)
+ if nums:
+ return tuple(int(n) for n in nums[:3])
+ return (0,)
+
+
+@lru_cache(maxsize=1)
+def _discovered_node_dirs() -> tuple[str, ...]:
+ """Existing node `bin` dirs from the common version managers + standard locations,
+ most-preferred first (newest version), for augmenting a minimal launch PATH. Only
+ dirs that actually contain a ``node`` executable are returned. Cached — the
+ filesystem probe runs once per process. Best-effort: any error yields fewer dirs."""
+ home = Path.home()
+ cands: list[str] = []
+
+ def _versioned(root: str | os.PathLike, pattern: str) -> None:
+ try:
+ hits = glob.glob(os.path.join(str(root), pattern))
+ except OSError:
+ return
+ cands.extend(sorted(hits, key=_version_sort_key, reverse=True))
+
+ # nvm: ~/.nvm/versions/node//bin (NVM_DIR overrides the location)
+ _versioned(os.environ.get("NVM_DIR") or home / ".nvm", "versions/node/*/bin")
+ # fnm: /node-versions//installation/bin
+ for fnm_root in (os.environ.get("FNM_DIR"), home / ".local/share/fnm", home / ".fnm"):
+ if fnm_root:
+ _versioned(fnm_root, "node-versions/*/installation/bin")
+ # Single shim/bin dirs: volta, asdf, nodenv, homebrew, common system.
+ cands.append(str(Path(os.environ.get("VOLTA_HOME") or home / ".volta") / "bin"))
+ cands.append(str(Path(os.environ.get("ASDF_DATA_DIR") or home / ".asdf") / "shims"))
+ cands.append(str(home / ".nodenv" / "shims"))
+ cands.extend(["/opt/homebrew/bin", "/usr/local/bin"])
+
+ out: list[str] = []
+ seen: set[str] = set()
+ for d in cands:
+ if d in seen:
+ continue
+ seen.add(d)
+ if os.path.exists(os.path.join(d, "node")) or os.path.exists(os.path.join(d, "node.exe")):
+ out.append(d)
+ return tuple(out)
+
+
def _launch_env(extra: dict[str, str] | None) -> dict[str, str]:
"""Build the subprocess environment for an ACP agent: the server's own ``os.environ``
with the nested-Claude markers stripped (see above), then the delegate's ``env``
overlaid last — so an operator who *deliberately* sets one of these in the delegate
- env still wins."""
+ env still wins. Finally, if ``node`` isn't resolvable on the resulting PATH, append the
+ discovered node version-manager dirs so a service-launched server can still find npx."""
env = {
k: v
for k, v in os.environ.items()
if k not in _NESTED_CLAUDE_ENV_EXACT and not k.startswith(_NESTED_CLAUDE_ENV_PREFIX)
}
env.update(extra or {})
+
+ path = env.get("PATH") or os.defpath
+ if shutil.which("node", path=path) is None:
+ # APPEND (not prepend): an explicitly-configured PATH still wins for anything it
+ # already provides; we only add fallbacks for what it's missing.
+ present = path.split(os.pathsep)
+ extra_dirs = [d for d in _discovered_node_dirs() if d not in present]
+ if extra_dirs:
+ env["PATH"] = os.pathsep.join([path, *extra_dirs])
return env
+def _missing_binary_message(command: str) -> str:
+ """The AcpError text for a launch that hit FileNotFoundError. For a node tool we add
+ the service-PATH hint, since a systemd/launchd server not seeing nvm/fnm node is by
+ far the most common cause."""
+ msg = f"agent binary not found: {command!r} (is it installed and on PATH?)"
+ if os.path.basename(command) in _NODE_TOOL_BASENAMES:
+ msg += (
+ " — if protoAgent runs as a service (systemd/launchd), its PATH likely doesn't "
+ "include your Node install: nvm/fnm/volta set PATH from your shell rc, which "
+ "services don't source. Add the node bin dir to the service PATH, or install "
+ "the agent's CLI on the system PATH."
+ )
+ return msg
+
+
class AcpError(Exception):
"""Any ACP transport / protocol failure. The caller speaks the message.
@@ -259,7 +348,7 @@ async def _start(self, *, open_session: bool = True) -> None:
limit=_STDOUT_LINE_LIMIT,
)
except FileNotFoundError as exc:
- raise AcpError(f"agent binary not found: {self.command!r} (is it installed and on PATH?)") from exc
+ raise AcpError(_missing_binary_message(self.command)) from exc
# The subprocess now exists. If the handshake raises OR the caller's wait_for
# cancels us mid-initialize (the health prober's 45s probe timeout), reap the
diff --git a/tests/test_coding_agent_plugin.py b/tests/test_coding_agent_plugin.py
index b1821019..bb65d559 100644
--- a/tests/test_coding_agent_plugin.py
+++ b/tests/test_coding_agent_plugin.py
@@ -18,7 +18,16 @@
import plugins.coding_agent as P
from plugins.coding_agent import _make_permission
-from plugins.coding_agent.acp_client import AcpClient, AcpError, _short_tool_name, _split_tool_title
+from plugins.coding_agent import acp_client
+from plugins.coding_agent.acp_client import (
+ AcpClient,
+ AcpError,
+ _launch_env,
+ _missing_binary_message,
+ _short_tool_name,
+ _split_tool_title,
+ _version_sort_key,
+)
def test_short_tool_name_peels_inline_args_and_mcp_source():
@@ -222,6 +231,70 @@ async def test_acp_client_missing_binary_raises_acp_error(tmp_path):
await client.prompt("hi", timeout=10.0)
+# ── PATH augmentation for service-launched servers (nvm/fnm node not on PATH) ────────
+
+
+def _fake_node_dir(tmp_path) -> str:
+ """A dir that looks like a node bin dir (has an executable `node`)."""
+ d = tmp_path / "nodebin"
+ d.mkdir()
+ node = d / "node"
+ node.write_text("#!/bin/sh\n")
+ node.chmod(0o755)
+ return str(d)
+
+
+def test_launch_env_appends_node_dir_when_node_missing(tmp_path, monkeypatch):
+ """A service PATH that can't see node gets the discovered node dirs appended, so an
+ ACP agent that launches via `npx` can still find node — the systemd/nvm fix (#1)."""
+ node_dir = _fake_node_dir(tmp_path)
+ monkeypatch.setattr(acp_client, "_discovered_node_dirs", lambda: (node_dir,))
+ monkeypatch.setenv("PATH", "/usr/bin:/bin") # minimal, no node
+ env = _launch_env(None)
+ assert env["PATH"].endswith(node_dir) # appended, not prepended
+ assert env["PATH"].startswith("/usr/bin:/bin") # original PATH preserved & wins
+
+
+def test_launch_env_leaves_path_when_node_already_resolvable(tmp_path, monkeypatch):
+ """If node is already on PATH, don't touch it — no override of a working setup."""
+ node_dir = _fake_node_dir(tmp_path)
+ extra_dir = str(tmp_path / "should-not-be-added")
+ monkeypatch.setattr(acp_client, "_discovered_node_dirs", lambda: (extra_dir,))
+ monkeypatch.setenv("PATH", node_dir) # node IS resolvable here
+ env = _launch_env(None)
+ assert env["PATH"] == node_dir # untouched
+ assert extra_dir not in env["PATH"]
+
+
+def test_launch_env_no_node_dirs_discovered_is_safe(monkeypatch):
+ """Nothing discovered → PATH is left as-is (the error path still fires later)."""
+ monkeypatch.setattr(acp_client, "_discovered_node_dirs", lambda: ())
+ monkeypatch.setenv("PATH", "/usr/bin:/bin")
+ assert _launch_env(None)["PATH"] == "/usr/bin:/bin"
+
+
+def test_missing_binary_message_hints_at_service_path_for_node_tools():
+ """The error for a node tool points at the service-PATH gap; a non-node binary doesn't."""
+ npx = _missing_binary_message("npx")
+ assert "npx" in npx and "service" in npx and "nvm" in npx
+ # the hint also fires when the command is an absolute path to a node tool
+ assert "nvm" in _missing_binary_message("/home/u/.nvm/versions/node/v22/bin/npx")
+ codex = _missing_binary_message("codex")
+ assert "codex" in codex and "service" not in codex # not a node tool → no node hint
+
+
+def test_version_sort_key_orders_newest_first():
+ """nvm/fnm version dirs sort newest-first so the preferred node wins on PATH."""
+ dirs = [
+ "/h/.nvm/versions/node/v18.20.0/bin",
+ "/h/.nvm/versions/node/v22.22.0/bin",
+ "/h/.nvm/versions/node/v20.16.0/bin",
+ ]
+ assert sorted(dirs, key=_version_sort_key, reverse=True)[0].endswith("v22.22.0/bin")
+ # fnm layout (version is the grandparent of bin) still parses
+ assert _version_sort_key("/h/.fnm/node-versions/v21.7.0/installation/bin") == (21, 7, 0)
+
+
async def test_acp_client_bad_workdir_raises_acp_error():
client = AcpClient(sys.executable, [], cwd="/no/such/dir/anywhere")
with pytest.raises(AcpError):
From e69f24b53d16675c70ed00eb8bf7343759912dea Mon Sep 17 00:00:00 2001
From: Josh Mabry <31560031+mabry1985@users.noreply.github.com>
Date: Fri, 26 Jun 2026 01:17:35 -0700
Subject: [PATCH 053/190] feat(plugins): live config accessor + comma-separated
string_list inputs (#1358)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two plugin-config ergonomics fixes, both surfaced by the GitHub board plugin.
1. PluginRegistry.live_config() — re-reads the plugin's resolved config from
STATE.graph_config.plugin_config[section] on each call, falling back to the
register-time snapshot. A mounted router/surface can read it per request to
reflect config saves WITHOUT a restart (a hot-reload rebuilds graph_config but
can't re-mount a FastAPI router). The loader now passes config_section so the
lookup key is right even when config_section != id.
2. string_list settings inputs are comma-friendly. The field was a controlled
textarea that re-derived its text from value.join("\n") every keystroke — so a
trailing separator was eaten by filter(Boolean) and you literally couldn't add a
second item. Extracted StringListInput with its own raw-text state (parse on
change, re-sync only on an external value change), split on comma OR newline,
and display comma-separated. Adding multiple repos/dirs now works.
Co-authored-by: GitHub CI
Co-authored-by: Claude Opus 4.8 (1M context)
---
apps/web/src/settings/SettingsCategory.tsx | 57 +++++++++++++++++-----
apps/web/src/settings/stringList.test.ts | 18 +++++++
graph/plugins/loader.py | 2 +-
graph/plugins/registry.py | 31 +++++++++++-
tests/test_plugins.py | 25 ++++++++++
5 files changed, 118 insertions(+), 15 deletions(-)
create mode 100644 apps/web/src/settings/stringList.test.ts
diff --git a/apps/web/src/settings/SettingsCategory.tsx b/apps/web/src/settings/SettingsCategory.tsx
index 15d1f589..02773e36 100644
--- a/apps/web/src/settings/SettingsCategory.tsx
+++ b/apps/web/src/settings/SettingsCategory.tsx
@@ -6,7 +6,7 @@ import { Badge, Button } from "@protolabsai/ui/primitives";
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { Loader2, RotateCcw, Save } from "lucide-react";
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import type { ReactNode } from "react";
import { Accordion, AccordionItem, PanelHeader } from "@protolabsai/ui/navigation";
@@ -434,6 +434,49 @@ function SettingRow({
);
}
+// A free-form string list (repos, allowed dirs, …) edited as ONE text field. Items are
+// separated by a comma OR a newline — both work, so you can paste "a, b, c" or one per
+// line. It keeps its own raw text while focused so a separator isn't eaten mid-type (the
+// old controlled `value={list.join(...)}` re-derived the text every keystroke, dropping
+// the trailing separator via filter(Boolean) — which made adding a 2nd item impossible).
+// Parsed → a clean string[] on every change; re-syncs from `value` on an external change
+// (discard / reset / load).
+// Split a string-list text field into clean items: comma OR newline separated, trimmed,
+// empties dropped — so "a, b", "a\nb", and "a , , b\n" all yield ["a","b"].
+export function parseStringList(text: string): string[] {
+ return text.split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
+}
+
+function StringListInput({ id, value, onChange }: { id: string; value: unknown; onChange: (v: unknown) => void }) {
+ const items = Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
+ const [text, setText] = useState(items.join(", "));
+ const lastParsed = useRef(items);
+ useEffect(() => {
+ // Adopt the parent value only when it changed to something we didn't just emit
+ // (e.g. Discard reverted it) — otherwise leave the user's in-progress text alone.
+ if (JSON.stringify(items) !== JSON.stringify(lastParsed.current)) {
+ setText(items.join(", "));
+ lastParsed.current = items;
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value]);
+ return (
+