Skip to content

harness: split native/harness/src/ui.zig into src/ui/* #652

Description

@btipling

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestharnessHarness / agent UI

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions