feat: unify session right utility panel#32
Merged
Conversation
Adds "sidepanel" to the TabsProps variant union and scoped CSS that: - disables the [data-slot="tabs-list"]::after flex-grow filler so the consumer's 40px right reserve stays flush against the panel edge - neutralizes the base tabs-trigger-wrapper chrome (borders, bg, selected tint) so consumers can style triggers via Tailwind - owns the color/bg/state contract on [data-slot="tabs-trigger"]: text-weak default, text-base selected, surface-raised-base selected bg, surface-base-hover on hover Type-level test asserts the union accepts "sidepanel" (tsc gates the regression; bun strips types at runtime so the test is a compile-time check in practice).
- DEFAULT_RIGHT_PANEL_WIDTH=340, MIN=300, MAX=520 (exported)
- clampRightPanelWidth pure factory (exported for unit coverage)
- rightPanel seed { width: 340 } in createStore
- layout.rightPanel accessor with clamped width() memo and resize()
Persisted store round-trips existing layout.v6 keys; missing rightPanel
in legacy blobs falls back to the seed default, then clamp guards.
- Export pure formatRightPanelWidth(open, width) helper for unit coverage - panelWidth now sources from layout.rightPanel.width() instead of calc(100% - session.width), so the panel stops growing with viewport - Tabs root uses variant="sidepanel" (tabs.css scoped block handles the wrapper-chrome reset + trigger color/bg contract) - 40px aria-hidden reserve div appended inside Tabs.List so the tab row has a right margin matching the design prototype DOM-render tests sit outside bun's Solid-incompatible JSX transform; the existing mock-to-null harness stays, with typecheck gating variant prop type and Task 6 e2e gating the end-to-end width wiring.
…emos
With the right panel now self-sized via layout.rightPanel (Task 2-3),
the middle splitter that resized layout.session is a zombie. This commit:
- deletes desktopSidePanelOpen + sessionPanelWidth (no readers left)
- flips session column wrapper from "shrink-0 md:flex-none + inline
style.width" to "min-w-0 flex-1" so it claims the gap between
sidebar and right panel on all breakpoints
- removes the <Show when={desktopReviewOpen()}>...<ResizeHandle> block
that drove layout.session.resize
size.active() still threads through (terminal panel + transition class
on the session column), size.start/touch still fire from the file-tree
handle inside Review tab; Task 5 adds the new left-edge handle that
also participates.
- Export makeRightPanelResizeHandler pure factory — touch-then-resize
callback is unit-testable outside Solid's DOM.
- Mount a wrapper <div onPointerDown={size.start}> above the tab shell,
with a ResizeHandle (edge="start", direction="horizontal") attached
to the aside's left edge. min=300 max=520.
- Clamping stays centralized in layout.rightPanel.resize (Task 2), so
the handler forwards the raw width straight through.
pointerdown + ResizeHandle prop binding are typecheck-gated; Task 6's
e2e drives the persist round-trip end-to-end.
- Expose the provider's layout object on window.__pawworkLayout under import.meta.env.DEV so e2e can drive layout.rightPanel.resize(n) without wiring a fake pointer-drag sequence. - New spec at e2e/session/right-panel-width.spec.ts: open the panel via the titlebar toggle, set width to 400, reload, verify persisted. Covers the unit gap where Task 5's mock doesn't exercise the real Solid store + persisted() round-trip.
Per crosscheck codex P2: a malformed persisted blob or a buggy drag math path could hand clampRightPanelWidth a NaN or ±Infinity, and Math.max/Math.min would propagate that straight through to the style width, producing "NaNpx" or "Infinitypx". Treat non-finite like undefined — fall back to DEFAULT. Two new test cases exercise NaN / ±Infinity.
Two crosscheck findings:
- tabs.css sidepanel variant: the base [data-component="tabs"]
[data-slot="tabs-list"] { height: 48px } rule has (0,2,0) specificity
and wins over the consumer's Tailwind h-11 (0,1,0). Explicit
height: 44px in the variant block makes the 44px tab row intentional
instead of a specificity accident.
- session.tsx: the column is now flex-1 with no explicit width, so
transition-[width] no longer animates anything (flex-basis changes
on flex-1 items don't go through the width transition). Strip the
classList entirely; the aside's own transition-[width] at
session-side-panel.tsx:315 is what actually smooths the open/close.
Round 2 crosscheck: - clampRightPanelWidth now rejects non-number inputs (string, null) in addition to the non-finite guard added in round 1. Corrupted persisted blobs from external migration tooling could hand the function a string; treat like undefined → default. Two new test cases cover string + null. - session-side-panel.tsx: add inline comment explaining why the 40px aria-hidden spacer inside Tabs.List exists (matches design prototype gutter) so a future editor doesn't trim it as dead space.
Two manual-test regressions:
- reviewSnap suppressed the 240ms transition on every desktopReviewOpen
flip. That guard was there to prevent double-animation jank back when
the session column had an explicit sessionPanelWidth() that animated
alongside the aside. With Task 4 the session column is pure flex-1,
so the aside is the only thing animating and the session column
auto-adjusts per frame. reviewSnap state + requestAnimationFrame
bookkeeping + prop are all removed.
- Right panel titlebar toggle SVG rendered at the button's 32x24 box
because the bare <svg data-slot="icon-svg"> matched icon.css's
[data-slot="icon-svg"] { width: 100% } without the sizing wrapper.
Wrap in <div data-component="icon" data-size="small"> to pin it at
16x16, matching the left sidebar toggle.
Two @smoke titles drifted after earlier PR #32 commits: - home server picker title → "@smoke project home status panel can open the server picker dialog" - file-tree entrypoint title → "@smoke review keeps the persistent file-tree pane for review navigation" The curated list in e2e-smoke-tagging.test.ts still referenced the old titles, failing the CI unit job. Sync to current titles.
Astro-Han
added a commit
that referenced
this pull request
May 9, 2026
Slice 10 of issue #440 — full composer + dock + model picker rewrite. 38 commits squashed. ## Highlights - **Composer joined-card shell**: new DockCard + DockSegment primitives in @opencode-ai/ui, dock widgets share the L34 joined-card layout. Hybrid composer shell renders permission UI inline. - **prompt-input.tsx 1602 → ~570 lines**: split into prompt-input/{store-types, editor-serialize, editor-imperatives, comment-routing, history-navigation, popover-controllers, editor-input, keydown, model-controls}. No behavior change — all factories injected via deps; popover↔editor-input cycle resolved via mutable forward-ref. - **DockWidgetHeader unified**: single 36px collapsed header with 30px chev IconButton (3+3 breathing) — consolidates Followup, Revert, Todo widgets. - **DOCK_MOTION contract**: composer/dock/widgets share `{ visualDuration: 0.3, bounce: 0 }` via packages/app/src/pages/session/composer/motion.ts. Aligns with global useSpring convention. - **Model picker rewrite**: variant control merged into model trigger; thinking-level promoted from separate popover to inline section. `[data-picker-content]` guard prevents nested popover dismiss. - **Send button + ContextUsage parity**: 30px outer / 16px inner glyph, theme-locked colors, larger stop glyph. - **Followup defaults to queue mode**: drops the legacy steer override. - **Control heights**: single 30px dense tier across composer; sidebar trailing actions step down to 26 per nesting rule. - **Picker contracts**: tighter padding, narrower width (340), dropped sticky labels. ## Post-review fixes (CI + 2 review rounds) - Critical: DockCard manual classList merge (single key with spaces) was throwing DOMException InvalidCharacterError — replaced with Solid native class+classList co-rendering. - CI typecheck: PopoverControllers types aligned with upstream solid-list `Accessor<string | null>` (was `undefined`). - CI e2e: dropped stale `prompt-variant-control` selector (variant folded into model picker). - Send button e2e contract: 32 → 30px to match dense tier. - Editor serialize: fixed double-newline regression for blank rows. - Keydown: removed dead `if (stopping())` branches (both arms identical). - Custom slash command id includes source to prevent workspace+user collision. - Model trigger label: pinned to `min-w-[80px] max-w-[180px]` to balance short names ("GPT-5.5") vs long ("DeepSeek V4 Pro") without jiggling the workspace control. ## Systemic finding (tracked under #34) PawWork sets `html { font: var(--type-body) }` → root font-size 13px. This means **Tailwind rem-based units render at 81% of their pixel-named values**: `h-9` = 29.25px, not 36px. Multiple visual bugs in this slice (chev clipping, header height) traced back to this. Remediation: use absolute pixels (`h-[36px]`) for grid-pinned heights; avoid Tailwind's pixel-named scale for 4pt-grid alignment. DESIGN.md docs debt to be filed under #34. ## Hand-test backlog (open after merge) - #32 dark theme regression sweep - #33 single-prompt E2E ## Files of note - packages/ui/src/components/dock-card.tsx (new) - packages/app/src/pages/session/composer/{motion.ts, use-dock-collapse.ts, dock-widget-header.tsx, session-composer-region.tsx, session-todo-dock.tsx} - packages/app/src/components/prompt-input/* (9 new files) - packages/app/e2e/composer/composer-slice-10.spec.ts (new) Closes part of #440.
This was referenced May 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
sidePanelTab: "changes"stateWhy
This slice lands the next phase of the PawWork desktop UI tracked in
#26. The goal is to replace split session utilities with one consistent right-side surface without regressing terminal access, status access, or old persisted layout state.Related Issue
Part of #26
How To Verify
Screenshots or Recordings
Checklist
devbranchPhase 2 — shell geometry refine (added 2026-04-19)
Reworks the outer shell introduced above. The first pass left the panel growing with the viewport and using the wrong tab chrome; this follow-up anchors it at a fixed width, rebuilds the tab row per
docs/design/src/rightpanel.jsx, deletes the zombie middle splitter, and wires a real left-edge resize handle.Changes
ResizeHandle. Session column flips toflex-1and claims the gap.variant="sidepanel"on@opencode-ai/ui/tabswith scoped CSS: disables the base::afterflex-grow filler so the 40px right-gutter reserve stays flush; neutralizes wrapper chrome; owns trigger color/bg/selected contract (consumer keeps geometry via Tailwind).layout.rightPanel.width(sibling oflayout.sidebar,layout.fileTree,layout.session).clampRightPanelWidthguards undefined / non-number / non-finite inputs and defaults to 340. Width persists across sessions and reloads.sessionPanelWidth+desktopSidePanelOpenmemos and the middle splitter<Show>block insession.tsx(0 remaining readers). Removestransition-[width]on the now-flex session column (no-op onflex-1).size.start/touchthreaded through the new handle to preserve transition suppression during drag, matching the sidebar + file-tree handles.Test coverage
clampRightPanelWidthpure-factory: default / inside-range / below-min / above-max / NaN / ±Infinity / string / null → 9 cases.formatRightPanelWidth+makeRightPanelResizeHandlerhelpers: closed/open, touch-before-resize, clamping delegation.tabs.test.tsxtype-level: union includes"sidepanel"(typecheck gates regression; bun strips types at runtime).e2e/session/right-panel-width.spec.ts: open via titlebar → drivelayout.rightPanel.resize(400)via DEV-onlywindow.__pawworkLayouthook → reload → assert 400px persisted.Review
Three rounds of multi-model crosscheck (Claude + Codex) converged:
[data-slot="tabs-list"] { height: 48px }at (0,2,0) was winning over consumer'sh-11at (0,1,0)), removed deadtransition-[width]onflex-1.typeof !== "number"guard on clamp, inline comment on the 40px reserve spacer.wantsReviewtab-gating (out of scope; this PR only renames"changes"→"review"in that expression) and false-positives disproved by code reading.Known tradeoffs
session-side-panel.test.tsx).bun testcompiles JSX with the React transform, not Solid's — real DOM rendering would require adding@solidjs/testing-library+ harness setup topackages/uiandpackages/app, which is scope creep on the upstream-synced test infra. Coverage shifts to pure helpers (formatRightPanelWidth,makeRightPanelResizeHandler,clampRightPanelWidth) + typecheck + e2e.window.__pawworkLayout(DEV-gated), not the pointer drag path. Playwright webServer always runs in DEV, so the hook is always present in practice; a real-drag test would be more brittle and cover less surface than the persistence round-trip.layout.session.widthis still persisted as a store field even though nothing reads it anymore — kept for layout.v6 blob back-compat; a future migration can strip it.Scope deferred
wantsReview/openReviewPanelchange-tab behavior (pre-existing; flagged by both reviewers but unchanged by this diff).Tabs.List— consistent with the existingpillvariant, which has the same descendant-leakage pattern; fixing both belongs in a broader tabs primitive refactor.