Skip to content

[Feature] Rewrite packages/ui + packages/app visual layer against PawWork design system (incremental PR series) #440

@Astro-Han

Description

@Astro-Han

Note (2026-05-12): docs/design/STANDARDS.md was renamed to docs/design/DESIGN.md. Lock IDs L<n> referenced below are legacy lock numbering carried over from STANDARDS.md — they are not literal line numbers in DESIGN.md.

What task are you trying to do?

Rewrite the desktop visual layer in packages/ui and packages/app end-to-end against PawWork's internal design system, replacing the upstream opencode token system that the codebase still inherits. This is the foundation for product visual differentiation: warm neutrals plus brand orange #FF5910 capped at ≤10% accent, dense desktop 13px base, system-font stack only — a permanent fork from opencode's cool grays and Catppuccin syntax tokens.

Which area would this change affect?

UI or design system

What do you do today?

packages/ui/src/styles/theme.css and packages/ui/src/theme/themes/pawwork.json still carry the upstream opencode token system, drifting roughly 80 lines from the internal truth. Component code (button, iconbtn, input, popover, dialog, sidebar, composer, right-pane) is scattered across packages/ui and packages/app, with radius, hover, spacing, and focus-ring values inconsistent across surfaces. Every new or revised component costs CSS overrides in multiple files; touching one place risks regressing ten others. packages/ui/** and packages/app/src/{components,pages,styles,theme,i18n}/** are already permanently carved out of upstream sync, but the carve-out is a written convention only — there is no git-level merge driver protecting it.

What would a good result look like?

The rewrite is broken into phases: slices 01–10 delivered (token layer, typography wiring, motion primitives, buttons + icon-button, inputs, tags + popover + tooltip, dialog + sheet + command palette, iconography, sidebar, composer + dock + model picker), slice 11a delivered (markdown body + code/diff + tool-use renderer), and four remaining slices (11b, 16, 17a, 17b) following the 2026-05-07 audit plus the 2026-05-10 size rebalance against locked workshop standards in docs/design/DESIGN.md (L8–L42), target sketches in docs/design/ui_kits/desktop/, and the live codebase.

Slices delivered (01–10)

# Scope PR Closed
01 Token layer + carve-out merge driver + parity test #442
02 Typography wiring + icon CSS housekeeping (note: chrome icon registry deferred to slice 08) #448
03 Motion primitives, hover overlays, reduced-motion #450
04 Buttons + icon-button (four-tier API) #459
05 Inputs (TextField + Select + Switch; remove forbidden primitives) #461
06 Tags, popover, inverse tooltip #458 #221
07 Dialog + sheet + command palette #460
08 Iconography rewrite: chrome registry redraw + size API collapse #491
09 Sidebar L35 row rewrite + session row/menu/status/sort polish #495 #303
10 Composer + dock + model picker rewrite + prompt-input split #508 #185

2026-05-07 audit findings

Seven gaps surfaced when re-checking the original 11-slice plan against locked standards and ui_kits/desktop/ target sketches:

  1. Iconography (L21) deferred from slice 02. PR refactor(ui): token housekeeping + typography wiring + icon CSS (slice #02, issue #440) #448 was scoped down to CSS housekeeping; the chrome icon registry on the four-keyshape spec was not delivered. — resolved by slice 08 (feat(ui,app): chrome icon registry redraw + size API collapse (slice 08, issue #440) #491).
  2. Conversation message flow has no slice and no workshop lock. message-part.tsx is 2410 lines and session-turn.tsx is 748 lines, covering markdown body, tool-use blocks, fold/expand, streaming states, attachments. Workshop only locked L36 (code + diff); turn shape, tool-use container, folding, and markdown body are unlocked.
  3. Status & feedback (L33) has no slice. Workshop already locked; production code spans pages/error.tsx, status-panel.tsx, status-popover.tsx, scattered empty states.
  4. Kbd / keybindings hint has no slice and no workshop lock. Used by command palette, tooltips, shortcut hints.
  5. Home page reconstruction has no slice and no workshop lock. Current pages/home.tsx is a project-picker; target ui_kits/desktop/Home.jsx is a hero + composer + starters layout (structurally different, cannot fold into slice 10's session composer).
  6. Settings page reconstruction has no slice and no workshop lock. Target ui_kits/desktop/Settings.jsx plus six in-product settings sub-pages.
  7. Large multi-responsibility frontend files must be split inside the slice that rewrites their surface. Slice 10 already exposed this by splitting the prompt-input/composer code while rebuilding the input bar — confirmed delivered. The remaining rewrite should do the same for message flow, right pane, status, settings, and final shell assembly, without pulling unrelated controller refactors into the visual rewrite.

Dropped from plan: L20 logo-mark slice — packages/ui/src/components/logo.tsx already matches workshop SVG; no rewrite needed.

Deferred to separate tracking: about-modal / release-notes dialog (no design yet), onboarding / first-run (no current product surface). The full layout.tsx controller refactor is now part of slice 17b, with #474 treated as the implementation/task reference rather than a separate post-#440 cleanup.

2026-05-09 size rebalance

A second pass against historical slice PR sizes (slice 01: 4859, slice 10: 4263, others: 144–2714 lines diff) and a 3000-line soft cap for reviewable single-surface visual rewrites split the two remaining oversized slices and folded the kbd primitive into the chrome slice it serves. The result is a 7 → 8 PR plan with a per-PR cap near 3000 lines of diff, in line with reviewable single-surface rewrites in this issue's history.

  • Slice 11 split into 11a + 11b along workshop and renderer/shell boundaries. 11a delivers the reusable markdown + code/diff + tool-use renderer; 11b delivers the turn shape, message-part shell, and message-timeline split that consume that renderer. Slice 11 as originally scoped touched ~5500 lines across four sub-surfaces, well above the historical reviewable PR ceiling.
  • Slice 17 split into 17a + 17b along visual chrome vs structural rewrite. 17a delivers titlebar + branch popover + dirty-branch dialog + the kbd primitive (formerly slice 14), against locks L38/L39/L41 already in DESIGN.md plus W4 to be locked. 17b delivers the three-column shell, layout.tsx rewrite, controller decomposition ([Task] Split layout.tsx into agent-readable controllers #474), and session header redundancy reduction ([Feature] Reduce session header redundancy: keep two layers (sidebar + chat) #276).
  • Former slice 14 (kbd primitive) folded into 17a. kbd is the dependency of titlebar shortcut hints and branch popover shortcut affordances; landing it inside 17a removes a one-primitive solo PR and keeps consumers on the same lock.

2026-05-10 size rebalance (PR #514 retrospective)

Slice 11a delivered as PR #514 with 1118-line diff across 13 files, far below the 3000-line soft cap and the ~5500-line slice-11 estimate the 2026-05-09 rebalance was sized against. The gap came from estimating "complete rewrite" without crediting how much of each slice's surface reuses upstream pipelines. 11a kept marked + DOMPurify + shiki + morphdom intact and only rewrote post-processing, styling, and a small click-routing layer; the W3 lock further compressed the CSS.

Sizing rationale going forward: 3000 lines is a reviewability ceiling, not a target. Use this issue's delivered-slice median (about 1197 lines) as the anchor, then size by reuse fraction: high-reuse surfaces are usually 500–1500 lines, full surface rewrites 2000–4500 lines, and geometry/style primitives 50–500 lines. A remaining slice under roughly 600 lines should be folded into its downstream consumer unless it has a strong independent rollback boundary.

Re-measuring remaining slices against current file LOC, import boundaries, and explicit reuse fractions yields a 5 → 4 remaining-PR plan that retires slice 13 instead of treating status/feedback as one standalone surface.

  • Slice 11b absorbs former slice 12 (right pane) and the session-local half of former slice 13. Turn shape, timeline, right pane, and SessionStatusPanel are all one session-view rollback unit. SessionStatusPanel is consumed from session-side-panel.tsx, so it belongs with the right-pane rewrite rather than the final shell rewrite. Slice 12 number retired.
  • Slice 13 retired and split by consumption surface. Session-local status goes to 11b. Global StatusPopover, error page, and empty feedback go to 17b because they close out shell/global feedback behavior. This is not a blanket "status belongs to layout" rule: layout.tsx does not own the status body directly.
  • Slice 17a absorbs former slice 15 (home page). 15 alone is small (139-LOC project-picker today, target hero + composer + starters reusing slice 10 composer; estimate 500–1000) and 17a alone is also small (titlebar 165 + branch popover + dirty-branch dialog + kbd primitive; estimate 600–1200). Combined estimate 1100–2200, well within cap. Cost: W4 + W5 must lock together. Surface story is "home page + visual chrome + kbd primitive." Slice 15 number retired.
  • PR cap revisited. Estimate remaining slices by "actual rewrite + new files + tests" with reuse fraction made explicit, not by file-LOC alone. The 2026-05-09 cap was correct as a ceiling but conservative as a target; consolidating below the ceiling reduces review/merge cycles without sacrificing reviewability.

Large-file split rule for remaining slices

When a remaining #440 slice rewrites a user-facing surface, it may include the smallest necessary split of a production file that is already acting as a multi-responsibility catch-all. The split must stay inside that slice's surface boundary and should make the code agent-readable: focused files, domain names, nearby tests, and no unrelated behavior change.

This is not permission to turn every remaining slice into global architecture cleanup. The exception is slice 17b: it owns the full layout.tsx rewrite because the final three-column shell, titlebar exit-points, branch popover wiring, dirty-branch dialog flow, settings entry points, session routing, and workspace/project actions all converge on that file today. Slice 17b should absorb #474 as its implementation reference and split layout.tsx into focused shell/controllers/components while preserving user-visible behavior unless a dead path is proven unused.

Workshop prerequisites for remaining slices

Each workshop preview HTML must be locked via the docs/design/AGENTS.md single-file reconciliation workflow before the corresponding code slice may begin. New DESIGN.md line numbers L43–L48 will be assigned during reconciliation.

  • W1message-flow.html (planned L43): turn shape, fold/expand affordance, streaming visual, attachment block, message grouping → consumed by slice 11b
  • W2tool-use.html (L44): tool block container, args/output regions, status badge, error state → locked, delivered via slice 11a (PR feat(ui): markdown renderer (slice 11a, issue #440) #514)
  • W3markdown-body.html (L45): conversation-flow heading levels, list, quote, link, inline code → locked, delivered via slice 11a (PR feat(ui): markdown renderer (slice 11a, issue #440) #514)
  • W4kbd.html (planned L46): kbd element + keybindings hint container → consumed by slice 17a
  • W5home.html (planned L47): hero + composer + starters → consumed by slice 17a
  • W6settings.html (planned L48): main page + sub-pages → consumed by slice 16

Slices active / planned (11a delivered; 11b, 16, 17a, 17b remaining)

Each slice ships in its own worktree branch, lands as a merge commit, and carries before/after light + dark dev:desktop screenshots plus a targeted golden-path E2E run. Strict serial — each must land on dev before the next branches.

11a. Markdown body + code/diff + tool-use renderer (L36 + L44 + L45). Delivered via PR #514 (1118-line diff, 13 files). Extracted and rewrote the markdown, code-block, diff, and tool-use rendering primitives previously embedded in packages/ui/src/components/message-part.tsx into focused reusable pieces. Workshop: W2 + W3 — both locked.

11b. Session view rewrite (L36 + L42 + L43; absorbs former slice 12 and session-local status from former slice 13). Split into 11b.1 + 11b.2 on 2026-05-12 sizing pass — see comments. 11b.1 rewrites the conversation flow (session-turn outer shape, message-part outer shell, message-timeline, attachments per DESIGN.md L458, scroll-lock, system events, working-time header) and lands first to freeze the message-part outbound contract. 11b.2 rewrites the right pane (session-side-panel shell, session-status-panel/summary/connections, new overview tab combining 进展 + 产出 per #154, session-review review filter, terminal-panel sub-tab strip + glyph per #65). Workshop W1 locked 2026-05-12. References #154 and #65 from 11b.2 (already closed manually). Excludes the right-panel Context tab redesign per #154's carve-out; tracked separately in #583.

  1. Retired. Former status & feedback scope is split by consumption surface: session-local status belongs to 11b; global status popover, error page, and empty feedback belong to 17b.

  2. Settings page (L48). Main page + 6 sub-pages. Splits settings-general.tsx and related settings panels by product page/domain instead of keeping general/provider/model/worktree behavior in one large component. Workshop: W6.

17a. Home page + visual chrome + kbd primitive (L38 + L39 + L41 + L46 + L47; absorbs former slice 15). Home page hero + composer + starters (replaces project-picker, reuses slice 10 composer pieces); titlebar; branch popover; dirty-branch dialog; kbd container used by command palette, tooltips, and shortcut hints. Workshop: W4 + W5. Invariant: 17a must not rewrite layout.tsx shell structure; if it touches layout.tsx, keep it to slot wiring or CSS-variable connection needed by the visual chrome.

17b. Three-column shell + layout.tsx rewrite + global feedback (L37 + L33 global scope). Final assembly. Fully rewrites/splits packages/app/src/pages/layout.tsx into agent-readable shell/controllers/components, absorbing #474 as the implementation reference. Scope includes shell composition, settings entry/overlay behavior, session routing/navigation, sidebar/workspace/project actions, deep links, global StatusPopover / status-popover-body.tsx, pages/error.tsx, empty feedback surfaces, and any layout-owned controller state needed to make the final shell coherent. Preserve existing product behavior unless dead/legacy code is proven unused by grep/tests/runtime path analysis. Depends on column shape from 09, 10, 11b, 17a. Closes #276 and should close #474 when the rewrite is complete.

What would count as done?

Sequencing constraints (revised 2026-05-10)

  • All slices are strictly serial: each lands on dev, then the next branches. No parallel slice branches.
  • Slices 08, 09, 10, 11a have landed.
  • Slice 11b is blocked by W1 (renderer reuse from 11a already in place) and includes session-local status panel work from retired slice 13.
  • Slice 13 is retired; do not open a standalone PR for status & feedback.
  • Slice 16 is blocked by W6.
  • Slice 17a is blocked by W4 + W5 and by slice 10 (composer reuse, already merged). It must not rewrite layout.tsx shell structure; only slot/CSS-var wiring is allowed if needed.
  • Slice 17b must be last; depends on column components from 09, 10, 11b, 17a and owns global status popover / error / empty feedback from retired slice 13.
  • State matrix coverage (default/hover/active/focus-visible/disabled/selected/expanded/invalid) is mandatory for slices 11b, 17a. Kobalte primitive changes must preserve aria, portal, escape, and focus-trap behavior — visual-only CSS swaps are insufficient.
  • Each slice PR description must enumerate: changed-file boundary check (no out-of-scope paths), before/after light + dark screenshots for each touched surface, and the targeted golden-path E2E run for that slice.
  • Workshop preview HTMLs (docs/design/preview/*.html) and DESIGN.md are local-only (excluded via .git/info/exclude); slice PRs cannot link to them publicly. Cite packages/ui/src/styles/theme.css and packages/ui/src/theme/themes/pawwork.json for token specs.

What should stay out of scope?

Audience

Both

Extra context

Supersedes: this issue replaces #348 (a 2026-04-30 proposal for a five-day burst rewrite plus stack swap that was never started) and #210 (a 2026-02 placeholder for adding a PawWork-native primitive layer on top of upstream, now obsolete since the token layer itself is being rewritten). Both will be closed referencing this issue.

Child issue task list:

Slice progress tracking: each slice PR maintains its own checklist in the PR description; this umbrella body only tracks the child-issue reference list, not every slice checkbox.

Child issue closeout rule: every slice PR must name the child issues it covers and use GitHub closing keywords such as Closes #154 in the PR body when the slice fully satisfies that issue. If a slice only partially covers a child issue, the PR body must say what remains and leave the checklist item open. Before merging a slice, re-check this umbrella checklist and update it in the same closeout pass so completed child issues do not stay open.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityappApplication behavior and product flowsenhancementNew feature or requestuiDesign system and user interface

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions