perf(renderer): isolate team detail summary subscription
This commit is contained in:
parent
17bd573ce3
commit
0656794026
1 changed files with 77 additions and 56 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
import { SessionContextPanel } from '@renderer/components/chat/SessionContextPanel/index';
|
import { SessionContextPanel } from '@renderer/components/chat/SessionContextPanel/index';
|
||||||
|
|
@ -202,6 +202,69 @@ function filterKanbanTasks(tasks: TeamTaskWithKanban[], query: string): TeamTask
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TeamOfflineStatusBanner = memo(function TeamOfflineStatusBanner({
|
||||||
|
teamName,
|
||||||
|
onLaunch,
|
||||||
|
}: {
|
||||||
|
teamName: string;
|
||||||
|
onLaunch: () => void;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const summary = useStore(
|
||||||
|
useShallow((s) => {
|
||||||
|
const team = s.teamByName[teamName];
|
||||||
|
if (!team) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
memberCount: team.memberCount,
|
||||||
|
expectedMemberCount: team.expectedMemberCount,
|
||||||
|
confirmedCount: team.confirmedCount,
|
||||||
|
runtimeAlivePendingCount: team.runtimeAlivePendingCount,
|
||||||
|
teamLaunchState: team.teamLaunchState,
|
||||||
|
partialLaunchFailure: team.partialLaunchFailure,
|
||||||
|
missingMemberCount: team.missingMembers?.length ?? 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const message =
|
||||||
|
summary?.teamLaunchState === 'partial_pending'
|
||||||
|
? summary.runtimeAlivePendingCount != null && summary.runtimeAlivePendingCount > 0
|
||||||
|
? `Last launch is still reconciling - ${summary.confirmedCount ?? 0}/${summary.expectedMemberCount ?? summary.memberCount} teammates confirmed alive, ${summary.runtimeAlivePendingCount} runtime${summary.runtimeAlivePendingCount === 1 ? '' : 's'} pending bootstrap`
|
||||||
|
: 'Last launch is still reconciling'
|
||||||
|
: summary?.partialLaunchFailure
|
||||||
|
? summary.missingMemberCount > 0
|
||||||
|
? `Last launch failed partway - ${summary.missingMemberCount}/${summary.expectedMemberCount ?? summary.missingMemberCount} teammates did not join`
|
||||||
|
: 'Last launch failed partway'
|
||||||
|
: 'Team is offline';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mb-3 flex items-center justify-between gap-3 rounded-md border px-3 py-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--warning-bg)',
|
||||||
|
borderColor: 'var(--warning-border)',
|
||||||
|
color: 'var(--warning-text)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1.5 text-xs">
|
||||||
|
<AlertTriangle size={14} className="shrink-0" />
|
||||||
|
{message}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 shrink-0 gap-1 px-2 text-xs text-[var(--step-done-text)] hover:bg-[var(--step-done-bg)]"
|
||||||
|
onClick={onLaunch}
|
||||||
|
>
|
||||||
|
<Play size={12} />
|
||||||
|
Launch
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const TeamDetailView = ({
|
export const TeamDetailView = ({
|
||||||
teamName,
|
teamName,
|
||||||
isPaneFocused = false,
|
isPaneFocused = false,
|
||||||
|
|
@ -730,24 +793,6 @@ export const TeamDetailView = ({
|
||||||
}
|
}
|
||||||
}, [kanbanFilterQuery, clearKanbanFilter]);
|
}, [kanbanFilterQuery, clearKanbanFilter]);
|
||||||
|
|
||||||
const currentTeamSummary = useStore(
|
|
||||||
useShallow((s) => {
|
|
||||||
const team = teamName ? s.teamByName[teamName] : undefined;
|
|
||||||
if (!team) return null;
|
|
||||||
return {
|
|
||||||
displayName: team.displayName,
|
|
||||||
projectPath: team.projectPath,
|
|
||||||
memberCount: team.memberCount,
|
|
||||||
expectedMemberCount: team.expectedMemberCount,
|
|
||||||
confirmedCount: team.confirmedCount,
|
|
||||||
runtimeAlivePendingCount: team.runtimeAlivePendingCount,
|
|
||||||
teamLaunchState: team.teamLaunchState,
|
|
||||||
partialLaunchFailure: team.partialLaunchFailure,
|
|
||||||
missingMemberCount: team.missingMembers?.length ?? 0,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load sessions for the team's project
|
// Load sessions for the team's project
|
||||||
const projectId = useMemo(
|
const projectId = useMemo(
|
||||||
() => resolveProjectIdByPath(data?.config.projectPath, projects, repositoryGroups),
|
() => resolveProjectIdByPath(data?.config.projectPath, projects, repositoryGroups),
|
||||||
|
|
@ -1397,6 +1442,10 @@ export const TeamDetailView = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error === 'TEAM_DRAFT') {
|
if (error === 'TEAM_DRAFT') {
|
||||||
|
const draftTeamSummary = useStore.getState().teamByName[teamName];
|
||||||
|
const draftDisplayName = draftTeamSummary?.displayName || teamName;
|
||||||
|
const draftMemberCount = draftTeamSummary?.memberCount ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="size-full overflow-auto p-6">
|
<div className="size-full overflow-auto p-6">
|
||||||
|
|
@ -1407,11 +1456,10 @@ export const TeamDetailView = ({
|
||||||
<div className="max-w-md text-center">
|
<div className="max-w-md text-center">
|
||||||
<p className="text-sm font-medium text-text">Team not launched yet</p>
|
<p className="text-sm font-medium text-text">Team not launched yet</p>
|
||||||
<p className="mt-2 text-xs text-text-secondary">
|
<p className="mt-2 text-xs text-text-secondary">
|
||||||
This is a draft team —{' '}
|
This is a draft team - <strong>{draftDisplayName}</strong> has been configured with{' '}
|
||||||
<strong>{currentTeamSummary?.displayName || teamName}</strong> has been configured
|
{draftMemberCount} member
|
||||||
with {currentTeamSummary?.memberCount ?? 0} member
|
{draftMemberCount === 1 ? '' : 's'} but hasn't been provisioned by CLI yet.
|
||||||
{currentTeamSummary?.memberCount === 1 ? '' : 's'} but hasn't been provisioned
|
Click Launch to select a model and start the team.
|
||||||
by CLI yet. Click Launch to select a model and start the team.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 flex justify-center gap-2">
|
<div className="mt-4 flex justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
|
@ -1437,7 +1485,7 @@ export const TeamDetailView = ({
|
||||||
open={launchDialogOpen}
|
open={launchDialogOpen}
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
members={[]}
|
members={[]}
|
||||||
defaultProjectPath={currentTeamSummary?.projectPath}
|
defaultProjectPath={draftTeamSummary?.projectPath}
|
||||||
provisioningError={provisioningError}
|
provisioningError={provisioningError}
|
||||||
clearProvisioningError={clearProvisioningError}
|
clearProvisioningError={clearProvisioningError}
|
||||||
onClose={() => setLaunchDialogOpen(false)}
|
onClose={() => setLaunchDialogOpen(false)}
|
||||||
|
|
@ -1757,37 +1805,10 @@ export const TeamDetailView = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!data.isAlive && !isTeamProvisioning ? (
|
{!data.isAlive && !isTeamProvisioning ? (
|
||||||
<div
|
<TeamOfflineStatusBanner
|
||||||
className="mb-3 flex items-center justify-between gap-3 rounded-md border px-3 py-2"
|
teamName={teamName}
|
||||||
style={{
|
onLaunch={() => setLaunchDialogOpen(true)}
|
||||||
backgroundColor: 'var(--warning-bg)',
|
/>
|
||||||
borderColor: 'var(--warning-border)',
|
|
||||||
color: 'var(--warning-text)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-1.5 text-xs">
|
|
||||||
<AlertTriangle size={14} className="shrink-0" />
|
|
||||||
{currentTeamSummary?.teamLaunchState === 'partial_pending'
|
|
||||||
? currentTeamSummary.runtimeAlivePendingCount &&
|
|
||||||
currentTeamSummary.runtimeAlivePendingCount > 0
|
|
||||||
? `Last launch is still reconciling — ${currentTeamSummary.confirmedCount ?? 0}/${currentTeamSummary.expectedMemberCount ?? currentTeamSummary.memberCount} teammates confirmed alive, ${currentTeamSummary.runtimeAlivePendingCount} runtime${currentTeamSummary.runtimeAlivePendingCount === 1 ? '' : 's'} pending bootstrap`
|
|
||||||
: 'Last launch is still reconciling'
|
|
||||||
: currentTeamSummary?.partialLaunchFailure
|
|
||||||
? currentTeamSummary.missingMemberCount > 0
|
|
||||||
? `Last launch failed partway — ${currentTeamSummary.missingMemberCount}/${currentTeamSummary.expectedMemberCount ?? currentTeamSummary.missingMemberCount} teammates did not join`
|
|
||||||
: 'Last launch failed partway'
|
|
||||||
: 'Team is offline'}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 shrink-0 gap-1 px-2 text-xs text-[var(--step-done-text)] hover:bg-[var(--step-done-bg)]"
|
|
||||||
onClick={() => setLaunchDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<Play size={12} />
|
|
||||||
Launch
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div ref={provisioningBannerRef}>
|
<div ref={provisioningBannerRef}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue