Summary
native/harness/src/ui.zig is 1637 lines. frame() alone is ~580; paintToolRun ~240; paintThinking ~120. Product paint still lands here after earlier extractions (busy_row, chip_preview, model_picker, transcript_split, thinking_collapse, rect_spinner).
A src/ui/ directory is feasible. main.zig only imports ui.zig (onInit / onDeinit / frame / BUILD_ID). Split is a mechanical move of already-bounded fns — not a redesign.
Why this works
- Paint helpers are already named and mostly take
src + args (paintToolRun, paintThinking, paintLastUserChip, paintStatusSlots, paintSkillAttached, paintStatusChip).
- Same pattern as
#607 / rect_spinner.zig and #645 / chip_preview.zig.
- Zig allows
src/ui.zig and src/ui/*.zig as siblings (@import("ui/toolrun.zig") from the facade).
build.zig does not special-case ui.zig by path.
Hard part (do not ignore)
File-level vars couple the paint: transcript_scroll, tool-run / thinking open maps, composer_last_h, chip (msg_content_y, last_user_slot, prev_chip_visible), prompt_buf. Those need a single src/ui/state.zig (or stay on the facade and get passed in). Do not clone globals per file.
Proposed layout
Keep src/ui.zig as the public facade (onInit, onDeinit, frame, BUILD_ID). Move bodies under src/ui/:
| File |
From ui.zig |
ui/state.zig |
file-level vars + resetTranscriptScroll / open-map clears |
ui/scroll.zig |
isNearBottom, clampScrollToContent, scrollToBottom |
ui/kinds.zig |
kindLabel, kindTextColor, kindFill, lifecycleLabel |
ui/chrome.zig |
chrome*Font, paintStatusChip, toolRunClipboard |
ui/toolrun.zig |
paintToolRun, isCommandLikeRun, detailUsesMono |
ui/thinking.zig |
paintThinking, thinkingPreview |
ui/chip.zig |
paintLastUserChip |
ui/status.zig |
paintStatusSlots, truncateStatusValue, truncateToWidthPx, utf8CharLen, statusPackMaxWidth |
ui/skill.zig |
paintSkillAttached |
ui/composer.zig |
composer constants, clearPrompt, submitText |
frame() stays on the facade until a later pass. Do not split it in the first PR unless it is still the only thing in ui.zig and still huge.
Goal
- Same pixels, same ids, same one-frame settle. Operator-visible behavior unchanged.
main.zig import path unchanged (@import("ui.zig")).
- Each moved file stays dvui-paint only — no new bridge protocol, no DOM.
Non-goals
- Behavior / palette / protocol changes.
- Rewriting
frame() layout math.
- Moving already-extracted modules (
busy_row, chip_preview, model_picker, …) into ui/.
src/ui/root.zig rename (keep the facade file so import sites stay put).
Proof
zig fmt on the new tree.
- Existing harness tests still green (
test-rich, layout tests that import busy_row / picker — they should not need ui.zig).
build-harness artifact still loads; smoke: composer hug, status pack, tool-run expand, thinking collapse, last-user chip, stick-to-bottom.
Suggested next: create-plan (one PR, mechanical).
Summary
native/harness/src/ui.zigis 1637 lines.frame()alone is ~580;paintToolRun~240;paintThinking~120. Product paint still lands here after earlier extractions (busy_row,chip_preview,model_picker,transcript_split,thinking_collapse,rect_spinner).A
src/ui/directory is feasible.main.zigonly importsui.zig(onInit/onDeinit/frame/BUILD_ID). Split is a mechanical move of already-bounded fns — not a redesign.Why this works
src+ args (paintToolRun,paintThinking,paintLastUserChip,paintStatusSlots,paintSkillAttached,paintStatusChip).#607/rect_spinner.zigand#645/chip_preview.zig.src/ui.zigandsrc/ui/*.zigas siblings (@import("ui/toolrun.zig")from the facade).build.zigdoes not special-caseui.zigby path.Hard part (do not ignore)
File-level
vars couple the paint:transcript_scroll, tool-run / thinking open maps,composer_last_h, chip (msg_content_y,last_user_slot,prev_chip_visible),prompt_buf. Those need a singlesrc/ui/state.zig(or stay on the facade and get passed in). Do not clone globals per file.Proposed layout
Keep
src/ui.zigas the public facade (onInit,onDeinit,frame,BUILD_ID). Move bodies undersrc/ui/:ui.zigui/state.zigresetTranscriptScroll/ open-map clearsui/scroll.zigisNearBottom,clampScrollToContent,scrollToBottomui/kinds.zigkindLabel,kindTextColor,kindFill,lifecycleLabelui/chrome.zigchrome*Font,paintStatusChip,toolRunClipboardui/toolrun.zigpaintToolRun,isCommandLikeRun,detailUsesMonoui/thinking.zigpaintThinking,thinkingPreviewui/chip.zigpaintLastUserChipui/status.zigpaintStatusSlots,truncateStatusValue,truncateToWidthPx,utf8CharLen,statusPackMaxWidthui/skill.zigpaintSkillAttachedui/composer.zigclearPrompt,submitTextframe()stays on the facade until a later pass. Do not split it in the first PR unless it is still the only thing inui.zigand still huge.Goal
main.zigimport path unchanged (@import("ui.zig")).Non-goals
frame()layout math.busy_row,chip_preview,model_picker, …) intoui/.src/ui/root.zigrename (keep the facade file so import sites stay put).Proof
zig fmton the new tree.test-rich, layout tests that importbusy_row/ picker — they should not needui.zig).build-harnessartifact still loads; smoke: composer hug, status pack, tool-run expand, thinking collapse, last-user chip, stick-to-bottom.Suggested next:
create-plan(one PR, mechanical).