Files
kandev/apps/web/lib/state/dockview-env-switch-action.test.ts
T
José Almeida 45ee2ff48f feat: key terminals + dockview layout to TaskEnvironment (#755)
* 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
2026-05-01 11:00:33 +01:00

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();
});
});