Skip to content

feat(app): pin workspace tabs - #4134

Closed
aliozkanozdurmus wants to merge 6 commits into
getpaseo:mainfrom
aliozkanozdurmus:feat/pinned-tabs
Closed

aliozkanozdurmus wants to merge 6 commits into
getpaseo:mainfrom
aliozkanozdurmus:feat/pinned-tabs

Conversation

@aliozkanozdurmus

Copy link
Copy Markdown

The problem

Workspace tabs accumulate over long sessions: a couple of anchors (the main agent session, a long-running terminal, a reference file) mixed into short-lived scratch tabs. Every bulk-close gesture — "Close other tabs", "Close to the left/right" — treats all tabs the same, so keeping anchors around means closing scratch tabs one by one, or reopening anchors after a bulk close. There is no way to say "this tab matters, keep it".

What changes

Users can pin any persistent workspace tab:

  • Pin tab / Unpin tab in the tab context menu (pin icon, next to Rename), for every persistent tab kind — agent, terminal, file, browser. Ephemeral targets that never persist (New Tab, commit diff) don't offer the action.
  • Pinned tabs show a pin affordance in the tab row (desktop row, compact rows, Explorer tab rail). The icon slot is always mounted, so toggling pin causes no layout shift.
  • Bulk closes skip pinned tabs: "Close other tabs", "Close to the left", "Close to the right" — from the tab menu, keyboard, and Command Center. An explicit "Close" on a pinned tab itself still closes it.
  • Close Pane is pin-safe (Cmd+Shift+W / Command Center): pinned tabs are relocated to a surviving pane. Relocation is preflighted atomically — if a move isn't possible, the pane close is blocked instead of tearing pinned tabs down.
  • Pin state persists across restarts with the workspace layout. isPinned is optional, so previously persisted layouts load unchanged.

This is separate from the existing internal pinnedAgentIds visibility-reconciliation mechanism, which is untouched.

Tests

Unit/integration (vitest, run from packages/app):

  • workspace-tab-menu.test.ts — Pin/Unpin item per tab kind, label switching, toggle dispatch, no pin for ephemeral kinds
  • workspace-tab-model.test.ts, workspace-tabs/model.test.ts — pinned state on the tab model and persistence eligibility
  • workspace-bulk-close.test.ts + workspace-tab-bulk-close-actions.test.ts — close before/after/others skip pinned, driven through the real store + menu handlers, not the selector in isolation
  • workspace-pane-close.test.ts — relocation, final-pane block, and failure-path atomicity (unpinned tabs present; zero mutations when relocation fails)
  • workspace-tab-presentation.test.tsx — rendered pin slot, stable width/height/flexShrink across pin toggles
  • workspace-layout-store.test.ts — persistence, tab replacement, reconciliation

Browser e2e: e2e/browser/pinned-tabs.spec.ts — a real Playwright run (isolated daemon + Metro harness) that creates three terminal tabs through the real UI, pins via the real context menu, and verifies "Close other tabs" from an unpinned anchor keeps the pinned tab, then that unpinning restores normal close behavior. Mutation-checked: with the pinned filter removed, the spec fails; restored, it passes.

QA evidence

Commands run on this branch:

$ cd packages/app && npx vitest run src/screens/workspace/workspace-tab-menu.test.ts \
    src/screens/workspace/workspace-tab-model.test.ts src/screens/workspace/workspace-bulk-close.test.ts \
    src/screens/workspace/workspace-pane-close.test.ts src/screens/workspace/workspace-tab-bulk-close-actions.test.ts \
    src/screens/workspace/workspace-tab-presentation.test.tsx src/workspace-tabs/model.test.ts \
    src/stores/workspace-layout-store.test.ts src/screens/workspace/workspace-tab-trailing-accessory.test.tsx

 Test Files  9 passed (9)
      Tests  181 passed (181)
$ cd packages/app && E2E_WORKERS=1 npx playwright test --project=browser e2e/browser/pinned-tabs.spec.ts

  ✓  1 [browser] › e2e/browser/pinned-tabs.spec.ts › pins a terminal tab and protects it from Close other tabs (7.5s)
  1 passed (22.4s)

npm run typecheck, targeted npm run lint -- <changed files> (0 warnings/errors), and npm run format:check all pass.

Screenshots captured from the e2e run:

Pin in the context menu Pinned tab in the row After "Close other tabs"
Pin tab menu item Pinned tab row Pinned tab survives close others

Video of the full interaction: pinned-tabs-e2e.webm

Platforms

Platform Tested Notes
iOS No Menu items come from the shared menu model that feeds the mobile sheet; compact rows render the pin via the shared accessory (unit-covered). No device run.
Android No Same as iOS.
Web Yes Chromium e2e spec + vitest suite above; screenshots/video from that run.
Desktop macOS No Same web code path; not run in the Electron shell.
Desktop Windows No Same as macOS.
Desktop Linux No Same as macOS.

Daemon and protocol are untouched — this is an app-side change only.

Notes

I know feature PRs here normally start with a product discussion; this one is framed workflow-first per the contributing guide, and I'm happy to open a Discussion or reshape any part of it (icon, placement, close semantics) to fit the direction you want. Maintainer edits are enabled.

@boudra

boudra commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Closing this feature PR. Please discuss the workflow and shared need in Discussions, following CONTRIBUTING.md.

@boudra boudra closed this Sep 8, 2026
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.

2 participants