fix(task): preserve saved panel focus on task return (#1907)

This commit is contained in:
Carlos Florêncio
2026-07-23 21:24:37 +01:00
committed by GitHub
parent 77ef32c86c
commit ba8d99f33f
3 changed files with 112 additions and 2 deletions
@@ -223,6 +223,36 @@ describe("applyChangesPanelAutoFocusState fallback session keys", () => {
});
});
describe("applyChangesPanelAutoFocusState saved layouts", () => {
it("keeps a returning environment's saved active panel over pending changes", () => {
const previousMarkers = {
envB: { count: 1, fingerprint: "b1" },
};
const pendingEnvKeys = new Set(["envB"]);
let activateCalls = 0;
applyChangesPanelAutoFocusState({
signalsByEnv: {
envB: signal(1, "b1"),
},
activeEnvKey: "envB",
previousActiveEnvKey: "envA",
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
hasSavedLayout: true,
isRestoringLayout: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(activateCalls).toBe(0);
expect(pendingEnvKeys.size).toBe(0);
});
});
describe("applyChangesPanelAutoFocusState", () => {
it("migrates keys before queuing, defers during restore, and clears after activation", () => {
const previousMarkers = {};
@@ -7,6 +7,7 @@ import { useAppStore } from "@/components/state-provider";
import { useDockviewStore } from "@/lib/state/dockview-store";
import type { AppState } from "@/lib/state/store";
import type { FileInfo, GitStatusEntry } from "@/lib/state/slices/session-runtime/types";
import { getEnvLayout } from "@/lib/local-storage";
import { djb2Hash } from "@/lib/utils/hash";
type DockviewPanel = NonNullable<ReturnType<DockviewApi["getPanel"]>>;
@@ -250,6 +251,7 @@ export function applyChangesPanelAutoFocusState(args: {
environmentIdBySessionId: Record<string, string>;
previousMarkers: Record<string, ChangesMarker>;
pendingEnvKeys: Set<string>;
hasSavedLayout?: boolean;
isRestoringLayout: boolean;
getIsRestoringLayout?: () => boolean;
activate: () => ActivateChangesPanelResult;
@@ -261,6 +263,7 @@ export function applyChangesPanelAutoFocusState(args: {
environmentIdBySessionId,
previousMarkers,
pendingEnvKeys,
hasSavedLayout = false,
isRestoringLayout,
getIsRestoringLayout,
activate,
@@ -286,7 +289,12 @@ export function applyChangesPanelAutoFocusState(args: {
const isCurrentlyRestoringLayout = () => isRestoringLayout || getIsRestoringLayout?.() === true;
if (activeEnvKey && !isCurrentlyRestoringLayout() && pendingEnvKeys.has(activeEnvKey)) {
// A saved env layout records the user's active panel. Pending Changes
// attention may choose the first panel for an unseen env, but must not
// override that explicit focus when the user returns to a task.
if (activeEnvKey && hasSavedLayout) {
pendingEnvKeys.delete(activeEnvKey);
} else if (activeEnvKey && !isCurrentlyRestoringLayout() && pendingEnvKeys.has(activeEnvKey)) {
const result = activate();
if (
shouldClearPendingChangesFocus(result) &&
@@ -307,6 +315,7 @@ export function useChangesPanelAutoFocus(activeEnvKey: string | null) {
const previousMarkersRef = useRef<Record<string, ChangesMarker>>({});
const pendingEnvKeysRef = useRef<Set<string>>(new Set());
const previousActiveEnvKeyRef = useRef<string | null>(null);
const hasSavedLayout = activeEnvKey !== null && getEnvLayout(activeEnvKey) !== null;
useEffect(() => {
previousActiveEnvKeyRef.current = applyChangesPanelAutoFocusState({
@@ -316,9 +325,17 @@ export function useChangesPanelAutoFocus(activeEnvKey: string | null) {
environmentIdBySessionId,
previousMarkers: previousMarkersRef.current,
pendingEnvKeys: pendingEnvKeysRef.current,
hasSavedLayout,
isRestoringLayout,
getIsRestoringLayout: () => useDockviewStore.getState().isRestoringLayout,
activate: () => activateChangesPanel(api),
});
}, [signalsByEnv, activeEnvKey, api, isRestoringLayout, environmentIdBySessionId]);
}, [
signalsByEnv,
activeEnvKey,
api,
isRestoringLayout,
environmentIdBySessionId,
hasSavedLayout,
]);
}
@@ -384,4 +384,67 @@ test.describe("Changes panel focus behavior", () => {
await expect(changesTab(testPage)).toHaveClass(/dv-active-tab/, { timeout: 5_000 });
});
test("new git updates do not replace a returning task's saved agent tab", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const taskA = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Saved Agent Focus A",
seedData.agentProfileId,
{
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
const taskB = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Saved Agent Focus B",
seedData.agentProfileId,
{
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!taskA.session_id || !taskB.session_id) {
throw new Error("Tasks did not start sessions");
}
await testPage.goto(`/t/${taskA.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle({ timeout: 30_000 });
await session.waitForDockviewReady();
await session.clickSessionChatTab();
await expect(session.activeChat()).toBeVisible();
await session.taskInSidebar("Saved Agent Focus B").click();
await expect(testPage).toHaveURL((url) => url.pathname.includes(taskB.id), {
timeout: 15_000,
});
await session.waitForLoad();
await setGitStatusForSession(testPage, taskA.session_id, []);
await expect
.poll(() => changesCountForSession(testPage, taskA.session_id!), { timeout: 15_000 })
.toBe(0);
await setGitStatusForSession(testPage, taskA.session_id, ["background-change.txt"]);
await expect
.poll(() => changesCountForSession(testPage, taskA.session_id!), { timeout: 15_000 })
.toBe(1);
await session.taskInSidebar("Saved Agent Focus A").click();
await expect(testPage).toHaveURL((url) => url.pathname.includes(taskA.id), {
timeout: 15_000,
});
await expect(session.activeChat()).toBeVisible({ timeout: 5_000 });
await expect(changesTab(testPage)).not.toHaveClass(/dv-active-tab/);
});
});