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:
Generated
+1
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user