Chat views are templates — the first emission waits on no data
The platform rule (core Doc/GUI/DataBinding → Templates first, data later): a layout area is a
template. It emits its whole control tree on the first render, the platform's loading shape
covers whatever has not arrived, and the values are bound — never read on the hub and baked
into the controls.
The shape every converted area uses
public static UiControl Thumbnail(LayoutAreaHost host, RenderingContext _)
{
var template = BuildThumbnailTemplate(host.Hub.Address.ToString(), host.Localize("chat.noMessages"));
return host.Workspace.GetMeshNodeStream()
.Select(node => ProjectThumbnail(node, options, access)) // pure: node → view record
.DistinctUntilChanged() // an equal record is not republished
.Bind(_ => template, ThumbnailViewId); // fed live into /data/{id}
}
Build…Templateis pure: every view is a control, every value a relativeJsonPointerReferenceinto the projection record.Project…is pure: the branches the area used to take while building controls (editing prompt or message, user or assistant, cells or none) are now fields of the record. A part the record does not have is hidden by its bound style (display: none;), so the tree's shape never depends on the data.- Every converted area dedupes its projection with
DistinctUntilChanged()beforeBind.Binditself skips a valueEqualsto the last one, so for these scalar-only records the operator is belt and braces — kept on every area alike so none depends on that detail. - A list inside a view (the compact streaming view's tool-call chips and delegation links) is a row
list of its own, published by
stream.BindMany(id, row => …)and rendered by a bound item template — a label per chip, aNavLinkper delegation, each row's text, href and colour a bound value. No markup is built on the hub, so nothing has to be HTML-encoded to stay safe. - Labels are resolved once on the render turn in the viewer's language; a count is a glyph and a
number (
💬 3), which needs no translation.
| Area | Before | Now |
|---|---|---|
Thread History |
node + child query → hand-built HTML cards | title bound to ThreadHistoryView (with the "Thread" fallback) + MeshSearch the GUI runs |
Thread Thumbnail |
node → card with interpolated HTML | template bound to ThreadThumbnailView |
Message Streaming |
node → stack built per emission | template bound to StreamingView; chips and delegation links are bound row lists |
Message Overview |
node .Take(1) → editing or message tree, frozen |
both shapes declared; OverviewView chooses, live |
Message Edit |
node .Take(1) → editor |
editor + Submit at once; draft buffer seeded when the message arrives |
Message Thumbnail |
node → card with interpolated HTML | template bound to MessageThumbnailView |
The draft buffer behind the editors (/data/editText) is new input — what the person is about to
resubmit — not a replica of the node: it is seeded once, read only by Submit, and never written back
to the message. "Once" is per area: the Overview (for an editing prompt) and the Edit area each seed
the buffer of their own layout host — each area is its own host with its own data store — so one
seed can never reach the other's draft, and after the first seed a later emission of the node never
writes the buffer again, so a typed draft has no writer on the hub to lose it to (pinned live, with a
negative control, by TheDraftBuffer_IsSeededOncePerArea_AndALaterNodeEmissionNeverReseedsIt). Resubmit without an edit reads the message's current text at the click, not a
render-time snapshot.
Nested slots that remain, and why
ThumbnailPreview(thread hub): the last cell's compactStreamingview. It chooses only WHICH address to embed (structure), so it renders in its own nested slot and the card around it waits on nothing.
The changed-nodes lists — row-scoped Undo
The thread Header, the thread Changes page and the message's MessageChanges slot list the nodes
a thread (or one message) changed: · v → v · Diff · Undo. They used to
be controls rebuilt on the hub for every change — one closure per row — because a button inside a
bound row could not say which row it was in. With row-scoped actions (core Doc/GUI/DataBinding →
Row-scoped actions) they are templates:
ThreadLayoutAreas.BindModifiedNodesdeclares the block once — a heading, a bound item template of rows and an optional Undo-all — and feeds it from the change aggregation (ModifiedNodesViewfor the block,ModifiedNodeRowper row, both pure projections).- Each row's Undo is ONE button, declared in the row template. Its action (
UndoRow) reads the row from the click (ctx.RowAs<ModifiedNodeRow>()): the row the person saw, never one re-read by position, so a list that grew while a round streamed still undoes the change that was clicked. A click with no row undoes nothing. - Undo all (message) and the Changes page's bulk Undo belong to no row: they read the rows the block shows at the click.
- The Header's frame is seeded at once from what the path says (a delegation's back-link) and widens when the aggregation reports changes; the Changes page declares its empty state and its bulk action up front and the projection chooses.
Native clients: the React ItemTemplate does not yet stamp the row on a click, so a row's Undo is a
no-op there until it does. The React chat renders messages from the thread node and embeds none of
these areas.
Verified, not converted
| Area | Why it stays |
|---|---|
Thread ThreadChat / Thread |
already a template: a static ThreadChatControl bound to a projected ThreadViewModel in /data |
Thread Overview (progress), Streaming |
structure only — the node decides which message's area to embed; no value is baked |
User/Threads inbox (ThreadInboxLayoutAreas) |
NOT converted to row-scoped actions on purpose: installed phone clients render it, and their ItemTemplate posts a click with no row, so per-row Close / Reopen / Delegated-tasks would stop working there. It waits for the React client to stamp the row (and for those clients to update) |
Composer Composer, Selectors |
every value is already node-bound; the node is read only to choose the binding root (standalone composer or the thread's inline one) |
Tests: MeshWeaver.AI.Test/ChatSurfacesAreTemplatesTest — every template has no deferred view and
binds only projection properties; the projections make the decisions (the History title keeps the
"Thread" fallback for a nameless thread); and against a real mesh the thread card's bound title follows a
rename, the streaming view's chip and link rows follow the message, and a later node emission never
re-seeds the draft buffer. MeshWeaver.AI.Test/ModifiedNodesRowActionsTest — the
changed-nodes templates are static and the row binds only row properties; against a real mesh,
clicking Undo in row k undoes change k for every k and nothing else; a no-row click undoes nothing
(negative control); Undo all undoes every row.