feat(header): session tabs — a horizontal working set of open sessions
Web/desktop header replaces the single session title with a strip of soft pill tabs, one per session the user has opened (sidebar, palette or deep link — opening anywhere adds a tab once). The active tab is the familiar title block — rename, meta row and the full session menu — inside a gently selected pill; a brand-new draft shows as a transient pill until its session exists. Inactive tabs show the title with a hover-revealed "..." menu (close tab, close other tabs, copy id) that nudges the text like sidebar rows, and close by middle-click too. Tabs drag to reorder, scroll behind the right-side header buttons with soft fade edges, respect the reserved window-controls inset, and persist across reloads. Closing the active tab activates its neighbour (or opens a new draft when it was the last). Tab ids whose session is not in the loaded list stay stored but hidden, so a partial session list never destroys the working set. VS Code keeps the plain title; mobile is untouched.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { beforeEach, describe, expect, test } from 'bun:test';
|
||||
|
||||
import { useSessionTabsStore } from './useSessionTabsStore';
|
||||
|
||||
describe('useSessionTabsStore', () => {
|
||||
beforeEach(() => {
|
||||
useSessionTabsStore.setState({ tabIds: [] });
|
||||
});
|
||||
|
||||
test('ensureTab appends once and preserves order', () => {
|
||||
const store = useSessionTabsStore.getState();
|
||||
store.ensureTab('a');
|
||||
store.ensureTab('b');
|
||||
store.ensureTab('a');
|
||||
expect(useSessionTabsStore.getState().tabIds).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
test('closeTab removes only the given id; closeOtherTabs keeps only it', () => {
|
||||
useSessionTabsStore.setState({ tabIds: ['a', 'b', 'c'] });
|
||||
useSessionTabsStore.getState().closeTab('b');
|
||||
expect(useSessionTabsStore.getState().tabIds).toEqual(['a', 'c']);
|
||||
useSessionTabsStore.getState().closeOtherTabs('c');
|
||||
expect(useSessionTabsStore.getState().tabIds).toEqual(['c']);
|
||||
});
|
||||
|
||||
test('reorderTabs moves by id and ignores unknown ids', () => {
|
||||
useSessionTabsStore.setState({ tabIds: ['a', 'b', 'c'] });
|
||||
useSessionTabsStore.getState().reorderTabs('c', 'a');
|
||||
expect(useSessionTabsStore.getState().tabIds).toEqual(['c', 'a', 'b']);
|
||||
const before = useSessionTabsStore.getState().tabIds;
|
||||
useSessionTabsStore.getState().reorderTabs('x', 'a');
|
||||
expect(useSessionTabsStore.getState().tabIds).toBe(before);
|
||||
});
|
||||
|
||||
test('removeTabs drops only confirmed-gone ids and no-ops otherwise', () => {
|
||||
useSessionTabsStore.setState({ tabIds: ['a', 'b'] });
|
||||
const before = useSessionTabsStore.getState().tabIds;
|
||||
useSessionTabsStore.getState().removeTabs(['x']);
|
||||
expect(useSessionTabsStore.getState().tabIds).toBe(before);
|
||||
useSessionTabsStore.getState().removeTabs(['a']);
|
||||
expect(useSessionTabsStore.getState().tabIds).toEqual(['b']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user