githubnext/chopinPublic

Let's compose rich plans together

Colour a fence, and draw a patch as the change it is #8

MergedKrzysztof-Cieslakmerged 2 commits intomainfromrefs/pull/8/head
  1. KC

    A fence is rendered beside the source it came from, by [`@pierre/diffs`](https://diffs.com) over Shiki, loaded on demand. A `diff` fence is rendered as the change it describes.

    ## The model is unchanged

    The source stays where it was — Lexical text children under Yjs, merged per character, editable by everyone at once. What is drawn from it is a second reading, never the document:

    - The library's **edit mode is off**, and must stay off. Turning it on would put a character model of its own beside the one the room is collaborating in, so two people typing would each be correcting a copy. - The **server never loads any of this**. It goes on validating and serialising an ordinary fenced code node, headless. - **Nothing new is persisted.** `data/<room>/plan.mdx` holds the same fence it always did, byte for byte.

    In a room: one person types, Yjs merges, everybody's renderer redraws from the source it now has. Changing the language is an edit like any other and everybody's copy is recoloured. Hiding the source is local to one reader, as collapsing already was.

    ## A fence with no language draws nothing

    Uncoloured text above the same uncoloured text is two of the same thing with a caret in only one of them, so `/code` opens a fence with a **language selector** and no preview. Choosing from it writes the language into the fence.

    The selector is the reason the control earns its place: nothing else could name a fence, since the markers are not in the document. A language the list does not offer — one the agent wrote — is kept, shown, and coloured if the highlighter knows it.

    ## A patch is repaired on the way to the renderer

    Nobody writes a hunk header correctly, least of all a model quoting a change it is proposing, and the counts in one are how a parser knows where the hunk ends — so a count that is too low truncates the change **silently**. `widgets/code.ts` recounts them from the lines that are actually there, reads a stripped blank line as the unchanged line it was, and takes off the `a/` and `b/` that the renderer only removes for git's own form.

    What comes out is handed over and thrown away; the fence keeps every byte the author wrote. Anything still malformed is refused by the parser and drawn as coloured diff text, which is the honest rendering of a fragment that was never a patch.

    ## Enter works in a fence now

    It did nothing at all before: Lexical asks a block to make its own successor and a code block returns null. No newline, and no way past a fence sitting at the end of a plan — a corner a reader could be typed into. Enter is a line break there now, and Enter on a line that is already blank leaves, taking the blank line with it.

    ## Cost

    | | | |---|---| | eager bundle | **+3.5 kB gzip** | | the renderer | **171 kB gzip**, fetched only when a plan has a rendered fence | | a grammar | 5–30 kB gzip, per language actually used |

    `dist` grows to 16 MB because Shiki emits every grammar as its own chunk. None are fetched unless a fence asks for one.

    ## Checks

    `bun run ci`, `bun run types`, `bun test` (525) and `bun run e2e` (49, nine new) all pass.

    The new unit tests cover what a fence says about itself and every shape of patch, with no browser and no renderer loaded. The browser tests cover the parts that need one: colour, the selector reaching the file, a patch drawn as a change, a fragment drawn as text, Enter, and hiding the source.

    Three of them put **two people in one fence**, because a rendering is a projection of a shared text and that is the part worth proving. They type at opposite ends and both end up in it, and in each other's rendering; a language chosen by one colours everybody's copy; hiding the source hides nobody else's. Playwright drives one page at a time, so those are edits that interleave rather than collide — a merge of two genuinely simultaneous keystrokes is `apps/web/src/collab.test.ts`, which runs two providers against a real server and can hold one back.

    ## Traps found on the way, written up in `agents.md`

    - A grammar the highlighter does not have **rejects a promise nobody is watching**, leaving the block blank rather than uncoloured. The language is resolved first, and falls back to plain text. - **One preview slot, two writers.** A passive effect's cleanup runs after React has committed the next renderer's nodes into the slot, so clearing it on the way out took them with it. Everything in `data-plan-preview` is React's now, including the markup KaTeX and Mermaid produce as strings. - **`ControlOrMeta+Home` is not the start of the document on a Mac**, and a keystroke a platform does not recognise is nothing at all, followed by typing that lands where the caret already was. Two people aiming at opposite ends of a fence both wrote at the end of it, which reads exactly like a merge that lost an edit. - **A `<select>` in the document answers to `getByRole("option")`** — the same role the slash menu uses. Option locators in the browser suite have to stay scoped to the menu.

  2. Krzysztof-Cieslakmerged 6fa35b365fef3 months ago on GitHub

  3. Krzysztof-Cieslakclosed3 months ago on GitHub

  4. Krzysztof-Cieslakhead-ref-deleted3 months ago on GitHub