diff --git a/agent/_patch_messages_reducer.py b/agent/_patch_messages_reducer.py new file mode 100644 index 00000000..532534ab --- /dev/null +++ b/agent/_patch_messages_reducer.py @@ -0,0 +1,72 @@ +"""Guard deepagents message reducer against None checkpoint state. + +DeltaChannel snapshots can persist ``None`` for ``messages`` (e.g. after a +cancelled run). The stock reducer calls ``convert_to_messages(None)``, which +raises when LangGraph replays checkpoint history for ``GET /threads/.../state``. + +Must load ``deepagents._messages_reducer`` without importing ``deepagents.graph`` +first — ``deepagents.__init__`` eagerly imports ``create_deep_agent``, which +binds the reducer into ``DeltaChannel`` at class definition time. +""" + +from __future__ import annotations + +import importlib +import importlib.util +import sys +from collections.abc import Sequence +from pathlib import Path + +from langchain_core.messages import AnyMessage + +_PATCHED_ATTR = "_open_swe_messages_reducer_patched" + + +def _load_messages_reducer_module(): + name = "deepagents._messages_reducer" + existing = sys.modules.get(name) + if existing is not None: + return existing + + spec = importlib.util.find_spec("deepagents") + if spec is None or not spec.origin: + raise ImportError("deepagents package not found") + root = Path(spec.origin).parent + reducer_spec = importlib.util.spec_from_file_location( + name, + root / "_messages_reducer.py", + ) + if reducer_spec is None or reducer_spec.loader is None: + raise ImportError("deepagents._messages_reducer not found") + mod = importlib.util.module_from_spec(reducer_spec) + sys.modules[name] = mod + reducer_spec.loader.exec_module(mod) + return mod + + +def _apply() -> None: + mod = _load_messages_reducer_module() + if getattr(mod._messages_delta_reducer, _PATCHED_ATTR, False): + return + + orig = mod._messages_delta_reducer + if getattr(orig, _PATCHED_ATTR, False): + return + + def _messages_delta_reducer( + state: list[AnyMessage] | None, writes: Sequence[list[AnyMessage]] + ) -> list[AnyMessage]: + if state is None: + state = [] + return orig(state, writes) + + mod._messages_delta_reducer = _messages_delta_reducer + setattr(_messages_delta_reducer, _PATCHED_ATTR, True) + + graph_mod = sys.modules.get("deepagents.graph") + if graph_mod is not None: + graph_mod._messages_delta_reducer = mod._messages_delta_reducer + importlib.reload(graph_mod) + + +_apply() diff --git a/agent/dashboard/message_adapter.py b/agent/dashboard/message_adapter.py new file mode 100644 index 00000000..1180fcdf --- /dev/null +++ b/agent/dashboard/message_adapter.py @@ -0,0 +1,238 @@ +"""Convert LangGraph / LangChain message dicts into dashboard UI message payloads.""" + +from __future__ import annotations + +import json +import uuid +from datetime import UTC, datetime +from typing import Any + +from ..utils.messages import extract_text_content + +_READ_TOOLS = frozenset({"read_file", "read", "glob", "grep"}) +_EDIT_TOOLS = frozenset({"write_file", "edit_file", "str_replace", "write", "edit", "patch"}) +_EXECUTE_TOOLS = frozenset({"execute", "bash", "shell", "run_terminal_cmd"}) +_SEARCH_TOOLS = frozenset({"glob", "grep", "web_search", "fetch_url", "search"}) +_INTERNAL_TOOLS = frozenset({"confirming_completion", "no_op"}) + + +def _now_iso() -> str: + return datetime.now(UTC).isoformat() + + +def _message_type(message: dict[str, Any]) -> str: + raw = message.get("type") + if isinstance(raw, str): + return raw.lower() + role = message.get("role") + if role == "user": + return "human" + if role == "assistant": + return "ai" + if role == "tool": + return "tool" + return "unknown" + + +def _tool_kind(name: str) -> str: + lowered = name.lower() + if lowered in _EDIT_TOOLS or any(token in lowered for token in ("edit", "write", "replace")): + return "edit" + if lowered in _EXECUTE_TOOLS: + return "execute" + if lowered in _SEARCH_TOOLS: + return "search" + if lowered in _READ_TOOLS or "read" in lowered: + return "read" + if lowered == "think": + return "think" + if lowered in {"fetch", "fetch_url", "http_request"}: + return "fetch" + return "other" + + +def _tool_title(name: str, args: dict[str, Any]) -> str: + path = args.get("path") or args.get("file_path") or args.get("target_file") + if isinstance(path, str) and path.strip(): + return f"{name} {path.strip()}" + command = args.get("command") + if isinstance(command, str) and command.strip(): + first = command.strip().splitlines()[0] + return first[:120] + return name.replace("_", " ").strip() or "Tool" + + +def _parse_tool_args(raw: Any) -> dict[str, Any]: + if isinstance(raw, dict): + return raw + if isinstance(raw, str): + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + return {"raw": raw} + return parsed if isinstance(parsed, dict) else {"raw": raw} + return {} + + +def _maybe_diff_from_args(name: str, args: dict[str, Any]) -> dict[str, Any] | None: + path = args.get("path") or args.get("file_path") or args.get("target_file") + if not isinstance(path, str) or not path.strip(): + return None + old_content = args.get("old_string") or args.get("original_content") + new_content = args.get("new_string") or args.get("content") or args.get("new_content") + if not isinstance(new_content, str): + return None + original = old_content if isinstance(old_content, str) else None + return { + "originalContent": original, + "newContent": new_content, + "filePath": path.strip(), + "isNewFile": original is None, + "isBinary": False, + "isTruncated": False, + "totalLines": max(new_content.count("\n"), 0) + 1, + } + + +def _is_internal_tool(name: str) -> bool: + return name in _INTERNAL_TOOLS + + +def _append_agent_chunks( + agent_turn: dict[str, Any] | None, + *, + msg_id: str, + timestamp: str, + chunks: list[dict[str, Any]], +) -> dict[str, Any]: + if agent_turn is None: + return { + "id": msg_id, + "author": "agent", + "timestamp": timestamp, + "chunks": list(chunks), + } + agent_turn["timestamp"] = timestamp + agent_turn["chunks"].extend(chunks) + return agent_turn + + +def _merge_text_chunks(chunks: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Keep only the latest text chunk when middleware produced a follow-up AI message.""" + text_indices = [i for i, chunk in enumerate(chunks) if chunk.get("kind") == "text"] + if len(text_indices) <= 1: + return chunks + last_text = text_indices[-1] + return [ + chunk for i, chunk in enumerate(chunks) if chunk.get("kind") != "text" or i == last_text + ] + + +def state_messages_to_ui(messages: list[Any]) -> list[dict[str, Any]]: + """Map LangGraph state ``messages`` to the dashboard ``Message`` JSON shape.""" + pending_tools: dict[str, dict[str, Any]] = {} + ui_messages: list[dict[str, Any]] = [] + agent_turn: dict[str, Any] | None = None + + for index, raw in enumerate(messages): + if not isinstance(raw, dict): + continue + msg_type = _message_type(raw) + msg_id = raw.get("id") if isinstance(raw.get("id"), str) else f"msg-{index}" + timestamp = raw.get("created_at") if isinstance(raw.get("created_at"), str) else _now_iso() + + if msg_type in {"human", "user"}: + if agent_turn is not None: + agent_turn["chunks"] = _merge_text_chunks(agent_turn["chunks"]) + ui_messages.append(agent_turn) + agent_turn = None + text = extract_text_content(raw.get("content", "")) + if not text: + continue + ui_messages.append( + { + "id": msg_id, + "author": "user", + "timestamp": timestamp, + "chunks": [{"kind": "text", "text": text}], + } + ) + continue + + if msg_type in {"ai", "assistant"}: + chunks: list[dict[str, Any]] = [] + text = extract_text_content(raw.get("content", "")) + if text: + chunks.append({"kind": "text", "text": text}) + + for tool_call in raw.get("tool_calls") or []: + if not isinstance(tool_call, dict): + continue + name = tool_call.get("name") + if not isinstance(name, str) or not name: + name = "tool" + if _is_internal_tool(name): + continue + tool_call_id = tool_call.get("id") or tool_call.get("tool_call_id") + if not isinstance(tool_call_id, str) or not tool_call_id: + tool_call_id = f"tool-{uuid.uuid4().hex[:8]}" + args = _parse_tool_args(tool_call.get("args")) + chunk: dict[str, Any] = { + "kind": "tool-execution", + "toolCallId": tool_call_id, + "title": _tool_title(name, args), + "toolKind": _tool_kind(name), + "input": args, + "status": "in_progress", + } + diff_data = _maybe_diff_from_args(name, args) + if diff_data: + chunk["diffData"] = diff_data + chunks.append(chunk) + pending_tools[tool_call_id] = chunk + + if chunks: + agent_turn = _append_agent_chunks( + agent_turn, msg_id=msg_id, timestamp=timestamp, chunks=chunks + ) + continue + + if msg_type == "tool": + tool_call_id = raw.get("tool_call_id") + if not isinstance(tool_call_id, str): + continue + name = raw.get("name") if isinstance(raw.get("name"), str) else "tool" + if _is_internal_tool(name): + pending_tools.pop(tool_call_id, None) + continue + chunk = pending_tools.get(tool_call_id) + output = extract_text_content(raw.get("content", "")) + if chunk is not None: + chunk["status"] = "error" if raw.get("status") == "error" else "completed" + if output: + chunk["output"] = output + continue + + if agent_turn is None: + agent_turn = { + "id": msg_id, + "author": "agent", + "timestamp": timestamp, + "chunks": [], + } + agent_turn["chunks"].append( + { + "kind": "tool-execution", + "toolCallId": tool_call_id, + "title": name, + "toolKind": _tool_kind(name), + "status": "completed", + "output": output, + } + ) + + if agent_turn is not None: + agent_turn["chunks"] = _merge_text_chunks(agent_turn["chunks"]) + ui_messages.append(agent_turn) + + return ui_messages diff --git a/agent/dashboard/routes.py b/agent/dashboard/routes.py index 29f32e5b..1982e82b 100644 --- a/agent/dashboard/routes.py +++ b/agent/dashboard/routes.py @@ -9,7 +9,7 @@ from typing import Any import httpx from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import RedirectResponse, Response +from fastapi.responses import RedirectResponse, Response, StreamingResponse from pydantic import BaseModel from .admin import is_admin @@ -61,6 +61,17 @@ from .team_settings import ( get_team_settings, upsert_team_settings, ) +from .thread_api import ( + ThreadCreateBody, + ThreadMessageBody, + cancel_dashboard_thread, + create_dashboard_thread, + delete_dashboard_thread, + get_dashboard_thread, + list_dashboard_threads, + send_dashboard_message, + stream_dashboard_thread, +) logger = logging.getLogger(__name__) @@ -562,3 +573,68 @@ async def api_delete_review_style( await cancel_review_style_analysis(full_name) await delete_review_style(full_name) return Response(status_code=204) + + +@router.get("/threads") +async def api_list_threads( + session: dict[str, Any] = _SESSION_DEP, +) -> list[dict[str, Any]]: + return await list_dashboard_threads(session["sub"]) + + +@router.post("/threads") +async def api_create_thread( + body: ThreadCreateBody, + session: dict[str, Any] = _SESSION_DEP, +) -> dict[str, Any]: + return await create_dashboard_thread(session["sub"], body) + + +@router.get("/threads/{thread_id}") +async def api_get_thread( + thread_id: str, + session: dict[str, Any] = _SESSION_DEP, +) -> dict[str, Any]: + return await get_dashboard_thread(thread_id, session["sub"]) + + +@router.post("/threads/{thread_id}/messages") +async def api_send_thread_message( + thread_id: str, + body: ThreadMessageBody, + session: dict[str, Any] = _SESSION_DEP, +) -> dict[str, Any]: + return await send_dashboard_message(thread_id, session["sub"], body) + + +@router.post("/threads/{thread_id}/cancel") +async def api_cancel_thread( + thread_id: str, + session: dict[str, Any] = _SESSION_DEP, +) -> dict[str, Any]: + return await cancel_dashboard_thread(thread_id, session["sub"]) + + +@router.delete("/threads/{thread_id}") +async def api_delete_thread( + thread_id: str, + session: dict[str, Any] = _SESSION_DEP, +) -> Response: + await delete_dashboard_thread(thread_id, session["sub"]) + return Response(status_code=204) + + +@router.get("/threads/{thread_id}/stream") +async def api_stream_thread( + thread_id: str, + session: dict[str, Any] = _SESSION_DEP, +) -> StreamingResponse: + async def event_generator(): + async for chunk in stream_dashboard_thread(thread_id, session["sub"]): + yield chunk + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, + ) diff --git a/agent/dashboard/thread_api.py b/agent/dashboard/thread_api.py new file mode 100644 index 00000000..0646f008 --- /dev/null +++ b/agent/dashboard/thread_api.py @@ -0,0 +1,441 @@ +"""Dashboard thread list/detail/run/stream endpoints backed by LangGraph.""" + +from __future__ import annotations + +import json +import logging +import os +import uuid +from collections.abc import AsyncIterator +from datetime import UTC, datetime +from typing import Any + +from fastapi import HTTPException +from langgraph_sdk.errors import InternalServerError +from pydantic import BaseModel, Field + +from ..utils.auth import persist_encrypted_github_token +from ..utils.thread_ops import is_thread_active, langgraph_client, queue_message_for_thread +from .agent_overrides import get_profile_default_repo +from .message_adapter import state_messages_to_ui +from .options import SUPPORTED_MODEL_IDS, model_supports_effort +from .profiles import OAUTH_TOKENS_NAMESPACE, get_profile, get_valid_access_token +from .profiles import _get_value as get_oauth_record + +logger = logging.getLogger(__name__) + +_ASSISTANT_ID = "agent" +_DASHBOARD_SOURCE = "dashboard" + + +def _agent_version_metadata() -> dict[str, str]: + revision = os.environ.get("LANGCHAIN_REVISION_ID") + return {"LANGSMITH_AGENT_VERSION": revision} if revision else {} + + +class ThreadCreateBody(BaseModel): + prompt: str = Field(min_length=1, max_length=20_000) + repo: str | None = None + model_id: str | None = None + effort: str | None = None + + +class ThreadMessageBody(BaseModel): + content: str = Field(min_length=1, max_length=20_000) + model_id: str | None = None + effort: str | None = None + + +def _normalize_model_choice( + model_id: str | None, effort: str | None +) -> tuple[str | None, str | None]: + if not isinstance(model_id, str) or model_id not in SUPPORTED_MODEL_IDS: + return None, None + if not isinstance(effort, str) or not model_supports_effort(model_id, effort): + return None, None + return model_id, effort + + +def _now_ms() -> int: + return int(datetime.now(UTC).timestamp() * 1000) + + +def _parse_repo(full_name: str | None) -> dict[str, str] | None: + if not isinstance(full_name, str): + return None + parts = full_name.strip().split("/", 1) + if len(parts) != 2: + return None + owner, name = parts[0].strip(), parts[1].strip() + if not owner or not name: + return None + return {"owner": owner, "name": name} + + +async def _persist_dashboard_github_token(thread_id: str, login: str) -> None: + token = await get_valid_access_token(login) + if not token: + raise HTTPException(401, "github token unavailable, re-login required") + record = await get_oauth_record(OAUTH_TOKENS_NAMESPACE, login) + expires_at = record.get("token_expires_at") if isinstance(record, dict) else None + await persist_encrypted_github_token( + thread_id, + token, + expires_at=expires_at if isinstance(expires_at, str) else None, + ) + + +def _thread_owner_login(metadata: dict[str, Any]) -> str | None: + login = metadata.get("github_login") + return login.strip() if isinstance(login, str) and login.strip() else None + + +def _assert_thread_owner(metadata: dict[str, Any], login: str) -> None: + owner = _thread_owner_login(metadata) + if owner != login: + raise HTTPException(404, "thread not found") + if metadata.get("source") != _DASHBOARD_SOURCE: + raise HTTPException(404, "thread not found") + + +def _metadata_repo(metadata: dict[str, Any]) -> tuple[str, str, str]: + owner = metadata.get("repo_owner") + name = metadata.get("repo_name") + if isinstance(owner, str) and isinstance(name, str) and owner and name: + return owner, name, f"{owner}/{name}" + repo = metadata.get("repo") + if isinstance(repo, dict): + o = repo.get("owner") + n = repo.get("name") + if isinstance(o, str) and isinstance(n, str) and o and n: + return o, n, f"{o}/{n}" + return "", "", "" + + +def _run_status_to_agent_status(thread_status: str | None, run_status: str | None) -> str: + if thread_status == "busy" or run_status in {"pending", "running"}: + return "running" + if run_status in {"error", "failed", "timeout", "interrupted"}: + return "error" + if run_status == "success": + return "finished" + return "idle" + + +def _thread_summary( + thread: dict[str, Any], *, messages: list[dict[str, Any]] | None = None +) -> dict[str, Any]: + metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {} + owner, name, full_name = _metadata_repo(metadata) + created_at = metadata.get("created_at_ms") + updated_at = metadata.get("updated_at_ms") + title = metadata.get("title") if isinstance(metadata.get("title"), str) else "Untitled agent" + model = metadata.get("model") if isinstance(metadata.get("model"), str) else "Default" + effort = metadata.get("effort") if isinstance(metadata.get("effort"), str) else None + thread_status = thread.get("status") if isinstance(thread.get("status"), str) else "idle" + latest_run_status = metadata.get("latest_run_status") + status = _run_status_to_agent_status( + thread_status, + latest_run_status if isinstance(latest_run_status, str) else None, + ) + + pr_number = metadata.get("pr_number") + pr_url = metadata.get("pr_url") + pr_title = metadata.get("pr_title") + pr_state = metadata.get("pr_state") + + summary: dict[str, Any] = { + "id": thread.get("thread_id") or thread.get("id"), + "title": title, + "repo": name or "unknown", + "repoFullName": full_name or "unknown/unknown", + "branch": metadata.get("branch_name") or metadata.get("base_branch") or "main", + "model": model, + "effort": effort, + "status": status, + "createdAt": int(created_at) if isinstance(created_at, (int, float)) else _now_ms(), + "updatedAt": int(updated_at) if isinstance(updated_at, (int, float)) else _now_ms(), + } + if isinstance(pr_number, int) and isinstance(pr_url, str): + summary["pr"] = { + "number": pr_number, + "title": pr_title if isinstance(pr_title, str) else title, + "state": pr_state if isinstance(pr_state, str) else "open", + "headRef": metadata.get("branch_name") or "", + "baseRef": metadata.get("base_branch") or "main", + "url": pr_url, + } + if messages is not None: + summary["messages"] = messages + else: + summary["messages"] = [] + return summary + + +async def _latest_run_status(thread_id: str) -> str | None: + runs = await langgraph_client().runs.list(thread_id, limit=1) + if not runs: + return None + run = runs[0] + raw = run.get("status") if isinstance(run, dict) else getattr(run, "status", None) + return raw.lower() if isinstance(raw, str) else None + + +async def list_dashboard_threads(login: str, *, limit: int = 50) -> list[dict[str, Any]]: + threads = await langgraph_client().threads.search( + metadata={"source": _DASHBOARD_SOURCE, "github_login": login}, + limit=limit, + sort_by="updated_at", + sort_order="desc", + ) + out: list[dict[str, Any]] = [] + for thread in threads or []: + if isinstance(thread, dict): + out.append(_thread_summary(thread)) + return out + + +async def get_dashboard_thread(thread_id: str, login: str) -> dict[str, Any]: + client = langgraph_client() + try: + thread = await client.threads.get(thread_id) + except Exception as exc: # noqa: BLE001 + logger.debug("Thread lookup failed for %s", thread_id, exc_info=True) + raise HTTPException(404, "thread not found") from exc + + metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {} + _assert_thread_owner(metadata, login) + + messages: list[dict[str, Any]] = [] + try: + state = await client.threads.get_state(thread_id) + except InternalServerError: + logger.warning( + "Thread state unavailable for %s (checkpoint replay failed); returning metadata only", + thread_id, + ) + else: + values = state.get("values") if isinstance(state, dict) else {} + raw_messages = values.get("messages") if isinstance(values, dict) else [] + messages = state_messages_to_ui(raw_messages if isinstance(raw_messages, list) else []) + + latest_run_status = await _latest_run_status(thread_id) + if latest_run_status and latest_run_status != metadata.get("latest_run_status"): + metadata = {**metadata, "latest_run_status": latest_run_status} + thread = {**thread, "metadata": metadata} + + return _thread_summary(thread, messages=messages) + + +async def _resolve_repo_config(login: str, repo: str | None) -> dict[str, str]: + parsed = _parse_repo(repo) + if parsed: + return parsed + profile_repo = await get_profile_default_repo(login) + if profile_repo: + return profile_repo + profile = await get_profile(login) + parsed = _parse_repo(profile.get("default_repo") if isinstance(profile, dict) else None) + if parsed: + return parsed + raise HTTPException(400, "no default repository configured — set one in Cloud Agents settings") + + +async def _start_agent_run( + thread_id: str, + *, + login: str, + repo_config: dict[str, str], + prompt: str, + title: str | None = None, + model_id: str | None = None, + effort: str | None = None, +) -> dict[str, Any]: + profile = await get_profile(login) or {} + now_ms = _now_ms() + chosen_model, chosen_effort = _normalize_model_choice(model_id, effort) + metadata_model = chosen_model or profile.get("default_model") or "Default" + metadata_effort = chosen_effort or profile.get("reasoning_effort") + metadata = { + "source": _DASHBOARD_SOURCE, + "github_login": login, + "title": title or prompt[:80] or "New agent", + "repo_owner": repo_config["owner"], + "repo_name": repo_config["name"], + "base_branch": profile.get("base_branch") or "main", + "branch_prefix": profile.get("branch_prefix"), + "model": metadata_model, + "effort": metadata_effort, + "created_at_ms": now_ms, + "updated_at_ms": now_ms, + } + + client = langgraph_client() + await client.threads.create(thread_id=thread_id, metadata=metadata, if_exists="do_nothing") + await client.threads.update(thread_id=thread_id, metadata=metadata) + await _persist_dashboard_github_token(thread_id, login) + + configurable: dict[str, Any] = { + "thread_id": thread_id, + "source": _DASHBOARD_SOURCE, + "github_login": login, + "repo": repo_config, + "user_email": profile.get("email"), + } + if chosen_model and chosen_effort: + configurable["agent_model_id"] = chosen_model + configurable["agent_effort"] = chosen_effort + + run = await client.runs.create( + thread_id, + _ASSISTANT_ID, + input={"messages": [{"role": "user", "content": prompt}]}, + config={"configurable": configurable, "metadata": _agent_version_metadata()}, + if_not_exists="create", + ) + run_id = run.get("run_id") if isinstance(run, dict) else getattr(run, "run_id", None) + await client.threads.update( + thread_id=thread_id, + metadata={"latest_run_id": run_id, "latest_run_status": "pending", "updated_at_ms": now_ms}, + ) + thread = await client.threads.get(thread_id) + return _thread_summary( + thread if isinstance(thread, dict) else {"thread_id": thread_id, "metadata": metadata} + ) + + +async def create_dashboard_thread(login: str, body: ThreadCreateBody) -> dict[str, Any]: + repo_config = await _resolve_repo_config(login, body.repo) + thread_id = str(uuid.uuid4()) + return await _start_agent_run( + thread_id, + login=login, + repo_config=repo_config, + prompt=body.prompt.strip(), + model_id=body.model_id, + effort=body.effort, + ) + + +async def send_dashboard_message( + thread_id: str, login: str, body: ThreadMessageBody +) -> dict[str, Any]: + client = langgraph_client() + try: + thread = await client.threads.get(thread_id) + except Exception as exc: # noqa: BLE001 + raise HTTPException(404, "thread not found") from exc + + metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {} + _assert_thread_owner(metadata, login) + owner, name, _ = _metadata_repo(metadata) + if not owner or not name: + raise HTTPException(400, "thread is missing repository metadata") + + prompt = body.content.strip() + now_ms = _now_ms() + chosen_model, chosen_effort = _normalize_model_choice(body.model_id, body.effort) + metadata_update: dict[str, Any] = {"updated_at_ms": now_ms} + if chosen_model and chosen_effort: + metadata_update["model"] = chosen_model + metadata_update["effort"] = chosen_effort + await client.threads.update(thread_id=thread_id, metadata=metadata_update) + + if await is_thread_active(thread_id): + queued = await queue_message_for_thread(thread_id, prompt) + if not queued: + raise HTTPException(502, "failed to queue follow-up message") + thread = await client.threads.get(thread_id) + return _thread_summary( + thread if isinstance(thread, dict) else {"thread_id": thread_id, "metadata": metadata} + ) + + await _persist_dashboard_github_token(thread_id, login) + profile = await get_profile(login) or {} + configurable: dict[str, Any] = { + "thread_id": thread_id, + "source": _DASHBOARD_SOURCE, + "github_login": login, + "repo": {"owner": owner, "name": name}, + "user_email": profile.get("email"), + } + if chosen_model and chosen_effort: + configurable["agent_model_id"] = chosen_model + configurable["agent_effort"] = chosen_effort + run = await client.runs.create( + thread_id, + _ASSISTANT_ID, + input={"messages": [{"role": "user", "content": prompt}]}, + config={"configurable": configurable, "metadata": _agent_version_metadata()}, + ) + run_id = run.get("run_id") if isinstance(run, dict) else getattr(run, "run_id", None) + await client.threads.update( + thread_id=thread_id, + metadata={"latest_run_id": run_id, "latest_run_status": "pending", "updated_at_ms": now_ms}, + ) + thread = await client.threads.get(thread_id) + return _thread_summary( + thread if isinstance(thread, dict) else {"thread_id": thread_id, "metadata": metadata} + ) + + +async def cancel_dashboard_thread(thread_id: str, login: str) -> dict[str, Any]: + client = langgraph_client() + try: + thread = await client.threads.get(thread_id) + except Exception as exc: # noqa: BLE001 + raise HTTPException(404, "thread not found") from exc + + metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {} + _assert_thread_owner(metadata, login) + + run_id = metadata.get("latest_run_id") + if isinstance(run_id, str) and run_id: + try: + await client.runs.cancel(thread_id, run_id, wait=False) + except Exception: + logger.debug("Could not cancel run %s for thread %s", run_id, thread_id, exc_info=True) + + await client.threads.update( + thread_id=thread_id, + metadata={"latest_run_status": "interrupted", "updated_at_ms": _now_ms()}, + ) + thread = await client.threads.get(thread_id) + return _thread_summary( + thread if isinstance(thread, dict) else {"thread_id": thread_id, "metadata": metadata} + ) + + +async def delete_dashboard_thread(thread_id: str, login: str) -> None: + client = langgraph_client() + try: + thread = await client.threads.get(thread_id) + except Exception as exc: # noqa: BLE001 + raise HTTPException(404, "thread not found") from exc + + metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {} + _assert_thread_owner(metadata, login) + + run_id = metadata.get("latest_run_id") + if isinstance(run_id, str) and run_id: + try: + await client.runs.cancel(thread_id, run_id, wait=False) + except Exception: + logger.debug("Could not cancel run %s for thread %s", run_id, thread_id, exc_info=True) + + await client.threads.delete(thread_id) + + +async def stream_dashboard_thread(thread_id: str, login: str) -> AsyncIterator[str]: + try: + thread = await langgraph_client().threads.get(thread_id) + except Exception as exc: # noqa: BLE001 + raise HTTPException(404, "thread not found") from exc + + metadata = thread.get("metadata") if isinstance(thread.get("metadata"), dict) else {} + _assert_thread_owner(metadata, login) + + stream = await langgraph_client().threads.join_stream(thread_id) + async for part in stream: + payload = part if isinstance(part, dict) else {"event": str(part)} + yield f"data: {json.dumps(payload, default=str)}\n\n" diff --git a/agent/middleware/ensure_no_empty_msg.py b/agent/middleware/ensure_no_empty_msg.py index 76aba32e..19580cfa 100644 --- a/agent/middleware/ensure_no_empty_msg.py +++ b/agent/middleware/ensure_no_empty_msg.py @@ -3,8 +3,11 @@ from uuid import uuid4 from langchain.agents.middleware import AgentState, after_model from langchain_core.messages import AnyMessage, ToolMessage +from langgraph.config import get_config from langgraph.runtime import Runtime +_DASHBOARD_SOURCE = "dashboard" + def get_every_message_since_last_human(state: AgentState) -> list[AnyMessage]: messages = state["messages"] @@ -40,6 +43,17 @@ def check_if_no_op(messages: list[AnyMessage]) -> bool: return False +def _is_dashboard_source() -> bool: + try: + config = get_config() + except RuntimeError: + return False + configurable = config.get("configurable", {}) + if not isinstance(configurable, dict): + return False + return configurable.get("source") == _DASHBOARD_SOURCE + + @after_model def ensure_no_empty_msg(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: last_msg = state["messages"][-1] @@ -67,9 +81,11 @@ def ensure_no_empty_msg(state: AgentState, runtime: Runtime) -> dict[str, Any] | if has_contents and not has_tool_calls: messages_since_last_human = get_every_message_since_last_human(state) - if check_if_model_messaged_user( - messages_since_last_human - ) or check_if_confirming_completion(messages_since_last_human): + if ( + check_if_model_messaged_user(messages_since_last_human) + or check_if_confirming_completion(messages_since_last_human) + or _is_dashboard_source() + ): return None tc_id = str(uuid4()) diff --git a/agent/review_style_analyzer.py b/agent/review_style_analyzer.py index 77a2c83a..4cc9dd11 100644 --- a/agent/review_style_analyzer.py +++ b/agent/review_style_analyzer.py @@ -19,6 +19,10 @@ from langgraph.pregel import Pregel warnings.filterwarnings("ignore", module="langchain_core._api.deprecation") warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarning) +from ._patch_messages_reducer import _apply as _apply_messages_reducer_patch + +_apply_messages_reducer_patch() + from deepagents import create_deep_agent from deepagents.backends.protocol import SandboxBackendProtocol from langchain.agents.middleware import ModelCallLimitMiddleware diff --git a/agent/reviewer.py b/agent/reviewer.py index 9fe3a7b3..388106cb 100644 --- a/agent/reviewer.py +++ b/agent/reviewer.py @@ -26,6 +26,10 @@ from langgraph.pregel import Pregel warnings.filterwarnings("ignore", module="langchain_core._api.deprecation") warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarning) +from ._patch_messages_reducer import _apply as _apply_messages_reducer_patch + +_apply_messages_reducer_patch() + from deepagents import create_deep_agent from langchain.agents.middleware import ModelCallLimitMiddleware diff --git a/agent/server.py b/agent/server.py index c6e59dce..639c2d60 100644 --- a/agent/server.py +++ b/agent/server.py @@ -22,6 +22,10 @@ import asyncio warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarning) # Now safe to import agent (which imports LangChain modules) +from ._patch_messages_reducer import _apply as _apply_messages_reducer_patch + +_apply_messages_reducer_patch() + from deepagents import create_deep_agent from deepagents.backends import LangSmithSandbox from deepagents.backends.protocol import SandboxBackendProtocol @@ -34,7 +38,7 @@ from .dashboard.agent_overrides import ( profile_create_prs, resolve_github_login, ) -from .dashboard.options import DEFAULT_MODEL_ID +from .dashboard.options import DEFAULT_MODEL_ID, SUPPORTED_MODEL_IDS, model_supports_effort from .dashboard.team_settings import get_team_default_model from .integrations.langsmith import _configure_github_proxy from .middleware import ( @@ -398,6 +402,23 @@ async def get_agent(config: RunnableConfig) -> Pregel: model_id = overridden_model profile_effort = overridden_effort + configurable = (config or {}).get("configurable") or {} + per_thread_model = configurable.get("agent_model_id") + per_thread_effort = configurable.get("agent_effort") + if ( + isinstance(per_thread_model, str) + and per_thread_model in SUPPORTED_MODEL_IDS + and isinstance(per_thread_effort, str) + and model_supports_effort(per_thread_model, per_thread_effort) + ): + logger.info( + "Applying per-thread model override: model=%s effort=%s", + per_thread_model, + per_thread_effort, + ) + model_id = per_thread_model + profile_effort = per_thread_effort + create_prs = profile_create_prs(profile) if not create_prs: logger.info("PR creation disabled by profile for %s", profile_login) diff --git a/agent/utils/auth.py b/agent/utils/auth.py index eaad44de..3dd19058 100644 --- a/agent/utils/auth.py +++ b/agent/utils/auth.py @@ -418,6 +418,29 @@ async def resolve_github_token( if not email: raise ValueError(f"No email mapping found for GitHub user '{github_login}'") return await save_encrypted_token_from_email(email, source) + if source == "dashboard": + cached_token, cached_encrypted, cached_expires_at = await get_github_token_from_thread( + thread_id + ) + if cached_token and cached_encrypted: + return cached_token, cached_encrypted, cached_expires_at + github_login = configurable.get("github_login") + if not isinstance(github_login, str) or not github_login.strip(): + raise ValueError("missing github_login for dashboard run") + from ..dashboard.profiles import OAUTH_TOKENS_NAMESPACE, get_valid_access_token + from ..dashboard.profiles import _get_value as get_oauth_record + + token = await get_valid_access_token(github_login.strip()) + if not token: + raise ValueError("github token unavailable, re-login required") + record = await get_oauth_record(OAUTH_TOKENS_NAMESPACE, github_login.strip()) + expires_at = record.get("token_expires_at") if isinstance(record, dict) else None + encrypted = await persist_encrypted_github_token( + thread_id, + token, + expires_at=expires_at if isinstance(expires_at, str) else None, + ) + return token, encrypted, expires_at if isinstance(expires_at, str) else None return await save_encrypted_token_from_email(configurable.get("user_email"), source) except ValueError as exc: logger.error("GitHub auth failed for thread %s: %s", thread_id, str(exc)) diff --git a/agent/utils/thread_ops.py b/agent/utils/thread_ops.py new file mode 100644 index 00000000..1c4913d2 --- /dev/null +++ b/agent/utils/thread_ops.py @@ -0,0 +1,68 @@ +"""Shared LangGraph thread helpers for webhooks and the dashboard.""" + +from __future__ import annotations + +import logging +import os +from typing import Any + +from langgraph_sdk import get_client + +logger = logging.getLogger(__name__) + + +def langgraph_url() -> str: + return os.environ.get("LANGGRAPH_URL") or os.environ.get( + "LANGGRAPH_URL_PROD", "http://localhost:2024" + ) + + +def langgraph_client(): + return get_client(url=langgraph_url()) + + +async def is_thread_active(thread_id: str) -> bool: + """Return whether the thread currently has a running run.""" + try: + thread = await langgraph_client().threads.get(thread_id) + status = thread.get("status", "idle") if isinstance(thread, dict) else "idle" + logger.info("Thread %s status check: status=%s", thread_id, status) + return status == "busy" + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to get thread status for %s: %s — assuming not active", + thread_id, + exc, + ) + return False + + +async def queue_message_for_thread( + thread_id: str, message_content: str | list[dict[str, Any]] | dict[str, Any] +) -> bool: + """Queue a follow-up message for a busy thread (FIFO store namespace).""" + client = langgraph_client() + try: + namespace = ("queue", thread_id) + key = "pending_messages" + new_message = {"content": message_content} + + existing_messages: list[dict[str, Any]] = [] + try: + existing_item = await client.store.get_item(namespace, key) + if existing_item and existing_item.get("value"): + existing_messages = existing_item["value"].get("messages", []) + except Exception: # noqa: BLE001 + logger.debug("No existing queued messages for thread %s", thread_id) + + existing_messages.append(new_message) + await client.store.put_item(namespace, key, {"messages": existing_messages}) + logger.info( + "Queued message for thread %s (total queued: %d)", + thread_id, + len(existing_messages), + ) + return True + except Exception: + logger.exception("Failed to queue message for thread %s", thread_id) + return False diff --git a/agent/webapp.py b/agent/webapp.py index 69586f44..a0c6ca85 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -85,6 +85,7 @@ from .utils.slack_feedback import ( process_slack_reaction_added, process_slack_reaction_removed, ) +from .utils.thread_ops import is_thread_active, queue_message_for_thread logger = logging.getLogger(__name__) @@ -556,37 +557,6 @@ async def get_slack_repo_config( return repo_config -async def is_thread_active(thread_id: str) -> bool: - """Check if a thread is currently active (has a running run). - - Args: - thread_id: The LangGraph thread ID - - Returns: - True if the thread status is "busy", False otherwise - """ - langgraph_client = get_client(url=LANGGRAPH_URL) - try: - logger.debug("Fetching thread status for %s from %s", thread_id, LANGGRAPH_URL) - thread = await langgraph_client.threads.get(thread_id) - status = thread.get("status", "idle") - logger.info( - "Thread %s status check: status=%s, is_busy=%s", - thread_id, - status, - status == "busy", - ) - except Exception as e: # noqa: BLE001 - logger.warning( - "Failed to get thread status for %s: %s (type: %s) - assuming not active", - thread_id, - e, - type(e).__name__, - ) - status = "idle" - return status == "busy" - - async def _thread_exists(thread_id: str) -> bool: """Return whether a LangGraph thread already exists.""" langgraph_client = get_client(url=LANGGRAPH_URL) @@ -611,53 +581,6 @@ async def _ensure_thread_exists_for_metadata( return False -async def queue_message_for_thread( - thread_id: str, message_content: str | list[dict[str, Any]] | dict[str, Any] -) -> bool: - """Queue a message for a thread that is currently active. - - Stores the message in the langgraph store, namespaced to the thread. - Supports multiple queued messages by storing them as a list (FIFO order). - The before_model middleware will pick them up and inject them into state. - - Args: - thread_id: The LangGraph thread ID - message_content: The message content to queue (text or content blocks) - - Returns: - True if successfully queued, False otherwise - """ - langgraph_client = get_client(url=LANGGRAPH_URL) - try: - namespace = ("queue", thread_id) - key = "pending_messages" - - new_message = {"content": message_content} - - existing_messages: list[dict[str, Any]] = [] - try: - existing_item = await langgraph_client.store.get_item(namespace, key) - if existing_item and existing_item.get("value"): - existing_messages = existing_item["value"].get("messages", []) - except Exception: # noqa: BLE001 - logger.debug("No existing queued messages for thread %s", thread_id) - - existing_messages.append(new_message) - value = {"messages": existing_messages} - - logger.info( - "Attempting to queue message for thread %s (total queued: %d)", - thread_id, - len(existing_messages), - ) - await langgraph_client.store.put_item(namespace, key, value) - logger.info("Successfully queued message for thread %s", thread_id) - return True # noqa: TRY300 - except Exception: - logger.exception("Failed to queue message for thread %s", thread_id) - return False - - async def process_linear_issue( # noqa: PLR0912, PLR0915 issue_data: dict[str, Any], repo_config: dict[str, str] ) -> None: diff --git a/tests/test_dashboard_message_adapter.py b/tests/test_dashboard_message_adapter.py new file mode 100644 index 00000000..b8e112c1 --- /dev/null +++ b/tests/test_dashboard_message_adapter.py @@ -0,0 +1,71 @@ +"""Tests for LangGraph message → dashboard UI adapter.""" + +from agent.dashboard.message_adapter import state_messages_to_ui + + +def test_state_messages_to_ui_maps_user_and_tool_calls() -> None: + messages = [ + {"type": "human", "id": "u1", "content": "Fix the bug"}, + { + "type": "ai", + "id": "a1", + "content": "I'll read the file first.", + "tool_calls": [ + { + "id": "call-1", + "name": "read_file", + "args": {"path": "app.py"}, + } + ], + }, + { + "type": "tool", + "tool_call_id": "call-1", + "name": "read_file", + "content": "print('hi')", + }, + ] + + ui = state_messages_to_ui(messages) + + assert len(ui) == 2 + assert ui[0]["author"] == "user" + assert ui[0]["chunks"][0]["text"] == "Fix the bug" + assert ui[1]["author"] == "agent" + tool_chunk = ui[1]["chunks"][-1] + assert tool_chunk["kind"] == "tool-execution" + assert tool_chunk["toolCallId"] == "call-1" + assert tool_chunk["status"] == "completed" + assert tool_chunk["output"] == "print('hi')" + + +def test_state_messages_to_ui_merges_agent_turn_and_hides_internal_tools() -> None: + messages = [ + {"type": "human", "id": "u1", "content": "hello"}, + { + "type": "ai", + "id": "a1", + "content": "Hi! What would you like me to work on?", + "tool_calls": [{"id": "call-cc", "name": "confirming_completion", "args": {}}], + }, + { + "type": "tool", + "tool_call_id": "call-cc", + "name": "confirming_completion", + "content": "Confirming task completion.", + }, + { + "type": "ai", + "id": "a2", + "content": "Hi! What would you like me to work on? Let me know the task.", + }, + ] + + ui = state_messages_to_ui(messages) + + assert len(ui) == 2 + assert ui[0]["author"] == "user" + assert len(ui[1]["chunks"]) == 1 + assert ( + ui[1]["chunks"][0]["text"] == "Hi! What would you like me to work on? Let me know the task." + ) diff --git a/tests/test_ensure_no_empty_msg.py b/tests/test_ensure_no_empty_msg.py index 4b63b40b..10013703 100644 --- a/tests/test_ensure_no_empty_msg.py +++ b/tests/test_ensure_no_empty_msg.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from langchain_core.messages import AIMessage, HumanMessage, ToolMessage @@ -187,3 +187,21 @@ class TestEnsureNoEmptyMsgNotify: result = ensure_no_empty_msg.after_model(state, self._make_runtime()) assert result is None + + def test_skips_confirming_completion_for_dashboard_source(self) -> None: + ai = AIMessage(content="Hi! How can I help?") + state = { + "messages": [ + HumanMessage(content="hello"), + ai, + ] + } + + with patch( + "agent.middleware.ensure_no_empty_msg.get_config", + return_value={"configurable": {"source": "dashboard"}}, + ): + result = ensure_no_empty_msg.after_model(state, self._make_runtime()) + + assert result is None + assert not ai.tool_calls diff --git a/tests/test_messages_reducer_patch.py b/tests/test_messages_reducer_patch.py new file mode 100644 index 00000000..e520317e --- /dev/null +++ b/tests/test_messages_reducer_patch.py @@ -0,0 +1,82 @@ +import importlib +import sys + +import pytest +from langchain_core.messages import HumanMessage + + +def _clear_deepagents_modules( + *, + include_agent_server: bool = False, + include_agent_reviewer: bool = False, +) -> None: + for name in list(sys.modules): + if name == "deepagents" or name.startswith("deepagents."): + del sys.modules[name] + elif include_agent_server and name == "agent.server": + del sys.modules[name] + elif include_agent_reviewer and name == "agent.reviewer": + del sys.modules[name] + importlib.invalidate_caches() + + +def _restore_agent_modules() -> None: + patch_mod = importlib.import_module("agent._patch_messages_reducer") + importlib.reload(patch_mod) + for name in ("agent.server", "agent.reviewer"): + mod = sys.modules.get(name) + if mod is not None: + importlib.reload(mod) + else: + importlib.import_module(name) + + +def _apply_patch() -> None: + _clear_deepagents_modules() + patch_mod = importlib.import_module("agent._patch_messages_reducer") + importlib.reload(patch_mod) + + +def test_messages_delta_reducer_accepts_none_state() -> None: + _apply_patch() + from deepagents import _messages_reducer as reducer_mod + + result = reducer_mod._messages_delta_reducer(None, []) + assert result == [] + + +def test_messages_delta_reducer_still_merges_writes() -> None: + _apply_patch() + from deepagents import _messages_reducer as reducer_mod + + msg = HumanMessage(content="hi", id="m1") + result = reducer_mod._messages_delta_reducer(None, [[msg]]) + assert len(result) == 1 + assert result[0].content == "hi" + + +def test_graph_binds_patched_reducer_on_server_import() -> None: + _clear_deepagents_modules(include_agent_server=True) + try: + importlib.import_module("agent.server") + from deepagents.graph import _messages_delta_reducer + + assert _messages_delta_reducer(None, []) == [] + finally: + _restore_agent_modules() + + +def test_late_graph_import_is_repaired_by_patch() -> None: + _clear_deepagents_modules() + try: + from deepagents.graph import _messages_delta_reducer as unpatched + + with pytest.raises(TypeError): + unpatched(None, []) + + importlib.reload(importlib.import_module("agent._patch_messages_reducer")) + from deepagents.graph import _messages_delta_reducer as patched + + assert patched(None, []) == [] + finally: + _restore_agent_modules() diff --git a/tests/test_proxy_auth.py b/tests/test_proxy_auth.py index bf9b9d71..221c4c83 100644 --- a/tests/test_proxy_auth.py +++ b/tests/test_proxy_auth.py @@ -7,7 +7,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest -from deepagents.backends import LangSmithSandbox from agent.integrations.langsmith import _configure_github_proxy @@ -336,8 +335,6 @@ class TestRefreshProxyOnSandboxReuse: inner_sandbox = MagicMock(name="sandbox-stopped") inner_sandbox.name = "sandbox-stopped" inner_sandbox._client.get_sandbox_status.return_value = MagicMock(status="stopped") - sandbox_backend = object.__new__(LangSmithSandbox) - sandbox_backend._sandbox = inner_sandbox with ( patch( @@ -348,7 +345,10 @@ class TestRefreshProxyOnSandboxReuse: patch("agent.server._configure_github_proxy") as mock_proxy, patch.dict("os.environ", {"SANDBOX_TYPE": "langsmith"}), ): - from agent.server import _refresh_github_proxy + from agent.server import LangSmithSandbox, _refresh_github_proxy + + sandbox_backend = object.__new__(LangSmithSandbox) + sandbox_backend._sandbox = inner_sandbox await _refresh_github_proxy(sandbox_backend) @@ -362,8 +362,6 @@ class TestRefreshProxyOnSandboxReuse: inner_sandbox = MagicMock(name="sandbox-ready") inner_sandbox.name = "sandbox-ready" inner_sandbox._client.get_sandbox_status.return_value = MagicMock(status="ready") - sandbox_backend = object.__new__(LangSmithSandbox) - sandbox_backend._sandbox = inner_sandbox with ( patch( @@ -374,7 +372,10 @@ class TestRefreshProxyOnSandboxReuse: patch("agent.server._configure_github_proxy") as mock_proxy, patch.dict("os.environ", {"SANDBOX_TYPE": "langsmith"}), ): - from agent.server import _refresh_github_proxy + from agent.server import LangSmithSandbox, _refresh_github_proxy + + sandbox_backend = object.__new__(LangSmithSandbox) + sandbox_backend._sandbox = inner_sandbox await _refresh_github_proxy(sandbox_backend) diff --git a/ui/bun.lock b/ui/bun.lock index cf3c1f94..9869361a 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -8,6 +8,7 @@ "@base-ui/react": "^1.4.1", "@fontsource-variable/inter": "^5.2.8", "@phosphor-icons/react": "^2.1.10", + "@pierre/diffs": "^1.2.1", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query": "^5.100.10", @@ -19,10 +20,17 @@ "@tanstack/router-plugin": "^1.166.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "diff": "^9.0.0", + "lucide-react": "^1.16.0", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.6.0", + "react-markdown": "^10.1.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", "shadcn": "^4.7.0", + "shiki": "^4.1.0", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.2.1", "tw-animate-css": "^1.4.0", @@ -294,6 +302,10 @@ "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], + "@pierre/diffs": ["@pierre/diffs@1.2.1", "", { "dependencies": { "@pierre/theme": "1.0.3", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-T9pYhh1KeaYLTwL1HMiAkeETj9CPjiKq6pqiUWRVJXvIkIvYutMrYyr3kN07cFWX2XPM8eqe6l2dknezs6K2AA=="], + + "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="], @@ -378,6 +390,24 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], + + "@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], + + "@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], + + "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + + "@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], "@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.5", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA=="], @@ -512,14 +542,24 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@22.19.19", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -530,6 +570,8 @@ "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="], @@ -552,6 +594,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], @@ -636,6 +680,8 @@ "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.29", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ=="], @@ -668,10 +714,20 @@ "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -698,6 +754,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "comment-parser": ["comment-parser@1.4.6", "", {}, "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg=="], @@ -748,6 +806,8 @@ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], @@ -768,7 +828,9 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -848,6 +910,8 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -868,6 +932,8 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -958,6 +1024,12 @@ "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "headers-polyfill": ["headers-polyfill@5.0.1", "", { "dependencies": { "@types/set-cookie-parser": "^2.4.10", "set-cookie-parser": "^3.0.1" } }, "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA=="], "hono": ["hono@4.12.18", "", {}, "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ=="], @@ -966,6 +1038,10 @@ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], @@ -988,14 +1064,22 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -1004,6 +1088,8 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], @@ -1098,16 +1184,56 @@ "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], + "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + + "lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -1118,6 +1244,62 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -1180,6 +1362,10 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1194,6 +1380,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], @@ -1242,6 +1430,8 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -1258,14 +1448,34 @@ "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + "react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -1322,6 +1532,8 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], @@ -1342,6 +1554,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], "stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="], @@ -1358,6 +1572,8 @@ "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -1368,6 +1584,10 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], @@ -1404,6 +1624,10 @@ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "ts-declaration-location": ["ts-declaration-location@1.0.7", "", { "dependencies": { "picomatch": "^4.0.2" }, "peerDependencies": { "typescript": ">=4.0.0" } }, "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA=="], @@ -1438,6 +1662,18 @@ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -1462,6 +1698,10 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -1528,6 +1768,8 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -1540,6 +1782,14 @@ "@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + + "@pierre/diffs/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + + "@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1552,6 +1802,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@tanstack/router-utils/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="], @@ -1604,10 +1856,14 @@ "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -1626,6 +1882,8 @@ "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "shadcn/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], @@ -1662,6 +1920,18 @@ "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "cheerio/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], diff --git a/ui/package.json b/ui/package.json index ab3910ba..b3296427 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,7 @@ "@base-ui/react": "^1.4.1", "@fontsource-variable/inter": "^5.2.8", "@phosphor-icons/react": "^2.1.10", + "@pierre/diffs": "^1.2.1", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query": "^5.100.10", @@ -26,11 +27,17 @@ "@tanstack/router-plugin": "^1.166.13", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "diff": "^9.0.0", + "lucide-react": "^1.16.0", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", "react-icons": "^5.6.0", + "react-markdown": "^10.1.0", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", "shadcn": "^4.7.0", + "shiki": "^4.1.0", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.2.1", "tw-animate-css": "^1.4.0", diff --git a/ui/src/components/AppSidebar.tsx b/ui/src/components/AppSidebar.tsx index 6586de5c..607b605d 100644 --- a/ui/src/components/AppSidebar.tsx +++ b/ui/src/components/AppSidebar.tsx @@ -1,19 +1,21 @@ -import { Link, useNavigate } from "@tanstack/react-router"; -import { useQueryClient } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; import { + IoArrowBackOutline, IoCloudOutline, IoExtensionPuzzleOutline, IoGitPullRequestOutline, - IoLogOutOutline, IoOptionsOutline, IoSettingsOutline, } from "react-icons/io5"; -import { useEffect, useRef, useState } from "react"; -import type {ComponentType, SVGProps} from "react"; +import type { ComponentType, SVGProps } from "react"; import type { SessionUser } from "@/lib/api"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { api } from "@/lib/api"; +import { SidebarUserMenu } from "@/components/SidebarUserMenu"; +import { + SidebarCollapseButton, + SidebarFrame, + useSidebarLayout, +} from "@/components/sidebar-layout"; import { cn } from "@/lib/utils"; type IconType = ComponentType>; @@ -34,41 +36,10 @@ const NAV: Array = [ ]; export function AppSidebar({ user }: { user: SessionUser }) { - const qc = useQueryClient(); - const navigate = useNavigate(); - const [menuOpen, setMenuOpen] = useState(false); - const menuRef = useRef(null); - - useEffect(() => { - if (!menuOpen) return; - const onClickOutside = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setMenuOpen(false); - } - }; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") setMenuOpen(false); - }; - document.addEventListener("mousedown", onClickOutside); - document.addEventListener("keydown", onKey); - return () => { - document.removeEventListener("mousedown", onClickOutside); - document.removeEventListener("keydown", onKey); - }; - }, [menuOpen]); - - const onLogout = async () => { - setMenuOpen(false); - await api.logout(); - qc.setQueryData(["session"], null); - navigate({ to: "/login" }); - }; - - const initials = (user.login || "?").slice(0, 2).toUpperCase(); - + const layout = useSidebarLayout(); return ( - + ); } diff --git a/ui/src/components/SidebarUserMenu.tsx b/ui/src/components/SidebarUserMenu.tsx new file mode 100644 index 00000000..c04f7645 --- /dev/null +++ b/ui/src/components/SidebarUserMenu.tsx @@ -0,0 +1,94 @@ +import { Link, useNavigate } from "@tanstack/react-router"; +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useRef, useState } from "react"; +import { IoLogOutOutline, IoSettingsOutline } from "react-icons/io5"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { api, type SessionUser } from "@/lib/api"; + +interface SidebarUserMenuProps { + user: SessionUser; + showSettingsLink?: boolean; +} + +export function SidebarUserMenu({ user, showSettingsLink = false }: SidebarUserMenuProps) { + const qc = useQueryClient(); + const navigate = useNavigate(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const onClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(false); + }; + document.addEventListener("mousedown", onClickOutside); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onClickOutside); + document.removeEventListener("keydown", onKey); + }; + }, [open]); + + const onLogout = async () => { + setOpen(false); + await api.logout(); + qc.setQueryData(["session"], null); + navigate({ to: "/login" }); + }; + + const initials = (user.login || "?").slice(0, 2).toUpperCase(); + + return ( +
+ + {open && ( +
+ {showSettingsLink && ( + setOpen(false)} + className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs/relaxed hover:bg-muted" + > + + Dashboard settings + + )} + +
+ )} +
+ ); +} diff --git a/ui/src/components/agents/AgentGitPanel.tsx b/ui/src/components/agents/AgentGitPanel.tsx new file mode 100644 index 00000000..2b097479 --- /dev/null +++ b/ui/src/components/agents/AgentGitPanel.tsx @@ -0,0 +1,157 @@ +import { useMemo, useState } from "react"; +import { MultiFileDiff } from "@pierre/diffs/react"; +import { CaretDownIcon, GitPullRequestIcon } from "@phosphor-icons/react"; + +import { diffOptions } from "@/components/agents/utils/diffUtils"; +import type { AgentThread } from "@/lib/agents/types"; +import { cn } from "@/lib/utils"; + +interface AgentGitPanelProps { + thread: AgentThread; +} + +export function AgentGitPanel({ thread }: AgentGitPanelProps) { + const [tab, setTab] = useState<"diff" | "review" | "commits">("diff"); + const pr = thread.pr; + + const fileContents = useMemo(() => { + const defaults: Record = { + "app.js": { + original: "export function init() {\n mount();\n}\n", + modified: "export function init() {\n // test comment\n mount();\n}\n", + }, + "contextPanel.js": { + original: "export function renderPanel() {\n return panel;\n}\n", + modified: "export function renderPanel() {\n // test comment\n return panel;\n}\n", + }, + "settings.js": { + original: "export const defaults = {};\n", + modified: "export const defaults = {};\n// test comment\n", + }, + }; + + return (thread.changedFiles ?? []).map((file) => ({ + path: file.path, + additions: file.additions, + original: defaults[file.path]?.original ?? "", + modified: defaults[file.path]?.modified ?? file.patch ?? "", + })); + }, [thread.changedFiles]); + + return ( + + ); +} + +function FileDiffSection({ + file, +}: { + file: { path: string; additions: number; original: string; modified: string }; +}) { + const [open, setOpen] = useState(true); + + return ( +
+ + {open && ( +
+ +
+ )} +
+ ); +} diff --git a/ui/src/components/agents/AgentPromptBar.tsx b/ui/src/components/agents/AgentPromptBar.tsx new file mode 100644 index 00000000..cb3c98f6 --- /dev/null +++ b/ui/src/components/agents/AgentPromptBar.tsx @@ -0,0 +1,2 @@ +export { CloudPromptBar as AgentPromptBar } from "@/components/agents/ported/CloudPromptBar"; +export type { CloudPromptBarProps as AgentPromptBarProps } from "@/components/agents/ported/CloudPromptBar"; diff --git a/ui/src/components/agents/AgentRunCard.tsx b/ui/src/components/agents/AgentRunCard.tsx new file mode 100644 index 00000000..31de47d9 --- /dev/null +++ b/ui/src/components/agents/AgentRunCard.tsx @@ -0,0 +1,73 @@ +import { Link } from "@tanstack/react-router"; +import { CheckCircleIcon, GitBranchIcon, GitPullRequestIcon } from "@phosphor-icons/react"; + +import { formatRelativeTime } from "@/lib/agents/api"; +import type { AgentThread } from "@/lib/agents/types"; +import { cn } from "@/lib/utils"; + +interface AgentRunCardProps { + thread: AgentThread; +} + +export function AgentRunCard({ thread }: AgentRunCardProps) { + const stats = thread.diffStats; + const hasPr = Boolean(thread.pr); + + return ( + +
+ {stats ? ( + <> +
+ {stats.files} {stats.files === 1 ? "file" : "files"} +
+
+ +{stats.additions} + -{stats.deletions} +
+ + ) : ( + + )} +
+ {hasPr ? ( + <> + + Draft + + ) : ( + <> + + Branch + + )} +
+
+ +
+
{thread.title}
+
+ {thread.model} + · + {thread.repo} + · + {formatRelativeTime(thread.updatedAt)} +
+
+ + + + ); +} diff --git a/ui/src/components/agents/AgentThreadView.tsx b/ui/src/components/agents/AgentThreadView.tsx new file mode 100644 index 00000000..acef4cbb --- /dev/null +++ b/ui/src/components/agents/AgentThreadView.tsx @@ -0,0 +1,202 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { ChevronDown } from "lucide-react"; + +import { AgentPromptBar } from "@/components/agents/AgentPromptBar"; +import { AgentsShell } from "@/components/agents/AgentsSidebar"; +import { + MessageView, + summarizeChangedFiles, + type MessageViewScrollControl, +} from "@/components/agents/ported"; +import type { SessionUser } from "@/lib/api"; +import type { AgentThread, Message } from "@/lib/agents/types"; +import { useSendAgentMessage } from "@/lib/agents/queries"; +import { + dropPendingPrompts, + getPendingPrompts, + type PendingPrompt, +} from "@/lib/agents/pendingPrompts"; +import { useAgentThreadStream } from "@/lib/agents/useThreadStream"; +import { useModelOptions, type ModelSelection } from "@/lib/agents/useModelOptions"; + +interface AgentThreadViewProps { + user: SessionUser; + thread: AgentThread; +} + +const PROMPT_OVERLAY_INSET = 128; + +export function AgentThreadView({ user, thread }: AgentThreadViewProps) { + const sendMessage = useSendAgentMessage(thread.id); + useAgentThreadStream(thread.id, thread.status === "running"); + const scrollControlRef = useRef(null); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); + const [pendingPrompts, setPendingPrompts] = useState(() => + getPendingPrompts(thread.id), + ); + + const { models, defaultSelection } = useModelOptions(); + const threadSelection = useMemo(() => { + if (!thread.model || !thread.effort) return null; + const supported = models.some( + (m) => m.id === thread.model && m.efforts.includes(thread.effort ?? ""), + ); + if (!supported) return null; + return { modelId: thread.model, effort: thread.effort }; + }, [models, thread.model, thread.effort]); + const [selection, setSelection] = useState(null); + + useEffect(() => { + if (selection !== null) return; + if (threadSelection) setSelection(threadSelection); + else if (defaultSelection) setSelection(defaultSelection); + }, [defaultSelection, selection, threadSelection]); + + const userMessageTexts = useMemo(() => { + return new Set( + thread.messages + .filter((m) => m.author === "user") + .map((m) => + m.chunks + .filter((c) => c.kind === "text") + .map((c) => (c as { kind: "text"; text: string }).text) + .join(""), + ), + ); + }, [thread.messages]); + + useEffect(() => { + setPendingPrompts((prev) => { + if (prev.length === 0) return prev; + const next = dropPendingPrompts(thread.id, (entry) => + userMessageTexts.has(entry.prompt), + ); + return next.length === prev.length ? prev : next; + }); + }, [thread.id, userMessageTexts]); + + const displayMessages = useMemo(() => { + if (pendingPrompts.length === 0) return thread.messages; + const baseTimestamp = new Date().toISOString(); + const result = thread.messages.slice(); + pendingPrompts.forEach((entry, i) => { + const synth: Message = { + id: `pending-user-${i}`, + author: "user", + timestamp: baseTimestamp, + chunks: [{ kind: "text", text: entry.prompt }], + }; + const at = Math.min(Math.max(entry.insertAt, 0), result.length); + result.splice(at, 0, synth); + }); + return result; + }, [thread.messages, pendingPrompts]); + + const changedFiles = useMemo(() => { + const agentMessages = thread.messages.filter((m) => m.author === "agent"); + const allChunks = agentMessages.flatMap((m) => m.chunks); + return summarizeChangedFiles(allChunks); + }, [thread.messages]); + + const hasMessages = displayMessages.length > 0; + const isStreaming = thread.status === "running" || pendingPrompts.length > 0; + + return ( + +
+
+ {hasMessages ? ( +
+
+ + + {changedFiles.length > 0 && ( +
+
+
+ {changedFiles.length} Files Changed +
+
+ {changedFiles.map((file) => ( +
+ {file.filePath} + +{file.additions} +
+ ))} +
+
+
+ )} +
+ +
+
+
+ {showScrollToBottom && ( + + )} + + sendMessage.mutate({ + content, + model_id: selection?.modelId ?? null, + effort: selection?.effort ?? null, + }) + } + models={models} + selection={selection ?? threadSelection ?? defaultSelection} + onSelectionChange={setSelection} + /> +
+
+
+ ) : ( +
+

This thread has no messages yet.

+
+ + sendMessage.mutate({ + content, + model_id: selection?.modelId ?? null, + effort: selection?.effort ?? null, + }) + } + models={models} + selection={selection ?? threadSelection ?? defaultSelection} + onSelectionChange={setSelection} + /> +
+
+ )} +
+
+ + ); +} diff --git a/ui/src/components/agents/AgentsHome.tsx b/ui/src/components/agents/AgentsHome.tsx new file mode 100644 index 00000000..f0d60651 --- /dev/null +++ b/ui/src/components/agents/AgentsHome.tsx @@ -0,0 +1,67 @@ +import { Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; + +import { AgentPromptBar } from "@/components/agents/AgentPromptBar"; +import { AgentRunCard } from "@/components/agents/AgentRunCard"; +import { Logo } from "@/components/agents/ported/Logo"; +import { useAgentThreads, useCreateAgentThread } from "@/lib/agents/queries"; +import { useModelOptions, type ModelSelection } from "@/lib/agents/useModelOptions"; + +export function AgentsHome() { + const threadsQuery = useAgentThreads(); + const createThread = useCreateAgentThread(); + const recentRuns = (threadsQuery.data ?? []).slice(0, 5); + const { models, defaultSelection } = useModelOptions(); + const [selection, setSelection] = useState(null); + + useEffect(() => { + if (selection === null && defaultSelection) setSelection(defaultSelection); + }, [defaultSelection, selection]); + + return ( +
+
+
+ + + createThread.mutate({ + prompt, + model_id: selection?.modelId ?? null, + effort: selection?.effort ?? null, + }) + } + disabled={createThread.isPending} + models={models} + selection={selection ?? defaultSelection} + onSelectionChange={setSelection} + /> +
+ +
+ {threadsQuery.isLoading ? ( +

Loading agents…

+ ) : recentRuns.length === 0 ? ( + + ) : ( + recentRuns.map((thread) => ) + )} +
+
+
+ ); +} + +export function AgentsHomeEmptyState() { + return ( +
+

No agents yet.

+ + Start your first agent + +
+ ); +} diff --git a/ui/src/components/agents/AgentsSidebar.tsx b/ui/src/components/agents/AgentsSidebar.tsx new file mode 100644 index 00000000..e7bc5640 --- /dev/null +++ b/ui/src/components/agents/AgentsSidebar.tsx @@ -0,0 +1,176 @@ +import { Link } from "@tanstack/react-router"; +import { ChartLineUpIcon, PlusIcon, XIcon } from "@phosphor-icons/react"; + +import type { SessionUser } from "@/lib/api"; +import { SidebarUserMenu } from "@/components/SidebarUserMenu"; +import { + SidebarCollapseButton, + SidebarFrame, + useSidebarLayout, +} from "@/components/sidebar-layout"; +import { groupThreads } from "@/lib/agents/api"; +import { useAgentThreads, useDeleteAgentThread } from "@/lib/agents/queries"; +import type { AgentThread } from "@/lib/agents/types"; +import { cn } from "@/lib/utils"; + +interface AgentsSidebarProps { + user: SessionUser; + activeThreadId?: string; +} + +const NAV = [{ to: "/my-settings", label: "Dashboard", icon: ChartLineUpIcon }] as const; + +export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) { + const threadsQuery = useAgentThreads(); + const threads = threadsQuery.data ?? []; + const groups = groupThreads(threads); + const layout = useSidebarLayout(); + + return ( + +
+ + + open-swe + + +
+ +
+ + + New Agent + +
+ + + +
+ + + +
+ +
+ +
+
+ ); +} + +function ThreadGroup({ + label, + threads, + activeThreadId, +}: { + label: string; + threads: AgentThread[]; + activeThreadId?: string; +}) { + if (threads.length === 0) return null; + + return ( +
+
+ {label} +
+ {threads.map((thread) => ( + + ))} +
+ ); +} + +function ThreadRow({ thread, isActive }: { thread: AgentThread; isActive: boolean }) { + const deleteThread = useDeleteAgentThread(); + const badge = + thread.diffStats && thread.diffStats.additions > 0 + ? `+${thread.diffStats.additions}` + : null; + const isDeleting = deleteThread.isPending && deleteThread.variables === thread.id; + + const onDelete = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (isDeleting) return; + if (!window.confirm(`Delete "${thread.title}"? This cannot be undone.`)) return; + deleteThread.mutate(thread.id); + }; + + return ( + + + {thread.title} + {badge && ( + + {badge} + + )} + + + ); +} + +export function AgentsShell({ + user, + activeThreadId, + children, +}: { + user: SessionUser; + activeThreadId?: string; + children: React.ReactNode; +}) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/ui/src/components/agents/ported/BranchSelector.tsx b/ui/src/components/agents/ported/BranchSelector.tsx new file mode 100644 index 00000000..54cb2570 --- /dev/null +++ b/ui/src/components/agents/ported/BranchSelector.tsx @@ -0,0 +1,188 @@ +import { useState, useRef, useCallback, useMemo } from 'react'; + +interface BranchSelectorProps { + projectPath: string; + currentBranch: string; +} + +export function BranchSelector({ projectPath, currentBranch }: BranchSelectorProps) { + const [isOpen, setIsOpen] = useState(false); + const [branches, setBranches] = useState([]); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [newBranchName, setNewBranchName] = useState(''); + const [showNewBranchInput, setShowNewBranchInput] = useState(false); + const [error, setError] = useState(null); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + const searchInputRef = useRef(null); + + const filteredBranches = useMemo(() => { + if (!searchQuery.trim()) return branches; + const query = searchQuery.toLowerCase(); + return branches.filter((branch) => branch.toLowerCase().includes(query)); + }, [branches, searchQuery]); + + const loadBranches = useCallback(async () => { + setLoading(true); + setError(null); + try { + const result = await window.git.listBranches(projectPath); + setBranches(result.branches); + } catch { + setError('Failed to load branches'); + } finally { + setLoading(false); + } + }, [projectPath]); + + const handleOpen = useCallback(() => { + setIsOpen(true); + setShowNewBranchInput(false); + setNewBranchName(''); + setSearchQuery(''); + setError(null); + loadBranches(); + setTimeout(() => searchInputRef.current?.focus(), 0); + }, [loadBranches]); + + const handleClose = useCallback(() => { + setIsOpen(false); + setShowNewBranchInput(false); + setNewBranchName(''); + setSearchQuery(''); + setError(null); + }, []); + + const handleSwitchBranch = useCallback(async (branchName: string) => { + if (branchName === currentBranch) { + handleClose(); + return; + } + setLoading(true); + setError(null); + const result = await window.git.switchBranch(projectPath, branchName); + setLoading(false); + if (result.success) { + handleClose(); + } else { + setError(result.error || 'Failed to switch branch'); + } + }, [projectPath, currentBranch, handleClose]); + + const handleCreateBranch = useCallback(async () => { + const name = newBranchName.trim(); + if (!name) return; + setLoading(true); + setError(null); + const result = await window.git.createBranch(projectPath, name); + setLoading(false); + if (result.success) { + handleClose(); + } else { + setError(result.error || 'Failed to create branch'); + } + }, [projectPath, newBranchName, handleClose]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + handleClose(); + } else if (e.key === 'Enter' && showNewBranchInput) { + handleCreateBranch(); + } + }, [handleClose, showNewBranchInput, handleCreateBranch]); + + return ( +
+ + + {isOpen && ( + <> +
+
+
+ setSearchQuery(e.target.value)} + placeholder="Search branches..." + className="w-full px-2 py-1.5 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:outline-none focus:border-[#5a9bc7]" + /> +
+ + {error && ( +
+ {error} +
+ )} + +
+ {loading && branches.length === 0 ? ( +
Loading...
+ ) : filteredBranches.length === 0 ? ( +
No branches found
+ ) : ( + filteredBranches.map((branch) => ( + + )) + )} +
+ +
+ {showNewBranchInput ? ( +
+ setNewBranchName(e.target.value)} + placeholder="Branch name" + autoFocus + className="flex-1 px-2 py-1 text-xs bg-gray-800 border border-gray-600 rounded text-gray-200 placeholder-gray-500 focus:outline-none focus:border-[#5a9bc7]" + /> + +
+ ) : ( + + )} +
+
+ + )} +
+ ); +} diff --git a/ui/src/components/agents/ported/BubblePrefix.tsx b/ui/src/components/agents/ported/BubblePrefix.tsx new file mode 100644 index 00000000..08b6a4ef --- /dev/null +++ b/ui/src/components/agents/ported/BubblePrefix.tsx @@ -0,0 +1,18 @@ +import type { Author } from '@/lib/agents/types'; + +interface BubblePrefixProps { + author: Author; +} + +export function BubblePrefix({ author }: BubblePrefixProps) { + switch (author) { + case 'user': + return >; + case 'agent': + return ✦; + case 'tool': + return ⚡; + case 'system': + return ℹ; + } +} diff --git a/ui/src/components/agents/ported/ChatView.tsx b/ui/src/components/agents/ported/ChatView.tsx new file mode 100644 index 00000000..01849dd6 --- /dev/null +++ b/ui/src/components/agents/ported/ChatView.tsx @@ -0,0 +1,591 @@ +import { useCallback, useRef, useEffect, useState, forwardRef } from "react"; +import { useStore } from "../../store"; +import { useShallow } from 'zustand/react/shallow'; +import { MessageView, summarizeChangedFiles } from "./MessageView"; +import { PromptBar } from "./PromptBar"; +import { TodoList } from "./TodoList"; +import { Logo } from "./Logo"; +import { ThreadPicker } from "./ThreadPicker"; +import { CompactingIndicator } from "./CompactingIndicator"; +import { executeCommand } from "../../commands"; +import type { Message, ChatMessage, ChatMessageContentBlock, ImageChunk, Thread, ToolExecutionChunk, ScheduledTask as ScheduledTaskType } from "@/lib/agents/types"; + +const PROMPT_CONTENT_WIDTH = "max-w-[44rem]"; +const STACKED_STATUS_WIDTH = PROMPT_CONTENT_WIDTH; +const MESSAGE_CONTENT_WIDTH = "max-w-[42rem]"; + +function messagesToChatMessages(messages: Message[]): ChatMessage[] { + const chatMessages: ChatMessage[] = []; + for (const msg of messages) { + if (msg.author === "user" || msg.author === "agent") { + const role = msg.author === "user" ? "user" as const : "assistant" as const; + const hasImages = msg.chunks.some((c) => c.kind === "image"); + + if (hasImages) { + const blocks: ChatMessageContentBlock[] = []; + for (const c of msg.chunks) { + if (c.kind === "image") { + blocks.push({ type: "image_url", image_url: { url: `data:${c.mimeType};base64,${c.base64}` } }); + } else if (c.kind === "text") { + blocks.push({ type: "text", text: c.text }); + } else if (c.kind === "code") { + blocks.push({ type: "text", text: `\n\`\`\`${c.language || ""}\n${c.text}\n\`\`\`\n` }); + } + } + if (blocks.length > 0) chatMessages.push({ role, content: blocks }); + } else { + const textContent = msg.chunks + .map((c) => { + if (c.kind === "text") return c.text; + if (c.kind === "code") return `\n\`\`\`${c.language || ""}\n${c.text}\n\`\`\`\n`; + return ""; + }) + .join(""); + if (textContent) chatMessages.push({ role, content: textContent }); + } + } + } + return chatMessages; +} + +function findLatestPendingApproval(messages: Message[]): ToolExecutionChunk | null { + for (let i = messages.length - 1; i >= 0; i -= 1) { + for (let j = messages[i].chunks.length - 1; j >= 0; j -= 1) { + const chunk = messages[i].chunks[j]; + if (chunk.kind === "tool-execution" && chunk.status === "pending" && chunk.approvalRequestId) { + return chunk; + } + } + } + return null; +} + +interface ChatViewProps { + tabId: string; +} + +export function ChatView({ tabId }: ChatViewProps) { + const containerRef = useRef(null); + const dragDepthRef = useRef(0); + const [pendingImages, setPendingImages] = useState([]); + const [queuedSubmissions, setQueuedSubmissions] = useState>([]); + const [isDragOver, setIsDragOver] = useState(false); + const [showThreadPicker, setShowThreadPicker] = useState(false); + const [showLoopManager, setShowLoopManager] = useState(false); + const [loopTasks, setLoopTasks] = useState([]); + const loopManagerRef = useRef(null); + + const tab = useStore(state => state.tabs[tabId] ?? null); + const session = useStore(state => { + const t = state.tabs[tabId]; + return t ? state.sessions[t.sessionId] ?? null : null; + }); + const hasScheduledPrompt = useStore(state => { + const t = state.tabs[tabId]; + if (!t) return false; + return state.scheduledPrompts.some(p => p.sessionId === t.sessionId); + }); + const loopTaskCount = useStore(state => { + const t = state.tabs[tabId]; + if (!t) return 0; + return state.sessionLoopCounts[t.sessionId] || 0; + }); + const actions = useStore(useShallow(state => ({ + setTabProject: state.setTabProject, + createSession: state.createSession, + addMessageToSession: state.addMessageToSession, + startStreaming: state.startStreaming, + setAutoApproveSession: state.setAutoApproveSession, + resumeThread: state.resumeThread, + consumeScheduledPrompt: state.consumeScheduledPrompt, + setRightPanelTab: state.setRightPanelTab, + }))); + + useEffect(() => { + if (session) window.agent.setMode(session.id, session.mode); + }, [session?.id, session?.mode]); + + useEffect(() => { + setQueuedSubmissions([]); + }, [session?.id]); + + useEffect(() => { + const project = tab?.project; + if (!project?.path) return; + if (project.worktreeType === 'worktree') return; + + const sync = () => { window.git.syncLocalBranch(project.path); }; + sync(); + const interval = setInterval(sync, 3000); + return () => clearInterval(interval); + }, [tab?.project?.path, tab?.project?.worktreeType]); + + const handleApprove = useCallback((id: string) => { + window.agent.respondToApproval({ requestId: id, decision: "approve" }); + }, []); + + const handleReject = useCallback((id: string) => { + window.agent.respondToApproval({ requestId: id, decision: "reject" }); + }, []); + + const handleAutoApprove = useCallback((id: string) => { + if (session) actions.setAutoApproveSession(session.id, true); + window.agent.respondToApproval({ requestId: id, decision: "auto-approve" }); + }, [session, actions]); + + const hasFileDragData = (e: React.DragEvent) => e.dataTransfer.types.includes("Files"); + + const handleDragEnter = useCallback((e: React.DragEvent) => { + if (!hasFileDragData(e)) return; + e.preventDefault(); e.stopPropagation(); + dragDepthRef.current += 1; + setIsDragOver(true); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + if (!hasFileDragData(e)) return; + e.preventDefault(); e.stopPropagation(); + if (!isDragOver) setIsDragOver(true); + }, [isDragOver]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + if (!hasFileDragData(e)) return; + e.preventDefault(); e.stopPropagation(); + dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); + if (dragDepthRef.current === 0) setIsDragOver(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + if (!hasFileDragData(e)) return; + e.preventDefault(); e.stopPropagation(); + dragDepthRef.current = 0; + setIsDragOver(false); + + const files = Array.from(e.dataTransfer.files).filter( + (f) => f.type === "image/png" || f.type === "image/jpeg" + ); + for (const file of files) { + const reader = new FileReader(); + reader.onload = () => { + const base64 = (reader.result as string).split(",")[1]; + setPendingImages((prev) => [...prev, { kind: "image", base64, mimeType: file.type, fileName: file.name }]); + }; + reader.readAsDataURL(file); + } + }, []); + + const handleRemoveImage = useCallback((index: number) => { + setPendingImages((prev) => prev.filter((_, i) => i !== index)); + }, []); + + const handleThreadSelect = useCallback((thread: Thread) => { + setShowThreadPicker(false); + actions.resumeThread(tabId, thread); + }, [actions, tabId]); + + const handleOpenLoopManager = useCallback(async () => { + if (!session?.id) return; + const tasks = await window.cron.list(session.id); + setLoopTasks(tasks); + setShowLoopManager(true); + }, [session?.id]); + + const handleDeleteLoop = useCallback(async (taskId: string) => { + await window.cron.delete(taskId); + setLoopTasks(prev => prev.filter(t => t.id !== taskId)); + }, []); + + const handleDeleteAllLoops = useCallback(async () => { + for (const task of loopTasks) await window.cron.delete(task.id); + setLoopTasks([]); + setShowLoopManager(false); + }, [loopTasks]); + + const handleClickOutsideLoopManager = useCallback((e: MouseEvent) => { + if (loopManagerRef.current && !loopManagerRef.current.contains(e.target as Node)) { + setShowLoopManager(false); + } + }, []); + + useEffect(() => { + if (showLoopManager) { + document.addEventListener('mousedown', handleClickOutsideLoopManager); + return () => document.removeEventListener('mousedown', handleClickOutsideLoopManager); + } + }, [showLoopManager, handleClickOutsideLoopManager]); + + useEffect(() => { + if (loopTaskCount === 0) setShowLoopManager(false); + }, [loopTaskCount]); + + const handleOpenDiff = useCallback( + (_diffData: { filePath: string; originalContent: string; modifiedContent: string }) => { + // TODO: pass diffData to the right panel so it can show the specific file diff + actions.setRightPanelTab('source-control'); + }, + [actions], + ); + + const handleContainerClick = useCallback(() => { + const textarea = containerRef.current?.querySelector("textarea"); + if (textarea) textarea.focus(); + }, []); + + const dragProps = { + onDragEnter: handleDragEnter, + onDragOver: handleDragOver, + onDragLeave: handleDragLeave, + onDrop: handleDrop, + }; + + const startAgentRun = useCallback((sessionId: string, query: string, images: ImageChunk[], displayText?: string) => { + const freshSession = useStore.getState().sessions[sessionId]; + if (!freshSession) return; + + const displayQuery = displayText ?? query; + const chunks = [...images, { kind: "text" as const, text: displayQuery }]; + const chatHistory = messagesToChatMessages(freshSession.messages); + + if (images.length > 0) { + const blocks: ChatMessageContentBlock[] = [ + ...images.map((img) => ({ type: "image_url" as const, image_url: { url: `data:${img.mimeType};base64,${img.base64}` } })), + { type: "text" as const, text: query }, + ]; + chatHistory.push({ role: "user", content: blocks }); + } else { + chatHistory.push({ role: "user", content: query }); + } + + actions.addMessageToSession(sessionId, "user", chunks); + actions.startStreaming(sessionId); + window.agent.stream(sessionId, tabId, chatHistory, freshSession.modelConfig, freshSession.mode); + }, [actions, tabId]); + + useEffect(() => { + if (!session) return; + if (session.isCompacting || session.isStreaming || session.busy) return; + if (queuedSubmissions.length === 0) return; + + const next = queuedSubmissions[0]; + setQueuedSubmissions((prev) => prev.slice(1)); + startAgentRun(session.id, next.query, next.images); + }, [session, queuedSubmissions, startAgentRun]); + + useEffect(() => { + if (!session) return; + if (session.isCompacting || session.isStreaming || session.busy) return; + if (!hasScheduledPrompt) return; + + const scheduledPrompt = actions.consumeScheduledPrompt(session.id); + if (!scheduledPrompt) return; + startAgentRun(session.id, scheduledPrompt.prompt, []); + }, [session, hasScheduledPrompt, actions, startAgentRun]); + + const handleSubmit = useCallback( + async (query: string) => { + if (!tab) return; + + if (query.startsWith("/")) { + const commandExecuted = executeCommand(query, { + sessionId: session?.id || null, + createSession: () => actions.createSession(tabId), + addSystemMessage: (sessionId, chunks) => actions.addMessageToSession(sessionId, 'system', chunks), + }); + if (commandExecuted) return; + } + + if (!session) return; + if (session.isCompacting) return; + + const images = pendingImages; + setPendingImages([]); + + if (session.isStreaming || session.busy) { + setQueuedSubmissions((prev) => [...prev, { query, images }]); + window.agent.cancel(session.id); + return; + } + + startAgentRun(session.id, query, images); + }, + [tab, session, tabId, pendingImages, startAgentRun, actions], + ); + + if (!tab || !tab.project) return null; + + const hasMessages = session && session.messages.length > 0; + const streamingMessageId = session?.streamingMessageId ?? null; + const streamingMessage = streamingMessageId + ? session?.messages.find((m) => m.id === streamingMessageId) ?? null + : null; + const streamingChangedFiles = streamingMessage ? summarizeChangedFiles(streamingMessage.chunks) : []; + let streamingAdditions = 0; + let streamingDeletions = 0; + for (const file of streamingChangedFiles) { + streamingAdditions += file.additions; + streamingDeletions += file.deletions; + } + const todos = session?.todos ?? []; + const hasTodos = todos.length > 0; + const hasStreamingChangedFiles = !!session?.isStreaming && streamingChangedFiles.length > 0; + const hasActiveLoops = loopTaskCount > 0; + const hasConnectedStack = hasTodos || hasStreamingChangedFiles || hasActiveLoops; + const runActive = !!session?.isStreaming || !!session?.busy; + const pendingApproval = session ? findLatestPendingApproval(session.messages) : null; + + if (!hasMessages) { + return ( +
+
+
+ +
+ {hasActiveLoops && ( + setShowLoopManager(false)} + className="-mb-2" + /> + )} + +
+
+
+ {showThreadPicker && tab.project && ( + setShowThreadPicker(false)} + /> + )} +
+ ); + } + + return ( +
+ + {session!.isCompacting ? ( +
+ +
+ ) : ( + <> + {hasConnectedStack && ( +
+
+ {hasActiveLoops && ( + setShowLoopManager(false)} + /> + )} + {hasTodos && ( + + )} + {hasStreamingChangedFiles && ( +
+ + {streamingChangedFiles.length} file{streamingChangedFiles.length === 1 ? "" : "s"} changed + +{streamingAdditions} + -{streamingDeletions} + + +
+ )} +
+
+ )} +
+
+ +
+
+ + )} + {showThreadPicker && tab.project && ( + setShowThreadPicker(false)} + /> + )} +
+ ); +} + +interface LoopManagerBlockProps { + loopTaskCount: number; + runActive: boolean; + showLoopManager: boolean; + loopTasks: ScheduledTaskType[]; + onOpenLoopManager: () => void; + onDeleteLoop: (taskId: string) => void; + onDeleteAllLoops: () => void; + onCloseLoopManager: () => void; + className?: string; +} + +const LoopManagerBlock = forwardRef(function LoopManagerBlock( + { loopTaskCount, runActive, showLoopManager, loopTasks, onOpenLoopManager, onDeleteLoop, onDeleteAllLoops, onCloseLoopManager, className }, + ref, +) { + return ( +
+ + {showLoopManager && ( +
+
+
+ Scheduled Loops +
+ {loopTasks.length > 0 && ( + + )} + +
+
+
+ {loopTasks.length === 0 ? ( +
No active loops
+ ) : ( + loopTasks.map(task => ( +
+
+
{task.prompt}
+
+ {task.isRecurring ? `Every ${Math.round(task.intervalMs / 60000)}m` : "One-time"}{" · "}{task.id} +
+
+ +
+ )) + )} +
+
+
+ )} +
+ ); +}); diff --git a/ui/src/components/agents/ported/CloudPromptBar.tsx b/ui/src/components/agents/ported/CloudPromptBar.tsx new file mode 100644 index 00000000..598f4bd4 --- /dev/null +++ b/ui/src/components/agents/ported/CloudPromptBar.tsx @@ -0,0 +1,156 @@ +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; + +import type { ModelOption } from "@/lib/api"; +import { + formatModelSelection, + type ModelSelection, +} from "@/lib/agents/useModelOptions"; +import { cn } from "@/lib/utils"; + +const PROMPT_TEXTAREA_MAX_HEIGHT = 200; + +export interface CloudPromptBarProps { + placeholder?: string; + compact?: boolean; + disabled?: boolean; + busy?: boolean; + onSubmit?: (value: string) => void; + models?: ModelOption[]; + selection?: ModelSelection | null; + onSelectionChange?: (next: ModelSelection) => void; +} + +/** Web-adapted PromptBar from open-swe-app — local state, no Electron/Zustand deps. */ +export const CloudPromptBar = memo(function CloudPromptBar({ + placeholder = "Ask Open SWE to build, fix bugs, explore", + compact = false, + disabled = false, + busy = false, + onSubmit, + models = [], + selection = null, + onSelectionChange, +}: CloudPromptBarProps) { + const [value, setValue] = useState(""); + const [modelDropdownOpen, setModelDropdownOpen] = useState(false); + const inputRef = useRef(null); + const modelDropdownRef = useRef(null); + + const combos = useMemo(() => { + const list: ModelSelection[] = []; + for (const model of models) { + for (const effort of model.efforts) { + list.push({ modelId: model.id, effort }); + } + } + return list; + }, [models]); + + const selectionLabel = formatModelSelection(models, selection); + + const handleSubmit = useCallback(() => { + const trimmed = value.trim(); + if (!trimmed || disabled) return; + onSubmit?.(trimmed); + setValue(""); + }, [disabled, onSubmit, value]); + + useLayoutEffect(() => { + const el = inputRef.current; + if (!el) return; + + el.style.height = "auto"; + const clampedHeight = Math.min(el.scrollHeight, PROMPT_TEXTAREA_MAX_HEIGHT); + el.style.height = `${clampedHeight}px`; + el.style.overflowY = el.scrollHeight > PROMPT_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden"; + }, [value]); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (modelDropdownRef.current && !modelDropdownRef.current.contains(e.target as Node)) { + setModelDropdownOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey && value.trim()) { + e.preventDefault(); + handleSubmit(); + } + }; + + const pickerDisabled = combos.length === 0 || !onSelectionChange; + + return ( +
+
+