fix(task): preserve saved panel focus on task return (#1907)
This commit is contained in:
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user