Files
multica/apps/desktop
Jiayuan Zhang 41753d17a2 feat(desktop): pin tab (MUL-2449) (#2914)
* feat(desktop): pin tab — keep parked tabs anchored across navigations (MUL-2449)

Adds tab pinning to the desktop tab bar. Pinned tabs render as icon-only at
the left, suppress the X close button, and intercept any `navigation.push()`
that would change their pathname — those are redirected into a new tab so
the pinned tab stays parked on its original route. Search/hash/back/forward
stay in-tab so pinned filter and drawer state still work.

Implements the FINAL combo from the MUL-2449 RFC §4: right-click menu +
⌘⇧P shortcut (D1 a+c), icon-only visual (D1v i), pathname-change → new tab
with same-path-allowed (D2a/b A), back / refresh allowed (D2c/d A), pinned
auto-cluster left and persist (D3a/b A), pinned can't be X-closed (D3c A),
dedupe respected (D4a A), default Issues tab pinnable (D4b A), drag clamped
to its zone (D4c A), deep link prefers pinned (D4e A).

Store changes:
  - Tab.pinned added; togglePin maintains the "pinned first" invariant by
    inserting at the zone boundary.
  - moveTab clamps cross-zone drags so dnd-kit can't violate the ordering.
  - Persistence bumped v2 → v3 with a defaulting migration (pinned=false).
    Rehydrate sorts pinned-first as a defensive net.

Navigation:
  - tryRouteToPinnedNewTab compares the active tab router's live pathname
    to the target. Same-pathname push (query / hash / sub-router) falls
    through to the router; different pathname → openTab + setActiveTab
    (foreground; respects dedupe).

UI:
  - Tab bar wraps each tab in a shadcn ContextMenu with Pin/Unpin + Close
    (Close disabled for pinned or last-remaining tab).
  - Pinned tabs use a narrower icon-only layout with an accent left border
    and a divider between the pinned and unpinned groups.
  - Global keydown listener registers ⌘⇧P / Ctrl+Shift+P to toggle pin on
    the active tab.

Tests: - tab-store: togglePin ordering, moveTab boundary clamping, v2→v3
    migration.
  - navigation: pinned push → new foreground tab; same-pathname push stays
    in tab; cross-workspace still wins over pin.
Co-authored-by: multica-agent <github@multica.ai>

* test(desktop): cover TabNavigationProvider.push pin interception (MUL-2449)

Add pathname-diff / same-pathname cases for the per-tab navigation
adapter. Existing tests only exercised the root-level
DesktopNavigationProvider, but in-tab AppLink / page clicks flow
through TabNavigationProvider — so a future refactor that drops the
pin check from that provider would silently regress.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(desktop): pin tab — hover button, full title, drop ⌘⇧P (MUL-2449)

Jiayuan's interactive review of PR #2914 surfaced three changes to the
RFC's D1 (entry / visual) decisions:

  1. Drop the ⌘⇧P global shortcut — it added a keybinding for a
     low-frequency action and crowded the shortcut namespace.
  2. Reveal a Pin / Unpin button on tab hover instead of relying on the
     right-click menu as the primary entry; right-click remains as a
     fallback (and for Close).
  3. Pinned tabs keep their full title and width. The only weak visual
     differences vs. unpinned tabs are the accent left border and the
     suppressed X close button.

Removes the global keydown listener (no other doc / handler referenced
it). Adds a hover-only Pin / Unpin span next to the existing close
affordance, both gated by group-hover. Drops the icon-only width /
hidden-title styling for pinned tabs.

Tests: new tab-bar.test.tsx covers Pin / Unpin button rendering, click
handlers (togglePin), the hidden-X invariant on pinned tabs, and the
full-title rendering. 146 passed, typecheck clean.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(desktop): pin tab — drop accent left border, swap leading icon to Pin (MUL-2449)

Jiayuan reported that the accent left border on pinned tabs reads as a
heavy black edge in light mode and looks unrefined. Replace it with a
quieter identifier: pinned tabs swap their route icon for a Pin glyph
in the leading slot (same size, no extra horizontal space). The hidden
X close button stays as the secondary cue. RFC §3 D1v moves from
iii FINAL to iv FINAL; iii is demoted to v2 FINAL → v3 REMOVED.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-20 09:14:43 +02:00
..