Gate workspace_update emissions to prevent unnecessary sidebar re-renders

Server: remove activityAt computation from workspace descriptors (always
sends null for backwards compat) and deduplicate emissions in
emitWorkspaceUpdatesForWorkspaceIds using fast-deep-equal — workspace
updates are now only sent when the descriptor actually changed.

Client: remove activityAt from WorkspaceDescriptor, sidebar entry types,
and baseline sort comparators. Workspace/project ordering is fully
handled by the persisted sidebar order store.
This commit is contained in:
Mohamed Boudra
2026-04-12 12:10:14 +07:00
parent b9269df3e4
commit 0d340959d7
12 changed files with 29 additions and 99 deletions
+1
View File
@@ -35400,6 +35400,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.18.2", "express": "^4.18.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.1.0", "fast-uri": "^3.1.0",
"mnemonic-id": "^3.2.7", "mnemonic-id": "^3.2.7",
"node-pty": "1.2.0-beta.11", "node-pty": "1.2.0-beta.11",
@@ -57,11 +57,6 @@ function HostIndexRouteContent() {
); );
const visibleWorkspaces = sessionWorkspaces ? Array.from(sessionWorkspaces.values()) : []; const visibleWorkspaces = sessionWorkspaces ? Array.from(sessionWorkspaces.values()) : [];
visibleWorkspaces.sort((left, right) => {
const leftTime = left.activityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
const rightTime = right.activityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
return rightTime - leftTime;
});
const primaryAgent = visibleAgents[0]; const primaryAgent = visibleAgents[0];
if (primaryAgent?.cwd?.trim()) { if (primaryAgent?.cwd?.trim()) {
@@ -15,7 +15,7 @@ function item(key: string): OrderedItem {
} }
function workspace( function workspace(
input: Pick<WorkspaceDescriptor, "id" | "projectId" | "name" | "status" | "activityAt"> & input: Pick<WorkspaceDescriptor, "id" | "projectId" | "name" | "status"> &
Partial< Partial<
Pick< Pick<
WorkspaceDescriptor, WorkspaceDescriptor,
@@ -32,7 +32,6 @@ function workspace(
workspaceKind: input.workspaceKind ?? "local_checkout", workspaceKind: input.workspaceKind ?? "local_checkout",
name: input.name, name: input.name,
status: input.status, status: input.status,
activityAt: input.activityAt,
diffStat: null, diffStat: null,
}; };
} }
@@ -100,7 +99,6 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
projectId: "project-1", projectId: "project-1",
name: "feat/hard-cut", name: "feat/hard-cut",
status: "failed", status: "failed",
activityAt: new Date("2026-01-01T00:00:00.000Z"),
}), }),
]; ];
@@ -117,21 +115,19 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
expect(projects[0]?.workspaces[0]?.statusBucket).toBe("failed"); expect(projects[0]?.workspaces[0]?.statusBucket).toBe("failed");
}); });
it("preserves stored project order even when activity changes", () => { it("preserves stored project order even when input order differs", () => {
const initialWorkspaces: WorkspaceDescriptor[] = [ const initialWorkspaces: WorkspaceDescriptor[] = [
workspace({ workspace({
id: "/repo/b", id: "/repo/b",
projectId: "project-b", projectId: "project-b",
name: "feat/b", name: "feat/b",
status: "running", status: "running",
activityAt: new Date("2026-01-02T00:00:00.000Z"),
}), }),
workspace({ workspace({
id: "/repo/a", id: "/repo/a",
projectId: "project-a", projectId: "project-a",
name: "feat/a", name: "feat/a",
status: "running", status: "running",
activityAt: new Date("2026-01-01T00:00:00.000Z"),
}), }),
]; ];
@@ -148,19 +144,17 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
const updatedProjects = buildSidebarProjectsFromWorkspaces({ const updatedProjects = buildSidebarProjectsFromWorkspaces({
serverId: "srv", serverId: "srv",
workspaces: [ workspaces: [
workspace({
id: "/repo/b",
projectId: "project-b",
name: "feat/b",
status: "running",
activityAt: new Date("2026-01-02T00:00:00.000Z"),
}),
workspace({ workspace({
id: "/repo/a", id: "/repo/a",
projectId: "project-a", projectId: "project-a",
name: "feat/a", name: "feat/a",
status: "running", status: "running",
activityAt: new Date("2026-01-03T00:00:00.000Z"), }),
workspace({
id: "/repo/b",
projectId: "project-b",
name: "feat/b",
status: "running",
}), }),
], ],
projectOrder: seededOrder, projectOrder: seededOrder,
@@ -168,8 +162,8 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
}); });
expect(updatedProjects.map((project) => project.projectKey)).toEqual([ expect(updatedProjects.map((project) => project.projectKey)).toEqual([
"project-b",
"project-a", "project-a",
"project-b",
]); ]);
}); });
@@ -182,21 +176,18 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
projectId: "project-c", projectId: "project-c",
name: "feat/c", name: "feat/c",
status: "running", status: "running",
activityAt: new Date("2026-01-04T00:00:00.000Z"),
}), }),
workspace({ workspace({
id: "/repo/b", id: "/repo/b",
projectId: "project-b", projectId: "project-b",
name: "feat/b", name: "feat/b",
status: "running", status: "running",
activityAt: new Date("2026-01-02T00:00:00.000Z"),
}), }),
workspace({ workspace({
id: "/repo/a", id: "/repo/a",
projectId: "project-a", projectId: "project-a",
name: "feat/a", name: "feat/a",
status: "running", status: "running",
activityAt: new Date("2026-01-01T00:00:00.000Z"),
}), }),
], ],
projectOrder: ["project-b", "project-a", "project-c"], projectOrder: ["project-b", "project-a", "project-c"],
@@ -219,14 +210,12 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
projectId: "project-1", projectId: "project-1",
name: "main", name: "main",
status: "running", status: "running",
activityAt: new Date("2026-01-02T00:00:00.000Z"),
}), }),
workspace({ workspace({
id: "/repo/feature", id: "/repo/feature",
projectId: "project-1", projectId: "project-1",
name: "feature", name: "feature",
status: "running", status: "running",
activityAt: new Date("2026-01-01T00:00:00.000Z"),
}), }),
], ],
projectOrder: ["project-1"], projectOrder: ["project-1"],
@@ -246,14 +235,12 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
projectId: "project-1", projectId: "project-1",
name: "main", name: "main",
status: "running", status: "running",
activityAt: new Date("2026-01-02T00:00:00.000Z"),
}), }),
workspace({ workspace({
id: "/repo/feature", id: "/repo/feature",
projectId: "project-1", projectId: "project-1",
name: "feature", name: "feature",
status: "running", status: "running",
activityAt: new Date("2026-01-03T00:00:00.000Z"),
}), }),
], ],
projectOrder: ["project-1"], projectOrder: ["project-1"],
@@ -263,8 +250,8 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
}); });
expect(projects[0]?.workspaces.map((workspace) => workspace.workspaceId)).toEqual([ expect(projects[0]?.workspaces.map((workspace) => workspace.workspaceId)).toEqual([
"/repo/main",
"/repo/feature", "/repo/feature",
"/repo/main",
]); ]);
}); });
}); });
@@ -7,6 +7,7 @@ import {
import { getHostRuntimeStore } from "@/runtime/host-runtime"; import { getHostRuntimeStore } from "@/runtime/host-runtime";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store"; import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import type { WorkspaceDescriptor } from "@/stores/session-store"; import type { WorkspaceDescriptor } from "@/stores/session-store";
import type { WorkspaceDescriptorPayload } from "@server/shared/messages";
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name"; import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
const EMPTY_ORDER: string[] = []; const EMPTY_ORDER: string[] = [];
@@ -21,7 +22,6 @@ export interface SidebarWorkspaceEntry {
workspaceId: string; workspaceId: string;
workspaceKind: WorkspaceDescriptor["workspaceKind"]; workspaceKind: WorkspaceDescriptor["workspaceKind"];
name: string; name: string;
activityAt: Date | null;
statusBucket: SidebarStateBucket; statusBucket: SidebarStateBucket;
diffStat: { additions: number; deletions: number } | null; diffStat: { additions: number; deletions: number } | null;
} }
@@ -34,7 +34,6 @@ export interface SidebarProjectEntry {
statusBucket: SidebarStateBucket; statusBucket: SidebarStateBucket;
activeCount: number; activeCount: number;
totalWorkspaces: number; totalWorkspaces: number;
latestActivityAt: Date | null;
workspaces: SidebarWorkspaceEntry[]; workspaces: SidebarWorkspaceEntry[];
} }
@@ -67,15 +66,6 @@ function compareWorkspaceBaseline(
left: SidebarWorkspaceEntry, left: SidebarWorkspaceEntry,
right: SidebarWorkspaceEntry, right: SidebarWorkspaceEntry,
): number { ): number {
if (left.activityAt && right.activityAt) {
const dateDelta = right.activityAt.getTime() - left.activityAt.getTime();
if (dateDelta !== 0) {
return dateDelta;
}
} else if (left.activityAt || right.activityAt) {
return left.activityAt ? -1 : 1;
}
const nameDelta = left.name.localeCompare(right.name, undefined, { const nameDelta = left.name.localeCompare(right.name, undefined, {
numeric: true, numeric: true,
sensitivity: "base", sensitivity: "base",
@@ -91,15 +81,6 @@ function compareWorkspaceBaseline(
} }
function compareProjectBaseline(left: SidebarProjectEntry, right: SidebarProjectEntry): number { function compareProjectBaseline(left: SidebarProjectEntry, right: SidebarProjectEntry): number {
if (left.latestActivityAt && right.latestActivityAt) {
const dateDelta = right.latestActivityAt.getTime() - left.latestActivityAt.getTime();
if (dateDelta !== 0) {
return dateDelta;
}
} else if (left.latestActivityAt || right.latestActivityAt) {
return left.latestActivityAt ? -1 : 1;
}
return left.projectName.localeCompare(right.projectName, undefined, { return left.projectName.localeCompare(right.projectName, undefined, {
numeric: true, numeric: true,
sensitivity: "base", sensitivity: "base",
@@ -126,7 +107,6 @@ export function buildSidebarProjectsFromWorkspaces(input: {
statusBucket: "done", statusBucket: "done",
activeCount: 0, activeCount: 0,
totalWorkspaces: 0, totalWorkspaces: 0,
latestActivityAt: null,
workspaces: [], workspaces: [],
} satisfies SidebarProjectEntry); } satisfies SidebarProjectEntry);
@@ -136,7 +116,6 @@ export function buildSidebarProjectsFromWorkspaces(input: {
workspaceId: workspace.id, workspaceId: workspace.id,
workspaceKind: workspace.workspaceKind, workspaceKind: workspace.workspaceKind,
name: workspace.name, name: workspace.name,
activityAt: workspace.activityAt,
statusBucket: workspace.status, statusBucket: workspace.status,
diffStat: workspace.diffStat, diffStat: workspace.diffStat,
}; };
@@ -147,12 +126,6 @@ export function buildSidebarProjectsFromWorkspaces(input: {
project.activeCount += 1; project.activeCount += 1;
} }
project.statusBucket = aggregateBucket(project.statusBucket, workspace.status); project.statusBucket = aggregateBucket(project.statusBucket, workspace.status);
if (
!project.latestActivityAt ||
(workspace.activityAt && workspace.activityAt.getTime() > project.latestActivityAt.getTime())
) {
project.latestActivityAt = workspace.activityAt;
}
byProject.set(workspace.projectId, project); byProject.set(workspace.projectId, project);
} }
@@ -251,17 +224,7 @@ function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string
return `${serverId.trim()}::${projectKey.trim()}`; return `${serverId.trim()}::${projectKey.trim()}`;
} }
function toWorkspaceDescriptor(payload: { function toWorkspaceDescriptor(payload: WorkspaceDescriptorPayload): WorkspaceDescriptor {
id: string;
projectId: string;
projectDisplayName: string;
projectRootPath: string;
projectKind: WorkspaceDescriptor["projectKind"];
workspaceKind: WorkspaceDescriptor["workspaceKind"];
name: string;
status: WorkspaceDescriptor["status"];
activityAt: string | null;
}): WorkspaceDescriptor {
return normalizeWorkspaceDescriptor(payload); return normalizeWorkspaceDescriptor(payload);
} }
@@ -17,7 +17,6 @@ describe("workspace source of truth consumption", () => {
workspaceKind: "local_checkout", workspaceKind: "local_checkout",
name: "feat/workspace-sot", name: "feat/workspace-sot",
status: "running", status: "running",
activityAt: new Date("2026-03-01T00:00:00.000Z"),
diffStat: null, diffStat: null,
}; };
@@ -13,7 +13,6 @@ function createWorkspace(
workspaceKind: input.workspaceKind ?? "local_checkout", workspaceKind: input.workspaceKind ?? "local_checkout",
name: input.name ?? "main", name: input.name ?? "main",
status: input.status ?? "done", status: input.status ?? "done",
activityAt: input.activityAt ?? null,
diffStat: input.diffStat ?? null, diffStat: input.diffStat ?? null,
}; };
} }
-3
View File
@@ -120,14 +120,12 @@ export interface WorkspaceDescriptor {
workspaceKind: WorkspaceDescriptorPayload["workspaceKind"]; workspaceKind: WorkspaceDescriptorPayload["workspaceKind"];
name: string; name: string;
status: WorkspaceDescriptorPayload["status"]; status: WorkspaceDescriptorPayload["status"];
activityAt: Date | null;
diffStat: { additions: number; deletions: number } | null; diffStat: { additions: number; deletions: number } | null;
} }
export function normalizeWorkspaceDescriptor( export function normalizeWorkspaceDescriptor(
payload: WorkspaceDescriptorPayload, payload: WorkspaceDescriptorPayload,
): WorkspaceDescriptor { ): WorkspaceDescriptor {
const activityAt = payload.activityAt ? new Date(payload.activityAt) : null;
return { return {
id: normalizeWorkspaceIdentity(payload.id) ?? payload.id, id: normalizeWorkspaceIdentity(payload.id) ?? payload.id,
projectId: payload.projectId, projectId: payload.projectId,
@@ -137,7 +135,6 @@ export function normalizeWorkspaceDescriptor(
workspaceKind: payload.workspaceKind, workspaceKind: payload.workspaceKind,
name: payload.name, name: payload.name,
status: payload.status, status: payload.status,
activityAt: activityAt && !Number.isNaN(activityAt.getTime()) ? activityAt : null,
diffStat: payload.diffStat ?? null, diffStat: payload.diffStat ?? null,
}; };
} }
@@ -15,7 +15,6 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
workspaceId: "/repo", workspaceId: "/repo",
workspaceKind: "directory", workspaceKind: "directory",
name: "paseo", name: "paseo",
activityAt: null,
statusBucket: "done", statusBucket: "done",
diffStat: null, diffStat: null,
...overrides, ...overrides,
@@ -31,7 +30,6 @@ function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEn
statusBucket: "done", statusBucket: "done",
activeCount: 0, activeCount: 0,
totalWorkspaces: 1, totalWorkspaces: 1,
latestActivityAt: null,
workspaces: [workspace()], workspaces: [workspace()],
...overrides, ...overrides,
}; };
@@ -13,7 +13,6 @@ function workspace(serverId: string, cwd: string): SidebarWorkspaceEntry {
workspaceId: cwd, workspaceId: cwd,
workspaceKind: "local_checkout", workspaceKind: "local_checkout",
name: cwd, name: cwd,
activityAt: null,
statusBucket: "done", statusBucket: "done",
diffStat: null, diffStat: null,
}; };
@@ -28,7 +27,6 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
statusBucket: "done", statusBucket: "done",
activeCount: 0, activeCount: 0,
totalWorkspaces: workspaces.length, totalWorkspaces: workspaces.length,
latestActivityAt: null,
workspaces, workspaces,
}; };
} }
@@ -17,7 +17,6 @@ function workspace(
workspaceKind: input.workspaceKind ?? "worktree", workspaceKind: input.workspaceKind ?? "worktree",
name: input.name ?? input.id, name: input.name ?? input.id,
status: input.status ?? "done", status: input.status ?? "done",
activityAt: input.activityAt ?? null,
diffStat: input.diffStat ?? null, diffStat: input.diffStat ?? null,
}; };
} }
+1
View File
@@ -76,6 +76,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.18.2", "express": "^4.18.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.1.0", "fast-uri": "^3.1.0",
"mnemonic-id": "^3.2.7", "mnemonic-id": "^3.2.7",
"node-pty": "1.2.0-beta.11", "node-pty": "1.2.0-beta.11",
+15 -22
View File
@@ -1,3 +1,4 @@
import equal from "fast-deep-equal";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { stat } from "fs/promises"; import { stat } from "fs/promises";
import { exec, execFile } from "node:child_process"; import { exec, execFile } from "node:child_process";
@@ -323,6 +324,7 @@ type WorkspaceUpdatesSubscriptionState = {
filter?: WorkspaceUpdatesFilter; filter?: WorkspaceUpdatesFilter;
isBootstrapping: boolean; isBootstrapping: boolean;
pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>; pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
lastEmittedByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
}; };
type FetchWorkspacesCursor = { type FetchWorkspacesCursor = {
sort: FetchWorkspacesRequestSort[]; sort: FetchWorkspacesRequestSort[];
@@ -5301,25 +5303,6 @@ export class Session {
return "done"; return "done";
} }
private accumulateLatestActivityAt(
current: string | null,
agent: AgentSnapshotPayload,
): string | null {
const candidateRaw = agent.lastUserMessageAt ?? agent.updatedAt;
const candidateMs = Date.parse(candidateRaw);
if (Number.isNaN(candidateMs)) {
return current;
}
if (!current) {
return new Date(candidateMs).toISOString();
}
const currentMs = Date.parse(current);
if (Number.isNaN(currentMs) || candidateMs > currentMs) {
return new Date(candidateMs).toISOString();
}
return current;
}
private async describeWorkspaceRecord( private async describeWorkspaceRecord(
workspace: PersistedWorkspaceRecord, workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null, projectRecord?: PersistedProjectRecord | null,
@@ -5464,7 +5447,6 @@ export class Session {
if (this.workspaceStatePriority[bucket] < this.workspaceStatePriority[existing.status]) { if (this.workspaceStatePriority[bucket] < this.workspaceStatePriority[existing.status]) {
existing.status = bucket; existing.status = bucket;
} }
existing.activityAt = this.accumulateLatestActivityAt(existing.activityAt, agent);
} }
return descriptorsByWorkspaceId; return descriptorsByWorkspaceId;
@@ -5746,6 +5728,8 @@ export class Session {
subscription.pendingUpdatesByWorkspaceId.set(workspaceId, payload); subscription.pendingUpdatesByWorkspaceId.set(workspaceId, payload);
return; return;
} }
const workspaceId = payload.kind === "upsert" ? payload.workspace.id : payload.id;
subscription.lastEmittedByWorkspaceId.set(workspaceId, payload);
this.emit({ this.emit({
type: "workspace_update", type: "workspace_update",
payload, payload,
@@ -5878,6 +5862,7 @@ export class Session {
: null; : null;
if (!nextWorkspace) { if (!nextWorkspace) {
subscription.lastEmittedByWorkspaceId.delete(workspaceId);
this.bufferOrEmitWorkspaceUpdate(subscription, { this.bufferOrEmitWorkspaceUpdate(subscription, {
kind: "remove", kind: "remove",
id: workspaceId, id: workspaceId,
@@ -5885,10 +5870,17 @@ export class Session {
continue; continue;
} }
this.bufferOrEmitWorkspaceUpdate(subscription, { const nextPayload: WorkspaceUpdatePayload = {
kind: "upsert", kind: "upsert",
workspace: nextWorkspace, workspace: nextWorkspace,
}); };
const lastEmitted = subscription.lastEmittedByWorkspaceId.get(workspaceId);
if (lastEmitted && lastEmitted.kind === "upsert" && equal(lastEmitted.workspace, nextWorkspace)) {
continue;
}
this.bufferOrEmitWorkspaceUpdate(subscription, nextPayload);
} }
if (!options?.skipReconcile) { if (!options?.skipReconcile) {
@@ -6001,6 +5993,7 @@ export class Session {
filter: request.filter, filter: request.filter,
isBootstrapping: true, isBootstrapping: true,
pendingUpdatesByWorkspaceId: new Map(), pendingUpdatesByWorkspaceId: new Map(),
lastEmittedByWorkspaceId: new Map(),
}; };
} }