Skip to content

fix(app): keep overflowing session tabs usable - #61397

Open
yushui2022 wants to merge 1 commit into
anomalyco:devfrom
yushui2022:tab-strip-overflow
Open

yushui2022 wants to merge 1 commit into
anomalyco:devfrom
yushui2022:tab-strip-overflow

Conversation

@yushui2022

@yushui2022 yushui2022 commented Nov 3, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #61396

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

When the top horizontal session tab strip overflowed, every tab was allowed to shrink to 28px (min-w-7), so the strip became a row of icon slivers with no reliable click-to-switch target. This keeps horizontal tabs at a usable minimum width (min-w-20, 80px) and lets the strip scroll horizontally instead. A vertical wheel gesture now scrolls the strip horizontally, and when a shortcut or number key selects a tab the strip scrolls that tab into view.

scrollIntoView also uses block: "nearest", inline: "nearest" so selecting a tab no longer scrolls unrelated ancestors.

How did you verify your code works?

Added an e2e regression in packages/app/e2e/regression/tabs.spec.ts covering: 14 tabs keep an 80px minimum width and the strip overflows, a vertical wheel scrolls it, clicking an off-screen tab switches to it, and Cmd/Ctrl+9 reveals the tab it selects. The test passes locally.

bun run typecheck, Prettier and oxlint pass for the touched files. The rest of tabs.spec.ts passes except five tests that also fail on unmodified dev on this macOS machine (they hardcode Control+Alt while mod is Command, and one asserts Linux keybind labels), so they are unrelated to this change.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" role="img" aria-label="Overflowing session tabs scroll horizontally">
  <defs>
    <clipPath id="tab-strip">
      <rect x="8" y="10" width="224" height="52" rx="8"/>
    </clipPath>
  </defs>
  <g clip-path="url(#tab-strip)">
    <g>
      <animateTransform attributeName="transform" type="translate" values="0 0;0 0;-56 0;-56 0;0 0" keyTimes="0;0.12;0.5;0.8;1" dur="5s" repeatCount="indefinite"/>
      <g opacity="0.16">
        <rect x="8" y="18" width="48" height="36" rx="7" fill="currentColor"/>
      </g>
      <circle cx="22" cy="36" r="5" fill="currentColor" opacity="0.55"/>
      <rect x="32" y="32" width="16" height="3" rx="1.5" fill="currentColor" opacity="0.55"/>
      <rect x="32" y="38" width="10" height="3" rx="1.5" fill="currentColor" opacity="0.3"/>
      <rect x="62" y="18" width="48" height="36" rx="7" stroke="currentColor" opacity="0.28"/>
      <circle cx="76" cy="36" r="5" fill="currentColor" opacity="0.35"/>
      <rect x="86" y="32" width="16" height="3" rx="1.5" fill="currentColor" opacity="0.35"/>
      <rect x="86" y="38" width="10" height="3" rx="1.5" fill="currentColor" opacity="0.2"/>
      <rect x="116" y="18" width="48" height="36" rx="7" stroke="currentColor" opacity="0.28"/>
      <circle cx="130" cy="36" r="5" fill="currentColor" opacity="0.35"/>
      <rect x="140" y="32" width="16" height="3" rx="1.5" fill="currentColor" opacity="0.35"/>
      <rect x="140" y="38" width="10" height="3" rx="1.5" fill="currentColor" opacity="0.2"/>
      <rect x="170" y="18" width="48" height="36" rx="7" stroke="currentColor" opacity="0.28"/>
      <circle cx="184" cy="36" r="5" fill="currentColor" opacity="0.35"/>
      <rect x="194" y="32" width="16" height="3" rx="1.5" fill="currentColor" opacity="0.35"/>
      <rect x="194" y="38" width="10" height="3" rx="1.5" fill="currentColor" opacity="0.2"/>
      <rect x="224" y="18" width="48" height="36" rx="7" stroke="currentColor" opacity="0.28"/>
      <circle cx="238" cy="36" r="5" fill="currentColor" opacity="0.35"/>
      <rect x="248" y="32" width="16" height="3" rx="1.5" fill="currentColor" opacity="0.35"/>
    </g>
  </g>
  <rect x="8" y="10" width="12" height="52" fill="currentColor" opacity="0.06"/>
  <rect x="220" y="10" width="12" height="52" fill="currentColor" opacity="0.06"/>
  <g opacity="0.55">
    <path d="M228 31l4 5-4 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
    <animate attributeName="opacity" values="0.25;0.75;0.25" dur="2s" repeatCount="indefinite"/>
  </g>
  <g opacity="0.5">
    <circle cx="204" cy="60" r="2.5" fill="currentColor"/>
    <circle cx="212" cy="60" r="2.5" fill="currentColor" opacity="0.6"/>
    <circle cx="220" cy="60" r="2.5" fill="currentColor" opacity="0.3"/>
    <animate attributeName="opacity" values="0.6;0.25;0.6" dur="2s" repeatCount="indefinite"/>
  </g>
</svg>

Screenshots / recordings

No screenshot: the visible change is that tabs stop shrinking below 80px and the strip scrolls when it overflows.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Fixes #50196

Related: #53412

Horizontal session tabs could shrink to 28px icon chips once the strip overflowed, leaving no reliable way to click a tab to switch. Keep tabs at a usable minimum width, scroll the strip horizontally with the wheel, and reveal the selected tab when a shortcut changes it.

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

The fix works. I ran tabs.spec.ts in headless Chromium. On the base, the new overflow test fails: with 14 sessions in an 800px window, every tab shrinks to an avatar with no title. With this change, tabs keep an 80px minimum with their titles showing, the strip scrolls, and all 45 tests in the file pass. typecheck:tsc for packages/app passes.

The public API is unchanged; this only affects the app's UI.

One non-blocking point: the deleted test icon-only inactive tabs retain their avatar and attention badge… also checked that an inactive tab shows the unread/attention dot and clears it on selection. Avatar-only horizontal tabs can't happen any more, so dropping that test makes sense. But no test now covers the dot on inactive horizontal tabs, so a short assertion for it in the new test or elsewhere would be worth keeping.

Fixes #50196
Related: #53412

Before

After

@yushui2022

Copy link
Copy Markdown
Author

Thanks for verifying the change and for the typecheck run.

On the dot coverage: the existing inactive session attention dots share the close control without marking the avatar or active tab case in tabs.spec.ts already covers this for horizontal tabs. The app's default tabLayout is horizontal, and that test asserts the attention dot is visible on an inactive tab, that tab-avatar-unread-dot stays hidden, and that the dot clears once the tab is selected.

The part that is no longer reachable is only the avatar-corner tab-avatar-unread-dot at <= 44px; horizontal tabs now keep an 80px minimum, so that compact state can't occur and dropping that assertion is safe.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant