45ee2ff48f
* feat(backend): key user shells by TaskEnvironmentID Sessions in the same task share a TaskEnvironment. Re-key the user shell runner so terminals are scoped per env, not per session — switching between sessions of the same task now sees the same shell list. - Add TaskEnvironmentID to AgentExecution / LaunchRequest / ExecutorCreateRequest / executor.LaunchAgentRequest, propagated through ToAgentExecution and the orchestrator launch path. - Add Manager.ResolveScopeKey(sessionID) — returns env when execution carries it, falls back to sessionID for legacy paths. - Rename runner param sessionID -> scopeID in interactive_shells.go; map keys are now scopeID:terminalID. - Shell handlers + WS terminal handler resolve sid -> scopeID via ResolveScopeKey before calling the runner. * feat(web): key dockview layout + portals to TaskEnvironmentID Switching between sessions of the same task no longer rebuilds the dockview layout — terminals and panels persist because layouts are env-scoped, not session-scoped. The bottom terminal also reuses a stable sessionId across same-env switches so it doesn't reconnect to a different shell. - Rename layout persistence: setSessionLayout/getSessionLayout/*SessionMaximizeState -> setEnvLayout/getEnvLayout/*EnvMaximizeState (kandev.dockview.env-layout. prefix). - Rename dockview-session-switch.ts -> dockview-env-switch.ts; performSessionSwitch -> performEnvSwitch with oldEnvId/newEnvId/activeSessionId. - dockview-store.ts: currentLayoutSessionId -> currentLayoutEnvId; switchSessionLayout -> switchEnvLayout(oldEnvId, newEnvId, activeSessionId). Same-env switches no-op. Drop _skipLayoutSwitchForSession + consumeSkipFlag — env-keyed action handles it structurally. - panel-portal-manager: PortalEntry.envId; releaseBySession -> releaseByEnv. ENV_SCOPED_COMPONENTS in dockview-desktop-layout (renamed from SESSION_SCOPED). - useEnvSwitchCleanup replaces useSessionSwitchCleanup; tracks env changes only. - All performLayoutSwitch callers (sidebar, dropdown, header, dialog) resolve sid -> envId via environmentIdBySessionId before switching. - bottom-terminal-panel: drop sessionId prop, use useEnvironmentSessionId(). - cleanupTaskStorage now takes envIds[] alongside sessionIds[] for env-scoped layout/maximize cleanup. * test: add coverage for env-keyed terminals + layout Playwright e2e (terminal-env-keyed.spec.ts) — switching between sessions of the same task keeps terminal scrollback intact and asserts both sessions share one task_environment_id. Update the comment in sessionless-task-switch.spec.ts to reference performEnvSwitch. * fix: address env layout PR feedback * test: fix flaky handshake auth check * fix: address env scope review findings
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import type { DockviewApi } from "dockview-react";
|
|
import { useDockviewStore } from "./dockview-store";
|
|
|
|
vi.mock("@/lib/local-storage", () => ({
|
|
getEnvLayout: vi.fn(() => null),
|
|
setEnvLayout: vi.fn(),
|
|
getEnvMaximizeState: vi.fn(() => null),
|
|
setEnvMaximizeState: vi.fn(),
|
|
removeEnvMaximizeState: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/lib/layout/panel-portal-manager", () => ({
|
|
panelPortalManager: {
|
|
releaseByEnv: vi.fn(),
|
|
reconcile: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import { setEnvLayout } from "@/lib/local-storage";
|
|
import { panelPortalManager } from "@/lib/layout/panel-portal-manager";
|
|
|
|
function makeMockApi(): DockviewApi {
|
|
return {
|
|
width: 800,
|
|
height: 600,
|
|
panels: [],
|
|
groups: [],
|
|
fromJSON: vi.fn(),
|
|
toJSON: vi.fn(() => ({})),
|
|
layout: vi.fn(),
|
|
activeGroup: null,
|
|
onDidActivePanelChange: vi.fn(() => ({ dispose: vi.fn() })),
|
|
getPanel: vi.fn(() => null),
|
|
addPanel: vi.fn(),
|
|
hasMaximizedGroup: vi.fn(() => false),
|
|
} as unknown as DockviewApi;
|
|
}
|
|
|
|
describe("switchEnvLayout — root fix for terminal/layout swapping", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
useDockviewStore.setState({
|
|
api: null,
|
|
currentLayoutEnvId: null,
|
|
preMaximizeLayout: null,
|
|
maximizedGroupId: null,
|
|
isRestoringLayout: false,
|
|
});
|
|
});
|
|
|
|
it("no-ops when switching between sessions of the same env", () => {
|
|
const api = makeMockApi();
|
|
useDockviewStore.setState({ api, currentLayoutEnvId: "env-shared" });
|
|
|
|
useDockviewStore.getState().switchEnvLayout("env-shared", "env-shared", "session-B");
|
|
|
|
// Same env = no layout rebuild + no portal release. This is the entire
|
|
// point of env-keyed layouts: terminals + panels stay put.
|
|
expect(api.fromJSON).not.toHaveBeenCalled();
|
|
expect(panelPortalManager.releaseByEnv).not.toHaveBeenCalled();
|
|
expect(setEnvLayout).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("saves outgoing env + releases its portals when switching to a new env", () => {
|
|
const api = makeMockApi();
|
|
useDockviewStore.setState({ api, currentLayoutEnvId: "env-old" });
|
|
|
|
useDockviewStore.getState().switchEnvLayout("env-old", "env-new", "session-X");
|
|
|
|
expect(setEnvLayout).toHaveBeenCalledWith("env-old", expect.anything());
|
|
expect(panelPortalManager.releaseByEnv).toHaveBeenCalledWith("env-old");
|
|
expect(useDockviewStore.getState().currentLayoutEnvId).toBe("env-new");
|
|
});
|
|
|
|
it("first adoption (no previous env) just records the new env", () => {
|
|
const api = makeMockApi();
|
|
useDockviewStore.setState({ api, currentLayoutEnvId: null });
|
|
|
|
useDockviewStore.getState().switchEnvLayout(null, "env-first", "session-Y");
|
|
|
|
// First adoption keeps existing layout, no portal release on the
|
|
// (empty) outgoing env.
|
|
expect(panelPortalManager.releaseByEnv).not.toHaveBeenCalled();
|
|
expect(useDockviewStore.getState().currentLayoutEnvId).toBe("env-first");
|
|
});
|
|
|
|
it("does nothing when api is unset", () => {
|
|
useDockviewStore.setState({ api: null });
|
|
useDockviewStore.getState().switchEnvLayout("env-a", "env-b", null);
|
|
expect(setEnvLayout).not.toHaveBeenCalled();
|
|
});
|
|
});
|