Skip to content

feat(app): group session tabs by project - #57896

Closed
liambreen wants to merge 2 commits into
anomalyco:devfrom
liambreen:tab-project-groups
Closed

liambreen wants to merge 2 commits into
anomalyco:devfrom
liambreen:tab-project-groups

Conversation

@liambreen

@liambreen liambreen commented Oct 21, 2026 •

Copy link
Copy Markdown

Issue for this PR

Refs #55446, refs #51759. Split out of #57888 per review feedback, together with #57895 (the Home list half of the same feedback). Stacked on #57894 — its one packages/ui commit shows in this diff until that merges first.

Type of change

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

What does this PR do?

Adds a Settings → General → Group tabs by project preference (default Off) that clusters session tabs under per-project headers in both horizontal and vertical layouts. All three strip mounts (titlebar, vertical sidebar, mobile drawer) share the implementation.

  • Horizontal: groups render as one-character badges in a stable project color (the project's own icon color when set, else a hash onto the non-gray avatar palette); the full name appears on hover; member tabs carry the same color as a thin accent strip.
  • Vertical: compact 20px headers, grouped rows drop the per-row project icon, and a toggle next to New session on the panel flips the same setting.
  • Membership is derived, not managed: each tab resolves through the project inventory (loaded session → persisted tabs.info fallback; drafts use their directory), so worktree and sandbox tabs join their parent project, and group ids include the server so the same path on two servers stays separate. Unresolved tabs trail the groups unheadered.
  • Drag and drop keeps working: dragging a header reorders its whole project (collapsed members follow); dragging a tab reorders it within its group; cross-group drops snap back, since a tab cannot change projects by dragging. The grouped view is a stable partition of the persisted tab order — turning grouping off restores the user's own arrangement exactly. Collapsed members leave the ctrl+tab / alt+arrows / mod+N cycle.
  • Group colors reuse the avatar tokens via a data-project-variant attribute in project-avatar.css; the hover tooltip uses the triggerAs span trigger from feat(ui): allow span tooltip triggers inside buttons #57894.
  • Drag semantics live in a new leaf module, shell/titlebar/tab-groups.ts, unit-tested without the component graph.

How did you verify your code works?

  • bun typecheck in packages/app and bun run check at the repo root, both green.
  • Unit: 12 cases in tab-groups.test.ts (partition order, per-server identity, variant stability, header drags carrying collapsed members, within-group drags, cross-group and boundary rejection) plus the settings model recovery case.
  • E2E: new keeper cases in tabs.spec.ts (clustering with a sandbox session unified, collapse, cycle-skip, reload persistence, badge/tooltip/accent assertions, and a pointer-driven drag case covering within-group reorder, group swap by header drag, and cross-group snap-back — in both layouts) and a preference row case in settings.spec.ts. 61 keepers pass locally; the only failures are five keybind-driven ones that fail identically on dev on this machine (mac chromium environment).

Screenshots / recordings

Horizontal — one-char badges, color-matched accent strips:

Horizontal tab groups

Horizontal badge with the project name on hover:

Badge name on hover

Vertical — compact headers, icon-free rows, panel toggle:

Vertical tab groups

Vertical with one group collapsed:

Vertical collapsed

Checklist

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

Added by the opencode bot (AI-generated).

Fixes #44589

Related: #51759, #55446

Tooltip's Kobalte trigger renders as a div, which the HTML content
model forbids inside buttons. triggerAs lets a call site request a span
trigger; upcoming tab-group badges use it to show the project name on
hover from inside their header buttons.
An opt-in Settings > General preference clusters session tabs under
per-project headers in both horizontal and vertical layouts. Horizontal
groups badge with a stable project color and members carry it as an
accent strip; vertical groups stay compact and drop the per-row project
icon, with a panel toggle sharing the setting. Membership derives from
each tab's session directory, so worktree and sandbox tabs join their
project. Dragging a header reorders the whole project, dragging a tab
reorders it within its group, and cross-group drops snap back; the
grouped view is a stable partition of the persisted order. Collapsed
members leave the tab cycle, and collapse state persists per window.

@opencode-agent opencode-agent 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.

Grouping works as described. In the e2e harness (Chromium) the PR's new tabs.spec.ts and settings.spec.ts grouping cases pass in both layouts. The unit tests and typecheck for packages/app and packages/ui are clean.

Public API: unchanged. The only new keys are appearance.tabGroups in the app's local settings and a window-scoped tabs.groups persisted store.

Two behaviour gaps with collapsed groups are worth fixing before this lands (see the inline comments):

  • Collapsing the group that holds the current tab, or opening a session whose group is already collapsed, hides the active tab, and nothing shows where it went. I reproduced this: the URL stays on the session, but no tab or header is marked current (screenshots below).
  • Collapsed members are unmounted. Their background refresh (permissions, forms, pending work) and unread/busy tracking stop, and the header doesn't show any of that state, so a collapsed project can need attention without the user seeing it.

Fixes #44589
Related: #51759, #55446

After

After


if (!result) return props.tabs
return [
...result.groups.flatMap((group) => (isGroupCollapsed(group.id) ? [] : group.tabs)),

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.

When the current tab belongs to a collapsed group, it's dropped from the strip and from visibleTabs. Nothing then marks where the user is, and the keyboard cycle starts from a tab that isn't in the list. This happens when you collapse the active tab's group, and also when you open a session (from Home, a new session, a link) whose group is already collapsed. Could the current tab stay visible inside a collapsed group (as browsers do), or could navigating to it expand the group? At minimum, the header should show that it holds the active tab.

vertical={vertical()}
onToggle={() => toggleGroupCollapsed(group().id)}
/>
<Show when={!isGroupCollapsed(group().id)}>

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.

Collapsing unmounts each member's SessionTabEntry. That entry is what runs the background refresh of permissions, forms and pending work for inactive tabs, and what reports unread (which the unread-tab cycle uses). So a collapsed project stops being refreshed, and its unread/busy state is frozen. The header doesn't summarise the members' state either. Could you keep the entries' state tracking alive while hidden, and show an aggregate busy/unread indicator on the collapsed header?

@liambreen liambreen closed this Oct 21, 2026
@liambreen
liambreen deleted the tab-project-groups branch October 21, 2026 20:24
@TylonHH

TylonHH commented Oct 24, 2026

Copy link
Copy Markdown

Hey @liambreen, I saw your PR got closed inadvertently — the opencode-agent review was positive and the implementation looks really solid. The two UX issues flagged (collapsed-group-with-active-tab visibility, and collapsed-group unmount vs hide) seem fixable.

This feature would make a huge difference for those of us coming from Codex who work across multiple projects daily. Any chance you'd consider re-opening this (or #57895 for Home grouping)? Happy to help test if that moves things forward.

@liambreen

liambreen commented Oct 24, 2026 via email

Copy link
Copy Markdown
Author

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Automatically group desktop session tabs by project

2 participants