Repository navigation
Let scene cards take a form, an outline and a colour - #1498
Merged
milanb17 merged 30 commits intoNov 4, 2026
Merged
Conversation
An animation's scene draws every card as the same box, so a reader cannot tell a database from the service in front of it or a third-party API from one of ours without reading each title. Cards (and a hand-made wb-node) now take `form`, `outline` and `color`: - form: box, pill, cut, cylinder, drum, stack, window or bars, drawn by the stage kit with borders and pseudo-elements so tones, highlights and the card's content keep working. - outline: solid, dashed or dotted; dashed reads as outside the system. - color: ink, accent, teal or violet at rest; a frame's tone takes over while set. teal and violet join the stage theme from the software map's storage colour and the merged purple. The kinds stay out of the API: the typings and the authoring instructions suggest which form fits a client, a service, a database, a cache, a queue or a third party, and the request-across-layers and cache-fill-evict references draw their cards that way. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
A playing animation gave no sign of when the next frame would come. Now the marker under the step showing is drawn left to right over the frame's duration, on a wash of the full underline: the one mark that says "this step" also says how much of it is left. A frame without labels sweeps a ring around its dot instead. The frame player keeps the clock: play starts it from where a pause left the same frame, the cleanup of each run notes how far it got, and a frame reached by hand has none. Time up with the stage still settling holds the line full; replay starts over. The strip and the dots draw it with a CSS animation from inline custom properties, so nothing ticks in React, and reduced motion shows the whole underline at once as before. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Ports #1459 onto the animations branch, where the block it changed had since split into a frame player, a clip player and a paint-in. An animation with `height: "auto"` changed height as the reader stepped, because the stage resized to its current frame, and the caption row moved when one caption wrapped and the next did not. Now a hidden copy of the stage, transparent and inert at the same width, shows every frame of the scenario as instant jumps within one task and reports the tallest; the visible stage takes the larger of that and its current frame. The copy is made again when the stage, scenario, theme, width or a slot's content height changes, gets the visible stage's slot heights, and posts nothing but `measured`. A new stage keeps its starting height until measured, or for a second. Every stop's caption shares one grid cell with only the current one visible, in both players. What a frame's handlers leave behind in the copy never reaches the stage the reader sees: the copy is thrown away. Clips have one timeline and are not measured. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Expand put the animation in the fullscreen overlay and let its stage take the overlay's width, so a scene spread out while its cards, text and arrows stayed their inline size. Now the stage keeps the width it had inline, so the author's layout and the measured height hold, and the block scales it up to fit the room the controls leave, at most 2x, centred. A room smaller than the inline stage scrolls as before. Code peeks drawn over the stage scale with it; their slot heights are read from layout, unscaled, so the stage sizes them as it did. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: 01a1a2b2-83cc-7700-a32e-af3103dae5ea Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605 Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d
The scene grid stretched every card to fill its column, so a scene folded to two columns drew 640px cards around a line of text and left its edges only the column gap. A card is now as wide as its content, between 140px and 300px, and sits mid-column; the edges take the room it gives up. A table keeps its column, and a scene's own CSS still sets any width it wants, as the cache reference does for its end cards. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: 01a1a2b2-83cc-7700-a32e-af3103dae5ea Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605 Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d
Two cards linked both ways got their edges fanned 9px apart by moving only the curve's control point, so each of the pair bowed; now the offset moves the whole line, ends and control alike, and a pair is two straight parallel lines. Scene rows centre their cards instead of top-aligning them, so an edge between a short card and a tall one runs level. A frame drawn without its motion, a new stage's first or the one after a jump or Replay, never moved when Play reached it: the reader waited out its duration on a still picture. Play now shows such a frame again with its motion. A send's chip travels at half its old speed, 900 to 2400 ms, after a short lead, so it can be read and fills more of its frame. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: 01a1a2b2-83cc-7700-a32e-af3103dae5ea Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605 Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d
…itself
Three things an agent could not say in a scene. `s.group({ id, at, title,
color })` draws a washed, titled box behind the cards in its cells, a
boundary such as a service or a layer; it hides when the scene folds,
since its cells no longer sit together. `change: "added" | "modified" |
"removed"` on a card marks what the change under review did to that
part, as its dot and a stripe down its left edge in the change colour at
rest, struck through when removed; a frame's tone still colours the whole
card over it. `s.legend([...])` draws one row under the scene with a
glyph per entry, a card drawn with the entry's own marks at 0.4 scale,
and its label.
The typings and the instructions describe all three, the request
reference groups its server, marks the API as modified and carries a
legend, and the cylinder's text sits centred in its body rather than
high under the lid.
Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683
Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Agent-Session: 01a1a2b2-83cc-7700-a32e-af3103dae5ea
Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605
Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d
Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
…tate A cylinder's or drum's text sits in the middle of it, as a label on a tank does, and a pill's too. The dot at a card's title was a tone indicator that showed grey on every card, so it read as a mark with no meaning; it now shows only when a tone or a change mark gives it one, in that colour. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: 01a1a2b2-83cc-7700-a32e-af3103dae5ea Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605 Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad
A scene that shows what the new scene marks are for: the client, the edge, our services and our storage as four groups, each card in the form of its kind, the stores in teal, the queue in violet, the CDN dashed as the one part we do not run, and a legend that decodes them. An upload walks down through the layers and a playback walks back out through the CDN, one send per frame. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: 01a1a2b2-83cc-7700-a32e-af3103dae5ea Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605 Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b
…' chips An edge that crossed a card bowed clear of it by half the card's longest side, so a horizontal edge over a wide card arced 150px up and out of the stage. The bow now uses the card's reach across the line, its extent along the normal, and a card sitting dead on the line is passed on the side nearer the body's middle rather than always to the left. A chip another frame sent could stay on its edge when the reader stepped or switched scenario before it faded. Showing a frame now hides every other frame's chip, except one told to stay. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: ef04727a-5f7d-4c56-a793-c4f98a02f605 Agent-Session: 534581db-1710-40c6-b043-ce052bd9e80d Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b
Groups bleed into the gaps around their cells, so with the scene's 28px rows the layers touched and every edge between them was squeezed through a slot too narrow to follow. A scene with groups now opens its rows and columns to 72px and its vertical padding to 32px, and the gap a row's labels ask for keeps that floor. A card within 12px of an edge's line is passed on the side nearer the stage's middle, so a top-row edge dips into the drawing rather than arcing over it. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
Beside the tour's pane the stage column is narrower than the inline column, and the stage took that width: a scene folded to one column, its groups hid, and its height grew past 1200px. That height could then be what the block measured and kept. Fullscreen, the stage now always lays out at its inline width and scales to fit the room, down to 0.4x, so the drawing and its measurement are the same ones the reader saw inline. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
The stage sized itself from the bottom of every visible element, and in Electron's Chromium a zoomed element's getBoundingClientRect comes back in unzoomed units, so a legend glyph at zoom 0.4 read as sitting 2.5x further down than it was. An animation with a legend got a stage more than twice its drawing's height, with the blank below. The glyph is now a 50x32 card scaled by a transform inside a 20x13 box, which measures where it is drawn. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
Scaled to 0.4, the glyph's 1px borders, lid and rule marks and change stripe thinned to under half a pixel and the dashed third-party glyph vanished. They are drawn 2.5x heavier so they land at a pixel. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
Painting cards over their group box gave them a z-index, which also put them over the edge and chip layers that had painted above them, so a travelling chip went behind the cards it passed. The two layers now sit above the cards, as before the groups. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
A send's chip faded out a quarter second after arriving, so what it carried was gone before it could be read, and it reappeared on a step back. It now waits at the head until the frame changes, when the next frame hides every other frame's chips; `stay: false` fades it on arrival for a chip that must not cover its target. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
One row per layer and every hop between neighbouring cells: the app over the gateway with the CDN beside it, the three services under the edge row, each store under the service that uses it. No edge crosses a card; the one curve is the CDN's fetch from the blob store. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad
A row of two cards could not sit centred under a row of one on whole columns. A scene may now declare twice the columns and place every card two wide; the fold rule counts the columns the cards make, not the ones declared, and only a card wider than that unit takes a folded row. The video platform reference puts its edge layer centred under its client this way. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad Agent-Session: c2a12499-97dd-4d8c-a7ef-f76a01d39f01
A chip's ride tweens a progress value from where it is to 1. After it had ridden once, a timeline built again, as changing scenario does, tweened it 1 to 1, so from then on the chip appeared at its head without moving. The rebuild resets every ride to 0 first. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad Agent-Session: c2a12499-97dd-4d8c-a7ef-f76a01d39f01
A frame drawn without motion, on load or by a jump, landed its sends at their heads, so a fresh board showed the first frame's chip already arrived. The chips now sit at their origins, visible and ready to be sent; Play or a step forward rides them. A chip told to stay keeps its head. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad Agent-Session: c2a12499-97dd-4d8c-a7ef-f76a01d39f01
A send's chip showed whichever had happened last: it waited at its origin when the frame was drawn still, sat at the head after a ride, and was hidden after a jump. Same frame, three pictures. A frame is a state, so its messages now rest as labels on their edges, which take the marker colour, in place of the edges' own labels. However the reader reaches the frame, that is what it shows. Stepping into the frame rides a chip from the sender while the label is quiet; at the head the chip dissolves into the label. The send option `stay` goes, since a message no longer has a head to wait at; a raw travel keeps it. A sent message's label no longer dodges its chip's swept path, since it is quiet while the chip rides, so it stays by its edge at rest. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: b5f9f841-35c5-4ec1-95a8-6485f64d254b Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad Agent-Session: c2a12499-97dd-4d8c-a7ef-f76a01d39f01 Agent-Session: 01a1a36e-2277-7060-97f7-bf84f55e1905
The queue, state machine and request timing references drew every part as a plain box. The producer and workers are now bars, the queue a violet stack, the start and final states pills, and the request's stops a window, bars, a drum and a teal cylinder, as the scene references and the instructions already do. Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad Agent-Session: 32e129b9-586e-4fd2-8961-e7224c5978ad Agent-Session: c2a12499-97dd-4d8c-a7ef-f76a01d39f01 Agent-Session: 01a1a36e-2277-7060-97f7-bf84f55e1905
Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
The fold's fit test counted the columns a half-step scene's cards make, but the fold still drew the declared count, so a six-column scene folded to three squeezed columns with its placement and groups thrown away. A scene now folds in the columns its cards make, and keeps its layers: each declared row wraps into lines of fewer cards, balanced and centred on half-step tracks, a wider part takes a line of its own, and a group spans the lines its cards wrap into or hides when another card shares one. The measuring copy runs a scene's due layout before it measures, so it sizes the folded layout. VITE_ANIMATION_WIDTH shows the references narrow. Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Folding a scene when the block narrows reflowed the drawing: rows mixed, groups vanished, edges crossed. A scene now keeps its declared columns at any width. It sets its own minimum width from the columns its cards make and their gap, the stage reports the width it needs with its size, and the block keeps the stage that wide and scrolls it sideways in a narrower block. The responsive option and the fold classes go. Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Narrow, the strip became a four-column grid of wrapped labels, two or more rows deep. It now stays one row at any width, scrolls sideways when its steps do not fit, and brings the step showing into view without scrolling the page. Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
…amp captions The stage now reports how far its content reaches past its right edge, whatever drew it, and the block keeps the stage at least that wide and scrolls it in a narrower block; a scene's minimum width is one such content. The block title no longer wraps below the narrow breakpoint but truncates as every diagram title does, and a step's caption stops at three lines. Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: ffed49d6-ca86-4940-b93c-e8eb7e0de440
The wash bled 18px above its cells and the title sat 4px under that edge, so a micro-caps title ended at the card's top and a highlighted card's ring covered it. The top bleed is 28px now. Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: ffed49d6-ca86-4940-b93c-e8eb7e0de440
…-e33a0d' into mb/scene-card-forms Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683 Agent-Session: ffed49d6-ca86-4940-b93c-e8eb7e0de440 Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad
thesiti92
approved these changes
Nov 4, 2026
milanb17
added a commit
that referenced
this pull request
Nov 4, 2026
Stacked on #1498 (the clip block and the reference animations live on that stack, so this is based on `mb/scene-card-forms`). After publishing an animation an agent had no way to see what it drew: `session_get` says only whether the script ran, so agents fell back to driving the screen. `session_render({ sessionId, targetId, frame })` shows one frame of an animation (or a clip at one marker; omitted, the first) on the whiteboard open in Desktop and returns it as a PNG image, with the error the stage reported, if any. The PNG is also written to `<home>/animation-renders/<session>/<block>/<key>.png` and named in the result, for agents that read files rather than images (`whiteboard api session_render` prints the JSON without the base64). Step 5 of "Work in files" in `animation.md` tells agents to render each frame they changed after publishing, instead of taking screenshots. What the agent gets back, from a scene animation and a clip published through `whiteboard api` into a dev Desktop:    ```json {"sessionId":"2d67401e-…","targetId":"block-1","type":"animation","frame":{"key":"query","title":"Postgres answers","position":2,"count":4},"clipped":false,"error":null,"file":"…/animation-renders/2d67401e-…/block-1/query.png"} ``` Over MCP the same call answers an `image` content block (the PNG) ahead of that text. ## How a frame is rendered: Desktop captures its own canvas Two ways were weighed. (A) The attached Desktop shows the frame on the open board and captures the block's rectangle with Electron's `capturePage`. (B) The review server renders the stage headless. This PR does A: - A frame as the reader sees it exists only in Desktop. The code peeks and embedded blocks a stage shows are native Monaco editors and canvas components that Desktop's host bridge lays over the stage; a headless renderer would have to rebuild that bridge or draw something else. - The headless server has no browser. `whiteboard server start` with an attached Desktop renders through the same relay `session_open` uses; without one the route answers 409. - The costs, and how they show: the board must be open in Desktop (the error says to call `session_open`); the window must be on screen (a covered or minimized window paints nothing and throttles its timers, so the canvas fails fast with a message saying so rather than waiting out the relay); the reader sees the frame change; a figure taller than the window is captured as far as it fits, with `clipped: true`. ## The pieces - **Protocol**: verb `renderAnimation {reviewId, targetId, frame}`; `ReviewCanvasHandle.renderAnimation` and `ReviewRenderedFrame`. - **Canvas**: `animation/stage-renderer.ts` keeps the stage blocks on a board by id. `StageBlock.show(key)` waits for the stage to boot and paint in, switches to a scenario that has the frame when the one showing does not, picks the stop with the new `PickedBy` `"render"` (playback stops; a clip pauses), waits for `settled` or the clip's `marker`, scrolls the figure into view, waits for it to hold still, and reports its rectangle in CSS px. The stage port now notes the frame or marker last asked for, and the block notes what the stage reports showing, so a render waits until they agree. - **Fork**: `reviewCanvasPart.ts` answers the verb for the board it shows (main window only), scales the rectangle by the window's zoom and calls `hostService.getScreenshot(rect, { format: "png", maxEdge: 1600 })`. The native host's `getScreenshot` gained those two options; JPEG stays the default, and an empty capture answers `undefined`. - **Server**: `review-api/animation-render.ts` holds the query and result schemas and `renderTarget`, which checks the frame against the block's frames or markers (naming them when it is wrong) and picks the first scenario's first frame when omitted. `GET /:id/render` in `http.ts`; `relayReviewCallbacks.render` in `review-server-core.ts`. The `/control/result` bound is 16 MiB so the PNG can ride back. - **Agent boundary**: the tool is listed with animation authoring (`session_checkout` beside it); `public-tools.ts` writes the file; `mcp.ts` returns the image block; `agent-cli.ts` keeps the base64 out of stdout. ## Verification - Node: `animation-render.test.ts` (target resolution, the route with a stub Desktop, the tool listing, the file and the image split), `render-relay.test.ts` (the relay callback: result parsing, Desktop's refusal and a missing Desktop as 409s), plus the existing authoring, public-tools, agent-client and authoring-capabilities tests. - Browser: `stage-renderer.browser.test.tsx` mounts the retry-backoff reference, renders `a3fail` (only in its second scenario), checks the reported rectangle against the figure and that Playwright's capture of it is a non-empty PNG; lands a clip on a marker at rest; reports unknown stops and blocks. The clip, paint, blocks and references suites pass (100 tests). - Desktop: `pnpm desktop:build` and `desktop:run` under an isolated `DEV_REVIEW_HOME`, animations enabled, a scene animation and a clip published through `whiteboard api`. `session_render` returned the captures above, exact to the figure, for frames in either direction and for markers, and the errors for a frame the block lacks and for a hidden window. Not checked: dark theme, a zoomed window, secondary windows. - `pnpm lint`, `pnpm format`, both typechecks and the fork's `typecheck-client` are clean. The screenshots live on `pr-assets/render-animation-frames`; delete that branch with this PR.
milanb17
added a commit
that referenced
this pull request
Nov 4, 2026
Since #1498, a hand-made animation whose `wb-node` elements are placed absolutely and take a `data-form` draws every node in normal flow: the nodes stack and overlap, and the SVG arrows drawn at fixed coordinates point at empty space. `animation.md` invites `data-form` on a `wb-node` in a hand-made layout, so this is the documented use. The cause is the kit's `:is(.wb-card, .wb-node)[data-form] { position: relative; }`, which weighs two classes (0,2,0) and beats an agent's `.node { position: absolute }` (0,1,0). ## The fix The form, change, outline and colour rules in `animation-stage-kit.ts` now match through `:where(.wb-card, .wb-node)[…]`, so they weigh one class: - They tie with the base `.wb-card, .wb-node` rule and win by coming after it, so a form's padding, radius and `::before`/`::after` still draw when the agent sets nothing. - An animation's own CSS comes after the kit (it is in the body, the kit in the head), so any single-class rule an agent writes wins. - The change marks move with them, so the kit keeps its order among these rules: a colour still sets the border after a change stripe, as before. - Tones and highlights keep their two-class weight and still take over during a frame. The scene's own cards are unchanged. `.wb-scene > .wb-card` keeps them relative, and the legend glyphs have their own two-class rule. The resting frames of `request-across-layers` and `cache-fill-evict` (window, bars, cylinder, drum, change stripe, legend) are byte-identical before and after. ## Test `blocks.browser.test.tsx` adds a 720×420 `.architecture` layout with two `node wb-node` cards (`data-form="bars"`) at `#a{left:12px;top:10px}` and `#b{left:262px;top:162px}`. The stage reports through the code slot's width whether both kept their position. The test fails on main and passes with the fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1483 (the references and the scene API live there).
An animation's
whiteboard.scenedraws every card as the same box, so a reader cannot tell a database from the service in front of it, or a third-party API from one of ours, without reading each title. This gives cards three presentational options and leaves the kinds out of the API: the typings and the authoring instructions say which form fits what, and two references draw their cards that way.The options
On
SceneCardOptions(actor, store, table), and asdata-form,data-outlineanddata-coloron a hand-madewb-node:formbox(default),pill,cut,cylinder,drum,stack,window,barscorner-shape: bevel, a lid pseudo-element for the cylinder and the drum, a box-shadow stack, a title-bar rule, side rulesoutlinesolid(default),dashed,dottedcolorink,accent,teal,violettonetakes over while settealandvioletjoin the stage theme as--wb-teal(the software map's storage colour) and--wb-violet(the merged purple), so both follow light and dark. Highlights, tones,enter/exit/onlyand the card's content work unchanged on every form; the stack keeps its echoes under the highlight ring.The option is called
formrather than shape because lint reserves "shape" in identifiers.Suggested defaults, not a schema
whiteboard.d.tsand the scene section ofanimation.mdsuggest:windowfor a client,barsfor a service or worker of this codebase,cylinderfor a database,drumfor a cache,stackfor a queue or topic,pillfor a start or end,cutfor a branch,outline: "dashed"for a third-party service,tealfor stores andvioletfor queues. Nothing in the runtime knows those words.The request-across-layers and cache-fill-evict references use them; their READMEs say so.
Verification
pnpm lint,pnpm formatand the canvas typecheck (which holdswhiteboard.d.tsto the runtime's types) are clean.VITE_ANIMATION_SHOTS=1), dark theme. Not checked: light theme and an isolated Desktop. The screenshots live onpr-assets/scene-card-forms, aspr-assets/home-pr-pillsdid; delete that branch with this PR.Second commit: a frame's remaining time along its step underline
While an animation plays, the marker under the step showing is drawn left to right over the frame's duration, on a wash of the full underline, so the one mark that says "this step" also says how much of it is left. Frames without labels sweep a ring around the current dot. Designed first on the "Play timer" page of the Animations Scratchpad Paper file.
--frame-timer-fill,--frame-timer-ms,--frame-timer-rest), so nothing ticks in React. Reduced motion shows the whole underline at once, as today.blocks.browser.test.tsx: play runs the clock on the current step, pause holds it with a fraction between 0 and 1, a hand step drops it; and an unlabelled animation sweeps its current dot.Third commit: steady height, ported from #1459
#1459 could not merge onto the animations branch mechanically, since the block it changed had split into a frame player, a clip player and a paint-in. Its changes are ported here by hand: the hidden measuring copy (
measure/slotSizeson init,measuredback),contentBottomandmeasureFramesin the runtime, the block'smeasuredHeightrule with its one-second fallback and remeasure triggers (stage, scenario, theme, width, slot heights), and the caption row keeping its tallest caption, now inStopCaptionfor both players. Clips are not measured. Its tests come along; the animation and block browser suites pass (97). #1459 can be closed once this merges.Fourth commit: an expanded animation scales up
Expand gave the stage the overlay's width but never made the drawing bigger. Now the stage keeps its inline width fullscreen and the block scales the whole canvas, code peeks included, to fit the room the controls leave, at most 2x, centred; a room smaller than the inline stage scrolls as before. The measured-height logic is untouched because the stage still lays out at one width. One browser test expands a block in a wide host and checks the stage scales above 1x, keeps its inline layout width, and its footprint grows with it.
Seen while checking, not from these commits: a scene with four declared columns folded to two rows at a 614px stage keeps a 150px stage height, so the second row is clipped, inline and expanded alike. Reproduced on the commit before the steady-height port. Worth its own look.
Fifth to seventh commits: scene layout, pacing, groups, change marks and a legend
s.group({ id, at, title, color })draws a washed, titled box behind the cards in its cells; hidden when the scene folds.change: "added" | "modified" | "removed"on a card marks what the review did to that part: dot and left stripe in the change colour, struck title when removed. A frame's tone still colours the card over it.s.legend([...])draws one row under the scene, a card-shaped glyph with the entry's marks and a label each.animation.mddescribe all of it; request-across-layers groups its server, marks the API as modified and carries a legend. The cylinder form's text sits centred in its body.Later commits: layout room, routing, the state-only dot, an architecture reference
video-platform-architecture: four grouped layers, forms, a dashed CDN and a legend, with upload and playback scenarios.