UI polish: header start-editing button, avatar initials, icon updates - #8
Closed
alcor wants to merge 92 commits into
Closed
UI polish: header start-editing button, avatar initials, icon updates#8alcor wants to merge 92 commits into
alcor wants to merge 92 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address review: y-markdown's delimiter map now reuses the shared DELIMITERS export from critic-constants.ts instead of duplicating the strings, and blockText nests all active mark types on a run (highlight outermost) instead of rendering only the first one, so overlapping criticHighlight + criticAddition/Deletion/Comment marks no longer lose a delimiter pair silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds mint/verify/revoke/list for per-document agent tokens: app/lib/agent-tokens.ts generates and hashes tokens (SHA-256), and DocumentAgent gains an agent_tokens SQLite table plus mintAgentToken/getAgentRoster/revokeAgentToken/verifyAgentToken RPC methods. Extends the integration test's sql mock with a generic in-memory table store for tables beyond doc_state, reusable by later agent tasks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alarm() only deleted doc_state, so tokens minted before a document expired stayed valid against whatever content later landed at the same doc id. Clear agent_tokens alongside doc_state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolveAnchor's nearest-index heuristic can resolve `to` below `from`
after duplicate-hash blocks or concurrent edits, which made deleteBlocks
pass a negative length that Yjs silently ignores while the insert still
ran -- degrading agentReplace into a pure insert that returned { ok: true }
despite the document diverging from the request. Reject inverted ranges
with a stale_anchor error before mutating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DocumentAgent's mutation RPCs (agentInsert/agentReplace/agentSuggest) now share a private applyMutation() for direct Yjs application. A pace of natural/fast with at least one connected human enqueues the mutation into a `performances` table instead of applying it instantly; a setTimeout-chain runner types insert/suggest text out via app/lib/performance-chunks.ts chunks so connected clients see it appear incrementally, while replace still applies atomically once dequeued. Anchors are re-resolved at dequeue time (not enqueue time) so a mutation gone stale while queued is simply dropped. Leftover queue rows are applied instantly on the next ensureInitialised() (eviction recovery), and the alarm handler now also purges the performances table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rmance engine Code review found two Important issues in the Task 6 performance queue: 1. performTypedInsert/performTypedSuggest captured a block index (or text position) once and kept reusing it across every `await sleep(...)` tick, so a concurrent pace:"instant" mutation (which bypasses the queue) could shift the document under them, landing later writes at a stale spot. Fixed by claiming the target slot synchronously (zero yield before the first write), then tracking the write position from there on via a Y.RelativePosition, which stays correct across concurrent structural edits and aborts cleanly if it stops resolving. 2. Eviction recovery re-applied a queued mutation's full original text even if some ticks had already landed, duplicating what was typed. Fixed by deleting a performance's `performances` row at the moment its first write lands (the same synchronous-claim moment from fix 1) instead of at completion — from that instant the typed content is already part of the Yjs doc and persisted normally, so an eviction mid-typing now loses only the untyped tail instead of duplicating anything. Added a covering test that reproduces fix 1 (an anchored typed insert vs. a concurrent instant insert on the same anchor) — verified it fails against the pre-fix code and passes against the fix. Redesigned the eviction recovery test to use two queued mutations so it genuinely exercises the pre-first-write case under the new claim-then-delete semantics. Tightened the natural-pace typing test's partial-content assertion to a bounded timer advance instead of an assertion that also passes for a fully atomic implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gives agents a synthetic Yjs awareness client so humans see them in the presence stack and caret while they work. app/lib/agent-awareness.ts hand-encodes MSG_AWARENESS frames for a stable per-name synthetic clientId; DocumentAgent tracks join/leave/idle state in a new agentPresence map, broadcasting on every change and replaying it to late joiners in onConnect. onPerformanceCursor now sources the caret position from the performance engine's live Y.RelativePosition tracking (the text node + current offset) instead of the frozen block index it was called with before, so it stays correct under concurrent edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an `events` table (seq/type/payload/created_at) plus `agentAwaitEvents` long-polling for it. Human-origin Yjs transactions are scanned via frag.observeDeep for @mentions of roster agents (with a 30s-capped doc_changed digest), and a threads-map observer notifies an agent when a human replies to its thread. Agent-originated mutations are tagged with a Yjs "agent" transaction origin so they're excluded from both detectors — this also covers onRequest's initial content/threads import, which isn't a live human edit. agentComment/agentReply were missing from the RPC surface (deferred from Task 5) and are needed to produce thread_reply events, so they're added here too: agentComment creates a ThreadData entry (requires `comment`), agentReply appends to one (doc_not_found for an unknown thread id). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rload - thread_reply now fires only when a human-origin threads-map edit actually grows replies.length (via event.changes.keys' oldValue), not on any edit to an agent-authored thread (resolve toggles, re-saves). - agentComment/agentReply now call checkRateLimit like the other mutation RPCs, instead of bypassing the agent's rate-limit budget. - Added "thread_not_found" to the closed AgentErrorCode union and use it from agentReply instead of overloading "doc_not_found". Also replaces the fake-timer tests' single-flush workaround (flushMicrotasks) with waitForTimerRegistered, which polls vi.getTimerCount() instead of guessing a fixed number of setImmediate ticks — the single flush was an intermittent hang under full-suite load (verifyAgentToken's real crypto.subtle.digest call didn't always resolve in one tick before vi.advanceTimersByTimeAsync raced ahead of the long-poll's setTimeout ever being registered). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a VaporMcp Durable Object that exposes the agent tool surface over streamable HTTP at /mcp. The tool table lives in agents/mcp-tools.ts, which imports nothing from the `agents` package so it stays testable in plain Vitest; agents/mcp.ts wires it to DocumentAgent stubs and adds create_document (no token, needs env). The bearer token from the Authorization header rides to the DO as ctx.props; DocumentAgent remains the only thing that validates it, and every tool — errors included — returns its result as JSON text content. Drops baseUrl from tsconfig.cloudflare.json: it resolved "agents/mcp" to this repo's own agents/ directory instead of the npm package. A paths entry keeps the package's internal types nameable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds GET /:id.md for a document's raw markdown (public, no token, backed by a new DocumentAgent.exportMarkdown RPC) and a GET /mcp help page shown to browsers (Accept: text/html) instead of a protocol error, with connection snippets for Claude Code, claude.ai, and generic MCP clients. The two handlers live in workers/routes.ts as pure functions that avoid importing the `agents` package, so they unit-test in plain vitest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restricts the origin interpolated into the /mcp help page to a strict http(s) allowlist, falling back to the default origin otherwise, since it derives from the client-controlled Host header and was being spliced unescaped into raw HTML/JSON. Also adds X-Content-Type-Options: nosniff to the new GET /:id.md response, since it serves raw user content at a public URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the /:id/agents resource route (GET roster, POST mint/revoke) backed by DocumentAgent's mintAgentToken/getAgentRoster/revokeAgentToken RPCs, with server-side capability validation since the route is the untyped boundary. Wires an InviteAgentDialog into the doc header: a form with a pre-filled unused name suggestion, capability switches (suggest+comment on, write off by default), a one-time token screen with copy-ready Claude Code/claude.ai/mcpServers snippets, and a live roster list with revoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds role="dialog"/aria-modal/aria-labelledby, Escape-to-close, a backdrop-click-only close (ignoring bubbled clicks from the panel), and minimal focus management (focus the name input on open, return focus to the invoking button on close). No dialog primitive exists in this codebase to build on, so this is the manual minimal version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add redirectHost() to workers/routes.ts that 301-redirects vpr.fyi, www.vpr.fyi, vaporware.fyi, www.vaporware.fyi, www.vapor.fyi to https://vapor.fyi while preserving path and query - Call redirectHost() FIRST in workers/app.ts fetch handler before MCP help - Add vpr.fyi and vaporware.fyi custom domain routes to wrangler.jsonc - Add comprehensive tests for all redirect scenarios and edge cases Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documents moved from /docs/:id to /:id, but vapor.fyi is live and documents last 99 hours, so links shared before the move are still being opened — and 404ing. Add redirectLegacyDocPath, a pure handler alongside redirectHost: it 301s GET /docs/:id to /:id and GET /docs/:id.md to /:id.md, preserving the query string and using a path-relative Location so the redirect stays on whichever host served it. Wired into workers/app.ts ahead of routeAgentRequest and React Router. Also note that redirectHost's www.* entries only fire if DNS is later pointed at this worker — they aren't registered routes today. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RESERVED_SLUGS was declared but never read, and two spec requirements around it were unimplemented. Add the missing ".well-known" entry and an isReservedSlug helper, then use it in both places the spec calls for: generateDocumentId re-rolls a candidate that collides with a reserved slug, and the /:id loader 404s reserved names explicitly before the id-shape check, without resolving a Durable Object stub. Also add MAX_AGENTS_PER_DOC alongside the list, for the roster cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parseCriticMarkupToContent throws on CriticMarkup substitution, and four agent paths called it unguarded on arbitrary MCP input. Worst was replace: deleteBlocks and the insert shared one Yjs transaction, and Yjs has no rollback, so a parse throw after the delete committed the delete and lost the replaced blocks. The throw also escaped the RPC as an exception instead of a typed error, wedged the performance queue with isPerforming stuck true, and could abort ensureInitialised mid-flight — leaving the Yjs observers unregistered (no mentions or events for the life of the instance) and a leftover performances row alive to collide with an id counter that restarts at 1. - critic-parser: add tryParseCriticMarkup, returning a result value. parseCriticMarkupToContent stays as a throwing wrapper for the document-import path, which catches it into a 400. - y-markdown: replace insertMarkdownBlocks with buildMarkdownBlocks (parse to detached nodes, no document touched) plus insertBlockNodes, so callers can validate before opening a transaction. - document: add the unsupported_markup error code and return it from dispatchMutation, before the instant/queued fork, so an agent gets the same typed error at any pace and nothing unparseable is ever persisted. applyMutation and performTypedInsert re-check as a backstop for rows written by older builds. - runPerformances clears isPerforming in a finally and drops a failing mutation (row included) so the queue drains past it; eviction recovery applies each leftover row in its own try/catch. - Guard the unguarded JSON.parse sites: agentRead skips an unparseable thread, agentReply returns thread_not_found for one, and a corrupt rate-limit log is treated as empty (it is rewritten on that same check). The integration test's SQL fake now enforces the PRIMARY KEY on performances.id and UNIQUE on agent_tokens.name, so the id-collision class of bug fails a test instead of passing silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three problems in the document event log, all in the same observer or its reader. Mentions only ever fired on paste. The observer matched findMentions against each individual delta op, and a human typing arrives one character per op, so "@scribe" never matched — the existing test inserted the whole string at once and masked it. Scan the containing block's full text instead (findBlockTextForXmlText already resolves it), de-duplicated per (block text node, agent name) so later keystrokes in the same block don't re-fire. A name is forgotten as soon as it leaves the block's text, so deleting and retyping a mention notifies again. The map is a WeakMap keyed by the live text node, so it needs no explicit clearing. doc_changed digests were recorded above the roster check, so every document with no agents on it accrued events rows nobody could read. Record the digest only once at least one agent is on the roster. agentAwaitEvents filtered on seq alone, so every agent received every other agent's mentions and thread replies. Filter both types to the calling agent; doc_changed stays broadcast. The returned cursor now advances to the highest row scanned rather than the last row returned, so an agent never re-scans notifications addressed to someone else. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
create_document reaches the same document store as POST /new but applied none of its input guards, so an MCP client could create a document from arbitrary-size or binary content. Add the same 1MB ceiling and NUL-byte check, as validateNewDocumentMarkdown in mcp-tools.ts — agents/mcp.ts can't be imported in plain Vitest, so the guard lives where it can be tested directly and is called from the tool. mintAgentToken had no roster ceiling either. A document is a public, unauthenticated URL, so cap it at MAX_AGENTS_PER_DOC (16) and return a rate_limited error naming the cap. Revoking an agent frees a slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cut README back to plain facts
- Comment/highlight marks use a solid 3px canary border-bottom instead of text-decoration, which fell back to a black currentColor bar when the shorthand's variable failed to resolve - Edit/Suggest becomes a header menu with Accept all / Reject all - Top-right header menu consolidates connection status, Agents, the account row (Google sign-in or name + sign-out), and a one-row light/dark/auto theme switcher - Comment threads restyled: avatar with stacked name/date, resolve check and overflow menu in the header, always-visible rounded reply input - Material Symbols Outlined (subset) for icon buttons - New vapor mark stored at public/logo.png; favicon.ico, favicon-32, apple-touch-icon, and og:image generated from it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Start Editing deletes onboarding comments for good: the 3s fallback timer in useThreads now re-scans the document before recreating a thread, so marks cleared in the meantime stay gone - Drop Accept/Reject buttons from the right column (menu has Accept all / Reject all; the bubble toolbar covers single suggestions) - Drop the comments header (count, + Add) and the empty state - Preview moves under the mode menu alongside Edit and Suggest; the big sidebar Preview button is gone (mobile keeps its tab) - Drop the border between the editor and the comment rail Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment cards no longer repeat the highlight text; selecting a thread scrolls the editor to center its highlight. The scroll targets the active decoration right after the dispatch that renders it, instantly — a smooth scroll was silently canceled by subsequent frames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Block ids replace content-hash anchors (hashes demote to staleness checks), the schema is constrained to markdown-complete forms (underline dropped), the serializer must be deterministic, and a cold-store projection section fixes the future database as derived, never authoritative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Button/Menu/Input/Toolbar wrappers over @base-ui/react plus cn(); ModeMenu, ShareButton, and ThemeSelector move to the kit Menu; Radix dropdown-menu and switch dependencies removed. Semantic accent, destructive, and primary tokens with dark/auto overrides; squircle utility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Heading scale, paragraph and list rhythm, blockquote/code/pre/hr treatments, task-list and table CSS (dormant until the rich schema lands), and the empty-document placeholder rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Yjs fragment now holds real StarterKit nodes (headings, lists,
quotes, code blocks, rules) instead of flat paragraphs of literal
markdown syntax. One serialization module (app/shared/rich-markdown.ts,
prosemirror-markdown + markdown-it with CriticMarkup inline rules) is
shared by the client editor and the DocumentAgent; y-markdown and the
critic parser/serializer/decoration layer are deleted.
- Blocks carry persistent ids (BlockId extension client-side, minted
server-side on agent inserts); anchors become {id}-{hash} with the
hash as a staleness check (new stale_block error), legacy b{n}-{hash}
anchors still resolve by content
- Critic delimiters no longer render — suggestions and highlights read
through mark styling; comment text hides inline behind its point
marker (clean view retired with nothing left to hide)
- Preview becomes the Markdown source view (the editor is the render)
- Typing engine goes block-structured: multi-block inserts animate
block by block, styled runs style while typing, natural pace retuned
to ~10 chars/s
- Markdown-looking plain-text pastes parse to rich nodes
- CriticMarkup substitution now degrades to literal text instead of
rejecting the mutation
- TipTap family aligned on 3.30.5 (3.30.6 is part-published upstream);
marked/dompurify/sugar-high dropped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Format (B/I/S/code + block types), Lists, and Insert (link dialog, divider, code block) menus in the document header, dimming while the editor is unfocused; icon subset extended; the home page's curl snippet drops its markdown-source costume. Known limitation, tracked in the plan: formatting commands in suggest mode apply directly rather than as tracked changes (typing and deleting remain tracked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The newer wrangler's generated Env shape ignored the old Cloudflare.Env augmentation, so CI (which has no .dev.vars) lost SESSION_SECRET. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Client: an idle (10min) or hidden (60s) tab disconnects its WebSocket so it stops pinning the document's Durable Object, and reconnects instantly on activity — the Yjs provider now re-syncs on every socket re-open, not just the first. Connection status returns to the header with Connected / Connecting / Reconnecting / Sleeping / Offline states. Server: await_events long-poll capped at 15s with a retryAfterMs pacing hint on empty results; typed performances get a 10s wall-clock budget (remainder applies instantly); doc persistence debounces to a 1s quiet edge (flushing on creation and last-disconnect) instead of writing full state per keystroke; the last connection closing also clears agent idle timers and presence so no standing timer pins the DO. Implements phases 1-2 of docs/plans/2026-08-31-sleeping-tabs-plan.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/do-usage.mjs reads DO active time and requests from the GraphQL Analytics API and reports each day against the free-tier budgets, warning at 70% and failing at 100%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tool menus sit left after the wordmark, then Edit, Share, and the doc id/expiry; connection status and the account menu move right of a flexible gap. Dev server binds all interfaces and allows .ts.net hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Notes-app import: WYSIWYG editor, formatting toolbar, UI kit, styles
Sleeping tabs plan + DO duration cost analysis
Named distinctly because an exported CLOUDFLARE_API_TOKEN shadows wrangler's OAuth login; the old name remains a fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TTL grants run to the document's remaining lifetime instead of 24h, and suspension requires sustained (>=1h) failure instead of five consecutive misses — a wake-on-webhook agent has no refresh daemon, and a lapsed subscription is exactly what would have woken it. Also adds the server-instructions update to the docs task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One protocol-agnostic core (agents/events.ts: catalog, cursors, Standard Webhooks signing, subscription ids, TTL policy) skinned three ways per the plan: - events/list|poll|subscribe|unsubscribe as spec-shaped JSON-RPC methods on VaporMcp, with the draft capability declared under experimental and the draft date tagged in _meta - events_* tool mirrors so today's clients can subscribe; await_events deprecated in favor of events_poll - a webhook dispatcher in DocumentAgent: subscriptions stored in the doc's own SQLite (dying with its 99h expiry), Standard-Webhooks signed POSTs off the hot path, retry then sustained-failure (>=1h) suspension, lazy TTL expiry, HTTPS-only with private-network rejection, and OAuth-door-only registration (-32012 for anonymous) TTL grants run to the document's remaining lifetime so set-and-forget subscribers never need a refresh daemon. Server instructions now teach the subscribe-over-poll norm; /mcp help page documents the surface. Fixes A-195 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verifies Standard Webhooks deliveries and forwards the event body to a Claude routine's /fire endpoint. Holds only the whsec and the routine-scoped fire token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MCP Events polyfill: plan + implementation
…y updates - Home: new tagline and meta description covering agent collaboration, plus an MCP connect command encouraging agents to iterate in the browser - Move the start-editing marquee button into the top toolbar - Add Avatar component with initials placeholder circles; use it in thread panels and the header menu (top-right avatar now 32x32) - Hide comment resolve/menu icons until hover - Auto theme now uses the laptop (computer) icon - Material Symbols weight set to 300 - Share menu items get link/download icons Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A batch of small UI fixes:
claude mcp addcommand block pitching vapor as a place for Claude and other agents to iterate in the browser. Meta description updated to match.Avatarcomponent renders placeholder circles with initials when there's no photo or animal glyph; used in thread panels and the header menu.computer(laptop) icon instead ofbrightness_auto.+computer,+download,-brightness_auto).Typecheck, lint, and all 458 tests pass.
🤖 Generated with Claude Code