* fix(app): stabilize streamed chat rendering Use deterministic Markdown AST keys and preserve unique row identities when an assistant message resumes after a tool. Keep native gesture and plan-card children correctly keyed to avoid unsupported Fabric reconciliation paths. * fix(app): stabilize retained native panels Keep retained workspace roots in a stable native order and make active panels present in the selection commit. Hidden panels now stop expensive subscriptions and animations without losing mounted state. * fix(app): harden retained panel activity
72 lines
2.2 KiB
TypeScript
72 lines
2.2 KiB
TypeScript
import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
|
|
import { deriveSidebarStateBucket } from "./sidebar-agent-state";
|
|
|
|
export interface WorkspaceAgentActivity {
|
|
agentId: string;
|
|
status: WorkspaceDescriptor["status"];
|
|
enteredAt: Date | null;
|
|
}
|
|
|
|
export function buildWorkspaceAgentActivityIndex(
|
|
agents: ReadonlyMap<string, Agent>,
|
|
previous?: ReadonlyMap<string, WorkspaceAgentActivity>,
|
|
): Map<string, WorkspaceAgentActivity> {
|
|
const activityByWorkspaceId = new Map<string, WorkspaceAgentActivity>();
|
|
const latestActivityAtByWorkspaceId = new Map<string, Date>();
|
|
|
|
for (const agent of agents.values()) {
|
|
if (agent.archivedAt || agent.parentAgentId || !agent.workspaceId) {
|
|
continue;
|
|
}
|
|
|
|
const enteredAt = agent.attentionTimestamp ?? agent.updatedAt;
|
|
const latestActivityAt = latestActivityAtByWorkspaceId.get(agent.workspaceId);
|
|
if (latestActivityAt && enteredAt <= latestActivityAt) {
|
|
continue;
|
|
}
|
|
latestActivityAtByWorkspaceId.set(agent.workspaceId, enteredAt);
|
|
|
|
const status = deriveSidebarStateBucket({
|
|
status: agent.status,
|
|
pendingPermissionCount: agent.pendingPermissions.length,
|
|
requiresAttention: agent.requiresAttention,
|
|
attentionReason: agent.attentionReason,
|
|
});
|
|
activityByWorkspaceId.set(agent.workspaceId, {
|
|
agentId: agent.id,
|
|
status,
|
|
enteredAt,
|
|
});
|
|
}
|
|
|
|
for (const [workspaceId, activity] of activityByWorkspaceId) {
|
|
const previousActivity = previous?.get(workspaceId);
|
|
if (
|
|
previousActivity?.agentId === activity.agentId &&
|
|
previousActivity.status === activity.status
|
|
) {
|
|
activityByWorkspaceId.set(workspaceId, previousActivity);
|
|
}
|
|
}
|
|
|
|
if (previous && areWorkspaceAgentActivityIndexesIdentical(previous, activityByWorkspaceId)) {
|
|
return previous instanceof Map ? previous : new Map(previous);
|
|
}
|
|
return activityByWorkspaceId;
|
|
}
|
|
|
|
function areWorkspaceAgentActivityIndexesIdentical(
|
|
previous: ReadonlyMap<string, WorkspaceAgentActivity>,
|
|
next: ReadonlyMap<string, WorkspaceAgentActivity>,
|
|
): boolean {
|
|
if (previous.size !== next.size) {
|
|
return false;
|
|
}
|
|
for (const [workspaceId, activity] of next) {
|
|
if (previous.get(workspaceId) !== activity) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|