refactor(app): right-panel visual reshape & portal tabs into titlebar#878
Conversation
Aligns Status / Files / Review tabs with docs/design/preview/right-pane.html and removes the seam between the titlebar and the right panel. Visual deltas (per docs/design/scratch/2026-05-23-area-b-*.html): - Status / Connections section headers: text-h3 uppercase → text-caption - Status sections separated by 24px breathing room only (no hairline, no chip background, no left indicator bar). Apply Occam's razor — three sections distinguish themselves by content shape alone. - Connections SectionRow loses border-b (header dot + count carry grouping) - Files row metadata drops middle-dot separator, switches to gap-3 - Files action Buttons gain leading icons (open-file / folder) - Review empty state removes the Mark watermark - Context breakdown swaps the syntax-* token rainbow for a brand-primary alpha ladder (system .30 / user 1.0 / assistant .70 / tool .45 / other .18) — syntax-* is reserved for code highlighting per DESIGN.md Tab strip — direction D (per scratch 2026-05-23-area-b-titlebar-tab-strip.html): - Every ShellTab renders 'icon + label' at rest, identical shape regardless of closable. On a closable tab, hovering swaps the leading icon for a close-small × in the same 14×14 slot — no separate close button, no layout shift. Width-at-rest is determined only by label length. - Active marker is weight + color shift only: fg-strong + font-weight 500 vs. fg-weak resting and fg-base on hover. No chip background, no brand underline; DESIGN.md keeps brand orange as a small accent and the titlebar tab strip is too prominent a surface to carry it. - ShellTab API takes an 'icon' prop instead of children; callers stop passing the icon + label as JSX children. Structural change: - Right-panel <Tabs.List> portals into a new pawwork-titlebar-tabs slot in <Titlebar>. The slot is anchored to the viewport right edge, width = var(--right-panel-width), so its border-left sits exactly on the right-panel-body border-left below — one continuous 1px from titlebar top to viewport bottom. Portal preserves the virtual tree so Tabs, SortableProvider, and DragDropProvider contexts still flow to the moved Tabs.List. - Tabs.css uses descendant selectors like '[data-component="tabs"] [data-slot="tabs-list"]'; portalling Tabs.List out of <Tabs> would drop all sidepanel styling (no flex, no hover, no selected state). The titlebar slot mirrors the same data attributes so existing CSS re-matches without forking the stylesheet. - The sidepanel tabs-list height was hard-coded to 44px; replaced with height: 100% so the slot drives the height (macOS 40px chrome / Windows 44px) and tabs never overflow the titlebar bottom edge. Snap: packages/app/e2e/snap/right-panel-titlebar.snap.ts seeds five todos across every status (completed / in_progress / pending / cancelled) and captures the chrome/body seam plus the multi-tab layout (Status + Files + Review) in light + dark, so future regressions to the resting view are caught.
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 42 minutes and 34 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR migrates the right-panel tab strip from inline rendering to a new portal slot in the titlebar, updates the ShellTab component API from ChangesRight-Panel Tabs Portal & UI Refactoring
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Suggested priority: P2 (includes user-path files (packages/app/src/components/session/session-context-tab.tsx, packages/app/src/components/session/session-sortable-shell-tab.tsx, packages/app/src/components/session/session-status-connections.tsx, packages/app/src/components/session/session-status-summary.tsx, packages/app/src/components/titlebar.tsx, packages/app/src/pages/layout.tsx, packages/app/src/pages/session/files-tab.tsx, packages/app/src/pages/session/session-side-panel.tsx)).
P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.
There was a problem hiding this comment.
Code Review
This pull request integrates the right-panel tab bar into the window titlebar via a portal to create a unified window chrome appearance. It includes UI refinements for session status and files tabs, such as an icon-swap interaction for closing tabs and updated layout spacing. Feedback focuses on improving accessibility by using semantic button elements for interactive toggles and ensuring keyboard navigability for hover-revealed elements. Additionally, it is recommended to replace hardcoded RGBA values with theme tokens for better maintainability.
# Conflicts: # packages/app/src/components/session/session-status-summary.tsx
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/src/components/session/session-sortable-shell-tab.tsx`:
- Around line 44-49: The click handler on the tab always closes when
props.closable is true, which prevents icon-area clicks from selecting the tab;
update the onClick in session-sortable-shell-tab.tsx to only call close() when
the click target is the explicit close control (e.g. check event.target or a
data attribute/class on the close button like data-close or ".close-button") and
otherwise allow the event to proceed (do not call event.preventDefault() or
event.stopPropagation() for non-close clicks) so icon clicks can select the tab;
keep using props.closable and the existing close() function but gate their
invocation by inspecting the click target.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 48b0bfec-b9c7-4510-9b35-c49a0aed4b1a
📒 Files selected for processing (10)
packages/app/e2e/snap/right-panel-titlebar.snap.tspackages/app/src/components/session/session-context-tab.tsxpackages/app/src/components/session/session-sortable-shell-tab.tsxpackages/app/src/components/session/session-status-connections.tsxpackages/app/src/components/session/session-status-summary.tsxpackages/app/src/components/titlebar.tsxpackages/app/src/pages/layout.tsxpackages/app/src/pages/session/files-tab.tsxpackages/app/src/pages/session/session-side-panel.tsxpackages/ui/src/components/tabs.css
…rtal The right-panel Tabs.List portals into the titlebar (#pawwork-titlebar-tabs) so the tab strip reads as window chrome rather than a second toolbar. That break two things that CI surfaced on PR #878: 1. E2E queries scoped to the complementary right panel via `rightPanel.getByRole("tablist")` could no longer find the (now-portalled) shell tabs. Confirmed failing @smoke: files/file-tree, inputs/select-review-filter. Other affected tests were silent (not in the smoke set) but would fail when their suites run. 2. CodeRabbit caught that the icon-area click on a closable tab always closed the tab — selecting the tab by clicking its icon (intended behavior when the × is not the visible glyph) was impossible. Changes: - Add `rightPanelTabsScopeSelector` and the `rightPanelTabList(page)` helper. Tests now reach the shell tabs via the stable `data-scope="right-panel"` hook on the portalled Tabs root, independent of whether the strip renders portalled (desktop) or inline. - Update all e2e tests that previously queried tablist / shell tabs / "Add tab" through the complementary region. The `commands/panels.spec.ts` "two tablists" assertion drops to one (only the inner tablist remains in the panel body) plus a separate visibility check on the shell strip. The "collapses shell tab labels" test sets `--right-panel-width` on `[data-component="desktop-shell"]` (where layout.tsx sets it inline) so the cascade actually reaches the titlebar slot's `width: var(...)`. - Gate the close-icon onClick on `swapRef.matches(":hover")`. When the default icon is showing (not hovered), the click bubbles to Tabs.Trigger and selects the tab; only when the × is the visible glyph does the close fire. Mirrors CodeRabbit's suggested fix. Verification: - `bun run --filter=@opencode-ai/app typecheck` — clean - `bun run --filter=@opencode-ai/app snap right-panel-titlebar` — passes - E2E suites left for PR CI to confirm (local e2e setup is heavy) Refs PR #878 review comments 3293113779 (gemini), 3293753551 (coderabbit).
The portalled tab strip lives in an absolutely-positioned `#pawwork-titlebar-tabs` slot anchored to the right edge of the titlebar with `z-10` and (previously) `pointer-events-auto`. When the right panel is open the slot is exactly as wide as the panel — which means its box sits on top of the Right utility panel toggle button rendered into `#pawwork-titlebar-right`. With `pointer-events-auto` the slot swallowed those clicks, so the user could open the panel but could not close it (and perf-probe-baseline's `session-streaming-long` scenario, which clicks the toggle once per loop iteration after the first run kept the panel open, timed out on iteration 2). Mirror the `#pawwork-titlebar-left` portal pattern: the slot itself is `pointer-events-none`; only the portalled `Tabs.List` (which carries the sidepanel hover/active styling) gets `pointer-events-auto`. Clicks land on the tab buttons as before, and any empty area inside the slot box passes through to the toggle beneath it. Verification: - `bun run --filter=@opencode-ai/app typecheck` — clean - `bun run --filter=@opencode-ai/app snap right-panel-titlebar` — passes
Perf delta summaryComparator: pass
|
Desktop Status panel renders Servers/MCP/LSP/Plugins as collapsible
`<button aria-expanded>` rows (SessionStatusConnections.SectionRow), not as
`role="tab"`. The selectors in status-popover.spec.ts were already stale on
dev — the previous PR migrated this surface to SectionRow but the spec kept
querying tabs. My earlier portal-helper migration mechanically moved the
broken selector to `rightPanelTabList`, which would have failed when the
suite runs.
- Query Servers/MCP/LSP/Plugins via `rightPanel.getByRole("button", ...)`.
- Rename "session status tab can switch to mcp" to "session status panel can
expand mcp section" and assert `aria-expanded` toggling instead of tab
selection — the row is a collapsible section, not a tab.
- Mobile popover still renders tabs inside the popover body; that assertion
is unchanged.
Caught in PR #878 review.
Refactor the right-panel shell tab close glyph from a hover-gated <span onClick> to a real semantic <button> via the existing `closeButton` slot on Tabs.Trigger (sibling-renders alongside the trigger, no nested-button HTML). Adds the ARIA APG closable-tabs Delete pattern, focus management on close, and a right-click context menu so screen-reader / keyboard users have a discoverable close path beyond the global `mod+w` shortcut. Driven by PR #878 review (consult with codex@xhigh). The thread argued for either a focusable per-tab × button (B) or "ship as is, keyboard users use mod+w" (A). Codex pointed out the established ARIA APG path: Delete on the focused tab + context menu, with the close button itself not adding a Tab stop. That's A+ — preserves the chromeless visual, gives AT users a proper keyboard close, doesn't bloat the tab-strip Tab order. Changes in this commit: session-sortable-shell-tab.tsx - Drop the `tab-icon-swap` span and the onClick hover guard entirely. - Pass `closeButton={<IconButton ...>}` to Tabs.Trigger (sibling slot). - IconButton is `tabIndex={-1}` + `aria-label="Close <label> tab"`; the trigger itself remains the focusable unit. - `aria-keyshortcuts="Delete"` on the trigger + onKeyDown handler so Delete/Backspace on a focused closable trigger closes the tab. - Focus management: before calling onClose, find the wrapper's next/prev sibling [data-slot="tabs-trigger-wrapper"] and `requestAnimationFrame` focus its [data-slot="tabs-trigger"] after Solid commits the removal — no focus loss to body. - Right-click and Shift+F10 open a ContextMenu with "Close tab" (only on closable tabs; Status has no menu since it has no actions). - Only pass `closeButton` when `props.closable`. Passing `<Show when={props.closable}>...</Show>` would still render an empty `[data-slot="tabs-trigger-close-button"]` div on Status (Tabs.Trigger's outer Show checks JSX truthiness, not the inner condition) — then the hover-swap CSS would fade Status's icon out with nothing to replace it. tabs.css (sidepanel variant) - Replace the previous `tab-icon-swap` opacity rules with the new positioning: - wrapper: `position: relative` (anchor for the absolute close slot). - default icon: hover-only opacity transition. - close button slot: `position: absolute; left: 10px; top: 50%` matching the trigger's leading-icon cell, 14x14, `opacity: 0; pointer-events: none` at rest; `opacity: 1; pointer-events: auto` on wrapper :hover. - Intentionally NOT triggered on `:focus-within` — keyboard arrow nav across the tablist would otherwise flip every focused tab's icon to × on the way past. Keyboard users have Delete advertised via aria-keyshortcuts. Verification: - `bun run --filter=@opencode-ai/app typecheck` clean - `bun run --filter=@opencode-ai/ui typecheck` clean - `bun run --filter=@opencode-ai/app snap right-panel-titlebar` passes; resting visual identical to pre-refactor (icons visible at rest, × hidden). Refs PR #878 reviewer's third P2 round.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/ui/src/components/tabs.css (1)
449-494: ⚖️ Poor tradeoffHardcoded coupling between CSS and TSX padding.
The
left: 10pxon Line 478 is hardcoded to match thepx-2.5(10px) padding defined in the TSX component (session-sortable-shell-tab.tsx:94). If the component padding changes, this CSS rule will break and the close button will misalign.Consider adding a comment cross-referencing the TSX file, or (higher effort) expose the padding as a CSS custom property from the component.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/tabs.css` around lines 449 - 494, The CSS rule for [data-slot="tabs-trigger-close-button"] uses a hardcoded left: 10px to match px-2.5 in the TSX (session-sortable-shell-tab.tsx:94), which will break if padding changes; fix by replacing the hardcoded offset with a CSS custom property (e.g. left: var(--tabs-trigger-padding-left, 10px)) and then set that property from the component (session-sortable-shell-tab.tsx) on the wrapper element (or, if you prefer the smaller change, add a clear comment in tabs.css referencing session-sortable-shell-tab.tsx:94 to warn maintainers of the coupling). Ensure the unique selectors [data-slot="tabs-trigger-wrapper"] and [data-slot="tabs-trigger-close-button"] are updated accordingly so alignment remains correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/ui/src/components/tabs.css`:
- Around line 449-494: The CSS rule for [data-slot="tabs-trigger-close-button"]
uses a hardcoded left: 10px to match px-2.5 in the TSX
(session-sortable-shell-tab.tsx:94), which will break if padding changes; fix by
replacing the hardcoded offset with a CSS custom property (e.g. left:
var(--tabs-trigger-padding-left, 10px)) and then set that property from the
component (session-sortable-shell-tab.tsx) on the wrapper element (or, if you
prefer the smaller change, add a clear comment in tabs.css referencing
session-sortable-shell-tab.tsx:94 to warn maintainers of the coupling). Ensure
the unique selectors [data-slot="tabs-trigger-wrapper"] and
[data-slot="tabs-trigger-close-button"] are updated accordingly so alignment
remains correct.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 58cb8190-230f-4544-bae4-415d2ca58408
📒 Files selected for processing (15)
packages/app/e2e/actions.tspackages/app/e2e/commands/panels.spec.tspackages/app/e2e/files/file-tree.spec.tspackages/app/e2e/inputs/select-review-filter.spec.tspackages/app/e2e/prompt/prompt-slash-terminal.spec.tspackages/app/e2e/selectors.tspackages/app/e2e/session/session-artifacts.spec.tspackages/app/e2e/session/session-composer-dock.spec.tspackages/app/e2e/session/session-review.spec.tspackages/app/e2e/snap/todo-status-only.snap.tspackages/app/e2e/status/status-popover.spec.tspackages/app/src/components/session/session-sortable-shell-tab.tsxpackages/app/src/components/titlebar.tsxpackages/app/src/pages/session/session-side-panel.tsxpackages/ui/src/components/tabs.css
CodeRabbit flagged the hardcoded `left: 10px` on the sidepanel `[data-slot="tabs-trigger-close-button"]` slot as coupled to the `px-2.5` trigger padding in session-sortable-shell-tab.tsx. Add a maintainer note in the CSS comment block pointing at the TSX site so the coupling is discoverable on edit. Keeping the cheap fix (comment) rather than the larger one (lift the offset to a CSS custom property set from the component) since the coupling is local to one variant. Refs PR #878 CodeRabbit review.
Refactor the right-panel shell tab close glyph from a hover-gated <span onClick> to a real semantic <button> via the existing `closeButton` slot on Tabs.Trigger (sibling-renders alongside the trigger, no nested-button HTML). Adds the ARIA APG closable-tabs Delete pattern, focus management on close, and a right-click context menu so screen-reader / keyboard users have a discoverable close path beyond the global `mod+w` shortcut. Driven by PR #878 review (consult with codex@xhigh). The thread argued for either a focusable per-tab × button (B) or "ship as is, keyboard users use mod+w" (A). Codex pointed out the established ARIA APG path: Delete on the focused tab + context menu, with the close button itself not adding a Tab stop. That's A+ — preserves the chromeless visual, gives AT users a proper keyboard close, doesn't bloat the tab-strip Tab order. Changes in this commit: session-sortable-shell-tab.tsx - Drop the `tab-icon-swap` span and the onClick hover guard entirely. - Pass `closeButton={<IconButton ...>}` to Tabs.Trigger (sibling slot). - IconButton is `tabIndex={-1}` + `aria-label="Close <label> tab"`; the trigger itself remains the focusable unit. - `aria-keyshortcuts="Delete"` on the trigger + onKeyDown handler so Delete/Backspace on a focused closable trigger closes the tab. - Focus management: before calling onClose, find the wrapper's next/prev sibling [data-slot="tabs-trigger-wrapper"] and `requestAnimationFrame` focus its [data-slot="tabs-trigger"] after Solid commits the removal — no focus loss to body. - Right-click and Shift+F10 open a ContextMenu with "Close tab" (only on closable tabs; Status has no menu since it has no actions). - Only pass `closeButton` when `props.closable`. Passing `<Show when={props.closable}>...</Show>` would still render an empty `[data-slot="tabs-trigger-close-button"]` div on Status (Tabs.Trigger's outer Show checks JSX truthiness, not the inner condition) — then the hover-swap CSS would fade Status's icon out with nothing to replace it. tabs.css (sidepanel variant) - Replace the previous `tab-icon-swap` opacity rules with the new positioning: - wrapper: `position: relative` (anchor for the absolute close slot). - default icon: hover-only opacity transition. - close button slot: `position: absolute; left: 10px; top: 50%` matching the trigger's leading-icon cell, 14x14, `opacity: 0; pointer-events: none` at rest; `opacity: 1; pointer-events: auto` on wrapper :hover. - Intentionally NOT triggered on `:focus-within` — keyboard arrow nav across the tablist would otherwise flip every focused tab's icon to × on the way past. Keyboard users have Delete advertised via aria-keyshortcuts. Verification: - `bun run --filter=@opencode-ai/app typecheck` clean - `bun run --filter=@opencode-ai/ui typecheck` clean - `bun run --filter=@opencode-ai/app snap right-panel-titlebar` passes; resting visual identical to pre-refactor (icons visible at rest, × hidden). Refs PR #878 reviewer's third P2 round.
CodeRabbit flagged the hardcoded `left: 10px` on the sidepanel `[data-slot="tabs-trigger-close-button"]` slot as coupled to the `px-2.5` trigger padding in session-sortable-shell-tab.tsx. Add a maintainer note in the CSS comment block pointing at the TSX site so the coupling is discoverable on edit. Keeping the cheap fix (comment) rather than the larger one (lift the offset to a CSS custom property set from the component) since the coupling is local to one variant. Refs PR #878 CodeRabbit review.
98ae609 to
c4beefd
Compare
…tabs Two correctness slips from the prior A+ refactor, caught in PR #878 review: 1. CSS: the hover-fade rule for `[data-slot="tab-icon-default"]` matched every sidepanel wrapper, including Status (which has no `closeButton` slot). Hovering Status would fade its icon out with nothing to take its place, leaving the cell blank. Scope the fade to wrappers that actually own a close-button slot via `:has([data-slot="tabs-trigger-close-button"])`. 2. TSX: `aria-keyshortcuts="Delete"` and the `onKeyDown` handler were unconditionally bound on the trigger. Status's onKeyDown returned early when `!props.closable`, but the announced shortcut still implied Delete would close the tab — a false promise to AT users. Gate both the attribute and the handler on `props.closable` so Status exposes neither. Snap (`right-panel-titlebar`) passes — resting visual unchanged.
The right-panel shell tab strip (Status / Files / Review) carried an app-level override in packages/app/src/index.css that painted var(--sidebar) on the selected trigger. This pre-dates PR #878 (the override has been there since the phase-1 desktop port in PR #35) but PR #878 reshaped the strip and surfaced the inconsistency: Status (the default landing tab) got a chip while Files / Review did not, so the selected tab visibly bulged wider than its siblings. The sidepanel comment in packages/ui/src/components/tabs.css already spelled out the intended active marker — 'weight + color shift only. No chip background, no brand underline' — but the app-level rule overrode it. Drop the rule. All three triggers now share a transparent background; the selected tab is conveyed by fg-strong + 500 weight, matching the tabs.css comment and DESIGN.md's restrained warm-neutral palette. Verification (TDD) - New e2e spec session-shell-tab-consistency.spec.ts asserts at the rendered seam that all three triggers share the same computed background-color. Red on HEAD~1 (Status rgb(248, 246, 243) vs Files rgba(0, 0, 0, 0)), green after the index.css deletion. The assertion does not hard-code 'transparent' — the contract is 'agree with each other'. - bun run --filter=@opencode-ai/app typecheck OK - bun run --filter=@opencode-ai/app snap right-panel-titlebar — light + dark grid shows all three tabs at equal visual weight.
PR #878 left two visual contracts unresolved on the right-panel tab strip: the selected-tab chip was still the opaque var(--sidebar) hex that read too heavy now that the strip lives in the titlebar, and the close × overflowed its slot because (a) the inner Icon is fixed at 16×16 in icon.css, (b) the slot was sized with literal 14px / 10px values that ignore this app's 13px html base font-size — so 'size-3.5' (used by the leading icon span and icon-button) actually renders ~11px, leaving the × off-center by 2-3px and visually bigger than its neighbours. Changes: - packages/app/src/index.css: switch the selected-tab chip from var(--sidebar) (opaque #f2f1ee / #1d1d1b) to var(--row-active-overlay) (rgba black/white at 6% — the exact overlay the sidebar session row uses for its own selected state). Single selection vocabulary across the app. - packages/ui/src/components/tabs.css: * Drop the redundant font-weight: 500 on [data-selected] — every sidepanel trigger already picks up 500 from text-h3 (--font-weight-h3), so the rule was setting an existing value and produced no visible emphasis. Selection is now color shift (fg-weak → fg-strong) + chip only, with constant tab widths under selection. * Replace literal 14px / 10px on the close-button slot with rem-aware values (0.875rem width/height, 0.625rem left) so it tracks 'size-3.5' and the trigger's 'px-2.5' padding under the 13px html base. The slot now matches the leading icon span exactly in size and position. * Constrain the inner [data-component='icon'] in the close-button slot to 8×8 (was 16×16 from icon.css) so the × reads as a subordinate affordance instead of a peer of the 16×16 leading icon. * Suppress the IconButton's own --hover-overlay background inside the slot so the × renders as a bare glyph, not a chip-shaped button. * Drop the 100ms opacity transition on the icon ↔ × swap — the fade produced a 'both icons visible' mid-frame whenever the cursor crossed multiple closable tabs in quick succession. Instant swap eliminates it. * Refresh the surrounding comments to describe the row-active-overlay chip + rem-aware footprint, replacing the now-incorrect 'no chip' and '14×14 cell' descriptions. Verification: - New packages/app/e2e/session/session-tab-chip-contract.spec.ts pins the contract: chip computes to a low-alpha rgba (overlay band, not opaque); the close-button slot's width/height equal the leading icon span's width/height; the visible × glyph is 6–10px; the leading icon and × centers align within 1px; the swap has zero transition; Status (the non-closable tab) never renders an × and keeps its icon on hover. All 5 assertions pass post-fix. - New packages/app/e2e/snap/right-panel-tabs-hover.snap.ts captures three states (rest / hover Files / hover Review) in light + dark — the existing right-panel-titlebar snap intentionally parks the mouse at (0,0) and would not catch hover-state regressions. Visually confirms chip is the lighter overlay, × is small and centered, no double-icon artifact.
Two visual regressions on the right-panel tab strip flagged after PR #878 shipped: 1. Hovering a tab produced no chip background — the strip felt dead and clicks landed without any visual lead-in. UX expects hover to preview the selection (lighter overlay), then click to lock in (heavier). 2. Status / Files / Review / Terminal labels have different lengths, so the chips rendered visibly mismatched. Fixes: - app: scoped two right-panel chip rules to the wrapper (matches the sidebar session row's chip-on-container vocabulary): hover → --row-hover-overlay (4% overlay) :has([data-selected]) → --row-active-overlay (6% overlay) Hover rule carries :not(:disabled) to tie specificity (0,6,0) with the sidepanel base rule that forces transparent bg on hover; later declaration order wins. - ui: sidepanel trigger gets min-width: 7.5rem (~97px @ 13px html base) plus justify-content: flex-start. Base trigger uses justify-content: center, which works for content-sized tabs but drifts the leading icon to the cell center as soon as min-width pushes the trigger wider than its content — the close-button slot stays at left: 0.625rem (absolute), so centering content offsets × from the leading icon by half the slack. background-color: 120ms transition added so the hover overlay fades in rather than snapping. E2e contract: 6 → 8 assertions. Adds 'hover paints a chip preview' and 'all sidepanel tabs share a uniform min-width chip'. Updated the selected- overlay assertion to sample the wrapper (where the chip now lives) instead of the inner trigger button. Verified: - bun script/e2e-local.ts -- session-tab-chip-contract → 8/8 - bun run typecheck → 8/8 - bun run snap right-panel-tabs-hover → grid shows hover + selected overlays in both light and dark themes, × stays aligned with the leading icon at the wider chip width.
PR #878 left the close-button slot anchored by `position: absolute; left: 0.625rem` — a literal mirror of the trigger's `px-2.5` padding — so the × visually drifted from the leading icon whenever a layout knob moved one side out of step. The previous round added `min-width` to unify chip widths and that surfaced the gap: even with `justify-content: flex-start` to keep the leading icon at padding-inline-start, the slot's absolute coordinate system and the trigger's flex flow never shared a single source of truth. The icon-button's base `margin: -0.25rem` also nudged the × ~3.25px off the leading icon's center. Refactor the sidepanel variant to a single-cell grid: - wrapper is `display: grid; grid-template-columns: 1fr` with a `--tab-leading-inset: 0.625rem` token - trigger and close-button slot both `grid-area: 1 / 1` so they overlay in the same cell - both flex-anchor their first child at `padding-inline-start: var(--tab-leading-inset)` (the trigger inherits it via SortableTab's `px-2.5` Tailwind class; the slot sets it explicitly) - close-button slot drops `position: absolute`, `left`, `width`, `height`, `transform` — there is no positional contract left to couple - sidepanel close-button clears the base `icon-button { margin: -0.25rem }` so the × icon's center sits on the leading icon's center Padding tweaks now propagate to both sides via one custom property; a future change to chip padding cannot resurrect the drift. Width: `min-width` drops 7.5rem → 5.5rem. The earlier value was sized to also fit "Terminal" but read too wide for the common 3-tab strip; Terminal is allowed to extend past the min naturally (the strip already scrolls horizontally). Tests: - rewrote the size assertion to check the × glyph alone (the slot is no longer a small fixed-size box, so slot-vs-leading width comparison is meaningless) - alignment test now measures the close icon-button center, not the slot center - uniform-width threshold updated to 5.5rem Verified: - bun script/e2e-local.ts -- session-tab-chip-contract → 8/8 - bun run typecheck → 8/8 - bun run snap right-panel-tabs-hover → leading icon and × land on the same column across all 4 cells in light + dark; the three short labels share a uniform chip width.
Round-of-rounds polish on the right-panel tab strip. After PR #878 shipped, the chip + × geometry drifted three different visual debts into the same surface — heavy chip, off-grid spacing, half-empty cells. This commit lands one coherent set of values, all locked to DESIGN.md L233's nine-step 4pt grid: shape radius-lg 14px (full pill on 28-high tab) width min-width 5.5rem (~71.5px), short chips share a uniform footprint; longer labels (Terminal) extend past it naturally padX 8px (was 10 / px-2.5 — off-grid) gap-inner 8px (was 6 / gap-1.5 — off-grid) gap-list 8px (was 0 — chips read as a single band) hover chip --row-hover-overlay (4%, matches sidebar row) selected chip --row-active-overlay (6%, matches sidebar row) × ↔ leading grid overlay, same padding-inline-start token Changes: - ui/tabs.css sidepanel block: tabs-list gets gap 0.5rem; wrapper gets border-radius var(--radius-lg) and --tab-leading-inset drops 0.625rem → 0.5rem to track the new padding. - app/SortableTab tsx classes: gap-1.5 px-2.5 → gap-2 px-2 so the trigger's Tailwind padding/gap line up with the slot's --tab-leading-inset. - e2e contract spec: width assertion goes back to the 5.5rem floor (an earlier round had inverted it to assert no min-width; that decision was reverted after the v4 HTML review). Decision trail (HTML variants): docs/design/scratch/2026-05-24-right-panel-tabs-v2.html (8-cell selected-color × hover-strength × padX × gap matrix) …-v3.html (locked colour, padX × gap) …-v4.html (locked padX 8, gap-inner × gap-list — variant 4 "全 8" selected by user, criterion: simplest + 4pt-compliant) Verified: bun script/e2e-local.ts -- session-tab-chip-contract → 8/8 bun run typecheck → 8/8 bun run snap right-panel-tabs-hover → chip pills read as separate cells in light + dark; × overlays leading icon pixel-for-pixel.
PR #878 shipped a new right-panel + titlebar tabs portal architecture but left four visible regressions and a click-occlusion bug that surfaced only at 4+ open tabs. This PR closes all of them, plus the follow-up review items the maintainer + Codex + GPT Pro + CodeRabbit raised over five rounds of review. ## What broke 1. **Visual** — sidepanel host paints `bg-base` and covered the right utility toggle; `.titlebar-icon` selector was bound to the icon-button primitive and silently dropped `aria-expanded` chip on chips that re-used the class; tab chip × glyph rendered at 16px overflowing the 11px slot (`html { font-size: 13px }` made `size-3.5` ≈ 11px, but tabs.css used 14/10px literals throughout); selected closable tab with mouse parked away showed both leading icon + × because the base `:has([data-selected]) close-button { opacity: 1 }` (4 attrs) beat the sidepanel `:hover` rule (3 attrs). 2. **Click occlusion (regression-review P1)** — titlebar tabs slot was absolute-positioned over `#pawwork-titlebar-right` and used `pointer-events: none` + opt-back-in on every interactive descendant to keep the toggle clickable through the overlay. At 4+ tabs the `+` dropdown trigger got pushed into the toggle's x range, and `pointer-events: auto` (mandatory for the dropdown to open) intercepted clicks meant for the toggle. Same shape would have re-broken every time a tab was added. ## How it's fixed - **chip visuals** — selected/hover backgrounds switched to `--row-active-overlay` / `--row-hover-overlay` (same vocabulary as the sidebar session row); tabs.css ported to rem-aware spacing tokens on the 4pt grid; inner close-button Icon clamped to 8×8; sidepanel close-button selector lifted to 4+ attrs so the hover-only rule wins. - **rail restructure (option D after consulting Codex)** — the titlebar tabs slot moved from absolute overlay to an **in-flow flex sibling** of the toggle inside the right grid column. The toggle is naturally pushed left by `--right-panel-width` when the panel opens and slides back to the viewport edge when it closes (existing 240ms transition carries it smoothly). Disjoint geometry, no pointer-events choreography, no z-index. Full chain (`pointer-events: none` on slot, list, spacer, opt-in on every interactive descendant) deleted. - **rail full-height seam** — outer right rail gets `self-stretch` so the tabs slot's `border-l` reaches the full 44px titlebar height and meets the right-panel body's `border-l` as one continuous separator. Without it `items-center` on the grid root collapsed the rail to toggle row height (≈30px) and the seam broke above/below the toggle. - **viewport gate** — `tabsRailActive` gated on `createMediaQuery ("(min-width: 768px)")` to match SessionSidePanel's own desktop predicate. Without it, opening the panel at desktop width and shrinking the viewport below 768px would leave the titlebar reserving panel-width of empty rail and push the toggle off-screen. - **route gate** — `tabsRailActive` also gated on the session pathname so navigating to home/settings with the panel left open doesn't claim panel-width and push the StatusPopover fallback inboard. ## Tests - `session-tab-chip-contract.spec.ts` — 11 assertions covering the chip's {selected/unselected} × {hover/no-hover} matrix, × glyph size and center alignment, instant icon swap, min-width parity between closable / non-closable, 4pt grid pinning. Scoped to `[data-component="tabs"][data-variant="sidepanel"][data-scope="right-panel"]` root so unrelated tablists can't pollute. - `titlebar-right-rail-contract.spec.ts` — split out as a separate spec covering the rail architecture: toggle stays clickable with the full 4-tab set open (closes the regression P1), slot height matches titlebar height (closes the seam followup), slot width collapses to 0 below the desktop breakpoint with `expect.poll` to wait out the viewport → media query → memo → DOM chain. - `right-panel-tabs-hover.snap.ts` — visual snap target capturing rest / hover-Files / hover-Review states in light + dark themes, with per-element opacity / bounding-box dumps for forensic diffing. ## CSS anchor positioning support The new close-button positioning depends on `position-anchor` / `anchor()` / `anchor-scope`. PawWork ships only as an Electron (Chromium 134 in Electron 40) desktop app on macOS + Windows per AGENTS.md, well above the Chromium 125 anchor-positioning floor. `bun run dev` web mode is a developer convenience, not a shipped surface; no fallback needed. ## Verified - `bun test` (1544 pass) - `bun run typecheck` (clean) - `bun run test:e2e session-tab-chip-contract.spec.ts` (11/11) - `bun run test:e2e titlebar-right-rail-contract.spec.ts` (3/3) - `bun run snap right-panel-titlebar` (visual: toggle left of tabs portal, all 4 tabs + `+` visible, panel border-l flush with titlebar slot border-l, seam continuous) - Adjacent `right-panel-width.spec.ts` failures (`[role="tablist"]` expected as aside descendant, turn-list visibility) reproduce on HEAD too — pre-existing, unrelated to this change. ## Review rounds closed 1. Initial reshape (chip visuals + × geometry + selected/hover matrix) 2. Codex consult on rail architecture choice → option D adopted 3. Codex review of D landed → caught `pr-2` misalignment, missing route gate, comment drift 4. Maintainer P2 review → seam full-height fix 5. Maintainer P2 review → viewport breakpoint gate 6. CodeRabbit → DOM scope tightening + post-resize poll convergence 7. CodeQL → js/redos false-positive on comment, reworded 8. Pushed back on CodeRabbit's `Control+\`` → `modKey` suggestion (terminal.toggle is hardcoded ctrl+\` on every platform) ## Follow-up scope (deferred) - Lift the desktop/route/state predicate to shared layout state and extract `DESKTOP_BREAKPOINT` constant (CodeRabbit nit, low value). - snap-spec nits (waitForTimeout → state-based, trackSession placement, toBe(3) → toBeGreaterThanOrEqual) — separate PR. Closes the regression-review thread on PR #878 follow-up.
Summary
Reshape the right panel (Status / Files / Review) visual surface and move the tab strip into the titlebar so the tabs read as window chrome instead of a second toolbar. The right-panel
border-leftnow lines up pixel-for-pixel with the titlebar separator above it, giving one continuous 1px line from titlebar top to viewport bottom.Visual deltas tracked against
docs/design/scratch/2026-05-23-area-b-*.html:text-h3 uppercase→text-caption; the three sections (Progress / Sources / Connections) are separated by 24px breathing room only — no hairline, no chip background, no left indicator bar. Applies Occam's razor: content shape alone distinguishes the sections.×in the same 14×14 slot — no separate close button, no layout shift. Active marker is weight + color only (fg-strong + 500vs.fg-weak / fg-base); no chip, no brand underline.DESIGN.mdkeeps brand orange as a small accent and the titlebar is too prominent a surface to carry it.gap-3; action buttons gain leading icons (open-file/folder).syntax-*token rainbow for abrand-primaryalpha ladder (system .30 / user 1.0 / assistant .70 / tool .45 / other .18).syntax-*is reserved for code highlighting perDESIGN.md.Structural change:
<Tabs.List>portals into a newpawwork-titlebar-tabsslot in<Titlebar>via Solid<Portal>. Solid Portal preserves the virtual tree, so Tabs / SortableProvider / DragDropProvider contexts still flow to the moved list.data-component="tabs" data-variant="sidepanel" data-orientation="horizontal" data-scope="right-panel"so the existing descendant selectors intabs.cssre-match without forking the stylesheet.tabs.csssidepaneltabs-listheight changes from hard-coded 44px to100%so the slot drives the height (macOS / Windows chrome both use--shell-titlebar-height: 44px).flex flex-row items-centerto vertically center the portal'd list.flex-rowis required because the inherited[data-component="tabs"]rule setsflex-direction: column— without the override, the tab strip pins to the top of the titlebar.Why
Status / Files / Reviewwere rendering inside the right-panel body as a second toolbar under the titlebar, which created a visual seam: the titlebar separator stopped at the right panel's top, and a new horizontal divider began. The user (non-programmer) reported the seam as the most jarring detail of the right panel. Moving the tabs into the titlebar is what the linked design scratch settled on, and required no new CSS variables — just the portal mechanism and the slot.The Status panel rework was a separate user request to "make the three sections feel more distinct without adding chrome." The conclusion (after several iterations comparing hairlines, chip backgrounds, color shifts, and indicator bars) was to remove all chrome and rely on 24px breathing room — Occam's razor.
Related Issue
None — direct user design feedback, no GitHub issue.
Human Review Status
Pending
Review Focus
packages/app/src/components/titlebar.tsx— the portal slot and its data attribute / class composition. Theflex-rowoverride is load-bearing (see comment in code).packages/app/src/pages/session/session-side-panel.tsx— theShow + Portalmount lookup pattern and what happens on right-panel close (Portal is unmounted, the slot empties cleanly).packages/ui/src/components/tabs.css— sidepanel variant changes: chip background removed, brand underline removed, list height changed from 44px to 100%, icon-default ↔ icon-close swap selectors.packages/app/src/components/session/session-sortable-shell-tab.tsx— newiconprop API (children dropped) and the same-slot icon ↔ close swap.Risk Notes
VITE_PAWWORK_SHELL_OS: "macos"), so dev preview matches mac chrome. Electron mac/Windows behavior verified mentally against--shell-titlebar-height(44px on both) — no real Electron run yet because user is keeping their own Electron session.SortableProviderancestor context flows through. No new DnD code.right-panel-titlebar.snap.tsseeds five todos across all four statuses and captures the chrome/body seam in light + dark, so regressions to the resting view are caught..github/workflows/touched.How To Verify
Screenshots or Recordings
The snap grid (light | dark) lives at
docs/design/preview/screenshots/right-panel-titlebar.pngand is the canonical visual record for this PR. Compares against the v4 scratch indocs/design/scratch/2026-05-23-area-b-status-redesign-v4.html.Checklist
bug,enhancement,task,documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.app,ui,platform,harness,ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.P0,P1,P2,P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.Pending,Approved by @<reviewer>, orNot required: <reason>(default isPending; "not required" is restricted to bot-authored low-risk PRs).dev, and my PR title and commit messages use Conventional Commits in English.Summary by CodeRabbit
New Features
Style
Tests