Plan header
| Field |
Value |
| Status |
HANDOFF-READY |
| Date |
2026-08-22 |
| Type |
single |
| Parent |
N/A |
| Source issue |
#779 — harness: Busy ■ Stop is pushed off the right edge of the canvas |
| Branch |
plan/stop-on-canvas (or main) |
| Layers |
harness |
| Reusability impact |
none (in-canvas chrome; no infra/config seam) |
| Production mutate? |
no |
| Cloud ops path |
N/A — no Production mutate |
| Living docs |
docs/harness-limits.md (Layout / Touch·mobile / Composer chrome rows + the iconCellW value) |
Human decision (2026-08-22) — cap approval
The operator approved the cap raise for plan #782 (one-word "approved", the confirmation form
established by #781). Sign-off: the trailing-icon reserve raise TOUCH_H×n + 8 (88 busy / 48 idle)
→ iconCellW×n + 8 (128 busy / 68 idle), +20 px/icon, implicitly accepting the all-width field
narrowing (idle 552→532, busy 512→472 @ 600 px) that is the tradeoff keeping ■ on-canvas at ~390 px.
This clears the plan-review BLOCK on cap governance and moves #782 BLOCKED → HANDOFF-READY. The
60 value is non-discretionary (a TOUCH_H-only reserve would re-truncate the real 60-px button);
TOUCH_H/SUBMIT_CAP/MAX_ITEMS themselves are NOT raised.
Review notes (2026-08-22)
Verdict: HANDOFF-READY. The single human-gated item (cap-governance) is RESOLVED — human
approved (2026-08-22). Everything else was verified-sound at plan-review and carries over as-is.
Baseline — all verified live (against main @ 393e5b3, the queue-pause merge):
| Claim |
Check |
Stop = Busy trailing ■; SEND id 0x70_0700, STOP id 0x70_0750, gravity_y = 1.0, min_size_content TOUCH_H×TOUCH_H |
✓ composer_chrome.zig 189–217 (busy) / 217–234 (idle) |
Reserve currently iconReserveW = TOUCH_H×n + 8 (88 busy / 48 idle); buttons set no .padding/.margin, so they inherit dvui defaults |
✓ iconPackW/iconReserveW/fieldW 85–98; button opts have no pad/margin override |
dvui ButtonWidget defaults = padding 6 / margin 4 (both sides) → real footprint TOUCH_H + 2·10 = 60 |
✓ zig-pkg/.../ButtonWidget.zig:16–21 (defaults: .margin = Rect.all(4), .padding = Rect.all(6)) |
| @ ~390 px the two 60-px buttons overrun → compactor crushes ■ flush to the right edge |
✓ mechanism matches plan; reserve under-counts 20 px/icon |
Composer laid out at true viewport width (avail_w = avail.w); queue band gets max_float_safe width → cannot widen canvas |
✓ ui.zig 714–716 (chrome .avail_w = avail.w); queue_band.zig:102 (max_size_content.w = dvui.max_float_safe) |
Tests: composer_layout.test.zig has WIN_LW = 600 only — no ~390 px row |
✓ const WIN_LW: f32 = 600 (line 31); no 390 anywhere |
Docs: harness-limits.md "Icon button" row documents the reserve as TOUCH_H×n + 8 px (40/square) |
✓ line 92; no dedicated Caps-table section (caps are inline per-row) |
No bridge export change → no public/harness/harness.wasm copy needed |
✓ in-scope files are wasm-paint only |
Findings
| Sev |
Axis |
Finding |
Fix applied to issue body |
BLOCK RESOLVED — human approved (2026-08-22) |
Cap governance |
iconReserveW is an existing effective cap/budget (documented TOUCH_H×n + 8 = 88/48) and the fix raises it to iconCellW×n + 8 = 128/68 (+20 px/icon), narrowing the field at every width (idle 552→532, busy 512→472 @ 600 px) — not only fixing ~390 px. |
Operator approved the cap raise (2026-08-22); recorded in the Human decision section. Caps table verdict → ✅ APPROVED by human |
| Minor |
Cap governance framing |
Plan asserted "not a raise or lower" — that is plan-self-justifying. iconCellW is documented as a NEW constant, but its net effect is a raise of the existing reserve budget token; the human decision must cover both the number AND the all-width field narrowing. |
Caps table note reworded to state the change is a reserve raise (not merely a NEW-constant entry), with the +20 px/icon delta and all-width effect made explicit — now covered by the human approval |
| Minor |
Docs |
Plan/Living-docs/DoD reference adding a "Caps table" to harness-limits.md, but that file documents caps inline (no separate Caps section exists). |
Living-docs row + Implementation step 3 + DoD reworded: place iconCellW (= 60) inline in the Composer chrome / Icon-button rows (file style) or add a small ### Caps values subsection — never claim a pre-existing Caps table |
| Minor |
Testing |
Existing WIN_LW = 600 assertions (fieldW(WIN_LW, …), send.x per row) will have changed expected values after the reserve raise (send.x shifts left by the delta). The "existing #737 idle lock preserved" wording must not read as "unchanged numbers." |
Added an explicit Implementation-order + Testing note that the 600 px row assertions are updated to the new reserve (field 552→532 idle, 512→472 busy), not merely retained |
Scores: Correctness 5 · Performance 5 (pure layout arith, no I/O/loops) · Architecture 5 · Testing 4 · Cloud ops N/A (no mutate, correctly asserted) · Living docs 4 · Cap governance RESOLVED (human approved 2026-08-22) · Layer/UI 5 (in-canvas chrome only; no dual-chat; palette unused) · Reusability N/A (no seam) · Parent N/A (single issue).
Blocked item → RESOLVED: the single human-gated caps-table decision was approved by the
operator (2026-08-22). Plan is HANDOFF-READY for implementation (restore stashed WIP → gates →
PR).
Summary
While a turn is Busy, the composer's trailing icon pack is ▶ enqueue + ■ Stop. On a
~390 px phone-width canvas the ■ was crushed flush into the right edge of the viewport
(its tag rect slid past win.w) — un-hittable, so the operator cannot tap-cancel the
in-flight turn from the width they are actually using. Root cause (layout-rect proven, not
assumed): the trailing-icon reserve counted only TOUCH_H (40) per button, but each
ButtonWidget's real tag-rect footprint includes dvui's default padding (6) + margin (4)
per side, i.e. TOUCH_H + 2·10 = 60. The reserve under-counted by 20 px/button, so the
two Busy buttons overran the row and dvui's horizontal compactor crushed the rightmost ■
off-canvas. This plan ships the full-footprint reserve (fieldW = avail_w − (n·iconCellW + 8)
with iconCellW = TOUCH_H + 2·(pad 6 + margin 4)), pins each button's padding/margin to the
same constants the arithmetic uses so the two can never drift, and locks the geometry with
new ~390 px on-canvas layout regressions.
Goals
| # |
Goal |
Success signal |
| 1 |
Busy ■ Stop is fully inside the visible canvas |
stop.x ≥ 0 and stop.x + stop.w ≤ viewport.w at ~390 px and at 600+ px (left rail open or closed) |
| 2 |
Busy ▶ enqueue stays on-canvas too |
Same inequality for the send tag; ▶ left of ■, no overlap with the field |
| 3 |
Idle ▶ Send stays on-canvas |
No regression of the #737 idle lock |
| 4 |
Queue non-empty (Pause + Clear visible on the queue-band header) does not push Stop off-screen |
Busy on-canvas with n>0, paused and unpaused, including · full |
| 5 |
Stop still cancels only the in-flight turn |
Hit target works; #757 / #760 queue semantics unchanged |
Non-goals / out of scope
Architectural decisions
N/A — no new decision. This is a pure in-canvas chrome width-reservation fix with no
ownership, protocol, persistence, deploy, or reuse change. The layer is unchanged (harness).
Layer placement
| Concern |
Layer |
Path(s) |
Rationale |
| Composer chrome trailing ▶/■ reserve + layout |
harness |
native/harness/src/ui/composer_chrome.zig |
In-canvas chrome; host shell owns nothing here |
| On-canvas geometry regression locks |
harness |
native/harness/src/composer_layout.test.zig |
dvui testing-backend rect asserts |
| No-h-scroll / touch limits doc |
harness |
docs/harness-limits.md |
Living limits table |
Current baseline (live code)
| Claim |
Path / symbol |
Notes |
| Stop is the Busy trailing ■ |
native/harness/src/ui/composer_chrome.zig busy branch (SEND id 0x70_0700, STOP id 0x70_0750) |
Idle = one ▶; Busy = ▶ then ■. gravity_y = 1.0 bottom-pinned, min_size_content TOUCH_H×TOUCH_H (40). Buttons set no padding/margin → inherit dvui defaults (6/4) |
| Pre-#779 reserve under-counted the footprint |
iconPackW/iconReserveW/fieldW |
Reserve was TOUCH_H×n (48/88 + 8 margin), but real ButtonWidget tag-rect width is TOUCH_H + 2·(default padding 6 + margin 4) = 60 → overrun → compactor crushes ■ off-canvas at ~390 px |
| dvui default button chrome |
vendored ButtonWidget.zig defaults |
.margin = Rect.all(4), .padding = Rect.all(6) — confirmed in the shipped 0.5.0-de sequence |
| Icons must stay on-canvas (prior lock, still closed) |
plan #737 (closed) / source #734 |
Field on a reserved sub-rect so a long unbreakable line cannot steal the icon columns |
| Pause/Clear queue-band header (above composer) |
PR #778 (merged, plan #777) |
Plan must include it in the ~390 px worst case so the header does not widen the canvas (queue_band width = avail.w with max_size_content.w = max_float_safe) |
| Layout tests |
native/harness/src/composer_layout.test.zig |
WIN_LW = 600 only before this plan — no ~390 px row, hence no regression caught. The 600 px rows assert fieldW(WIN_LW, …) and send.x — their expected values update under the new reserve |
| Docs |
docs/harness-limits.md Touch / mobile + Composer chrome |
“No horizontal overflow expected” — under-specified for Busy ~390 px; the Icon-button row documents the reserve as TOUCH_H×n + 8 (40/square), which this plan updates |
Design
- Reserve the FULL button footprint, not
TOUCH_H. Define
ICON_PAD = 6, ICON_MARGIN = 4, ICON_EDGE_W = ICON_PAD + ICON_MARGIN (=10).
iconCellW() = TOUCH_H + 2·ICON_EDGE_W (=60). iconPackW(busy) = (2|1)·iconCellW().
iconReserveW(busy) = iconPackW(busy) + TE_MARGIN_RIGHT(8). fieldW(avail_w, busy) = @max(0, avail_w − iconReserveW(busy)).
- Pin each trailing button's options to those constants. On all three button sites
(Busy ▶ enqueue, Busy ■ Stop, Idle ▶ Send) set .padding = {x,y,w,h}=ICON_PAD and
.margin = {x,y,w,h}=ICON_MARGIN so the widget footprint is EXACTLY what the reserve
arithmetic counts — the two can never drift apart.
- Field wrapper already reports
min=max=fieldW. paintComposerChrome keeps the
trailing-reserved sub-rect (composer-field-wrap, FIELD_WRAP_ID) as a packed child
(gravity.x = 0), so the box's leftover math gives the field at most
avail_w − iconReserveW and the icons always land post-reserve at their full cell.
The textEntry width max is fieldW (min 120 stays a floor that yields to the cap on
narrow widths); scroll_horizontal = false lets break_lines fire at the field width.
- Queue-band header isolation: the queue band (Pause ≥64 / Clear ≥56 +
{n} queued +
optional · full/· paused) paints in its own band (queue_band.paint(…, avail.w) in
ui.zig) above the composer; max_size_content.w = max_float_safe means it cannot
widen the canvas. Composer is laid out at the true viewport width. The regression test
covers the worst-case header present so a future min-width growth is caught.
- Cap note:
TOUCH_H stays 40; SUBMIT_CAP / MAX_ITEMS unchanged. The reserve
budget does raise (TOUCH_H×n + 8 → iconCellW×n + 8, +20 px/icon), narrowing the
field at all widths. This raise was one-word-approved by the operator (2026-08-22) —
see Human decision + Caps table.
Cloud ops path
N/A — no Production mutate. Harness-only layout chrome; no data/secret/deploy/env
cutover. Zig gates run in the agent workspace / CI as usual (no new GHA work).
Living docs plan
| Surface |
Change |
Notes |
docs/harness-limits.md Layout row |
Restate: trailing ▶ / ■ stay fully on-canvas at ~390 px and wider (left rail open/closed), including with the queue-band Pause/Clear header visible (plan #779) |
timeless |
docs/harness-limits.md Composer wrap/grow row |
#734 bound updated to full footprint: field_w = avail_w − (n·iconCellW + 8), iconCellW = TOUCH_H + 2·(pad 6 + margin 4); note why a TOUCH_H-only reserve crushed ■ at ~390 px |
timeless |
docs/harness-limits.md Icon button row |
Reserve value → n × (TOUCH_H + 2·(pad 6 + margin 4)) + 8; never crushed flush to the edge even on ~390 px Busy |
timeless; caps here are inline (file style) |
docs/harness-limits.md iconCellW value |
Add iconCellW = TOUCH_H + 2·ICON_EDGE_W = 60 inline in the Composer chrome / Icon-button rows (this file carries caps inline, no separate Caps table) — or add a small ### Caps values subsection if preferred |
rationale + code location; not a pre-existing Caps table |
AGENTS.md |
N/A — no infra rule / ownership table change; ownership unchanged (harness) |
|
README.md |
N/A — visitor-facing behavior unchanged |
|
SECURITY.md / .env.example |
N/A — no secrets or new env |
|
Caps table
| Cap / ceiling |
Value |
Rationale |
Code location |
iconCellW (full trailing-button footprint) |
TOUCH_H + 2·(pad 6 + margin 4) = 60 px |
Reserve-vs-real-footprint reconciliation for plan #779; the reserve raises TOUCH_H×n + 8 → iconCellW×n + 8, +20 px/icon, narrowing the field at all widths (idle 552→532, busy 512→472 @ 600 px). TOUCH_H itself is NOT raised; this is a width-reservation budget raise, not a product touch-target/capacity change. Carrier is canvas-local pixels (no wire). 2 busy cells + 8 margin = 128 ≤ the ~390 px floor, so no new narrow ceiling — no over-ceiling in play |
native/harness/src/ui/composer_chrome.zig ICON_PAD/ICON_MARGIN/ICON_EDGE_W/iconCellW |
✅ APPROVED by human (2026-08-22). The operator approved the reserve raise TOUCH_H×n + 8 →
iconCellW×n + 8 (+20 px/icon, implicit all-width field narrowing). No further decision
required; this plan is HANDOFF-READY for implementation. Outside width budget (a plan that
reserved only TOUCH_H for a real 60-px button would re-truncate ■) is the inviolable floor that
forces this particular raise; the 60 value is not discretionary.
Implementation order
composer_chrome.zig: add ICON_PAD/ICON_MARGIN/ICON_EDGE_W, redefine
iconPackW/iconReserveW/fieldW on iconCellW(), pin button padding/margin on all
three sites; update the module doc-comment.
composer_layout.test.zig: parameterize the layout helper to accept an arbitrary content
width (a global width slot) and add ~390 px locks: Busy empty-queue, Busy non-empty
queue (Pause + Clear header), Busy paused/· full, idle narrow + 600. Update the
existing 600 px row assertions to the new reserve (field 552→532 idle, 512→472 busy;
send.x shifts left by the delta) — do not leave stale numbers.
docs/harness-limits.md: update Layout / wrap-grow / Icon-button rows + add the
iconCellW value (inline or a ### Caps values subsection) in the same PR as the
code — never deferred to a later docs-only pass.
- Gates:
zig fmt --check, zig build test-rich, zig build test-rich-invariants,
zig build harness -Doptimize=Debug.
Testing
| # |
Case |
Layer |
Type |
Command / method |
| 1 |
Busy, canvas ~390 px, empty queue |
harness |
layout unit |
zig build test-rich — assert stop.x ≥ 0 and stop.x + stop.w ≤ win.w; assert send.x + send.w ≤ stop.x |
| 2 |
Busy, ~390 px, queue non-empty (Pause + Clear header) |
harness |
layout unit |
worst-case header; ■/▶ still on-canvas; header does not widen window |
| 3 |
Busy, ~390 px, paused + · full |
harness |
layout unit |
still on-canvas |
| 4 |
Idle ▶ at ~390 and 600 (left rail 40 / 220) |
harness |
layout unit |
existing #737 tests repointed to the new reserve + a narrow row; ▶ on-canvas |
| 5 |
Operator: Busy turn on a phone-width /harness |
browser |
operator |
■ tappable and cancels the turn; Esc still works |
Gates (min locked rows): zig fmt --check, zig build test-rich (composer_layout),
zig build test-rich-invariants, zig build harness -Doptimize=Debug. Since bridge
exports do not change, no public/harness/harness.wasm copy / wasm-int rerun is
strictly required unless the Debug build is used for local wasm-int fuzz.
Definition of done
Risks & mitigations
| Risk |
Mitigation |
| Reserve math and button options drift apart again |
Constrain the arithmetic AND the widget options to the same ICON_PAD/ICON_MARGIN/ICON_EDGE_W constants + comment the invariant |
| Queue-band header min-width grows and re-widens the canvas |
Locked at ~390 px in test 2/3 so any future growth is caught; header stays max_size_content.w = max_float_safe (cannot widen) |
| Field min 120 floor leaves too little room below ~200 px canvas |
Out of scope (sub-phone widths); fieldW clamps at 0 so reserve still wins |
| Laptop-only cutover / missing GHA |
N/A — no Production mutate (explicit) |
Open questions
- None — the caps-table reserve raise (BLOCK) was approved by the operator (2026-08-22) and is
resolved. Plan is HANDOFF-READY.
References
Approved by operator 2026-08-22 (cap raise). Status BLOCKED → HANDOFF-READY. Deliverable was the reviewed plan-issue body update only; implementation is a separate /implement_plan invocation.
Plan header
docs/harness-limits.md(Layout / Touch·mobile / Composer chrome rows + theiconCellWvalue)Human decision (2026-08-22) — cap approval
The operator approved the cap raise for plan #782 (one-word "approved", the confirmation form
established by #781). Sign-off: the trailing-icon reserve raise
TOUCH_H×n + 8(88 busy / 48 idle)→
iconCellW×n + 8(128 busy / 68 idle), +20 px/icon, implicitly accepting the all-width fieldnarrowing (idle 552→532, busy 512→472 @ 600 px) that is the tradeoff keeping ■ on-canvas at ~390 px.
This clears the plan-review BLOCK on cap governance and moves #782 BLOCKED → HANDOFF-READY. The
60 value is non-discretionary (a
TOUCH_H-only reserve would re-truncate the real 60-px button);TOUCH_H/SUBMIT_CAP/MAX_ITEMSthemselves are NOT raised.Review notes (2026-08-22)
Verdict: HANDOFF-READY. The single human-gated item (cap-governance) is RESOLVED — human
approved (2026-08-22). Everything else was verified-sound at plan-review and carries over as-is.
Baseline — all verified live (against
main@ 393e5b3, the queue-pause merge):0x70_0700, STOP id0x70_0750,gravity_y = 1.0,min_size_contentTOUCH_H×TOUCH_Hcomposer_chrome.zig189–217 (busy) / 217–234 (idle)iconReserveW = TOUCH_H×n + 8(88 busy / 48 idle); buttons set no.padding/.margin, so they inherit dvui defaultsiconPackW/iconReserveW/fieldW85–98; button opts have no pad/margin overrideTOUCH_H + 2·10 = 60zig-pkg/.../ButtonWidget.zig:16–21(defaults:.margin = Rect.all(4),.padding = Rect.all(6))avail_w = avail.w); queue band getsmax_float_safewidth → cannot widen canvasui.zig714–716 (chrome.avail_w = avail.w);queue_band.zig:102(max_size_content.w = dvui.max_float_safe)composer_layout.test.zighasWIN_LW = 600only — no ~390 px rowconst WIN_LW: f32 = 600(line 31); no 390 anywhereharness-limits.md"Icon button" row documents the reserve asTOUCH_H×n + 8px (40/square)public/harness/harness.wasmcopy neededFindings
BLOCKRESOLVED — human approved (2026-08-22)iconReserveWis an existing effective cap/budget (documentedTOUCH_H×n + 8= 88/48) and the fix raises it toiconCellW×n + 8= 128/68 (+20 px/icon), narrowing the field at every width (idle 552→532, busy 512→472 @ 600 px) — not only fixing ~390 px.iconCellWis documented as a NEW constant, but its net effect is a raise of the existing reserve budget token; the human decision must cover both the number AND the all-width field narrowing.harness-limits.md, but that file documents caps inline (no separate Caps section exists).iconCellW(= 60) inline in the Composer chrome / Icon-button rows (file style) or add a small### Caps valuessubsection — never claim a pre-existing Caps tableWIN_LW = 600assertions (fieldW(WIN_LW, …),send.xper row) will have changed expected values after the reserve raise (send.x shifts left by the delta). The "existing #737 idle lock preserved" wording must not read as "unchanged numbers."Scores: Correctness 5 · Performance 5 (pure layout arith, no I/O/loops) · Architecture 5 · Testing 4 · Cloud ops N/A (no mutate, correctly asserted) · Living docs 4 · Cap governance RESOLVED (human approved 2026-08-22) · Layer/UI 5 (in-canvas chrome only; no dual-chat; palette unused) · Reusability N/A (no seam) · Parent N/A (single issue).
Blocked item → RESOLVED: the single human-gated caps-table decision was approved by the
operator (2026-08-22). Plan is HANDOFF-READY for implementation (restore stashed WIP → gates →
PR).
Summary
While a turn is Busy, the composer's trailing icon pack is ▶ enqueue + ■ Stop. On a
~390 px phone-width canvas the ■ was crushed flush into the right edge of the viewport
(its tag rect slid past
win.w) — un-hittable, so the operator cannot tap-cancel thein-flight turn from the width they are actually using. Root cause (layout-rect proven, not
assumed): the trailing-icon reserve counted only
TOUCH_H(40) per button, but eachButtonWidget's real tag-rect footprint includes dvui's default padding (6) + margin (4)
per side, i.e.
TOUCH_H + 2·10 = 60. The reserve under-counted by 20 px/button, so thetwo Busy buttons overran the row and dvui's horizontal compactor crushed the rightmost ■
off-canvas. This plan ships the full-footprint reserve (
fieldW = avail_w − (n·iconCellW + 8)with
iconCellW = TOUCH_H + 2·(pad 6 + margin 4)), pins each button's padding/margin to thesame constants the arithmetic uses so the two can never drift, and locks the geometry with
new ~390 px on-canvas layout regressions.
Goals
stop.x ≥ 0andstop.x + stop.w ≤ viewport.wat ~390 px and at 600+ px (left rail open or closed)n>0, paused and unpaused, including· fullNon-goals / out of scope
TOUCH_H/SUBMIT_CAP/MAX_ITEMS(the product caps stay exactly where they are — the reserve raise below is a width-reservation budget, not a product-touch-target/capacity change)Architectural decisions
N/A — no new decision. This is a pure in-canvas chrome width-reservation fix with no
ownership, protocol, persistence, deploy, or reuse change. The layer is unchanged (harness).
Layer placement
native/harness/src/ui/composer_chrome.zignative/harness/src/composer_layout.test.zigdocs/harness-limits.mdCurrent baseline (live code)
native/harness/src/ui/composer_chrome.zigbusy branch (SEND id0x70_0700, STOP id0x70_0750)gravity_y = 1.0bottom-pinned,min_size_contentTOUCH_H×TOUCH_H(40). Buttons set no padding/margin → inherit dvui defaults (6/4)iconPackW/iconReserveW/fieldWTOUCH_H×n(48/88 + 8 margin), but real ButtonWidget tag-rect width isTOUCH_H + 2·(default padding 6 + margin 4) = 60→ overrun → compactor crushes ■ off-canvas at ~390 pxButtonWidget.zigdefaults.margin = Rect.all(4),.padding = Rect.all(6)— confirmed in the shipped 0.5.0-de sequencequeue_bandwidth =avail.wwithmax_size_content.w = max_float_safe)native/harness/src/composer_layout.test.zigWIN_LW = 600only before this plan — no ~390 px row, hence no regression caught. The 600 px rows assertfieldW(WIN_LW, …)andsend.x— their expected values update under the new reservedocs/harness-limits.mdTouch / mobile + Composer chromeTOUCH_H×n + 8(40/square), which this plan updatesDesign
TOUCH_H. DefineICON_PAD = 6,ICON_MARGIN = 4,ICON_EDGE_W = ICON_PAD + ICON_MARGIN(=10).iconCellW() = TOUCH_H + 2·ICON_EDGE_W(=60).iconPackW(busy) = (2|1)·iconCellW().iconReserveW(busy) = iconPackW(busy) + TE_MARGIN_RIGHT(8).fieldW(avail_w, busy) = @max(0, avail_w − iconReserveW(busy)).(Busy ▶ enqueue, Busy ■ Stop, Idle ▶ Send) set
.padding = {x,y,w,h}=ICON_PADand.margin = {x,y,w,h}=ICON_MARGINso the widget footprint is EXACTLY what the reservearithmetic counts — the two can never drift apart.
min=max=fieldW.paintComposerChromekeeps thetrailing-reserved sub-rect (
composer-field-wrap,FIELD_WRAP_ID) as a packed child(
gravity.x = 0), so the box's leftover math gives the field at mostavail_w − iconReserveWand the icons always land post-reserve at their full cell.The textEntry width
maxisfieldW(min 120 stays a floor that yields to the cap onnarrow widths);
scroll_horizontal = falseletsbreak_linesfire at the field width.{n} queued+optional
· full/· paused) paints in its own band (queue_band.paint(…, avail.w)inui.zig) above the composer;max_size_content.w = max_float_safemeans it cannotwiden the canvas. Composer is laid out at the true viewport width. The regression test
covers the worst-case header present so a future min-width growth is caught.
TOUCH_Hstays 40;SUBMIT_CAP/MAX_ITEMSunchanged. The reservebudget does raise (
TOUCH_H×n + 8→iconCellW×n + 8, +20 px/icon), narrowing thefield at all widths. This raise was one-word-approved by the operator (2026-08-22) —
see Human decision + Caps table.
Cloud ops path
N/A — no Production mutate. Harness-only layout chrome; no data/secret/deploy/env
cutover. Zig gates run in the agent workspace / CI as usual (no new GHA work).
Living docs plan
docs/harness-limits.mdLayout rowdocs/harness-limits.mdComposer wrap/grow row#734bound updated to full footprint:field_w = avail_w − (n·iconCellW + 8),iconCellW = TOUCH_H + 2·(pad 6 + margin 4); note why aTOUCH_H-only reserve crushed ■ at ~390 pxdocs/harness-limits.mdIcon button rown × (TOUCH_H + 2·(pad 6 + margin 4)) + 8; never crushed flush to the edge even on ~390 px Busydocs/harness-limits.mdiconCellWvalueiconCellW = TOUCH_H + 2·ICON_EDGE_W = 60inline in the Composer chrome / Icon-button rows (this file carries caps inline, no separate Caps table) — or add a small### Caps valuessubsection if preferredAGENTS.mdREADME.mdSECURITY.md/.env.exampleCaps table
iconCellW(full trailing-button footprint)TOUCH_H + 2·(pad 6 + margin 4)= 60 pxTOUCH_H×n + 8→iconCellW×n + 8, +20 px/icon, narrowing the field at all widths (idle 552→532, busy 512→472 @ 600 px).TOUCH_Hitself is NOT raised; this is a width-reservation budget raise, not a product touch-target/capacity change. Carrier is canvas-local pixels (no wire). 2 busy cells + 8 margin = 128 ≤ the ~390 px floor, so no new narrow ceiling — no over-ceiling in playnative/harness/src/ui/composer_chrome.zigICON_PAD/ICON_MARGIN/ICON_EDGE_W/iconCellW✅ APPROVED by human (2026-08-22). The operator approved the reserve raise
TOUCH_H×n + 8→iconCellW×n + 8(+20 px/icon, implicit all-width field narrowing). No further decisionrequired; this plan is HANDOFF-READY for implementation. Outside width budget (a plan that
reserved only
TOUCH_Hfor a real 60-px button would re-truncate ■) is the inviolable floor thatforces this particular raise; the 60 value is not discretionary.
Implementation order
composer_chrome.zig: addICON_PAD/ICON_MARGIN/ICON_EDGE_W, redefineiconPackW/iconReserveW/fieldWoniconCellW(), pin button padding/margin on allthree sites; update the module doc-comment.
composer_layout.test.zig: parameterize the layout helper to accept an arbitrary contentwidth (a global width slot) and add ~390 px locks: Busy empty-queue, Busy non-empty
queue (Pause + Clear header), Busy paused/
· full, idle narrow + 600. Update theexisting 600 px row assertions to the new reserve (field 552→532 idle, 512→472 busy;
send.xshifts left by the delta) — do not leave stale numbers.docs/harness-limits.md: update Layout / wrap-grow / Icon-button rows + add theiconCellWvalue (inline or a### Caps valuessubsection) in the same PR as thecode — never deferred to a later docs-only pass.
zig fmt --check,zig build test-rich,zig build test-rich-invariants,zig build harness -Doptimize=Debug.Testing
zig build test-rich— assertstop.x ≥ 0andstop.x + stop.w ≤ win.w; assertsend.x + send.w ≤ stop.x· full/harnessGates (min locked rows):
zig fmt --check,zig build test-rich(composer_layout),zig build test-rich-invariants,zig build harness -Doptimize=Debug. Since bridgeexports do not change, no
public/harness/harness.wasmcopy / wasm-int rerun isstrictly required unless the Debug build is used for local wasm-int fuzz.
Definition of done
test-richgreentest-richgreen on those tootest-rich-invariantsgreenzig fmt --checkgreen on touched Zigzig build harness -Doptimize=Debugcompiles (full Wasm gate)TOUCH_H-based cap raised (the reserve raise is the only width budget change)docs/harness-limits.mdLayout / Composer rows +iconCellWvalue updated in the same PR.env.example: N/A justifiedRisks & mitigations
ICON_PAD/ICON_MARGIN/ICON_EDGE_Wconstants + comment the invariantmax_size_content.w = max_float_safe(cannot widen)fieldWclamps at 0 so reserve still winsOpen questions
resolved. Plan is HANDOFF-READY.
References
Approved by operator 2026-08-22 (cap raise). Status BLOCKED → HANDOFF-READY. Deliverable was the reviewed plan-issue body update only; implementation is a separate
/implement_planinvocation.