fix(app): restore titlebar toggle visual contract after PR #878#880
Conversation
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'.
|
Warning Review limit reached
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 We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRefactors 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. ChangesSidepanel Portal Styling and Tests
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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.
Perf delta summaryComparator: 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.
This reverts commit 412d706.
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.
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).
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/app/e2e/session/session-tab-chip-contract.spec.ts (1)
32-33: ⚡ Quick winUse
modKeyhelper for shortcut chords on Line 32 and Line 33.Please replace the hardcoded
ControlOrMeta+...strings with the sharedmodKeyutility 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 winTrack the created session inside
beforeGotoimmediately after creation.This avoids a cleanup gap if
project.open()fails aftersdk.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
📒 Files selected for processing (4)
packages/app/e2e/session/session-tab-chip-contract.spec.tspackages/app/e2e/snap/right-panel-tabs-hover.snap.tspackages/app/src/index.csspackages/ui/src/components/tabs.css
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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/app/src/pages/session/session-side-panel.tsx (1)
360-365: ⚡ Quick winCorrect the documented gap token/value in this comment.
Line 361 currently documents
var(--space-sm)(8px), but sidepanel tabs gap is nowvar(--space-xs)(4px) inpackages/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 winUpdate token reference in the inline comment (
radius-lg→radius-md).The comment at Line 95 references
--radius-lg, but the sidepanel chip radius is now driven by--radius-mdinpackages/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 winReplace 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
📒 Files selected for processing (7)
packages/app/e2e/session/session-tab-chip-contract.spec.tspackages/app/e2e/snap/right-panel-tabs-hover.snap.tspackages/app/src/components/session/session-sortable-shell-tab.tsxpackages/app/src/index.csspackages/app/src/pages/session/session-side-panel.tsxpackages/ui/src/components/icon-button.csspackages/ui/src/components/tabs.css
💤 Files with no reviewable changes (1)
- packages/ui/src/components/icon-button.css
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.
There was a problem hiding this comment.
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 winAvoid 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
📒 Files selected for processing (7)
packages/app/e2e/session/session-tab-chip-contract.spec.tspackages/app/e2e/snap/right-panel-tabs-hover.snap.tspackages/app/src/components/session/session-sortable-shell-tab.tsxpackages/app/src/components/titlebar.tsxpackages/app/src/index.csspackages/app/src/pages/session/session-side-panel.tsxpackages/ui/src/components/tabs.css
…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.
…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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/app/src/components/titlebar.tsx (1)
26-33: 💤 Low valueConsider extracting the breakpoint to a shared constant.
The
768pxbreakpoint must stay in sync withSessionSidePanel's desktop gate. The comment documents this well, but a shared constant (e.g.,DESKTOP_BREAKPOINTin 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
📒 Files selected for processing (3)
packages/app/e2e/session/session-tab-chip-contract.spec.tspackages/app/e2e/session/titlebar-right-rail-contract.spec.tspackages/app/src/components/titlebar.tsx
…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.
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.
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.
…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.
Summary
Fix two visual regressions surfaced by #878 (right-panel tab strip portalled into the titlebar):
aria-expandedselected-state chip.Why
PR #878 added
#pawwork-titlebar-tabs, an absolute-positioned slot anchored to the viewport right edge withwidth: var(--right-panel-width)andz-10. To make the existing sidepanel CSS inpackages/ui/src/components/tabs.cssmatch the portalled<Tabs.List>, the slot stampsdata-component="tabs"+data-variant="sidepanel". That borrowed identity also inherits the base[data-component="tabs"]rulebackground-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'spointer-events-nonefix restored clickability, but not visibility.The sidebar-toggle chip regression is older:
.titlebar-icon[aria-expanded="true"]inicon-button.csswas 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: resetbackground-color: transparenton 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-iconrules. 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 whypointer-events-nonewas needed; expanded to document both the click-occlusion and visual-occlusion failure modes.Scope blast
.titlebar-iconhas 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 thearia-expandedchip. 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.cssselected-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
PendingReview Focus
tabs.csssidepanel host bg reset — confirm no consumer relied on the opaque host bg. The real<Tabs variant="sidepanel">root insession-side-panel.tsxsits insideright-panel-bodywhich isbg-bg-base; transparent on top of bg-base is visually identical.icon-button.cssselector broadening —.titlebar-iconis 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-iconshape.rightPanelTabList(page)inpackages/app/e2e/actions.ts:344still works — the slot keepsdata-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
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
bugapp,uiP2Pending,Approved by @<reviewer>, orNot required: <reason>(default isPending; "not required" is restricted to bot-authored low-risk PRs).dev, and my PR title and commit messages use Conventional Commits in English.Summary by CodeRabbit
New Features
Style
Accessibility
Tests
Documentation