Skip to content

fix(app): restore titlebar toggle visual contract after PR #878#880

Merged
Astro-Han merged 19 commits into
devfrom
claude/right-panel-followup-regression
May 24, 2026
Merged

fix(app): restore titlebar toggle visual contract after PR #878#880
Astro-Han merged 19 commits into
devfrom
claude/right-panel-followup-regression

Conversation

@Astro-Han

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

Copy link
Copy Markdown
Owner

Summary

Fix two visual regressions surfaced by #878 (right-panel tab strip portalled into the titlebar):

  1. The Right utility panel toggle vanishes when the right panel is open.
  2. The sidebar toggle in the titlebar no longer shows the aria-expanded selected-state chip.

Why

PR #878 added #pawwork-titlebar-tabs, an absolute-positioned slot anchored to the viewport right edge with width: var(--right-panel-width) and z-10. To make the existing sidepanel CSS in packages/ui/src/components/tabs.css match the portalled <Tabs.List>, the slot stamps data-component="tabs" + data-variant="sidepanel". That borrowed identity also inherits the base [data-component="tabs"] rule background-color: var(--bg-base) — an opaque page background. When the panel is open, the slot paints over the toggle button portalled into #pawwork-titlebar-right. PR #878's pointer-events-none fix restored clickability, but not visibility.

The sidebar-toggle chip regression is older: .titlebar-icon[aria-expanded="true"] in icon-button.css was scoped to [data-component="icon-button"], but all four titlebar usages render <Button variant="ghost"> (data-component="button"). The selector silently never matched. PR #878's broader titlebar reshape made the missing chip noticeable.

Change boundary

  • packages/ui/src/components/tabs.css: reset background-color: transparent on the sidepanel host. The right-panel-body below already owns the visible surface; the titlebar slot is just a transparent portal landing.
  • packages/ui/src/components/icon-button.css: drop the [data-component="icon-button"] ancestor from the .titlebar-icon rules. The class name carries the contract; the selector should not narrow it.
  • packages/app/src/components/titlebar.tsx: comment-only — the existing slot comment under-described why pointer-events-none was needed; expanded to document both the click-occlusion and visual-occlusion failure modes.

Scope blast

.titlebar-icon has four callsites — titlebar.tsx:110 (sidebar toggle), titlebar.tsx:129 (new-session), status-popover.tsx:31 (StatusPopover trigger), session-header.tsx:159 (Right utility panel toggle). All four are <Button variant="ghost"> and were previously dropping the aria-expanded chip. Broadening the selector restores the chip across the board, including the Right utility panel toggle and the StatusPopover trigger (twin instances of the same bug).

Reverted scope (out of this PR)

An earlier commit on this branch tried to drop the index.css selected-tab chip rule to level visual widths between Status / Files / Review. Manual Electron verification surfaced multiple downstream regressions (hover affordance lost, close-button visibility timing changed, icon mismatch) that the snap target did not catch (mouse at (0,0), static frame only). The chip removal commit was reverted; the chip-width inconsistency is filed as a separate follow-up so the root cause can be investigated with real-app interaction coverage, not headless static captures.

Related Issue

No standalone issue — direct PR #878 follow-up.

Human Review Status

Pending

Review Focus

  • tabs.css sidepanel host bg reset — confirm no consumer relied on the opaque host bg. The real <Tabs variant="sidepanel"> root in session-side-panel.tsx sits inside right-panel-body which is bg-bg-base; transparent on top of bg-base is visually identical.
  • icon-button.css selector broadening — .titlebar-icon is now an element-agnostic chrome class. Grep confirms it lives only in the four titlebar callsites; no test selectors hard-coded the prior [data-component="icon-button"].titlebar-icon shape.
  • E2E helper rightPanelTabList(page) in packages/app/e2e/actions.ts:344 still works — the slot keeps data-component="tabs" + data-scope="right-panel", and the portalled [role="tablist"] still lives inside the slot.

Risk Notes

Low. CSS-only behavior change with a comment update. No JS / DOM / data flow changes. Two-direction snap check (light + dark) covers both regressions on the same target.

How To Verify

bun run --filter=@opencode-ai/app typecheck  → exit 0
bun run --filter=@opencode-ai/ui typecheck   → exit 0
bun test packages/ui/test/                   → 357 passed
bun run --filter=@opencode-ai/app snap right-panel-titlebar → passed
  - light/dark grid: sidebar toggle shows surface-base chip when sidebar
    is open; close-right-panel button visible at right edge when right
    panel is open. Both were absent on the pre-fix capture.

Also manually verified in bun run dev:desktop: panel open / close transitions show the toggle button at the right edge, sidebar toggle chip visible when sidebar opened.

Screenshots or Recordings

Before (sidebar toggle without chip, close-panel button missing) vs after (chip restored, close-panel icon back at right edge) — captured locally via bun run --filter=@opencode-ai/app snap right-panel-titlebar.

Checklist

  • Type labelbug
  • Routing labelsapp, ui
  • Priority labelP2
  • 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 rail now only appears at desktop sizes and on session pages, collapsing to zero width elsewhere.
  • Style

    • Refined tab chip layout, spacing, and close-button positioning for consistent visuals and instant icon/close swapping.
    • Titlebar icon expanded-state visuals applied via a shared styling class.
    • Updated hover/selected tab colors to lighter overlay tokens.
  • Accessibility

    • Closable tabs advertise both Delete and Backspace.
  • Tests

    • Added E2E contract tests and visual regression snapshots for right-panel tabs.
  • Documentation

    • Clarified inline docs about titlebar/tab layout and styling.

Review Change Stack

PR #878 portalled the right-panel tab strip into the titlebar and added
a #pawwork-titlebar-tabs slot. Two visual regressions surfaced after the
merge:

1. The Right utility panel toggle vanished when the right panel was
   open. The slot stamps data-component='tabs' so the sidepanel CSS in
   tabs.css matches the portalled <Tabs.List>; that borrowed identity
   also inherits the base [data-component='tabs'] rule
   'background-color: var(--bg-base)'. With the slot positioned
   absolute / right: 0 / width: var(--right-panel-width) / z-10, the
   opaque background painted over the toggle button portalled into
   #pawwork-titlebar-right. PR #878 added pointer-events-none which
   restored clickability, but not visibility.

   Fix: reset background-color to transparent in the sidepanel host
   block in packages/ui/src/components/tabs.css. The slot becomes the
   transparent portal-landing it was always intended to be; the real
   right-panel-body underneath owns the visible surface.

2. The sidebar toggle in the titlebar lost its aria-expanded selected
   chip (surface-base background when sidebar is open). The CSS rule
   .titlebar-icon[aria-expanded='true'] was scoped to
   [data-component='icon-button'], but all four titlebar usages render
   <Button variant='ghost'> (data-component='button'). The selector
   silently never matched — this predates #878 but the broader titlebar
   reshape made it visible.

   Fix: drop the [data-component='icon-button'] ancestor from the
   .titlebar-icon rules in packages/ui/src/components/icon-button.css.
   The class name carries the contract; the selector should not narrow
   it. Scope blast: same fix re-enables the selected chip on the Right
   utility panel toggle and the StatusPopover trigger — both also use
   <Button class='titlebar-icon'> + aria-expanded.

Also updated the slot comment in titlebar.tsx to document both the
click and visual occlusion modes so the next reader does not have to
re-derive them.

Verification

- bun run --filter=@opencode-ai/app typecheck OK
- bun run --filter=@opencode-ai/ui typecheck OK
- bun run --filter=@opencode-ai/ui (357 unit tests) OK
- bun run --filter=@opencode-ai/app snap right-panel-titlebar OK,
  light + dark grid shows the sidebar toggle chip + the close-panel
  button restored at the right edge.

E2E helper rightPanelTabList is untouched: the slot keeps both
data-component='tabs' and data-scope='right-panel'.
@github-actions github-actions Bot added app Application behavior and product flows ui Design system and user interface P2 Medium priority labels May 24, 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/titlebar.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.

@Astro-Han Astro-Han added the bug Something isn't working label May 24, 2026
@coderabbitai

coderabbitai Bot commented May 24, 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 52 minutes and 45 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: c0cf88a7-c77d-4aee-a045-f89c8f79ce4e

📥 Commits

Reviewing files that changed from the base of the PR and between cd73d82 and 8ec16d0.

📒 Files selected for processing (1)
  • packages/app/e2e/session/titlebar-right-rail-contract.spec.ts
📝 Walkthrough

Walkthrough

Refactors titlebar right-rail layout with desktop breakpoint gating, updates sidepanel/tabs CSS (close-button anchoring and chip overlay tokens), removes local session trigger spacing in favor of tabs.css tokens, and adds Playwright contract and visual-snap tests for right-panel tab geometry and hover behavior.

Changes

Sidepanel Portal Styling and Tests

Layer / File(s) Summary
Titlebar right-rail portal & layout
packages/app/src/components/titlebar.tsx
Introduce tabsRailActive and tabsRailWidth(), replace absolute/portal-only right-titlebar markup with a flex sibling rail (#pawwork-titlebar-right, #pawwork-titlebar-tabs), and update portal-slot documentation.
Selection-state tokens and app CSS
packages/app/src/index.css
Add --row-hover-overlay / --row-active-overlay vocabulary and apply them to right-panel tab trigger-wrapper hover/selected backgrounds.
Tabs sidepanel variant adjustments
packages/ui/src/components/tabs.css
Set sidepanel host background transparent, adjust tablist height/gap for portal layout, scope leading-icon fade to :has(...):hover, anchor close-button via CSS anchor positioning, clamp inner close icon to 8px, and rewrite active-marker sizing/spacing.
Titlebar icon styling refactor
packages/ui/src/components/icon-button.css
Replace data-component-scoped titlebar icon selectors with a global .titlebar-icon class, move aria-expanded state styling to the new class, and add scoping documentation.
Session component spacing & focus guard
packages/app/src/components/session/session-sortable-shell-tab.tsx
Guard focus restoration with isConnected, remove local Tailwind spacing/border-radius from Tabs.Trigger, and add Backspace to aria-keyshortcuts.
Session-side-panel portal list adjustments
packages/app/src/pages/session/session-side-panel.tsx
Remove gap-0 and pointer-events-auto from the portal-rendered Tabs.List and document spacer behavior that pushes the add button to the rail edge.
Playwright contract tests (tab chip & close)
packages/app/e2e/session/session-tab-chip-contract.spec.ts
Add tests for opening extra tabs, selected/hover token equality, close glyph sizing/centering, instant icon/close swap, hover/rest regressions, non-closable tab behavior, padding parity, and chip geometry constraints.
Playwright titlebar right-rail contract tests
packages/app/e2e/session/titlebar-right-rail-contract.spec.ts
Add tests verifying toggle clickability with max tabs, responsive collapse of tabs slot below desktop breakpoint, and matching titlebar/tabs slot heights.
Playwright visual snap (hover states)
packages/app/e2e/snap/right-panel-tabs-hover.snap.ts
Add a snap that captures rest/hover-Files/hover-Review states in light/dark themes, emits bounding-box/opacities dumps for verification, and composes a grid output.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Astro-Han/pawwork#878: Related changes to right-panel/titlebar tab implementation and sidepanel tab styling that overlap with this PR's tab/CSS/test adjustments.
  • Astro-Han/pawwork#110: Prior work touching session tab trigger spacing and Tabs.Trigger styling used by the right-panel shell tabs.
  • Astro-Han/pawwork#369: Earlier edits touching the titlebar right slot and selectors that relate to the current titlebar layout changes.

Poem

🐰 I hopped into code, nudged rails to align,
tabs learned to hide when they're out of line,
icons scoped tidy, close-buttons take aim,
tests watched the pixels and captured the frame,
a small rabbit claps—geometry tame.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 title clearly summarizes the main change—restoring visual regressions in the titlebar toggle after PR #878—and is specific to the changeset.
Description check ✅ Passed The description covers all required sections: Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, How To Verify, Screenshots/Recordings, and a complete Checklist with all required items ticked.
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-followup-regression

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.

@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 addresses visual and click occlusion issues in the titlebar by setting the sidepanel tabs background to transparent and updating documentation. It also refactors .titlebar-icon CSS selectors to ensure styles are applied correctly when using different button components. A redundant CSS selector was identified in icon-button.css that should be simplified to improve maintainability.

Comment thread packages/ui/src/components/icon-button.css Outdated
@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) 32 -> 48 (+16) 86 -> 63 (-23) 36 -> 13 (-23) 16.8 -> 16.8 (0) 116.7 -> 116.7 (0) 4 -> 3 (-1) 0 -> 0 (0) pass
default / long-session-input-lag 40 -> 48 (+8) 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 -> 48 (+8) 72 -> 64 (-8) 0 -> 0 (0) 0 -> 0 (0) 16.8 -> 16.8 (0) 33.3 -> 33.3 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-call-expand 40 -> 40 (0) 40 -> 40 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.7 (0) 16.7 -> 16.7 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-default-open-heavy-bash 16 -> 16 (0) 16 -> 24 (+8) 64 -> 63 (-1) 26 -> 23 (-3) 50 -> 50 (0) 116.6 -> 116.6 (0) 3 -> 3 (0) 0 -> 0.004 (+0.004) pass
default / terminal-side-panel-open 48 -> 48 (0) 64 -> 48 (-16) 0 -> 0 (0) 0 -> 0 (0) 33.3 -> 33.3 (0) 33.4 -> 33.3 (-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.8 -> 16.8 (0) 16.8 -> 16.8 (0) 0 -> 0 (0) 0 -> 0 (0) pass
low-end / session-scroll-reading-long 0 -> 0 (0) 0 -> 0 (0) 0 -> 0 (0) 0 -> 0 (0) 33.3 -> 33.3 (0) 50 -> 50 (0) 0 -> 0 (0) 0 -> 0 (0) pass
low-end / session-timeline-recompute 32 -> 40 (+8) 48 -> 40 (-8) 0 -> 0 (0) 0 -> 0 (0) 33.3 -> 33.4 (+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) 16.8 -> 16.8 (0) 0 -> 0 (0) 0 -> 0 (0) pass

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 Astro-Han changed the title fix(app): restore titlebar toggle visual contract after PR #878 fix(app): restore titlebar toggle visual contract + level shell tab widths May 24, 2026
@Astro-Han Astro-Han changed the title fix(app): restore titlebar toggle visual contract + level shell tab widths fix(app): restore titlebar toggle visual contract after PR #878 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.
Comment thread packages/app/e2e/session/session-tab-chip-contract.spec.ts Fixed
The previous commit (858c394) addressed the chip color, × size, and
alignment, but missed one state: a selected closable tab with the cursor
parked away rendered both the leading icon AND the ×. Reproduction was
the user-reported 'click Files, mouse off the tab strip — folder icon and
× visible at the same time.'

Root cause is a specificity loss. The base [data-component="tabs"] block
in this file declares:

  [data-slot="tabs-trigger-wrapper"]:has([data-selected])
    [data-slot="tabs-trigger-close-button"] { opacity: 1 }

at 4 attribute selectors. The sidepanel variant's slot rule sat at 3 attrs
([data-component="tabs"][data-variant="sidepanel"]
 [data-slot="tabs-trigger-close-button"]), so the :has([data-selected])
forced opacity:1 on every selected closable tab regardless of hover. The
hide-leading-icon rule only fires on :hover, so the leading icon stayed
visible whenever the cursor wasn't over the tab — producing the double-icon
state the sidepanel design explicitly forbids.

Fix: route the sidepanel slot's rest and :hover rules through
[data-slot="tabs-trigger-wrapper"] so the chain hits 4 attrs (rest) and
5 (hover), winning over the base :has([data-selected]) path. The × is
now strictly hover-driven on sidepanel; selected closable tabs at rest
show only their leading icon.

Verification:

- New assertion in session-tab-chip-contract.spec.ts: click Files (select +
  closable), park the cursor far off the tab strip, then assert leading
  opacity 1 and close-slot opacity 0. Six contract assertions now pass
  (chip color, × size, alignment, instant swap, hover-only ×, Status guard).

@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 (2)
packages/app/e2e/session/session-tab-chip-contract.spec.ts (1)

32-33: ⚡ Quick win

Use modKey helper for shortcut chords on Line 32 and Line 33.

Please replace the hardcoded ControlOrMeta+... strings with the shared modKey utility pattern used in E2E tests.

As per coding guidelines: Use modKey from utils for cross-platform keyboard shortcuts (Meta on Mac, Control on Linux/Windows).

🤖 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/app/e2e/session/session-tab-chip-contract.spec.ts` around lines 32 -
33, Tests use hardcoded "ControlOrMeta+..." chords; replace those with the
shared modKey helper. Update the two page.keyboard.press calls in
session-tab-chip-contract.spec.ts (the calls sending "ControlOrMeta+\\" and
"ControlOrMeta+Shift+R") to build the shortcut using the exported modKey utility
(e.g., modKey("\\") and modKey("Shift+R")) so the test uses the cross-platform
modifier; keep using page.keyboard.press but pass the modKey-composed string.
Ensure modKey is imported from the tests/utils module at the top of the file if
not already.
packages/app/e2e/snap/right-panel-tabs-hover.snap.ts (1)

130-138: ⚡ Quick win

Track the created session inside beforeGoto immediately after creation.

This avoids a cleanup gap if project.open() fails after sdk.session.create(...) but before post-open tracking runs.

♻️ Suggested change
   await project.open({
     beforeGoto: async ({ sdk }) => {
       const session = await sdk.session.create({ title: "snap right panel tabs hover" }).then((res) => res.data)
       sessionID = session?.id
+      if (sessionID) project.trackSession(sessionID)
     },
   })
   if (!sessionID) throw new Error("Session create did not return an id")
-  project.trackSession(sessionID)

Based on learnings: call project.trackSession(sessionID) inside the beforeGoto callback ... and use post-open tracking for directories/cross-workspace resources.

🤖 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/app/e2e/snap/right-panel-tabs-hover.snap.ts` around lines 130 - 138,
Move the call to project.trackSession(sessionID) into the beforeGoto callback
immediately after creating the session (inside the promise chain from
sdk.session.create) so the session is tracked even if project.open() fails;
specifically update the beforeGoto block that calls sdk.session.create to assign
sessionID and call project.trackSession(sessionID) there, and remove or limit
the later post-open project.trackSession call (keep post-open tracking only for
directory/cross-workspace resources).
🤖 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/app/e2e/session/session-tab-chip-contract.spec.ts`:
- Around line 32-33: Tests use hardcoded "ControlOrMeta+..." chords; replace
those with the shared modKey helper. Update the two page.keyboard.press calls in
session-tab-chip-contract.spec.ts (the calls sending "ControlOrMeta+\\" and
"ControlOrMeta+Shift+R") to build the shortcut using the exported modKey utility
(e.g., modKey("\\") and modKey("Shift+R")) so the test uses the cross-platform
modifier; keep using page.keyboard.press but pass the modKey-composed string.
Ensure modKey is imported from the tests/utils module at the top of the file if
not already.

In `@packages/app/e2e/snap/right-panel-tabs-hover.snap.ts`:
- Around line 130-138: Move the call to project.trackSession(sessionID) into the
beforeGoto callback immediately after creating the session (inside the promise
chain from sdk.session.create) so the session is tracked even if project.open()
fails; specifically update the beforeGoto block that calls sdk.session.create to
assign sessionID and call project.trackSession(sessionID) there, and remove or
limit the later post-open project.trackSession call (keep post-open tracking
only for directory/cross-workspace resources).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6136aad0-44bc-4ea6-a0fb-510398c06273

📥 Commits

Reviewing files that changed from the base of the PR and between c989a92 and d4684fd.

📒 Files selected for processing (4)
  • packages/app/e2e/session/session-tab-chip-contract.spec.ts
  • packages/app/e2e/snap/right-panel-tabs-hover.snap.ts
  • packages/app/src/index.css
  • packages/ui/src/components/tabs.css

Astro-Han added 5 commits May 24, 2026 18:41
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.
…e button

Two visible regressions in the right-panel tab strip:

1. Content (leading icon + label) was left-biased inside the 5.5rem
   min-width chip, not visually centered.
2. Closable wrappers (Files / Review / Terminal) rendered ~12px wider
   than the non-closable Status wrapper, so chips looked uneven.

Root cause: the previous grid-overlay-cell approach forced
`justify-content: flex-start` on the trigger (so the leading-icon and
the overlaid × shared one padding-inline-start anchor), and base tabs
CSS adds `padding-right: 12px` to any wrapper that owns a
close-button slot — which had no purpose here but leaked through into
sidepanel.

Switch to CSS anchor positioning (Chrome 125+, Electron 40 ships
Chromium 134):

- leading icon span owns `anchor-name: --tab-leading`
- close-button slot is `position: absolute` with `position-anchor:
  --tab-leading` + `left/top: anchor(center)` + `translate: -50% -50%`,
  so × is mathematically pinned to the leading icon's center with no
  padding coupling
- wrapper sets `anchor-scope: --tab-leading` so each tab only sees its
  own leading icon
- trigger restored to `justify-content: center` — content is now
  visually centered inside the chip
- sidepanel wrapper resets `padding-right: 0` on `:has(close-button-slot)`
  to neutralise the base 12px and bring closable wrappers to parity
  with Status
- `--tab-leading-inset` token + `grid-area: 1/1` indirection removed:
  the anchor system makes the coupling explicit at the DOM level

Regression test added: `closable wrapper has no trailing padding
(parity with non-closable)` — RED-verified by temporarily removing the
override (paddingRight reports 12px). Existing × center-alignment test
(within 1px of leading-icon center) still passes — anchor positioning
satisfies the same observable contract by construction.

Verification: 10/10 e2e contract tests green; lint clean.
Five tweaks bundled together. The first is the actual CI fix (CodeQL
ReDoS); the rest land while we're touching the same files.

1. Replace nested-quantifier regex in the chip contract spec with a
   split/every check. CodeQL flagged `/^(0s\s*,?\s*)+$/` as ReDoS
   (exponential backtracking on `0s 0s 0s ...` inputs). The replacement
   reads more cleanly and is provably linear.

2. Drop the redundant
   `.titlebar-icon[data-icon="menu"][aria-expanded="true"]` selector —
   fully covered by the immediately following
   `.titlebar-icon[aria-expanded="true"]` (gemini-code-assist).

3. Use shared `modKey` helper in `openExtraTabs` instead of hardcoded
   `ControlOrMeta+...` chords (CodeRabbit nitpick).

4. In `right-panel-tabs-hover.snap.ts`, call `project.trackSession`
   inside the `beforeGoto` callback right after `sdk.session.create`,
   so cleanup runs even if a later step in `project.open()` throws
   (CodeRabbit nitpick).

5. Visual refinement from this round of design review:
   - chip border-radius: `--radius-lg` (14) → `--radius-md` (10), one
     of the three standard radii per DESIGN.md L305
   - trigger min-width: 80px → 72px (still covers 2-char CJK + icon +
     gap + padding with breathing room, on the 4pt grid)
   - trigger padding-inline: `--space-sm` (8) → `--space-xs` (4) to
     tighten the chip to its content
   - tabs-list gap: `--space-sm` (8) → `--space-xs` (4) so chips read
     as a strip rather than spaced cards
   - e2e contract test asserts updated to match (min 72, list gap 4)

Verification: 10/10 chip-contract e2e green.

@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

🧹 Nitpick comments (3)
packages/app/src/pages/session/session-side-panel.tsx (1)

360-365: ⚡ Quick win

Correct the documented gap token/value in this comment.

Line 361 currently documents var(--space-sm) (8px), but sidepanel tabs gap is now var(--space-xs) (4px) in packages/ui/src/components/tabs.css (Line 424).

Suggested fix
-                    {/* `gap` is intentionally omitted — the sidepanel variant
-                        in tabs.css owns the inter-tab gap via `var(--space-sm)`
-                        (8px / 4pt-grid). Tailwind's `gap-0` (or `gap-2`)
+                    {/* `gap` is intentionally omitted — the sidepanel variant
+                        in tabs.css owns the inter-tab gap via `var(--space-xs)`
+                        (4px / 4pt-grid). Tailwind's `gap-0` (or `gap-2`)
🤖 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/app/src/pages/session/session-side-panel.tsx` around lines 360 -
365, Update the inline comment in session-side-panel.tsx that currently states
the sidepanel variant uses `var(--space-sm)` (8px) — change it to reflect the
actual gap token/value `var(--space-xs)` (4px) which is defined in the tabs
component CSS (see tabs.css, tabs component rules around the sidepanel variant);
keep the rest of the comment intact but correct the token name and pixel value.
packages/app/src/components/session/session-sortable-shell-tab.tsx (1)

93-100: ⚡ Quick win

Update token reference in the inline comment (radius-lgradius-md).

The comment at Line 95 references --radius-lg, but the sidepanel chip radius is now driven by --radius-md in packages/ui/src/components/tabs.css (Line 456). Keeping this aligned avoids future styling drift.

Suggested fix
-        // `--radius-lg` (14px) tokens. Tailwind utilities are kept out here
+        // `--radius-md` (10px) tokens. Tailwind utilities are kept out here
🤖 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/app/src/components/session/session-sortable-shell-tab.tsx` around
lines 93 - 100, Update the inline comment in session-sortable-shell-tab.tsx that
mentions the radius token so it matches the current CSS: change the reference
from `--radius-lg` to `--radius-md` in the comment block above the `button`
variant (the comment immediately preceding the `button: "h-7 min-h-7 inline-flex
items-center whitespace-nowrap text-h3 text-fg-weak",` line). Ensure the wording
still explains that spacing and border-radius live in tabs.css under the
sidepanel variant and now reference `--radius-md` to keep the documentation
aligned with packages/ui/src/components/tabs.css.
packages/app/e2e/snap/right-panel-tabs-hover.snap.ts (1)

85-86: ⚡ Quick win

Replace fixed waitForTimeout(200) delays with state-based waits before screenshots.

These sleeps can make snap output timing-sensitive in CI. Wait for the expected visual state (e.g., close-slot/leading-icon opacity or hovered tab style) before capture.

Proposed fix (state-based wait)
-  await page.waitForTimeout(200)
+  await expect(page.getByRole("tab", { name: "Files" })).toBeVisible()

...
-  await page.getByRole("tab", { name: "Files" }).hover()
-  await page.waitForTimeout(200)
+  await page.getByRole("tab", { name: "Files" }).hover()
+  await expect
+    .poll(async () =>
+      page.evaluate(() => {
+        const wrap = document.querySelector('[data-slot="tabs-trigger-wrapper"][data-value="files"]')
+        const slot = wrap?.querySelector('[data-slot="tabs-trigger-close-button"]') as HTMLElement | null
+        return slot ? getComputedStyle(slot).opacity : null
+      }),
+    )
+    .toBe("1")

...
-  await page.getByRole("tab", { name: "Review" }).hover()
-  await page.waitForTimeout(200)
+  await page.getByRole("tab", { name: "Review" }).hover()
+  await expect
+    .poll(async () =>
+      page.evaluate(() => {
+        const wrap = document.querySelector('[data-slot="tabs-trigger-wrapper"][data-value="review"]')
+        const slot = wrap?.querySelector('[data-slot="tabs-trigger-close-button"]') as HTMLElement | null
+        return slot ? getComputedStyle(slot).opacity : null
+      }),
+    )
+    .toBe("1")

Also applies to: 98-99, 113-114

🤖 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/app/e2e/snap/right-panel-tabs-hover.snap.ts` around lines 85 - 86,
Replace the fixed sleep calls (page.waitForTimeout(200)) before dumpBoxes with
state-based waits: wait for the specific DOM/CSS state that indicates the tab
hover is rendered (e.g., check the close-slot or leading-icon opacity or that
the tab has the hovered style) using page.waitForSelector/page.waitForFunction
or equivalent, then call dumpBoxes(page, `${label}-rest`); update the same
pattern for the other instances near dumpBoxes calls (the occurrences analogous
to lines 98-99 and 113-114) so snapshots are taken only after the expected
visual state appears.
🤖 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/e2e/session/session-tab-chip-contract.spec.ts`:
- Around line 259-264: The DOM queries for tab triggers/wrappers are currently
global and can pick up unrelated tab UIs; change the evaluation to first select
the right-panel tablist root (use the existing helper rightPanelTabList(page) or
the root selector '[data-component="tabs"][data-variant="sidepanel"]') and then
scope querySelectorAll('[data-slot="tabs-trigger"]') and the wrapper query to
that root before mapping widths/paddings (apply same scoping for the other block
around lines 284-294). Locate the code that builds the widths/paddings arrays
and replace the top-level document.querySelectorAll calls with
root.querySelectorAll to ensure only right-panel tabs are measured.

---

Nitpick comments:
In `@packages/app/e2e/snap/right-panel-tabs-hover.snap.ts`:
- Around line 85-86: Replace the fixed sleep calls (page.waitForTimeout(200))
before dumpBoxes with state-based waits: wait for the specific DOM/CSS state
that indicates the tab hover is rendered (e.g., check the close-slot or
leading-icon opacity or that the tab has the hovered style) using
page.waitForSelector/page.waitForFunction or equivalent, then call
dumpBoxes(page, `${label}-rest`); update the same pattern for the other
instances near dumpBoxes calls (the occurrences analogous to lines 98-99 and
113-114) so snapshots are taken only after the expected visual state appears.

In `@packages/app/src/components/session/session-sortable-shell-tab.tsx`:
- Around line 93-100: Update the inline comment in
session-sortable-shell-tab.tsx that mentions the radius token so it matches the
current CSS: change the reference from `--radius-lg` to `--radius-md` in the
comment block above the `button` variant (the comment immediately preceding the
`button: "h-7 min-h-7 inline-flex items-center whitespace-nowrap text-h3
text-fg-weak",` line). Ensure the wording still explains that spacing and
border-radius live in tabs.css under the sidepanel variant and now reference
`--radius-md` to keep the documentation aligned with
packages/ui/src/components/tabs.css.

In `@packages/app/src/pages/session/session-side-panel.tsx`:
- Around line 360-365: Update the inline comment in session-side-panel.tsx that
currently states the sidepanel variant uses `var(--space-sm)` (8px) — change it
to reflect the actual gap token/value `var(--space-xs)` (4px) which is defined
in the tabs component CSS (see tabs.css, tabs component rules around the
sidepanel variant); keep the rest of the comment intact but correct the token
name and pixel value.
🪄 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: 14ac8d60-9c65-49a8-8d4a-1df9bd971b10

📥 Commits

Reviewing files that changed from the base of the PR and between d4684fd and c89ce1a.

📒 Files selected for processing (7)
  • packages/app/e2e/session/session-tab-chip-contract.spec.ts
  • packages/app/e2e/snap/right-panel-tabs-hover.snap.ts
  • packages/app/src/components/session/session-sortable-shell-tab.tsx
  • packages/app/src/index.css
  • packages/app/src/pages/session/session-side-panel.tsx
  • packages/ui/src/components/icon-button.css
  • packages/ui/src/components/tabs.css
💤 Files with no reviewable changes (1)
  • packages/ui/src/components/icon-button.css

Comment thread packages/app/e2e/session/session-tab-chip-contract.spec.ts Outdated
Astro-Han added 4 commits May 24, 2026 21:16
Acted on the 6 crosscheck findings worth merging; declined 3 with reason.

Code findings addressed
-----------------------
1. e2e contract assertions were too loose (codex P2/P3, flagged 4×):
   - selected-overlay test now compares chip bg to a paint-resolved
     `--row-active-overlay` exactly, instead of "some low-alpha rgba"
     in 0-0.2. Swapping the active token for hover (or any other low-alpha
     value) now fails.
   - hover-overlay test compares to `--row-hover-overlay` exactly and
     also asserts inequality with the active overlay, so the two-tier
     vocabulary stays distinct.
   - min-width footprint test was a "≥ 71" floor — silently passed if
     the floor was raised to 120. Now per-tab: Status + Files must equal
     72 (the floor is what keeps them uniform); Review tolerates ≥ 72
     because its Latin natural width slightly exceeds the floor.
   - "trigger gap" test extended to a full chip-geometry contract: also
     pins trigger padding-inline (4px) and wrapper border-radius (10px,
     --radius-md), the two geometry decisions PR #880 settled that had
     no test coverage.

2. Pointer-events leak in the titlebar tab strip (codex P2 / claude P1):
   `<Tabs.List>` re-enables pointer events for the whole list so the tab
   triggers stay clickable, but the flex-1 spacer between the last tab
   and the `+` button was also inheriting that, silently swallowing
   clicks on the Right utility toggle in `#pawwork-titlebar-right` that
   overlaps the same x range. Fix is one class on the spacer:
   `pointer-events-none`. Titlebar comment updated to record the new
   chain (slot none → list auto → spacer none).

3. `aria-keyshortcuts="Delete"` on closable tabs (codex P3) — the
   handler also accepts Backspace as the macOS alias, so screen readers
   were getting a partial advertisement. Now `"Delete Backspace"` per
   ARIA spec.

4. Post-close focus restore could land on a detached node (claude P2):
   rapid successive closes can remove the captured sibling before its
   rAF fires; calling `.focus()` on a detached element silently moves
   focus to <body>. Guard with `isConnected` inside the rAF.

5. Duplicate comment blocks in titlebar.tsx (claude P3): two stacked
   block comments both explained the portal landing pad's "borrowed
   identity" and the two-fold occlusion fix. Merged into one ordered
   block.

6. `openExtraTabs` poll asserted `=== 3` (claude P3): would fail before
   reaching real assertions if any future default-open tab landed in the
   strip. Relaxed to `>= 3`.

Declined with reason
--------------------
- Anchor positioning Chrome-125 floor (claude P1): Electron 40 ships
  Chromium 134, comfortably past the floor. The dev web preview also
  targets recent Chromium. The comment in tabs.css already records this.
- 4-attr-specificity workaround and split chip-bg authority between
  tabs.css and index.css (claude P2 ×2): real architectural smells, but
  fixing them means cascade/@layer surgery touching unrelated variants.
  Out of scope for this PR; worth its own follow-up if it keeps biting.
- `tabs-drag-preview` still uses the old 26h/6r dimensions (claude P3):
  drag-preview is a single shared component across all tab variants and
  was already mismatched before PR #880. Per-variant drag-preview
  geometry is out of scope.

Verification: 10/10 chip-contract e2e green; lint clean.
Three small simplifications surfaced by the code-simplifier pass.
No behavior change: visual contract, e2e assertions, and lint all
green at HEAD.

- e2e contract spec: drop the `ensureRightPanelOpen(page)` helper. It
  was a one-line passthrough to `openRightPanel(page)` repeated 10×
  with no extra logic; calling the underlying action directly removes
  the indirection.
- snap: hoist the repeated `waitForTimeout(200) + dumpBoxes + screenshot`
  sequence into a `captureShot(page, name)` helper, and lift the
  identical `clip` rectangle into a `TAB_STRIP_CLIP` const. Three
  near-duplicate state blocks collapse into three one-liners; clip
  geometry, animation freezing, and dump-box output are unchanged.
- index.css: merge two adjacent comment blocks that restated the same
  "chip lives on the wrapper, matches the sidebar session row" point.
  All distinct content (overlay tokens, specificity-dance rationale)
  preserved in one cohesive comment.

Verification: 10/10 chip-contract e2e green; lint clean.
Two code-review findings from the latest round.

P2 — selected right-panel tab visually weakened on hover
------------------------------------------------------------
Before this fix, the hover wrapper rule in `packages/app/src/index.css`
had specificity b=6 (3 data-attrs + data-slot + :hover + :not(:disabled))
while the selected rule was only b=5 (4 attrs + :has(...)). When the
*currently selected* tab was hovered both rules matched and hover won,
so the chip repainted with the lighter 4% `--row-hover-overlay` instead
of the heavier 6% `--row-active-overlay`. The active tab visually
weakened under the cursor — the test title already promised "selected
wins", but the existing assertion only hovered an *unselected* Files
tab and never exercised the combined state.

Fix: bump the selected rule to b=6 by adding `:not(:disabled)`, then
rely on source order — the selected rule is declared after the hover
rule, so on the selected+hover wrapper both rules match at b=6 and
selected (declared later) wins.

Earlier attempt — narrowing the hover rule to `:not(:has([data-selected]))`
to skip selected wrappers entirely — passed the dropping-hover side of
the contract but left the selected rule alone at b=5, and `getComputedStyle`
on the wrapper returned `rgba(0, 0, 0, 0)` (transparent) instead of the
expected active overlay. The matching-rule inspector only reported my
selected rule as a bg-color source for the wrapper, yet a hard-coded
`rgba(0, 255, 0, 0.5)` test value also failed to paint while `outline:
2px solid magenta` on the same rule did paint — so the rule was applying
to the element, but something in the cascade was strictly winning on
`background-color` in a way the JS inspector did not surface. Bumping
the selected rule's specificity sidesteps the mystery entirely and
gives us the symmetric "two equal-specificity rules, source order
decides" pattern already used for the hover-rule's specificity dance
against the sidepanel base in tabs.css.

Regression test: new "selected tab on hover keeps the active overlay
(selected wins)" — clicks Files to select, hovers Files, asserts the
wrapper bg equals the resolved `--row-active-overlay` and NOT
`--row-hover-overlay`. Verified RED before the CSS change (received
0.04 hover alpha), GREEN after.

P3 — stale comments pointing at old token values
---------------------------------------------------
Three comments still referenced the pre-settled geometry:

- `session-side-panel.tsx:361` — said inter-tab gap is `--space-sm` (8px)
  but the sidepanel variant in tabs.css uses `--space-xs` (4px) since
  the last round.
- `session-sortable-shell-tab.tsx:97-98` — said the chip rides on
  `--space-sm` (8px) and `--radius-lg` (14px) tokens, but production
  uses `--space-sm` for icon↔label gap only, `--space-xs` (4px) for
  chip-edge padding, and `--radius-md` (10px) for the corner.

Comments now match the actual current tokens so a future reader is
not misled into "fixing" the code to match the stale narrative.

Verification: 11/11 chip-contract e2e green; lint clean.
…ggle own disjoint geometry

The titlebar tabs portal slot was absolute-positioned over
`#pawwork-titlebar-right`, relying on `pointer-events: none` on the slot
and `pointer-events: auto` on every interactive descendant to let the
right utility toggle stay clickable through the overlay. The hack held
up to three tabs; at four (status + files + review + terminal) the
`+` dropdown trigger got pushed into the toggle's x-range and — because
`+` itself must be `pointer-events: auto` for its dropdown to open —
intercepted clicks meant for the toggle. Same pattern would have
re-broken with any future tab addition.

Replace the overlay with an in-flow flex rail. `#pawwork-titlebar-right`
and `#pawwork-titlebar-tabs` are now siblings 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 (the existing 240ms transition on the CSS var carries it
smoothly). Disjoint geometry, no overlay, no pointer-events choreography.

Gate `border-l` and the panel-width track on `tabsRailActive` (session
route + right panel open) so navigating to home/settings with the panel
left open does not still claim panel-width in the titlebar and push the
StatusPopover fallback inboard. Move `pr-2` from the outer rail onto
`#pawwork-titlebar-right` so the tabs slot's `border-l` stays flush with
the right-panel body's `border-l` directly below it (8px misalignment
otherwise).

Also addresses P3 from the same review: scope the chip-contract spec's
`querySelectorAll` calls to
`[data-component="tabs"][data-variant="sidepanel"][data-scope="right-panel"]`
so unrelated tablists on the page (e.g. Review's inner tab row) cannot
pollute the assertion.

Verified:
- bun test (1544 pass)
- bun run typecheck (clean)
- bun run test:e2e session-tab-chip-contract.spec.ts (12/12 pass,
  including the previously-red "right utility toggle stays clickable
  with the full tab set open" regression)
- bun run snap right-panel-titlebar (visual: toggle sits to the left
  of the tabs portal, all four tabs + `+` visible, panel border-l
  flush with titlebar slot border-l)

Adjacent failures in right-panel-width.spec.ts (`[role="tablist"]`
descendant of `<aside>`, turn-list visibility) reproduce on HEAD too —
pre-existing, unrelated to this change.

Refs PR #880.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/app/e2e/snap/right-panel-tabs-hover.snap.ts (1)

29-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid exact tab-count gating here; require a minimum instead.

toBe(3) is brittle and can fail before assertions when an additional default tab is introduced.

Suggested fix
-  await expect.poll(() => page.getByRole("tab").count(), { timeout: 5_000 }).toBe(3)
+  await expect
+    .poll(() => page.getByRole("tab").count(), { timeout: 5_000 })
+    .toBeGreaterThanOrEqual(3)
🤖 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/app/e2e/snap/right-panel-tabs-hover.snap.ts` at line 29, Change the
brittle exact tab-count assertion to require a minimum: replace the call to
expect.poll(() => page.getByRole("tab").count(), { timeout: 5_000 }).toBe(3)
with a minimum check such as expect.poll(() => page.getByRole("tab").count(), {
timeout: 5_000 }).toBeGreaterThanOrEqual(3) (or otherwise assert >= 3) so the
test waits for at least three tabs instead of exactly three.
🤖 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/e2e/session/session-tab-chip-contract.spec.ts`:
- Line 426: Replace the hardcoded "Control+`" key press in the test so it uses
the cross-platform modKey from your utils; locate the page.keyboard.press call
in session-tab-chip-contract.spec.ts (the line that currently calls
page.keyboard.press("Control+`")) and change it to use the imported modKey
variable (ensure modKey is imported from your utils if missing) so the test
sends `${modKey}+`` via page.keyboard.press for consistent behavior on
macOS/Windows/Linux.

---

Outside diff comments:
In `@packages/app/e2e/snap/right-panel-tabs-hover.snap.ts`:
- Line 29: Change the brittle exact tab-count assertion to require a minimum:
replace the call to expect.poll(() => page.getByRole("tab").count(), { timeout:
5_000 }).toBe(3) with a minimum check such as expect.poll(() =>
page.getByRole("tab").count(), { timeout: 5_000 }).toBeGreaterThanOrEqual(3) (or
otherwise assert >= 3) so the test waits for at least three tabs instead of
exactly three.
🪄 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: 713e73a1-9cba-499e-bd98-21b05e67e633

📥 Commits

Reviewing files that changed from the base of the PR and between c89ce1a and 3902f44.

📒 Files selected for processing (7)
  • packages/app/e2e/session/session-tab-chip-contract.spec.ts
  • packages/app/e2e/snap/right-panel-tabs-hover.snap.ts
  • packages/app/src/components/session/session-sortable-shell-tab.tsx
  • packages/app/src/components/titlebar.tsx
  • packages/app/src/index.css
  • packages/app/src/pages/session/session-side-panel.tsx
  • packages/ui/src/components/tabs.css

Comment thread packages/app/e2e/session/session-tab-chip-contract.spec.ts Outdated
Astro-Han added 2 commits May 24, 2026 23:06
…meets the panel separator

Followup-review P2 (non-blocking): with `#pawwork-titlebar-tabs` moved
from absolute overlay to an in-flow flex sibling, its `self-stretch`
only reaches the parent rail's content height. The titlebar root uses
`items-center` (grid), which lets each grid cell collapse to its child
content box unless the cell opts out — so the rail was sized to the
toggle button (≈30px) and the tabs slot's `border-l` only painted the
toggle row, breaking above and below it and failing to meet the
right-panel body's `border-l` directly underneath.

Add `self-stretch` to the outer right rail so it fills the full
titlebar height (44px on desktop). `#pawwork-titlebar-right`'s inner
`items-center` keeps the toggle vertically centered as before.

Regression test pins the tabs slot's bounding height to the titlebar's
own height — would have caught this drift the first time and will catch
any future regression that drops the stretch chain.

Verified:
- bun run test:e2e session-tab-chip-contract.spec.ts (13/13, including
  the new "tabs slot border-l spans the full titlebar height" guard)
- bun run snap right-panel-titlebar (the seam now reads as one
  continuous line from titlebar top to viewport bottom)

Refs PR #880.
The right utility toggle clickability test and the tabs-slot full-height
test were sitting in session-tab-chip-contract.spec.ts because they
shared the "open 3+ tabs" fixture, but they assert a different contract:
the titlebar right rail's *layout* (toggle/tabs disjoint geometry, rail
full-height so the slot's border-l meets the panel separator). That has
nothing to do with chip visuals, × geometry, hover, or 4pt grid pinning.

Move both tests into their own spec so future titlebar-rail regressions
(e.g. toggle motion when the panel opens, the home/settings 0-width
gate, sidebar↔rail interactions) have an obvious home and the chip
spec stays focused on its single subject.

No behavior change — same 13 assertions across the two files.

Verified:
- bun run test:e2e session-tab-chip-contract.spec.ts (11/11)
- bun run test:e2e titlebar-right-rail-contract.spec.ts (2/2)

Refs PR #880.
Astro-Han added 2 commits May 24, 2026 23:33
…essionSidePanel

PR #880 followup review P2: `SessionSidePanel` only renders the panel
(and portals tab content into the titlebar slot) at viewport ≥768px,
guarded by `createMediaQuery("(min-width: 768px)")`. But
`--right-panel-width` and the titlebar's `tabsRailActive` only checked
`layout.rightPanel.opened()`. Opening the panel at desktop width and
then shrinking the viewport below 768px would leave the titlebar
reserving panel-width of empty rail (nothing portals in under the
breakpoint), pushing the right utility toggle off the viewport edge
with nothing visible to justify the gap.

Add the same `createMediaQuery("(min-width: 768px)")` gate to
`tabsRailActive` so the titlebar rail and the panel agree on "is the
right panel actually visible right now": route + state + viewport.

A cleaner future direction is to lift this predicate to shared layout
state so titlebar and panel both consume it; out of scope for this PR.

CSS anchor positioning note (reviewer's "待验证" item): PawWork is a
macOS/Windows desktop product per AGENTS.md, shipped only via Electron
(Chromium 134+ in Electron 40, well above the 125+ anchor-positioning
floor). `bun run dev` web mode is a dev convenience, not a shipped
surface — no fallback needed.

Verified:
- bun run typecheck (clean)
- bun run test:e2e titlebar-right-rail-contract.spec.ts (3/3 including
  the new "tabs slot shrinks to 0 width when viewport drops below the
  desktop breakpoint" guard, which was RED before this fix)

Refs PR #880.
CodeQL's js/redos rule flagged the literal `"0s, 0s"` example inside a
test comment (line 152 cols 66-69 in the previous text), interpreting
the quoted duration sample as a regex pattern even though no regex is
constructed anywhere in the test — the actual implementation already
uses split-and-check.

Drop the literal example from the comment so the parser has no quoted
"0s,…" string to misread. Behavior unchanged; same assertion shape.

Skipped review comments and why:
- gemini icon-button.css redundant selector: stale; only one
  `.titlebar-icon[aria-expanded="true"]` exists in the file, no
  `data-icon="menu"` variant present.
- coderabbit Control+\` → modKey: terminal.toggle's keybind is
  hardcoded `ctrl+\`` in use-session-commands.tsx (not `mod+\``), so
  it's Ctrl on every platform. modKey would send Meta on macOS and
  break the test.
- coderabbit modKey at L32-33 / DOM scope to right-panel root:
  already done in earlier commits (3b16637, bc70865).
- coderabbit comment drift (radius-lg, space-sm, etc.):
  comments already reference radius-md / space-xs in the current tree.
- snap file nits (trackSession move, waitForTimeout, toBe(3)):
  right-panel-tabs-hover.snap.ts wasn't modified in this PR's followup
  cleanup — those land in their own PR if pursued.

Refs PR #880.

@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

🧹 Nitpick comments (1)
packages/app/src/components/titlebar.tsx (1)

26-33: 💤 Low value

Consider extracting the breakpoint to a shared constant.

The 768px breakpoint must stay in sync with SessionSidePanel's desktop gate. The comment documents this well, but a shared constant (e.g., DESKTOP_BREAKPOINT in a layout utilities module) would make the coupling explicit and eliminate the risk of accidental divergence.

This is low-priority given the thorough documentation.

🤖 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/app/src/components/titlebar.tsx` around lines 26 - 33, Extract the
hard-coded "768px" into a shared constant and use it where the media query is
created so the breakpoint stays in sync with SessionSidePanel; specifically, add
a DESKTOP_BREAKPOINT (or similar) in a shared layout/utilities module and
replace the literal in the createMediaQuery call that sets isDesktop in
titlebar.tsx (and update SessionSidePanel to import and use the same
DESKTOP_BREAKPOINT) so both components reference the single source of truth.
🤖 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/e2e/session/titlebar-right-rail-contract.spec.ts`:
- Around line 76-89: The assertion reads the `#pawwork-titlebar-tabs` geometry
immediately after page.setViewportSize(...) which is flaky; replace the
immediate page.evaluate call with a wait-for-convergence that polls the element
until its computed geometry stabilizes (e.g., use page.waitForFunction to
repeatedly read document.getElementById("pawwork-titlebar-tabs") and its
getBoundingClientRect().width and getComputedStyle(...).borderLeftWidth until
width === 0 and borderLeft === "0px" or a reasonable timeout elapses). Update
the test in titlebar-right-rail-contract.spec.ts where page.setViewportSize and
the page.evaluate occur to use this wait/poll strategy so the expect checks run
only after layout has settled.

---

Nitpick comments:
In `@packages/app/src/components/titlebar.tsx`:
- Around line 26-33: Extract the hard-coded "768px" into a shared constant and
use it where the media query is created so the breakpoint stays in sync with
SessionSidePanel; specifically, add a DESKTOP_BREAKPOINT (or similar) in a
shared layout/utilities module and replace the literal in the createMediaQuery
call that sets isDesktop in titlebar.tsx (and update SessionSidePanel to import
and use the same DESKTOP_BREAKPOINT) so both components reference the single
source of truth.
🪄 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: 468858a7-f309-43a9-aabc-c344755cea7e

📥 Commits

Reviewing files that changed from the base of the PR and between 3902f44 and cd73d82.

📒 Files selected for processing (3)
  • packages/app/e2e/session/session-tab-chip-contract.spec.ts
  • packages/app/e2e/session/titlebar-right-rail-contract.spec.ts
  • packages/app/src/components/titlebar.tsx

Comment thread packages/app/e2e/session/titlebar-right-rail-contract.spec.ts Outdated
…spec

PR #880 review followup: reading `#pawwork-titlebar-tabs` geometry
immediately after `page.setViewportSize(...)` raced the
viewport → media query → Solid memo → DOM update chain. Replace the
single `page.evaluate` with `expect.poll` so the assertion only runs
after layout settles.

Skipped review nit: extracting `768px` to a shared `DESKTOP_BREAKPOINT`
constant was already flagged as a follow-up scope item in the previous
commit (alongside lifting the entire desktop/route/state predicate to
shared layout state). CodeRabbit itself marked it 💤 Low value.

Refs PR #880.
@Astro-Han Astro-Han merged commit 87cc225 into dev May 24, 2026
27 checks passed
@Astro-Han Astro-Han deleted the claude/right-panel-followup-regression branch May 24, 2026 16:22
Astro-Han added a commit that referenced this pull request May 24, 2026
Bump the desktop Electron package version to v2026.5.25 for the release train.

Included release-readiness checks:
- Local app and desktop Electron typechecks passed.
- Focused UI component tests for grouped tool-call summaries passed.
- PR CI passed, including app/ui/opencode/desktop units, typecheck, CodeQL, desktop smoke, and e2e-artifacts.
- Computer Use manual E2E covered new session creation, an actual execute-command tool call, right-panel tabs and collapse, #880 sidebar/right-panel toggle affordances, and pinned session drag reordering.

This release includes the current dev branch work through #880 and the later fixes #881 and #883.
Astro-Han added a commit that referenced this pull request May 24, 2026
Two follow-ups to the macOS seam fix earlier in this PR, both surfaced
once the alignment seam was visibly continuous and motion against it
became readable:

## 1. Stable toggle position (replaces #880's slide)

PR #880 made the right utility toggle an in-flow flex sibling of the
titlebar tabs slot, so opening the right panel pushed the toggle left
by `--right-panel-width` over the 240ms transition. The slide read as
visually jarring once the seam was aligned and the motion became
visible against the now-continuous border-l.

Fix: `#pawwork-titlebar-right` is now absolute-positioned to the
rail's top-right corner (`right-2` on macOS, `right-0` on Windows),
so its x-coordinate stays at `viewport.right - 8px` regardless of
panel state. The tabs slot is the only flex sibling and pins to the
rail's right edge as before; the toggle visually overlaps the tabs
slot's rightmost area but in practice doesn't collide — Kobalte's
`Tabs.List` sizes to content (~150px for 1-4 chips), leaving the
trailing `+` button in the middle of the panel column with empty
space at the corner for the toggle to float in.

Regression test `right utility toggle keeps the same x-position across
open/close` in `titlebar-right-rail-contract.spec.ts`: asserts
`|toggle.right(open) - toggle.right(closed)| < 12px`. Pre-fix this
came back `Received: 229.25` (the full panel-width displacement);
post-fix it's within sub-pixel rounding.

## 2. Drag-resize seam sync

While dragging the panel resize handle, the body's `<aside>` inline
`width` snapped to each pointermove (its transition is gated on the
session-scope `props.size.active()`), but `--right-panel-width` on
the desktop shell kept its 240ms cubic-bezier transition because the
shell's gate uses `state.sizing` — a layout.tsx-local flag scoped to
the sidebar resize handler only. The two sides of the seam moved at
different speeds; one led, one trailed, visibly drifting apart.

Fix: a small effect in `SessionSidePanel` mirrors
`props.size.active()` to a `data-resizing-right-panel` attribute on
`<desktop-shell>`. A CSS rule in `index.css` matches the attribute
and disables the shell's transition while the user is dragging. This
avoids plumbing session-scope sizing state into the global layout
context — the bridge stays one attribute wide and one CSS rule
shallow. The 120ms timeout in `createSizing` re-enables the
transition shortly after the user lets go, so the open/close
animation still uses the full 240ms cubic-bezier curve.

## Verified

- `bun run test:e2e:local titlebar-right-rail-contract.spec.ts`
  — 5/5 pass (incl. the new x-position-stability assertion)
- `bun run snap right-panel-titlebar` — passes; grid PNG shows
  toggle at corner in both states, seam continuous
- `bun run lint` — clean
- `bun --cwd packages/app run typecheck` — clean
- Manual Electron verification (mac): toggle stable across open/close,
  drag-resize seam moves as one continuous line.
Astro-Han added a commit that referenced this pull request May 25, 2026
…resize sync (#887)

Three connected fixes for the right-panel titlebar seam, reported post-release as a follow-up to #880. All three only become visible together: once the seam aligns, the toggle slide and the drag-resize lag become readable.

**Fix 1 — macOS seam alignment (8px)**
`padding-inline: 8px` → `padding-inline-start: 8px` on the macOS `[data-component="titlebar-shell"]` rule (`packages/app/src/index.css`). The symmetric 8px padding pushed the right rail 8px inboard of the viewport while the `<aside>` directly below reached the actual edge — `border-l`s ended up 8px apart on macOS only. Windows already correct.

**Fix 2 — Stable toggle position across open/close**
`#pawwork-titlebar-right` (`packages/app/src/components/titlebar.tsx`) changed from in-flow flex sibling of the tabs slot to absolute-positioned at the rail's top-right corner (`right-2` macOS / `right-0` Windows). PR #880's flex layout slid the toggle by `--right-panel-width` on a 240ms transition; that motion read as jarring once fix #1 aligned the seam.

**Fix 3 — Drag-resize seam sync**
New effect in `SessionSidePanel` mirrors `props.size.active()` to a `data-resizing-right-panel` attribute on `<desktop-shell>`; matching CSS rule in `index.css` disables the shell's transition while the attribute is present. During drag the body's inline `width` snapped per-pointermove while the shell's `--right-panel-width` kept its 240ms transition — the two sides of the seam moved at different speeds. Attribute-bridge avoids plumbing session-scope sizing state into the global layout context.

**Hardening from code review**
- P1 (toggle/+ collision risk): added `padding-right: 44px` reserve on `#pawwork-titlebar-tabs` when the rail is active (toggle 30 + `right-2` 8 + gap 4 + 2px buffer for Tabs.List `px-1`) so the no-collision contract no longer depends on Kobalte's Tabs.List rendering at content-width.
- P2 (attribute leak): effect now unconditionally `removeAttribute` first then re-evaluates, plus `onCleanup` clear. Closes leaks via `isDesktop()` flip, component unmount, and stale residue.
- P3 (test quality): tightened tolerance to 1px, replaced `waitForTimeout` with `expect.poll`, added `+` click test and a collision-guard that runtime-injects `width: 100% !important` on Tabs.List.
- Bot-review follow-up: x-axis seam test now polls for steady state (panel open/close runs a 240ms transition that the drag-only gate doesn't suppress); toggle test normalizes `aria-expanded` to false before sampling closed state.

**Regression tests** (`packages/app/e2e/session/titlebar-right-rail-contract.spec.ts`, 8 total)
- `border-l aligns horizontally with right-panel body` — pre-fix #1: `Received: 8`; post: passes
- `toggle keeps the same x-position across open/close` — pre-fix #2: `Received: 229.25`; post: <1px
- `+ add-tab button stays clickable and does not close the panel`
- `+ stays clickable even if Tabs.List is forced to fill the slot (collision guard)`
- `data-resizing-right-panel attribute does not leak when viewport drops below the desktop breakpoint mid-resize state`
- Plus 3 pre-existing tabs-rail contract tests

**Verification**
- `bun run test:e2e:local titlebar-right-rail-contract.spec.ts` → 8/8 pass
- `bun run snap right-panel-titlebar` → pass, no visual regression
- `bun run lint` → clean
- `bun --cwd packages/app run typecheck` → clean
- Manual Electron (macOS): toggle stable across open/close, drag-resize seam moves as one continuous line
- CI: 27/27 green

**Follow-up tracked separately**
- #889 — `session-side-panel.tsx` is 604 lines and growing; refactor scheduled in a dedicated PR.

**Risk notes**
- Fix #1 macOS-only; fixes #2 and #3 affect both platforms.
- Closed-state toggle inset changes from 16px to 8px on macOS, matching native toolbar conventions.
- Open-state 240ms toggle slide is removed; body width still animates as before.
- No `.github/workflows/` touched.
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 bug Something isn't working P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants