diff --git a/agent/dashboard/plan_api.py b/agent/dashboard/plan_api.py
index 8dc271cb..7f381e29 100644
--- a/agent/dashboard/plan_api.py
+++ b/agent/dashboard/plan_api.py
@@ -1,13 +1,15 @@
-"""REST API for the plan-review page: read the plan, approve, or request changes.
+"""REST API for the plan-review page: read the plan, comment, approve, or request
+changes — all plain HTTP, no CRDT/WebSocket.
-Comment threads themselves live in the collaborative Yjs document (BlockNote's
-``YjsThreadStore``) and are harvested client-side; on approve/reject the client
-sends the harvested feedback here, where it is formatted and handed to the agent
-as the instruction for the follow-up run. The agent never sees comments during
-review — only this aggregated feedback at the decision point.
+Reviewers leave whole-document comments via this API; they're stored server-side
+and listed for everyone who can read the thread. On approve/reject the comments
+are read back here, formatted, and handed to the agent as the instruction for the
+follow-up run. The agent never sees comments during review — only this aggregated
+feedback at the decision point.
-Permissions: any authenticated org member can read a surfaced thread and request
-changes (reject); only the thread owner can approve.
+Permissions: any authenticated org member can read a surfaced thread, comment, and
+request changes (reject); only the thread owner can approve. A comment can be
+deleted by its author or the thread owner.
"""
from __future__ import annotations
@@ -17,13 +19,16 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from langgraph_sdk import get_client
-from pydantic import BaseModel, Field
+from pydantic import BaseModel
from .oauth import require_same_origin_for_mutations, require_session
from .plan_store import (
PLAN_STATUS_APPROVED,
PLAN_STATUS_REVISING,
+ add_plan_comment,
+ delete_plan_comment,
get_plan_content,
+ list_plan_comments,
set_plan_status,
)
from .thread_api import (
@@ -43,15 +48,8 @@ plan_router = APIRouter(
_SESSION_DEP = Depends(require_session)
-class PlanComment(BaseModel):
- author: str | None = None
- body: str = ""
- quote: str | None = None
- resolved: bool = False
-
-
-class PlanDecisionBody(BaseModel):
- comments: list[PlanComment] = Field(default_factory=list)
+class CommentBody(BaseModel):
+ body: str
async def _thread_metadata(thread_id: str) -> dict[str, Any]:
@@ -88,17 +86,60 @@ async def get_plan(thread_id: str, session: dict[str, Any] = _SESSION_DEP) -> di
}
-@plan_router.post("/{thread_id}/approve")
-async def approve_plan(
- thread_id: str,
- body: PlanDecisionBody,
- session: dict[str, Any] = _SESSION_DEP,
+@plan_router.get("/{thread_id}/comments")
+async def get_plan_comments(
+ thread_id: str, session: dict[str, Any] = _SESSION_DEP
) -> dict[str, Any]:
+ metadata = await _thread_metadata(thread_id)
+ if not _thread_is_readable(metadata):
+ raise HTTPException(404, "thread not found")
+ return {"comments": await list_plan_comments(thread_id)}
+
+
+@plan_router.post("/{thread_id}/comments")
+async def post_plan_comment(
+ thread_id: str, body: CommentBody, session: dict[str, Any] = _SESSION_DEP
+) -> dict[str, Any]:
+ metadata = await _thread_metadata(thread_id)
+ if not _thread_is_readable(metadata):
+ raise HTTPException(404, "thread not found")
+ text = body.body.strip()
+ if not text:
+ raise HTTPException(422, "comment body cannot be empty")
+ login = session["sub"]
+ return await add_plan_comment(
+ thread_id, author=session.get("name") or login, author_login=login, body=text
+ )
+
+
+@plan_router.delete("/{thread_id}/comments/{comment_id}")
+async def remove_plan_comment(
+ thread_id: str, comment_id: str, session: dict[str, Any] = _SESSION_DEP
+) -> dict[str, Any]:
+ metadata = await _thread_metadata(thread_id)
+ if not _thread_is_readable(metadata):
+ raise HTTPException(404, "thread not found")
+ comments = await list_plan_comments(thread_id)
+ target = next((c for c in comments if c.get("id") == comment_id), None)
+ if target is None:
+ raise HTTPException(404, "comment not found")
+ login = session["sub"]
+ is_owner = _user_owns_thread(metadata, login, session.get("email"))
+ if target.get("author_login") != login and not is_owner:
+ raise HTTPException(403, "only the author or the plan owner can delete a comment")
+ await delete_plan_comment(thread_id, comment_id)
+ return {"ok": True}
+
+
+@plan_router.post("/{thread_id}/approve")
+async def approve_plan(thread_id: str, session: dict[str, Any] = _SESSION_DEP) -> dict[str, Any]:
metadata = await _thread_metadata(thread_id)
if not _user_owns_thread(metadata, session["sub"], session.get("email")):
raise HTTPException(403, "only the plan owner can approve")
+ # Read comments BEFORE mutating state: a store failure here aborts the
+ # decision (500) rather than dispatching the run without the feedback.
+ feedback = _format_comments(await list_plan_comments(thread_id, raise_on_error=True))
await set_plan_status(thread_id, PLAN_STATUS_APPROVED, plan_mode=False)
- feedback = _format_comments(body.comments)
if feedback:
text = (
"The plan has been approved. Implement it now, taking this reviewer "
@@ -111,16 +152,12 @@ async def approve_plan(
@plan_router.post("/{thread_id}/reject")
-async def reject_plan(
- thread_id: str,
- body: PlanDecisionBody,
- session: dict[str, Any] = _SESSION_DEP,
-) -> dict[str, Any]:
+async def reject_plan(thread_id: str, session: dict[str, Any] = _SESSION_DEP) -> dict[str, Any]:
metadata = await _thread_metadata(thread_id)
if not _thread_is_readable(metadata):
raise HTTPException(404, "thread not found")
+ feedback = _format_comments(await list_plan_comments(thread_id, raise_on_error=True))
await set_plan_status(thread_id, PLAN_STATUS_REVISING, plan_mode=True)
- feedback = _format_comments(body.comments)
text = (
"The plan needs changes before implementation. Address this reviewer "
"feedback and publish an updated plan with the save_plan tool:\n\n"
@@ -130,19 +167,16 @@ async def reject_plan(
return {"status": PLAN_STATUS_REVISING}
-def _format_comments(comments: list[PlanComment]) -> str:
+def _format_comments(comments: list[dict[str, Any]]) -> str:
lines: list[str] = []
- for index, comment in enumerate(comments, start=1):
- body = comment.body.strip()
+ index = 1
+ for comment in comments:
+ body = str(comment.get("body", "")).strip()
if not body:
continue
- author = (comment.author or "reviewer").strip()
- quote = (comment.quote or "").strip()
- status = " (resolved)" if comment.resolved else ""
- if quote:
- lines.append(f'{index}. On "{quote}"{status}:\n - {author}: {body}')
- else:
- lines.append(f"{index}. {author}{status}: {body}")
+ author = str(comment.get("author") or "reviewer").strip()
+ lines.append(f"{index}. {author}: {body}")
+ index += 1
return "\n".join(lines)
diff --git a/agent/dashboard/plan_collab.py b/agent/dashboard/plan_collab.py
deleted file mode 100644
index 5810e5db..00000000
--- a/agent/dashboard/plan_collab.py
+++ /dev/null
@@ -1,178 +0,0 @@
-"""Yjs collaboration server for the plan-review document.
-
-A single :class:`WebsocketServer` (started for the app lifetime via
-:func:`collab_lifespan`) relays Yjs sync/awareness frames between the reviewers
-connected to a plan. Each plan is a room keyed by ``thread_id``; the room's
-document is seeded from / snapshotted to the LangGraph store so a plan and its
-comment threads survive a restart.
-
-The browser connects with the standard ``y-websocket`` client, which appends the
-room name to the provider URL — hence the ``/yjs/{thread_id}`` route shape.
-
-Any user who can read the thread can both edit the plan and comment — there is
-no document-level owner/commenter split (only plan *approval* is owner-gated, in
-the REST API). So relaying frames to every readable connection is intentional.
-"""
-
-from __future__ import annotations
-
-import asyncio
-import contextlib
-import logging
-from collections.abc import AsyncIterator
-from contextlib import asynccontextmanager
-from typing import Any
-
-from fastapi import APIRouter, WebSocket, WebSocketDisconnect
-from langgraph_sdk import get_client
-from pycrdt.websocket import WebsocketServer, YRoom
-from pycrdt.websocket.websocket import HttpxWebsocket
-
-from .oauth import COOKIE_NAME, _origin_of, allowed_dashboard_origins, decode_session
-from .plan_store import load_yjs_snapshot, save_yjs_snapshot
-from .thread_api import _thread_is_readable
-
-logger = logging.getLogger(__name__)
-
-collab_router = APIRouter(prefix="/dashboard/api/plan", tags=["plan-collab"])
-
-_SNAPSHOT_DEBOUNCE_SECONDS = 1.5
-
-_server: WebsocketServer | None = None
-_dirty: set[str] = set()
-# Per-room state, alive only while at least one reviewer is connected.
-_connections: dict[str, int] = {}
-_flushers: dict[str, asyncio.Task[None]] = {}
-_subscriptions: dict[str, Any] = {}
-
-
-@asynccontextmanager
-async def collab_lifespan() -> AsyncIterator[None]:
- """Run the Yjs collaboration server for the app's lifetime."""
- global _server
- async with WebsocketServer(auto_clean_rooms=False) as server:
- _server = server
- try:
- yield
- finally:
- _server = None
- for task in _flushers.values():
- task.cancel()
- _flushers.clear()
- _subscriptions.clear()
- _connections.clear()
- _dirty.clear()
-
-
-def _session_login(websocket: WebSocket) -> str | None:
- token = websocket.cookies.get(COOKIE_NAME)
- if not token:
- return None
- try:
- session = decode_session(token)
- except Exception:
- return None
- sub = session.get("sub") if isinstance(session, dict) else None
- return sub if isinstance(sub, str) and sub else None
-
-
-def _origin_allowed(websocket: WebSocket) -> bool:
- """Same-origin (CSRF) gate for the WS handshake, mirroring the REST
- ``require_same_origin`` check: a no-op when no dashboard origins are
- configured (local/dev), otherwise the handshake ``Origin`` must be in the
- allowlist so a malicious site can't open a cross-site socket on the
- victim's cookie."""
- allowed = allowed_dashboard_origins()
- if not allowed:
- return True
- origin = _origin_of(websocket.headers.get("origin", "") or "")
- return bool(origin) and origin in allowed
-
-
-async def _can_read_thread(thread_id: str) -> bool:
- """Same read gate as the plan REST API: only surfaced threads are joinable."""
- try:
- thread = await get_client().threads.get(thread_id)
- except Exception:
- return False
- metadata = (
- thread.get("metadata") if isinstance(thread, dict) else getattr(thread, "metadata", None)
- )
- return _thread_is_readable(metadata if isinstance(metadata, dict) else {})
-
-
-async def _open_room(server: WebsocketServer, thread_id: str) -> None:
- """Seed the room from the store and start its snapshot flusher (idempotent
- while connected)."""
- if thread_id in _flushers:
- return
- room = await server.get_room(thread_id)
- snapshot = await load_yjs_snapshot(thread_id)
- if snapshot:
- try:
- room.ydoc.apply_update(snapshot)
- except Exception:
- logger.warning("Failed to seed plan doc %s from snapshot", thread_id, exc_info=True)
-
- def _on_change(_event: object) -> None:
- _dirty.add(thread_id)
-
- _subscriptions[thread_id] = room.ydoc.observe(_on_change)
- _flushers[thread_id] = asyncio.create_task(_flush_loop(thread_id, room))
-
-
-async def _close_room(thread_id: str) -> None:
- """Tear down a room's flusher + observer once the last reviewer leaves."""
- task = _flushers.pop(thread_id, None)
- _subscriptions.pop(thread_id, None)
- if task is not None:
- task.cancel()
- with contextlib.suppress(asyncio.CancelledError):
- await task
- if _server is not None and thread_id in _server.rooms:
- await _persist(thread_id, _server.rooms[thread_id])
- _dirty.discard(thread_id)
-
-
-async def _flush_loop(thread_id: str, room: YRoom) -> None:
- while True:
- await asyncio.sleep(_SNAPSHOT_DEBOUNCE_SECONDS)
- if thread_id in _dirty:
- _dirty.discard(thread_id)
- await _persist(thread_id, room)
-
-
-async def _persist(thread_id: str, room: YRoom) -> None:
- try:
- await save_yjs_snapshot(thread_id, bytes(room.ydoc.get_update()))
- except Exception:
- logger.warning("Failed to snapshot plan doc %s", thread_id, exc_info=True)
-
-
-@collab_router.websocket("/yjs/{thread_id}")
-async def plan_yjs_socket(websocket: WebSocket, thread_id: str) -> None:
- if _session_login(websocket) is None:
- await websocket.close(code=4401)
- return
- if not _origin_allowed(websocket):
- await websocket.close(code=4403)
- return
- if not await _can_read_thread(thread_id):
- await websocket.close(code=4403)
- return
- if _server is None:
- await websocket.close(code=1013) # try again later
- return
-
- await websocket.accept()
- await _open_room(_server, thread_id)
- _connections[thread_id] = _connections.get(thread_id, 0) + 1
- channel = HttpxWebsocket(websocket, thread_id)
- try:
- with contextlib.suppress(WebSocketDisconnect):
- await _server.serve(channel)
- finally:
- _connections[thread_id] = _connections.get(thread_id, 1) - 1
- if _connections.get(thread_id, 0) <= 0:
- _connections.pop(thread_id, None)
- await _close_room(thread_id)
diff --git a/agent/dashboard/plan_store.py b/agent/dashboard/plan_store.py
index d130fa34..b6098163 100644
--- a/agent/dashboard/plan_store.py
+++ b/agent/dashboard/plan_store.py
@@ -1,25 +1,25 @@
-"""Persistence for the collaborative plan-review feature.
+"""Persistence for the plan-review feature.
The plan lives in two places:
- the agent's sandbox, as a real ``plan.md`` file (written by the ``save_plan``
tool — the source artifact the agent produces and can re-read), and
- - the LangGraph store, as the published snapshot the dashboard renders and the
- seed for the collaborative Yjs document.
+ - the LangGraph store, as the published snapshot the dashboard renders.
-Comment threads live in the Yjs document (BlockNote's ``YjsThreadStore``); the
-binary Yjs state is snapshotted here so a plan + its comments survive a sandbox
-teardown or a server restart.
+Reviewers leave whole-document comments, stored one item per comment under
+``["plan", "comments", thread_id]`` so listing and deletion are simple plain
+store operations (no CRDT/WebSocket).
"""
from __future__ import annotations
-import base64
+import uuid
+from datetime import UTC, datetime
from typing import Any
from langgraph_sdk import get_client
PLAN_CONTENT_NAMESPACE = ["plan", "content"]
-PLAN_YJS_NAMESPACE = ["plan", "yjs"]
+PLAN_COMMENTS_NAMESPACE = ["plan", "comments"]
# Plan lifecycle, stored on both the content record and the thread metadata.
PLAN_STATUS_PLANNING = "planning"
@@ -43,13 +43,22 @@ def _item_value(item: Any) -> dict[str, Any] | None:
async def save_plan_content(
thread_id: str, *, markdown: str, status: str = PLAN_STATUS_READY
) -> None:
- """Publish the plan markdown + status for the dashboard to render."""
+ """Publish the plan markdown + status for the dashboard to render.
+
+ A republished (revised) plan supersedes the prior revision, so comments left
+ on it are cleared — otherwise stale feedback would resurface on the new plan
+ and be fed back to the agent on the next approve/reject."""
client = _client()
await client.store.put_item(
PLAN_CONTENT_NAMESPACE,
thread_id,
{"markdown": markdown, "status": status},
)
+ try:
+ await clear_plan_comments(thread_id)
+ except Exception:
+ # Best-effort: a failed cleanup must not block publishing the new plan.
+ pass
await _merge_thread_metadata(thread_id, {"plan_status": status, "plan_mode": True})
@@ -77,31 +86,57 @@ async def set_plan_status(thread_id: str, status: str, *, plan_mode: bool | None
await _merge_thread_metadata(thread_id, metadata)
-async def load_yjs_snapshot(thread_id: str) -> bytes | None:
+def _comments_namespace(thread_id: str) -> list[str]:
+ return [*PLAN_COMMENTS_NAMESPACE, thread_id]
+
+
+async def list_plan_comments(
+ thread_id: str, *, raise_on_error: bool = False
+) -> list[dict[str, Any]]:
+ """All comments on a plan, oldest first.
+
+ With ``raise_on_error=True`` a store/search failure propagates instead of
+ resolving to ``[]``. Approve/reject use this so a transient failure surfaces
+ (the decision endpoint errors) rather than silently feeding the agent an
+ empty comment set and dropping the reviewer's feedback."""
client = _client()
try:
- item = await client.store.get_item(PLAN_YJS_NAMESPACE, thread_id)
+ items = await client.store.search_items(_comments_namespace(thread_id), limit=1000)
except Exception:
- return None
- value = _item_value(item)
- if not value:
- return None
- encoded = value.get("b64")
- if not isinstance(encoded, str) or not encoded:
- return None
- try:
- return base64.b64decode(encoded)
- except (ValueError, TypeError):
- return None
+ if raise_on_error:
+ raise
+ return []
+ raw = items.get("items", []) if isinstance(items, dict) else getattr(items, "items", [])
+ comments = [v for v in (_item_value(item) for item in raw) if v]
+ comments.sort(key=lambda c: str(c.get("created_at", "")))
+ return comments
-async def save_yjs_snapshot(thread_id: str, data: bytes) -> None:
- client = _client()
- await client.store.put_item(
- PLAN_YJS_NAMESPACE,
- thread_id,
- {"b64": base64.b64encode(data).decode("ascii")},
- )
+async def clear_plan_comments(thread_id: str) -> None:
+ """Delete every comment on a thread (called when a revised plan is published)."""
+ for comment in await list_plan_comments(thread_id):
+ comment_id = comment.get("id")
+ if isinstance(comment_id, str) and comment_id:
+ await delete_plan_comment(thread_id, comment_id)
+
+
+async def add_plan_comment(
+ thread_id: str, *, author: str, author_login: str, body: str
+) -> dict[str, Any]:
+ """Append a whole-document comment; returns the stored comment."""
+ comment = {
+ "id": uuid.uuid4().hex,
+ "author": author,
+ "author_login": author_login,
+ "body": body,
+ "created_at": datetime.now(UTC).isoformat(),
+ }
+ await _client().store.put_item(_comments_namespace(thread_id), comment["id"], comment)
+ return comment
+
+
+async def delete_plan_comment(thread_id: str, comment_id: str) -> None:
+ await _client().store.delete_item(_comments_namespace(thread_id), comment_id)
async def _merge_thread_metadata(thread_id: str, metadata: dict[str, Any]) -> None:
diff --git a/agent/webapp.py b/agent/webapp.py
index ba13bc70..55ace137 100644
--- a/agent/webapp.py
+++ b/agent/webapp.py
@@ -131,14 +131,12 @@ logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
- from .dashboard.plan_collab import collab_lifespan
from .utils.model import validate_local_dev_llm_config
from .utils.sandbox import validate_sandbox_startup_config
validate_sandbox_startup_config()
validate_local_dev_llm_config()
- async with collab_lifespan():
- yield
+ yield
app = FastAPI(lifespan=lifespan)
@@ -162,10 +160,8 @@ if DASHBOARD_ALLOWED_ORIGINS:
app.include_router(dashboard_router)
from .dashboard.plan_api import plan_router # noqa: E402
-from .dashboard.plan_collab import collab_router # noqa: E402
app.include_router(plan_router)
-app.include_router(collab_router)
LINEAR_WEBHOOK_SECRET = os.environ.get("LINEAR_WEBHOOK_SECRET", "")
GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET", "")
diff --git a/pyproject.toml b/pyproject.toml
index c53a4196..5d43456f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,8 +29,6 @@ dependencies = [
"exa-py>=2.13.0",
"langchain-google-genai>=4.2.4",
"langchain-mcp-adapters>=0.2.2",
- "pycrdt>=0.14.1,<0.15",
- "pycrdt-websocket>=0.16.3,<0.17",
]
[project.optional-dependencies]
diff --git a/tests/e2e/tests/plan_review.spec.ts b/tests/e2e/tests/plan_review.spec.ts
index 006da900..85b57ec0 100644
--- a/tests/e2e/tests/plan_review.spec.ts
+++ b/tests/e2e/tests/plan_review.spec.ts
@@ -9,10 +9,11 @@ import {
// user asks Open SWE in Slack to PLAN something ->
// agent calls enter_plan_mode, posts the plan-review link to Slack, writes the
// plan as a markdown file (save_plan), and posts "ready" back to Slack ->
-// owner (user1) and a collaborator (user2) both open the plan and leave inline
-// comments using BlockNote's native comment UI (synced live via Yjs) ->
+// owner (user1) and a collaborator (user2) open the plan and leave whole-document
+// comments over plain HTTP (each polls and sees the other's) ->
// only the owner can approve -> on approval the agent implements, opens a PR,
-// and replies in Slack with the link, having received the reviewers' comments.
+// and replies in Slack with the link, echoing the reviewers' comments (which the
+// server harvested from the comment store).
// Only the LLM is faked; all agent + dashboard code runs for real.
const OWNER = { login: "alice", email: "alice@example.com" };
@@ -24,18 +25,13 @@ async function botMessages(request: APIRequestContext): Promise> {
return msgs.filter((m) => m.is_bot).map((m) => m.text);
}
-// Create an inline comment via BlockNote's native UI: select text in the plan,
-// click "Add comment" in the formatting toolbar, type, and save.
-async function addInlineComment(page: Page, anchorText: string, comment: string) {
- await page.locator(".bn-editor").getByText(anchorText, { exact: false }).first().selectText();
- await page.locator('[data-test="addcomment"]').click();
- await page.locator(".bn-comment-editor .ProseMirror").last().waitFor({ state: "visible" });
- await page.keyboard.type(comment);
- await page.locator('[data-test="save"]').click();
+async function addComment(page: Page, text: string) {
+ await page.getByTestId("comment-input").fill(text);
+ await page.getByTestId("comment-submit").click();
}
-test.describe("Plan review (BlockNote native comments)", () => {
- test("Slack plan request → inline comments → owner approves → PR", async ({
+test.describe("Plan review (HTTP comments)", () => {
+ test("Slack plan request → comments → owner approves → PR", async ({
browser,
request,
}) => {
@@ -51,8 +47,7 @@ test.describe("Plan review (BlockNote native comments)", () => {
// 1a. enter_plan_mode must actually engage, not error out. Its Command must
// carry a terminating ToolMessage; without it the tool call fails and is
// swallowed into an error tool message while the agent silently proceeds
- // as a normal run. Assert the tool's success message landed in the
- // thread (it wouldn't if the call had errored).
+ // as a normal run. Assert the tool's success message landed in the thread.
await expect
.poll(
async () => {
@@ -78,8 +73,8 @@ test.describe("Plan review (BlockNote native comments)", () => {
.poll(async () => (await botMessages(request)).join("\n"), { timeout: 60_000 })
.toMatch(/ready for review/i);
- // 3. The OWNER opens the conversation, sees the "Review plan" banner, and
- // follows it into the plan — which renders inside the agents shell.
+ // 3. The OWNER opens the conversation, follows the "Review plan" banner, and
+ // sees the rendered plan.
const ownerCtx = await browser.newContext();
await ownerCtx.request.post("/control/login", { data: OWNER });
const owner = await ownerCtx.newPage();
@@ -89,20 +84,18 @@ test.describe("Plan review (BlockNote native comments)", () => {
await reviewLink.click();
await expect(owner).toHaveURL(new RegExp(`/agents/${threadId}/plan$`));
await expect(owner.getByTestId("plan-review")).toBeVisible({ timeout: 30_000 });
- // The agents sidebar is present (plan lives inside the shell).
await expect(owner.getByText("Back to conversation")).toBeVisible();
await expect(owner.getByTestId("plan-document")).toContainText("greet", {
timeout: 30_000,
});
await expect(owner.getByTestId("approve-plan")).toBeVisible();
- // Owner leaves an inline comment on the plan heading (a comment wraps the
- // text in a `.bn-thread-mark`).
- await addInlineComment(owner, "Add greet() helper", "Owner: looks solid, ship it.");
- await expect(owner.locator(".bn-thread-mark")).toHaveCount(1);
+ // Owner leaves a comment.
+ await addComment(owner, "Owner: looks solid, ship it.");
+ await expect(owner.getByTestId("plan-comment")).toHaveCount(1);
// 4. A COLLABORATOR opens the same plan: sees it AND the owner's comment
- // (synced live via Yjs), but has NO approve button.
+ // (fetched over HTTP), but has NO approve button.
const collabCtx = await browser.newContext();
await collabCtx.request.post("/control/login", { data: COLLABORATOR });
const collab = await collabCtx.newPage();
@@ -111,26 +104,23 @@ test.describe("Plan review (BlockNote native comments)", () => {
await expect(collab.getByTestId("plan-document")).toContainText("greet", {
timeout: 30_000,
});
- await expect(collab.locator(".bn-thread-mark")).toHaveCount(1); // owner's comment, synced
+ await expect(collab.getByTestId("plan-comment")).toHaveCount(1, { timeout: 30_000 });
+ await expect(collab.getByTestId("plan-comment")).toContainText("looks solid");
await expect(collab.getByTestId("approve-plan")).toHaveCount(0);
await expect(collab.getByTestId("reject-plan")).toBeVisible();
- // Collaborator leaves inline feedback on a different part of the plan.
- await addInlineComment(
- collab,
- "tiny greeting helper",
- "Reviewer: please also add a docstring.",
- );
- await expect(collab.locator(".bn-thread-mark")).toHaveCount(2);
+ // Collaborator leaves feedback.
+ await addComment(collab, "Reviewer: please also add a docstring.");
+ await expect(collab.getByTestId("plan-comment")).toHaveCount(2);
- // 5. The owner sees the collaborator's comment sync in, then approves.
- await expect(owner.locator(".bn-thread-mark")).toHaveCount(2);
+ // 5. The owner sees the collaborator's comment (polled), then approves.
+ await expect(owner.getByTestId("plan-comment")).toHaveCount(2, { timeout: 30_000 });
await owner.getByTestId("approve-plan").click();
await expect(owner.getByTestId("plan-decision")).toContainText(/implementing/i);
// 6. The agent implements, opens a PR, and links it back in the Slack thread,
- // echoing the reviewers' feedback it received — which proves the
- // collaborator's comment synced to the owner and was harvested on approve.
+ // echoing the reviewers' feedback — which proves the comments were stored
+ // and harvested server-side on approve.
await expect
.poll(async () => (await botMessages(request)).join("\n"), { timeout: 90_000 })
.toMatch(/\/pull\//);
diff --git a/tests/test_plan_review.py b/tests/test_plan_review.py
index 84ee4bb9..b9a43035 100644
--- a/tests/test_plan_review.py
+++ b/tests/test_plan_review.py
@@ -18,19 +18,18 @@ def test_dashboard_plan_url_none_without_thread() -> None:
assert dashboard_plan_url("") is None
-def test_format_comments_with_quote_and_author() -> None:
- from agent.dashboard.plan_api import PlanComment, _format_comments
+def test_format_comments_numbers_and_skips_blank() -> None:
+ from agent.dashboard.plan_api import _format_comments
text = _format_comments(
[
- PlanComment(author="alice", body="add a docstring", quote="def greet"),
- PlanComment(author="bob", body="looks good", resolved=True),
- PlanComment(author="carol", body=" "), # blank → skipped
+ {"author": "alice", "body": "add a docstring"},
+ {"author": "bob", "body": "looks good"},
+ {"author": "carol", "body": " "}, # blank → skipped
]
)
- assert 'On "def greet"' in text
- assert "alice: add a docstring" in text
- assert "bob (resolved): looks good" in text
+ assert "1. alice: add a docstring" in text
+ assert "2. bob: looks good" in text
assert "carol" not in text
@@ -40,14 +39,60 @@ def test_format_comments_empty() -> None:
assert _format_comments([]) == ""
-def test_plan_decision_body_defaults() -> None:
- from agent.dashboard.plan_api import PlanComment, PlanDecisionBody
+def test_plan_comment_helpers_exported() -> None:
+ from agent.dashboard import plan_store
- body = PlanDecisionBody()
- assert body.comments == []
- comment = PlanComment(body="hi")
- assert comment.author is None
- assert comment.resolved is False
+ assert plan_store.PLAN_COMMENTS_NAMESPACE == ["plan", "comments"]
+ assert callable(plan_store.add_plan_comment)
+ assert callable(plan_store.list_plan_comments)
+ assert callable(plan_store.delete_plan_comment)
+ assert callable(plan_store.clear_plan_comments)
+
+
+def _fake_client(store: Any) -> Any:
+ return type("C", (), {"store": store})()
+
+
+async def test_list_plan_comments_swallows_errors_by_default(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ from agent.dashboard import plan_store
+
+ class _Store:
+ async def search_items(self, *a: Any, **k: Any) -> Any:
+ raise RuntimeError("boom")
+
+ monkeypatch.setattr(plan_store, "_client", lambda: _fake_client(_Store()))
+ assert await plan_store.list_plan_comments("t") == []
+
+
+async def test_list_plan_comments_raises_with_flag(monkeypatch: pytest.MonkeyPatch) -> None:
+ from agent.dashboard import plan_store
+
+ class _Store:
+ async def search_items(self, *a: Any, **k: Any) -> Any:
+ raise RuntimeError("boom")
+
+ monkeypatch.setattr(plan_store, "_client", lambda: _fake_client(_Store()))
+ with pytest.raises(RuntimeError):
+ await plan_store.list_plan_comments("t", raise_on_error=True)
+
+
+async def test_clear_plan_comments_deletes_each(monkeypatch: pytest.MonkeyPatch) -> None:
+ from agent.dashboard import plan_store
+
+ deleted: list[str] = []
+
+ class _Store:
+ async def search_items(self, *a: Any, **k: Any) -> Any:
+ return {"items": [{"value": {"id": "a"}}, {"value": {"id": "b"}}]}
+
+ async def delete_item(self, _ns: Any, key: str) -> None:
+ deleted.append(key)
+
+ monkeypatch.setattr(plan_store, "_client", lambda: _fake_client(_Store()))
+ await plan_store.clear_plan_comments("t")
+ assert deleted == ["a", "b"]
def test_save_plan_requires_run_context() -> None:
@@ -74,7 +119,9 @@ def test_plan_routes_registered() -> None:
assert "/dashboard/api/plan/{thread_id}" in paths
assert "/dashboard/api/plan/{thread_id}/approve" in paths
assert "/dashboard/api/plan/{thread_id}/reject" in paths
- assert "/dashboard/api/plan/yjs/{thread_id}" in paths
+ assert "/dashboard/api/plan/{thread_id}/comments" in paths
+ assert "/dashboard/api/plan/{thread_id}/comments/{comment_id}" in paths
+ assert "/dashboard/api/plan/yjs/{thread_id}" not in paths
def test_save_plan_exported_and_wired() -> None:
@@ -98,31 +145,6 @@ def test_http_request_excluded_in_plan_mode() -> None:
assert "http_request" in PLAN_MODE_EXCLUDED_TOOLS
-class _FakeWS:
- def __init__(self, origin: str | None) -> None:
- self.headers = {"origin": origin} if origin is not None else {}
-
-
-def test_collab_origin_allowed_noop_without_allowlist(monkeypatch: pytest.MonkeyPatch) -> None:
- from agent.dashboard.plan_collab import _origin_allowed
-
- monkeypatch.delenv("DASHBOARD_BASE_URL", raising=False)
- monkeypatch.delenv("DASHBOARD_ALLOWED_ORIGINS", raising=False)
- # No configured origins (local/dev): the gate is a no-op.
- assert _origin_allowed(_FakeWS(None)) is True
- assert _origin_allowed(_FakeWS("https://evil.test")) is True
-
-
-def test_collab_origin_allowed_enforced_with_allowlist(monkeypatch: pytest.MonkeyPatch) -> None:
- from agent.dashboard.plan_collab import _origin_allowed
-
- monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example")
- monkeypatch.delenv("DASHBOARD_ALLOWED_ORIGINS", raising=False)
- assert _origin_allowed(_FakeWS("https://app.example")) is True
- assert _origin_allowed(_FakeWS("https://evil.test")) is False
- assert _origin_allowed(_FakeWS(None)) is False
-
-
class _FakeReq:
def __init__(self, tools: list[Any], state: dict[str, Any]) -> None:
self.tools = tools
diff --git a/ui/bun.lock b/ui/bun.lock
index bb46dc59..f484806d 100644
--- a/ui/bun.lock
+++ b/ui/bun.lock
@@ -6,9 +6,6 @@
"name": "open-swe-dashboard",
"dependencies": {
"@base-ui/react": "^1.4.1",
- "@blocknote/core": "^0.51.4",
- "@blocknote/mantine": "^0.51.4",
- "@blocknote/react": "^0.51.4",
"@fontsource-variable/inter": "^5.2.8",
"@langchain/core": "^1.1.48",
"@langchain/langgraph-sdk": "^1.9.21",
@@ -41,8 +38,6 @@
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^5.1.4",
- "y-websocket": "^3.0.0",
- "yjs": "^13.6.27",
},
"devDependencies": {
"@tanstack/devtools-vite": "^0.6.0",
@@ -277,12 +272,6 @@
"@base-ui/utils": ["@base-ui/utils@0.2.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="],
- "@blocknote/core": ["@blocknote/core@0.51.4", "", { "dependencies": { "@emoji-mart/data": "^1.2.1", "@handlewithcare/prosemirror-inputrules": "^0.1.4", "@shikijs/types": "^4", "@tanstack/store": "^0.7.7", "@tiptap/core": "^3.13.0", "@tiptap/extension-bold": "^3.13.0", "@tiptap/extension-code": "^3.13.0", "@tiptap/extension-horizontal-rule": "^3.13.0", "@tiptap/extension-italic": "^3.13.0", "@tiptap/extension-paragraph": "^3.13.0", "@tiptap/extension-strike": "^3.13.0", "@tiptap/extension-text": "^3.13.0", "@tiptap/extension-underline": "^3.13.0", "@tiptap/extensions": "^3.13.0", "@tiptap/pm": "^3.13.0", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "lib0": "^0.2.99", "prosemirror-highlight": "^0.15.1", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.3", "prosemirror-transform": "^1.11.0", "prosemirror-view": "^1.41.4", "y-prosemirror": "^1.3.7", "y-protocols": "^1.0.6", "yjs": "^13.6.27" } }, "sha512-uR7/BlW6VVlCx/JzfGbkaLGz7O5uI3bu2tbNIdzGTfSloiEp8Qc14if36Dy7DDs+dVWfZgwds0jWiGfE3AzJiw=="],
-
- "@blocknote/mantine": ["@blocknote/mantine@0.51.4", "", { "dependencies": { "@blocknote/core": "0.51.4", "@blocknote/react": "0.51.4", "react-icons": "^5.5.0" }, "peerDependencies": { "@mantine/core": "^8.3.11 || ^9.0.2", "@mantine/hooks": "^8.3.11 || ^9.0.2", "react": "^18.0 || ^19.0 || >= 19.0.0-rc", "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" } }, "sha512-Ka7QjzhgB/T2ekwtXgwa9zIiiixAm4dob3FklJIedu+9j/eO4auokmRnWAhZQmNJsOdE7mQTUmnv6X4k/lHyCQ=="],
-
- "@blocknote/react": ["@blocknote/react@0.51.4", "", { "dependencies": { "@blocknote/core": "0.51.4", "@emoji-mart/data": "^1.2.1", "@floating-ui/react": "^0.27.18", "@floating-ui/utils": "^0.2.10", "@tanstack/react-store": "0.7.7", "@tiptap/core": "^3.13.0", "@tiptap/pm": "^3.13.0", "@tiptap/react": "^3.13.0", "@types/use-sync-external-store": "1.5.0", "emoji-mart": "^5.6.0", "fast-deep-equal": "^3.1.3", "lodash.merge": "^4.6.2", "react-icons": "^5.5.0", "use-sync-external-store": "1.6.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || >= 19.0.0-rc", "react-dom": "^18.0 || ^19.0 || >= 19.0.0-rc" } }, "sha512-tWe039t/Gkj9YDoEXruclXQvI+toI0ctcyikbHa8dm3Q6zTgy24+T/P1t1fYSNPBkSnhq+ILz2fLLnCK994qkA=="],
-
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
"@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
@@ -311,8 +300,6 @@
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
- "@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="],
-
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
@@ -387,16 +374,12 @@
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
- "@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="],
-
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="],
- "@handlewithcare/prosemirror-inputrules": ["@handlewithcare/prosemirror-inputrules@0.1.4", "", { "dependencies": { "prosemirror-history": "^1.4.1", "prosemirror-transform": "^1.0.0" }, "peerDependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-GMqlBeG2MKM+tXEFd2N+wIv5z4VvJTg8JtfJUrdjvFq2W6v+AW8oTgiWyFw8L3iEQwvtQcVJxU873iB0LXUNNw=="],
-
"@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
@@ -435,10 +418,6 @@
"@langchain/react": ["@langchain/react@1.0.22", "", { "dependencies": { "@langchain/langgraph-sdk": "1.9.21" }, "peerDependencies": { "@langchain/core": "^1.1.48", "react": "^18 || ^19" } }, "sha512-e3llirWyH6RI6XoTwm7i9e7jiV3A6GDp50JCFKyo8XfZiufE6HiMzZHtpiMLkR/JezfdOTEHDyEC148C0gVB8A=="],
- "@mantine/core": ["@mantine/core@8.3.18", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA=="],
-
- "@mantine/hooks": ["@mantine/hooks@8.3.18", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw=="],
-
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
@@ -681,7 +660,7 @@
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.19", "", { "dependencies": { "@tanstack/react-router": "1.170.15", "@tanstack/router-core": "1.171.13", "@tanstack/start-server-core": "1.169.14" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-+eMpAwDreQvCwgX45MdUHTUCF/Wad36+PwQafe6W5wa3qVkGyN3P131ShGyRwT/0WwKa5EVGdW1zFgwby8UNqA=="],
- "@tanstack/react-store": ["@tanstack/react-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg=="],
+ "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
"@tanstack/router-core": ["@tanstack/router-core@1.171.13", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA=="],
@@ -705,7 +684,7 @@
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.15", "", { "dependencies": { "@tanstack/router-core": "1.171.13" } }, "sha512-Jy0q4vdG6pv76N92+X+ag3fuOV2zINQagYyMN1/es7tPI1vzpKECIU8AqHqzI6ahkwaph7XDvmfUkiLJ3i4LOA=="],
- "@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
+ "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
@@ -713,34 +692,6 @@
"@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
- "@tiptap/core": ["@tiptap/core@3.27.1", "", { "peerDependencies": { "@tiptap/pm": "3.27.1" } }, "sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ=="],
-
- "@tiptap/extension-bold": ["@tiptap/extension-bold@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ=="],
-
- "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.27.1", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw=="],
-
- "@tiptap/extension-code": ["@tiptap/extension-code@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w=="],
-
- "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.27.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q=="],
-
- "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg=="],
-
- "@tiptap/extension-italic": ["@tiptap/extension-italic@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA=="],
-
- "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q=="],
-
- "@tiptap/extension-strike": ["@tiptap/extension-strike@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q=="],
-
- "@tiptap/extension-text": ["@tiptap/extension-text@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg=="],
-
- "@tiptap/extension-underline": ["@tiptap/extension-underline@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ=="],
-
- "@tiptap/extensions": ["@tiptap/extensions@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw=="],
-
- "@tiptap/pm": ["@tiptap/pm@3.27.1", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.7", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.0", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.41.8" } }, "sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw=="],
-
- "@tiptap/react": ["@tiptap/react@3.27.1", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.27.1", "@tiptap/extension-floating-menu": "^3.27.1" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/Wn2fc9zMtX08MXYScDFsm4wJ8lzfhfPEdbtls7WCDlbtrop48PWlkHDBBJrywARfAQTB2mFs9KiFy9yrQm5Lg=="],
-
"@trickfilm400/rollup-plugin-off-main-thread": ["@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1", "", { "dependencies": { "ejs": "^3.1.10", "json5": "^2.2.3", "magic-string": "^0.30.21", "string.prototype.matchall": "^4.0.12" } }, "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q=="],
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
@@ -853,8 +804,6 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
- "@types/use-sync-external-store": ["@types/use-sync-external-store@1.5.0", "", {}, "sha512-5dyB8nLC/qogMrlCizZnYWQTA4lnb/v+It+sqNl5YnSRAPMlIqY/X0Xn+gZw8vOL+TgTTr28VEbn3uf8fUtAkw=="],
-
"@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.61.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/type-utils": "8.61.0", "@typescript-eslint/utils": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw=="],
@@ -1209,8 +1158,6 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
- "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
-
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
@@ -1231,8 +1178,6 @@
"electron-to-chromium": ["electron-to-chromium@1.5.371", "", {}, "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w=="],
- "emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="],
-
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
@@ -1329,8 +1274,6 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
- "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
-
"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=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -1393,8 +1336,6 @@
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
- "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
-
"get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="],
"get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
@@ -1597,8 +1538,6 @@
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
- "isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
-
"jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
@@ -1651,8 +1590,6 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
- "lib0": ["lib0@0.2.117", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw=="],
-
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
@@ -1685,8 +1622,6 @@
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
- "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
-
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
@@ -1883,8 +1818,6 @@
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
- "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
-
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
@@ -1969,34 +1902,6 @@
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
- "prosemirror-changeset": ["prosemirror-changeset@2.4.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="],
-
- "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
-
- "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
-
- "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="],
-
- "prosemirror-highlight": ["prosemirror-highlight@0.15.2", "", { "peerDependencies": { "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@shikijs/types": "^1.29.2 || ^2.0.0 || ^3.0.0 || ^4.0.0", "@types/hast": "^3.0.0", "highlight.js": "^11.9.0", "lowlight": "^3.1.0", "prosemirror-model": "^1.19.3", "prosemirror-state": "^1.4.3", "prosemirror-transform": "^1.8.0", "prosemirror-view": "^1.32.4", "refractor": "^5.0.0", "sugar-high": "^0.6.1 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^1.0.0" }, "optionalPeers": ["@lezer/common", "@lezer/highlight", "@shikijs/types", "@types/hast", "highlight.js", "lowlight", "prosemirror-model", "prosemirror-state", "prosemirror-transform", "prosemirror-view", "refractor", "sugar-high"] }, "sha512-G5Wn4kn+MZMus/7vSh4ZNsC+3yrMpjKsgV6QKKYfwKOPMknRX+3H6FWr+E+Qji4xhmkqdXiToG1VTQ2bZn5b0w=="],
-
- "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="],
-
- "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="],
-
- "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
-
- "prosemirror-model": ["prosemirror-model@1.25.9", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA=="],
-
- "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
-
- "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="],
-
- "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="],
-
- "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="],
-
- "prosemirror-view": ["prosemirror-view@1.41.9", "", { "dependencies": { "prosemirror-model": "^1.25.8", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A=="],
-
"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=="],
@@ -2017,18 +1922,8 @@
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
- "react-number-format": ["react-number-format@5.4.5", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw=="],
-
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
- "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
-
- "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
-
- "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
-
- "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
-
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"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=="],
@@ -2089,8 +1984,6 @@
"rollup": ["rollup@4.61.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="],
- "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
-
"rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="],
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
@@ -2221,8 +2114,6 @@
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
- "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
-
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
@@ -2343,16 +2234,6 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
- "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
-
- "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="],
-
- "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
-
- "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="],
-
- "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
-
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@@ -2381,8 +2262,6 @@
"vue-eslint-parser": ["vue-eslint-parser@10.4.1", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA=="],
- "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
-
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
@@ -2457,16 +2336,8 @@
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
- "y-prosemirror": ["y-prosemirror@1.3.7", "", { "dependencies": { "lib0": "^0.2.109" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-NpM99WSdD4Fx4if5xOMDpPtU3oAmTSjlzh5U4353ABbRHl1HtAFUx6HlebLZfyFxXN9jzKMDkVbcRjqOZVkYQg=="],
-
- "y-protocols": ["y-protocols@1.0.7", "", { "dependencies": { "lib0": "^0.2.85" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw=="],
-
- "y-websocket": ["y-websocket@3.0.0", "", { "dependencies": { "lib0": "^0.2.102", "y-protocols": "^1.0.5" }, "peerDependencies": { "yjs": "^13.5.6" } }, "sha512-mUHy7AzkOZ834T/7piqtlA8Yk6AchqKqcrCXjKW8J1w2lPtRDjz8W5/CvXz9higKAHgKRKqpI3T33YkRFLkPtg=="],
-
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
- "yjs": ["yjs@13.6.31", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw=="],
-
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"yocto-spinner": ["yocto-spinner@1.2.0", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw=="],
@@ -2489,8 +2360,6 @@
"@langchain/langgraph-sdk/p-queue": ["p-queue@9.3.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang=="],
- "@mantine/core/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
-
"@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=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@@ -2511,16 +2380,12 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
- "@tanstack/react-router/@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
-
"@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/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
- "@tiptap/react/@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
-
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
@@ -2643,8 +2508,6 @@
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
- "@tanstack/react-router/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
-
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
diff --git a/ui/package.json b/ui/package.json
index 0ad00616..1af93dc2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -13,9 +13,6 @@
},
"dependencies": {
"@base-ui/react": "^1.4.1",
- "@blocknote/core": "^0.51.4",
- "@blocknote/mantine": "^0.51.4",
- "@blocknote/react": "^0.51.4",
"@fontsource-variable/inter": "^5.2.8",
"@langchain/core": "^1.1.48",
"@langchain/langgraph-sdk": "^1.9.21",
@@ -47,9 +44,7 @@
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0",
- "vite-tsconfig-paths": "^5.1.4",
- "y-websocket": "^3.0.0",
- "yjs": "^13.6.27"
+ "vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
"@tanstack/devtools-vite": "^0.6.0",
diff --git a/ui/src/components/agents/PlanReview.tsx b/ui/src/components/agents/PlanReview.tsx
index 9f1bae12..ea1c9c71 100644
--- a/ui/src/components/agents/PlanReview.tsx
+++ b/ui/src/components/agents/PlanReview.tsx
@@ -1,198 +1,94 @@
-import "@blocknote/core/fonts/inter.css"
-import "@blocknote/mantine/style.css"
+import { useCallback, useEffect, useState } from "react"
-import { useCallback, useEffect, useMemo, useRef, useState } from "react"
-import * as Y from "yjs"
-import { WebsocketProvider } from "y-websocket"
+import type { PlanComment, PlanData } from "@/lib/plan"
import {
- CommentsExtension,
- DefaultThreadStoreAuth,
- YjsThreadStore,
-} from "@blocknote/core/comments"
-import { useCreateBlockNote } from "@blocknote/react"
-import { BlockNoteView } from "@blocknote/mantine"
-
-import type { HarvestedComment, PlanData } from "@/lib/plan"
-import { approvePlan, planCollabUrl, rejectPlan } from "@/lib/plan"
+ addPlanComment,
+ approvePlan,
+ deletePlanComment,
+ getPlanComments,
+ rejectPlan,
+} from "@/lib/plan"
import { Button } from "@/components/ui/button"
+import { Markdown } from "@/components/agents/ported"
import { useResolvedTheme } from "@/lib/theme"
-const CURSOR_COLORS = [
- "#e11d48",
- "#2563eb",
- "#059669",
- "#d97706",
- "#7c3aed",
- "#0891b2",
-]
-
-function colorFor(id: string): string {
- let hash = 0
- for (let i = 0; i < id.length; i++)
- hash = (hash * 31 + id.charCodeAt(i)) >>> 0
- return CURSOR_COLORS[hash % CURSOR_COLORS.length] ?? "#2563eb"
-}
-
-function bodyToText(body: unknown): string {
- if (typeof body === "string") return body
- if (!Array.isArray(body)) return ""
- const parts: Array = []
- for (const block of body) {
- const content = (block as { content?: unknown }).content
- if (typeof content === "string") parts.push(content)
- else if (Array.isArray(content))
- parts.push(
- content.map((node) => (node as { text?: string }).text ?? "").join("")
- )
- }
- return parts.join("\n").trim()
-}
+const POLL_MS = 4000
export function PlanReview({ plan }: { plan: PlanData }) {
const resolvedTheme = useResolvedTheme()
-
- const { doc, provider, threadStore } = useMemo(() => {
- const ydoc = new Y.Doc()
- const wsProvider = new WebsocketProvider(
- planCollabUrl(),
- plan.threadId,
- ydoc,
- { connect: true }
- )
- wsProvider.awareness.setLocalStateField("user", {
- name: plan.user.name,
- color: colorFor(plan.user.id),
- })
- // Everyone with read access edits the plan and comments alike — there's no
- // owner/commenter split for the document (only approval is owner-gated). The
- // collab WS already relays to any readable user, so this matches the server.
- const auth = new DefaultThreadStoreAuth(plan.user.id, "editor")
- const store = new YjsThreadStore(plan.user.id, ydoc.getMap("threads"), auth)
- return { doc: ydoc, provider: wsProvider, threadStore: store }
- }, [plan.threadId, plan.user.id, plan.user.name])
-
- // Defer teardown so React StrictMode's dev-only unmount→remount of the same
- // memoized provider doesn't destroy instances the remount reuses: the remount
- // cancels the pending destroy before it runs. A real unmount still tears down.
- const pendingDestroy = useRef | null>(null)
- useEffect(() => {
- if (pendingDestroy.current) {
- clearTimeout(pendingDestroy.current)
- pendingDestroy.current = null
- }
- return () => {
- pendingDestroy.current = setTimeout(() => {
- provider.destroy()
- doc.destroy()
- }, 0)
- }
- }, [provider, doc])
-
- // Track reviewers' display names from awareness so harvested comments are
- // attributed to people, not raw ids.
- const usersRef = useRef>({
- [plan.user.id]: plan.user.name,
- })
- useEffect(() => {
- const sync = () => {
- for (const state of provider.awareness.getStates().values()) {
- const user = (state as { user?: { name?: string } }).user
- if (user?.name) usersRef.current[user.name] = user.name
- }
- }
- provider.awareness.on("change", sync)
- sync()
- return () => provider.awareness.off("change", sync)
- }, [provider])
-
- const resolveUsers = useCallback(
- (userIds: Array) =>
- Promise.resolve(
- userIds.map((id) => ({
- id,
- username: usersRef.current[id] ?? id,
- avatarUrl: "",
- }))
- ),
- []
- )
-
- const editor = useCreateBlockNote(
- {
- collaboration: {
- provider,
- fragment: doc.getXmlFragment("blocknote"),
- user: { name: plan.user.name, color: colorFor(plan.user.id) },
- showCursorLabels: "activity",
- },
- extensions: [CommentsExtension({ threadStore, resolveUsers })],
- },
- [provider, threadStore, resolveUsers]
- )
-
- // Seed the shared document from the agent's plan markdown the first time
- // anyone opens an empty plan. The `seeded` flag + sync barrier prevent a
- // second reviewer from double-seeding.
- useEffect(() => {
- let cancelled = false
- const seed = async (isSynced: boolean) => {
- if (!isSynced || cancelled) return
- const meta = doc.getMap("meta")
- if (meta.get("seeded")) return
- const blocks = editor.document
- const empty =
- blocks.length === 0 ||
- (blocks.length === 1 && bodyToText(blocks) === "")
- if (!empty || !plan.markdown.trim()) return
- const parsed = await editor.tryParseMarkdownToBlocks(plan.markdown)
- // Re-check after the await: another reviewer may have seeded concurrently.
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (cancelled || !parsed.length || meta.get("seeded")) return
- editor.replaceBlocks(editor.document, parsed)
- meta.set("seeded", true)
- }
- provider.on("sync", seed)
- if (provider.synced) void seed(true)
- return () => {
- cancelled = true
- provider.off("sync", seed)
- }
- }, [editor, provider, doc, plan.markdown])
-
- const harvest = useCallback((): Array => {
- const out: Array = []
- for (const thread of threadStore.getThreads().values()) {
- for (const comment of thread.comments) {
- if (!comment.body) continue
- out.push({
- author: usersRef.current[comment.userId] ?? comment.userId,
- body: bodyToText(comment.body),
- resolved: thread.resolved,
- })
- }
- }
- return out
- }, [threadStore])
-
+ const [comments, setComments] = useState>([])
+ const [draft, setDraft] = useState("")
+ const [posting, setPosting] = useState(false)
const [decision, setDecision] = useState(null)
const [busy, setBusy] = useState<"approve" | "reject" | null>(null)
+ const [error, setError] = useState(null)
+
+ // Poll so reviewers see each other's comments without a realtime transport.
+ useEffect(() => {
+ let cancelled = false
+ const load = async () => {
+ try {
+ const next = await getPlanComments(plan.threadId)
+ if (!cancelled) setComments(next)
+ } catch {
+ /* transient; next tick retries */
+ }
+ }
+ void load()
+ const timer = setInterval(load, POLL_MS)
+ return () => {
+ cancelled = true
+ clearInterval(timer)
+ }
+ }, [plan.threadId])
+
+ const submitComment = useCallback(async () => {
+ const body = draft.trim()
+ if (!body) return
+ setPosting(true)
+ setError(null)
+ try {
+ const created = await addPlanComment(plan.threadId, body)
+ setComments((prev) => [...prev, created])
+ setDraft("")
+ } catch (e) {
+ setError((e as Error).message)
+ } finally {
+ setPosting(false)
+ }
+ }, [draft, plan.threadId])
+
+ const removeComment = useCallback(
+ async (id: string) => {
+ try {
+ await deletePlanComment(plan.threadId, id)
+ setComments((prev) => prev.filter((c) => c.id !== id))
+ } catch (e) {
+ setError((e as Error).message)
+ }
+ },
+ [plan.threadId]
+ )
+
const decide = useCallback(
async (kind: "approve" | "reject") => {
setBusy(kind)
+ setError(null)
try {
- const comments = harvest()
- if (kind === "approve") await approvePlan(plan.threadId, comments)
- else await rejectPlan(plan.threadId, comments)
+ if (kind === "approve") await approvePlan(plan.threadId)
+ else await rejectPlan(plan.threadId)
setDecision(
kind === "approve"
? "Plan approved — the agent is implementing it."
: "Changes requested — the agent is revising the plan."
)
+ } catch (e) {
+ setError((e as Error).message)
} finally {
setBusy(null)
}
},
- [harvest, plan.threadId]
+ [plan.threadId]
)
return (
@@ -209,7 +105,6 @@ export function PlanReview({ plan }: { plan: PlanData }) {
Reviewing as {plan.user.name}
{plan.isOwner ? " (owner)" : ""} · status:{" "}
{plan.status}
- {" · edit the plan, or select text to comment"}
@@ -240,16 +135,89 @@ export function PlanReview({ plan }: { plan: PlanData }) {
-
-
+
+
+
+ {plan.markdown.trim() ? (
+
+ ) : (
+
+ The plan hasn't been written yet.
+
+ )}
+
+
+
)
diff --git a/ui/src/lib/plan.ts b/ui/src/lib/plan.ts
index 84c8f2cd..106c2b43 100644
--- a/ui/src/lib/plan.ts
+++ b/ui/src/lib/plan.ts
@@ -1,9 +1,9 @@
/**
- * Client for the plan-review API + the Yjs collaboration endpoint.
+ * Client for the plan-review API — plain HTTP, no realtime transport.
*
- * The plan document and its comment threads sync over a y-websocket connection;
- * approve/reject post the client-harvested comments so the agent receives them
- * as the instruction for the follow-up run.
+ * The agent publishes the plan markdown; reviewers read it and leave
+ * whole-document comments. On approve/reject the server reads those comments and
+ * hands them to the agent as the instruction for the follow-up run.
*/
const API_BASE = (import.meta.env.VITE_DASHBOARD_API_BASE_URL ?? "").replace(
@@ -38,11 +38,12 @@ export interface PlanData {
user: PlanUser
}
-export interface HarvestedComment {
+export interface PlanComment {
+ id: string
author: string
+ author_login: string
body: string
- quote?: string
- resolved: boolean
+ created_at: string
}
export class PlanApiError extends Error {
@@ -83,29 +84,43 @@ export function getPlan(threadId: string): Promise {
return req(`/plan/${encodeURIComponent(threadId)}`)
}
-export function approvePlan(
+export async function getPlanComments(
+ threadId: string
+): Promise> {
+ const { comments } = await req<{ comments: Array }>(
+ `/plan/${encodeURIComponent(threadId)}/comments`
+ )
+ return comments
+}
+
+export function addPlanComment(
threadId: string,
- comments: Array
-): Promise<{ status: string }> {
+ body: string
+): Promise {
+ return req(`/plan/${encodeURIComponent(threadId)}/comments`, {
+ method: "POST",
+ body: JSON.stringify({ body }),
+ })
+}
+
+export function deletePlanComment(
+ threadId: string,
+ commentId: string
+): Promise<{ ok: boolean }> {
+ return req(
+ `/plan/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`,
+ { method: "DELETE" }
+ )
+}
+
+export function approvePlan(threadId: string): Promise<{ status: string }> {
return req(`/plan/${encodeURIComponent(threadId)}/approve`, {
method: "POST",
- body: JSON.stringify({ comments }),
})
}
-export function rejectPlan(
- threadId: string,
- comments: Array
-): Promise<{ status: string }> {
+export function rejectPlan(threadId: string): Promise<{ status: string }> {
return req(`/plan/${encodeURIComponent(threadId)}/reject`, {
method: "POST",
- body: JSON.stringify({ comments }),
})
}
-
-/** Base URL the y-websocket provider connects to; it appends `/`. */
-export function planCollabUrl(): string {
- const base = apiBase()
- const wsBase = base.replace(/^http/, "ws")
- return `${wsBase}/dashboard/api/plan/yjs`
-}
diff --git a/uv.lock b/uv.lock
index c474ae8b..23141150 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2010,8 +2010,6 @@ dependencies = [
{ name = "langgraph-sdk" },
{ name = "langsmith" },
{ name = "markdownify" },
- { name = "pycrdt" },
- { name = "pycrdt-websocket" },
{ name = "pyjwt" },
{ name = "uvicorn" },
]
@@ -2046,8 +2044,6 @@ requires-dist = [
{ name = "langgraph-sdk", specifier = ">=0.4.2" },
{ name = "langsmith", specifier = "==0.8.18" },
{ name = "markdownify", specifier = ">=1.2.2" },
- { name = "pycrdt", specifier = ">=0.14.1,<0.15" },
- { name = "pycrdt-websocket", specifier = ">=0.16.3,<0.17" },
{ name = "pygments", marker = "extra == 'dev'", specifier = ">=2.20.0" },
{ name = "pyjwt", specifier = ">=2.12.1" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
@@ -2485,104 +2481,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
-[[package]]
-name = "pycrdt"
-version = "0.14.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5d/4b/827ec39efadd52cf9dca9a663e7a5e215a4415a3ddd1b187536e21238f99/pycrdt-0.14.1.tar.gz", hash = "sha256:56e7c2d36407716f4977b5f3b972892c51758d31dafff9d043702b5c7fd1aa87", size = 95362, upload-time = "2026-06-17T14:18:04.784Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/2f/ed28aa8a47ad152e18e171176a049084f852c70c740a8378a4ade5740f4c/pycrdt-0.14.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:444a26e13c369d1e61062c523de3ea8532c6e699932a504cd84368434b761132", size = 1931105, upload-time = "2026-06-17T14:16:16.656Z" },
- { url = "https://files.pythonhosted.org/packages/c5/fe/effa6f6fab73473f2e86af459c69779da6d4de3a080b246f53ed78424418/pycrdt-0.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e9bc0aa1af974f207772230a8213c7e3da48b0d694d7849533936408e071e92", size = 1048952, upload-time = "2026-06-17T14:16:18.122Z" },
- { url = "https://files.pythonhosted.org/packages/19/07/119fdc7000c4465e73bf00d7dab498300fa3d1a900929ec07d0362e22fd0/pycrdt-0.14.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90142ecf27f0f318fed6049a84d48da8eb7d76bc031799be424e3501c3b45f6c", size = 1068708, upload-time = "2026-06-17T14:16:22.193Z" },
- { url = "https://files.pythonhosted.org/packages/77/61/7eea3f81cc70f24e6148c51b8dc24f3fdd288ce12c6808c1a3ebf1b1118d/pycrdt-0.14.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2cb83a5519a6421d6891b1ffbcd26245125aa9a66cec891c385d6f16c1a491", size = 1253482, upload-time = "2026-06-17T14:16:23.842Z" },
- { url = "https://files.pythonhosted.org/packages/3f/0e/8f6a7c21721af39da5df4c053444e4e17e1c28fa66c04b0ec1eb3db63164/pycrdt-0.14.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1068ca7466009058cc33ee0d65410c1a6c09dae13aa9471d22f8d0d9a586f466", size = 1103057, upload-time = "2026-06-17T14:16:25.193Z" },
- { url = "https://files.pythonhosted.org/packages/0c/73/43f9b51ab818ccf83d3a8a1ba0e4adb23701aab8ee8a97037ce9d916c746/pycrdt-0.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48d6845b72d88dfbbf1ff9927b5070ea09c7d4eec9b8c498eeb1b5083f514b79", size = 1074008, upload-time = "2026-06-17T14:16:26.827Z" },
- { url = "https://files.pythonhosted.org/packages/40/95/785ba888d26886e5f468760123d3119086f8de5ea760725eea3a00d3c285/pycrdt-0.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8a50a1a5a934d45414bffce04c2e447587a7031dd650b92100364b6b4807e26", size = 1171836, upload-time = "2026-06-17T14:16:28.365Z" },
- { url = "https://files.pythonhosted.org/packages/18/4e/407bd9fcb8520675c2fcc9047e0df53962a78f1be1e1baf91db7362364f7/pycrdt-0.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b35a8da35b6dc690088e78be9990e6628a5693ecbe80a0645d2be004dec469c7", size = 1223056, upload-time = "2026-06-17T14:16:29.881Z" },
- { url = "https://files.pythonhosted.org/packages/d7/c2/95ff0a5d390ee2d91eb953d0c7ee7fa4c7ce10dac21d3d6d895c9212781f/pycrdt-0.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9a4ce8e1646b01aba9c288b58b870a72e1fbbb287ce76636b73158ebf7434b47", size = 1341061, upload-time = "2026-06-17T14:16:31.445Z" },
- { url = "https://files.pythonhosted.org/packages/b8/cc/853994459b08ec21cfb1901c864707778036840514db4c088ee8f2e964e7/pycrdt-0.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bfa85fbe2cea468b23b79361acc45f810dd2e835859b0f3513796ab178cf027b", size = 1339013, upload-time = "2026-06-17T14:16:33.007Z" },
- { url = "https://files.pythonhosted.org/packages/18/10/f97044ae2d958d196d254ed9df6e72b6f4e34a7d3cdc9533827df3795ce8/pycrdt-0.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98e0d92ce0ad441dbbe943122c7891a43b34ceb6360f5cfde803e9c9da3272f4", size = 1286805, upload-time = "2026-06-17T14:16:35.077Z" },
- { url = "https://files.pythonhosted.org/packages/90/3d/2a74498c213660a4048c446000f7609d4046140271b84be007a8f6f36aa2/pycrdt-0.14.1-cp311-cp311-win32.whl", hash = "sha256:f140d6dba639b7af665ad178f1e8be7723ef616dd9887a75851a9fb2d1fd5053", size = 802987, upload-time = "2026-06-17T14:16:36.582Z" },
- { url = "https://files.pythonhosted.org/packages/cf/7c/c66dfcbf37f813ab7b983bc730c7c4dd44582803dee0fabf567b59cf6eef/pycrdt-0.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:c593d61e1ee90d23677883372b59cf7b5e188f6070d77eb80e67379dbd54b053", size = 865483, upload-time = "2026-06-17T14:16:39.599Z" },
- { url = "https://files.pythonhosted.org/packages/5d/d1/a61cb33ccaca7d5ede9f59bf16c4d4e254e57dbc1d306cf7c92cdeb56bf0/pycrdt-0.14.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35a3f4bbad8b5848c653997a9cb2bc8df4c6cad8aaea55e9261d23d71a574e55", size = 1920341, upload-time = "2026-06-17T14:16:41.184Z" },
- { url = "https://files.pythonhosted.org/packages/0d/01/abbf4134d7353690a6ebece3beace80ec581c67deb2e264957ec9da85241/pycrdt-0.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53ea32083193b8740c5409795087254dd01c4df65aca6edf1f83ceda3fc2980d", size = 1039888, upload-time = "2026-06-17T14:16:42.92Z" },
- { url = "https://files.pythonhosted.org/packages/2d/67/1adbb5acbc784200501851ec56d7d64b6110fa1e9b9e3dfd67c29ae1b737/pycrdt-0.14.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f301fee6960320a64075494e1ac25ba6da5e3984e126c13f9e3f890043cac0f", size = 1061004, upload-time = "2026-06-17T14:16:44.554Z" },
- { url = "https://files.pythonhosted.org/packages/2b/83/0e9b77b867f03cd5040b0508f05071c8903af43801e1232cfd13ff0e16c6/pycrdt-0.14.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b9c99f44b3d89e76cd2edda5fd0ad74e03dd02abbe1524feff939909ff29e91", size = 1246302, upload-time = "2026-06-17T14:16:46.586Z" },
- { url = "https://files.pythonhosted.org/packages/96/3a/956e915b3d9546869a7136564d1b9a122b354b45c1264efbc0ddb4ca31eb/pycrdt-0.14.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12ea055f21596320f00f181ec8d082049a4157703b07594ee4f6b5195e15d6c8", size = 1096588, upload-time = "2026-06-17T14:16:48.404Z" },
- { url = "https://files.pythonhosted.org/packages/77/53/b30626fdb8dc635728b2bc3860eb0ae6966fd96595215abe09efd7f978cc/pycrdt-0.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62219393c1c9aa60a00ac7a0da574746edd62f571a83aca05b1632ceabefa4db", size = 1069006, upload-time = "2026-06-17T14:16:50.032Z" },
- { url = "https://files.pythonhosted.org/packages/c6/7b/18efd6c541112c962515d1f181761d10953aaae3ab07083d6924af8a2f30/pycrdt-0.14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a6abaede60fb9d2acf0dd8ced85d0d328da7a5a03823a25eb3203acc56d3d6b", size = 1163604, upload-time = "2026-06-17T14:16:51.545Z" },
- { url = "https://files.pythonhosted.org/packages/d2/e8/e0299e9e8173dcf0f001ffc2ae6f8cf25b39419ef1db65b7e26623fc8b5a/pycrdt-0.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a669c1a3ffb621c5034ce08b562196fd1d2d02b138e2cfb39345dfc16bc9d59", size = 1214943, upload-time = "2026-06-17T14:16:53.239Z" },
- { url = "https://files.pythonhosted.org/packages/fa/b3/7aad8b7a9797ad98ea1b2d70021a44b01af4cbea2d0cc37beb17499dd994/pycrdt-0.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ca57f015894aa3b560d73b6f48d5455a61e69bcde30601295409924f47724195", size = 1337232, upload-time = "2026-06-17T14:16:54.874Z" },
- { url = "https://files.pythonhosted.org/packages/a6/22/ff4a93fc6b92f4173e11ce7ebce57f45b979d35da3a3e3a6899bdb9da362/pycrdt-0.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f253e6a9ac9e9487c7d69e078e8904918f1f356e6e9565564e36cefd44faa055", size = 1332373, upload-time = "2026-06-17T14:16:56.439Z" },
- { url = "https://files.pythonhosted.org/packages/c0/40/eae0b6d990577fa5b59145783d117d8984b1a9540d4ba037905f4824b9f2/pycrdt-0.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5a07a3c3b8424458793805abf9592d078dec454f9bc345e71e0d0fb788d21f", size = 1282048, upload-time = "2026-06-17T14:16:58.125Z" },
- { url = "https://files.pythonhosted.org/packages/0b/a9/e97e637ab9703cb4d951f3ff1e62641db0ca2517f842837598eaaf3667ff/pycrdt-0.14.1-cp312-cp312-win32.whl", hash = "sha256:a638ed29386eec9b202114cbab3f35ad89d8a44c7883d9ee84cf90761dd8ae52", size = 798254, upload-time = "2026-06-17T14:17:00.201Z" },
- { url = "https://files.pythonhosted.org/packages/e5/b2/5cdc49f8a021732a1a7070b8cbec2073ca5045fbf6c0b5718c3e283d254e/pycrdt-0.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7e6876eda749d79f15c2dcc10316f08f1f47756bb25631341a3314e7356ed41", size = 863716, upload-time = "2026-06-17T14:17:02.033Z" },
- { url = "https://files.pythonhosted.org/packages/a6/17/d970446b932d614445dca61739f592fdd328ed435dd54e67b5c804a31ef7/pycrdt-0.14.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5371deb5d40d3c9ddd5757adb5556ea8566b2fb46c1553ad313e9a5dfc84154e", size = 1919734, upload-time = "2026-06-17T14:17:03.806Z" },
- { url = "https://files.pythonhosted.org/packages/a6/d4/9ad8ee06131ee9a8b3366698bd68b39ebdc59764f68c436b61e4f18f7278/pycrdt-0.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbadc42dd0a6c3869859cf8900b59b21e8160e415a95e85d82e40cc11c887660", size = 1039641, upload-time = "2026-06-17T14:17:05.601Z" },
- { url = "https://files.pythonhosted.org/packages/66/5c/2828a1414478e1df6c51289962bf483a55377a888439d92fc8c6e6699f8d/pycrdt-0.14.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7770624c7003198312646cea57bb0c4d7a231ef57b1975f4487dd7d790fa1196", size = 1061111, upload-time = "2026-06-17T14:17:07.369Z" },
- { url = "https://files.pythonhosted.org/packages/c6/02/171aa04758453db0e976b3aaf87de7dbd8f3da6145d3d48d40dbf1073eac/pycrdt-0.14.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c348b780da2792d6cdc2e4dc237b6e8d7f87fe9b1fc2f24e467bf294f0f7581", size = 1246233, upload-time = "2026-06-17T14:17:08.988Z" },
- { url = "https://files.pythonhosted.org/packages/fb/d8/1f186f211ba1e4fffc8124d21ae9547f876c2c9c1e2d834919d5c5569f75/pycrdt-0.14.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d28a206a061cb44b284a53587561028547c3a12c4a12408bd0e499f887753a5", size = 1096827, upload-time = "2026-06-17T14:17:10.663Z" },
- { url = "https://files.pythonhosted.org/packages/a3/e5/5a027de04406e92282c29c3ec49e5a60c457255aac524cf8ed88064c124b/pycrdt-0.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35149ce7388788681c1d36115545b5c5ceda8d46982eb36f7ed0fab4379f8907", size = 1068703, upload-time = "2026-06-17T14:17:12.299Z" },
- { url = "https://files.pythonhosted.org/packages/01/08/2bc8005634e5f0c067cb9d942f0a2c628e88152f218a159a1e4aea51785f/pycrdt-0.14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b708985f4aaddeb7d7bf7888fe65f49c91fa0b36ed09a78a1041cb888cd64b50", size = 1162543, upload-time = "2026-06-17T14:17:13.985Z" },
- { url = "https://files.pythonhosted.org/packages/3d/53/b10ef7aaadfd24d74ca7d9894af9b4c925eaed5a4ee3a098728d0ac81bc1/pycrdt-0.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eeeee3284e9c9dc2dfcc45db7c55445e70f17bb12015583bd388f6c8f901237c", size = 1214684, upload-time = "2026-06-17T14:17:15.701Z" },
- { url = "https://files.pythonhosted.org/packages/10/9c/19af353a9b2d407cf942f8bec752dc94a7e82d40fb039be34db9a91eba6c/pycrdt-0.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4e715421a47702dbf81d90a32213f6868567e751926629f60912b03af397bd9a", size = 1337139, upload-time = "2026-06-17T14:17:17.647Z" },
- { url = "https://files.pythonhosted.org/packages/ac/a4/e1bf0c56a9b6bae6a89cda3ac7ad2ada525c827693f531435a14922b859f/pycrdt-0.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:34288b0a97ccc38e1e87f55448b1c08734deb9ad3360042d4653084bee6a4f80", size = 1331647, upload-time = "2026-06-17T14:17:19.301Z" },
- { url = "https://files.pythonhosted.org/packages/15/04/30f05d4f277f76bfefd7d856592922abaf1d17983c0b084aa347a96ad580/pycrdt-0.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07fecd0e9275e336e46f81203778ab811ee97c7272dfd7937b66844af6fcc13b", size = 1281303, upload-time = "2026-06-17T14:17:20.896Z" },
- { url = "https://files.pythonhosted.org/packages/da/5b/5b05f492e36f31b3e4be062675750b049ad86dbce64c5197dab660f70988/pycrdt-0.14.1-cp313-cp313-win32.whl", hash = "sha256:3b8324b7fb6fb31b2b55eb8dfeedf2e0a40d2e7143c6011d4b63ee102b44f3de", size = 798158, upload-time = "2026-06-17T14:17:22.736Z" },
- { url = "https://files.pythonhosted.org/packages/e7/06/e0f436b1b769dc8befd25e27dd133fb1690eba3e82734cd173aa95609340/pycrdt-0.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1bfb808acae8e97e5725086e334c4b5dc79d693962080abf1ab97e70d7400ba1", size = 863084, upload-time = "2026-06-17T14:17:24.437Z" },
- { url = "https://files.pythonhosted.org/packages/0e/22/4dec10d664f3f4d09e1142504ad354f993919c9a9ad9a7bb9d3b2eb277b2/pycrdt-0.14.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:008b67d78cc8a99a07c2bbe876a866c5d4ef7e3a90cf33463af7521fa5837a6c", size = 1931551, upload-time = "2026-06-17T14:17:26.19Z" },
- { url = "https://files.pythonhosted.org/packages/bd/ec/42e16b63a70b1907301945ea3a9e85cbfad0a4a1cfd669367e02f28a8e0f/pycrdt-0.14.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389c8dda4a41a1d6d664ab5c4c66d859e75d24add7e2f5df8ce6954effbe23c6", size = 1041602, upload-time = "2026-06-17T14:17:27.974Z" },
- { url = "https://files.pythonhosted.org/packages/89/76/ba53f5f3ebd5a104b431a4d3433dba9770bded5ece259e582530e289f89b/pycrdt-0.14.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b89eb0020711b2faaec41bd701af64b7c201ef039e781fe5fc945d148301fde", size = 1061937, upload-time = "2026-06-17T14:17:29.755Z" },
- { url = "https://files.pythonhosted.org/packages/88/86/6127c295d5c9fdfa13f61e569c2fb1cba3ce3bb1fbb7a912f14e3baac04a/pycrdt-0.14.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ba13fcb11665d05f3e3a80942dda42ff0d91a33f685909c2e90d0e57cacdad5", size = 1249379, upload-time = "2026-06-17T14:17:31.719Z" },
- { url = "https://files.pythonhosted.org/packages/46/63/fa06b2b8488788bf073d81fb8020eed5c8084465037c18478aaa91c06a8a/pycrdt-0.14.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a64e7a2e4ea5bc27f1675b64a3669c588adb6a8f2ed33a798d42d7d1f607845", size = 1099321, upload-time = "2026-06-17T14:17:33.547Z" },
- { url = "https://files.pythonhosted.org/packages/83/24/a19c5825f25135485527f2c83c4b4151ba6688a3fac7c7a0c8d20bd2b25f/pycrdt-0.14.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a153994b1f32ec6012d00ec645f80bf7e77ebea907759ee5fbe4e08a6f242bd7", size = 1069402, upload-time = "2026-06-17T14:17:35.295Z" },
- { url = "https://files.pythonhosted.org/packages/ab/0a/f91f8689d5abb8e0d3fa73ee71e52326185913f27ac0189223d0b7bad77c/pycrdt-0.14.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa6f02d8272737e734033a7ec62382d3929b2ab70681aac54b9d21373d78bf82", size = 1165406, upload-time = "2026-06-17T14:17:37.264Z" },
- { url = "https://files.pythonhosted.org/packages/b2/59/829f7aab8706e26fd5876ff37371359ff2930211b534570cd3d11305d6f6/pycrdt-0.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e83047e92cf7248c2d40d7be7a42fd7065598c3cc31f3d422def2886499787d6", size = 1216928, upload-time = "2026-06-17T14:17:39.232Z" },
- { url = "https://files.pythonhosted.org/packages/19/8f/a69823e4b02f1da3329d82316b2c733b1875c96e3aff5a2c1ebd8cefd1f8/pycrdt-0.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9d33a6dd2dab10a6380ce9bd7fee79f7e3b868214e95e977842adf9e54906fdf", size = 1337450, upload-time = "2026-06-17T14:17:40.991Z" },
- { url = "https://files.pythonhosted.org/packages/30/5d/040bbcaf59a0e590e3f89946afa611960d08f87c60367260a3ecf01c02de/pycrdt-0.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e347942c2ac71c1ac6ef04212fcd3eb8456f88053b71bf51a288084531c27135", size = 1334972, upload-time = "2026-06-17T14:17:42.6Z" },
- { url = "https://files.pythonhosted.org/packages/67/e4/be739b1301575b04e85eab7d76cb557a9abd167451d7442592e3f3d65b95/pycrdt-0.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cce1a249ab5b3d9757ceb42f22da51043c50f1d5c18b0673522b41a7311ce232", size = 1280543, upload-time = "2026-06-17T14:17:45.715Z" },
- { url = "https://files.pythonhosted.org/packages/18/06/1760c307048b431a195b41027faf48906dc8f22b8ee647817d6a15258c9c/pycrdt-0.14.1-cp314-cp314-win32.whl", hash = "sha256:fb93e7e277fc9b2cc26ce5b51d3e3b6436a762d0a0985ceaa50459ff6a33230f", size = 800359, upload-time = "2026-06-17T14:17:47.843Z" },
- { url = "https://files.pythonhosted.org/packages/5c/5c/ef3db9970413a661680972274f6b93f961130748e581a41c0c9960e29275/pycrdt-0.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:e3334b8f9fc8caeb41ac8a754adeb9dc640bb8fa1420c38cdb8dc2001a4777c8", size = 864167, upload-time = "2026-06-17T14:17:49.65Z" },
- { url = "https://files.pythonhosted.org/packages/a3/f0/33cf0c68c70e541e26a08b8d6eede09e26b134b87a9f825337b28b698206/pycrdt-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d814be8a83cf15238b7c5b8b4c6c775234f1254aea61b17cf9d71dea40d9ca8e", size = 1935886, upload-time = "2026-06-17T14:17:51.486Z" },
- { url = "https://files.pythonhosted.org/packages/45/7f/7fdd3f5fbff47233db93488766995db52fa80c9b73af33fe85d102d8da97/pycrdt-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:827943404994a71177742b56b47cde92ec97cc4de9c816efc2d19772416b22d9", size = 1050174, upload-time = "2026-06-17T14:17:53.397Z" },
- { url = "https://files.pythonhosted.org/packages/89/ad/b866000af356fef4f2f0271822698334bef7716166ed27a65de83ebdbb22/pycrdt-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7cc796cdea989f80057c07632667d44cacbe4c6001bd5d7910ef0487a5b971e", size = 1070987, upload-time = "2026-06-17T14:17:55.101Z" },
- { url = "https://files.pythonhosted.org/packages/4d/aa/5a93d3959e1cca6db70a10d0660bf6dee2deafac384c2b7e1171ff81198d/pycrdt-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd7b2e2e1b81e5fa5a550d835bee73b50cd48ccc999ed66b21900078d3e24027", size = 1261468, upload-time = "2026-06-17T14:17:57Z" },
- { url = "https://files.pythonhosted.org/packages/f0/10/3f6499c2fcc41fabc47d4e5fb1e292cc3f09e96a7b352685f6bfa09750b2/pycrdt-0.14.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c41f0487f2c913eb8c1835d0c386e322c482124e1b9b0b90d69976808501f05", size = 1104527, upload-time = "2026-06-17T14:17:59.074Z" },
- { url = "https://files.pythonhosted.org/packages/0d/1e/1e70d8050741d96f32e138b6adb7148f7813def4b8f2404bd6f280668515/pycrdt-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7dfef69c501bf15cc7f7b6056ef9cab9c91bfc5d4e86f4ea979240947a75c50", size = 1075181, upload-time = "2026-06-17T14:18:00.983Z" },
- { url = "https://files.pythonhosted.org/packages/36/49/871a8a110fef882fd1a4f330d8ae06a42f7cbe56b4258de810dbf5bb881c/pycrdt-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18ea987fd4346f28463fe80acf8c94658227ad631852e3419eb421bbd90a5f59", size = 1176188, upload-time = "2026-06-17T14:18:02.773Z" },
-]
-
-[[package]]
-name = "pycrdt-store"
-version = "0.1.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "pycrdt" },
- { name = "sqlite-anyio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ec/aa/e6d175db0106b72dcfe33d8cd718f9625e34c1d9cd0d3852052b75e9aab8/pycrdt_store-0.1.5.tar.gz", hash = "sha256:10cb0a71b86e167735235b3ab82816015ba746a2e660bcb133adb67a8d5c8818", size = 14845, upload-time = "2026-06-11T07:42:01.876Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/65/2edb1bb1cdce06df2aa2dcf617594344b5fc7f823d0770db2647e0306010/pycrdt_store-0.1.5-py3-none-any.whl", hash = "sha256:5ef6978d363be01275ffc337567072050363fa18fd6a51f0a48917dc210025f2", size = 11840, upload-time = "2026-06-11T07:42:00.827Z" },
-]
-
-[[package]]
-name = "pycrdt-websocket"
-version = "0.16.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "pycrdt" },
- { name = "pycrdt-store" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a4/0f/e9a9f3b298d6e6b6abacf8a41ea7f4f697aa8116215a15c8674fa1445f39/pycrdt_websocket-0.16.3.tar.gz", hash = "sha256:dd2723d531d9ca7048a67dd8d27c63e4362f5d909ca8c97efcfb2bd1d15d8d2a", size = 23822, upload-time = "2026-06-11T09:03:43.46Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a3/c6/c4176d2093bce6304e9f34470aae7f733c3818d3e7b54bf31892f1b3429e/pycrdt_websocket-0.16.3-py3-none-any.whl", hash = "sha256:dc3585705d3c2060b24ec329bb7c2065dae9dc50eb7d85854dcf0ed018958c4b", size = 14906, upload-time = "2026-06-11T09:03:42.355Z" },
-]
-
[[package]]
name = "pydantic"
version = "2.12.5"
@@ -3251,19 +3149,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
]
-[[package]]
-name = "sqlite-anyio"
-version = "0.2.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/20/a3/7830c95b37f1268dbb47e559d1f1ae027f3a4c36b1f7fc1b2dc5de1c5073/sqlite_anyio-0.2.8.tar.gz", hash = "sha256:d68b51a18c01a7dfa9cedbc319871ce77ab3ed0822518fb32810bb465b52d761", size = 3271, upload-time = "2026-03-02T10:37:43.466Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/aa/182981b92659df83c3eb7d6f8fb0874984d72ad688fa4054cb96bc044bb0/sqlite_anyio-0.2.8-py3-none-any.whl", hash = "sha256:bbdfefb144aed2633d2618ee1508edd3abe67a00389379360949da4671640d86", size = 4041, upload-time = "2026-03-02T10:37:42.246Z" },
-]
-
[[package]]
name = "sse-starlette"
version = "3.3.4"