perf(renderer): split noisy team detail subscriptions
This commit is contained in:
parent
0656794026
commit
affd9ac748
1 changed files with 1174 additions and 1058 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { api } from '@renderer/api';
|
||||
import { SessionContextPanel } from '@renderer/components/chat/SessionContextPanel/index';
|
||||
|
|
@ -265,6 +266,160 @@ const TeamOfflineStatusBanner = memo(function TeamOfflineStatusBanner({
|
|||
);
|
||||
});
|
||||
|
||||
type TeamMessagesPanelBridgeProps = Omit<
|
||||
ComponentProps<typeof MessagesPanel>,
|
||||
'leadActivity' | 'leadContextUpdatedAt'
|
||||
>;
|
||||
type SharedTeamMessagesPanelProps = Omit<TeamMessagesPanelBridgeProps, 'position'>;
|
||||
type TeamMemberListBridgeProps = Omit<
|
||||
ComponentProps<typeof MemberList>,
|
||||
'leadActivity' | 'memberSpawnStatuses'
|
||||
> & {
|
||||
teamName: string;
|
||||
};
|
||||
type TeamMemberDetailDialogBridgeProps = Omit<
|
||||
ComponentProps<typeof MemberDetailDialog>,
|
||||
'leadActivity' | 'spawnEntry'
|
||||
>;
|
||||
type TeamSidebarRailBridgeProps = Omit<
|
||||
ComponentProps<typeof TeamSidebarRail>,
|
||||
'messagesPanelProps'
|
||||
> & {
|
||||
messagesPanelProps: SharedTeamMessagesPanelProps;
|
||||
};
|
||||
|
||||
function buildMemberSpawnStatusMap(
|
||||
memberSpawnStatuses: Record<string, MemberSpawnStatusEntry> | undefined
|
||||
): Map<string, MemberSpawnStatusEntry> | undefined {
|
||||
if (!memberSpawnStatuses) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const map = new Map<string, MemberSpawnStatusEntry>(Object.entries(memberSpawnStatuses));
|
||||
return map.size > 0 ? map : undefined;
|
||||
}
|
||||
|
||||
const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
|
||||
teamName,
|
||||
isTeamProvisioning,
|
||||
isTeamAlive,
|
||||
}: {
|
||||
teamName: string;
|
||||
isTeamProvisioning: boolean;
|
||||
isTeamAlive?: boolean;
|
||||
}): null {
|
||||
const { leadActivity, memberSpawnStatuses, fetchMemberSpawnStatuses } = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
|
||||
fetchMemberSpawnStatuses: s.fetchMemberSpawnStatuses,
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldFetchSpawnStatuses =
|
||||
isTeamProvisioning ||
|
||||
(memberSpawnStatuses == null &&
|
||||
(isTeamAlive === true || leadActivity === 'active' || leadActivity === 'idle'));
|
||||
if (shouldFetchSpawnStatuses) {
|
||||
void fetchMemberSpawnStatuses(teamName);
|
||||
}
|
||||
}, [
|
||||
fetchMemberSpawnStatuses,
|
||||
isTeamAlive,
|
||||
isTeamProvisioning,
|
||||
leadActivity,
|
||||
memberSpawnStatuses,
|
||||
teamName,
|
||||
]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
||||
teamName,
|
||||
...props
|
||||
}: TeamMemberListBridgeProps): React.JSX.Element {
|
||||
const { leadActivity, memberSpawnStatuses } = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
|
||||
}))
|
||||
);
|
||||
const memberSpawnStatusMap = useMemo(
|
||||
() => buildMemberSpawnStatusMap(memberSpawnStatuses),
|
||||
[memberSpawnStatuses]
|
||||
);
|
||||
|
||||
return (
|
||||
<MemberList {...props} leadActivity={leadActivity} memberSpawnStatuses={memberSpawnStatusMap} />
|
||||
);
|
||||
});
|
||||
|
||||
const TeamMessagesPanelBridge = memo(function TeamMessagesPanelBridge({
|
||||
teamName,
|
||||
...props
|
||||
}: TeamMessagesPanelBridgeProps): React.JSX.Element {
|
||||
const { leadActivity, leadContextUpdatedAt } = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
leadContextUpdatedAt: s.leadContextByTeam[teamName]?.updatedAt,
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<MessagesPanel
|
||||
{...props}
|
||||
teamName={teamName}
|
||||
leadActivity={leadActivity}
|
||||
leadContextUpdatedAt={leadContextUpdatedAt}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const TeamSidebarRailBridge = memo(function TeamSidebarRailBridge({
|
||||
messagesPanelProps,
|
||||
...props
|
||||
}: TeamSidebarRailBridgeProps): React.JSX.Element {
|
||||
const { leadActivity, leadContextUpdatedAt } = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[messagesPanelProps.teamName],
|
||||
leadContextUpdatedAt: s.leadContextByTeam[messagesPanelProps.teamName]?.updatedAt,
|
||||
}))
|
||||
);
|
||||
const bridgedMessagesPanelProps = useMemo(
|
||||
() => ({
|
||||
...messagesPanelProps,
|
||||
leadActivity,
|
||||
leadContextUpdatedAt,
|
||||
}),
|
||||
[leadActivity, leadContextUpdatedAt, messagesPanelProps]
|
||||
);
|
||||
|
||||
return <TeamSidebarRail {...props} messagesPanelProps={bridgedMessagesPanelProps} />;
|
||||
});
|
||||
|
||||
const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge({
|
||||
teamName,
|
||||
member,
|
||||
...props
|
||||
}: TeamMemberDetailDialogBridgeProps): React.JSX.Element | null {
|
||||
const leadActivity = useStore((s) => s.leadActivityByTeam[teamName]);
|
||||
const spawnEntry = useStore((s) =>
|
||||
member ? s.memberSpawnStatusesByTeam[teamName]?.[member.name] : undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<MemberDetailDialog
|
||||
{...props}
|
||||
teamName={teamName}
|
||||
member={member}
|
||||
leadActivity={leadActivity}
|
||||
spawnEntry={spawnEntry}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const TeamDetailView = ({
|
||||
teamName,
|
||||
isPaneFocused = false,
|
||||
|
|
@ -532,10 +687,6 @@ export const TeamDetailView = ({
|
|||
provisioningError,
|
||||
clearProvisioningError,
|
||||
isTeamProvisioning,
|
||||
leadActivity,
|
||||
leadContextUpdatedAt,
|
||||
memberSpawnStatuses,
|
||||
fetchMemberSpawnStatuses,
|
||||
refreshTeamData,
|
||||
kanbanFilterQuery,
|
||||
clearKanbanFilter,
|
||||
|
|
@ -582,10 +733,6 @@ export const TeamDetailView = ({
|
|||
data: s.selectedTeamName === teamName ? s.selectedTeamData : null,
|
||||
loading: s.selectedTeamName === teamName ? s.selectedTeamLoading : false,
|
||||
error: s.selectedTeamName === teamName ? s.selectedTeamError : null,
|
||||
leadActivity: teamName ? s.leadActivityByTeam[teamName] : undefined,
|
||||
leadContextUpdatedAt: teamName ? s.leadContextByTeam[teamName]?.updatedAt : undefined,
|
||||
memberSpawnStatuses: teamName ? s.memberSpawnStatusesByTeam[teamName] : undefined,
|
||||
fetchMemberSpawnStatuses: s.fetchMemberSpawnStatuses,
|
||||
refreshTeamData: s.refreshTeamData,
|
||||
kanbanFilterQuery: s.kanbanFilterQuery,
|
||||
clearKanbanFilter: s.clearKanbanFilter,
|
||||
|
|
@ -683,52 +830,6 @@ export const TeamDetailView = ({
|
|||
}
|
||||
}, [isTeamProvisioning]);
|
||||
|
||||
// Fetch initial spawn statuses when provisioning starts
|
||||
useEffect(() => {
|
||||
const shouldFetchSpawnStatuses =
|
||||
Boolean(teamName) &&
|
||||
(isTeamProvisioning ||
|
||||
(memberSpawnStatuses == null &&
|
||||
(data?.isAlive === true || leadActivity === 'active' || leadActivity === 'idle')));
|
||||
if (teamName && shouldFetchSpawnStatuses) {
|
||||
void fetchMemberSpawnStatuses(teamName);
|
||||
}
|
||||
}, [
|
||||
data?.isAlive,
|
||||
fetchMemberSpawnStatuses,
|
||||
isTeamProvisioning,
|
||||
leadActivity,
|
||||
memberSpawnStatuses,
|
||||
teamName,
|
||||
]);
|
||||
|
||||
// Convert Record<string, MemberSpawnStatusEntry> → Map<string, MemberSpawnEntry>
|
||||
const memberSpawnStatusMap = useMemo(() => {
|
||||
if (!memberSpawnStatuses) return undefined;
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
status: MemberSpawnStatusEntry['status'];
|
||||
launchState: MemberSpawnStatusEntry['launchState'];
|
||||
error?: string;
|
||||
updatedAt: string;
|
||||
runtimeAlive?: boolean;
|
||||
livenessSource?: MemberSpawnStatusEntry['livenessSource'];
|
||||
}
|
||||
>();
|
||||
for (const [name, entry] of Object.entries(memberSpawnStatuses)) {
|
||||
map.set(name, {
|
||||
status: entry.status,
|
||||
launchState: entry.launchState,
|
||||
error: entry.error,
|
||||
updatedAt: entry.updatedAt,
|
||||
runtimeAlive: entry.runtimeAlive,
|
||||
livenessSource: entry.livenessSource,
|
||||
});
|
||||
}
|
||||
return map.size > 0 ? map : undefined;
|
||||
}, [memberSpawnStatuses]);
|
||||
|
||||
const [kanbanSearch, setKanbanSearch] = useState('');
|
||||
|
||||
// Open editor overlay when a file reveal is requested (e.g. from chip click)
|
||||
|
|
@ -1373,7 +1474,7 @@ export const TeamDetailView = ({
|
|||
})();
|
||||
};
|
||||
|
||||
const sharedMessagesPanelProps = useMemo(
|
||||
const sharedMessagesPanelProps = useMemo<SharedTeamMessagesPanelProps>(
|
||||
() => ({
|
||||
teamName,
|
||||
onTogglePosition: toggleMessagesPanelMode,
|
||||
|
|
@ -1381,8 +1482,6 @@ export const TeamDetailView = ({
|
|||
tasks: data?.tasks ?? [],
|
||||
messages: data?.messages ?? [],
|
||||
isTeamAlive: data?.isAlive,
|
||||
leadActivity,
|
||||
leadContextUpdatedAt,
|
||||
timeWindow,
|
||||
teamSessionIds,
|
||||
currentLeadSessionId: data?.config.leadSessionId,
|
||||
|
|
@ -1407,8 +1506,6 @@ export const TeamDetailView = ({
|
|||
handleRestartTeam,
|
||||
handleSelectMember,
|
||||
handleTaskIdClick,
|
||||
leadActivity,
|
||||
leadContextUpdatedAt,
|
||||
pendingRepliesByMember,
|
||||
teamName,
|
||||
teamSessionIds,
|
||||
|
|
@ -1425,6 +1522,15 @@ export const TeamDetailView = ({
|
|||
);
|
||||
}
|
||||
|
||||
const spawnStatusWatcher = (
|
||||
<TeamSpawnStatusWatcher
|
||||
teamName={teamName}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
isTeamAlive={data?.isAlive}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderBody = (): React.JSX.Element => {
|
||||
if ((loading && !data) || (data && data.teamName !== teamName)) {
|
||||
return (
|
||||
<div className="size-full overflow-auto p-4">
|
||||
|
|
@ -1456,8 +1562,8 @@ export const TeamDetailView = ({
|
|||
<div className="max-w-md text-center">
|
||||
<p className="text-sm font-medium text-text">Team not launched yet</p>
|
||||
<p className="mt-2 text-xs text-text-secondary">
|
||||
This is a draft team - <strong>{draftDisplayName}</strong> has been configured with{' '}
|
||||
{draftMemberCount} member
|
||||
This is a draft team - <strong>{draftDisplayName}</strong> has been configured
|
||||
with {draftMemberCount} member
|
||||
{draftMemberCount === 1 ? '' : 's'} but hasn't been provisioned by CLI yet.
|
||||
Click Launch to select a model and start the team.
|
||||
</p>
|
||||
|
|
@ -1551,7 +1657,9 @@ export const TeamDetailView = ({
|
|||
>
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">Visible Context</p>
|
||||
<p className="text-sm font-medium text-[var(--color-text)]">
|
||||
Visible Context
|
||||
</p>
|
||||
<p className="text-[10px] text-[var(--color-text-muted)]">
|
||||
{leadSessionLoading ? 'Loading…' : 'No session loaded'}
|
||||
</p>
|
||||
|
|
@ -1589,7 +1697,7 @@ export const TeamDetailView = ({
|
|||
isActive={isThisTabActive}
|
||||
isFocused={isPaneFocused}
|
||||
>
|
||||
<TeamSidebarRail
|
||||
<TeamSidebarRailBridge
|
||||
teamName={teamName}
|
||||
messagesPanelProps={sharedMessagesPanelProps}
|
||||
isResizing={isMessagesPanelResizing}
|
||||
|
|
@ -1870,15 +1978,14 @@ export const TeamDetailView = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<MemberList
|
||||
<TeamMemberListBridge
|
||||
teamName={teamName}
|
||||
members={data.members}
|
||||
memberTaskCounts={memberTaskCounts}
|
||||
taskMap={taskMap}
|
||||
pendingRepliesByMember={pendingRepliesByMember}
|
||||
memberSpawnStatuses={memberSpawnStatusMap}
|
||||
isTeamAlive={data.isAlive}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={leadActivity}
|
||||
launchParams={launchParams}
|
||||
onMemberClick={handleSelectMember}
|
||||
onSendMessage={handleSendMessageToMember}
|
||||
|
|
@ -1958,7 +2065,10 @@ export const TeamDetailView = ({
|
|||
onApprove={(taskId) => {
|
||||
void (async () => {
|
||||
try {
|
||||
await updateKanban(teamName, taskId, { op: 'set_column', column: 'approved' });
|
||||
await updateKanban(teamName, taskId, {
|
||||
op: 'set_column',
|
||||
column: 'approved',
|
||||
});
|
||||
} catch {
|
||||
// error via store
|
||||
}
|
||||
|
|
@ -2125,7 +2235,7 @@ export const TeamDetailView = ({
|
|||
{messagesPanelMode !== 'sidebar' && <ClaudeLogsSection teamName={teamName} />}
|
||||
|
||||
{messagesPanelMode === 'inline' && (
|
||||
<MessagesPanel position="inline" {...sharedMessagesPanelProps} />
|
||||
<TeamMessagesPanelBridge position="inline" {...sharedMessagesPanelProps} />
|
||||
)}
|
||||
|
||||
<ReviewDialog
|
||||
|
|
@ -2153,7 +2263,7 @@ export const TeamDetailView = ({
|
|||
}}
|
||||
/>
|
||||
|
||||
<MemberDetailDialog
|
||||
<TeamMemberDetailDialogBridge
|
||||
open={selectedMember !== null}
|
||||
member={selectedMember}
|
||||
teamName={teamName}
|
||||
|
|
@ -2161,8 +2271,6 @@ export const TeamDetailView = ({
|
|||
messages={data.messages}
|
||||
isTeamAlive={data.isAlive}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={leadActivity}
|
||||
spawnEntry={selectedMember ? memberSpawnStatusMap?.get(selectedMember.name) : undefined}
|
||||
onClose={() => setSelectedMember(null)}
|
||||
onSendMessage={() => {
|
||||
const name = selectedMember?.name ?? '';
|
||||
|
|
@ -2285,8 +2393,8 @@ export const TeamDetailView = ({
|
|||
<DialogHeader>
|
||||
<DialogTitle>Remove member</DialogTitle>
|
||||
<DialogDescription>
|
||||
Remove “{removeMemberConfirm}” from the team? Tasks and messages will
|
||||
be preserved, but this name cannot be reused.
|
||||
Remove “{removeMemberConfirm}” from the team? Tasks and messages
|
||||
will be preserved, but this name cannot be reused.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
|
@ -2508,3 +2616,11 @@ export const TeamDetailView = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{spawnStatusWatcher}
|
||||
{renderBody()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue