perf(app): keep warm workspace switching responsive (#3447)

* perf(app): reduce warm workspace switch work

Retained workspaces avoided mounts but pane activity still rerendered the full composer, message input, and agent-control presentation. Keep the activity contract intact while moving focus-sensitive behavior behind small context consumers, and add a Playwright benchmark that measures rapid retained workspace switching on one browser clock.

* perf(app): accelerate retained workspace switching

Avoid repeated retained-stream layout reconciliation and unrelated workspace structure recomputation. Use the lightweight route transition while the host is already focused, while preserving targeted root POP_TO for returns from app-wide routes where it prevents duplicate hidden decks.

* perf(app): defer composer focus during workspace switches

* perf(app): retain recently used workspaces longer

Keep up to ten workspace screens warm so normal multi-workspace navigation does not repeatedly rebuild recently used timelines. Inactive screens expire after ten minutes, while the active screen remains protected and capacity eviction stays least-recently-used.

* fix(app): reconcile retained timeline geometry

* fix(app): refresh retained reading position
This commit is contained in:
Mohamed Boudra
2026-08-16 12:40:28 +02:00
committed by GitHub
parent d60d92e909
commit d4435bd016
20 changed files with 1297 additions and 228 deletions
@@ -1,10 +1,13 @@
import { describe, expect, it } from "vitest";
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import {
getNextWorkspaceDeckExpirationDelay,
orderWorkspaceSelectionsForStableRender,
pruneMountedWorkspaceSelections,
reconcileRetainedWorkspaceSelections,
resolveWorkspaceDeckEntries,
shouldKeepWorkspaceDeckEntryMounted,
WORKSPACE_DECK_INACTIVE_TTL_MS,
WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES,
} from "@/screens/workspace/workspace-deck-retention";
function workspace(workspaceId: string, serverId = "server"): ActiveWorkspaceSelection {
@@ -15,65 +18,115 @@ function mountedWorkspaceIds(selections: ActiveWorkspaceSelection[]): string[] {
return selections.map((selection) => selection.workspaceId);
}
describe("pruneMountedWorkspaceSelections", () => {
it("retains the deck while an app-wide route temporarily clears the active workspace", () => {
const mountedSelections = [workspace("A"), workspace("B")];
function retained(workspaceId: string, inactiveSince: number | null) {
return { selection: workspace(workspaceId), inactiveSince };
}
function retainedWorkspaceIds(
entries: ReturnType<typeof reconcileRetainedWorkspaceSelections>,
): string[] {
return entries.map((entry) => entry.selection.workspaceId);
}
describe("reconcileRetainedWorkspaceSelections", () => {
it("retains inactive workspaces for ten minutes across app-wide routes", () => {
const retainedEntries = [retained("A", 1_000), retained("B", 2_000)];
expect(
pruneMountedWorkspaceSelections({
currentSelections: mountedSelections,
reconcileRetainedWorkspaceSelections({
currentEntries: retainedEntries,
activeSelection: null,
now: 2_000 + WORKSPACE_DECK_INACTIVE_TTL_MS - 1,
}),
).toBe(mountedSelections);
).toEqual([retained("B", 2_000)]);
});
it("keeps the active workspace and the two most recent inactive workspaces", () => {
const mountedAfterA = pruneMountedWorkspaceSelections({
currentSelections: [],
activeSelection: workspace("A"),
it("keeps the active workspace and nine most recently activated inactive workspaces", () => {
let retainedEntries = Array.from(
{ length: WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES },
(_, index) => retained(String(index + 1), index + 1),
).toReversed();
retainedEntries = reconcileRetainedWorkspaceSelections({
currentEntries: retainedEntries,
activeSelection: workspace("11"),
now: 11,
});
const mountedAfterB = pruneMountedWorkspaceSelections({
currentSelections: mountedAfterA,
expect(retainedWorkspaceIds(retainedEntries)).toEqual([
"11",
"10",
"9",
"8",
"7",
"6",
"5",
"4",
"3",
"2",
]);
});
it("starts the TTL when the active workspace becomes inactive", () => {
const retainedEntries = reconcileRetainedWorkspaceSelections({
currentEntries: [retained("A", null), retained("B", 2_000)],
activeSelection: workspace("B"),
});
const mountedAfterC = pruneMountedWorkspaceSelections({
currentSelections: mountedAfterB,
activeSelection: workspace("C"),
});
const mountedAfterD = pruneMountedWorkspaceSelections({
currentSelections: mountedAfterC,
activeSelection: workspace("D"),
now: 3_000,
});
expect(mountedWorkspaceIds(mountedAfterD)).toEqual(["D", "C", "B"]);
expect(retainedEntries).toEqual([retained("B", null), retained("A", 3_000)]);
});
it("retains the active workspace", () => {
const mountedSelections = pruneMountedWorkspaceSelections({
currentSelections: [workspace("A")],
it("never expires the active workspace", () => {
const retainedEntries = reconcileRetainedWorkspaceSelections({
currentEntries: [retained("A", 0)],
activeSelection: workspace("A"),
now: WORKSPACE_DECK_INACTIVE_TTL_MS,
});
expect(mountedWorkspaceIds(mountedSelections)).toEqual(["A"]);
expect(retainedEntries).toEqual([retained("A", null)]);
});
it("expires an inactive workspace at the TTL boundary", () => {
const retainedEntries = reconcileRetainedWorkspaceSelections({
currentEntries: [retained("B", null), retained("A", 0)],
activeSelection: workspace("B"),
now: WORKSPACE_DECK_INACTIVE_TTL_MS,
});
expect(retainedEntries).toEqual([retained("B", null)]);
});
it("deduplicates retained workspace selections", () => {
const mountedSelections = pruneMountedWorkspaceSelections({
currentSelections: [workspace("B"), workspace("A"), workspace("B")],
const retainedEntries = reconcileRetainedWorkspaceSelections({
currentEntries: [retained("B", 2), retained("A", 1), retained("B", 0)],
activeSelection: workspace("A"),
now: 3,
});
expect(mountedWorkspaceIds(mountedSelections)).toEqual(["A", "B"]);
expect(retainedWorkspaceIds(retainedEntries)).toEqual(["A", "B"]);
});
});
describe("getNextWorkspaceDeckExpirationDelay", () => {
it("returns the remaining lifetime of the oldest inactive workspace", () => {
expect(
getNextWorkspaceDeckExpirationDelay({
entries: [retained("C", null), retained("B", 2_000), retained("A", 1_000)],
activeSelection: workspace("C"),
now: 4_000,
}),
).toBe(WORKSPACE_DECK_INACTIVE_TTL_MS - 3_000);
});
it("always allows at least the active workspace", () => {
const mountedSelections = pruneMountedWorkspaceSelections({
currentSelections: [workspace("A"), workspace("B")],
activeSelection: workspace("C"),
maxMountedWorkspaces: 0,
});
expect(mountedWorkspaceIds(mountedSelections)).toEqual(["C"]);
it("does not schedule expiration for the active workspace", () => {
expect(
getNextWorkspaceDeckExpirationDelay({
entries: [retained("A", null)],
activeSelection: workspace("A"),
now: 4_000,
}),
).toBeNull();
});
});
@@ -1,11 +1,26 @@
import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
export const WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES = 3;
export const WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES = 10;
export const WORKSPACE_DECK_INACTIVE_TTL_MS = 10 * 60 * 1000;
interface PruneMountedWorkspaceSelectionsInput {
currentSelections: ActiveWorkspaceSelection[];
export interface RetainedWorkspaceSelection {
selection: ActiveWorkspaceSelection;
inactiveSince: number | null;
}
interface ReconcileRetainedWorkspaceSelectionsInput {
currentEntries: RetainedWorkspaceSelection[];
activeSelection: ActiveWorkspaceSelection | null;
now: number;
maxMountedWorkspaces?: number;
inactiveTtlMs?: number;
}
interface GetNextWorkspaceDeckExpirationDelayInput {
entries: RetainedWorkspaceSelection[];
activeSelection: ActiveWorkspaceSelection | null;
now: number;
inactiveTtlMs?: number;
}
interface WorkspaceDeckEntryMountInput {
@@ -30,53 +45,92 @@ export function areWorkspaceSelectionsEqual(
return left?.serverId === right?.serverId && left?.workspaceId === right?.workspaceId;
}
export function areWorkspaceSelectionListsEqual(
left: ActiveWorkspaceSelection[],
right: ActiveWorkspaceSelection[],
export function areRetainedWorkspaceSelectionListsEqual(
left: RetainedWorkspaceSelection[],
right: RetainedWorkspaceSelection[],
): boolean {
if (left.length !== right.length) {
return false;
}
return left.every((selection, index) =>
areWorkspaceSelectionsEqual(selection, right[index] ?? null),
);
return left.every((entry, index) => {
const otherEntry = right[index];
return (
otherEntry !== undefined &&
entry.inactiveSince === otherEntry.inactiveSince &&
areWorkspaceSelectionsEqual(entry.selection, otherEntry.selection)
);
});
}
export function pruneMountedWorkspaceSelections({
currentSelections,
export function reconcileRetainedWorkspaceSelections({
currentEntries,
activeSelection,
now,
maxMountedWorkspaces = WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES,
}: PruneMountedWorkspaceSelectionsInput): ActiveWorkspaceSelection[] {
if (!activeSelection) {
return currentSelections;
}
const maxSelections = Math.max(1, maxMountedWorkspaces);
const nextSelections: ActiveWorkspaceSelection[] = [];
inactiveTtlMs = WORKSPACE_DECK_INACTIVE_TTL_MS,
}: ReconcileRetainedWorkspaceSelectionsInput): RetainedWorkspaceSelection[] {
const maxSelections = activeSelection
? Math.max(1, maxMountedWorkspaces)
: Math.max(0, maxMountedWorkspaces);
const nextEntries: RetainedWorkspaceSelection[] = [];
const seenSelectionKeys = new Set<string>();
function appendSelection(selection: ActiveWorkspaceSelection): void {
if (nextSelections.length >= maxSelections) {
function appendEntry(entry: RetainedWorkspaceSelection): void {
if (nextEntries.length >= maxSelections) {
return;
}
const selectionKey = getWorkspaceSelectionKey(selection);
const selectionKey = getWorkspaceSelectionKey(entry.selection);
if (seenSelectionKeys.has(selectionKey)) {
return;
}
seenSelectionKeys.add(selectionKey);
nextSelections.push(selection);
nextEntries.push(entry);
}
appendSelection(activeSelection);
if (activeSelection) {
appendEntry({ selection: activeSelection, inactiveSince: null });
}
for (const selection of currentSelections) {
if (areWorkspaceSelectionsEqual(selection, activeSelection)) {
for (const entry of currentEntries) {
if (areWorkspaceSelectionsEqual(entry.selection, activeSelection)) {
continue;
}
appendSelection(selection);
const inactiveSince = entry.inactiveSince ?? now;
if (now - inactiveSince >= inactiveTtlMs) {
continue;
}
appendEntry({ selection: entry.selection, inactiveSince });
}
return nextSelections;
if (areRetainedWorkspaceSelectionListsEqual(currentEntries, nextEntries)) {
return currentEntries;
}
return nextEntries;
}
export function getNextWorkspaceDeckExpirationDelay({
entries,
activeSelection,
now,
inactiveTtlMs = WORKSPACE_DECK_INACTIVE_TTL_MS,
}: GetNextWorkspaceDeckExpirationDelayInput): number | null {
let nextExpirationDelay: number | null = null;
for (const entry of entries) {
if (
areWorkspaceSelectionsEqual(entry.selection, activeSelection) ||
entry.inactiveSince === null
) {
continue;
}
const expirationDelay = Math.max(0, entry.inactiveSince + inactiveTtlMs - now);
nextExpirationDelay =
nextExpirationDelay === null
? expirationDelay
: Math.min(nextExpirationDelay, expirationDelay);
}
return nextExpirationDelay;
}
export function orderWorkspaceSelectionsForStableRender(