Skip to content

refactor(app): right-panel visual reshape & portal tabs into titlebar#878

Merged
Astro-Han merged 8 commits into
devfrom
claude/right-panel-visual
May 24, 2026
Merged

refactor(app): right-panel visual reshape & portal tabs into titlebar#878
Astro-Han merged 8 commits into
devfrom
claude/right-panel-visual

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 23, 2026

Copy link
Copy Markdown
Owner

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-left now 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:

  • Status panel — section headers go text-h3 uppercasetext-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.
  • Tab strip (direction D) — every tab renders icon + label at rest with identical shape regardless of closable. On a closable tab, hovering swaps the leading icon for a close × in the same 14×14 slot — no separate close button, no layout shift. Active marker is weight + color only (fg-strong + 500 vs. fg-weak / fg-base); no chip, no brand underline. DESIGN.md keeps brand orange as a small accent and the titlebar is too prominent a surface to carry it.
  • Files row — drops the middle-dot metadata separator for gap-3; 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.

Structural change:

  • Right-panel <Tabs.List> portals into a new pawwork-titlebar-tabs slot in <Titlebar> via Solid <Portal>. Solid Portal preserves the virtual tree, so Tabs / SortableProvider / DragDropProvider contexts still flow to the moved list.
  • The slot mirrors data-component="tabs" data-variant="sidepanel" data-orientation="horizontal" data-scope="right-panel" so the existing descendant selectors in tabs.css re-match without forking the stylesheet.
  • tabs.css sidepanel tabs-list height changes from hard-coded 44px to 100% so the slot drives the height (macOS / Windows chrome both use --shell-titlebar-height: 44px).
  • Slot uses flex flex-row items-center to vertically center the portal'd list. flex-row is required because the inherited [data-component="tabs"] rule sets flex-direction: column — without the override, the tab strip pins to the top of the titlebar.

Why

Status / Files / Review were 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. The flex-row override is load-bearing (see comment in code).
  • packages/app/src/pages/session/session-side-panel.tsx — the Show + Portal mount 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 — new icon prop API (children dropped) and the same-slot icon ↔ close swap.

Risk Notes

  • Platform: tabs are portalled into the titlebar slot, which is a desktop-shell-only surface. Web/dev mode also renders the slot (snap config injects 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.
  • Drag & drop: tab reorder still works because Solid Portal preserves the virtual tree; SortableProvider ancestor context flows through. No new DnD code.
  • Snap coverage: new right-panel-titlebar.snap.ts seeds five todos across all four statuses and captures the chrome/body seam in light + dark, so regressions to the resting view are caught.
  • No .github/workflows/ touched.

How To Verify

bun run snap right-panel-titlebar     → 1 passed; light + dark grid PNG matches design scratch
bun run lint                          → clean
bun --cwd packages/app run typecheck  → clean
DOM probe (deleted after use):
  header height = 44px (mac shell)
  slot height = 44px
  Tabs.List bounding rect: y=8, height=30 → 7px top + 7px bottom breathing

Screenshots or Recordings

The snap grid (light | dark) lives at docs/design/preview/screenshots/right-panel-titlebar.png and is the canonical visual record for this PR. Compares against the v4 scratch in docs/design/scratch/2026-05-23-area-b-status-redesign-v4.html.

Checklist

  • Type label — this PR carries exactly one of 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.
  • Routing labels — this PR carries at least one of 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.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

Summary by CodeRabbit

  • New Features

    • Right-panel tabs now appear in the shell titlebar for improved visual integration and keyboard/tab close support.
  • Style

    • Refined spacing, layout, and titlebar height for visual consistency.
    • Updated tab icon/hover behavior and section color mapping.
    • Adjusted file row spacing and explicit icons for file actions.
  • Tests

    • Added visual regression coverage for the right-panel titlebar and updated end-to-end tab tests.

Review Change Stack

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.
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've used your available PR reviews for now.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dac223a6-b1f1-481d-b2e5-8a7fd8151b64

📥 Commits

Reviewing files that changed from the base of the PR and between cb53eb9 and a270db0.

📒 Files selected for processing (2)
  • packages/app/src/components/session/session-sortable-shell-tab.tsx
  • packages/ui/src/components/tabs.css
📝 Walkthrough

Walkthrough

This PR migrates the right-panel tab strip from inline rendering to a new portal slot in the titlebar, updates the ShellTab component API from children to icon props, adjusts CSS styling to support the portalled layout, and adds supporting typography and spacing updates across status-panel components. A visual snap test validates the new titlebar-panel seam.

Changes

Right-Panel Tabs Portal & UI Refactoring

Layer / File(s) Summary
Titlebar portal slot and height adjustment
src/components/titlebar.tsx, src/pages/layout.tsx
Titlebar fallback height increases from 40px to 44px; new absolute-positioned portal slot #pawwork-titlebar-tabs added with Tabs-matching data attributes to anchor portalled tab strip on viewport right edge.
ShellTab icon prop and keyboard/close behavior
src/components/session/session-sortable-shell-tab.tsx
ShellTab refactored to accept icon prop instead of children, renders fixed icon+label layout, adds Delete/Backspace keyboard close handling, focus-restoration after close, and optional ContextMenu close action.
Session-side-panel portal setup and rendering
src/pages/session/session-side-panel.tsx
Portal imported from solid-js/web; new tabsPortalMount signal mounted to the titlebar slot element on component init. Tabs.List (sortable shell tabs + add-tab dropdown) is conditionally wrapped in Portal when the mount element exists; empty-tab UI simplified.
Tabs CSS sidepanel variant updates
packages/ui/src/components/tabs.css
Sidepanel tabs-list height changed from fixed 44px to 100%; tabs-trigger-wrapper height:auto removed; hover/selected styling rewritten to transition color/weight and implement icon↔close opacity swap without layout shift.
Status summary and connections panel styling
src/components/session/session-status-summary.tsx, src/components/session/session-status-connections.tsx
SessionStatusSummary outer wrapper replaced with fragment; section padding increased to py-6; SessionStatusConnections header switched to caption style; SectionRow borders removed; footer spacing adjusted.
Context tab colors and files-tab button icons
src/components/session/session-context-tab.tsx, src/pages/session/files-tab.tsx
BREAKDOWN_COLOR now uses hardcoded rgba brand-primary alpha values; files-tab status row spacing adjusted and separator removed; action buttons explicitly set icon="open-file" and icon="folder".
Right-panel titlebar visual snap test & e2e helpers
packages/app/e2e/*, packages/app/e2e/snap/right-panel-titlebar.snap.ts
Adds rightPanelTabList(page) and rightPanelTabsScopeSelector, updates many e2e specs to use portal-aware queries, and introduces a Playwright snap that seeds todos, opens Files/Review via keyboard from the portalled titlebar, and captures light+dark screenshots for the right-panel-titlebar grid.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped into the titlebar light,
portaled tabs now snug and tight,
icons swap with a gentle sigh,
status panes breathe, layouts tidy—hi! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: refactoring the right-panel visual layout and moving tabs into the titlebar via Portal.
Description check ✅ Passed The PR description is comprehensive and complete, covering all required template sections with detailed explanations of what changed, why, review focus, risks, and verification steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/right-panel-visual

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Astro-Han Astro-Han added the enhancement New feature or request label May 23, 2026
@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface P2 Medium priority labels May 23, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/app/src/components/session/session-sortable-shell-tab.tsx Outdated
Comment thread packages/app/src/components/session/session-context-tab.tsx
# Conflicts:
#	packages/app/src/components/session/session-status-summary.tsx
@Astro-Han

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0bf1014 and 4729a65.

📒 Files selected for processing (10)
  • packages/app/e2e/snap/right-panel-titlebar.snap.ts
  • 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
  • packages/ui/src/components/tabs.css

Comment thread packages/app/src/components/session/session-sortable-shell-tab.tsx Outdated
Astro-Han added 2 commits May 24, 2026 13:10
…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
@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown

Perf delta summary

Comparator: pass

Profile / Scenario interaction median interaction worst long task max tbt frame gap p95 frame gap max jank count cls status
default / homepage-cold 24 -> 32 (+8) 40 -> 48 (+8) 88 -> 68 (-20) 38 -> 18 (-20) 16.8 -> 16.8 (0) 183.3 -> 166.7 (-16.6) 4 -> 2 (-2) 0 -> 0 (0) pass
default / long-session-input-lag 48 -> 48 (0) 48 -> 48 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.7 (0) 16.8 -> 16.8 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / session-streaming-long 40 -> 40 (0) 80 -> 56 (-24) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.7 (0) 33.3 -> 33.4 (+0.1) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-call-expand 16 -> 16 (0) 24 -> 24 (0) 0 -> 0 (0) 0 -> 0 (0) 16.8 -> 16.8 (0) 16.8 -> 16.8 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-default-open-heavy-bash 16 -> 16 (0) 16 -> 16 (0) 74 -> 60 (-14) 37 -> 10 (-27) 50 -> 33.4 (-16.6) 116.8 -> 116.6 (-0.2) 3 -> 3 (0) 0 -> 0 (0) pass
default / terminal-side-panel-open 48 -> 48 (0) 56 -> 56 (0) 0 -> 0 (0) 0 -> 0 (0) 33.3 -> 33.4 (+0.1) 33.3 -> 33.4 (+0.1) 0 -> 0 (0) 0 -> 0 (0) pass
default / session-scroll-reading 16 -> 16 (0) 16 -> 16 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.8 (+0.1) 16.7 -> 16.8 (+0.1) 0 -> 0 (0) 0 -> 0 (0) pass
low-end / session-scroll-reading-long 0 -> 0 (0) 0 -> 0 (0) 51 -> 55 (+4) 1 -> 6 (+5) 33.3 -> 33.3 (0) 49.9 -> 50.1 (+0.2) 0 -> 1 (+1) 0 -> 0 (0) pass
low-end / session-timeline-recompute 32 -> 48 (+16) 40 -> 48 (+8) 0 -> 0 (0) 0 -> 0 (0) 33.4 -> 33.3 (-0.1) 33.4 -> 33.4 (0) 0 -> 0 (0) 1.075 -> 1.075 (0) pass
low-end / concurrent-shimmer-extreme 0 -> 0 (0) 0 -> 0 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.7 (0) 33.3 -> 16.8 (-16.5) 0 -> 0 (0) 0 -> 0 (0) 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.
Astro-Han added a commit that referenced this pull request May 24, 2026
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/ui/src/components/tabs.css (1)

449-494: ⚖️ Poor tradeoff

Hardcoded coupling between CSS and TSX padding.

The left: 10px on Line 478 is hardcoded to match the px-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

📥 Commits

Reviewing files that changed from the base of the PR and between 4729a65 and cb53eb9.

📒 Files selected for processing (15)
  • packages/app/e2e/actions.ts
  • packages/app/e2e/commands/panels.spec.ts
  • packages/app/e2e/files/file-tree.spec.ts
  • packages/app/e2e/inputs/select-review-filter.spec.ts
  • packages/app/e2e/prompt/prompt-slash-terminal.spec.ts
  • packages/app/e2e/selectors.ts
  • packages/app/e2e/session/session-artifacts.spec.ts
  • packages/app/e2e/session/session-composer-dock.spec.ts
  • packages/app/e2e/session/session-review.spec.ts
  • packages/app/e2e/snap/todo-status-only.snap.ts
  • packages/app/e2e/status/status-popover.spec.ts
  • packages/app/src/components/session/session-sortable-shell-tab.tsx
  • packages/app/src/components/titlebar.tsx
  • packages/app/src/pages/session/session-side-panel.tsx
  • packages/ui/src/components/tabs.css

Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Astro-Han added 2 commits May 24, 2026 14:56
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.
@Astro-Han Astro-Han force-pushed the claude/right-panel-visual branch from 98ae609 to c4beefd Compare May 24, 2026 06:56
…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.
@Astro-Han Astro-Han merged commit 2d53e91 into dev May 24, 2026
27 checks passed
@Astro-Han Astro-Han deleted the claude/right-panel-visual branch May 24, 2026 07:18
Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Astro-Han added a commit that referenced this pull request May 24, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows enhancement New feature or request P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant