219 lines
7.1 KiB
TypeScript
219 lines
7.1 KiB
TypeScript
import { buildTeamGraphDefaultLayoutSeed } from '@shared/utils/teamGraphDefaultLayout';
|
|
import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId';
|
|
|
|
import type { GraphOwnerSlotAssignment } from '@claude-teams/agent-graph';
|
|
import type { TeamMemberSnapshot, TeamViewSnapshot } from '@shared/types';
|
|
|
|
export const GRAPH_STABLE_SLOT_LAYOUT_VERSION = 'stable-slots-v1' as const;
|
|
export const DISABLE_PERSISTED_TEAM_GRAPH_SLOT_ASSIGNMENTS = true;
|
|
|
|
export type TeamGraphSlotAssignments = Record<string, GraphOwnerSlotAssignment>;
|
|
export type TeamGraphMemberSeedInput = Pick<TeamMemberSnapshot, 'name' | 'agentId' | 'removedAt'>;
|
|
export type TeamGraphConfigMemberSeedInput = Pick<
|
|
NonNullable<TeamViewSnapshot['config']['members']>[number],
|
|
'name' | 'agentId' | 'removedAt'
|
|
>;
|
|
|
|
export interface TeamGraphLayoutSessionState {
|
|
mode: 'default' | 'manual';
|
|
signature: string | null;
|
|
}
|
|
|
|
export function migrateStableSlotAssignmentsForMembers(
|
|
assignments: TeamGraphSlotAssignments | undefined,
|
|
members: readonly TeamGraphMemberSeedInput[]
|
|
): { assignments: TeamGraphSlotAssignments; changed: boolean } {
|
|
const nextAssignments: TeamGraphSlotAssignments = { ...(assignments ?? {}) };
|
|
let changed = false;
|
|
|
|
for (const member of members) {
|
|
const fallbackKey = member.name.trim();
|
|
const stableOwnerId = getStableTeamOwnerId(member);
|
|
const fallbackAssignment = nextAssignments[fallbackKey];
|
|
const stableAssignment = nextAssignments[stableOwnerId];
|
|
|
|
if (stableOwnerId !== fallbackKey && fallbackAssignment && !stableAssignment) {
|
|
nextAssignments[stableOwnerId] = fallbackAssignment;
|
|
delete nextAssignments[fallbackKey];
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
if (stableOwnerId !== fallbackKey && fallbackAssignment && stableAssignment) {
|
|
delete nextAssignments[fallbackKey];
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return { assignments: nextAssignments, changed };
|
|
}
|
|
|
|
export function seedStableSlotAssignmentsForMembers(
|
|
assignments: TeamGraphSlotAssignments,
|
|
members: readonly TeamGraphMemberSeedInput[],
|
|
configMembers: readonly TeamGraphConfigMemberSeedInput[] = []
|
|
): { assignments: TeamGraphSlotAssignments; changed: boolean } {
|
|
const defaultSeed = buildTeamGraphDefaultLayoutSeed(members, configMembers);
|
|
if (
|
|
defaultSeed.orderedVisibleOwnerIds.length === 0 ||
|
|
Object.keys(defaultSeed.assignments).length === 0
|
|
) {
|
|
return { assignments, changed: false };
|
|
}
|
|
|
|
const visibleStableOwnerIds = defaultSeed.orderedVisibleOwnerIds;
|
|
const hasAnyVisibleAssignments = visibleStableOwnerIds.some(
|
|
(stableOwnerId) => assignments[stableOwnerId] != null
|
|
);
|
|
if (hasAnyVisibleAssignments) {
|
|
return { assignments, changed: false };
|
|
}
|
|
|
|
const nextAssignments: TeamGraphSlotAssignments = { ...assignments };
|
|
visibleStableOwnerIds.forEach((stableOwnerId) => {
|
|
nextAssignments[stableOwnerId] = defaultSeed.assignments[stableOwnerId]!;
|
|
});
|
|
|
|
return { assignments: nextAssignments, changed: true };
|
|
}
|
|
|
|
export function areTeamGraphSlotAssignmentsEqual(
|
|
left: TeamGraphSlotAssignments | undefined,
|
|
right: TeamGraphSlotAssignments | undefined
|
|
): boolean {
|
|
const leftEntries = Object.entries(left ?? {});
|
|
const rightEntries = Object.entries(right ?? {});
|
|
if (leftEntries.length !== rightEntries.length) {
|
|
return false;
|
|
}
|
|
|
|
for (const [stableOwnerId, leftAssignment] of leftEntries) {
|
|
const rightAssignment = right?.[stableOwnerId];
|
|
if (
|
|
rightAssignment?.ringIndex !== leftAssignment.ringIndex ||
|
|
rightAssignment.sectorIndex !== leftAssignment.sectorIndex
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function normalizeTeamGraphSlotAssignmentsForVisibleOwners(
|
|
assignments: TeamGraphSlotAssignments | undefined,
|
|
visibleOwnerIds: readonly string[]
|
|
): TeamGraphSlotAssignments {
|
|
if (visibleOwnerIds.length === 0 || !assignments) {
|
|
return {};
|
|
}
|
|
|
|
const normalizedAssignments: TeamGraphSlotAssignments = {};
|
|
for (const stableOwnerId of visibleOwnerIds) {
|
|
const assignment = assignments[stableOwnerId];
|
|
if (!assignment) {
|
|
continue;
|
|
}
|
|
normalizedAssignments[stableOwnerId] = assignment;
|
|
}
|
|
return normalizeLegacySixRowOrbitAssignments(normalizedAssignments, visibleOwnerIds);
|
|
}
|
|
|
|
export function normalizeLegacySixRowOrbitAssignments(
|
|
assignments: TeamGraphSlotAssignments,
|
|
visibleOwnerIds: readonly string[]
|
|
): TeamGraphSlotAssignments {
|
|
if (visibleOwnerIds.length !== 6) {
|
|
return assignments;
|
|
}
|
|
|
|
const visibleAssignments = visibleOwnerIds.flatMap((stableOwnerId) => {
|
|
const assignment = assignments[stableOwnerId];
|
|
return assignment ? [assignment] : [];
|
|
});
|
|
const hasLegacyTwoRowBottomMarker = visibleAssignments.some(
|
|
(assignment) => assignment.ringIndex === 1 && assignment.sectorIndex === 2
|
|
);
|
|
let changed = false;
|
|
const normalizedAssignments: TeamGraphSlotAssignments = { ...assignments };
|
|
|
|
for (const stableOwnerId of visibleOwnerIds) {
|
|
const assignment = normalizedAssignments[stableOwnerId];
|
|
if (!assignment) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
hasLegacyTwoRowBottomMarker &&
|
|
assignment.ringIndex === 1 &&
|
|
assignment.sectorIndex >= 0 &&
|
|
assignment.sectorIndex < 3
|
|
) {
|
|
normalizedAssignments[stableOwnerId] = {
|
|
ringIndex: 2,
|
|
sectorIndex: assignment.sectorIndex,
|
|
};
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
if (assignment.ringIndex === 0 && assignment.sectorIndex >= 3 && assignment.sectorIndex < 6) {
|
|
normalizedAssignments[stableOwnerId] = {
|
|
ringIndex: 2,
|
|
sectorIndex: assignment.sectorIndex - 3,
|
|
};
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return changed ? normalizedAssignments : assignments;
|
|
}
|
|
|
|
export function pruneTeamGraphSlotAssignmentsForVisibleOwners(
|
|
assignments: TeamGraphSlotAssignments | undefined,
|
|
visibleOwnerIds: readonly string[]
|
|
): TeamGraphSlotAssignments | undefined {
|
|
const normalizedAssignments = normalizeTeamGraphSlotAssignmentsForVisibleOwners(
|
|
assignments,
|
|
visibleOwnerIds
|
|
);
|
|
return Object.keys(normalizedAssignments).length > 0 ? normalizedAssignments : undefined;
|
|
}
|
|
|
|
export function normalizeTeamGraphGridOwnerOrder(
|
|
order: readonly string[] | undefined,
|
|
visibleOwnerIds: readonly string[]
|
|
): string[] {
|
|
const visibleOwnerIdSet = new Set(visibleOwnerIds);
|
|
const normalizedOrder: string[] = [];
|
|
const seenOwnerIds = new Set<string>();
|
|
|
|
for (const stableOwnerId of order ?? []) {
|
|
if (!visibleOwnerIdSet.has(stableOwnerId) || seenOwnerIds.has(stableOwnerId)) {
|
|
continue;
|
|
}
|
|
normalizedOrder.push(stableOwnerId);
|
|
seenOwnerIds.add(stableOwnerId);
|
|
}
|
|
|
|
for (const stableOwnerId of visibleOwnerIds) {
|
|
if (seenOwnerIds.has(stableOwnerId)) {
|
|
continue;
|
|
}
|
|
normalizedOrder.push(stableOwnerId);
|
|
seenOwnerIds.add(stableOwnerId);
|
|
}
|
|
|
|
return normalizedOrder;
|
|
}
|
|
|
|
export function getDefaultTeamGraphSlotAssignmentsForMembers(
|
|
members: readonly TeamGraphMemberSeedInput[],
|
|
configMembers: readonly TeamGraphConfigMemberSeedInput[] = []
|
|
): TeamGraphSlotAssignments {
|
|
return buildTeamGraphDefaultLayoutSeed(members, configMembers).assignments;
|
|
}
|
|
|
|
export function isTeamGraphSlotPersistenceDisabled(): boolean {
|
|
return DISABLE_PERSISTED_TEAM_GRAPH_SLOT_ASSIGNMENTS;
|
|
}
|