Spaces:
Sleeping
Sleeping
Commit History
-
Merge pull request #19 from huggingface/public-share-links
Leandro von WerraUnverified signatureAnyone with the link: read a project without an account7ef4ebf -
Merge pull request #23 from huggingface/agent-comments
Leandro von WerraUnverified signatureCarry the tightened agent prompt onto main19beb97 -
Never show the collaborator list to a link visitor who happens to be signed in
Found while rebasing onto #18/#21/#22 and re-checking what a link reaches. The snapshot gated the collaborator list on `req.user` — "is anyone signed in?". That was right when the share cookie only ever carried anonymous visitors. It stopped being right when this branch let the grant carry a SIGNED-IN stranger too: they have a req.user, so they were handed sharedWith for a project they are not on. The test is whether they could open the project anyway, not whether they have an account. public_link was already gated on being the creator and was never affected. Smoke now asserts it from the stranger's side: they read the text, get an empty shared_with and a null public_link, and cannot enumerate the project's agents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>3a00d59 -
Merge pull request #20 from huggingface/prompt-tighten
Leandro von WerraUnverified signatureTighten the agent prompt: 10.6k characters down to 7.1k136a39b -
Keep origin_thread_id and anchor_kind in the tightened prompt
Agent Manager +1: Claude Opus 5The rebase onto agent-comments put the tightening on top of the origin-ID work, and the tightened text documented neither field. Both are live on this base: the server accepts origin_thread_id on /threads and /suggestions and delivers it plus anchor_kind on every mention. A prompt that never names them makes the capability unreachable — an agent cannot link what it writes back to the thread that asked for it, and reads a position anchor as the comment's subject rather than its location. The prompt contract test now asserts both, since presence of a field name in the server is not what makes it usable by an agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>c756f9b
-
Fix two prompt defects the presence test could not see
Both from review on #20, both real: 1. Every JSON example passed "as_agent": "@handle". agentIdentity compares that value against the BARE handle — with an agent key it reads as "this key belongs to someone else", and with an owner token it looks for a registered handle literally called "@handle". Every example in the prompt was a request that would be rejected. Introduced by the rewrite; the originals were bare. 2. The text said to pass mention_id exactly once, and then two examples carried it. An agent following the normal reply-plus-suggestion flow sent it twice. It now lives on the reply — the write an agent always makes — and the text says where to put it when there is no reply. The contract test checked that field names were present, which both defects survived. It now asserts the examples would actually run: the bare handle, and exactly one example closing the request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>97ca254 -
Tighten the agent prompt: 10.6k chars down to 7.1k
The prompt is the entire interface an agent gets, and it had grown by accretion. The length rule was stated three times, keeping the long-poll alive took fifteen lines of prose, and the page-structure section carried three curl examples for one endpoint. Every fact survives; the repetition does not. Structure now follows what an agent actually does: wait, read, work, respond — then reference sections for markdown, images, embeds and pages, which are looked up rather than read top to bottom. Two things said better than before: - One suggestion per edit, scoped to the blocks it touches — not a whole section rewritten to fix two sentences. - Its own comments, one per point, each anchored to the block it is about, so a human can close them one at a time. What you want changed still belongs in a suggestion, not only in a comment. Also pins the contract: a smoke test asserts the rendered prompt still explains every endpoint an agent needs (stream, dismiss, reply, threads, suggestio…2c2546f -
Close four holes in the share link, from review
1. A signed-in visitor could not use a link. authenticate() only consulted the share cookie when nobody was signed in, so a session made `user` truthy and the ACL check then refused them — while REST let the same request through. The page loaded and never synced. The grant now applies whenever the visitor lacks access of their own, signed in or not, and boot says which case it is so the client shows the read-only view to a signed-in stranger too. 2. The token stopped living in the URL. /p/<token> now sets the cookie and redirects to /d/<id>: a rendered page left the credential in the address bar, in history, and — since documents carry external images and links — in the Referer header sent to whatever host those point at. Referrer-Policy: no-referrer is set for the whole app as well. 3. Revoking reaches sockets that are already open. A viewer authenticated before the token changed stayed attached and kept receiving edits, so "revoked" meant "no new vi…830cb4d -
Hide the hover-comment button from link visitors too
It arrives with #15 (comment beside a paragraph, from a hover in the right margin). A visitor holding a read-only link must not be offered a composer whose write the socket then drops — the rule is harmless until that PR lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>a6671ed -
Anyone with the link: read a project without an account
A second way to share, next to inviting people by username: one long unguessable URL that opens the project read-only, for someone who has no Hugging Face account and should not need one. The token (24 random bytes) is exchanged for a cookie the moment the visitor lands on /p/<token>, so nothing downstream has to carry it: the REST snapshot, the websocket handshake and the <img> requests are all covered by the same grant. Lookup is constant-time, and an unknown or revoked token is a 404 that does not confirm the project exists. Read-only is enforced where it counts. The connection is marked readOnly in onAuthenticate, so hocuspocus drops any update or awareness message the client sends: a visitor who types sees their own keystrokes and nothing else does. REST keeps requireUser everywhere except GET of the project and its page tree, so a link cannot suggest, comment, add a page, re-share or delete. The non-editable editor and the hidden buttons are a courtesy on top of that, not the m…3a73292
-
Merge pull request #13 from huggingface/agent-comments
Leandro von WerraUnverified signatureAgents can open their own comments; suggestions preview on subpages7a5db07
-
Agents can open their own comments, and suggestions preview on subpages
Two halves of the same problem: giving feedback in small pieces. Agents could only reply into a thread a human had already opened — there was no way to leave several small remarks across a long page, so everything arrived as one long reply. POST /docs/:id/threads opens a thread: anchored with block_index (or an anchor pair from GET /docs/:id) to mark the text it is about, or with no anchor at all for a comment about the page as a whole. One call per point; each is resolved on its own. The agent prompt now says to prefer that over one wall of text, and to keep passing mention_id to close the request. A page-level thread needed no client work — cards come from the threads map and layoutMargin already stacks a card with no range after the anchored ones — but it now says "On this page", so it does not read as a comment whose highlight went missing. The subpage half: anchors are relative positions into a page's OWN fragment, and three places reached for 'default', which is home's. On any…94f8eb1
-
Merge pull request #21 from huggingface/feat/standalone-feedback
Leandro von WerraUnverified signatureMake agent feedback standalone and traceable3bbf68d
-
Keep feedback actions visible and mark comment ranges
Agent Managera54b6e7
-
Keep traceability after source comments close
Agent Manager7cdc17b -
Make agent feedback standalone and traceable
Agent Manager26dfc7b
-
Merge pull request #18 from huggingface/agent-privacy
Leandro von WerraUnverified signatureAn agent you register is not shared with your collaboratorsb622b57 -
Merge pull request #22 from huggingface/feat/default-user-agent
Leandro von WerraUnverified signatureGive every user a private built-in agent1552487
-
Close stdin for managed OpenCode runs
Agent Manager1126bdd -
Merge pull request #17 from thomwolf/agent/mobile-reflow
Thomas WolfUnverified signatureA phone could not read the document, only pan across it5762a8e -
Merge pull request #16 from thomwolf/sidebar-search
Thomas WolfUnverified signatureThe sidebar box searches page text, not just page titlesf9e13f4 -
Merge pull request #15 from thomwolf/agent/hover-position-comment
Thomas WolfUnverified signatureComment beside a paragraph, from a hover in the right margin5ad864d -
Merge pull request #14 from thomwolf/agent/reader-text-size
Thomas WolfUnverified signatureA reader text size next to zoom: the knob that does reflowdfe3711 -
Guard the invariant the two search walks rest on, and the review nits
The mutation testing in review found one real gap: nothing failed when docHitRanges stopped counting a character per non-text inline node, because no fixture paragraph had an atom before a hit. The new probe paragraph puts a mark and two formulas ahead of one, and asserts the contract directly — an atom is one character no query can match, so `wom`+formula+`bat` is not a hit. That kills the mutation in both walks (as a spurious mark in the document, as a count the sidebar never made). The second escaping mutation, docPos -> pos + 1 + off, is arithmetically identical given that invariant and cannot be killed while the invariant holds; break both and marks slide left by one per atom. Documented at the code rather than chased with a test. Also: lowercase each block once at index time instead of once per repaint; never let the read-only index path create a Yjs type (_structure and ghost pages return first); one history entry per hit click, on the open page as much as across pages; an out…064739c -
Tell a merged-away paragraph from a deleted one
Claude +1: Claude Opus 5 (1M context)Backspace at the start of an anchored paragraph joins it into the one above. The block boundary goes, so the anchor lands outside any text block exactly as it does for a real deletion — but every word survives one line up, and the card struck them through and said the paragraph had been deleted while pointing straight at them. orphaned no longer means "the anchor is not inside a block". When the position gives up, the text is asked instead: a merge leaves the remembered words in a block touching that boundary, a deletion does not. Comparing text would be the wrong tool for judging a live block, and is the right one here because it only runs once the position has already failed. Also: drop the hover button on scroll, since it travels with the text and ends up beside a paragraph the pointer has left; document that a table row anchors to its leftmost cell; and correct the comment claiming a mid-page delete reuses the surviving element — measured, it orphans like any other. Tests: the m…e5d08da -
A dragged table put the sideways pan back, and the tests could not see it
Review found the ≤840 block let one class of content overflow exactly the way this PR exists to prevent, and that the new tests were structurally unable to catch it. Both reproduced before changing anything. The comment at the table rules claimed "No min-width, deliberately". That is true of the stylesheet and false of the DOM: prosemirror-tables records a column drag as inline geometry — style="min-width:561px" on the table, style="width: 513px" on a <col> — and inline styles are stored in the document, so a drag on a desktop follows the reader onto their phone. Measured at 390 on the branch tip: a 562px table inside a 354px measure, with 190px of horizontal scroll on #editor-col (220px at 360). The reader pans across the row. Both declarations are needed. Measured, same repro: col { width: auto !important } 561px table, 189px scroll table { min-width: 0 !important } 562px table, 190px scroll both 354px table, 0px scroll both, !…a404034 -
Give the toolbar a row of its own when it stops fitting
Review of #14 found the pair pushed #header-tools from 721px to 818px and past its track on a 1280px window: the readout truncated to "15p", #text-larger gone, and #image-btn — which fits on main at that size — gone with it. The scrollbar is suppressed by design, so nothing said so. The reviewer suggested a breakpoint at ~1300px. Measuring says a breakpoint cannot do it: #doc-title is capped at 260px but a short one is ~66px, so the same window is ~180px better or worse off depending on the title. With a short title the branch stopped fitting at 1340px; with a full-length one at 1520px. main has the same bug and only wore it more quietly — it clipped 48px at 1200px, and 2px at 1440px with a long title. So it is measured, next to fitLayout() and asking the same question of the header that fitLayout() asks of the columns: if the toolbar does not fit beside the title and the actions, it takes a row of its own — the layout the 980px breakpoint already uses, now for its own reason. Nothin…f64215e -
Skip network discovery in managed OpenCode runs
Agent Manager2612d9d -
Skip OpenCode title generation in managed runs
Agent Manager4a68cda -
Make managed OpenCode startup deterministic
Agent Manager0d13486 -
Add safe diagnostics for managed agent runs
Agent Managerca40e67 -
Harden default agent isolation and capacity
Agent Manager889dff6 -
Add a private default agent for each user
Agent Manager314a59f -
An agent you register is not shared with your collaborators
Registering an agent stored no visibility flag, and every read treated a missing flag as shared: agentPresence() shared: info.shared !== false -> undefined means shared mention routing agents[h]?.shared !== false || ... -> undefined means shared So a handle was exposed the moment it was created: every collaborator on every doc you shared could see it in the agent list and @mention it into work. The toggle in the UI worked, but it defended a door that was already open — and the existing test set shared:false explicitly, so it never saw the default. Absent now means unshared, in both reads, and registration writes the flag down instead of leaving it to be inferred. Re-registering your own handle keeps the choice you made before. This matters because an agent is not a document permission: it runs on your machine, with your credentials, and does work in your name. Sharing it is a deliberate act — one click on the toggle that already exists. Behaviour change: existing hand…abaa17c -
A phone could not read the document, only pan across it
The sheet was a fixed-width page at every window size: 8.5in of paper with horizontal scroll below about 900px, and zoom as the only way to fit it. That works on a desktop, where the page is the point. On a handset it means the reader pans left and right to follow a line of text. Reflow the sheet into the screen, in two stages, so the change is never louder than the window forces: * ≤840px changes geometry only. The sheet goes fluid but the text column is still held at the 624px measure the page itself gives, so a 768px tablet lands on the same measure, the same 11pt Arial, and the same line breaks as a desktop — with the horizontal scroll simply gone. Crossing 840 does not re-break a single line. * ≤700px is where a handset cannot hold 624px at any margin, so the type gives too: 16px base, 18px minimum gutters, 354px of measure and ~42 characters a line at 390px. Zoom changes meaning across that line and now has two floors. Where the page is a page, 0.4 …29c066e -
The sidebar box searches page text, not just page titles
Filtering the tree by title only finds a page if you already remember what it is called. Every page of a project is a named fragment of the one Yjs document the tab already syncs, so the text to search is in memory before the first keystroke — no endpoint, no per-page fetch, and no permission logic to re-derive, since the socket handshake already decided access. The index is built from the live CRDT, so it follows a collaborator's typing. A match now shows its occurrences as snippets under the page, each labelled with the heading it sits under, and clicking one opens that page scrolled to that occurrence and no other. Hits in the open document are marked in amber — not the accent green, which on this page always means somebody said something about the text. The query lives in the URL (?q=&n=), so a search is a link you can send someone. Titles and slugs still match from the first character; searching text waits for a second one, or one letter highlights half the prose. Co-Authored-B…8c82c03 -
Comment beside a paragraph, from a hover in the right margin
Claude +1: Claude Opus 5 (1M context)Hover the right third of the page and a small comment button appears out in the page's own right margin, level with the paragraph you are next to. Click it and you get a comment anchored to that place, with no selected span. The anchor is one Yjs relative position taken inside that paragraph — the same machinery the existing span comments use, written into both anchor fields so every path that already resolves a thread's anchors keeps working and simply sees an empty range. The card shows the paragraph's opening words as a locator instead of a quote, because there is nothing quoted; agents get anchor_kind + anchor_context so a null excerpt is not mistaken for deleted text. Deleting the paragraph never closes the thread. The zone is hover-only and invisible: no gutter, nothing while a selection or a composer owns the flow, and nothing at all on touch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>f9ca812 -
A reader text size next to zoom: the knob that does reflow
Zoom scales the sheet and the type by one factor, so the words-per-line never moves — deliberately, because a page that reflowed would not be a zoom. This adds the other control a reader actually wants: --doc-text-scale multiplies the type and nothing else, so the page keeps the width it has on screen and the lines re-break. A Kindle's A/A, next to the magnifiers. The stored value is a factor rather than a size, so it rides on top of whatever the document style asks for: one step up in Docs stays one step up after switching to Reading, and each style still decides what 100% means for it (14.6667px and 16px). An absolute size would have had to either lose the reader's choice on a style switch or overrule the style's own typography. Persisted as doc-text-scale, alongside doc-zoom and cw-doc-style. Left out of the print path on purpose: it is one reader's comfort setting in one browser, and a PDF is handed to other people. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.c…f6cf611
-
Merge pull request #12 from huggingface/mention-watcher
Leandro von WerraUnverified signatureFix: a page-scoped websocket name could silence a project's @mentionsedcad24 -
A page-scoped websocket name could silence a project's @mentions
Pages became fields of one project document, but a page-scoped document name (`<id>::<slug>`) was still accepted on the websocket. Syncing one loaded a SECOND instance of the same project, and because watchers were keyed by project id, attaching its watcher took the observer off the instance every browser was editing. From then on @mentions in that project were never detected: the comment was stored and looked posted, but no chip, no queue entry, no agent — until the process restarted. - watchers are keyed by the document instance, so no instance can unhook another's observer (releaseWatcher still scopes to one project) - the websocket refuses a page-scoped name outright: it can only ever sync an isolated replica whose edits reach nobody - state loads under the project id, so a replica can't start from the pre-migration per-page row (frozen before every page added since) - only the canonical instance may persist, so a partial replica cannot write over what the real instance ho…ab8524b
-
Merge pull request #11 from huggingface/bugfixes
Leandro von WerraUnverified signatureFour fixes: agent subpages, the pending selection, overlapping marks, the starc19e556 -
Four fixes: agent subpages, the pending selection, overlapping marks, the star
AN AGENT'S SUBPAGE LANDED AT THE TOP LEVEL. Placement worked only for an exact ref — a page's slug, or a group's quoted path array. But an agent works from what it can see, and what it sees is a TITLE: sending "Research Notes" for the page research-notes, or 'Getting started' for the group '"Getting started"', was a 400, so a proposal made without placement went to the top level. Parents now resolve from a slug, a page title (any case), a group label with or without its quotes, or a one-element array holding any of those; parent_slug/parent_page/under work as field names. Exact refs still win before any guessing. The response and GET .../page-suggestions/:id both echo the resolved parent and index, so a caller can see its intent was understood, and an unknown parent is refused with the real page list attached. THE TEXT YOU WERE COMMENTING ON WENT UNMARKED. Focusing the composer moves focus out of the editor, which drops the browser's own selection highlight — so the words you had pick…63bc919 -
Merge pull request #10 from huggingface/download-export
Thomas WolfUnverified signatureDownload: this page or the project, as markdown or PDF18e535a -
Download: this page or the project, as markdown or PDF
A Download button in the header opens two choices — how much (this page / whole project) and in what shape (markdown / PDF). MARKDOWN is a zip built server-side: one .md per page in sidebar order, plus an assets/ folder holding the images those pages use, with every /files/<hash> reference rewritten to its bundled copy. That rewrite is the point — a /files URL only resolves for someone signed in to this Space, so without it an exported document renders as broken images anywhere else. Project scope also carries structure.yaml, since the yaml IS the project's shape (order, nesting, group headers) and without it an unzipped project is a flat pile of files. An image whose bytes have gone missing is listed in assets/MISSING.txt rather than failing the whole export. The zip is written by server/zip.js, ~100 lines over node's zlib, rather than by adding a dependency: the Space builds with `npm ci`, which hard-fails on any package.json/package-lock disagreement, and this is not worth that ri…3b2fb14 -
Merge pull request #9 from huggingface/proposals-in-place
Leandro von WerraUnverified signatureProposed pages live in the tree, where they would landb92c908 -
Proposed pages live in the tree, where they would land
Three things, all about the page tree. AGENTS COULD NOT PROPOSE A PLACE. page-suggestions took a title and a body and nothing else, so an accepted page was appended at the top level wherever the agent meant it to go — and there was no way to say otherwise. Proposals now take the same {parent, index} refs /structure/move already uses: a page parent is its slug, a group parent is its path array, no parent means top level. The placement rides on the proposal, so accepting files the page there via moveNodesInYaml. An unknown parent is a 400 (it used to be accepted and quietly flattened), and the agent instructions in api.js document the field with examples. PROPOSALS SAT IN A LIST UNDERNEATH EVERYTHING. They were rendered after the tree and the unfiled section, under a "proposed" heading, which hid the one thing a reviewer wants to know: where is this page going? They are now grafted into the tree at their proposed position and rendered as ordinary rows — dashed, badged, still opening th…aeb6b2b -
Merge pull request #8 from thomwolf/agent/sidebar-groups
Leandro von WerraUnverified signatureSidebar groups, collapse carets, and drag-and-drop2373fd4 -
Make groups draggable again
The interactive-control drag guard swallowed pointerdowns on group labels, which are <button>s — pages kept dragging because their labels are anchors. Controls can now opt back in with data-td-drag, and the group label (the group's whole hit area) does. The e2e now drags groups directly — empty, with subtree, in and out of a page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>66103fc
-
Left carets, home star, project subtitles, empty-group rename fix
Fold carets move to the left of the label (aligned spacer for leaf rows); the hover trash overlays the row end so long titles run to the edge. A muted star marks the home page. Renaming a group with no children no longer loses its input — every group-label single-click consequence now defers behind the same 280ms timer the double-click cancels. The home page's first paragraph becomes the project's subtitle (derived on store, persisted on the registry) and previews under each title in the project dropdown. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>3f3e761 -
Dialog-free UI, project-name header, rl-wiki sidebar polish
The Space runs in an iframe where Chrome suppresses native dialogs, so confirm()/alert()/prompt() silently did nothing — deletes looked broken. All 33 call sites are gone: sidebar deletes/dissolves are now arm-to-confirm buttons with toasts for errors; everything else goes through new in-app uiConfirm/uiAlert/uiPrompt/uiToast helpers (common.js, keyboard-complete, dark-mode aware). The browser tests now fail on any native dialog. The top-left selector always shows the project name (tab becomes 'page · project'), server-rendered too, live while renaming home. Sidebar: exact rl-wiki type scale and row treatments, search replaces the 'Pages' heading, trash sits at the far right of rows, and the bar is a seamless full-height surface — page background, no right border, reaching the writing sheet. treedrag no longer arms drags from buttons/inputs, so control clicks can't be swallowed. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus <noreply@anthropic.com> Co-Authore…d3283b8