From f5a2e56040d06f935bee79c5e9132df534bd14e2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 3 Aug 2026 20:00:26 -0700 Subject: [PATCH 01/10] blog(agent-as-yjs-peer): draft post on streaming an agent into a collaborative doc as a Yjs peer --- .../content/blog/agent-as-yjs-peer/index.mdx | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 apps/sim/content/blog/agent-as-yjs-peer/index.mdx diff --git a/apps/sim/content/blog/agent-as-yjs-peer/index.mdx b/apps/sim/content/blog/agent-as-yjs-peer/index.mdx new file mode 100644 index 00000000000..8a64b97597b --- /dev/null +++ b/apps/sim/content/blog/agent-as-yjs-peer/index.mdx @@ -0,0 +1,110 @@ +--- +slug: agent-as-yjs-peer +title: 'The Agent Is Just Another Peer' +description: How we let an AI agent write into a live collaborative document by making it an ordinary Yjs peer, and the things that turned out to be easy, hard, and surprising. +date: 2026-08-03 +updated: 2026-08-03 +authors: + - waleed +readingTime: 10 +tags: [Yjs, CRDT, Collaboration, AI, ProseMirror, TipTap, Streaming, Architecture] +ogImage: /blog/agent-as-yjs-peer/cover.jpg +canonical: https://www.sim.ai/blog/agent-as-yjs-peer +draft: true +faq: + - q: "How does Sim let an AI agent write into a document while people are editing it?" + a: "The agent is treated as an ordinary Yjs peer. Each streamed chunk is reconciled into a private copy of the document, the single resulting CRDT update is captured, and only that update is applied to the live document, the same way an edit from another user would arrive. Concurrent human edits merge with it automatically." + - q: "Does the agent overwrite what collaborators are typing?" + a: "No. The agent applies a minimal update rather than replacing the document, so its operations merge with everyone else's. An agent writing at the bottom leaves an edit at the top alone, and an agent inserting above where someone is typing does not move their text." + - q: "Do you need relative-position anchoring to keep the agent's edits in the right place?" + a: "No, and that was the surprising part. Yjs operations are defined by item identity rather than character offset, so they are already relative. A whole-document diff that produces Yjs operations is robust to concurrent inserts without any explicit anchoring. Anchoring is only needed by systems that insert at absolute offsets." + - q: "How does undo work when an agent and a person are both editing?" + a: "Agent operations are applied under a dedicated transaction origin that the collaboration undo manager does not track, so pressing undo only reverts your own edits and never steps through the agent's output. On other screens the agent's writing arrives as remote updates, which a local undo manager never captures." + - q: "Why is streaming an AI into a rich-text document harder than into a text box?" + a: "A collaborative rich-text document is a shared data structure, not a string, so you cannot append to it. And when the storage format is Markdown, a rich-text editor and a Markdown file disagree about what things like blank lines mean, which you have to reconcile." +--- + +The first version of this wiped out people's edits. The agent would generate a sentence, we'd set the document to the new text, and whatever you'd typed a moment earlier would just disappear. It worked fine when you were the only one editing, but the whole point of these files is that other people are in them too, and the second someone else was typing, the agent kept clobbering their work. + +The underlying issue is that a collaborative document isn't really a string you can write into. It's a shared data structure (a [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)), and everyone editing it is mutating that structure at the same time. So when the agent replaced the whole document on each chunk, it wasn't so much appending its text as overwriting the structure out from under everyone else. + +What we needed was for the agent to change the document the way a person does, in small incremental edits that merge with everyone else's, instead of one big replace every time. + +## Writing like a keystroke + +The nice thing about that framing is that the editor already knows how to do it. When you type a character, it doesn't ship the whole document to your collaborators. It works out the smallest change your keystroke made and sends only that. Sim's files use [Yjs](https://yjs.dev) for the shared state, and the function that turns an editor change into one of those minimal updates is `updateYFragment`. It's the same function that runs on every keypress. + +So we wanted the agent to go through `updateYFragment` as well. The only real question was what to diff against. The live document keeps changing while the agent writes, and if the agent diffs against the live document, a collaborator's edit shows up as a difference the agent will try to reconcile away. The agent would end up quietly undoing the humans. + +The way around that is to give the agent its own private copy of the document to work against. When a stream starts, we snapshot the live document into a throwaway replica that nothing else touches: + +```ts +export function beginAgentStream(editor) { + const binding = ySyncPluginKey.getState(editor.state)?.binding + if (!binding) return null + const shadow = new Y.Doc() + Y.applyUpdate(shadow, Y.encodeStateAsUpdate(binding.doc)) // snapshot the live doc, once + return { shadow, fragment: shadow.getXmlFragment('default'), meta: null } +} +``` + +Then, for each chunk the model produces, the agent reconciles that private copy toward the new text, we capture the single update the reconciliation produced, and we apply just that update to the real document, exactly as if it had arrived over the network from another user: + +```ts +// simplified; the real version cleans up the listener and handles the no-binding case +export function applyAgentStreamFrame(editor, session, body) { + const target = PMNode.fromJSON(editor.schema, parseMarkdownToDoc(body)) + let delta = null + session.shadow.on('update', (update, origin) => { + if (origin === AGENT_STREAM_ORIGIN) delta = update + }) + session.shadow.transact(() => { + updateYFragment(session.shadow, session.fragment, target, session.meta) + }, AGENT_STREAM_ORIGIN) + if (delta) Y.applyUpdate(binding.doc, delta, AGENT_STREAM_ORIGIN) +} +``` + +The private copy is the part that took a while to appreciate. Because it only ever sees the agent's own reconciliations and never anyone else's edits, the difference between one chunk and the next is precisely what the agent changed and nothing else. The agent never forms an opinion about what the whole document should look like. It produces a small, well-scoped change and hands it to the CRDT, which merges it with whatever everyone else is doing the same way it merges any two people's edits. Moment.dev [described arriving at a similar idea from a different direction](https://www.moment.dev/blog/collab-with-ai-is-hard), which is to not let the agent chase a moving target. + +## What you get for free + +Once the agent's output is just ordinary CRDT operations, a few things we'd braced for stopped being problems. + +The first is concurrent editing, and this was the pleasant surprise. We assumed we would eventually need something like relative-position anchoring so the agent's edits landed correctly while people typed around them. We never did. Yjs operations are defined in terms of item identities rather than character offsets, so they are already relative. If a collaborator inserts a paragraph above where the agent is writing, the agent's operations still land in the right place, because they were never pinned to a numeric position to begin with. We tested this fairly hard: an agent appending at the bottom while someone edits the top, an agent inserting above where someone is typing, and an agent rewriting the exact paragraph someone else is editing. In every case the two sets of edits merge and both documents converge. Systems that insert at absolute character offsets do need anchoring, and [Electric's server-side agent](https://electric.ax/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs) is a good example of one that uses it, but a whole-document diff never computes an offset, so the problem doesn't come up. + +The second is undo. If the agent's output went onto your undo stack, a single undo would walk backward through the model's tokens, which is not what anyone wants. We apply every agent operation under a dedicated transaction origin (`AGENT_STREAM_ORIGIN`, which is just a `Symbol`), and the collaboration undo manager only tracks its own origin. Your undo history has your edits in it and not the agent's. On everyone else's screen the agent's writing arrives as remote updates, which a local undo manager never captures anyway. + +The third is the streaming itself. Because each chunk is a real operation on the shared document, a collaborator watching the same file sees the same smooth, formatted stream that the person who triggered it sees, and we didn't write anything to make that happen. The channel that keeps two people in sync is the same channel that carries the stream. We had actually built a separate server-side path to stream content into the document, and once this was working we deleted it. + +## Where it got genuinely hard: Markdown + +The concurrency turned out to be the easy part. The hard part was that our files are stored as Markdown, and a Markdown file and a rich-text editor don't agree on what a blank line means. + +We found this the way you usually find these things. Someone opened a document, it looked fine for the first section, and then there was an enormous run of blank space with the rest of the content pushed so far down you'd never scroll to it. The file hadn't lost any data. It had picked up close to two thousand empty paragraphs in the middle. + +The cause is a real impedance mismatch. In Markdown, a run of blank lines between two blocks is insignificant, and two blank lines mean the same thing as ten. But the editor rebuilds an empty paragraph node for every blank line it sees, to preserve spacing faithfully. So a document that at some point picked up a large run of blank lines, from a paste or from a model that emitted a lot of newlines, would turn that run into a couple thousand real nodes. Because the nodes are real, they get written back out to the Markdown file, parsed into nodes again on the next open, and never go away on their own. It was also behind a subtler problem where the document visibly reflowed about half a second after opening, because the static preview collapses empty paragraphs while the live editor gives each one a line of height. + +The fix was to stop treating blank runs as meaningful when we parse. Markdown says they're insignificant, so we collapse them, which has the nice side effect of making the file render the same in our editor as it does on GitHub or anywhere else it gets opened. We left the serializer alone, since a blank line inside a fenced code block is significant and collapsing that would quietly break code samples. + +The part I find most interesting is why we hit this when Notion and Obsidian don't. Notion doesn't store Markdown; its source of truth is blocks, so it can keep empty blocks around without a file to answer to. Obsidian edits the Markdown text directly rather than through a rich-text node tree, so a blank line is just a blank line. We took the harder middle option, a rich-text tree on top of a Markdown file, because we want editing to feel like a document and the storage to be a plain file you can read, diff, and hand to an agent. It's the right tradeoff for us, but it means the normalization between the two representations is our problem to own, and blank lines are the first place that shows up. + +## Keeping it fast and honest + +Two smaller decisions are worth mentioning. + +The server and the client convert Markdown with the exact same code. When a file opens, the server turns its Markdown into the initial CRDT document, and when it saves, it turns the CRDT document back into Markdown. Both directions go through the same parse and serialize functions the editor uses in the browser, and the CRDT step uses the same binding library. There is no second Markdown implementation to drift from the first, so the preview, the collaborative document, and the file on disk can't quietly disagree. + +The other is a performance detail on the hot path. Reconciling the shadow needs a mapping between the editor's nodes and the CRDT's, and building that mapping from scratch takes time proportional to the size of the document. But `updateYFragment` maintains the mapping in place as it runs, which is what the editor's own binding relies on for the whole life of a document. So we build it once when the stream starts and reuse it for every chunk. That's only safe because nothing but the agent touches the shadow, and it's a small win that grows with document size. + +## How we convinced ourselves + +We didn't want to trust any of this on reasoning alone, so we reproduced the two-writer situation with real peers: two editors wired together over Yjs, one running the actual streaming code and the other typing in between the agent's chunks. We ran it for the obvious cases and the awkward ones, including a full rewrite of the document while someone edits a paragraph the rewrite deletes. + +That last case taught us something we had wrong. We assumed that if the agent rewrote the document and deleted the paragraph a collaborator was editing, that person's edit would be lost, and we wrote the test asserting exactly that. It failed. Yjs doesn't drop the edit. It keeps the inserted text and reattaches it to whatever survived nearby, so the edit moved instead of vanishing. Our intuition said someone loses in a conflict, and the CRDT quietly chose to lose nothing. We fixed the test to match what actually happens, which is the right order to do it in. + +## Takeaways + +If you're putting an AI agent into a document people edit together, the most useful move is to stop treating the agent as special. Have it produce the same small operations a keystroke produces, in the CRDT's own language, and let the CRDT do the merging. With Yjs in particular you probably don't need position anchoring, because the operations are already relative. The channel that syncs collaborators is the same one that streams the agent, so there's no reason to build two. And if your editor is rich text while your storage is Markdown, the real work isn't the merging. It's deciding, carefully, what the two representations are allowed to disagree about. + +{/* still to do before publish: cover.jpg under public/blog/agent-as-yjs-peer/, confirm readingTime, flip draft:false */} From 38b9ca054351d5e41b7226cb89fb2b807ca74e75 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Aug 2026 13:11:17 -0700 Subject: [PATCH 02/10] blog(agent-as-yjs-peer): add cover image --- .../sim/public/blog/agent-as-yjs-peer/cover.jpg | Bin 0 -> 87910 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/sim/public/blog/agent-as-yjs-peer/cover.jpg diff --git a/apps/sim/public/blog/agent-as-yjs-peer/cover.jpg b/apps/sim/public/blog/agent-as-yjs-peer/cover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..60572190657815e83ed429a44eac8a5acf0e9df7 GIT binary patch literal 87910 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%0A0%TP5BSx@Uio*V+l%DRb~TSjNX7CN7oNOS1C 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 N&C;L#mHGAUKLFRJC)5A{ literal 0 HcmV?d00001 From 6a2e8bcb7d2e3b586dc757a32284b31b13f2133d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Aug 2026 13:22:33 -0700 Subject: [PATCH 03/10] blog(agent-as-yjs-peer): add live two-peer editing demo component --- .../blog/agent-as-yjs-peer/components.tsx | 1 + .../components/agent-peer-demo.tsx | 283 ++++++++++++++++++ .../content/blog/agent-as-yjs-peer/index.mdx | 2 + apps/sim/lib/blog/registry.ts | 1 + 4 files changed, 287 insertions(+) create mode 100644 apps/sim/content/blog/agent-as-yjs-peer/components.tsx create mode 100644 apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx diff --git a/apps/sim/content/blog/agent-as-yjs-peer/components.tsx b/apps/sim/content/blog/agent-as-yjs-peer/components.tsx new file mode 100644 index 00000000000..aded5728a4f --- /dev/null +++ b/apps/sim/content/blog/agent-as-yjs-peer/components.tsx @@ -0,0 +1 @@ +export { AgentPeerDemo } from './components/agent-peer-demo' diff --git a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx new file mode 100644 index 00000000000..dacd9e07d5e --- /dev/null +++ b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx @@ -0,0 +1,283 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +/** + * A small, self-contained illustration for the post: two peers editing the same document at once. + * A teammate ("Zoe") extends the top line while the agent ("Sim") writes the bottom line, and + * neither one overwrites the other. It is a scripted animation, not a live CRDT, but it is faithful + * to the idea: both carets advance independently and both edits land. + * + * Conventions match the other blog demos: 'use client', inline styles, no dependencies. Degrades to + * the finished document when JavaScript is off or reduced motion is requested. + */ + +type Token = string | { code: string } + +const HUMAN = { name: 'Zoe', color: '#e0872f' } +const AGENT = { name: 'Sim', color: '#6f5bf0' } + +// The top line already exists; the teammate is still appending to it. +const HUMAN_BASE = 'Rollout is set for Friday.' +const HUMAN_ADD = ' Ops signed off this morning.' + +// The bottom line is written from empty by the agent, one piece at a time. The code token pops in +// formatted, which is the small detail that sells "it arrives as real, formatted content." +const AGENT_LINE: Token[] = [ + 'The p95 latency held at ', + { code: '180ms' }, + ' through the load test, and errors stayed flat.', +] + +const tokenLen = (t: Token) => (typeof t === 'string' ? t.length : t.code.length) +const AGENT_TOTAL = AGENT_LINE.reduce((n, t) => n + tokenLen(t), 0) + +const HUMAN_MS = 46 // ms per character +const AGENT_MS = 30 +const HOLD_MS = 1900 // pause on the finished document before looping + +const codeStyle: React.CSSProperties = { + fontFamily: + 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace', + fontSize: '0.86em', + background: '#f1f1f3', + color: '#2a2a2a', + border: '1px solid rgba(0,0,0,0.06)', + borderRadius: '5px', + padding: '1px 5px', +} + +function Caret({ who }: { who: typeof HUMAN }) { + return ( + + + {/* Name tag sits to the RIGHT of the caret, on the same line, so it never collides with the + line above. This is how Google Docs / Figma render a live collaborator's caret. */} + + {who.name} + + + ) +} + +function renderAgent(revealed: number) { + const nodes: React.ReactNode[] = [] + let offset = 0 + let capped = false + for (let i = 0; i < AGENT_LINE.length; i++) { + const t = AGENT_LINE[i] + const len = tokenLen(t) + if (revealed <= offset) { + capped = true + break + } + if (typeof t === 'string') { + const local = Math.min(len, revealed - offset) + nodes.push({t.slice(0, local)}) + if (local < len) { + capped = true + offset += len + break + } + } else { + nodes.push( + + {t.code} + + ) + if (revealed < offset + len) { + capped = true + offset += len + break + } + } + offset += len + } + return { nodes, done: !capped && revealed >= AGENT_TOTAL } +} + +export function AgentPeerDemo() { + const [humanN, setHumanN] = useState(HUMAN_ADD.length) + const [agentN, setAgentN] = useState(AGENT_TOTAL) + const raf = useRef(null) + + useEffect(() => { + const reduce = + typeof window !== 'undefined' && + window.matchMedia?.('(prefers-reduced-motion: reduce)').matches + if (reduce) return + + let start = performance.now() + let holdUntil = 0 + setHumanN(0) + setAgentN(0) + + const tick = (now: number) => { + if (holdUntil) { + if (now >= holdUntil) { + holdUntil = 0 + start = now + setHumanN(0) + setAgentN(0) + } + raf.current = requestAnimationFrame(tick) + return + } + const elapsed = now - start + const h = Math.min(HUMAN_ADD.length, Math.floor(elapsed / HUMAN_MS)) + const a = Math.min(AGENT_TOTAL, Math.floor(elapsed / AGENT_MS)) + setHumanN(h) + setAgentN(a) + if (h >= HUMAN_ADD.length && a >= AGENT_TOTAL) holdUntil = now + HOLD_MS + raf.current = requestAnimationFrame(tick) + } + + raf.current = requestAnimationFrame(tick) + return () => { + if (raf.current) cancelAnimationFrame(raf.current) + } + }, []) + + const humanDone = humanN >= HUMAN_ADD.length + const agent = renderAgent(agentN) + + return ( +
+ +
+ {/* header: filename + presence */} +
+
+ launch-notes.md +
+
+ {[AGENT, HUMAN].map((p, i) => ( + + {p.name[0]} + + ))} +
+
+ + {/* document body */} +
+
+ Launch notes +
+ +

+ {HUMAN_BASE} + {HUMAN_ADD.slice(0, humanN)} + {!humanDone && } +

+ +

+ {agent.nodes} + {!agent.done && } +

+
+
+ +
+ Sim and a teammate editing the same file at once. Neither one overwrites the other. +
+
+ ) +} diff --git a/apps/sim/content/blog/agent-as-yjs-peer/index.mdx b/apps/sim/content/blog/agent-as-yjs-peer/index.mdx index 8a64b97597b..fd6edd05a1d 100644 --- a/apps/sim/content/blog/agent-as-yjs-peer/index.mdx +++ b/apps/sim/content/blog/agent-as-yjs-peer/index.mdx @@ -30,6 +30,8 @@ The underlying issue is that a collaborative document isn't really a string you What we needed was for the agent to change the document the way a person does, in small incremental edits that merge with everyone else's, instead of one big replace every time. + + ## Writing like a keystroke The nice thing about that framing is that the editor already knows how to do it. When you type a character, it doesn't ship the whole document to your collaborators. It works out the smallest change your keystroke made and sends only that. Sim's files use [Yjs](https://yjs.dev) for the shared state, and the function that turns an editor change into one of those minimal updates is `updateYFragment`. It's the same function that runs on every keypress. diff --git a/apps/sim/lib/blog/registry.ts b/apps/sim/lib/blog/registry.ts index ff766e02d7f..1841140a717 100644 --- a/apps/sim/lib/blog/registry.ts +++ b/apps/sim/lib/blog/registry.ts @@ -8,6 +8,7 @@ const AUTHORS_DIR = path.join(process.cwd(), 'content', 'authors') const BLOG_COMPONENT_LOADERS = { enterprise: () => import('@/content/blog/enterprise/components'), 'v0-5': () => import('@/content/blog/v0-5/components'), + 'agent-as-yjs-peer': () => import('@/content/blog/agent-as-yjs-peer/components'), } const blogRegistry = createContentRegistry({ From 3a30ee3452060f25b43494575021fc85b92d3bf3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Aug 2026 15:10:31 -0700 Subject: [PATCH 04/10] improvement(blog): match code blocks and inline code to the in-app editor tokens --- apps/sim/lib/content/code.tsx | 23 ++++++++++++++--------- apps/sim/lib/content/mdx.tsx | 9 +++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/sim/lib/content/code.tsx b/apps/sim/lib/content/code.tsx index 16436e52ccb..c1aecc715b9 100644 --- a/apps/sim/lib/content/code.tsx +++ b/apps/sim/lib/content/code.tsx @@ -1,21 +1,26 @@ 'use client' -import { Code } from '@sim/emcn' +import { Code, chipFieldSurfaceClass, cn } from '@sim/emcn' interface CodeBlockProps { code: string language: 'javascript' | 'json' | 'python' } +/** + * Blog code block. Renders through emcn's read-only `Code.Viewer` wearing the exact same + * `chipFieldSurfaceClass` surface the in-app code editor uses (the custom-tools `CodeEditor`), + * so a fenced block in a post matches the real editor: same Prism theme, gutter, and font, on + * the same theme-following surface (`--surface-5`/`--border-1`, rounded-lg) — no forced dark, + * all from existing design tokens. + */ export function CodeBlock({ code, language }: CodeBlockProps) { return ( -
- -
+ ) } diff --git a/apps/sim/lib/content/mdx.tsx b/apps/sim/lib/content/mdx.tsx index 8a0973abfb2..ddd4f5c7ae3 100644 --- a/apps/sim/lib/content/mdx.tsx +++ b/apps/sim/lib/content/mdx.tsx @@ -182,16 +182,21 @@ export const mdxComponents: MDXRemoteProps['components'] = { } return
   },
+  // Inline code chip — matches the rich markdown editor's `.rich-markdown-prose code` exactly
+  // (--surface-5 fill, Martian Mono, 0.875em, 4px radius, 0.125rem/0.375rem padding, no border).
   code: (props: any) => {
     if (!props.className) {
       return (
         
       )
     }

From 6e8f7386dbbeb70b3fa2e4f2f29fd871c930a8b7 Mon Sep 17 00:00:00 2001
From: Waleed Latif 
Date: Tue, 4 Aug 2026 15:10:31 -0700
Subject: [PATCH 05/10] blog(agent-as-yjs-peer): finalize copy, streaming demo,
 and feature the post

---
 .../components/agent-peer-demo.tsx            | 509 ++++++++++++------
 .../content/blog/agent-as-yjs-peer/index.mdx  |  33 +-
 2 files changed, 368 insertions(+), 174 deletions(-)

diff --git a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx
index dacd9e07d5e..89d02b3b02d 100644
--- a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx
+++ b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx
@@ -4,49 +4,121 @@ import { useEffect, useRef, useState } from 'react'
 
 /**
  * A small, self-contained illustration for the post: two peers editing the same document at once.
- * A teammate ("Zoe") extends the top line while the agent ("Sim") writes the bottom line, and
- * neither one overwrites the other. It is a scripted animation, not a live CRDT, but it is faithful
- * to the idea: both carets advance independently and both edits land.
+ * A teammate ("Zoe") extends the intro line while the agent ("Sim") writes a short formatted block
+ * below — a bold lead, an inline code chip, and a bullet list, arriving already rendered. Neither one
+ * overwrites the other. It is a scripted animation, not a live CRDT, but it is faithful to the idea:
+ * both carets advance independently and both edits land.
  *
- * Conventions match the other blog demos: 'use client', inline styles, no dependencies. Degrades to
- * the finished document when JavaScript is off or reduced motion is requested.
+ * Everything is styled from the SAME design tokens the real markdown editor uses (`--font-inter`,
+ * `--font-martian-mono`, `--text-primary`, `--surface-5`, `--border`, `--surface-1`, `--bg`), the
+ * carets replicate `CollaborationCaret`'s bar + notched name label (rich-markdown-editor.css), and
+ * the identity colors are the real `USER_COLORS` (lib/workspaces/colors.ts). No shadows — the app
+ * frames surfaces with a 1px border. The finished document is always laid out; unrevealed text is
+ * kept `visibility: hidden` so the card never changes size as content streams in. Degrades to the
+ * finished document when JavaScript is off or reduced motion is requested.
  */
 
-type Token = string | { code: string }
+type Seg = { t: string } | { code: string } | { b: string }
+interface Block {
+  kind: 'p' | 'li'
+  segs: Seg[]
+}
+
+const FONT_SANS = 'var(--font-inter, ui-sans-serif, system-ui, -apple-system, sans-serif)'
+const FONT_MONO = 'var(--font-martian-mono, ui-monospace, SFMono-Regular, Menlo, monospace)'
 
-const HUMAN = { name: 'Zoe', color: '#e0872f' }
-const AGENT = { name: 'Sim', color: '#6f5bf0' }
+// Two collaborators, colored from the real identity palette (USER_COLORS), as getUserColor assigns.
+const AGENT = { name: 'Sim', color: '#60C5FF' } // Blue
+const HUMAN = { name: 'Zoe', color: '#F472B6' } // Pink
 
-// The top line already exists; the teammate is still appending to it.
+// The intro line already exists; the teammate is still appending to it.
 const HUMAN_BASE = 'Rollout is set for Friday.'
 const HUMAN_ADD = ' Ops signed off this morning.'
 
-// The bottom line is written from empty by the agent, one piece at a time. The code token pops in
-// formatted, which is the small detail that sells "it arrives as real, formatted content."
-const AGENT_LINE: Token[] = [
-  'The p95 latency held at ',
-  { code: '180ms' },
-  ' through the load test, and errors stayed flat.',
+// The block the agent writes from empty, streamed one character at a time. It arrives already
+// formatted: the bold lead, the code chip, and the bullets all render as they land.
+const AGENT_BLOCKS: Block[] = [
+  {
+    kind: 'p',
+    segs: [{ b: 'Load test: ' }, { t: 'p95 held at ' }, { code: '180ms' }, { t: ', errors flat.' }],
+  },
+  { kind: 'li', segs: [{ t: 'Rollback path verified' }] },
+  { kind: 'li', segs: [{ t: 'On-call paged and acked' }] },
 ]
 
-const tokenLen = (t: Token) => (typeof t === 'string' ? t.length : t.code.length)
-const AGENT_TOTAL = AGENT_LINE.reduce((n, t) => n + tokenLen(t), 0)
+const segText = (s: Seg): string => ('t' in s ? s.t : 'code' in s ? s.code : s.b)
+const AGENT_FLAT = AGENT_BLOCKS.flatMap((b) => b.segs.map(segText)).join('')
+const HUMAN_TOTAL = HUMAN_ADD.length
+const AGENT_TOTAL = AGENT_FLAT.length
+
+/**
+ * Builds a per-character reveal timeline with an organic, non-uniform cadence: a smooth
+ * multi-frequency wobble in typing speed, plus natural pauses at spaces and punctuation. Fully
+ * deterministic (no RNG) so the server and client agree and it never jitters between renders — the
+ * variation comes from the character index and content, not randomness. `times[i]` is the ms at
+ * which character `i` should appear.
+ */
+function buildSchedule(
+  text: string,
+  o: { base: number; wobble: number; space: number; punct: number; startAt?: number }
+): number[] {
+  const times: number[] = []
+  let t = o.startAt ?? 0
+  for (let i = 0; i < text.length; i++) {
+    const ch = text[i]
+    const wob = 1 + o.wobble * (0.6 * Math.sin(i * 1.7 + 0.5) + 0.4 * Math.sin(i * 0.53 + 1.1))
+    t += o.base * wob
+    if (ch === ' ') t += o.space
+    else if (ch === '.' || ch === ',' || ch === ':') t += o.punct
+    times.push(t)
+  }
+  return times
+}
+
+// The agent streams a touch faster and steadier (it is a machine); the teammate types slower with
+// bigger word/sentence pauses, and starts a beat later so the two are never metronomically locked.
+const AGENT_TIMES = buildSchedule(AGENT_FLAT, { base: 34, wobble: 0.5, space: 45, punct: 150 })
+const HUMAN_TIMES = buildSchedule(HUMAN_ADD, {
+  base: 58,
+  wobble: 0.45,
+  space: 65,
+  punct: 150,
+  startAt: 280,
+})
 
-const HUMAN_MS = 46 // ms per character
-const AGENT_MS = 30
-const HOLD_MS = 1900 // pause on the finished document before looping
+const lastTime = (a: number[]) => (a.length ? a[a.length - 1] : 0)
+const countReached = (times: number[], t: number): number => {
+  let n = 0
+  while (n < times.length && times[n] <= t) n++
+  return n
+}
+
+// The demo streams through ONCE when it scrolls into view, then rests on the finished document —
+// no loop, no fade. `RUN_MS` is when the last character has landed; at that point the reveal is
+// clamped to the finished state and the animation stops (no further frames are scheduled).
+const RUN_MS = Math.max(lastTime(AGENT_TIMES), lastTime(HUMAN_TIMES))
+
+const proseStyle: React.CSSProperties = {
+  fontFamily: FONT_SANS,
+  fontSize: '15px',
+  fontWeight: 430,
+  lineHeight: '25px',
+  letterSpacing: 0,
+  color: 'var(--text-primary)',
+  overflowWrap: 'anywhere',
+}
 
 const codeStyle: React.CSSProperties = {
-  fontFamily:
-    'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
-  fontSize: '0.86em',
-  background: '#f1f1f3',
-  color: '#2a2a2a',
-  border: '1px solid rgba(0,0,0,0.06)',
-  borderRadius: '5px',
-  padding: '1px 5px',
+  fontFamily: FONT_MONO,
+  fontSize: '0.875em',
+  background: 'var(--surface-5)',
+  borderRadius: '4px',
+  padding: '0.125rem 0.375rem',
 }
 
+/** Replicates CollaborationCaret in its active state: a 2px identity-colored bar with the notched
+ *  name label above it (rich-markdown-editor.css `.collaboration-carets__bar` / `__label`). Zero
+ *  inline width, so inserting it at the write head never shifts the surrounding text. */
 function Caret({ who }: { who: typeof HUMAN }) {
   return (
     
       
-      {/* Name tag sits to the RIGHT of the caret, on the same line, so it never collides with the
-          line above. This is how Google Docs / Figma render a live collaborator's caret. */}
       
         {who.name}
@@ -95,185 +165,304 @@ function Caret({ who }: { who: typeof HUMAN }) {
   )
 }
 
-function renderAgent(revealed: number) {
+/** Render one block's inline segments, revealing up to `revealed` characters (global index) and
+ *  reserving the rest with `visibility: hidden` so layout is stable. Drops the caret exactly at the
+ *  write head when the boundary falls inside this block. Returns the nodes and whether it placed the
+ *  caret, so the caller can append a trailing caret when the whole document is complete. */
+function renderBlockNodes(
+  block: Block,
+  blockStart: number,
+  revealed: number,
+  placed: boolean,
+  isLast: boolean
+) {
   const nodes: React.ReactNode[] = []
-  let offset = 0
-  let capped = false
-  for (let i = 0; i < AGENT_LINE.length; i++) {
-    const t = AGENT_LINE[i]
-    const len = tokenLen(t)
-    if (revealed <= offset) {
-      capped = true
-      break
-    }
-    if (typeof t === 'string') {
-      const local = Math.min(len, revealed - offset)
-      nodes.push({t.slice(0, local)})
-      if (local < len) {
-        capped = true
-        offset += len
-        break
+  let offset = blockStart
+  let didPlace = placed
+  for (let si = 0; si < block.segs.length; si++) {
+    const seg = block.segs[si]
+    const raw = segText(seg)
+    const start = offset
+    const end = offset + raw.length
+    const key = `${blockStart}-${si}`
+
+    if ('code' in seg) {
+      if (!didPlace && revealed >= start && revealed < end) {
+        nodes.push()
+        didPlace = true
       }
-    } else {
       nodes.push(
-        
-          {t.code}
+        = end ? 'visible' : 'hidden' }}
+        >
+          {raw}
         
       )
-      if (revealed < offset + len) {
-        capped = true
-        offset += len
-        break
+    } else {
+      const shown = Math.max(0, Math.min(raw.length, revealed - start))
+      const vis = raw.slice(0, shown)
+      const hid = raw.slice(shown)
+      const weight = 'b' in seg ? 600 : undefined
+      if (vis) {
+        nodes.push(
+          
+            {vis}
+          
+        )
+      }
+      if (!didPlace && revealed >= start && revealed <= end && shown < raw.length) {
+        nodes.push()
+        didPlace = true
       }
+      if (hid) {
+        nodes.push(
+          
+            {hid}
+          
+        )
+      }
+    }
+    offset = end
+  }
+  // Once the whole block is written, the caret rests at the end of the last block — the peer stays present.
+  if (isLast && !didPlace) {
+    nodes.push()
+    didPlace = true
+  }
+  return { nodes, placed: didPlace }
+}
+
+function AgentDoc({ revealed }: { revealed: number }) {
+  const out: React.ReactNode[] = []
+  let list: React.ReactNode[] = []
+  let offset = 0
+  let placed = false
+
+  const flushList = () => {
+    if (list.length === 0) return
+    out.push(
+      
    + {list} +
+ ) + list = [] + } + + for (let bi = 0; bi < AGENT_BLOCKS.length; bi++) { + const block = AGENT_BLOCKS[bi] + const res = renderBlockNodes(block, offset, revealed, placed, bi === AGENT_BLOCKS.length - 1) + placed = res.placed + offset += block.segs.reduce((m, s) => m + segText(s).length, 0) + + if (block.kind === 'li') { + list.push( +
  • + {res.nodes} +
  • + ) + } else { + flushList() + out.push( +

    + {res.nodes} +

    + ) } - offset += len } - return { nodes, done: !capped && revealed >= AGENT_TOTAL } + flushList() + + return <>{out} +} + +function Avatar({ who, overlap }: { who: typeof HUMAN; overlap: boolean }) { + return ( + + {who.name[0]} + + ) } export function AgentPeerDemo() { - const [humanN, setHumanN] = useState(HUMAN_ADD.length) + // Initial (SSR / no-JS / reduced-motion) state is the finished document, so the content is always + // present without JavaScript. When the demo scrolls into view it resets to empty, streams through + // once, and rests on the finished document. + const [humanN, setHumanN] = useState(HUMAN_TOTAL) const [agentN, setAgentN] = useState(AGENT_TOTAL) - const raf = useRef(null) + const cardRef = useRef(null) useEffect(() => { const reduce = typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches - if (reduce) return + const el = cardRef.current + if (reduce || !el) return - let start = performance.now() - let holdUntil = 0 + let raf = 0 + let played = false + // Hold at empty until the reader reaches it, so they watch it type rather than arriving to a + // finished doc that quietly retypes. setHumanN(0) setAgentN(0) - const tick = (now: number) => { - if (holdUntil) { - if (now >= holdUntil) { - holdUntil = 0 - start = now - setHumanN(0) - setAgentN(0) + const play = () => { + const start = performance.now() + // A character lands every ~30–60ms but frames fire every ~16ms, so most frames reveal nothing + // new. Track the last emitted counts and only setState on an actual change, so React renders + // roughly once per character (~90 times total) rather than every frame. + let lastH = 0 + let lastA = 0 + const tick = (now: number) => { + const t = now - start + if (t >= RUN_MS) { + if (lastH !== HUMAN_TOTAL) setHumanN(HUMAN_TOTAL) + if (lastA !== AGENT_TOTAL) setAgentN(AGENT_TOTAL) + return // rest on the finished document — no loop, no further frames } - raf.current = requestAnimationFrame(tick) - return + const h = countReached(HUMAN_TIMES, t) + const a = countReached(AGENT_TIMES, t) + if (h !== lastH) { + setHumanN(h) + lastH = h + } + if (a !== lastA) { + setAgentN(a) + lastA = a + } + raf = requestAnimationFrame(tick) } - const elapsed = now - start - const h = Math.min(HUMAN_ADD.length, Math.floor(elapsed / HUMAN_MS)) - const a = Math.min(AGENT_TOTAL, Math.floor(elapsed / AGENT_MS)) - setHumanN(h) - setAgentN(a) - if (h >= HUMAN_ADD.length && a >= AGENT_TOTAL) holdUntil = now + HOLD_MS - raf.current = requestAnimationFrame(tick) + raf = requestAnimationFrame(tick) } - raf.current = requestAnimationFrame(tick) + const io = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting && !played) { + played = true + io.disconnect() + play() + } + }, + { threshold: 0.4 } + ) + io.observe(el) + return () => { - if (raf.current) cancelAnimationFrame(raf.current) + io.disconnect() + if (raf) cancelAnimationFrame(raf) } }, []) - const humanDone = humanN >= HUMAN_ADD.length - const agent = renderAgent(agentN) - return (
    -
    - {/* header: filename + presence */} + {/* header: filename + presence — mirrors Resource.Header (min-height 48px, border-b, px-4). */}
    -
    - launch-notes.md -
    -
    - {[AGENT, HUMAN].map((p, i) => ( - - {p.name[0]} - - ))} +
    + + + launch-notes.md +
    + + + +
    - {/* document body */} -
    + {/* document body — the real prose scale (15px / 430 / 25px, --font-inter, --text-primary). */} +
    Launch notes
    -

    +

    {HUMAN_BASE} - {HUMAN_ADD.slice(0, humanN)} - {!humanDone && } + {HUMAN_ADD.slice(0, humanN)} + + {HUMAN_ADD.slice(humanN)}

    -

    - {agent.nodes} - {!agent.done && } -

    +
    Sim and a teammate editing the same file at once. Neither one overwrites the other. diff --git a/apps/sim/content/blog/agent-as-yjs-peer/index.mdx b/apps/sim/content/blog/agent-as-yjs-peer/index.mdx index fd6edd05a1d..3383fb25a43 100644 --- a/apps/sim/content/blog/agent-as-yjs-peer/index.mdx +++ b/apps/sim/content/blog/agent-as-yjs-peer/index.mdx @@ -6,11 +6,12 @@ date: 2026-08-03 updated: 2026-08-03 authors: - waleed -readingTime: 10 +readingTime: 11 tags: [Yjs, CRDT, Collaboration, AI, ProseMirror, TipTap, Streaming, Architecture] ogImage: /blog/agent-as-yjs-peer/cover.jpg canonical: https://www.sim.ai/blog/agent-as-yjs-peer -draft: true +draft: false +featured: true faq: - q: "How does Sim let an AI agent write into a document while people are editing it?" a: "The agent is treated as an ordinary Yjs peer. Each streamed chunk is reconciled into a private copy of the document, the single resulting CRDT update is captured, and only that update is applied to the live document, the same way an edit from another user would arrive. Concurrent human edits merge with it automatically." @@ -24,7 +25,9 @@ faq: a: "A collaborative rich-text document is a shared data structure, not a string, so you cannot append to it. And when the storage format is Markdown, a rich-text editor and a Markdown file disagree about what things like blank lines mean, which you have to reconcile." --- -The first version of this wiped out people's edits. The agent would generate a sentence, we'd set the document to the new text, and whatever you'd typed a moment earlier would just disappear. It worked fine when you were the only one editing, but the whole point of these files is that other people are in them too, and the second someone else was typing, the agent kept clobbering their work. +Sim's files are documents you edit together. Several people can be in the same file at once, with everyone's cursors and changes showing up live, and we wanted the AI agent to be able to write into them too. + +Our first attempt at that wiped out people's edits. The agent would generate a sentence, we'd set the document to the new text, and whatever you'd typed a moment earlier would just disappear. It worked fine when you were the only one editing, but the second someone else was typing, the agent kept clobbering their work. The underlying issue is that a collaborative document isn't really a string you can write into. It's a shared data structure (a [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type)), and everyone editing it is mutating that structure at the same time. So when the agent replaced the whole document on each chunk, it wasn't so much appending its text as overwriting the structure out from under everyone else. @@ -34,7 +37,7 @@ What we needed was for the agent to change the document the way a person does, i ## Writing like a keystroke -The nice thing about that framing is that the editor already knows how to do it. When you type a character, it doesn't ship the whole document to your collaborators. It works out the smallest change your keystroke made and sends only that. Sim's files use [Yjs](https://yjs.dev) for the shared state, and the function that turns an editor change into one of those minimal updates is `updateYFragment`. It's the same function that runs on every keypress. +The editor already knows how to do this. When you type a character, it doesn't ship the whole document to your collaborators; it works out the smallest change your keystroke made and sends only that. Sim's files use [Yjs](https://yjs.dev) for the shared state, and the function that turns an editor change into one of those minimal updates is `updateYFragment`. It's the same function that runs on every keypress. So we wanted the agent to go through `updateYFragment` as well. The only real question was what to diff against. The live document keeps changing while the agent writes, and if the agent diffs against the live document, a collaborator's edit shows up as a difference the agent will try to reconcile away. The agent would end up quietly undoing the humans. @@ -67,17 +70,17 @@ export function applyAgentStreamFrame(editor, session, body) { } ``` -The private copy is the part that took a while to appreciate. Because it only ever sees the agent's own reconciliations and never anyone else's edits, the difference between one chunk and the next is precisely what the agent changed and nothing else. The agent never forms an opinion about what the whole document should look like. It produces a small, well-scoped change and hands it to the CRDT, which merges it with whatever everyone else is doing the same way it merges any two people's edits. Moment.dev [described arriving at a similar idea from a different direction](https://www.moment.dev/blog/collab-with-ai-is-hard), which is to not let the agent chase a moving target. +The private copy is the part that took a while to appreciate. Because it only ever sees the agent's own reconciliations and never anyone else's edits, the difference between one chunk and the next is precisely what the agent changed and nothing else. The agent never forms an opinion about what the whole document should look like. It produces a small, well-scoped change and hands it to the CRDT, which merges it with whatever everyone else is doing the same way it merges any two people's edits. Moment.dev [described arriving at a similar idea from a different direction](https://www.moment.dev/blog/collab-with-ai-is-hard): don't let the agent chase a moving target. ## What you get for free Once the agent's output is just ordinary CRDT operations, a few things we'd braced for stopped being problems. -The first is concurrent editing, and this was the pleasant surprise. We assumed we would eventually need something like relative-position anchoring so the agent's edits landed correctly while people typed around them. We never did. Yjs operations are defined in terms of item identities rather than character offsets, so they are already relative. If a collaborator inserts a paragraph above where the agent is writing, the agent's operations still land in the right place, because they were never pinned to a numeric position to begin with. We tested this fairly hard: an agent appending at the bottom while someone edits the top, an agent inserting above where someone is typing, and an agent rewriting the exact paragraph someone else is editing. In every case the two sets of edits merge and both documents converge. Systems that insert at absolute character offsets do need anchoring, and [Electric's server-side agent](https://electric.ax/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs) is a good example of one that uses it, but a whole-document diff never computes an offset, so the problem doesn't come up. +The first is concurrent editing, and this was the pleasant surprise. We assumed we would eventually need something like relative-position anchoring so the agent's edits landed correctly while people typed around them. We never did. Yjs operations are defined in terms of item identities rather than character offsets, so they are already relative. If a collaborator inserts a paragraph above where the agent is writing, the agent's operations still land in the right place, because they were never pinned to a numeric position to begin with. We tested this fairly hard: an agent appending at the bottom while someone edits the top, an agent inserting above where someone is typing, and an agent rewriting a paragraph someone else is editing. In every case the two documents converge to the same state instead of one write clobbering the other, which was the whole problem we started with. Systems that insert at absolute character offsets do need anchoring, and [Electric's server-side agent](https://electric.ax/blog/2026/04/08/ai-agents-as-crdt-peers-with-yjs) is a good example of one that uses it, but a whole-document diff never computes an offset, so the problem doesn't come up. The second is undo. If the agent's output went onto your undo stack, a single undo would walk backward through the model's tokens, which is not what anyone wants. We apply every agent operation under a dedicated transaction origin (`AGENT_STREAM_ORIGIN`, which is just a `Symbol`), and the collaboration undo manager only tracks its own origin. Your undo history has your edits in it and not the agent's. On everyone else's screen the agent's writing arrives as remote updates, which a local undo manager never captures anyway. -The third is the streaming itself. Because each chunk is a real operation on the shared document, a collaborator watching the same file sees the same smooth, formatted stream that the person who triggered it sees, and we didn't write anything to make that happen. The channel that keeps two people in sync is the same channel that carries the stream. We had actually built a separate server-side path to stream content into the document, and once this was working we deleted it. +The third is the streaming itself. Because each chunk is a real operation on the shared document, a collaborator watching the same file sees the same smooth, formatted stream that the person who triggered it sees, and we didn't write anything to make that happen. The channel that keeps two people in sync is the same channel that carries the stream, so the agent's output reaches everyone through the path a person's keystrokes already take. There is no separate streaming pipe to build or keep in step with it. The one server-side write left is the final save, which turns the finished document back into Markdown on disk. ## Where it got genuinely hard: Markdown @@ -87,17 +90,21 @@ We found this the way you usually find these things. Someone opened a document, The cause is a real impedance mismatch. In Markdown, a run of blank lines between two blocks is insignificant, and two blank lines mean the same thing as ten. But the editor rebuilds an empty paragraph node for every blank line it sees, to preserve spacing faithfully. So a document that at some point picked up a large run of blank lines, from a paste or from a model that emitted a lot of newlines, would turn that run into a couple thousand real nodes. Because the nodes are real, they get written back out to the Markdown file, parsed into nodes again on the next open, and never go away on their own. It was also behind a subtler problem where the document visibly reflowed about half a second after opening, because the static preview collapses empty paragraphs while the live editor gives each one a line of height. -The fix was to stop treating blank runs as meaningful when we parse. Markdown says they're insignificant, so we collapse them, which has the nice side effect of making the file render the same in our editor as it does on GitHub or anywhere else it gets opened. We left the serializer alone, since a blank line inside a fenced code block is significant and collapsing that would quietly break code samples. +The fix was to stop treating blank runs as meaningful when we parse. Markdown says they're insignificant, so we collapse them, which also makes the file render the same in our editor as it does on GitHub or anywhere else it gets opened. We left the serializer alone, since a blank line inside a fenced code block is significant and collapsing that would break code samples. The part I find most interesting is why we hit this when Notion and Obsidian don't. Notion doesn't store Markdown; its source of truth is blocks, so it can keep empty blocks around without a file to answer to. Obsidian edits the Markdown text directly rather than through a rich-text node tree, so a blank line is just a blank line. We took the harder middle option, a rich-text tree on top of a Markdown file, because we want editing to feel like a document and the storage to be a plain file you can read, diff, and hand to an agent. It's the right tradeoff for us, but it means the normalization between the two representations is our problem to own, and blank lines are the first place that shows up. ## Keeping it fast and honest -Two smaller decisions are worth mentioning. +A few smaller decisions are worth mentioning. + +The server and the client convert Markdown with the exact same code. When a file opens, the server turns its Markdown into the initial CRDT document, and when it saves, it turns the CRDT document back into Markdown. Both directions go through the same parse and serialize functions the editor uses in the browser, and the CRDT step uses the same binding library. There is no second Markdown implementation to drift from the first, so the preview, the collaborative document, and the file on disk can't disagree. -The server and the client convert Markdown with the exact same code. When a file opens, the server turns its Markdown into the initial CRDT document, and when it saves, it turns the CRDT document back into Markdown. Both directions go through the same parse and serialize functions the editor uses in the browser, and the CRDT step uses the same binding library. There is no second Markdown implementation to drift from the first, so the preview, the collaborative document, and the file on disk can't quietly disagree. +There's also a performance detail on the hot path. Reconciling the shadow needs a mapping between the editor's nodes and the CRDT's, and building that mapping from scratch takes time proportional to the size of the document. But `updateYFragment` maintains the mapping in place as it runs, which is what the editor's own binding relies on for the whole life of a document. So we build it once, on the first chunk, and reuse it for every chunk after. That's only safe because nothing but the agent touches the shadow, and it's a small win that grows with document size. -The other is a performance detail on the hot path. Reconciling the shadow needs a mapping between the editor's nodes and the CRDT's, and building that mapping from scratch takes time proportional to the size of the document. But `updateYFragment` maintains the mapping in place as it runs, which is what the editor's own binding relies on for the whole life of a document. So we build it once when the stream starts and reuse it for every chunk. That's only safe because nothing but the agent touches the shadow, and it's a small win that grows with document size. +On the screen that starts the stream, we make the editor read-only until the agent finishes. The agent is already writing there, and letting you type into the same place at the same moment is a fight over the cursor with nothing to gain. Everyone else stays fully editable, because on their screen the agent's writing is just remote updates arriving, the same as another person typing. + +The last one is about who applies the stream when the file is open in more than one place. Only one client should write the agent's chunks into the shared document; if two did, every change would land twice. So the clients pick one to do it, over the same awareness channel they already use to show who's present, and everyone else receives the result as ordinary updates. ## How we convinced ourselves @@ -107,6 +114,4 @@ That last case taught us something we had wrong. We assumed that if the agent re ## Takeaways -If you're putting an AI agent into a document people edit together, the most useful move is to stop treating the agent as special. Have it produce the same small operations a keystroke produces, in the CRDT's own language, and let the CRDT do the merging. With Yjs in particular you probably don't need position anchoring, because the operations are already relative. The channel that syncs collaborators is the same one that streams the agent, so there's no reason to build two. And if your editor is rich text while your storage is Markdown, the real work isn't the merging. It's deciding, carefully, what the two representations are allowed to disagree about. - -{/* still to do before publish: cover.jpg under public/blog/agent-as-yjs-peer/, confirm readingTime, flip draft:false */} +If you're putting an AI agent into a document people edit together, the most useful move is to stop treating the agent as special. Have it produce the same small operations a keystroke produces, in the CRDT's own language, and let the CRDT do the merging. With Yjs in particular you probably don't need position anchoring, because the operations are already relative. The channel that syncs collaborators is the same one that streams the agent, so there's no reason to build two. And if your editor is rich text while your storage is Markdown, the merging is the easy part, and the real work is deciding, carefully, what the two representations are allowed to disagree about. From df54aa16151c0c9dee349d3b4a11268c35dd3564 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Aug 2026 15:17:11 -0700 Subject: [PATCH 06/10] fix(blog): guard agent demo against missing/never-firing IntersectionObserver --- .../agent-as-yjs-peer/components/agent-peer-demo.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx index 89d02b3b02d..e1793934462 100644 --- a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx +++ b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx @@ -316,16 +316,18 @@ export function AgentPeerDemo() { typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches const el = cardRef.current - if (reduce || !el) return + // Reduced motion, no element, or an environment without IntersectionObserver (older browsers, + // some test runners): leave the finished document in place as the fallback and never clear it. + if (reduce || !el || typeof IntersectionObserver === 'undefined') return let raf = 0 let played = false - // Hold at empty until the reader reaches it, so they watch it type rather than arriving to a - // finished doc that quietly retypes. - setHumanN(0) - setAgentN(0) const play = () => { + // Clear to empty only now that the demo is actually on screen, so it's never left blank if the + // observer never fires (the element never reaches the visibility threshold). + setHumanN(0) + setAgentN(0) const start = performance.now() // A character lands every ~30–60ms but frames fire every ~16ms, so most frames reveal nothing // new. Track the last emitted counts and only setState on an actual change, so React renders From 32151a0e75168652045e726fa2650d4f8ac9a64a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Aug 2026 15:28:56 -0700 Subject: [PATCH 07/10] fix(blog): stream demo bullets and code chip per-character to match the typing cadence --- .../components/agent-peer-demo.tsx | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx index e1793934462..978a789506a 100644 --- a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx +++ b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx @@ -187,16 +187,21 @@ function renderBlockNodes( const key = `${blockStart}-${si}` if ('code' in seg) { - if (!didPlace && revealed >= start && revealed < end) { - nodes.push() - didPlace = true - } + // Type the chip in character by character, at the same cadence as the surrounding text, with + // the caret moving through it. The box reserves its full width up front (the unrevealed tail is + // visibility:hidden inside it) so the card never reflows. Revealing it as one chunk instead made + // it either freeze-and-jump (caret parked before an empty gap) or fly past as a skipped unit. + const reached = revealed >= start + const shown = reached ? Math.min(raw.length, revealed - start) : 0 + const atBoundary = !didPlace && reached && shown < raw.length + if (atBoundary) didPlace = true nodes.push( - = end ? 'visible' : 'hidden' }} - > - {raw} + + {reached ? raw.slice(0, shown) : raw} + {atBoundary && } + {reached && shown < raw.length && ( + {raw.slice(shown)} + )} ) } else { @@ -254,13 +259,27 @@ function AgentDoc({ revealed }: { revealed: number }) { for (let bi = 0; bi < AGENT_BLOCKS.length; bi++) { const block = AGENT_BLOCKS[bi] - const res = renderBlockNodes(block, offset, revealed, placed, bi === AGENT_BLOCKS.length - 1) + const blockStart = offset + const res = renderBlockNodes( + block, + blockStart, + revealed, + placed, + bi === AGENT_BLOCKS.length - 1 + ) placed = res.placed - offset += block.segs.reduce((m, s) => m + segText(s).length, 0) + offset = blockStart + block.segs.reduce((m, s) => m + segText(s).length, 0) if (block.kind === 'li') { + // The bullet is the
  • 's own list marker, so hiding the item's text with visibility:hidden + // would still leave an empty bullet showing. Hide the whole item until the caret reaches it, so + // each bullet appears with its line rather than all of them up front. The hidden item still + // reserves its height, so the card never changes size. list.push( -
  • +
  • = blockStart ? 'visible' : 'hidden' }} + > {res.nodes}
  • ) From 1912f4401d4f0717aaf7ca0512e5588693bb10c6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 4 Aug 2026 15:42:35 -0700 Subject: [PATCH 08/10] chore(blog): trim demo comments and apply cleanup (adopt cn, hoist languageMap, chip color) --- .../components/agent-peer-demo.tsx | 91 +++++++------------ apps/sim/lib/content/code.tsx | 8 +- apps/sim/lib/content/mdx.tsx | 78 ++++++++-------- 3 files changed, 74 insertions(+), 103 deletions(-) diff --git a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx index 978a789506a..96cc050ecf0 100644 --- a/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx +++ b/apps/sim/content/blog/agent-as-yjs-peer/components/agent-peer-demo.tsx @@ -3,19 +3,15 @@ import { useEffect, useRef, useState } from 'react' /** - * A small, self-contained illustration for the post: two peers editing the same document at once. - * A teammate ("Zoe") extends the intro line while the agent ("Sim") writes a short formatted block - * below — a bold lead, an inline code chip, and a bullet list, arriving already rendered. Neither one - * overwrites the other. It is a scripted animation, not a live CRDT, but it is faithful to the idea: - * both carets advance independently and both edits land. + * A small illustration for the post: two peers editing one document at once. A teammate ("Zoe") + * extends the intro line while the agent ("Sim") writes a short formatted block below (bold lead, + * code chip, bullet list). Neither overwrites the other. A scripted animation, but faithful to the + * idea: both carets advance independently and both edits land. * - * Everything is styled from the SAME design tokens the real markdown editor uses (`--font-inter`, - * `--font-martian-mono`, `--text-primary`, `--surface-5`, `--border`, `--surface-1`, `--bg`), the - * carets replicate `CollaborationCaret`'s bar + notched name label (rich-markdown-editor.css), and - * the identity colors are the real `USER_COLORS` (lib/workspaces/colors.ts). No shadows — the app - * frames surfaces with a 1px border. The finished document is always laid out; unrevealed text is - * kept `visibility: hidden` so the card never changes size as content streams in. Degrades to the - * finished document when JavaScript is off or reduced motion is requested. + * Styled from the same design tokens as the real markdown editor, with carets replicating + * `CollaborationCaret` and identity colors from `USER_COLORS`. The finished document is always laid + * out; unrevealed text is kept `visibility: hidden` so the card never resizes as content streams in. + * Degrades to the finished document with JavaScript off or reduced motion. */ type Seg = { t: string } | { code: string } | { b: string } @@ -27,16 +23,15 @@ interface Block { const FONT_SANS = 'var(--font-inter, ui-sans-serif, system-ui, -apple-system, sans-serif)' const FONT_MONO = 'var(--font-martian-mono, ui-monospace, SFMono-Regular, Menlo, monospace)' -// Two collaborators, colored from the real identity palette (USER_COLORS), as getUserColor assigns. -const AGENT = { name: 'Sim', color: '#60C5FF' } // Blue -const HUMAN = { name: 'Zoe', color: '#F472B6' } // Pink +// Colors from the real identity palette (USER_COLORS / getUserColor). +const AGENT = { name: 'Sim', color: '#60C5FF' } +const HUMAN = { name: 'Zoe', color: '#F472B6' } // The intro line already exists; the teammate is still appending to it. const HUMAN_BASE = 'Rollout is set for Friday.' const HUMAN_ADD = ' Ops signed off this morning.' -// The block the agent writes from empty, streamed one character at a time. It arrives already -// formatted: the bold lead, the code chip, and the bullets all render as they land. +// The agent's block, streamed one char at a time, arriving already formatted. const AGENT_BLOCKS: Block[] = [ { kind: 'p', @@ -52,11 +47,9 @@ const HUMAN_TOTAL = HUMAN_ADD.length const AGENT_TOTAL = AGENT_FLAT.length /** - * Builds a per-character reveal timeline with an organic, non-uniform cadence: a smooth - * multi-frequency wobble in typing speed, plus natural pauses at spaces and punctuation. Fully - * deterministic (no RNG) so the server and client agree and it never jitters between renders — the - * variation comes from the character index and content, not randomness. `times[i]` is the ms at - * which character `i` should appear. + * Per-character reveal timeline with a deterministic (no-RNG) speed wobble plus pauses at spaces and + * punctuation, so the server and client agree and it never jitters between renders. `times[i]` is the + * ms at which character `i` appears. */ function buildSchedule( text: string, @@ -75,8 +68,7 @@ function buildSchedule( return times } -// The agent streams a touch faster and steadier (it is a machine); the teammate types slower with -// bigger word/sentence pauses, and starts a beat later so the two are never metronomically locked. +// Agent streams faster and steadier; the human is slower and starts a beat later, so the two never lock in sync. const AGENT_TIMES = buildSchedule(AGENT_FLAT, { base: 34, wobble: 0.5, space: 45, punct: 150 }) const HUMAN_TIMES = buildSchedule(HUMAN_ADD, { base: 58, @@ -93,9 +85,7 @@ const countReached = (times: number[], t: number): number => { return n } -// The demo streams through ONCE when it scrolls into view, then rests on the finished document — -// no loop, no fade. `RUN_MS` is when the last character has landed; at that point the reveal is -// clamped to the finished state and the animation stops (no further frames are scheduled). +// RUN_MS: when the last character lands. The stream plays once, then clamps to the finished doc and stops. const RUN_MS = Math.max(lastTime(AGENT_TIMES), lastTime(HUMAN_TIMES)) const proseStyle: React.CSSProperties = { @@ -116,9 +106,8 @@ const codeStyle: React.CSSProperties = { padding: '0.125rem 0.375rem', } -/** Replicates CollaborationCaret in its active state: a 2px identity-colored bar with the notched - * name label above it (rich-markdown-editor.css `.collaboration-carets__bar` / `__label`). Zero - * inline width, so inserting it at the write head never shifts the surrounding text. */ +/** Replicates CollaborationCaret (rich-markdown-editor.css): a 2px identity-colored bar with the + * notched name label above it. Zero inline width, so placing it at the write head never shifts text. */ function Caret({ who }: { who: typeof HUMAN }) { return ( = start const shown = reached ? Math.min(raw.length, revealed - start) : 0 const atBoundary = !didPlace && reached && shown < raw.length @@ -230,7 +216,7 @@ function renderBlockNodes( } offset = end } - // Once the whole block is written, the caret rests at the end of the last block — the peer stays present. + // Caret rests at the end of the last block; the peer stays present. if (isLast && !didPlace) { nodes.push() didPlace = true @@ -271,10 +257,8 @@ function AgentDoc({ revealed }: { revealed: number }) { offset = blockStart + block.segs.reduce((m, s) => m + segText(s).length, 0) if (block.kind === 'li') { - // The bullet is the
  • 's own list marker, so hiding the item's text with visibility:hidden - // would still leave an empty bullet showing. Hide the whole item until the caret reaches it, so - // each bullet appears with its line rather than all of them up front. The hidden item still - // reserves its height, so the card never changes size. + // visibility:hidden on the text still leaves the
  • marker showing, so hide the whole item + // until the caret reaches it. Hidden items still reserve height, so the card never resizes. list.push(
  • (null) @@ -335,22 +317,19 @@ export function AgentPeerDemo() { typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches const el = cardRef.current - // Reduced motion, no element, or an environment without IntersectionObserver (older browsers, - // some test runners): leave the finished document in place as the fallback and never clear it. + // No IntersectionObserver / no element / reduced motion: keep the finished document, never clear it. if (reduce || !el || typeof IntersectionObserver === 'undefined') return let raf = 0 let played = false const play = () => { - // Clear to empty only now that the demo is actually on screen, so it's never left blank if the - // observer never fires (the element never reaches the visibility threshold). + // Clear to empty only once on-screen, so the doc is never left blank if the observer never fires. setHumanN(0) setAgentN(0) const start = performance.now() - // A character lands every ~30–60ms but frames fire every ~16ms, so most frames reveal nothing - // new. Track the last emitted counts and only setState on an actual change, so React renders - // roughly once per character (~90 times total) rather than every frame. + // Frames fire ~16ms but chars land every ~30-60ms; only setState when a count actually changes, + // so React re-renders ~once per character instead of every frame. let lastH = 0 let lastA = 0 const tick = (now: number) => { @@ -358,7 +337,7 @@ export function AgentPeerDemo() { if (t >= RUN_MS) { if (lastH !== HUMAN_TOTAL) setHumanN(HUMAN_TOTAL) if (lastA !== AGENT_TOTAL) setAgentN(AGENT_TOTAL) - return // rest on the finished document — no loop, no further frames + return // rest on the finished document; no loop } const h = countReached(HUMAN_TIMES, t) const a = countReached(AGENT_TIMES, t) @@ -406,7 +385,7 @@ export function AgentPeerDemo() { overflow: 'hidden', }} > - {/* header: filename + presence — mirrors Resource.Header (min-height 48px, border-b, px-4). */} + {/* header: filename + presence, mirroring Resource.Header. */}
    - {/* document body — the real prose scale (15px / 430 / 25px, --font-inter, --text-primary). */} + {/* document body: the real prose scale (see proseStyle). */}
    = { + js: 'javascript', + jsx: 'javascript', + ts: 'javascript', + tsx: 'javascript', + typescript: 'javascript', + javascript: 'javascript', + json: 'json', + python: 'python', + py: 'python', +} + export const mdxComponents: MDXRemoteProps['components'] = { img: (props: any) => ( {children} @@ -51,7 +63,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {

    {children}

    @@ -60,7 +72,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {

    {children}

    @@ -69,14 +81,14 @@ export const mdxComponents: MDXRemoteProps['components'] = {

    ), ul: (props: any) => (