feat(header): Alt+W closes the active session tab, customizable in Settings
The close-with-neighbour-activation logic moved into a shared helper (the strip and the shortcut use the same path). Alt+W is the default because the browser owns Cmd/Ctrl+W on web; desktop users can rebind it — the action is registered as customizable, so it appears in the Settings shortcuts section and the shortcuts help automatically. VS Code is excluded (it has no session tabs).
This commit is contained in:
@@ -31,6 +31,7 @@ import { Icon } from '@/components/icon/Icon';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useSessionTabsStore } from '@/stores/useSessionTabsStore';
|
||||
import { closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs';
|
||||
import { useGlobalSessionsStore, resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useGlobalSessionStatus } from '@/sync/sync-context';
|
||||
@@ -391,13 +392,11 @@ export const SessionTabsStrip: React.FC<{
|
||||
const { t } = useI18n();
|
||||
const tabIds = useSessionTabsStore((state) => state.tabIds);
|
||||
const ensureTab = useSessionTabsStore((state) => state.ensureTab);
|
||||
const closeTab = useSessionTabsStore((state) => state.closeTab);
|
||||
const closeOtherTabs = useSessionTabsStore((state) => state.closeOtherTabs);
|
||||
const reorderTabs = useSessionTabsStore((state) => state.reorderTabs);
|
||||
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
|
||||
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
||||
const activeSessions = useGlobalSessionsStore((state) => state.activeSessions);
|
||||
|
||||
// Opening a session anywhere (sidebar, palette, deep link) adds its tab.
|
||||
@@ -425,20 +424,9 @@ export const SessionTabsStrip: React.FC<{
|
||||
setCurrentSession(tab.id, resolveGlobalSessionDirectory(tab.session));
|
||||
}, [setCurrentSession]);
|
||||
|
||||
const activateNeighbour = React.useCallback((closedId: string) => {
|
||||
const index = tabs.findIndex((tab) => tab.id === closedId);
|
||||
const neighbour = tabs[index + 1] ?? tabs[index - 1] ?? null;
|
||||
if (neighbour) {
|
||||
handleSelect(neighbour);
|
||||
} else {
|
||||
openNewSessionDraft();
|
||||
}
|
||||
}, [tabs, handleSelect, openNewSessionDraft]);
|
||||
|
||||
const handleClose = React.useCallback((id: string) => {
|
||||
if (id === currentSessionId) activateNeighbour(id);
|
||||
closeTab(id);
|
||||
}, [activateNeighbour, closeTab, currentSessionId]);
|
||||
closeSessionTabAndActivateNeighbour(id);
|
||||
}, []);
|
||||
|
||||
const handleCloseOthers = React.useCallback((id: string) => {
|
||||
closeOtherTabs(id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { isTerminalEventTarget } from '@/lib/terminalFocus';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { closeSessionTabAndActivateNeighbour } from '@/lib/sessionTabs';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import * as sessionActions from '@/sync/session-actions';
|
||||
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
|
||||
@@ -299,6 +300,14 @@ export const useKeyboardShortcuts = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isVSCodeRuntime() && eventMatchesShortcut(e, combo('close_session_tab'))) {
|
||||
e.preventDefault();
|
||||
if (currentSessionId) {
|
||||
closeSessionTabAndActivateNeighbour(currentSessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedNewSessionShortcut = eventMatchesShortcut(e, combo('new_chat'));
|
||||
const matchedWorktreeShortcut = eventMatchesShortcut(e, combo('new_chat_worktree'));
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useSessionTabsStore } from '@/stores/useSessionTabsStore';
|
||||
import { useGlobalSessionsStore, resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
|
||||
/**
|
||||
* Close one header session tab. Closing the active tab activates its right
|
||||
* neighbour (falling back left), or opens a new-session draft when it was the
|
||||
* last tab. Only tabs whose session is present in the loaded session list
|
||||
* count as neighbours — the same rule the strip uses for rendering. The
|
||||
* session itself is never touched.
|
||||
*/
|
||||
export const closeSessionTabAndActivateNeighbour = (sessionId: string): void => {
|
||||
const { tabIds, closeTab } = useSessionTabsStore.getState();
|
||||
if (!tabIds.includes(sessionId)) return;
|
||||
|
||||
const { currentSessionId, setCurrentSession, openNewSessionDraft } = useSessionUIStore.getState();
|
||||
if (sessionId === currentSessionId) {
|
||||
const sessionsById = new Map(
|
||||
useGlobalSessionsStore.getState().activeSessions.map((session) => [session.id, session] as const),
|
||||
);
|
||||
const renderable = tabIds.filter((id) => sessionsById.has(id));
|
||||
const index = renderable.indexOf(sessionId);
|
||||
const neighbourId = renderable[index + 1] ?? renderable[index - 1] ?? null;
|
||||
const neighbour = neighbourId ? sessionsById.get(neighbourId) : null;
|
||||
if (neighbour) {
|
||||
setCurrentSession(neighbour.id, resolveGlobalSessionDirectory(neighbour));
|
||||
} else {
|
||||
openNewSessionDraft();
|
||||
}
|
||||
}
|
||||
|
||||
closeTab(sessionId);
|
||||
};
|
||||
@@ -239,6 +239,13 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
|
||||
description: 'Create a new worktree and open a draft in it',
|
||||
customizable: true,
|
||||
},
|
||||
{
|
||||
id: 'close_session_tab',
|
||||
defaultCombo: 'alt+w',
|
||||
label: 'Close session tab',
|
||||
description: 'Close the active session tab in the header (the session itself stays)',
|
||||
customizable: true,
|
||||
},
|
||||
{
|
||||
id: 'new_mini_chat',
|
||||
defaultCombo: 'mod+alt+n',
|
||||
|
||||
Reference in New Issue
Block a user