From e87085139bb5c2eae26846823ff9526a7e85cf20 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Fri, 22 May 2026 11:15:59 -0700 Subject: [PATCH] feat: add Agents chat UI for cloud threads (#1323) * feat(ui): add Agents chat UI ported from open-swe-app Introduce a Cursor-style Agents surface separate from the dashboard, with ported chat/diff components and mock thread data until LangGraph APIs land. Co-authored-by: Cursor * feat(dashboard): wire Agents UI to LangGraph thread APIs Add dashboard thread list/detail/run/message/stream endpoints with a LangGraph message adapter, dashboard OAuth auth for runs, and TanStack Query hooks replacing mock data. Co-authored-by: Cursor * fix(dashboard): single agent reply per turn in Agents UI Use UUID thread IDs LangGraph accepts, skip confirming_completion for dashboard threads, and merge adapter agent messages so duplicate bubbles do not render. Co-authored-by: Cursor * feat(ui): polish Agents UI with floating prompt and layout cleanup Remove no-op chrome (git panel, headers, sidebar search), port CloudPromptBar from open-swe-app, and refine chat layout so messages scroll behind the input. Co-authored-by: Cursor * fix(agent): patch deepagents reducer for None messages on checkpoint replay LangGraph thread state could 500 when cancelled runs left messages as None. Apply the reducer guard before graph import, fall back to metadata in the dashboard API, and adjust Agents prompt bar layout. Co-authored-by: Cursor * feat(ui): unify sidebar user menu and clean up Agents UI navigation Extract SidebarUserMenu so the dashboard and Agents sidebars render the same profile button, drop the redundant Agents nav row in favor of the existing Back to Agents link, add the open-swe logo header to the Agents sidebar, flatten the New Agent button, and cap the home screen run list to keep the prompt input in view. * feat(ui): resizable/collapsible sidebar shared across dashboard and Agents Add a useSidebarLayout hook + SidebarFrame wrapper so both sidebars share a persisted width (default 260px, drag to resize, 200-420 range) and a collapse toggle that hides the panel and surfaces a floating reopen button. Also adds a DELETE /threads/{id} endpoint and an X-on- hover thread delete control in the Agents sidebar. * feat(ui): instant user message and busy indicator on Agents transition Stash submitted prompts in sessionStorage, pre-populate the new thread detail cache, and merge pending prompts into the rendered message list so the Agents page renders the user bubble plus the existing thinking spinner immediately instead of flashing a skeleton and "Agent is starting" while the run boots. * feat(ui): token-stream agent replies in the Agents thread view Opt the LangGraph runs into messages-tuple streaming and forward those events through the existing SSE channel. The frontend now applies AIMessageChunk deltas directly to the cached thread (cancelling any in-flight refetch first so optimistic tokens are not clobbered) and keeps positional pending prompts so the user bubble stays in the right place while the agent streams its reply. * fix(dashboard): await threads.join_stream before iterating threads.join_stream is async def returning an AsyncIterator, so it must be awaited before async for. The SSE endpoint was raising TypeError: 'async for' requires an object with __aiter__ method, got coroutine on every connection. * fix(dashboard): drop messages-tuple stream_mode that broke thinking-mode tool turns Setting stream_mode=["values","messages-tuple","updates"] on runs.create forces langchain_anthropic into streaming, and on the second model call (after tool execution) its serialized thinking blocks come back malformed, so Anthropic rejects the request with 'messages.1.content.0.thinking.thinking: Field required'. Revert to the default stream_mode so claude-opus thinking + tool use runs to completion. The frontend keeps the messages-event handler in place as a no-op fallback for when streaming is re-enabled. * feat(agents): per-thread model picker wired through to the run Add optional model_id/effort to the create-thread and send-message request bodies, forward them as agent_model_id/agent_effort in the LangGraph run configurable, and record the resolved choice in thread metadata so the UI can show the model the run is actually using. get_agent now picks the per-thread override last (highest priority over team default + profile override) and falls back gracefully when it is absent or unsupported. The frontend prompt bar becomes a controlled component fed by a shared useModelOptions hook (options + profile -> defaultSelection). AgentsHome seeds the picker from the user's profile default; the thread view seeds from the thread's recorded model/effort and lets each follow-up retarget the run. * refactor(ui): align Agents prompt bar layout with open-swe-app PromptBar Drop the absolute-positioned send button, restore the original px-4 py-3.5 min-h-[106px] flex-col container, and move the model picker into a mt-auto pt-2 footer row so the placeholder text and the model selector share the same horizontal padding. * chore: fix lint/format CI failures Remove unused imports and reformat two files flagged by ruff. * fix(tests): stop messages-reducer patch tests from polluting the suite Restore agent modules after reducer patch tests and import LangSmithSandbox from agent.server in proxy refresh tests so isinstance checks stay valid. --------- Co-authored-by: open-swe[bot] Co-authored-by: Cursor --- agent/_patch_messages_reducer.py | 72 + agent/dashboard/message_adapter.py | 238 ++++ agent/dashboard/routes.py | 78 +- agent/dashboard/thread_api.py | 441 ++++++ agent/middleware/ensure_no_empty_msg.py | 22 +- agent/review_style_analyzer.py | 4 + agent/reviewer.py | 4 + agent/server.py | 23 +- agent/utils/auth.py | 23 + agent/utils/thread_ops.py | 68 + agent/webapp.py | 79 +- tests/test_dashboard_message_adapter.py | 71 + tests/test_ensure_no_empty_msg.py | 20 +- tests/test_messages_reducer_patch.py | 82 ++ tests/test_proxy_auth.py | 15 +- ui/bun.lock | 272 +++- ui/package.json | 7 + ui/src/components/AppSidebar.tsx | 103 +- ui/src/components/SidebarUserMenu.tsx | 94 ++ ui/src/components/agents/AgentGitPanel.tsx | 157 +++ ui/src/components/agents/AgentPromptBar.tsx | 2 + ui/src/components/agents/AgentRunCard.tsx | 73 + ui/src/components/agents/AgentThreadView.tsx | 202 +++ ui/src/components/agents/AgentsHome.tsx | 67 + ui/src/components/agents/AgentsSidebar.tsx | 176 +++ .../agents/ported/BranchSelector.tsx | 188 +++ .../components/agents/ported/BubblePrefix.tsx | 18 + ui/src/components/agents/ported/ChatView.tsx | 591 ++++++++ .../agents/ported/CloudPromptBar.tsx | 156 +++ ui/src/components/agents/ported/CodeBlock.tsx | 133 ++ .../agents/ported/CompactingIndicator.tsx | 22 + .../agents/ported/ContextIndicator.tsx | 136 ++ ui/src/components/agents/ported/DiffView.tsx | 316 +++++ ui/src/components/agents/ported/Footer.tsx | 82 ++ ui/src/components/agents/ported/HeaderBar.tsx | 27 + ui/src/components/agents/ported/Logo.tsx | 19 + ui/src/components/agents/ported/Markdown.tsx | 59 + .../agents/ported/MarkdownTable.tsx | 43 + .../components/agents/ported/MessageView.tsx | 890 ++++++++++++ .../agents/ported/PanelResizeHandle.tsx | 61 + ui/src/components/agents/ported/PromptBar.tsx | 786 +++++++++++ .../components/agents/ported/ShellCommand.tsx | 105 ++ .../agents/ported/SourceControlPanel.tsx | 1 + .../agents/ported/SourceControlTile.tsx | 1198 +++++++++++++++++ .../components/agents/ported/ThreadPicker.tsx | 201 +++ ui/src/components/agents/ported/TodoList.tsx | 104 ++ .../agents/ported/ToolExecution.tsx | 287 ++++ ui/src/components/agents/ported/index.ts | 14 + ui/src/components/agents/utils/diffUtils.ts | 63 + ui/src/components/agents/z-index.ts | 6 + ui/src/components/sidebar-layout.tsx | 158 +++ ui/src/lib/agents/api.ts | 107 ++ ui/src/lib/agents/pendingPrompts.ts | 53 + ui/src/lib/agents/queries.ts | 111 ++ ui/src/lib/agents/types.ts | 169 +++ ui/src/lib/agents/useModelOptions.ts | 69 + ui/src/lib/agents/useThreadStream.ts | 167 +++ ui/src/routeTree.gen.ts | 70 + ui/src/routes/agents.tsx | 32 + ui/src/routes/agents/$threadId.tsx | 33 + ui/src/routes/agents/index.tsx | 20 + ui/src/routes/index.tsx | 2 +- ui/src/styles.css | 1 + ui/src/styles/agents.css | 47 + ui/tsconfig.json | 10 + 65 files changed, 8778 insertions(+), 170 deletions(-) create mode 100644 agent/_patch_messages_reducer.py create mode 100644 agent/dashboard/message_adapter.py create mode 100644 agent/dashboard/thread_api.py create mode 100644 agent/utils/thread_ops.py create mode 100644 tests/test_dashboard_message_adapter.py create mode 100644 tests/test_messages_reducer_patch.py create mode 100644 ui/src/components/SidebarUserMenu.tsx create mode 100644 ui/src/components/agents/AgentGitPanel.tsx create mode 100644 ui/src/components/agents/AgentPromptBar.tsx create mode 100644 ui/src/components/agents/AgentRunCard.tsx create mode 100644 ui/src/components/agents/AgentThreadView.tsx create mode 100644 ui/src/components/agents/AgentsHome.tsx create mode 100644 ui/src/components/agents/AgentsSidebar.tsx create mode 100644 ui/src/components/agents/ported/BranchSelector.tsx create mode 100644 ui/src/components/agents/ported/BubblePrefix.tsx create mode 100644 ui/src/components/agents/ported/ChatView.tsx create mode 100644 ui/src/components/agents/ported/CloudPromptBar.tsx create mode 100644 ui/src/components/agents/ported/CodeBlock.tsx create mode 100644 ui/src/components/agents/ported/CompactingIndicator.tsx create mode 100644 ui/src/components/agents/ported/ContextIndicator.tsx create mode 100644 ui/src/components/agents/ported/DiffView.tsx create mode 100644 ui/src/components/agents/ported/Footer.tsx create mode 100644 ui/src/components/agents/ported/HeaderBar.tsx create mode 100644 ui/src/components/agents/ported/Logo.tsx create mode 100644 ui/src/components/agents/ported/Markdown.tsx create mode 100644 ui/src/components/agents/ported/MarkdownTable.tsx create mode 100644 ui/src/components/agents/ported/MessageView.tsx create mode 100644 ui/src/components/agents/ported/PanelResizeHandle.tsx create mode 100644 ui/src/components/agents/ported/PromptBar.tsx create mode 100644 ui/src/components/agents/ported/ShellCommand.tsx create mode 100644 ui/src/components/agents/ported/SourceControlPanel.tsx create mode 100644 ui/src/components/agents/ported/SourceControlTile.tsx create mode 100644 ui/src/components/agents/ported/ThreadPicker.tsx create mode 100644 ui/src/components/agents/ported/TodoList.tsx create mode 100644 ui/src/components/agents/ported/ToolExecution.tsx create mode 100644 ui/src/components/agents/ported/index.ts create mode 100644 ui/src/components/agents/utils/diffUtils.ts create mode 100644 ui/src/components/agents/z-index.ts create mode 100644 ui/src/components/sidebar-layout.tsx create mode 100644 ui/src/lib/agents/api.ts create mode 100644 ui/src/lib/agents/pendingPrompts.ts create mode 100644 ui/src/lib/agents/queries.ts create mode 100644 ui/src/lib/agents/types.ts create mode 100644 ui/src/lib/agents/useModelOptions.ts create mode 100644 ui/src/lib/agents/useThreadStream.ts create mode 100644 ui/src/routes/agents.tsx create mode 100644 ui/src/routes/agents/$threadId.tsx create mode 100644 ui/src/routes/agents/index.tsx create mode 100644 ui/src/styles/agents.css 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 ( +
+
+