Record pages go full width, cards stop nesting, and the rail stops repeating the page
A record page now uses the whole window and the conversation rail overlays it instead of taking a column out of it. Bordered surfaces may no longer contain other bordered surfaces, with a dev-time warning when they do. And the rail no longer renders a second copy of what the page is already showing — it carries a pointer. Release notes 2026-09-16 — vocion-core#372 and #378.
Status: shipped. #372, commit 6829a9bb, cut as v2.88.0, and #378, commit 29b90f44, which is on main one commit after that tag and is not yet in a release. No migrations, no environment variables, no flags in either. Part of Vocion 2.88.
Three changes to the same surface, all of them from someone using the product and saying what was wrong with it.
Full width, and the rail lies on top
"Can we use full width by default here?"
A record page used to share the window with the conversation rail: open the rail and the record got narrower. On a page whose job is to show a deal, a lead or a brief, that is the wrong trade — the rail is a thing you open to talk, not a permanent tenant.
The rail is now an overlay. The page keeps the whole width; the rail lies over the right edge of it; and the page's right gutter grows by exactly the rail's width while it is open, so the content slides clear of it rather than being occluded, and slides back the moment it closes. The width the rail is taking is published as one CSS variable at the root of the document, which is zero when the rail is collapsed, zero on a narrow viewport where the rail is a sheet instead, and zero when it unmounts.
There is one exception to collapsed-by-default, and it is the right one: a record with a decision waiting opens the rail by itself. Hiding the decision behind a tab on a page whose masthead reads "Ready for review" is not something to make somebody discover. That rule lives in one place rather than in each page that mounts a rail.
The scroll bug, and where it actually was
The rail's composer sat below the fold until you scrolled the page — a bug that had survived a couple of attempts to fix it in the rail's own styles.
The rail was not the problem. The lead page mounted its own rail from inside the shell's page gutter, and that gutter is a CSS container. A CSS container becomes the positioning parent for anything fixed inside it, so the rail's geometry was being measured against a padded, width-capped column instead of against the window. Every fix applied to the rail was correcting a symptom of where it was mounted.
Every rail now renders into the document body regardless of which page mounted it, which puts them all in the same frame. The container query on the page gutter stays, because it is wanted for what it was added for.
Bordered surfaces may not contain bordered surfaces
A card inside a card inside a card is the most reliable way to make a dense page look like a filing cabinet. This release makes it a rule with something checking it.
A bordered surface is now a named component, and it counts its own depth. Render one inside another and, in development, the console says so by name:
Nested bordered surface:
<Surface name="guided review">is rendered inside another Surface. A bordered surface never contains another bordered surface — nest with hairlines, spacing and eyebrows instead.
It warns once per surface, so a list of forty rows prints one line rather than forty. It is a development-time warning, not a thrown error — the page still renders, because breaking a customer's screen over a design rule is a worse outcome than the nested border.
What replaces the inner card is a section primitive: a hairline rule, a small uppercase eyebrow, and spacing. Structure without a second box.
Be clear about the coverage. The guard sees only what renders through that component, and two component trees use it today. Around thirty other files still hand-roll the same border classes directly, and the guard is blind to every one of them. The rule is written down and enforced where the new primitive is used; converting the rest is ongoing work, not something this release finished.
Asking for a change, without hoping the wording lands
Select a passage on a record page and the popover now offers two things: Ask about this, and Add change.
Add change arms the turn with a @change tag — the same composer tag mechanism as @artifact, shown as a chip you can see and remove. It is the difference between the system guessing from your phrasing whether "make this shorter" is a question about the draft or an instruction to rewrite it, and the system being told. The wording heuristic survives as the fallback for an untagged message; with the tag, the ask is a revision, and it targets the passage you highlighted.
The tag is offered from the composer's (+) menu only when there is a draft in view to change, and it is stripped before the message goes to the model — it steers the turn, it is not something the model has to interpret.
A tool error you can hand to someone
The Tool error marker on an agent message was a label. It is now a button.
Clicking it expands the work timeline, opens every failed step and its parent, and shows the failure detail. Beside it, Copy details puts a short plain-text block on your clipboard — the turn, the conversation, when it happened, which tool, which delegate if there was one, and the error — with unknown written in for anything missing rather than the line being quietly dropped. It is the thing you paste into a message to whoever can fix it.
What is shown on screen has internal identifiers taken out: tenant, organisation, workspace and user ids become [id], internal sentinel names become [internal].
The rail is the conversation, never a second copy of the page
This is the change from #378, and it came from one observation while looking at a lead page with the rail open: "review cards isn't a card. I don't really understand what to do with it… and it's mixed with chat. above or below?"
The screen was showing the same four sends twice. The page rendered the outreach sequence in full, each send expandable, the decision in a sticky bar at the bottom. The rail then rendered its own sequence overview beside it, listing the same four sends, with the first one's full body and a button of its own.
"Above or below?" has no answer, because the second copy belongs to neither surface. It is not a message and it is not part of the record, and because it carries a verb it is not even inert.
The rule now: the rail carries the conversation and the agent's own output, and never re-renders what the page already shows. The page owns the record — its facts, its content and its verbs. The rail owns the talking about it.
So on a record page the rail shows the transcript and, at the top of it, one line in the transcript's own voice naming what is under discussion, with a text link that scrolls the page to it. No overview, no send bodies, no buttons. The decision verbs were already on the page's sticky bar and stay there.
Two details worth being exact about:
- the rail's "Looks good · send 2 next" control is not deleted — it is no longer rendered beside a record page, because it was a read receipt for a walkthrough that only makes sense where there is no page to read the sends on. It remains the live control in full-page chat, where there is no record beside it;
- asking for a rewrite still works, and the result lands on the page. The rail says "I rewrote Day 3 …" — that is the agent reporting its own work — and the page shows the new copy marked edited. Reprinting the send in the rail to prove it happened would be the second copy all over again.
Whether the page beside the rail already shows the record is decided once, from the record the page declares to the shell. Nothing is threaded down the component tree for two surfaces to divide the work between them, and a page about a different record does not silence the rail — there is a test that holds exactly that line.
Still open: a record page with a decision waiting still opens the rail by itself, which now means opening it to show a pointer. Whether that is still the right default belongs with the work on the right column, and it is deliberately not answered here.
Where this lives
- Overlay geometry and the rail:
packages/core/src/features/dashboard/AppShell.tsx,features/dashboard/chat/ChatDock.tsx,chat/dockState.ts - The surface guard:
packages/core/src/components/ui/surface.tsx @change:features/dashboard/chat/composerTags.ts,features/comments/CommentLayer.tsx- Failure detail:
packages/core/src/libs/chat/redact.ts,features/dashboard/chat/WorkTimeline.tsx - The pointer and the predicate:
features/personalization/SequencePointer.tsx,features/personalization/draftRevision.ts,services/chat/pageContext.ts - The written rules:
docs/design/patterns.mdinvocion/vocion-core