fix(agent-graph): stabilize startup slots and launch hud
This commit is contained in:
parent
19463edfc9
commit
0b97cc0794
7 changed files with 451 additions and 156 deletions
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from '@shared/utils/idleNotificationSemantics';
|
||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout';
|
||||
|
||||
import {
|
||||
buildInlineActivityEntries,
|
||||
|
|
@ -247,10 +248,11 @@ export class TeamGraphAdapter {
|
|||
const visibleMemberByStableOwnerId = new Map(
|
||||
visibleMembers.map((member) => [getGraphStableOwnerId(member), member] as const)
|
||||
);
|
||||
const assignedStableOwnerIds = new Set(Object.keys(slotAssignments ?? {}));
|
||||
const configStableOwnerIds = new Set(
|
||||
(data.config.members ?? []).map((member) => getGraphStableOwnerId(member))
|
||||
const canonicalVisibleOwnerIds = buildOrderedVisibleTeamGraphOwnerIds(
|
||||
data.members,
|
||||
data.config.members ?? []
|
||||
);
|
||||
const assignedStableOwnerIds = new Set(Object.keys(slotAssignments ?? {}));
|
||||
|
||||
const pushMember = (member: TeamData['members'][number] | undefined): void => {
|
||||
if (!member) {
|
||||
|
|
@ -264,44 +266,26 @@ export class TeamGraphAdapter {
|
|||
ownerOrder.push(nodeId);
|
||||
};
|
||||
|
||||
const assignedVisibleMembersOutsideConfig = visibleMembers
|
||||
.filter((member) => {
|
||||
const stableOwnerId = getGraphStableOwnerId(member);
|
||||
return (
|
||||
assignedStableOwnerIds.has(stableOwnerId) && !configStableOwnerIds.has(stableOwnerId)
|
||||
);
|
||||
})
|
||||
.toSorted((left, right) =>
|
||||
getGraphStableOwnerId(left).localeCompare(getGraphStableOwnerId(right))
|
||||
);
|
||||
|
||||
for (const configMember of data.config.members ?? []) {
|
||||
const stableOwnerId = getGraphStableOwnerId(configMember);
|
||||
for (const stableOwnerId of canonicalVisibleOwnerIds) {
|
||||
const visibleMember = visibleMemberByStableOwnerId.get(stableOwnerId);
|
||||
if (!visibleMember) {
|
||||
continue;
|
||||
}
|
||||
if (!assignedStableOwnerIds.has(stableOwnerId)) {
|
||||
continue;
|
||||
}
|
||||
pushMember(visibleMemberByStableOwnerId.get(stableOwnerId));
|
||||
}
|
||||
|
||||
for (const member of assignedVisibleMembersOutsideConfig) {
|
||||
pushMember(member);
|
||||
}
|
||||
|
||||
for (const configMember of data.config.members ?? []) {
|
||||
const stableOwnerId = getGraphStableOwnerId(configMember);
|
||||
if (assignedStableOwnerIds.has(stableOwnerId)) {
|
||||
continue;
|
||||
}
|
||||
const visibleMember = visibleMemberByStableOwnerId.get(stableOwnerId);
|
||||
pushMember(visibleMember);
|
||||
}
|
||||
|
||||
const remainingMembers = visibleMembers.toSorted((left, right) =>
|
||||
getGraphStableOwnerId(left).localeCompare(getGraphStableOwnerId(right))
|
||||
);
|
||||
|
||||
for (const member of remainingMembers) {
|
||||
pushMember(member);
|
||||
for (const stableOwnerId of canonicalVisibleOwnerIds) {
|
||||
const visibleMember = visibleMemberByStableOwnerId.get(stableOwnerId);
|
||||
if (!visibleMember) {
|
||||
continue;
|
||||
}
|
||||
if (assignedStableOwnerIds.has(stableOwnerId)) {
|
||||
continue;
|
||||
}
|
||||
pushMember(visibleMember);
|
||||
}
|
||||
|
||||
const normalizedAssignments: Record<string, GraphOwnerSlotAssignment> = {};
|
||||
|
|
|
|||
|
|
@ -3,17 +3,16 @@
|
|||
* Thin wrapper — instantiates the class adapter and calls adapt() with store data.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||
|
||||
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
|
||||
import { useStore } from '@renderer/store';
|
||||
import {
|
||||
getDefaultTeamGraphSlotAssignmentsForMembers,
|
||||
getCurrentProvisioningProgressForTeam,
|
||||
hasAppliedDefaultTeamGraphSlotAssignments,
|
||||
isTeamGraphSlotPersistenceDisabled,
|
||||
selectTeamDataForName,
|
||||
} from '@renderer/store/slices/teamSlice';
|
||||
import { buildTeamGraphDefaultLayoutSeed } from '@shared/utils/teamGraphDefaultLayout';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { TeamGraphAdapter } from '../adapters/TeamGraphAdapter';
|
||||
|
|
@ -35,6 +34,7 @@ export function useTeamGraphAdapter(teamName: string): GraphDataPort {
|
|||
provisioningProgress,
|
||||
memberSpawnSnapshot,
|
||||
slotAssignments,
|
||||
graphLayoutSession,
|
||||
ensureTeamGraphSlotAssignments,
|
||||
} = useStore(
|
||||
useShallow((s) => ({
|
||||
|
|
@ -49,6 +49,7 @@ export function useTeamGraphAdapter(teamName: string): GraphDataPort {
|
|||
provisioningProgress: teamName ? getCurrentProvisioningProgressForTeam(s, teamName) : null,
|
||||
memberSpawnSnapshot: teamName ? s.memberSpawnSnapshotsByTeam[teamName] : undefined,
|
||||
slotAssignments: teamName ? s.slotAssignmentsByTeam[teamName] : undefined,
|
||||
graphLayoutSession: teamName ? s.graphLayoutSessionByTeam[teamName] : undefined,
|
||||
ensureTeamGraphSlotAssignments: s.ensureTeamGraphSlotAssignments,
|
||||
}))
|
||||
);
|
||||
|
|
@ -72,18 +73,44 @@ export function useTeamGraphAdapter(teamName: string): GraphDataPort {
|
|||
if (!isTeamGraphSlotPersistenceDisabled()) {
|
||||
return slotAssignments;
|
||||
}
|
||||
if (hasAppliedDefaultTeamGraphSlotAssignments(teamName)) {
|
||||
if (graphLayoutSession?.mode === 'manual') {
|
||||
return slotAssignments;
|
||||
}
|
||||
const defaults = getDefaultTeamGraphSlotAssignmentsForMembers(teamData.members);
|
||||
return Object.keys(defaults).length === 0 ? undefined : defaults;
|
||||
}, [slotAssignments, teamData, teamName]);
|
||||
const defaultSeed = buildTeamGraphDefaultLayoutSeed(
|
||||
teamData.members,
|
||||
teamData.config.members ?? []
|
||||
);
|
||||
const defaultAssignments =
|
||||
Object.keys(defaultSeed.assignments).length === 0 ? undefined : defaultSeed.assignments;
|
||||
if (!slotAssignments) {
|
||||
return defaultAssignments;
|
||||
}
|
||||
if (graphLayoutSession?.signature !== defaultSeed.signature) {
|
||||
return defaultAssignments;
|
||||
}
|
||||
const visibleAssignmentKeys = defaultSeed.orderedVisibleOwnerIds.filter(
|
||||
(stableOwnerId) => slotAssignments[stableOwnerId]
|
||||
);
|
||||
const hasExactVisibleDefaults =
|
||||
visibleAssignmentKeys.length === Object.keys(defaultSeed.assignments).length &&
|
||||
visibleAssignmentKeys.every((stableOwnerId) => {
|
||||
const currentAssignment = slotAssignments[stableOwnerId];
|
||||
const defaultAssignment = defaultSeed.assignments[stableOwnerId];
|
||||
return (
|
||||
currentAssignment &&
|
||||
defaultAssignment &&
|
||||
currentAssignment.ringIndex === defaultAssignment.ringIndex &&
|
||||
currentAssignment.sectorIndex === defaultAssignment.sectorIndex
|
||||
);
|
||||
});
|
||||
return hasExactVisibleDefaults ? slotAssignments : defaultAssignments;
|
||||
}, [graphLayoutSession, slotAssignments, teamData]);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!teamName || !teamData) {
|
||||
return;
|
||||
}
|
||||
ensureTeamGraphSlotAssignments(teamName, teamData.members);
|
||||
ensureTeamGraphSlotAssignments(teamName, teamData.members, teamData.config.members ?? []);
|
||||
}, [ensureTeamGraphSlotAssignments, teamData, teamName]);
|
||||
|
||||
return useMemo(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@renderer/components/ui/dialog';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
|
||||
import type { TeamProvisioningPresentation } from '@renderer/utils/teamProvisioningPresentation';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
|
@ -38,29 +37,6 @@ function shouldRenderLaunchHud(presentation: TeamProvisioningPresentation | null
|
|||
return presentation != null;
|
||||
}
|
||||
|
||||
function getToneClasses(tone: TeamProvisioningPresentation['compactTone']): {
|
||||
border: string;
|
||||
} {
|
||||
switch (tone) {
|
||||
case 'error':
|
||||
return {
|
||||
border: 'border-red-400/35 bg-[rgba(26,10,16,0.9)]',
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
border: 'border-amber-400/35 bg-[rgba(31,18,8,0.9)]',
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
border: 'border-emerald-400/35 bg-[rgba(8,24,18,0.9)]',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
border: 'border-cyan-400/25 bg-[rgba(8,14,26,0.9)]',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface GraphProvisioningHudProps {
|
||||
teamName: string;
|
||||
enabled?: boolean;
|
||||
|
|
@ -74,7 +50,6 @@ export const GraphProvisioningHud = ({
|
|||
const lastActiveStepRef = useRef(-1);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const shouldRender = enabled && shouldRenderLaunchHud(presentation);
|
||||
const tone = presentation ? getToneClasses(presentation.compactTone) : null;
|
||||
const errorStepIndex = presentation?.isFailed
|
||||
? lastActiveStepRef.current >= 0
|
||||
? lastActiveStepRef.current
|
||||
|
|
@ -97,7 +72,7 @@ export const GraphProvisioningHud = ({
|
|||
return parts.join(' - ') || 'Open launch details';
|
||||
}, [presentation?.compactDetail, presentation?.compactTitle]);
|
||||
|
||||
if (!shouldRender || !presentation || !tone) {
|
||||
if (!shouldRender || !presentation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -105,22 +80,16 @@ export const GraphProvisioningHud = ({
|
|||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'w-full rounded-xl border px-3 py-2 text-left text-slate-100 shadow-[0_14px_34px_rgba(5,5,16,0.24)] backdrop-blur-xl transition-colors hover:bg-[rgba(12,18,32,0.96)]',
|
||||
tone.border
|
||||
)}
|
||||
className="focus-visible:ring-white/18 w-full rounded-xl bg-transparent px-1 py-0.5 text-left text-slate-100 transition-opacity hover:opacity-95 focus-visible:outline-none focus-visible:ring-1"
|
||||
onClick={() => setDetailsOpen(true)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-lg border border-white/10 bg-[rgba(4,10,20,0.54)] px-2.5 py-1.5"
|
||||
style={HUD_STEPPER_STYLE}
|
||||
>
|
||||
<div className="px-1 py-0.5" style={HUD_STEPPER_STYLE}>
|
||||
<StepProgressBar
|
||||
steps={MINI_STEPS}
|
||||
currentIndex={presentation.currentStepIndex}
|
||||
errorIndex={errorStepIndex}
|
||||
className="w-full origin-top scale-[0.9]"
|
||||
className="w-full origin-top scale-[0.88]"
|
||||
/>
|
||||
</div>
|
||||
<span className="sr-only">{ariaLabel}</span>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { isLeadMember } from '@shared/utils/leadDetection';
|
|||
import { createLogger } from '@shared/utils/logger';
|
||||
import { getTaskKanbanColumn } from '@shared/utils/reviewState';
|
||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||
import { buildTeamGraphDefaultLayoutSeed } from '@shared/utils/teamGraphDefaultLayout';
|
||||
import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId';
|
||||
|
||||
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
||||
|
|
@ -83,33 +84,20 @@ const teamRefreshBurstDiagnostics = new Map<
|
|||
{ windowStartedAt: number; count: number; lastWarnAt: number }
|
||||
>();
|
||||
const memberSpawnUiEqualLastWarnAtByTeam = new Map<string, number>();
|
||||
const sessionDefaultGraphSlotAssignmentsAppliedByTeam = new Set<string>();
|
||||
interface RefreshTeamDataOptions {
|
||||
withDedup?: boolean;
|
||||
}
|
||||
|
||||
type TeamGraphSlotAssignments = Record<string, GraphOwnerSlotAssignment>;
|
||||
type TeamGraphMemberSeedInput = Pick<TeamData['members'][number], 'name' | 'agentId' | 'removedAt'>;
|
||||
|
||||
const SMALL_TEAM_CARDINAL_SLOT_PRESETS: ReadonlyArray<ReadonlyArray<GraphOwnerSlotAssignment>> = [
|
||||
[],
|
||||
[{ ringIndex: 0, sectorIndex: 0 }],
|
||||
[
|
||||
{ ringIndex: 0, sectorIndex: 0 },
|
||||
{ ringIndex: 0, sectorIndex: 1 },
|
||||
],
|
||||
[
|
||||
{ ringIndex: 0, sectorIndex: 0 },
|
||||
{ ringIndex: 0, sectorIndex: 1 },
|
||||
{ ringIndex: 0, sectorIndex: 2 },
|
||||
],
|
||||
[
|
||||
{ ringIndex: 0, sectorIndex: 0 },
|
||||
{ ringIndex: 0, sectorIndex: 1 },
|
||||
{ ringIndex: 0, sectorIndex: 2 },
|
||||
{ ringIndex: 0, sectorIndex: 3 },
|
||||
],
|
||||
];
|
||||
type TeamGraphConfigMemberSeedInput = Pick<
|
||||
NonNullable<TeamData['config']['members']>[number],
|
||||
'name' | 'agentId' | 'removedAt'
|
||||
>;
|
||||
type TeamGraphLayoutSessionState = {
|
||||
mode: 'default' | 'manual';
|
||||
signature: string | null;
|
||||
};
|
||||
|
||||
export function isTeamDataRefreshPending(teamName: string): boolean {
|
||||
return (
|
||||
|
|
@ -131,7 +119,6 @@ export function __resetTeamSliceModuleStateForTests(): void {
|
|||
memberSpawnStatusesIpcBackoffUntilByTeam.clear();
|
||||
teamRefreshBurstDiagnostics.clear();
|
||||
memberSpawnUiEqualLastWarnAtByTeam.clear();
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.clear();
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
|
|
@ -997,14 +984,18 @@ function migrateStableSlotAssignmentsForMembers(
|
|||
|
||||
function seedStableSlotAssignmentsForMembers(
|
||||
assignments: TeamGraphSlotAssignments,
|
||||
members: readonly TeamGraphMemberSeedInput[]
|
||||
members: readonly TeamGraphMemberSeedInput[],
|
||||
configMembers: readonly TeamGraphConfigMemberSeedInput[] = []
|
||||
): { assignments: TeamGraphSlotAssignments; changed: boolean } {
|
||||
const visibleMembers = members.filter((member) => !member.removedAt && !isLeadMember(member));
|
||||
if (visibleMembers.length === 0 || visibleMembers.length > 4) {
|
||||
const defaultSeed = buildTeamGraphDefaultLayoutSeed(members, configMembers);
|
||||
if (
|
||||
defaultSeed.orderedVisibleOwnerIds.length === 0 ||
|
||||
Object.keys(defaultSeed.assignments).length === 0
|
||||
) {
|
||||
return { assignments, changed: false };
|
||||
}
|
||||
|
||||
const visibleStableOwnerIds = visibleMembers.map((member) => getStableTeamOwnerId(member));
|
||||
const visibleStableOwnerIds = defaultSeed.orderedVisibleOwnerIds;
|
||||
const hasAnyVisibleAssignments = visibleStableOwnerIds.some(
|
||||
(stableOwnerId) => assignments[stableOwnerId] != null
|
||||
);
|
||||
|
|
@ -1012,14 +1003,9 @@ function seedStableSlotAssignmentsForMembers(
|
|||
return { assignments, changed: false };
|
||||
}
|
||||
|
||||
const preset = SMALL_TEAM_CARDINAL_SLOT_PRESETS[visibleMembers.length];
|
||||
if (!preset || preset.length !== visibleMembers.length) {
|
||||
return { assignments, changed: false };
|
||||
}
|
||||
|
||||
const nextAssignments: TeamGraphSlotAssignments = { ...assignments };
|
||||
visibleMembers.forEach((member, index) => {
|
||||
nextAssignments[getStableTeamOwnerId(member)] = preset[index]!;
|
||||
visibleStableOwnerIds.forEach((stableOwnerId) => {
|
||||
nextAssignments[stableOwnerId] = defaultSeed.assignments[stableOwnerId]!;
|
||||
});
|
||||
|
||||
return { assignments: nextAssignments, changed: true };
|
||||
|
|
@ -1049,20 +1035,47 @@ function areTeamGraphSlotAssignmentsEqual(
|
|||
return true;
|
||||
}
|
||||
|
||||
export function getDefaultTeamGraphSlotAssignmentsForMembers(
|
||||
members: readonly TeamGraphMemberSeedInput[]
|
||||
function normalizeTeamGraphSlotAssignmentsForVisibleOwners(
|
||||
assignments: TeamGraphSlotAssignments | undefined,
|
||||
visibleOwnerIds: readonly string[]
|
||||
): TeamGraphSlotAssignments {
|
||||
return seedStableSlotAssignmentsForMembers({}, members).assignments;
|
||||
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 normalizedAssignments;
|
||||
}
|
||||
|
||||
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 getDefaultTeamGraphSlotAssignmentsForMembers(
|
||||
members: readonly TeamGraphMemberSeedInput[],
|
||||
configMembers: readonly TeamGraphConfigMemberSeedInput[] = []
|
||||
): TeamGraphSlotAssignments {
|
||||
return buildTeamGraphDefaultLayoutSeed(members, configMembers).assignments;
|
||||
}
|
||||
|
||||
export function isTeamGraphSlotPersistenceDisabled(): boolean {
|
||||
return DISABLE_PERSISTED_TEAM_GRAPH_SLOT_ASSIGNMENTS;
|
||||
}
|
||||
|
||||
export function hasAppliedDefaultTeamGraphSlotAssignments(teamName: string): boolean {
|
||||
return sessionDefaultGraphSlotAssignmentsAppliedByTeam.has(teamName);
|
||||
}
|
||||
|
||||
function isVisibleInActiveTeamSurface(
|
||||
state: Pick<AppState, 'paneLayout'>,
|
||||
teamName: string | null | undefined
|
||||
|
|
@ -1126,6 +1139,7 @@ export interface TeamSlice {
|
|||
teamDataCacheByName: Record<string, TeamData>;
|
||||
slotLayoutVersion: string;
|
||||
slotAssignmentsByTeam: Record<string, TeamGraphSlotAssignments>;
|
||||
graphLayoutSessionByTeam: Record<string, TeamGraphLayoutSessionState>;
|
||||
selectedTeamLoading: boolean;
|
||||
selectedTeamLoadNonce: number;
|
||||
selectedTeamError: string | null;
|
||||
|
|
@ -1170,7 +1184,8 @@ export interface TeamSlice {
|
|||
clearKanbanFilter: () => void;
|
||||
ensureTeamGraphSlotAssignments: (
|
||||
teamName: string,
|
||||
members: readonly TeamGraphMemberSeedInput[]
|
||||
members: readonly TeamGraphMemberSeedInput[],
|
||||
configMembers?: readonly TeamGraphConfigMemberSeedInput[]
|
||||
) => void;
|
||||
setTeamGraphOwnerSlotAssignment: (
|
||||
teamName: string,
|
||||
|
|
@ -1443,6 +1458,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
teamDataCacheByName: {},
|
||||
slotLayoutVersion: GRAPH_STABLE_SLOT_LAYOUT_VERSION,
|
||||
slotAssignmentsByTeam: {},
|
||||
graphLayoutSessionByTeam: {},
|
||||
selectedTeamLoading: false,
|
||||
selectedTeamLoadNonce: 0,
|
||||
selectedTeamError: null,
|
||||
|
|
@ -1862,33 +1878,72 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
set({ kanbanFilterQuery: null });
|
||||
},
|
||||
|
||||
ensureTeamGraphSlotAssignments: (teamName, members) => {
|
||||
ensureTeamGraphSlotAssignments: (teamName, members, configMembers = []) => {
|
||||
set((state) => {
|
||||
const nextState: Partial<TeamSlice> = {};
|
||||
let changed = false;
|
||||
|
||||
let nextSlotAssignmentsByTeam = state.slotAssignmentsByTeam;
|
||||
let nextGraphLayoutSessionByTeam = state.graphLayoutSessionByTeam;
|
||||
if (state.slotLayoutVersion !== GRAPH_STABLE_SLOT_LAYOUT_VERSION) {
|
||||
nextState.slotLayoutVersion = GRAPH_STABLE_SLOT_LAYOUT_VERSION;
|
||||
nextSlotAssignmentsByTeam = {};
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.clear();
|
||||
nextGraphLayoutSessionByTeam = {};
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const defaultSeed = buildTeamGraphDefaultLayoutSeed(members, configMembers);
|
||||
const visibleAssignments = pruneTeamGraphSlotAssignmentsForVisibleOwners(
|
||||
nextSlotAssignmentsByTeam[teamName],
|
||||
defaultSeed.orderedVisibleOwnerIds
|
||||
);
|
||||
const currentSession = nextGraphLayoutSessionByTeam[teamName];
|
||||
|
||||
if (DISABLE_PERSISTED_TEAM_GRAPH_SLOT_ASSIGNMENTS) {
|
||||
if (!sessionDefaultGraphSlotAssignmentsAppliedByTeam.has(teamName)) {
|
||||
const currentAssignments = nextSlotAssignmentsByTeam[teamName];
|
||||
const defaultAssignments = getDefaultTeamGraphSlotAssignmentsForMembers(members);
|
||||
if (!areTeamGraphSlotAssignmentsEqual(currentAssignments, defaultAssignments)) {
|
||||
if (currentSession?.mode === 'manual') {
|
||||
if (
|
||||
!areTeamGraphSlotAssignmentsEqual(
|
||||
nextSlotAssignmentsByTeam[teamName],
|
||||
visibleAssignments
|
||||
)
|
||||
) {
|
||||
nextSlotAssignmentsByTeam = { ...nextSlotAssignmentsByTeam };
|
||||
if (Object.keys(defaultAssignments).length === 0) {
|
||||
delete nextSlotAssignmentsByTeam[teamName];
|
||||
if (visibleAssignments) {
|
||||
nextSlotAssignmentsByTeam[teamName] = visibleAssignments;
|
||||
} else {
|
||||
nextSlotAssignmentsByTeam[teamName] = defaultAssignments;
|
||||
delete nextSlotAssignmentsByTeam[teamName];
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.add(teamName);
|
||||
} else {
|
||||
if (
|
||||
!areTeamGraphSlotAssignmentsEqual(
|
||||
nextSlotAssignmentsByTeam[teamName],
|
||||
visibleAssignments
|
||||
) ||
|
||||
!areTeamGraphSlotAssignmentsEqual(visibleAssignments, defaultSeed.assignments)
|
||||
) {
|
||||
nextSlotAssignmentsByTeam = { ...nextSlotAssignmentsByTeam };
|
||||
if (Object.keys(defaultSeed.assignments).length === 0) {
|
||||
delete nextSlotAssignmentsByTeam[teamName];
|
||||
} else {
|
||||
nextSlotAssignmentsByTeam[teamName] = defaultSeed.assignments;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
currentSession?.mode !== 'default' ||
|
||||
currentSession?.signature !== defaultSeed.signature
|
||||
) {
|
||||
nextGraphLayoutSessionByTeam = {
|
||||
...nextGraphLayoutSessionByTeam,
|
||||
[teamName]: {
|
||||
mode: 'default',
|
||||
signature: defaultSeed.signature,
|
||||
},
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
|
|
@ -1896,12 +1951,17 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
}
|
||||
|
||||
nextState.slotAssignmentsByTeam = nextSlotAssignmentsByTeam;
|
||||
nextState.graphLayoutSessionByTeam = nextGraphLayoutSessionByTeam;
|
||||
return nextState;
|
||||
}
|
||||
|
||||
const currentAssignments = nextSlotAssignmentsByTeam[teamName];
|
||||
const migrated = migrateStableSlotAssignmentsForMembers(currentAssignments, members);
|
||||
const seeded = seedStableSlotAssignmentsForMembers(migrated.assignments, members);
|
||||
const seeded = seedStableSlotAssignmentsForMembers(
|
||||
migrated.assignments,
|
||||
members,
|
||||
configMembers
|
||||
);
|
||||
if (migrated.changed || seeded.changed) {
|
||||
nextSlotAssignmentsByTeam = {
|
||||
...nextSlotAssignmentsByTeam,
|
||||
|
|
@ -1915,6 +1975,9 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
}
|
||||
|
||||
nextState.slotAssignmentsByTeam = nextSlotAssignmentsByTeam;
|
||||
if (nextGraphLayoutSessionByTeam !== state.graphLayoutSessionByTeam) {
|
||||
nextState.graphLayoutSessionByTeam = nextGraphLayoutSessionByTeam;
|
||||
}
|
||||
return nextState;
|
||||
});
|
||||
},
|
||||
|
|
@ -1952,6 +2015,13 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
[stableOwnerId]: assignment,
|
||||
},
|
||||
},
|
||||
graphLayoutSessionByTeam: {
|
||||
...state.graphLayoutSessionByTeam,
|
||||
[teamName]: {
|
||||
mode: 'manual',
|
||||
signature: state.graphLayoutSessionByTeam[teamName]?.signature ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
@ -2012,6 +2082,13 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
...state.slotAssignmentsByTeam,
|
||||
[teamName]: nextAssignments,
|
||||
},
|
||||
graphLayoutSessionByTeam: {
|
||||
...state.graphLayoutSessionByTeam,
|
||||
[teamName]: {
|
||||
mode: 'manual',
|
||||
signature: state.graphLayoutSessionByTeam[teamName]?.signature ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
@ -2039,6 +2116,13 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
[otherStableOwnerId]: left,
|
||||
},
|
||||
},
|
||||
graphLayoutSessionByTeam: {
|
||||
...state.graphLayoutSessionByTeam,
|
||||
[teamName]: {
|
||||
mode: 'manual',
|
||||
signature: state.graphLayoutSessionByTeam[teamName]?.signature ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
@ -2046,29 +2130,35 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
clearTeamGraphSlotAssignments: (teamName) => {
|
||||
set((state) => {
|
||||
if (!teamName) {
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.clear();
|
||||
if (
|
||||
Object.keys(state.slotAssignmentsByTeam).length === 0 &&
|
||||
state.slotLayoutVersion === GRAPH_STABLE_SLOT_LAYOUT_VERSION
|
||||
state.slotLayoutVersion === GRAPH_STABLE_SLOT_LAYOUT_VERSION &&
|
||||
Object.keys(state.graphLayoutSessionByTeam).length === 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
slotLayoutVersion: GRAPH_STABLE_SLOT_LAYOUT_VERSION,
|
||||
slotAssignmentsByTeam: {},
|
||||
graphLayoutSessionByTeam: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (!(teamName in state.slotAssignmentsByTeam)) {
|
||||
if (
|
||||
!(teamName in state.slotAssignmentsByTeam) &&
|
||||
!(teamName in state.graphLayoutSessionByTeam)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const nextAssignmentsByTeam = { ...state.slotAssignmentsByTeam };
|
||||
const nextGraphLayoutSessionByTeam = { ...state.graphLayoutSessionByTeam };
|
||||
delete nextAssignmentsByTeam[teamName];
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.delete(teamName);
|
||||
delete nextGraphLayoutSessionByTeam[teamName];
|
||||
return {
|
||||
slotLayoutVersion: GRAPH_STABLE_SLOT_LAYOUT_VERSION,
|
||||
slotAssignmentsByTeam: nextAssignmentsByTeam,
|
||||
graphLayoutSessionByTeam: nextGraphLayoutSessionByTeam,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
@ -2090,36 +2180,37 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
}
|
||||
|
||||
const teamData = selectTeamDataForName(state, teamName);
|
||||
const defaultAssignments = teamData
|
||||
? getDefaultTeamGraphSlotAssignmentsForMembers(teamData.members)
|
||||
: {};
|
||||
const defaultSeed = teamData
|
||||
? buildTeamGraphDefaultLayoutSeed(teamData.members, teamData.config.members ?? [])
|
||||
: { orderedVisibleOwnerIds: [], signature: null, assignments: {} };
|
||||
const currentAssignments = state.slotAssignmentsByTeam[teamName];
|
||||
const hasCurrentAssignments =
|
||||
currentAssignments && Object.keys(currentAssignments).length > 0;
|
||||
const currentSession = state.graphLayoutSessionByTeam[teamName];
|
||||
|
||||
if (
|
||||
areTeamGraphSlotAssignmentsEqual(currentAssignments, defaultAssignments) &&
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.has(teamName)
|
||||
areTeamGraphSlotAssignmentsEqual(currentAssignments, defaultSeed.assignments) &&
|
||||
currentSession?.mode === 'default' &&
|
||||
currentSession.signature === defaultSeed.signature
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const nextAssignmentsByTeam = { ...state.slotAssignmentsByTeam };
|
||||
if (Object.keys(defaultAssignments).length === 0) {
|
||||
if (Object.keys(defaultSeed.assignments).length === 0) {
|
||||
delete nextAssignmentsByTeam[teamName];
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.delete(teamName);
|
||||
} else {
|
||||
nextAssignmentsByTeam[teamName] = defaultAssignments;
|
||||
sessionDefaultGraphSlotAssignmentsAppliedByTeam.add(teamName);
|
||||
}
|
||||
|
||||
if (!hasCurrentAssignments && Object.keys(defaultAssignments).length === 0) {
|
||||
return {};
|
||||
nextAssignmentsByTeam[teamName] = defaultSeed.assignments;
|
||||
}
|
||||
|
||||
return {
|
||||
slotLayoutVersion: GRAPH_STABLE_SLOT_LAYOUT_VERSION,
|
||||
slotAssignmentsByTeam: nextAssignmentsByTeam,
|
||||
graphLayoutSessionByTeam: {
|
||||
...state.graphLayoutSessionByTeam,
|
||||
[teamName]: {
|
||||
mode: 'default',
|
||||
signature: defaultSeed.signature,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
|||
97
src/shared/utils/teamGraphDefaultLayout.ts
Normal file
97
src/shared/utils/teamGraphDefaultLayout.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId';
|
||||
|
||||
import type { GraphOwnerSlotAssignment } from '@claude-teams/agent-graph';
|
||||
|
||||
export interface TeamGraphDefaultLayoutMemberInput {
|
||||
name: string;
|
||||
agentId?: string | null;
|
||||
removedAt?: number | null;
|
||||
}
|
||||
|
||||
export interface TeamGraphDefaultLayoutSeed {
|
||||
orderedVisibleOwnerIds: string[];
|
||||
signature: string | null;
|
||||
assignments: Record<string, GraphOwnerSlotAssignment>;
|
||||
}
|
||||
|
||||
const SMALL_TEAM_CARDINAL_SLOT_PRESETS: ReadonlyArray<ReadonlyArray<GraphOwnerSlotAssignment>> = [
|
||||
[],
|
||||
[{ ringIndex: 0, sectorIndex: 0 }],
|
||||
[
|
||||
{ ringIndex: 0, sectorIndex: 0 },
|
||||
{ ringIndex: 0, sectorIndex: 1 },
|
||||
],
|
||||
[
|
||||
{ ringIndex: 0, sectorIndex: 0 },
|
||||
{ ringIndex: 0, sectorIndex: 1 },
|
||||
{ ringIndex: 0, sectorIndex: 2 },
|
||||
],
|
||||
[
|
||||
{ ringIndex: 0, sectorIndex: 0 },
|
||||
{ ringIndex: 0, sectorIndex: 1 },
|
||||
{ ringIndex: 0, sectorIndex: 2 },
|
||||
{ ringIndex: 0, sectorIndex: 3 },
|
||||
],
|
||||
];
|
||||
|
||||
export function buildOrderedVisibleTeamGraphOwnerIds(
|
||||
members: readonly TeamGraphDefaultLayoutMemberInput[],
|
||||
configMembers: readonly TeamGraphDefaultLayoutMemberInput[] = []
|
||||
): string[] {
|
||||
const visibleMembers = members.filter((member) => !member.removedAt && !isLeadMember(member));
|
||||
if (visibleMembers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const visibleMemberByStableOwnerId = new Map(
|
||||
visibleMembers.map((member) => [getStableTeamOwnerId(member), member] as const)
|
||||
);
|
||||
const orderedVisibleOwnerIds: string[] = [];
|
||||
const seenVisibleOwnerIds = new Set<string>();
|
||||
|
||||
for (const configMember of configMembers) {
|
||||
if (configMember.removedAt || isLeadMember(configMember)) {
|
||||
continue;
|
||||
}
|
||||
const stableOwnerId = getStableTeamOwnerId(configMember);
|
||||
if (
|
||||
!visibleMemberByStableOwnerId.has(stableOwnerId) ||
|
||||
seenVisibleOwnerIds.has(stableOwnerId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
orderedVisibleOwnerIds.push(stableOwnerId);
|
||||
seenVisibleOwnerIds.add(stableOwnerId);
|
||||
}
|
||||
|
||||
const remainingVisibleOwnerIds = visibleMembers
|
||||
.map((member) => getStableTeamOwnerId(member))
|
||||
.filter((stableOwnerId) => !seenVisibleOwnerIds.has(stableOwnerId))
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
orderedVisibleOwnerIds.push(...remainingVisibleOwnerIds);
|
||||
return orderedVisibleOwnerIds;
|
||||
}
|
||||
|
||||
export function buildTeamGraphDefaultLayoutSeed(
|
||||
members: readonly TeamGraphDefaultLayoutMemberInput[],
|
||||
configMembers: readonly TeamGraphDefaultLayoutMemberInput[] = []
|
||||
): TeamGraphDefaultLayoutSeed {
|
||||
const orderedVisibleOwnerIds = buildOrderedVisibleTeamGraphOwnerIds(members, configMembers);
|
||||
const signature = orderedVisibleOwnerIds.length > 0 ? orderedVisibleOwnerIds.join('|') : null;
|
||||
const preset = SMALL_TEAM_CARDINAL_SLOT_PRESETS[orderedVisibleOwnerIds.length];
|
||||
const assignments: Record<string, GraphOwnerSlotAssignment> = {};
|
||||
|
||||
if (preset && preset.length === orderedVisibleOwnerIds.length) {
|
||||
orderedVisibleOwnerIds.forEach((stableOwnerId, index) => {
|
||||
assignments[stableOwnerId] = preset[index]!;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
orderedVisibleOwnerIds,
|
||||
signature,
|
||||
assignments,
|
||||
};
|
||||
}
|
||||
|
|
@ -68,6 +68,65 @@ describe('TeamGraphAdapter particles', () => {
|
|||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('builds ownerOrder from config member order instead of transient member array order', () => {
|
||||
const adapter = TeamGraphAdapter.create();
|
||||
const graph = adapter.adapt(
|
||||
createBaseTeamData({
|
||||
config: {
|
||||
name: 'My Team',
|
||||
members: [{ name: 'team-lead' }, { name: 'alice' }, { name: 'bob' }, { name: 'tom' }],
|
||||
projectPath: '/repo',
|
||||
},
|
||||
members: [
|
||||
{
|
||||
name: 'team-lead',
|
||||
status: 'active',
|
||||
currentTaskId: null,
|
||||
taskCount: 0,
|
||||
lastActiveAt: null,
|
||||
messageCount: 0,
|
||||
agentType: 'team-lead',
|
||||
},
|
||||
{
|
||||
name: 'tom',
|
||||
status: 'active',
|
||||
currentTaskId: null,
|
||||
taskCount: 0,
|
||||
lastActiveAt: null,
|
||||
messageCount: 0,
|
||||
},
|
||||
{
|
||||
name: 'bob',
|
||||
status: 'active',
|
||||
currentTaskId: null,
|
||||
taskCount: 0,
|
||||
lastActiveAt: null,
|
||||
messageCount: 0,
|
||||
},
|
||||
{
|
||||
name: 'alice',
|
||||
status: 'active',
|
||||
currentTaskId: null,
|
||||
taskCount: 0,
|
||||
lastActiveAt: null,
|
||||
messageCount: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
'my-team',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
new Set()
|
||||
);
|
||||
|
||||
expect(graph.layout?.ownerOrder).toEqual([
|
||||
'member:my-team:alice',
|
||||
'member:my-team:bob',
|
||||
'member:my-team:tom',
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a message particle for a new incoming message from the newest message set', () => {
|
||||
const adapter = TeamGraphAdapter.create();
|
||||
const baseline = createBaseTeamData();
|
||||
|
|
|
|||
|
|
@ -236,6 +236,10 @@ describe('teamSlice actions', () => {
|
|||
'agent-alice': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
});
|
||||
expect(store.getState().graphLayoutSessionByTeam['my-team']).toEqual({
|
||||
mode: 'manual',
|
||||
signature: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces persisted slot assignments with defaults while persistence is disabled', () => {
|
||||
|
|
@ -270,6 +274,33 @@ describe('teamSlice actions', () => {
|
|||
{ name: 'jack', agentId: 'agent-jack' },
|
||||
]);
|
||||
|
||||
expect(store.getState().slotAssignmentsByTeam['my-team']).toEqual({
|
||||
'agent-alice': { ringIndex: 0, sectorIndex: 0 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
'agent-jack': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-tom': { ringIndex: 0, sectorIndex: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses config member order instead of transient visible member array order for defaults', () => {
|
||||
const store = createSliceStore();
|
||||
|
||||
store.getState().ensureTeamGraphSlotAssignments(
|
||||
'my-team',
|
||||
[
|
||||
{ name: 'jack', agentId: 'agent-jack' },
|
||||
{ name: 'tom', agentId: 'agent-tom' },
|
||||
{ name: 'alice', agentId: 'agent-alice' },
|
||||
{ name: 'bob', agentId: 'agent-bob' },
|
||||
],
|
||||
[
|
||||
{ name: 'alice', agentId: 'agent-alice' },
|
||||
{ name: 'bob', agentId: 'agent-bob' },
|
||||
{ name: 'tom', agentId: 'agent-tom' },
|
||||
{ name: 'jack', agentId: 'agent-jack' },
|
||||
]
|
||||
);
|
||||
|
||||
expect(store.getState().slotAssignmentsByTeam['my-team']).toEqual({
|
||||
'agent-alice': { ringIndex: 0, sectorIndex: 0 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
|
|
@ -292,8 +323,8 @@ describe('teamSlice actions', () => {
|
|||
expect(store.getState().slotAssignmentsByTeam['my-team']).toEqual({
|
||||
'agent-alice': { ringIndex: 0, sectorIndex: 0 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
'agent-tom': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-jack': { ringIndex: 0, sectorIndex: 3 },
|
||||
'agent-jack': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-tom': { ringIndex: 0, sectorIndex: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -367,7 +398,34 @@ describe('teamSlice actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not reseed a team again after defaults were applied once in the session', () => {
|
||||
it('reseeds defaults again while the team remains in default mode and visible owners change', () => {
|
||||
const store = createSliceStore();
|
||||
|
||||
store.getState().ensureTeamGraphSlotAssignments('my-team', [
|
||||
{ name: 'alice', agentId: 'agent-alice' },
|
||||
{ name: 'bob', agentId: 'agent-bob' },
|
||||
]);
|
||||
|
||||
store.getState().ensureTeamGraphSlotAssignments('my-team', [
|
||||
{ name: 'alice', agentId: 'agent-alice' },
|
||||
{ name: 'bob', agentId: 'agent-bob' },
|
||||
{ name: 'tom', agentId: 'agent-tom' },
|
||||
{ name: 'jack', agentId: 'agent-jack' },
|
||||
]);
|
||||
|
||||
expect(store.getState().slotAssignmentsByTeam['my-team']).toEqual({
|
||||
'agent-alice': { ringIndex: 0, sectorIndex: 0 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
'agent-jack': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-tom': { ringIndex: 0, sectorIndex: 3 },
|
||||
});
|
||||
expect(store.getState().graphLayoutSessionByTeam['my-team']).toEqual({
|
||||
mode: 'default',
|
||||
signature: 'agent-alice|agent-bob|agent-jack|agent-tom',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not reshuffle existing owners after the team enters manual mode', () => {
|
||||
const store = createSliceStore();
|
||||
|
||||
store.getState().ensureTeamGraphSlotAssignments('my-team', [
|
||||
|
|
@ -383,12 +441,18 @@ describe('teamSlice actions', () => {
|
|||
store.getState().ensureTeamGraphSlotAssignments('my-team', [
|
||||
{ name: 'alice', agentId: 'agent-alice' },
|
||||
{ name: 'bob', agentId: 'agent-bob' },
|
||||
{ name: 'tom', agentId: 'agent-tom' },
|
||||
{ name: 'jack', agentId: 'agent-jack' },
|
||||
]);
|
||||
|
||||
expect(store.getState().slotAssignmentsByTeam['my-team']).toEqual({
|
||||
'agent-alice': { ringIndex: 1, sectorIndex: 4 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
});
|
||||
expect(store.getState().graphLayoutSessionByTeam['my-team']).toEqual({
|
||||
mode: 'manual',
|
||||
signature: 'agent-alice|agent-bob',
|
||||
});
|
||||
});
|
||||
|
||||
it('resets graph slot assignments back to defaults when reopening the graph surface', () => {
|
||||
|
|
@ -423,7 +487,7 @@ describe('teamSlice actions', () => {
|
|||
'my-team',
|
||||
'agent-alice',
|
||||
{ ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-tom',
|
||||
'agent-jack',
|
||||
{ ringIndex: 0, sectorIndex: 0 }
|
||||
);
|
||||
|
||||
|
|
@ -432,8 +496,12 @@ describe('teamSlice actions', () => {
|
|||
expect(store.getState().slotAssignmentsByTeam['my-team']).toEqual({
|
||||
'agent-alice': { ringIndex: 0, sectorIndex: 0 },
|
||||
'agent-bob': { ringIndex: 0, sectorIndex: 1 },
|
||||
'agent-tom': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-jack': { ringIndex: 0, sectorIndex: 3 },
|
||||
'agent-jack': { ringIndex: 0, sectorIndex: 2 },
|
||||
'agent-tom': { ringIndex: 0, sectorIndex: 3 },
|
||||
});
|
||||
expect(store.getState().graphLayoutSessionByTeam['my-team']).toEqual({
|
||||
mode: 'default',
|
||||
signature: 'agent-alice|agent-bob|agent-jack|agent-tom',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue