fix: prevent automatic session tab pinning (#1538)

This commit is contained in:
Carlos Florêncio
2026-06-29 18:59:47 +01:00
committed by GitHub
parent cc4ddf8467
commit bdbde9d12e
9 changed files with 570 additions and 58 deletions
@@ -47,8 +47,8 @@ import { PreviewFileTab, PreviewDiffTab, PreviewCommitTab, PinnedDefaultTab } fr
import { SessionTab } from "./session-tab";
import { TerminalTab } from "./terminal-tab";
import { useTabMaximizeOnDoubleClick } from "./use-tab-maximize";
import { setupSessionTabSync } from "./dockview-session-tab-sync";
import {
setupSessionTabSync,
setupChatPanelSafetyNet,
useAutoSessionTab,
useAutoPRPanel,
@@ -583,8 +583,10 @@ export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({
useDockviewStore.setState({ currentLayoutEnvId: currentEnvId });
readyDisposersRef.current.push(setupGroupTracking(api));
setupSessionTabSync(api, appStore);
setupChatPanelSafetyNet(api, appStore);
const sessionTabSyncDisposable = setupSessionTabSync(api, appStore);
readyDisposersRef.current.push(() => sessionTabSyncDisposable.dispose());
const chatPanelSafetyNetDisposable = setupChatPanelSafetyNet(api, appStore);
readyDisposersRef.current.push(() => chatPanelSafetyNetDisposable.dispose());
readyDisposersRef.current.push(setupLayoutPersistence(api, saveTimerRef, envIdRef));
setupPortalCleanup(api, appStore);
readyDisposersRef.current.push(setupContainerResizeSync(api));
@@ -0,0 +1,196 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DockviewReadyEvent } from "dockview-react";
import type { StoreApi } from "zustand";
import type { AppState } from "@/lib/state/store";
import { useDockviewStore } from "@/lib/state/dockview-store";
import {
clearSessionTabUserActivationIntentsForTest,
markSessionTabUserActivationIntent,
} from "./session-tab-activation-intent";
import { setupSessionTabSync } from "./dockview-session-tab-sync";
const TASK_ID = "task-A";
const ACTIVE_SESSION_ID = "s-active";
const OTHER_SESSION_ID = "s-other";
const OTHER_SESSION_PANEL_ID = `session:${OTHER_SESSION_ID}`;
type SessionTabSyncPanel = {
id: string;
api: { setActive: ReturnType<typeof vi.fn<[], void>> };
};
type SessionTabSyncApi = {
panels: SessionTabSyncPanel[];
getPanel: (id: string) => SessionTabSyncPanel | null;
onDidActivePanelChange: (callback: (panel: { id: string } | null) => void) => {
dispose: ReturnType<typeof vi.fn<[], void>>;
};
};
type SessionTabSyncStore = {
getState: () => {
tasks: { activeTaskId: string; activeSessionId: string };
taskSessions: {
items: Record<string, { id: string; task_id: string }>;
};
environmentIdBySessionId: Record<string, string>;
setActiveSession: ReturnType<typeof vi.fn<[string, string], void>>;
};
};
type SessionTabSyncHarness = ReturnType<typeof makeSessionTabSyncHarness>;
function makeSessionTabSyncHarness(args: {
activeTaskId: string;
activeSessionId: string;
otherSessionId: string;
includeOtherEnv?: boolean;
}) {
let activePanelChange: ((panel: { id: string } | null) => void) | null = null;
const activePanelSetActive = vi.fn(() => {
activePanelChange?.({ id: `session:${args.activeSessionId}` });
});
const otherPanelSetActive = vi.fn();
const panels: SessionTabSyncPanel[] = [
{ id: `session:${args.activeSessionId}`, api: { setActive: activePanelSetActive } },
{ id: `session:${args.otherSessionId}`, api: { setActive: otherPanelSetActive } },
];
const api: SessionTabSyncApi = {
panels,
getPanel: (id: string) => panels.find((panel) => panel.id === id) ?? null,
onDidActivePanelChange: (callback: (panel: { id: string } | null) => void) => {
activePanelChange = callback;
return { dispose: vi.fn() };
},
};
const setActiveSession = vi.fn();
const environmentIdBySessionId = {
[args.activeSessionId]: "env-A",
...(args.includeOtherEnv === false ? {} : { [args.otherSessionId]: "env-A" }),
};
const appStore: SessionTabSyncStore = {
getState: () => ({
tasks: {
activeTaskId: args.activeTaskId,
activeSessionId: args.activeSessionId,
},
taskSessions: {
items: {
[args.activeSessionId]: { id: args.activeSessionId, task_id: args.activeTaskId },
[args.otherSessionId]: { id: args.otherSessionId, task_id: args.activeTaskId },
},
},
environmentIdBySessionId,
setActiveSession,
}),
};
return {
api,
appStore,
setActiveSession,
activePanelSetActive,
otherPanelSetActive,
fireActivePanelChange: (panelId: string | null) => {
activePanelChange?.(panelId ? { id: panelId } : null);
},
};
}
function makeDefaultSessionTabSyncHarness(args?: { includeOtherEnv?: boolean }) {
return makeSessionTabSyncHarness({
activeTaskId: TASK_ID,
activeSessionId: ACTIVE_SESSION_ID,
otherSessionId: OTHER_SESSION_ID,
includeOtherEnv: args?.includeOtherEnv,
});
}
function startSessionTabSync(harness: SessionTabSyncHarness) {
setupSessionTabSync(
harness.api as unknown as DockviewReadyEvent["api"],
harness.appStore as unknown as StoreApi<AppState>,
);
}
describe("setupSessionTabSync", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
clearSessionTabUserActivationIntentsForTest();
useDockviewStore.setState({ isRestoringLayout: false });
});
afterEach(() => {
clearSessionTabUserActivationIntentsForTest();
useDockviewStore.setState({ isRestoringLayout: false });
vi.useRealTimers();
});
it("does not pin a session when Dockview activates another session panel without user intent", () => {
const harness = makeDefaultSessionTabSyncHarness();
startSessionTabSync(harness);
harness.fireActivePanelChange(OTHER_SESSION_PANEL_ID);
expect(harness.setActiveSession).not.toHaveBeenCalled();
expect(harness.activePanelSetActive).toHaveBeenCalledTimes(1);
});
it("pins the session when the active panel change follows explicit session-tab user intent", () => {
const harness = makeDefaultSessionTabSyncHarness();
startSessionTabSync(harness);
markSessionTabUserActivationIntent(OTHER_SESSION_ID);
harness.fireActivePanelChange(OTHER_SESSION_PANEL_ID);
expect(harness.setActiveSession).toHaveBeenCalledWith(TASK_ID, OTHER_SESSION_ID);
expect(harness.activePanelSetActive).not.toHaveBeenCalled();
});
it("ignores active panel changes while Dockview is restoring layout", () => {
const harness = makeDefaultSessionTabSyncHarness();
useDockviewStore.setState({ isRestoringLayout: true });
startSessionTabSync(harness);
markSessionTabUserActivationIntent(OTHER_SESSION_ID);
harness.fireActivePanelChange(OTHER_SESSION_PANEL_ID);
expect(harness.setActiveSession).not.toHaveBeenCalled();
expect(harness.activePanelSetActive).not.toHaveBeenCalled();
});
it("restores the active panel when intent exists for a different session", () => {
const harness = makeDefaultSessionTabSyncHarness();
startSessionTabSync(harness);
markSessionTabUserActivationIntent(ACTIVE_SESSION_ID);
harness.fireActivePanelChange(OTHER_SESSION_PANEL_ID);
expect(harness.setActiveSession).not.toHaveBeenCalled();
expect(harness.activePanelSetActive).toHaveBeenCalledTimes(1);
});
it("ignores null and non-session panel changes", () => {
const harness = makeDefaultSessionTabSyncHarness();
startSessionTabSync(harness);
harness.fireActivePanelChange(null);
harness.fireActivePanelChange("files");
expect(harness.setActiveSession).not.toHaveBeenCalled();
expect(harness.activePanelSetActive).not.toHaveBeenCalled();
expect(harness.otherPanelSetActive).not.toHaveBeenCalled();
});
it("restores active panel for stale session panels without an environment mapping", () => {
const harness = makeDefaultSessionTabSyncHarness({ includeOtherEnv: false });
startSessionTabSync(harness);
markSessionTabUserActivationIntent(OTHER_SESSION_ID);
harness.fireActivePanelChange(OTHER_SESSION_PANEL_ID);
expect(harness.setActiveSession).not.toHaveBeenCalled();
expect(harness.activePanelSetActive).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,83 @@
import type { DockviewReadyEvent } from "dockview-react";
import type { StoreApi } from "zustand";
import type { AppState } from "@/lib/state/store";
import { useDockviewStore } from "@/lib/state/dockview-store";
import { createDebugLogger, isDebug } from "@/lib/debug/log";
import { consumeSessionTabUserActivationIntent } from "./session-tab-activation-intent";
import { resolveSessionTabSyncTarget } from "./dockview-session-tabs";
const debug = createDebugLogger("dockview:session-tabs");
function restoreActiveSessionPanel(
api: DockviewReadyEvent["api"],
activeSessionId: string | null,
): void {
if (!activeSessionId) return;
api.getPanel(`session:${activeSessionId}`)?.api.setActive();
}
function isDifferentSessionPanel(panelId: string, activeSessionId: string | null): boolean {
return panelId.startsWith("session:") && panelId !== `session:${activeSessionId}`;
}
/**
* Sync `activeSessionId` in the store when the user explicitly activates a
* session tab. Dockview can also activate panels internally while restoring
* layout or reconciling tabs; those activations must not pin a different
* session or they create an app-level feedback loop.
*/
export function setupSessionTabSync(api: DockviewReadyEvent["api"], appStore: StoreApi<AppState>) {
return api.onDidActivePanelChange((panel) => {
if (!panel) return;
const isRestoring = useDockviewStore.getState().isRestoringLayout;
if (isDebug()) {
debug("setupSessionTabSync: onDidActivePanelChange", {
panelId: panel.id,
isRestoring,
currentActiveSessionId: appStore.getState().tasks.activeSessionId,
currentActiveTaskId: appStore.getState().tasks.activeTaskId,
livePanelIds: api.panels.map((p) => p.id),
});
}
if (isRestoring) return;
const state = appStore.getState();
const target = resolveSessionTabSyncTarget({
panelId: panel.id,
activeTaskId: state.tasks.activeTaskId,
activeSessionId: state.tasks.activeSessionId,
taskSessionsById: state.taskSessions.items,
environmentIdBySessionId: state.environmentIdBySessionId,
});
if (!target) {
const shouldRestoreActiveSession = isDifferentSessionPanel(
panel.id,
state.tasks.activeSessionId,
);
if (isDebug() && panel.id.startsWith("session:")) {
debug("setupSessionTabSync: skip (stale or cross-task panel)", {
panelId: panel.id,
activeTaskId: state.tasks.activeTaskId,
});
}
if (shouldRestoreActiveSession) restoreActiveSessionPanel(api, state.tasks.activeSessionId);
return;
}
if (!consumeSessionTabUserActivationIntent(target.sessionId)) {
if (isDebug()) {
debug("setupSessionTabSync: skip (no user activation intent)", {
panelId: panel.id,
activeSessionId: state.tasks.activeSessionId,
});
}
restoreActiveSessionPanel(api, state.tasks.activeSessionId);
return;
}
if (isDebug()) {
debug("setupSessionTabSync: setActiveSession", {
taskId: target.taskId,
newSessionId: target.sessionId,
});
}
state.setActiveSession(target.taskId, target.sessionId);
});
}
@@ -58,53 +58,6 @@ export function resolveSessionTabSyncTarget(args: {
return { taskId: activeTaskId, sessionId: sid };
}
/**
* Sync `activeSessionId` in the store when the user clicks a session tab.
* Layouts are env-keyed, so switching between sessions of the same task is
* a no-op at the layout level — the env switch action short-circuits when
* old==new env. No manual skip flag needed.
*/
export function setupSessionTabSync(api: DockviewReadyEvent["api"], appStore: StoreApi<AppState>) {
return api.onDidActivePanelChange((panel) => {
if (!panel) return;
const isRestoring = useDockviewStore.getState().isRestoringLayout;
if (isDebug()) {
debug("setupSessionTabSync: onDidActivePanelChange", {
panelId: panel.id,
isRestoring,
currentActiveSessionId: appStore.getState().tasks.activeSessionId,
currentActiveTaskId: appStore.getState().tasks.activeTaskId,
livePanelIds: api.panels.map((p) => p.id),
});
}
if (isRestoring) return;
const state = appStore.getState();
const target = resolveSessionTabSyncTarget({
panelId: panel.id,
activeTaskId: state.tasks.activeTaskId,
activeSessionId: state.tasks.activeSessionId,
taskSessionsById: state.taskSessions.items,
environmentIdBySessionId: state.environmentIdBySessionId,
});
if (!target) {
if (isDebug() && panel.id.startsWith("session:")) {
debug("setupSessionTabSync: skip (stale or cross-task panel)", {
panelId: panel.id,
activeTaskId: state.tasks.activeTaskId,
});
}
return;
}
if (isDebug()) {
debug("setupSessionTabSync: setActiveSession", {
taskId: target.taskId,
newSessionId: target.sessionId,
});
}
state.setActiveSession(target.taskId, target.sessionId);
});
}
/**
* Re-create a chat or session panel if the last one is removed.
* Prevents the user from ending up with no chat panel at all.
@@ -13,6 +13,7 @@ import { useTaskSessions } from "@/hooks/use-task-sessions";
import { addSessionPanel } from "@/lib/state/dockview-panel-actions";
import { getSessionStateIcon } from "@/lib/ui/state-icons";
import { AgentLogo } from "@/components/agent-logo";
import { markSessionTabUserActivationIntent } from "@/components/task/session-tab-activation-intent";
import type { TaskSession } from "@/lib/types/http";
import type { AgentProfileOption } from "@/lib/state/slices";
@@ -73,6 +74,7 @@ export function SessionReopenMenuItems({
if (!api) return;
// Reopening a session within the same task = same env, so the env switch
// action no-ops naturally. We just create the chat panel.
markSessionTabUserActivationIntent(sessionId);
addSessionPanel(api, groupId ?? centerGroupId, sessionId, label);
},
[api, centerGroupId],
@@ -0,0 +1,172 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearSessionTabUserActivationIntentsForTest,
consumeSessionTabUserActivationIntent,
markSessionPanelUserActivationIntent,
markSessionTabUserActivationIntent,
shouldMarkSessionTabUserActivationIntent,
} from "./session-tab-activation-intent";
const INACTIVE_SESSION_ID = "s-inactive";
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
clearSessionTabUserActivationIntentsForTest();
});
afterEach(() => {
clearSessionTabUserActivationIntentsForTest();
vi.useRealTimers();
});
describe("session tab activation intent lifecycle", () => {
it("ignores null and undefined session ids", () => {
markSessionTabUserActivationIntent(null);
markSessionTabUserActivationIntent(undefined);
expect(consumeSessionTabUserActivationIntent("s-active")).toBe(false);
});
it("expires marked intent after the TTL", () => {
markSessionTabUserActivationIntent("s-active");
vi.advanceTimersByTime(1501);
expect(consumeSessionTabUserActivationIntent("s-active")).toBe(false);
});
it("preserves one session intent when another session is consumed", () => {
markSessionTabUserActivationIntent("s-active");
expect(consumeSessionTabUserActivationIntent("s-other")).toBe(false);
expect(consumeSessionTabUserActivationIntent("s-active")).toBe(true);
});
it("consumes a matching intent once", () => {
markSessionTabUserActivationIntent("s-active");
expect(consumeSessionTabUserActivationIntent("s-active")).toBe(true);
expect(consumeSessionTabUserActivationIntent("s-active")).toBe(false);
});
it("keeps independent intents for fast successive tab activations", () => {
markSessionTabUserActivationIntent("s-first");
markSessionTabUserActivationIntent("s-second");
expect(consumeSessionTabUserActivationIntent("s-first")).toBe(true);
expect(consumeSessionTabUserActivationIntent("s-second")).toBe(true);
});
it("marks session intent from a session panel id", () => {
markSessionPanelUserActivationIntent("session:s-active");
markSessionPanelUserActivationIntent("files");
expect(consumeSessionTabUserActivationIntent("s-active")).toBe(true);
expect(consumeSessionTabUserActivationIntent("files")).toBe(false);
});
});
describe("session tab activation intent guard", () => {
it("does not mark intent for already-active tabs", () => {
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: "s-active",
activeSessionId: "s-other",
isActive: true,
target: document.createElement("span"),
}),
).toBe(false);
});
it("does not mark intent for the current active session when another panel is active", () => {
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: "s-active",
activeSessionId: "s-active",
isActive: false,
target: document.createElement("span"),
}),
).toBe(false);
});
it("does not mark intent without a session id", () => {
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: null,
activeSessionId: "s-active",
isActive: false,
target: document.createElement("span"),
}),
).toBe(false);
});
it("does not mark intent from nested interactive controls", () => {
const button = document.createElement("button");
const label = document.createElement("span");
button.append(label);
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: INACTIVE_SESSION_ID,
activeSessionId: "s-active",
isActive: false,
target: label,
}),
).toBe(false);
});
it("does not mark intent from role-based nested interactive controls", () => {
const button = document.createElement("span");
button.setAttribute("role", "button");
const label = document.createElement("span");
button.append(label);
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: INACTIVE_SESSION_ID,
activeSessionId: "s-active",
isActive: false,
target: label,
}),
).toBe(false);
});
it("does not mark intent from Dockview close actions", () => {
const closeAction = document.createElement("div");
closeAction.className = "dv-default-tab-action";
const label = document.createElement("span");
closeAction.append(label);
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: INACTIVE_SESSION_ID,
activeSessionId: "s-active",
isActive: false,
target: label,
}),
).toBe(false);
});
it("marks intent for non-element targets on inactive tabs", () => {
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: INACTIVE_SESSION_ID,
activeSessionId: "s-active",
isActive: false,
target: null,
}),
).toBe(true);
});
it("marks intent for inactive tab activation surfaces", () => {
expect(
shouldMarkSessionTabUserActivationIntent({
sessionId: INACTIVE_SESSION_ID,
activeSessionId: "s-active",
isActive: false,
target: document.createElement("span"),
}),
).toBe(true);
});
});
@@ -0,0 +1,48 @@
const SESSION_TAB_USER_ACTIVATION_TTL_MS = 1500;
const NESTED_INTERACTIVE_SELECTOR =
".dv-default-tab-action, button, a, input, select, textarea, [role='button'], [role='menuitem']";
type SessionTabActivationIntent = {
expiresAt: number;
};
const sessionTabActivationIntents = new Map<string, SessionTabActivationIntent>();
export function shouldMarkSessionTabUserActivationIntent(args: {
sessionId: string | null | undefined;
activeSessionId: string | null | undefined;
isActive: boolean;
target: EventTarget | null;
}): boolean {
if (!args.sessionId || args.isActive || args.sessionId === args.activeSessionId) return false;
if (typeof Element === "undefined" || !(args.target instanceof Element)) return true;
return !args.target.closest(NESTED_INTERACTIVE_SELECTOR);
}
export function markSessionTabUserActivationIntent(sessionId: string | null | undefined): void {
if (!sessionId) return;
sessionTabActivationIntents.set(sessionId, {
expiresAt: Date.now() + SESSION_TAB_USER_ACTIVATION_TTL_MS,
});
}
export function markSessionPanelUserActivationIntent(panelId: string | null | undefined): void {
if (!panelId?.startsWith("session:")) return;
markSessionTabUserActivationIntent(panelId.slice("session:".length));
}
export function consumeSessionTabUserActivationIntent(sessionId: string): boolean {
const intent = sessionTabActivationIntents.get(sessionId);
if (!intent) return false;
const now = Date.now();
if (intent.expiresAt < now) {
sessionTabActivationIntents.delete(sessionId);
return false;
}
sessionTabActivationIntents.delete(sessionId);
return true;
}
export function clearSessionTabUserActivationIntentsForTest(): void {
sessionTabActivationIntents.clear();
}
+60 -7
View File
@@ -1,6 +1,13 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
useCallback,
useEffect,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent,
} from "react";
import { DockviewDefaultTab, type IDockviewPanelHeaderProps } from "dockview-react";
import { IconStar } from "@tabler/icons-react";
import { AgentLogo } from "@/components/agent-logo";
@@ -35,6 +42,10 @@ import { HandoffContextMenuSub } from "@/components/task/handoff-profile-menu-it
import { NewSessionDialog, type HandoffPreset } from "@/components/task/new-session-dialog";
import { usableConfigOptions } from "@/components/model-config-selector";
import type { TaskSessionState } from "@/lib/types/http";
import {
markSessionTabUserActivationIntent,
shouldMarkSessionTabUserActivationIntent,
} from "./session-tab-activation-intent";
import { isSessionActive } from "./session-sort";
import { resolveSessionTabTitle } from "./session-tab-title";
import { useTabMaximizeOnDoubleClick } from "./use-tab-maximize";
@@ -139,6 +150,45 @@ function useSessionTabActions(
return { handleSetPrimary, handleStop, handleResume, handleDelete, handleCloseOthers };
}
function useSessionTabUserActivationIntent(
sessionId: string | undefined,
activeSessionId: string | null,
isActive: boolean,
) {
const markUserActivationIntent = useCallback(
(target: EventTarget | null) => {
if (
!shouldMarkSessionTabUserActivationIntent({ sessionId, activeSessionId, isActive, target })
)
return;
markSessionTabUserActivationIntent(sessionId);
},
[activeSessionId, isActive, sessionId],
);
const handlePointerDownCapture = useCallback(
(event: ReactPointerEvent) => {
if (event.button === 0) markUserActivationIntent(event.target);
},
[markUserActivationIntent],
);
const handleKeyDownCapture = useCallback(
(event: ReactKeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") markUserActivationIntent(event.target);
},
[markUserActivationIntent],
);
return { handlePointerDownCapture, handleKeyDownCapture };
}
function useDockviewTabActiveState(api: IDockviewPanelHeaderProps["api"]) {
const [isActive, setIsActive] = useState(api.isActive);
useEffect(() => {
const disposable = api.onDidActiveChange((e) => setIsActive(e.isActive));
return () => disposable.dispose();
}, [api]);
return isActive;
}
function DeleteSessionDialog({
open,
onOpenChange,
@@ -335,7 +385,8 @@ export function SessionTab(props: IDockviewPanelHeaderProps) {
const [shareOpen, setShareOpen] = useState(false);
const [handoffOpen, setHandoffOpen] = useState(false);
const [handoffPreset, setHandoffPreset] = useState<HandoffPreset | null>(null);
const [isActive, setIsActive] = useState(api.isActive);
const isActive = useDockviewTabActiveState(api);
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
const canShare = !!taskId && !!sessionId && shareableSessionStateClient(sessionState);
const handleHandoffProfile = useCallback(
(profileId: string) => {
@@ -346,11 +397,6 @@ export function SessionTab(props: IDockviewPanelHeaderProps) {
[sessionId],
);
useEffect(() => {
const disposable = api.onDidActiveChange((e) => setIsActive(e.isActive));
return () => disposable.dispose();
}, [api]);
useEffect(() => {
if (tabTitle && api.title !== tabTitle) api.setTitle(tabTitle);
}, [tabTitle, api]);
@@ -362,6 +408,11 @@ export function SessionTab(props: IDockviewPanelHeaderProps) {
const handleCloseTab = useCallback(() => {
setConfirmDelete(true);
}, []);
const { handlePointerDownCapture, handleKeyDownCapture } = useSessionTabUserActivationIntent(
sessionId,
activeSessionId,
isActive,
);
return (
<>
@@ -369,6 +420,8 @@ export function SessionTab(props: IDockviewPanelHeaderProps) {
<ContextMenuTrigger
className="flex h-full items-center cursor-pointer select-none"
data-testid={sessionId ? `session-tab-${sessionId}` : undefined}
onPointerDownCapture={handlePointerDownCapture}
onKeyDownCapture={handleKeyDownCapture}
onDoubleClick={onDoubleClick}
>
<SessionTabTriggerContent
+4 -1
View File
@@ -3,6 +3,7 @@
import { useEffect, useRef } from "react";
import { useDockviewStore } from "@/lib/state/dockview-store";
import { useAppStoreApi } from "@/components/state-provider";
import { markSessionPanelUserActivationIntent } from "@/components/task/session-tab-activation-intent";
import { createUserShell } from "@/lib/api/domains/user-shell-api";
import { matchesShortcut } from "@/lib/keyboard/utils";
import { getShortcut, type StoredShortcutOverrides } from "@/lib/keyboard/shortcut-overrides";
@@ -23,7 +24,9 @@ function handleTabNavigation(e: KeyboardEvent, api: DockviewApi) {
const direction = e.code === "BracketLeft" ? -1 : 1;
const nextIndex = (currentIndex + direction + panels.length) % panels.length;
panels[nextIndex].api.setActive();
const nextPanel = panels[nextIndex];
markSessionPanelUserActivationIntent(nextPanel.id);
nextPanel.api.setActive();
}
function handleTerminalToggle(