diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index e9208ff..a82b82a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -17,7 +17,7 @@ jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: uipath-ubuntu-latest needs: - lint @@ -30,15 +30,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version-file: ".python-version" @@ -49,14 +49,14 @@ jobs: run: uv build - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: release-dists path: dist/ pypi-publish: name: Upload release to PyPI - runs-on: ubuntu-latest + runs-on: uipath-ubuntu-latest environment: pypi needs: @@ -67,10 +67,10 @@ jobs: steps: - name: Retrieve release distributions - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: release-dists path: dist/ - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 8562e45..d88c491 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -6,18 +6,18 @@ on: jobs: commitlint: name: Commit Lint - runs-on: ubuntu-latest + runs-on: uipath-ubuntu-latest permissions: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1 with: node-version: 22 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bd659a3..b06b51d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,31 +1,31 @@ -name: Deploy - -on: - push: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - deploy: - environment: website-prod - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Azure login - uses: azure/login@v2 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Install Azure CLI - uses: pietrobolcato/install-azure-cli-action@main - - - name: Run Deploy.ps1 - shell: pwsh - run: ./Deploy.ps1 ` - -resourceGroupName ${{ secrets.AZURE_RESOURCE_GROUP }} ` - -appName ${{ secrets.AZURE_APP_NAME }} ` - -acrName ${{ secrets.AZURE_ACR_NAME }} +name: Deploy + +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + deploy: + environment: website-prod + runs-on: uipath-ubuntu-latest + + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Azure login + uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Install Azure CLI + uses: pietrobolcato/install-azure-cli-action@161e654d6c82d50576f1b32adbeea9433bf2a0ea # main + + - name: Run Deploy.ps1 + shell: pwsh + run: ./Deploy.ps1 ` + -resourceGroupName ${{ secrets.AZURE_RESOURCE_GROUP }} ` + -appName ${{ secrets.AZURE_APP_NAME }} ` + -acrName ${{ secrets.AZURE_ACR_NAME }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d259d94..00568c0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,21 +6,21 @@ on: jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: uipath-ubuntu-latest permissions: contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version-file: ".python-version" diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index ae0907d..4d712df 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -6,7 +6,7 @@ on: jobs: publish-dev: - runs-on: ubuntu-latest + runs-on: uipath-ubuntu-latest permissions: contents: read pull-requests: write @@ -16,15 +16,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: enable-cache: true - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version-file: ".python-version" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96de3f5..6644878 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,10 +6,14 @@ on: jobs: test: name: Test - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.os == 'windows-latest' && 'uipath-windows-latest' || 'uipath-ubuntu-latest' }} strategy: matrix: python-version: ["3.11", "3.12", "3.13"] + # Stock labels on purpose: GitHub builds the check-run name from these + # matrix values, and branch protection requires the stock names + # ("Test (3.11, ubuntu-latest)"). runs-on below maps them onto the + # uipath- pool, so the jobs run on uipath runners either way. os: [ubuntu-latest, windows-latest] permissions: @@ -17,13 +21,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index c1cbcb6..dcdedca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,19 @@ [project] name = "uipath-dev" -version = "0.0.59" +version = "0.0.85" description = "UiPath Developer Console" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ - "uipath-runtime>=0.9.0, <0.10.0", + "uipath-runtime>=0.13.0, <0.14.0", "textual>=7.5.0, <8.0.0", "pyperclip>=1.11.0, <2.0.0", "fastapi>=0.128.8", "uvicorn[standard]>=0.40.0", + "uipath>=2.14.1, <2.15.0", + "aiosqlite>=0.20.0", + "pywinpty>=2.0.0; sys_platform == 'win32'", + "mcp[cli]>=1.0.0", ] classifiers = [ "Intended Audience :: Developers", @@ -31,6 +35,7 @@ Documentation = "https://uipath.github.io/uipath-python/" [project.scripts] uipath-dev = "uipath.dev.__mock__:main" uipath-dev-server = "uipath.dev.__mock_server__:main" +uipath-dev-mcp = "uipath.dev.mcp:main" [build-system] requires = ["hatchling"] @@ -57,6 +62,7 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["src/uipath"] artifacts = ["src/uipath/dev/server/static/**"] +exclude = ["src/uipath/dev/server/frontend/**"] [tool.hatch.build.targets.sdist] exclude = ["demo"] @@ -99,6 +105,10 @@ no_implicit_reexport = true disallow_untyped_defs = false +[[tool.mypy.overrides]] +module = "uipath.dev.services.cli_agent.pty_session" +warn_unused_ignores = false + [tool.pydantic-mypy] init_forbid_extra = true init_typed = true diff --git a/src/uipath/dev/mcp/__init__.py b/src/uipath/dev/mcp/__init__.py new file mode 100644 index 0000000..c391fab --- /dev/null +++ b/src/uipath/dev/mcp/__init__.py @@ -0,0 +1,350 @@ +"""UiPath Dev MCP Server — exposes dev server run tools via stdio.""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import httpx +import websockets +from mcp.server.fastmcp import Context, FastMCP + +mcp = FastMCP( + "uipath-dev", + instructions=( + "This MCP server is the ONLY way to run, test, or execute agents, " + "functions, and projects. Do NOT use shell commands like " + "`uipath run`, `langgraph dev`, `python -m`, or any subprocess. " + "Always use the tools provided here instead." + ), +) + + +def _get_port() -> str: + """Read port from .uipath/dev-server.port if present, else fall back to env var.""" + from pathlib import Path + + port_file = Path(".uipath") / "dev-server.port" + if port_file.exists(): + return port_file.read_text().strip() + return os.environ.get("UIPATH_DEV_SERVER_PORT", "8080") + + +def _base_url() -> str: + host = os.environ.get("UIPATH_DEV_SERVER_HOST", "localhost") + return f"http://{host}:{_get_port()}" + + +def _ws_url() -> str: + host = os.environ.get("UIPATH_DEV_SERVER_HOST", "localhost") + return f"ws://{host}:{_get_port()}/ws" + + +def _api_url(path: str) -> str: + return f"{_base_url()}/api{path}" + + +async def _report_tool_call(tool: str, args: dict[str, Any] | None = None) -> None: + """Notify the dev server that an MCP tool was invoked.""" + try: + async with httpx.AsyncClient() as client: + await client.post( + _api_url("/mcp/events"), + json={"tool": tool, "args": args or {}}, + timeout=5, + ) + except Exception: + pass # Best-effort — don't fail the tool if reporting fails + + +@mcp.tool() +async def list_entrypoints() -> list[dict[str, Any]]: + """List all runnable agents, functions, and projects. + + Call this first to discover what is available to run. + Use the returned names with get_entrypoint_schema or run_entrypoint. + """ + await _report_tool_call("list_entrypoints") + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url("/entrypoints"), timeout=10) + resp.raise_for_status() + return resp.json() + + +@mcp.tool() +async def get_entrypoint_schema(entrypoint: str) -> dict[str, Any]: + """Get the input/output schema for an entrypoint. + + Args: + entrypoint: Name of the entrypoint (from list_entrypoints). + + Returns the JSON schema describing the entrypoint's expected + input and output. Use this to construct input_data for run_entrypoint. + """ + await _report_tool_call("get_entrypoint_schema", {"entrypoint": entrypoint}) + async with httpx.AsyncClient() as client: + resp = await client.get( + _api_url(f"/entrypoints/{entrypoint}/schema"), + timeout=30, + ) + resp.raise_for_status() + return resp.json() + + +@mcp.tool() +async def run_entrypoint( + entrypoint: str, + ctx: Context, # type: ignore[type-arg] + input_data: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Run an agent, function, or project. + + This is the ONLY correct way to execute code in this project — never + use shell commands. + + Args: + entrypoint: Name of the entrypoint (from list_entrypoints). + input_data: Input data matching the entrypoint's input schema. + Use get_entrypoint_schema to see what fields are expected. + Defaults to empty dict if not provided. + + Executes on the dev server with real-time graph visualization in the + browser. Streams progress as nodes execute, then returns the result. + """ + await _report_tool_call( + "run_entrypoint", {"entrypoint": entrypoint, "input_data": input_data} + ) + async with httpx.AsyncClient() as client: + resp = await client.post( + _api_url("/runs"), + json={ + "entrypoint": entrypoint, + "input_data": input_data or {}, + "mode": "run", + }, + timeout=10, + ) + resp.raise_for_status() + run: dict[str, Any] = resp.json() + run_id = run["id"] + + await ctx.log("info", f"Run {run_id} created — streaming updates...") + + # Connect to WebSocket and subscribe to run events + terminal_statuses = {"completed", "failed", "suspended"} + final_status = None + + async with websockets.connect(_ws_url()) as ws: + # Subscribe to this run's events + await ws.send(json.dumps({"type": "subscribe", "payload": {"run_id": run_id}})) + + async for raw in ws: + msg = json.loads(raw) + msg_type = msg.get("type", "") + payload = msg.get("payload", {}) + + if msg_type == "run.updated" and payload.get("id") == run_id: + status = payload.get("status", "") + await ctx.report_progress( + progress=1 if status in terminal_statuses else 0, + total=1, + message=f"Run status: {status}", + ) + if status in terminal_statuses: + final_status = status + break + + elif msg_type == "state" and payload.get("run_id") == run_id: + node = payload.get("node_name", "") + phase = payload.get("phase", "") + if phase == "started": + await ctx.log("info", f"Node '{node}' started") + elif phase == "completed": + await ctx.log("info", f"Node '{node}' completed") + + elif msg_type == "log" and payload.get("run_id") == run_id: + level = payload.get("level", "info").lower() + message = payload.get("message", "") + await ctx.log(level, message) + + # Fetch final run result + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url(f"/runs/{run_id}"), timeout=10) + resp.raise_for_status() + result: dict[str, Any] = resp.json() + + if final_status == "failed": + error = result.get("error", {}) + await ctx.log("error", f"Run failed: {error.get('detail', 'unknown error')}") + else: + await ctx.log("info", f"Run {final_status}: {run_id}") + + return result + + +@mcp.tool() +async def get_run_status(run_id: str) -> dict[str, Any]: + """Get current status and details of a run. + + Args: + run_id: The run ID returned by run_entrypoint. + + Returns full run details including status, output, traces, and logs. + """ + await _report_tool_call("get_run_status", {"run_id": run_id}) + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url(f"/runs/{run_id}"), timeout=10) + resp.raise_for_status() + return resp.json() + + +def _summarize_eval_run(result: dict[str, Any]) -> dict[str, Any]: + """Return only scores and justifications per item — drop everything else.""" + items = [ + { + "name": item.get("name"), + "status": item.get("status"), + "scores": item.get("scores", {}), + "justifications": item.get("justifications", {}), + "overall_score": item.get("overall_score"), + "error": (item["error"][:100] + "...") + if item.get("error") and len(item["error"]) > 100 + else item.get("error"), + } + for item in result.get("results", []) + ] + return { + "id": result.get("id"), + "status": result.get("status"), + "overall_score": result.get("overall_score"), + "evaluator_scores": result.get("evaluator_scores", {}), + "results": items, + } + + +@mcp.tool() +async def list_eval_sets() -> list[dict[str, Any]]: + """List all evaluation sets. + + Returns the available eval sets with their IDs, names, item counts, + and attached evaluator IDs. Use the returned IDs with run_eval_set. + """ + await _report_tool_call("list_eval_sets") + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url("/eval-sets"), timeout=10) + resp.raise_for_status() + return resp.json() + + +@mcp.tool() +async def get_eval_set(eval_set_id: str) -> dict[str, Any]: + """Get full details of an evaluation set including all items. + + Args: + eval_set_id: ID of the eval set (from list_eval_sets). + + Returns the eval set with all items, their inputs, expected outputs, + and evaluation criteria. + """ + await _report_tool_call("get_eval_set", {"eval_set_id": eval_set_id}) + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url(f"/eval-sets/{eval_set_id}"), timeout=10) + resp.raise_for_status() + return resp.json() + + +@mcp.tool() +async def run_eval_set( + eval_set_id: str, + ctx: Context, # type: ignore[type-arg] +) -> dict[str, Any]: + """Run an evaluation set against the agent. + + Args: + eval_set_id: ID of the eval set (from list_eval_sets). + + Starts the eval run and streams progress as each item completes. + Returns the full run result with per-item scores and overall score. + """ + await _report_tool_call("run_eval_set", {"eval_set_id": eval_set_id}) + async with httpx.AsyncClient() as client: + resp = await client.post(_api_url(f"/eval-sets/{eval_set_id}/runs"), timeout=30) + resp.raise_for_status() + run: dict[str, Any] = resp.json() + run_id = run["id"] + + await ctx.log("info", f"Eval run {run_id} created — streaming progress...") + + async with websockets.connect(_ws_url()) as ws: + async for raw in ws: + msg = json.loads(raw) + msg_type = msg.get("type", "") + payload = msg.get("payload", {}) + + if msg_type == "eval_run.progress" and payload.get("run_id") == run_id: + completed = payload.get("completed", 0) + total = payload.get("total", 0) + item = payload.get("item_result") + if item: + name = item.get("name", "") + status = item.get("status", "") + score = item.get("overall_score") + score_str = f" — score: {score:.0%}" if score is not None else "" + await ctx.log( + "info", f" [{completed}/{total}] {name}: {status}{score_str}" + ) + await ctx.report_progress(progress=completed, total=total) + + elif msg_type == "eval_run.completed" and payload.get("run_id") == run_id: + overall = payload.get("overall_score") + if overall is not None: + await ctx.log( + "info", f"Eval run completed — overall score: {overall:.0%}" + ) + break + + # Fetch final run detail + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url(f"/eval-runs/{run_id}"), timeout=10) + resp.raise_for_status() + result = resp.json() + + return _summarize_eval_run(result) + + +@mcp.tool() +async def list_eval_runs() -> list[dict[str, Any]]: + """List all evaluation runs. + + Returns summaries of all eval runs with their status, scores, + and progress. Use run IDs with get_eval_run for full details. + """ + await _report_tool_call("list_eval_runs") + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url("/eval-runs"), timeout=10) + resp.raise_for_status() + return resp.json() + + +@mcp.tool() +async def get_eval_run(eval_run_id: str) -> dict[str, Any]: + """Get full details of an evaluation run including per-item results. + + Args: + eval_run_id: ID of the eval run. + + Returns per-item evaluator scores and justifications. + """ + await _report_tool_call("get_eval_run", {"eval_run_id": eval_run_id}) + async with httpx.AsyncClient() as client: + resp = await client.get(_api_url(f"/eval-runs/{eval_run_id}"), timeout=10) + resp.raise_for_status() + result = resp.json() + + return _summarize_eval_run(result) + + +def main() -> None: + """Entry point for the uipath-dev-mcp CLI command.""" + mcp.run(transport="stdio") diff --git a/src/uipath/dev/models/chat.py b/src/uipath/dev/models/chat.py index ef14139..920fb96 100644 --- a/src/uipath/dev/models/chat.py +++ b/src/uipath/dev/models/chat.py @@ -40,7 +40,6 @@ def add( role=self.get_role(event), content_parts=[], tool_calls=[], - interrupts=[], created_at=self.get_timestamp(event), updated_at=self.get_timestamp(event), ) @@ -205,7 +204,6 @@ def get_user_message(user_text: str) -> UiPathConversationMessage: ) ], tool_calls=[], - interrupts=[], role="user", ) diff --git a/src/uipath/dev/models/eval_data.py b/src/uipath/dev/models/eval_data.py new file mode 100644 index 0000000..8fda331 --- /dev/null +++ b/src/uipath/dev/models/eval_data.py @@ -0,0 +1,132 @@ +"""Data models for evaluation runs.""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any + + +@dataclass +class EvalSetInfo: + """Summary of a discovered evaluation set.""" + + id: str + name: str + eval_count: int + evaluator_ids: list[str] + + +@dataclass +class EvalItemResult: + """Result of evaluating a single item.""" + + name: str + inputs: dict[str, Any] = field(default_factory=dict) + expected_output: Any = None + scores: dict[str, float] = field(default_factory=dict) + overall_score: float = 0.0 + output: Any = None + justifications: dict[str, str] = field(default_factory=dict) + duration_ms: float | None = None + status: str = "pending" # pending | running | completed | failed + error: str | None = None + traces: list[dict[str, Any]] = field(default_factory=list) + + +@dataclass +class EvalRunState: + """Full state of an eval run.""" + + id: str = field(default_factory=lambda: str(uuid.uuid4())) + eval_set_id: str = "" + eval_set_name: str = "" + status: str = "pending" # pending | running | completed | failed + progress_completed: int = 0 + progress_total: int = 0 + overall_score: float | None = None + evaluator_scores: dict[str, float] = field(default_factory=dict) + results: list[EvalItemResult] = field(default_factory=list) + start_time: datetime | None = None + end_time: datetime | None = None + + def to_summary(self) -> dict[str, Any]: + """Serialize to summary dict (no per-item results).""" + return { + "id": self.id, + "eval_set_id": self.eval_set_id, + "eval_set_name": self.eval_set_name, + "status": self.status, + "progress_completed": self.progress_completed, + "progress_total": self.progress_total, + "overall_score": self.overall_score, + "evaluator_scores": self.evaluator_scores, + "start_time": self.start_time.isoformat() if self.start_time else None, + "end_time": self.end_time.isoformat() if self.end_time else None, + } + + def to_detail(self) -> dict[str, Any]: + """Serialize to detail dict (includes per-item results).""" + base = self.to_summary() + base["results"] = [ + { + "name": r.name, + "inputs": r.inputs, + "expected_output": r.expected_output, + "scores": r.scores, + "overall_score": r.overall_score, + "output": str(r.output) + if isinstance(r.output, Exception) + else r.output, + "justifications": r.justifications, + "duration_ms": r.duration_ms, + "status": r.status, + "error": r.error, + "traces": r.traces, + } + for r in self.results + ] + return base + + def start(self) -> None: + """Mark run as started.""" + self.status = "running" + self.start_time = datetime.now(timezone.utc) + + def complete(self) -> None: + """Mark run as completed, computing final scores.""" + self.status = "completed" + self.end_time = datetime.now(timezone.utc) + self._compute_scores() + + def fail(self) -> None: + """Mark run as failed.""" + self.status = "failed" + self.end_time = datetime.now(timezone.utc) + + def _compute_scores(self) -> None: + """Compute overall and per-evaluator scores from item results.""" + scored = [r for r in self.results if r.status in ("completed", "failed")] + if not scored: + self.overall_score = 0.0 + return + + # Per-evaluator averages (failed items count as 0 for each evaluator) + evaluator_totals: dict[str, list[float]] = {} + for r in scored: + for ev_id, score in r.scores.items(): + evaluator_totals.setdefault(ev_id, []).append(score) + + failed = [r for r in scored if r.status == "failed"] + for ev_id in evaluator_totals: + for _ in failed: + evaluator_totals[ev_id].append(0.0) + + self.evaluator_scores = { + ev_id: sum(scores) / len(scores) + for ev_id, scores in evaluator_totals.items() + } + + # Overall = average of item overall_scores + self.overall_score = sum(r.overall_score for r in scored) / len(scored) diff --git a/src/uipath/dev/server/__init__.py b/src/uipath/dev/server/__init__.py index 2e3ba31..af3d30d 100644 --- a/src/uipath/dev/server/__init__.py +++ b/src/uipath/dev/server/__init__.py @@ -11,6 +11,7 @@ import time import webbrowser from collections.abc import Callable +from pathlib import Path from typing import Any import uvicorn @@ -19,13 +20,15 @@ from uipath.dev.models.data import ( ChatData, - InterruptData, LogData, StateData, TraceData, ) +from uipath.dev.models.eval_data import EvalItemResult, EvalRunState from uipath.dev.models.execution import ExecutionRun from uipath.dev.server.debug_bridge import WebDebugBridge +from uipath.dev.services.cli_agent import CliAgentService +from uipath.dev.services.eval_service import EvalService from uipath.dev.services.run_service import RunService logger = logging.getLogger(__name__) @@ -81,11 +84,25 @@ def __init__( on_trace=self._on_trace, on_chat=self._on_chat, on_state=self._on_state, - on_interrupt=self._on_interrupt, debug_bridge_factory=lambda mode: WebDebugBridge(mode=mode), on_run_removed=self.connection_manager.remove_run_subscriptions, ) + self.eval_service = EvalService( + runtime_factory=self.runtime_factory, + trace_manager=self.trace_manager, + on_eval_run_created=self._on_eval_run_created, + on_eval_run_progress=self._on_eval_run_progress, + on_eval_run_completed=self._on_eval_run_completed, + ) + + self.cli_agent_service = CliAgentService( + on_output=self._on_cli_agent_output, + on_exit=self._on_cli_agent_exit, + server_port=self.port, + server_host=self.host, + ) + def create_app(self) -> Any: """Create and return a FastAPI application.""" from uipath.dev.server.app import create_app @@ -102,6 +119,11 @@ async def run_async(self) -> None: self.port = self._find_free_port(self.host, self.port) app = self.create_app() + # write the server port to .uipath/dev-server.port so the MCP can discover it + port_file = Path(".uipath") / "dev-server.port" + port_file.parent.mkdir(exist_ok=True) + port_file.write_text(str(self.port)) + base_url = f"http://{self.host}:{self.port}" self._print_banner(base_url) @@ -111,9 +133,8 @@ async def run_async(self) -> None: daemon=True, ).start() - # Start file watcher if factory_creator is available - if self.factory_creator is not None: - self._start_watcher() + # Start file watcher for editor auto-refresh and factory hot-reload + self._start_watcher() config = uvicorn.Config( app, @@ -122,12 +143,19 @@ async def run_async(self) -> None: log_level="warning", ) server = uvicorn.Server(config) - await server.serve() + try: + await server.serve() + finally: + await self.shutdown() async def shutdown(self) -> None: """Clean up resources before shutting down.""" logger.info("Shutting down server resources...") + port_file = Path(".uipath") / "dev-server.port" + port_file.unlink(missing_ok=True) self._stop_watcher() + # Stop any active CLI agent PTY sessions + await self.cli_agent_service.stop_all_sessions() # Close any active WebSocket connections await self.connection_manager.disconnect_all() # Give threads time to finish @@ -171,20 +199,50 @@ async def reload_factory(self) -> None: del sys.modules[name] logger.debug("Flushed %d user modules", len(to_remove)) - # Recreate factory + # Recreate factory — preserve state file across reloads so the dev + # server doesn't delete state.db while other processes may use it. self.runtime_factory = self.factory_creator() + try: + self.runtime_factory.context.keep_state_file = True # type: ignore[attr-defined] + except AttributeError: + pass self.run_service.runtime_factory = self.runtime_factory await self.run_service.apply_factory_settings() self.reload_pending = False logger.debug("Factory reloaded successfully") + async def _auto_reload(self, py_files: list[str]) -> None: + """Auto-reload factory when Python files change. + + If runs are active, broadcast reload event so the frontend shows a + manual-reload prompt instead. + """ + active = [ + r + for r in self.run_service.runs.values() + if r.status in ("pending", "running") + ] + if active: + logger.debug("Runs in progress — deferring reload to user") + self.reload_pending = True + self.connection_manager.broadcast_reload(py_files) + return + + try: + await self.reload_factory() + self.connection_manager.broadcast_reload(py_files, reloaded=True) + except Exception: + logger.warning("Auto-reload failed", exc_info=True) + self.reload_pending = True + self.connection_manager.broadcast_reload(py_files) + def _start_watcher(self) -> None: """Start the file watcher background task.""" - from uipath.dev.server.watcher import watch_python_files + from uipath.dev.server.watcher import watch_project_files self._watcher_stop = asyncio.Event() self._watcher_task = asyncio.create_task( - watch_python_files( + watch_project_files( on_change=self._on_files_changed, stop_event=self._watcher_stop, ) @@ -200,8 +258,28 @@ def _stop_watcher(self) -> None: def _on_files_changed(self, changed_files: list[str]) -> None: """Handle file change events from the watcher.""" - self.reload_pending = True - self.connection_manager.broadcast_reload(changed_files) + # Convert to relative paths with forward slashes for frontend + cwd = os.getcwd() + relative_files = [] + for f in changed_files: + try: + relative_files.append(os.path.relpath(f, cwd).replace("\\", "/")) + except ValueError: + continue # different drive on Windows + + # Broadcast files.changed for editor auto-refresh + if relative_files: + self.connection_manager.broadcast_files_changed(relative_files) + + # Factory hot-reload for Python files only (use normalized relative paths) + py_files = [f for f in relative_files if f.endswith((".py", ".pyx"))] + if py_files and self.factory_creator is not None: + loop = self.connection_manager._get_loop() + + def _schedule_reload(files: list[str] = py_files) -> None: + asyncio.ensure_future(self._auto_reload(files)) + + loop.call_soon_threadsafe(_schedule_reload) # ------------------------------------------------------------------ # Internal callbacks @@ -223,14 +301,41 @@ def _on_chat(self, chat_data: ChatData) -> None: """Broadcast chat message to subscribed WebSocket clients.""" self.connection_manager.broadcast_chat(chat_data) - def _on_interrupt(self, interrupt_data: InterruptData) -> None: - """Broadcast chat interrupt to subscribed WebSocket clients.""" - self.connection_manager.broadcast_interrupt(interrupt_data) - def _on_state(self, state_data: StateData) -> None: """Broadcast state transition to subscribed WebSocket clients.""" self.connection_manager.broadcast_state(state_data) + def _on_eval_run_created(self, run: EvalRunState) -> None: + """Broadcast eval run created to all connected clients.""" + self.connection_manager.broadcast_eval_run_created(run) + + def _on_eval_run_progress( + self, + run_id: str, + completed: int, + total: int, + item_result: EvalItemResult | None, + ) -> None: + """Broadcast eval run progress to all connected clients.""" + self.connection_manager.broadcast_eval_run_progress( + run_id, completed, total, item_result + ) + + def _on_eval_run_completed(self, run: EvalRunState) -> None: + """Broadcast eval run completed to all connected clients.""" + self.connection_manager.broadcast_eval_run_completed(run) + + def _on_cli_agent_output(self, session_id: str, data: bytes) -> None: + """Broadcast CLI agent PTY output as base64.""" + import base64 + + encoded = base64.b64encode(data).decode("ascii") + self.connection_manager.broadcast_cli_agent_output(session_id, encoded) + + def _on_cli_agent_exit(self, session_id: str, exit_code: int) -> None: + """Broadcast CLI agent process exit.""" + self.connection_manager.broadcast_cli_agent_exit(session_id, exit_code) + @staticmethod def _find_free_port(host: str, start_port: int, max_attempts: int = 100) -> int: """Find a free port starting from *start_port*. diff --git a/src/uipath/dev/server/app.py b/src/uipath/dev/server/app.py index d608338..6c7b0d6 100644 --- a/src/uipath/dev/server/app.py +++ b/src/uipath/dev/server/app.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import logging import os from pathlib import Path @@ -150,22 +151,65 @@ async def _config(): # Register routes from uipath.dev.server.routes.entrypoints import router as entrypoints_router + from uipath.dev.server.routes.evals import router as evals_router + from uipath.dev.server.routes.evaluators import router as evaluators_router + from uipath.dev.server.routes.files import router as files_router from uipath.dev.server.routes.graph import router as graph_router from uipath.dev.server.routes.reload import router as reload_router from uipath.dev.server.routes.runs import router as runs_router from uipath.dev.server.ws.handler import router as ws_router if auth_enabled: - from uipath.dev.server.auth import restore_session + from uipath.dev.server.auth import get_auth_state, restore_session from uipath.dev.server.routes.auth import router as auth_router app.include_router(auth_router, prefix="/api") restore_session() + # Reload the runtime factory when authentication completes so the + # newly-written credentials are picked up by subsequent runs. + def _on_authenticated() -> None: + async def _safe_reload() -> None: + # Wait for active runs to finish before reloading + while any( + r.status in ("pending", "running") + for r in server.run_service.runs.values() + ): + await asyncio.sleep(1) + await server.reload_factory() + + def _on_reload_done(t: asyncio.Task[None]) -> None: + try: + t.result() + except asyncio.CancelledError: + pass + except Exception: + logger.exception("Factory reload after login failed") + + task = asyncio.create_task(_safe_reload()) + task.add_done_callback(_on_reload_done) + + get_auth_state()._on_authenticated = _on_authenticated + app.include_router(entrypoints_router, prefix="/api") app.include_router(runs_router, prefix="/api") app.include_router(graph_router, prefix="/api") app.include_router(reload_router, prefix="/api") + app.include_router(evaluators_router, prefix="/api") + app.include_router(evals_router, prefix="/api") + app.include_router(files_router, prefix="/api") + + from uipath.dev.server.routes.cli_agent import router as cli_agent_router + + app.include_router(cli_agent_router, prefix="/api") + + from uipath.dev.server.routes.mcp import router as mcp_router + + app.include_router(mcp_router, prefix="/api") + + from uipath.dev.server.routes.statedb import router as statedb_router + + app.include_router(statedb_router, prefix="/api") app.include_router(ws_router) # Auto-build frontend if source is available and build is stale diff --git a/src/uipath/dev/server/auth.py b/src/uipath/dev/server/auth.py index c76dd54..fd6dd6d 100644 --- a/src/uipath/dev/server/auth.py +++ b/src/uipath/dev/server/auth.py @@ -18,12 +18,14 @@ import socketserver import threading import time +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path from typing import Any from urllib.parse import urlencode import httpx +from dotenv import load_dotenv logger = logging.getLogger(__name__) @@ -108,6 +110,8 @@ class AuthState: _last_tenant: str | None = None _last_org: dict[str, str] = field(default_factory=dict) _last_environment: str | None = None + # Callback invoked after successful authentication + _on_authenticated: Callable[[], None] | None = None # internal _code_verifier: str | None = None _state: str | None = None @@ -127,9 +131,30 @@ def get_auth_state() -> AuthState: def reset_auth_state() -> None: - """Reset the auth state to its initial (unauthenticated) values.""" - global _auth - _auth = AuthState() + """Reset the auth state to its initial (unauthenticated) values. + + Preserves registered callbacks (e.g. ``_on_authenticated``). + """ + _auth.status = "unauthenticated" + _auth.environment = "cloud" + _auth.token_data = {} + _auth.tenants = [] + _auth.organization = {} + _auth.uipath_url = None + _auth._last_tenant = None + _auth._last_org = {} + _auth._last_environment = None + _auth._code_verifier = None + _auth._state = None + _auth._port = None + _auth._callback_server = None + if _auth._wait_task and not _auth._wait_task.done(): + _auth._wait_task.cancel() + _auth._wait_task = None + if _auth._token_event: + _auth._token_event.set() + _auth._token_event = None + _auth._loop = None # --------------------------------------------------------------------------- @@ -989,13 +1014,52 @@ def select_tenant(tenant_name: str) -> dict[str, Any]: return {"status": "authenticated", "uipath_url": auth.uipath_url} +def _update_env_file(env_contents: dict[str, str]) -> None: + """Merge *env_contents* into the CWD ``.env`` file. + + New keys take priority; existing keys not in *env_contents* are preserved. + Comments and blank lines are kept as-is. + """ + env_path = Path.cwd() / ".env" + lines: list[str] = [] + seen_keys: set[str] = set() + + if env_path.exists(): + with open(env_path) as f: + for raw_line in f: + stripped = raw_line.strip() + if stripped.startswith("#") or "=" not in stripped: + # Preserve comments and blank lines + lines.append(raw_line) + continue + key = stripped.split("=", 1)[0] + if key in env_contents: + lines.append(f"{key}={env_contents[key]}\n") + else: + lines.append(raw_line) + seen_keys.add(key) + + # Append new keys that weren't already in the file + for key, value in env_contents.items(): + if key not in seen_keys: + lines.append(f"{key}={value}\n") + + with open(env_path, "w") as f: + f.writelines(lines) + + def _finalize_tenant(auth: AuthState, tenant_name: str) -> None: """Write .env and os.environ with the resolved credentials.""" org_name = auth.organization.get("name", "") + org_id = auth.organization.get("id", "") domain = f"https://{auth.environment}.uipath.com" uipath_url = f"{domain}/{org_name}/{tenant_name}" access_token = auth.token_data.get("access_token", "") + # Resolve tenant ID from the tenants list + tenant = next((t for t in auth.tenants if t["name"] == tenant_name), None) + tenant_id = tenant["id"] if tenant else "" + auth.uipath_url = uipath_url auth.status = "authenticated" @@ -1004,35 +1068,27 @@ def _finalize_tenant(auth: AuthState, tenant_name: str) -> None: auth._last_org = dict(auth.organization) auth._last_environment = auth.environment - # Update os.environ - os.environ["UIPATH_ACCESS_TOKEN"] = access_token - os.environ["UIPATH_URL"] = uipath_url - - # Write/update .env file (preserving comments, blank lines, and ordering) - env_path = Path.cwd() / ".env" - lines: list[str] = [] - updated_keys: set[str] = set() - new_values = {"UIPATH_ACCESS_TOKEN": access_token, "UIPATH_URL": uipath_url} + # Write .env using the same approach as `uipath auth` + _update_env_file( + { + "UIPATH_ACCESS_TOKEN": access_token, + "UIPATH_URL": uipath_url, + "UIPATH_TENANT_ID": tenant_id, + "UIPATH_ORGANIZATION_ID": org_id, + } + ) - if env_path.exists(): - with open(env_path) as f: - for raw_line in f: - stripped = raw_line.strip() - if "=" in stripped and not stripped.startswith("#"): - key = stripped.split("=", 1)[0] - if key in new_values: - lines.append(f"{key}={new_values[key]}\n") - updated_keys.add(key) - continue - lines.append(raw_line) - - # Append any keys that weren't already in the file - for key, value in new_values.items(): - if key not in updated_keys: - lines.append(f"{key}={value}\n") + # Reload .env into os.environ (same as CLI root: cwd + override) + load_dotenv( + dotenv_path=os.path.join(os.getcwd(), ".env"), + override=True, + ) - with open(env_path, "w") as f: - f.writelines(lines) + if auth._on_authenticated: + try: + auth._on_authenticated() + except Exception: + logger.exception("Error in post-authentication callback") def logout() -> None: @@ -1043,6 +1099,8 @@ def logout() -> None: os.environ.pop("UIPATH_ACCESS_TOKEN", None) os.environ.pop("UIPATH_URL", None) + os.environ.pop("UIPATH_TENANT_ID", None) + os.environ.pop("UIPATH_ORGANIZATION_ID", None) reset_auth_state() diff --git a/src/uipath/dev/server/frontend/package-lock.json b/src/uipath/dev/server/frontend/package-lock.json index 30693ab..aca4b86 100644 --- a/src/uipath/dev/server/frontend/package-lock.json +++ b/src/uipath/dev/server/frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "uipath-dev-frontend", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "elkjs": "^0.11.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -22,6 +25,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", + "rollup": "^4.59.0", "tailwindcss": "^4.0.0", "typescript": "~5.7.0", "vite": "^6.0.0" @@ -801,6 +805,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -1079,9 +1106,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -1093,9 +1120,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -1107,9 +1134,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -1121,9 +1148,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -1135,9 +1162,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -1149,9 +1176,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -1163,9 +1190,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -1177,9 +1204,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -1191,9 +1218,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -1205,9 +1232,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -1219,9 +1246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], @@ -1233,9 +1260,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -1247,9 +1274,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], @@ -1261,9 +1288,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -1275,9 +1302,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -1289,9 +1316,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -1303,9 +1330,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -1317,9 +1344,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -1331,9 +1358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -1345,9 +1372,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], @@ -1359,9 +1386,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -1373,9 +1400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1387,9 +1414,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1401,9 +1428,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1415,9 +1442,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1429,49 +1456,49 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], @@ -1482,13 +1509,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -1499,13 +1526,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], @@ -1516,13 +1543,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], @@ -1533,13 +1560,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], @@ -1550,13 +1577,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], @@ -1567,13 +1594,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], @@ -1584,13 +1611,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], @@ -1601,13 +1628,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], @@ -1618,13 +1645,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1640,21 +1667,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], @@ -1665,13 +1692,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], @@ -1682,19 +1709,19 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -2053,9 +2080,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", - "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2071,6 +2098,14 @@ "@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", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2104,6 +2139,21 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -2115,13 +2165,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/browserslist": { @@ -2159,9 +2212,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001775", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", + "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", "dev": true, "funding": [ { @@ -2425,10 +2478,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, @@ -2439,9 +2502,9 @@ "license": "EPL-2.0" }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2776,9 +2839,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -2792,23 +2855,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -2827,9 +2890,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -2848,9 +2911,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -2869,9 +2932,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -2890,9 +2953,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -2911,9 +2974,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -2932,9 +2995,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -2953,9 +3016,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -2974,9 +3037,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -2995,9 +3058,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -3016,9 +3079,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -3091,6 +3154,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "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", @@ -3108,9 +3184,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "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", @@ -3924,6 +4000,17 @@ ], "license": "MIT" }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4200,9 +4287,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4216,31 +4303,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -4280,6 +4367,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -4313,9 +4406,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "dev": true, "license": "MIT" }, diff --git a/src/uipath/dev/server/frontend/package.json b/src/uipath/dev/server/frontend/package.json index 9638678..1bd7b62 100644 --- a/src/uipath/dev/server/frontend/package.json +++ b/src/uipath/dev/server/frontend/package.json @@ -9,6 +9,9 @@ "preview": "vite preview" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "elkjs": "^0.11.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -23,6 +26,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", + "rollup": "^4.59.0", "tailwindcss": "^4.0.0", "typescript": "~5.7.0", "vite": "^6.0.0" diff --git a/src/uipath/dev/server/frontend/src/App.tsx b/src/uipath/dev/server/frontend/src/App.tsx index 8249df3..cf21e22 100644 --- a/src/uipath/dev/server/frontend/src/App.tsx +++ b/src/uipath/dev/server/frontend/src/App.tsx @@ -6,18 +6,34 @@ import { useWebSocket } from "./store/useWebSocket"; import { listRuns, listEntrypoints, getRun } from "./api/client"; import type { RunDetail } from "./types/run"; import { useHashRoute } from "./hooks/useHashRoute"; +import type { Section } from "./hooks/useHashRoute"; import { useIsMobile } from "./hooks/useIsMobile"; -import Sidebar from "./components/layout/Sidebar"; +import ActivityBar from "./components/layout/ActivityBar"; +import DebugSidebar from "./components/layout/DebugSidebar"; import StatusBar from "./components/layout/StatusBar"; import NewRunPanel from "./components/runs/NewRunPanel"; import SetupView from "./components/runs/SetupView"; import RunDetailsPanel from "./components/runs/RunDetailsPanel"; import ReloadToast from "./components/shared/ReloadToast"; +import ToastContainer from "./components/shared/ToastContainer"; +import { useEvalStore } from "./store/useEvalStore"; +import { listEvalSets, listEvaluators, listEvalRuns, listLocalEvaluators } from "./api/eval-client"; +import EvalsSidebar from "./components/evals/EvalsSidebar"; +import EvalSetDetail from "./components/evals/EvalSetDetail"; +import EvalRunResults from "./components/evals/EvalRunResults"; +import CreateEvalSetView from "./components/evals/CreateEvalSetView"; +import EvaluatorsSidebar from "./components/evaluators/EvaluatorsSidebar"; +import EvaluatorsView from "./components/evaluators/EvaluatorDetail"; +import CreateEvaluatorView from "./components/evaluators/CreateEvaluatorView"; +import ExplorerSidebar from "./components/explorer/ExplorerSidebar"; +import ExplorerContent from "./components/explorer/ExplorerContent"; export default function App() { const ws = useWebSocket(); const isMobile = useIsMobile(); const [sidebarOpen, setSidebarOpen] = useState(false); + const [sidebarWidth, setSidebarWidth] = useState(248); + const [isDraggingSidebar, setIsDraggingSidebar] = useState(false); const { runs, selectedRunId, @@ -33,14 +49,30 @@ export default function App() { setActiveNode, removeActiveNode, } = useRunStore(); - const { view, runId: routeRunId, setupEntrypoint, setupMode, navigate } = useHashRoute(); + const { + section, + view, + runId: routeRunId, + setupEntrypoint, + setupMode, + evalCreating, + evalSetId, + evalRunId, + evalRunItemName, + evaluatorCreateType, + evaluatorId, + evaluatorFilter, + navigate, + } = useHashRoute(); + + const { setEvalSets, setEvaluators, setLocalEvaluators, setEvalRuns } = useEvalStore(); // Sync route runId → store selection useEffect(() => { - if (view === "details" && routeRunId && routeRunId !== selectedRunId) { + if (section === "debug" && view === "details" && routeRunId && routeRunId !== selectedRunId) { selectRun(routeRunId); } - }, [view, routeRunId, selectedRunId, selectRun]); + }, [section, view, routeRunId, selectedRunId, selectRun]); // Load existing runs, entrypoints, auth status, and config on mount const initAuth = useAuthStore((s) => s.init); @@ -54,6 +86,49 @@ export default function App() { initConfig(); }, [setRuns, setEntrypoints, initAuth, initConfig]); + // Load eval data when switching to evals/evaluators section + useEffect(() => { + if (section === "evals") { + listEvalSets().then((sets) => setEvalSets(sets)).catch(console.error); + listEvalRuns().then((runs) => setEvalRuns(runs)).catch(console.error); + } + if (section === "evals" || section === "evaluators") { + listEvaluators().then((evs) => setEvaluators(evs)).catch(console.error); + listLocalEvaluators().then((evs) => setLocalEvaluators(evs)).catch(console.error); + } + }, [section, setEvalSets, setEvaluators, setLocalEvaluators, setEvalRuns]); + + // Auto-select latest run or first eval set when navigating to evals with no selection + const evalSets = useEvalStore((s) => s.evalSets); + const evalRuns = useEvalStore((s) => s.evalRuns); + useEffect(() => { + if (section !== "evals" || evalCreating || evalSetId || evalRunId) return; + // Pick latest run by start_time + const runs = Object.values(evalRuns).sort( + (a, b) => new Date(b.start_time ?? 0).getTime() - new Date(a.start_time ?? 0).getTime(), + ); + if (runs.length > 0) { + navigate(`#/evals/runs/${runs[0].id}`); + return; + } + // Fallback: first eval set + const sets = Object.values(evalSets); + if (sets.length > 0) { + navigate(`#/evals/sets/${sets[0].id}`); + } + }, [section, evalCreating, evalSetId, evalRunId, evalRuns, evalSets, navigate]); + + // Keyboard shortcuts + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && sidebarOpen) { + setSidebarOpen(false); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [sidebarOpen]); + const selectedRun = selectedRunId ? runs[selectedRunId] : null; // Shared helper: apply a full run detail response to the store @@ -169,70 +244,233 @@ export default function App() { }, [selectedRunId, selectedRun?.status, applyRunDetail]); const handleRunCreated = (runId: string) => { - navigate(`#/runs/${runId}/traces`); + navigate(`#/debug/runs/${runId}/traces`); selectRun(runId); setSidebarOpen(false); }; const handleSelectRun = (runId: string) => { - navigate(`#/runs/${runId}/traces`); + navigate(`#/debug/runs/${runId}/traces`); selectRun(runId); setSidebarOpen(false); }; const handleNewRun = () => { - navigate("#/new"); + navigate("#/debug/new"); setSidebarOpen(false); }; + const handleSectionChange = (s: Section) => { + if (s === "debug") navigate("#/debug/new"); + else if (s === "evals") navigate("#/evals"); + else if (s === "evaluators") navigate("#/evaluators"); + else if (s === "explorer") navigate("#/explorer"); + }; + + // --- Sidebar col resize --- + const onSidebarResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + setIsDraggingSidebar(true); + + const startX = "touches" in e ? e.touches[0].clientX : e.clientX; + const startW = sidebarWidth; + + const onMove = (ev: MouseEvent | TouchEvent) => { + const clientX = "touches" in ev ? ev.touches[0].clientX : ev.clientX; + const newW = Math.max(200, Math.min(480, startW + (clientX - startX))); + setSidebarWidth(newW); + }; + + const onUp = () => { + setIsDraggingSidebar(false); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + document.removeEventListener("touchmove", onMove); + document.removeEventListener("touchend", onUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + document.addEventListener("touchmove", onMove, { passive: false }); + document.addEventListener("touchend", onUp); + }, [sidebarWidth]); + + // --- Render main content based on section --- + const renderMainContent = () => { + if (section === "explorer") { + return ; + } + + if (section === "evals") { + if (evalCreating) return ; + if (evalRunId) return ; + if (evalSetId) return ; + return ; + } + + if (section === "evaluators") { + if (evaluatorCreateType) { + return ; + } + return ; + } + + // Debug section + if (view === "new") { + return ; + } + if (view === "setup" && setupEntrypoint && setupMode) { + return ( + + ); + } + if (selectedRun) { + return ; + } + return ( +
+ Select a run or create a new one +
+ ); + }; + + // --- Mobile layout --- + if (isMobile) { + return ( +
+
+ {!sidebarOpen && ( + + )} + {sidebarOpen && ( + <> +
setSidebarOpen(false)} + /> + + + )} +
+ {renderMainContent()} +
+
+ + + +
+ ); + } + + // --- Desktop layout --- return (
-
- {/* Mobile hamburger button */} - {isMobile && !sidebarOpen && ( - - )} - setSidebarOpen(false)} +
+ {/* Left aside: shared header + ActivityBar + section sidebar */} + +
- {view === "new" ? ( - - ) : view === "setup" && setupEntrypoint && setupMode ? ( - - ) : selectedRun ? ( - - ) : ( -
- Select a run or create a new one -
- )} + {renderMainContent()}
+
); } diff --git a/src/uipath/dev/server/frontend/src/api/cli-agent-client.ts b/src/uipath/dev/server/frontend/src/api/cli-agent-client.ts new file mode 100644 index 0000000..85d0bbb --- /dev/null +++ b/src/uipath/dev/server/frontend/src/api/cli-agent-client.ts @@ -0,0 +1,11 @@ +export interface CliAgentInfo { + id: string; + name: string; + installed: boolean; +} + +export async function listCliAgents(): Promise { + const res = await fetch("/api/cli-agent/available"); + if (!res.ok) return []; + return res.json(); +} diff --git a/src/uipath/dev/server/frontend/src/api/cli-terminal-bridge.ts b/src/uipath/dev/server/frontend/src/api/cli-terminal-bridge.ts new file mode 100644 index 0000000..3a917bb --- /dev/null +++ b/src/uipath/dev/server/frontend/src/api/cli-terminal-bridge.ts @@ -0,0 +1,21 @@ +/** + * Module-level registry for routing WebSocket PTY output to xterm instances. + * The CliAgentTerminal component registers its writer on mount and + * deregisters on unmount. + */ + +type TerminalWriter = (data: Uint8Array) => void; + +const writers = new Map(); + +export function registerTerminalWriter(sessionId: string, writer: TerminalWriter): void { + writers.set(sessionId, writer); +} + +export function unregisterTerminalWriter(sessionId: string): void { + writers.delete(sessionId); +} + +export function getTerminalWriter(sessionId: string): TerminalWriter | undefined { + return writers.get(sessionId); +} diff --git a/src/uipath/dev/server/frontend/src/api/eval-client.ts b/src/uipath/dev/server/frontend/src/api/eval-client.ts new file mode 100644 index 0000000..ae24d7f --- /dev/null +++ b/src/uipath/dev/server/frontend/src/api/eval-client.ts @@ -0,0 +1,185 @@ +import type { EvaluatorInfo, LocalEvaluator, LlmModel, EvalSetSummary, EvalSetDetail, EvalItem, EvalRunSummary, EvalRunDetail } from "../types/eval"; + +const BASE = "/api"; + +async function fetchJson(url: string, options?: RequestInit): Promise { + const res = await fetch(url, options); + if (!res.ok) { + let errorDetail; + try { + const body = await res.json(); + errorDetail = body.detail || res.statusText; + } catch { + errorDetail = res.statusText; + } + const error = new Error(`HTTP ${res.status}`); + (error as any).detail = errorDetail; + (error as any).status = res.status; + throw error; + } + return res.json(); +} + +export async function listEvaluators(): Promise { + return fetchJson(`${BASE}/evaluators`); +} + +export async function listLlmModels(): Promise { + return fetchJson(`${BASE}/llm-models`); +} + +export async function listEvalSets(): Promise { + return fetchJson(`${BASE}/eval-sets`); +} + +export async function createEvalSet(body: { + name: string; + evaluator_refs: string[]; +}): Promise { + return fetchJson(`${BASE}/eval-sets`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +export async function addEvalItem( + evalSetId: string, + item: { + name: string; + inputs: Record; + expected_output: unknown; + evaluation_criterias?: Record>; + }, +): Promise { + return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/items`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(item), + }); +} + +export async function updateEvalItem( + evalSetId: string, + itemName: string, + body: { + name?: string; + inputs?: Record; + expected_output?: unknown; + expected_behavior?: string; + simulation_instructions?: string; + }, +): Promise { + return fetchJson( + `${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/items/${encodeURIComponent(itemName)}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, + ); +} + +export async function updateEvalItemEvaluators( + evalSetId: string, + itemName: string, + evaluationCriterias: Record, +): Promise { + return fetchJson( + `${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/items/${encodeURIComponent(itemName)}/evaluators`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ evaluation_criterias: evaluationCriterias }), + }, + ); +} + +export async function deleteEvalItem( + evalSetId: string, + itemName: string, +): Promise { + await fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/items/${encodeURIComponent(itemName)}`, { + method: "DELETE", + }); +} + +export async function getEvalSet(id: string): Promise { + return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(id)}`); +} + +export async function startEvalRun(evalSetId: string): Promise { + return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/runs`, { + method: "POST", + }); +} + +export async function listEvalRuns(): Promise { + return fetchJson(`${BASE}/eval-runs`); +} + +export async function getEvalRun(id: string): Promise { + return fetchJson(`${BASE}/eval-runs/${encodeURIComponent(id)}`); +} + +export async function listLocalEvaluators(): Promise { + return fetchJson(`${BASE}/local-evaluators`); +} + +export async function createLocalEvaluator(body: { + name: string; + description: string; + evaluator_type_id: string; + config: Record; +}): Promise { + return fetchJson(`${BASE}/local-evaluators`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +export async function updateEvalSetEvaluators( + evalSetId: string, + evaluatorRefs: string[], +): Promise { + return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/evaluators`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ evaluator_refs: evaluatorRefs }), + }); +} + +export async function scaffoldCustomEvaluator(body: { + name: string; + description?: string; +}): Promise<{ file_path: string; filename: string; class_name: string; evaluator_id: string }> { + return fetchJson(`${BASE}/custom-evaluators/scaffold`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +export async function registerCustomEvaluator(filename: string): Promise<{ evaluator_id: string; spec_path: string }> { + return fetchJson(`${BASE}/custom-evaluators/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ filename }), + }); +} + +export async function updateLocalEvaluator( + id: string, + body: { + description?: string; + evaluator_type_id?: string; + config?: Record; + }, +): Promise { + return fetchJson(`${BASE}/local-evaluators/${encodeURIComponent(id)}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} diff --git a/src/uipath/dev/server/frontend/src/api/explorer-client.ts b/src/uipath/dev/server/frontend/src/api/explorer-client.ts new file mode 100644 index 0000000..e178222 --- /dev/null +++ b/src/uipath/dev/server/frontend/src/api/explorer-client.ts @@ -0,0 +1,37 @@ +import type { FileEntry, FileContent } from "../types/explorer"; + +const BASE = "/api"; + +async function fetchJson(url: string, options?: RequestInit): Promise { + const res = await fetch(url, options); + if (!res.ok) { + let errorDetail; + try { + const body = await res.json(); + errorDetail = body.detail || res.statusText; + } catch { + errorDetail = res.statusText; + } + const error = new Error(`HTTP ${res.status}`); + (error as any).detail = errorDetail; + (error as any).status = res.status; + throw error; + } + return res.json(); +} + +export async function listDirectory(path: string): Promise { + return fetchJson(`${BASE}/files/tree?path=${encodeURIComponent(path)}`); +} + +export async function readFile(path: string): Promise { + return fetchJson(`${BASE}/files/content?path=${encodeURIComponent(path)}`); +} + +export async function saveFile(path: string, content: string): Promise { + await fetchJson(`${BASE}/files/content?path=${encodeURIComponent(path)}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); +} diff --git a/src/uipath/dev/server/frontend/src/api/statedb-client.ts b/src/uipath/dev/server/frontend/src/api/statedb-client.ts new file mode 100644 index 0000000..cfd2556 --- /dev/null +++ b/src/uipath/dev/server/frontend/src/api/statedb-client.ts @@ -0,0 +1,54 @@ +import type { + StateDbTable, + StateDbTableData, + StateDbQueryResult, +} from "../types/statedb"; + +const BASE = "/api"; + +async function fetchJson(url: string, options?: RequestInit): Promise { + const res = await fetch(url, options); + if (!res.ok) { + let errorDetail; + try { + const body = await res.json(); + errorDetail = body.detail || res.statusText; + } catch { + errorDetail = res.statusText; + } + const error = new Error(`HTTP ${res.status}`); + (error as any).detail = errorDetail; + (error as any).status = res.status; + throw error; + } + return res.json(); +} + +export async function getStateDbStatus(): Promise<{ exists: boolean }> { + return fetchJson(`${BASE}/statedb/status`); +} + +export async function getStateDbTables(): Promise { + return fetchJson(`${BASE}/statedb/tables`); +} + +export async function getStateDbTableData( + table: string, + limit = 100, + offset = 0, +): Promise { + return fetchJson( + `${BASE}/statedb/tables/${encodeURIComponent(table)}?limit=${limit}&offset=${offset}`, + ); +} + +export async function executeStateDbQuery( + sql: string, + limit?: number, +): Promise { + return fetchJson(`${BASE}/statedb/query`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sql, limit }), + }); +} diff --git a/src/uipath/dev/server/frontend/src/api/websocket.ts b/src/uipath/dev/server/frontend/src/api/websocket.ts index 64bc1a7..032c1ed 100644 --- a/src/uipath/dev/server/frontend/src/api/websocket.ts +++ b/src/uipath/dev/server/frontend/src/api/websocket.ts @@ -123,4 +123,20 @@ export class WsClient { setBreakpoints(runId: string, breakpoints: string[]): void { this.send("debug.set_breakpoints", { run_id: runId, breakpoints }); } + + sendCliAgentStart(agentId: string, sessionId: string, cols: number, rows: number): void { + this.send("cli_agent.start", { agent_id: agentId, session_id: sessionId, cols, rows }); + } + + sendCliAgentInput(sessionId: string, data: string): void { + this.send("cli_agent.input", { session_id: sessionId, data }); + } + + sendCliAgentResize(sessionId: string, cols: number, rows: number): void { + this.send("cli_agent.resize", { session_id: sessionId, cols, rows }); + } + + sendCliAgentStop(sessionId: string): void { + this.send("cli_agent.stop", { session_id: sessionId }); + } } diff --git a/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx b/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx index 55a3d72..95ec9e5 100644 --- a/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx +++ b/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx @@ -36,13 +36,14 @@ export default function ChatInput({ onSend, disabled, placeholder }: Props) { onKeyDown={handleKeyDown} disabled={disabled} placeholder={placeholder ?? "Message..."} - className="flex-1 bg-transparent text-sm py-1 focus:outline-none disabled:opacity-40 placeholder:text-[var(--text-muted)]" + className="flex-1 bg-transparent text-sm py-1 disabled:opacity-40 placeholder:text-[var(--text-muted)]" style={{ color: "var(--text-primary)" }} /> +

+ ) : ( +
+ {localEvaluators.map((ev) => ( + + ))} +
+ )} +
+ + {/* Error */} + {error && ( +

{error}

+ )} + + {/* Create button */} + +
+
+ ); +} diff --git a/src/uipath/dev/server/frontend/src/components/evals/EvalRunResults.tsx b/src/uipath/dev/server/frontend/src/components/evals/EvalRunResults.tsx new file mode 100644 index 0000000..c1f3199 --- /dev/null +++ b/src/uipath/dev/server/frontend/src/components/evals/EvalRunResults.tsx @@ -0,0 +1,727 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { getEvalRun } from "../../api/eval-client"; +import { useEvalStore } from "../../store/useEvalStore"; +import { useHashRoute } from "../../hooks/useHashRoute"; +import type { EvalRunDetail, EvalItemResult } from "../../types/eval"; +import TraceTree from "../traces/TraceTree"; +import JsonHighlight from "../shared/JsonHighlight"; +import DataSection from "../shared/DataSection"; + +interface Props { + evalRunId: string; + itemName?: string | null; +} + +function formatScore(score: number | null | undefined): string { + if (score == null) return "-"; + return `${Math.round(score * 100)}%`; +} + +function scoreColor(score: number | null | undefined): string { + if (score == null) return "var(--text-muted)"; + const pct = score * 100; + if (pct >= 80) return "var(--success)"; + if (pct >= 50) return "var(--warning)"; + return "var(--error)"; +} + +function formatDuration(startTime: string | null, endTime: string | null): string { + if (!startTime) return "-"; + const start = new Date(startTime).getTime(); + const end = endTime ? new Date(endTime).getTime() : Date.now(); + const secs = Math.round((end - start) / 1000); + if (secs < 60) return `${secs}s`; + return `${Math.floor(secs / 60)}m ${secs % 60}s`; +} + +function stripEvaluatorSuffix(name: string): string { + return name.replace(/\s*Evaluator$/i, ""); +} + +const statusStyles: Record = { + pending: { color: "var(--text-muted)", bg: "var(--bg-tertiary)", label: "Pending" }, + running: { color: "var(--info)", bg: "rgba(59,130,246,0.1)", label: "Running" }, + completed: { color: "var(--success)", bg: "rgba(34,197,94,0.1)", label: "Completed" }, + failed: { color: "var(--error)", bg: "rgba(239,68,68,0.1)", label: "Failed" }, +}; + +export default function EvalRunResults({ evalRunId, itemName }: Props) { + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(true); + const { navigate } = useHashRoute(); + + const selectedItemName = itemName ?? null; + + // Item list height (top panel, resizable like GraphPanel) + const [itemListHeight, setItemListHeight] = useState(220); + const containerRef = useRef(null); + const draggingRow = useRef(false); + + // Sidebar width (right panel, resizable like ChatPanel) + const [sidebarWidth, setSidebarWidth] = useState(() => { + const saved = localStorage.getItem("evalSidebarWidth"); + return saved ? parseInt(saved, 10) : 320; + }); + const [isDragging, setIsDragging] = useState(false); + const outerRef = useRef(null); + + useEffect(() => { + localStorage.setItem("evalSidebarWidth", String(sidebarWidth)); + }, [sidebarWidth]); + + const storeRun = useEvalStore((s) => s.evalRuns[evalRunId]); + const evaluators = useEvalStore((s) => s.evaluators); + const streamingItems = useEvalStore((s) => s.streamingResults[evalRunId]); + + useEffect(() => { + setLoading(true); + getEvalRun(evalRunId) + .then((d) => { + setDetail(d); + // Auto-navigate to first item if none selected + if (!itemName) { + const first = d.results.find((r) => r.status === "completed") ?? d.results[0]; + if (first) navigate(`#/evals/runs/${evalRunId}/${encodeURIComponent(first.name)}`); + } + }) + .catch(console.error) + .finally(() => setLoading(false)); + }, [evalRunId]); + + // Re-fetch when store run status reaches terminal + useEffect(() => { + if (storeRun?.status === "completed" || storeRun?.status === "failed") { + getEvalRun(evalRunId).then(setDetail).catch(console.error); + } + }, [storeRun?.status, evalRunId]); + + // Auto-select first completed item as results come in (when no item is in route) + useEffect(() => { + if (itemName || !detail?.results) return; + const results = streamingItems + ? detail.results.map((r) => streamingItems[r.name] ?? r) + : detail.results; + const first = results.find((r) => r.status === "completed") ?? results[0]; + if (first) navigate(`#/evals/runs/${evalRunId}/${encodeURIComponent(first.name)}`); + }, [detail?.results, streamingItems]); + + // --- Row resize (item list height) --- + const onRowResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + draggingRow.current = true; + + const startY = "touches" in e ? e.touches[0].clientY : e.clientY; + const startH = itemListHeight; + + const onMove = (ev: MouseEvent | TouchEvent) => { + if (!draggingRow.current) return; + const container = containerRef.current; + if (!container) return; + const clientY = "touches" in ev ? ev.touches[0].clientY : ev.clientY; + const maxH = container.clientHeight - 100; + const newH = Math.max(80, Math.min(maxH, startH + (clientY - startY))); + setItemListHeight(newH); + }; + + const onUp = () => { + draggingRow.current = false; + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + document.removeEventListener("touchmove", onMove); + document.removeEventListener("touchend", onUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + document.body.style.cursor = "row-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + document.addEventListener("touchmove", onMove, { passive: false }); + document.addEventListener("touchend", onUp); + }, [itemListHeight]); + + // --- Sidebar col resize --- + const onSidebarResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + setIsDragging(true); + + const startX = "touches" in e ? e.touches[0].clientX : e.clientX; + const startW = sidebarWidth; + + const onMove = (ev: MouseEvent | TouchEvent) => { + const container = outerRef.current; + if (!container) return; + const clientX = "touches" in ev ? ev.touches[0].clientX : ev.clientX; + const maxW = container.clientWidth - 300; + const newW = Math.max(280, Math.min(maxW, startW + (startX - clientX))); + setSidebarWidth(newW); + }; + + const onUp = () => { + setIsDragging(false); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + document.removeEventListener("touchmove", onMove); + document.removeEventListener("touchend", onUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + document.addEventListener("touchmove", onMove, { passive: false }); + document.addEventListener("touchend", onUp); + }, [sidebarWidth]); + + if (loading) { + return ( +
+ Loading... +
+ ); + } + + if (!detail) { + return ( +
+ Eval run not found +
+ ); + } + + const run = storeRun ?? detail; + const status = statusStyles[run.status] ?? statusStyles.pending; + const isRunning = run.status === "running"; + // Derive evaluator IDs from run scores (set on completion) or from first streamed item + const evaluatorIds = Object.keys(run.evaluator_scores ?? {}).length > 0 + ? Object.keys(run.evaluator_scores) + : Object.keys( + streamingItems + ? Object.values(streamingItems).find((r) => Object.keys(r.scores).length > 0)?.scores ?? {} + : {}, + ); + + // Merge streaming item results over the fetched detail results + const mergedResults = streamingItems + ? detail.results.map((r) => streamingItems[r.name] ?? r) + : detail.results; + + const selectedItem = mergedResults.find((r) => r.name === selectedItemName) ?? null; + const selectedTraces = (selectedItem?.traces ?? []).map((t) => ({ ...t, run_id: "" })); + + return ( +
+ {/* Main content: item list (top) + traces (bottom) */} +
+ {/* Header bar */} +
+

+ {run.eval_set_name} +

+ + {status.label} + + + {formatScore(run.overall_score)} + + + {formatDuration(run.start_time, run.end_time)} + + {isRunning && ( +
+
+
0 ? (run.progress_completed / run.progress_total) * 100 : 0}%`, + background: "var(--info)", + }} + /> +
+ + {run.progress_completed}/{run.progress_total} + +
+ )} + {/* Per-evaluator scores inline */} + {evaluatorIds.length > 0 && ( +
+ {evaluatorIds.map((id) => { + const ev = evaluators.find((e) => e.id === id); + const score = run.evaluator_scores[id]; + return ( +
+ + {stripEvaluatorSuffix(ev?.name ?? id)} + +
+
+
+ + {formatScore(score)} + +
+ ); + })} +
+ )} +
+ + {/* Item list (resizable height, like GraphPanel) */} +
+ {/* Table header */} +
+ + Name + Score + {evaluatorIds.map((id) => { + const ev = evaluators.find((e) => e.id === id); + return ( + {stripEvaluatorSuffix(ev?.name ?? id)} + ); + })} + Time +
+ {/* Scrollable item rows */} +
+ {mergedResults.map((item: EvalItemResult) => { + const isPending = item.status === "pending"; + const isFailed = item.status === "failed"; + const isSelected = item.name === selectedItemName; + return ( + + ); + })} + {mergedResults.length === 0 && ( +
+ {isRunning ? "Waiting for results..." : "No results"} +
+ )} +
+
+ + {/* Row drag handle */} +
+ + {/* Trace tree (bottom, flex-1, like TraceTree in debug) */} +
+ {selectedItem && selectedTraces.length > 0 ? ( + + ) : ( +
+ {selectedItem?.status === "pending" ? "Pending..." : "No traces available"} +
+ )} +
+
+ + {/* Sidebar drag handle */} +
+ + {/* Right sidebar */} + +
+ ); +} + +type DetailTab = "score" | "io" | "logs"; + +const detailTabs: { id: DetailTab; label: string }[] = [ + { id: "score", label: "Score" }, + { id: "io", label: "I/O" }, + { id: "logs", label: "Logs" }, +]; + +function DetailsSidebar({ + width, + item, + evaluators, + isRunning, + isDragging, +}: { + width: number; + item: EvalItemResult | null; + evaluators: { id: string; name: string }[]; + isRunning: boolean; + isDragging: boolean; +}) { + const [tab, setTab] = useState("score"); + + const showSidebar = !!item; + + return ( +
+ {/* Tab bar */} +
+ {detailTabs.map((t) => ( + + ))} + {isRunning && ( + + Running... + + )} +
+ + {/* Content */} +
+ {!item ? null : item.status === "pending" ? ( +
+ Pending... +
+ ) : ( + <> + {item.status === "failed" && ( +
+
+ + Evaluator error +
+ {item.error && ( +
+ {item.error} +
+ )} +
+ )} + {tab === "score" ? ( + + ) : tab === "io" ? ( + + ) : ( +
+ Logs coming soon +
+ )} + + )} +
+
+ ); +} + +function ScoreTab({ item, evaluators }: { item: EvalItemResult; evaluators: { id: string; name: string }[] }) { + const evalIds = Object.keys(item.scores); + + return ( +
+ {/* Overall */} +
+
+ + Overall + +
+
+
+
+ + {formatScore(item.overall_score)} + +
+
+
+ + {/* Failed: no evaluator scores */} + {item.status === "failed" && evalIds.length === 0 && ( +
+ All evaluators failed — no scores available +
+ )} + + {/* Per-evaluator scores */} + {evalIds.map((evId) => { + const ev = evaluators.find((e) => e.id === evId); + const score = item.scores[evId]; + const justification = item.justifications[evId]; + return ( +
+
+ + {ev?.name ?? evId} + +
+
+
+
+ + {formatScore(score)} + +
+
+ {justification && ( + + )} +
+ ); + })} +
+ ); +} + +function IOTab({ item }: { item: EvalItemResult }) { + const inputJson = JSON.stringify(item.inputs, null, 2); + const outputJson = typeof item.output === "string" ? item.output : JSON.stringify(item.output, null, 2); + const expectedJson = item.expected_output != null + ? (typeof item.expected_output === "string" ? item.expected_output : JSON.stringify(item.expected_output, null, 2)) + : null; + + return ( +
+ + + + + {expectedJson && ( + + + + )} + + + {(item.duration_ms / 1000).toFixed(2)}s + + ) : undefined} + > + + +
+ ); +} + +/** Try to parse `expected="..." actual="..."` from justification text into structured blocks. */ +function parseExpectedActual(text: string): { expected: string; actual: string; meta: Record } | null { + const m = text.match(/expected="(.+?)"\s+actual="(.+?)"(.*)/s); + if (!m) return null; + // Parse trailing key=value pairs like matched_leaves=1.0 total_leaves=1.0 + const meta: Record = {}; + const rest = m[3]?.trim() ?? ""; + if (rest) { + for (const pair of rest.match(/(\w+)=([\S]+)/g) ?? []) { + const eq = pair.indexOf("="); + meta[pair.slice(0, eq)] = pair.slice(eq + 1); + } + } + return { expected: m[1], actual: m[2], meta }; +} + +function tryFormatValue(raw: string): string { + // Python dict repr → JSON: single quotes → double quotes, True/False/None → JSON equivalents + try { + const jsonLike = raw + .replace(/'/g, '"') + .replace(/\bTrue\b/g, "true") + .replace(/\bFalse\b/g, "false") + .replace(/\bNone\b/g, "null"); + const parsed = JSON.parse(jsonLike); + return JSON.stringify(parsed, null, 2); + } catch { + return raw; + } +} + +function JustificationBlock({ text }: { text: string }) { + const parsed = parseExpectedActual(text); + + if (!parsed) { + // Fallback: plain text + return ( +
+
+ {text} +
+
+ ); + } + + const expected = tryFormatValue(parsed.expected); + const actual = tryFormatValue(parsed.actual); + const match = expected === actual; + + return ( +
+
+ {/* Expected */} +
+
+ Expected +
+
+            {expected}
+          
+
+ {/* Actual */} +
+
+ + Actual + + +
+
+            {actual}
+          
+
+
+ {Object.keys(parsed.meta).length > 0 && ( +
+ {Object.entries(parsed.meta).map(([k, v]) => ( + + {k.replace(/_/g, " ")}{" "} + {v} + + ))} +
+ )} +
+ ); +} diff --git a/src/uipath/dev/server/frontend/src/components/evals/EvalSetDetail.tsx b/src/uipath/dev/server/frontend/src/components/evals/EvalSetDetail.tsx new file mode 100644 index 0000000..0211abd --- /dev/null +++ b/src/uipath/dev/server/frontend/src/components/evals/EvalSetDetail.tsx @@ -0,0 +1,993 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { getEvalSet, startEvalRun, updateEvalSetEvaluators, updateEvalItemEvaluators, updateEvalItem, addEvalItem, deleteEvalItem } from "../../api/eval-client"; +import { useEvalStore } from "../../store/useEvalStore"; +import { useHashRoute } from "../../hooks/useHashRoute"; +import type { EvalSetDetail as EvalSetDetailType, EvalItem, EvaluatorInfo, LocalEvaluator } from "../../types/eval"; +import { getSchemaConfigFields } from "../evaluators/EvaluatorDetail"; + +interface Props { + evalSetId: string; +} + +function truncateJson(val: unknown, max = 60): string { + const s = typeof val === "string" ? val : JSON.stringify(val); + if (!s || s === "null") return "-"; + return s.length > max ? s.slice(0, max) + "..." : s; +} + +type SidebarTab = "io" | "evaluators"; + +export default function EvalSetDetail({ evalSetId }: Props) { + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(true); + const [selectedItemName, setSelectedItemName] = useState(null); + const [runLoading, setRunLoading] = useState(false); + const [sidebarTab, setSidebarTab] = useState("io"); + const evaluators = useEvalStore((s) => s.evaluators); + const localEvaluators = useEvalStore((s) => s.localEvaluators); + const storeUpdateEvaluators = useEvalStore((s) => s.updateEvalSetEvaluators); + const incrementEvalSetCount = useEvalStore((s) => s.incrementEvalSetCount); + const upsertEvalRun = useEvalStore((s) => s.upsertEvalRun); + const { navigate } = useHashRoute(); + + // Evaluator edit popover state + const [editOpen, setEditOpen] = useState(false); + const [editRefs, setEditRefs] = useState>(new Set()); + const [editSaving, setEditSaving] = useState(false); + const popoverRef = useRef(null); + + // Sidebar width (resizable) + const [sidebarWidth, setSidebarWidth] = useState(() => { + const saved = localStorage.getItem("evalSetSidebarWidth"); + return saved ? parseInt(saved, 10) : 320; + }); + const [isDragging, setIsDragging] = useState(false); + const outerRef = useRef(null); + + useEffect(() => { + localStorage.setItem("evalSetSidebarWidth", String(sidebarWidth)); + }, [sidebarWidth]); + + useEffect(() => { + setLoading(true); + setSelectedItemName(null); + getEvalSet(evalSetId) + .then((d) => { + setDetail(d); + if (d.items.length > 0) setSelectedItemName(d.items[0].name); + }) + .catch(console.error) + .finally(() => setLoading(false)); + }, [evalSetId]); + + const handleRun = async () => { + setRunLoading(true); + try { + const run = await startEvalRun(evalSetId); + upsertEvalRun(run); + navigate(`#/evals/runs/${run.id}`); + } catch (err) { + console.error(err); + } finally { + setRunLoading(false); + } + }; + + const handleDeleteItem = async (itemName: string) => { + if (!detail) return; + try { + await deleteEvalItem(evalSetId, itemName); + setDetail((prev) => { + if (!prev) return prev; + const items = prev.items.filter((i) => i.name !== itemName); + return { ...prev, items, eval_count: items.length }; + }); + incrementEvalSetCount(evalSetId, -1); + if (selectedItemName === itemName) setSelectedItemName(null); + } catch (err) { + console.error(err); + } + }; + + const handleAddItem = async () => { + if (!detail) return; + const itemName = `item-${detail.items.length + 1}`; + try { + const item = await addEvalItem(evalSetId, { + name: itemName, + inputs: {}, + expected_output: null, + }); + setDetail((prev) => { + if (!prev) return prev; + return { ...prev, items: [...prev.items, item], eval_count: prev.eval_count + 1 }; + }); + incrementEvalSetCount(evalSetId); + setSelectedItemName(itemName); + setSidebarTab("io"); + } catch (err) { + console.error(err); + } + }; + + const handleItemUpdated = (updated: EvalItem, oldName?: string) => { + setDetail((prev) => { + if (!prev) return prev; + const matchName = oldName ?? updated.name; + return { + ...prev, + items: prev.items.map((it) => it.name === matchName ? updated : it), + }; + }); + if (oldName && selectedItemName === oldName) { + setSelectedItemName(updated.name); + } + }; + + // --- Evaluator edit popover --- + const openEditPopover = useCallback(() => { + if (detail) { + setEditRefs(new Set(detail.evaluator_ids)); + } + setEditOpen(true); + }, [detail]); + + const toggleEditRef = (id: string) => { + setEditRefs((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + const handleSaveEvaluators = async () => { + if (!detail) return; + setEditSaving(true); + try { + const updated = await updateEvalSetEvaluators(evalSetId, Array.from(editRefs)); + setDetail(updated); + storeUpdateEvaluators(evalSetId, updated.evaluator_ids); + setEditOpen(false); + } catch (err) { + console.error(err); + } finally { + setEditSaving(false); + } + }; + + // Click-outside to close popover + useEffect(() => { + if (!editOpen) return; + const handler = (e: MouseEvent) => { + if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { + setEditOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [editOpen]); + + // --- Sidebar col resize --- + const onSidebarResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + setIsDragging(true); + + const startX = "touches" in e ? e.touches[0].clientX : e.clientX; + const startW = sidebarWidth; + + const onMove = (ev: MouseEvent | TouchEvent) => { + const container = outerRef.current; + if (!container) return; + const clientX = "touches" in ev ? ev.touches[0].clientX : ev.clientX; + const maxW = container.clientWidth - 300; + const newW = Math.max(280, Math.min(maxW, startW + (startX - clientX))); + setSidebarWidth(newW); + }; + + const onUp = () => { + setIsDragging(false); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + document.removeEventListener("touchmove", onMove); + document.removeEventListener("touchend", onUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + document.addEventListener("touchmove", onMove, { passive: false }); + document.addEventListener("touchend", onUp); + }, [sidebarWidth]); + + if (loading) { + return ( +
+ Loading... +
+ ); + } + + if (!detail) { + return ( +
+ Eval set not found +
+ ); + } + + const selectedItem = detail.items.find((i) => i.name === selectedItemName) ?? null; + + return ( +
+ {/* Main content: header + item grid */} +
+ {/* Header bar */} +
+

+ {detail.name} +

+ + {detail.eval_count} items + +
+ + {detail.evaluator_ids.map((id) => { + const ev = evaluators.find((e) => e.id === id); + return ( + + {ev?.name ?? id} + + ); + })} + {editOpen && ( +
+
+ Evaluators +
+
+ {localEvaluators.length === 0 ? ( +
+ No evaluators available +
+ ) : ( + localEvaluators.map((ev) => ( + + )) + )} +
+
+ +
+
+ )} +
+ + +
+ + {/* Table header */} +
+ Name + Input + Expected Behavior + Expected Output + Simulation Instr. + Evaluators +
+ + {/* Scrollable item rows */} +
+ {detail.items.map((item: EvalItem) => { + const isSelected = item.name === selectedItemName; + return ( + + ); + })} + {detail.items.length === 0 && ( +
+ No items in this eval set +
+ )} +
+
+ + {/* Sidebar drag handle */} +
+ + {/* Right sidebar */} +
+ {/* Tab bar */} +
+ {(["io", "evaluators"] as const).map((tab) => { + const active = sidebarTab === tab; + const label = tab === "io" ? "I/O" : "Evaluators"; + return ( + + ); + })} +
+ + {/* Content */} +
+ {selectedItem ? ( + sidebarTab === "io" ? ( + + ) : ( + + ) + ) : null} +
+
+
+ ); +} + +/** Inline-editable field: click to edit, blur/Enter to save, Escape to cancel. */ +function InlineField({ + label, + value, + placeholder, + onSave, + mono, + multiline, +}: { + label: string; + value: string; + placeholder: string; + onSave: (val: string) => void; + mono?: boolean; + multiline?: boolean; +}) { + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(value); + const ref = useRef(null); + + useEffect(() => { setDraft(value); }, [value]); + useEffect(() => { if (editing) ref.current?.focus(); }, [editing]); + + const save = () => { + setEditing(false); + if (draft !== value) onSave(draft); + }; + const cancel = () => { setEditing(false); setDraft(value); }; + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") cancel(); + if (e.key === "Enter" && !multiline) save(); + if (e.key === "Enter" && e.metaKey && multiline) save(); + }; + + const inputStyle: React.CSSProperties = { + background: "var(--bg-primary)", + border: "1px solid var(--border)", + color: "var(--text-primary)", + }; + + return ( +
+
+ {label} +
+ {editing ? ( +
+ {multiline ? ( +