One File, Two Shapes — A List That Does Not Know Its Length
5 minSo far the values changed but the shape did not. Real screens are not like that — nobody waiting and three waiting need to show different things.
Where it splits
{ "type": "conditional", "condition": "{{empty}}",
"then": { "type": "text", "text": "Nobody is waiting", ... },
"orElse": { "type": "box", ... } }True takes then, false takes orElse. Both are ordinary nodes, so anything can go in either.
Using {{empty}} here is the point. It is not {{waiting == 0}}. Who decides the condition is the server, not the screen — if the definition of "empty" changes (say, reservations stop counting), only the server changes.
Rows without a count
{ "type": "list", "items": "{{rows}}",
"itemTemplate": { "type": "linear", "direction": "horizontal", "distribution": "spaceBetween",
"children": [
{ "type": "text", "text": "{{item.no}}", ... },
{ "type": "text", "text": "{{item.since}}", ... } ] } }items points at an array; itemTemplate is the shape of one row. How many rows there are is not written anywhere.
Inside the template, {{item.*}} reaches that row's values, so they never collide with the state outside.
Put three in {{rows}} and there are three; put ten and there are ten. The file stays 1,234 bytes.
The same file, rendered twice
This part's claim is that one file becomes two shapes. One capture cannot show that.
await render('step5', {'empty': true, 'waiting': 0, 'rows': <dynamic>[]},
as: 'step5_empty', ...);
const rows = [
{'no': '44', 'since': 'waiting 2 min'},
{'no': '45', 'since': 'waiting 5 min'},
{'no': '46', 'since': 'waiting 9 min'},
];
await render('step5', {'empty': false, 'waiting': rows.length, 'rows': rows},
as: 'step5_filled', ...);Then it counts how many rows actually got built.
final built = [
for (final r in rows)
if (find.text(r['no']!).evaluate().isNotEmpty) r['no']!
];step5_empty 1234 B, 16 lines, type "page" -> step5_empty.png
empty state shows: 1 placeholder, 0 rows
step5_filled 1234 B, 16 lines, type "page" -> step5_filled.png
filled state shows: 0 placeholder, 3 rows (44, 45, 46)Counting matters because a wrong count still looks plausible. Draw two of three rows and the card still looks full.
grep -qE '"(44|45|46)"' screens/step5.json && die "step5: the rows are baked in"
grep -q '"itemTemplate"' screens/step5.json || die "step5: no row template"
cmp -s captures/step5_empty.png captures/step5_filled.png && \
die "step5: the conditional did not change anything"
grep -q 'empty state shows: 1 placeholder, 0 rows' $LOG || \
die "step5: the empty branch is wrong"
grep -q 'filled state shows: 0 placeholder, 3 rows (44, 45, 46)' $LOG || \
die "step5: the list did not build one row per item"Replace {{item.no}} with 44 and run: the first line fires.
Five parts
step1 233 B, 4 lines, type "page" -> step1.png
step2 906 B, 12 lines, type "page" -> step2.png
step3 one file, two states, two pictures (27577 B vs 24734 B)
step4 pressed -> the host was asked for: queue.next, queue.take
step5 empty state shows: 1 placeholder, 0 rows
filled state shows: 0 placeholder, 3 rows (44, 45, 46)
live ui://desk — 546 B, 6 lines, type "page", title "Desk"
the server now reports {"waiting":2} after one press
5 steps + the live loop · each checked against its own claim · one host, five screens, no widgets written by handThat last line is the point of the track. Five screens do five different things and there is one piece of drawing code. The rendering part of the host has not changed since part 1; what grew is the body of onToolCall and the lines that write down what a capture cannot see.
Where the three tracks meet
course-server six parts opens tools, refuses, serves the screen as a resource, says when it changed
course-client five parts connects, asks, calls, receives the screen, subscribes
course-ui five parts that screen becomes a pictureAll three are one waiting room. The server's desk.admit is the client's callTool, and the JSON the server serves at ui://desk is the same grammar as this track's screens/*.json.
The last check settles whether that is true. host/test/live_test.dart launches the server track's finished server, reads ui://desk — a screen that is not in screens/ — renders it as it came, and presses the Admit one that screen was carrying.
ui://desk — 546 B, 6 lines, type "page", title "Desk"
nothing about this screen is on disk here; it arrived just now
opened with 3 waiting
pressed Admit one
the server now reports {"waiting":2}One press took 3 to 2, and the check also confirms the two captures around it differ. Copy the server's screen into this folder and [ -e screens/desk.json ] fires — rendering a local copy would empty this part of its claim.
The client track uses no mock server — all five steps launch the server track's actual server and attach to it. So when the two tracks drift, a check fires. It did once, in client part 3, and that is how a refusal message that had been wrongly collapsed on the server side got found.
Run it yourself
cd content/sample/course-ui
bash verify.sh
open captures/step5_empty.png captures/step5_filled.pngWhat to take away
conditional— take the condition as state, not as an expressionlist+itemTemplate— write the shape of one row- Count the rows — a wrong count still looks plausible
Next
Three tracks covered one waiting room. From next month this screen goes down to where people actually stand — the warehouse, the shift handover, the till with the network gone.
Run the sample
cd course-ui bash verify.sh