fix(ci): restore workspace validation

This commit is contained in:
777genius 2026-04-16 22:52:56 +03:00
parent d8672c32f6
commit bb60bbb0ec
42 changed files with 208 additions and 201 deletions

View file

@ -6,8 +6,8 @@ import {
} from '@renderer/store/slices/teamSlice'; } from '@renderer/store/slices/teamSlice';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import type { TeamSummary } from '@shared/types/team';
import type { TeamGraphData } from '../adapters/TeamGraphAdapter'; import type { TeamGraphData } from '../adapters/TeamGraphAdapter';
import type { TeamSummary } from '@shared/types/team';
export function useGraphActivityContext(teamName: string): { export function useGraphActivityContext(teamName: string): {
teamData: TeamGraphData | null; teamData: TeamGraphData | null;

View file

@ -9,8 +9,8 @@ import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { import {
getCurrentProvisioningProgressForTeam, getCurrentProvisioningProgressForTeam,
selectResolvedMembersForTeamName,
isTeamGraphSlotPersistenceDisabled, isTeamGraphSlotPersistenceDisabled,
selectResolvedMembersForTeamName,
selectTeamDataForName, selectTeamDataForName,
selectTeamMessages, selectTeamMessages,
} from '@renderer/store/slices/teamSlice'; } from '@renderer/store/slices/teamSlice';
@ -19,8 +19,8 @@ import { useShallow } from 'zustand/react/shallow';
import { TeamGraphAdapter } from '../adapters/TeamGraphAdapter'; import { TeamGraphAdapter } from '../adapters/TeamGraphAdapter';
import type { GraphDataPort } from '@claude-teams/agent-graph';
import type { TeamGraphData } from '../adapters/TeamGraphAdapter'; import type { TeamGraphData } from '../adapters/TeamGraphAdapter';
import type { GraphDataPort } from '@claude-teams/agent-graph';
export function useTeamGraphAdapter(teamName: string): GraphDataPort { export function useTeamGraphAdapter(teamName: string): GraphDataPort {
const adapterRef = useRef<TeamGraphAdapter>(TeamGraphAdapter.create()); const adapterRef = useRef<TeamGraphAdapter>(TeamGraphAdapter.create());

View file

@ -1,6 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { isTeamGraphSlotPersistenceDisabled } from '@renderer/store/slices/teamSlice';
import { parseGraphMemberNodeId } from '../../core/domain/graphOwnerIdentity'; import { parseGraphMemberNodeId } from '../../core/domain/graphOwnerIdentity';
@ -8,6 +9,7 @@ import type { GraphOwnerSlotAssignment } from '@claude-teams/agent-graph';
export function useTeamGraphSurfaceActions(teamName: string): { export function useTeamGraphSurfaceActions(teamName: string): {
openTeamPage: () => void; openTeamPage: () => void;
resetOwnerSlotAssignmentsToDefaults: () => void;
commitOwnerSlotDrop: (payload: { commitOwnerSlotDrop: (payload: {
nodeId: string; nodeId: string;
assignment: GraphOwnerSlotAssignment; assignment: GraphOwnerSlotAssignment;
@ -19,6 +21,13 @@ export function useTeamGraphSurfaceActions(teamName: string): {
useStore.getState().openTeamTab(teamName); useStore.getState().openTeamTab(teamName);
}, [teamName]); }, [teamName]);
const resetOwnerSlotAssignmentsToDefaults = useCallback(() => {
if (!isTeamGraphSlotPersistenceDisabled()) {
return;
}
useStore.getState().resetTeamGraphSlotAssignmentsToDefaults(teamName);
}, [teamName]);
const commitOwnerSlotDrop = useCallback( const commitOwnerSlotDrop = useCallback(
(payload: { (payload: {
nodeId: string; nodeId: string;
@ -51,6 +60,7 @@ export function useTeamGraphSurfaceActions(teamName: string): {
return { return {
openTeamPage, openTeamPage,
resetOwnerSlotAssignmentsToDefaults,
commitOwnerSlotDrop, commitOwnerSlotDrop,
}; };
} }

View file

@ -7,8 +7,6 @@ import { useCallback, useLayoutEffect, useMemo } from 'react';
import { GraphView } from '@claude-teams/agent-graph'; import { GraphView } from '@claude-teams/agent-graph';
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost'; import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
import { useStore } from '@renderer/store';
import { isTeamGraphSlotPersistenceDisabled } from '@renderer/store/slices/teamSlice';
import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog'; import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog';
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility'; import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
@ -54,10 +52,11 @@ export const TeamGraphOverlay = ({
onOpenMemberProfile, onOpenMemberProfile,
}: TeamGraphOverlayProps): React.JSX.Element => { }: TeamGraphOverlayProps): React.JSX.Element => {
const graphData = useTeamGraphAdapter(teamName); const graphData = useTeamGraphAdapter(teamName);
const { openTeamPage: openTeamTab, commitOwnerSlotDrop } = useTeamGraphSurfaceActions(teamName); const {
const resetTeamGraphSlotAssignmentsToDefaults = useStore( openTeamPage: openTeamTab,
(s) => s.resetTeamGraphSlotAssignmentsToDefaults resetOwnerSlotAssignmentsToDefaults,
); commitOwnerSlotDrop,
} = useTeamGraphSurfaceActions(teamName);
const { sidebarVisible: persistedSidebarVisible, toggleSidebarVisible } = const { sidebarVisible: persistedSidebarVisible, toggleSidebarVisible } =
useGraphSidebarVisibility(); useGraphSidebarVisibility();
const { dialog: createTaskDialog, openCreateTaskDialog } = useGraphCreateTaskDialog(teamName); const { dialog: createTaskDialog, openCreateTaskDialog } = useGraphCreateTaskDialog(teamName);
@ -92,11 +91,8 @@ export const TeamGraphOverlay = ({
}, [openCreateTaskDialog]); }, [openCreateTaskDialog]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!isTeamGraphSlotPersistenceDisabled()) { resetOwnerSlotAssignmentsToDefaults();
return; }, [resetOwnerSlotAssignmentsToDefaults]);
}
resetTeamGraphSlotAssignmentsToDefaults(teamName);
}, [resetTeamGraphSlotAssignmentsToDefaults, teamName]);
const events: GraphEventPort = { const events: GraphEventPort = {
onNodeDoubleClick: useCallback( onNodeDoubleClick: useCallback(

View file

@ -7,8 +7,6 @@ import { lazy, Suspense, useCallback, useLayoutEffect, useMemo, useState } from
import { GraphView } from '@claude-teams/agent-graph'; import { GraphView } from '@claude-teams/agent-graph';
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost'; import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
import { useStore } from '@renderer/store';
import { isTeamGraphSlotPersistenceDisabled } from '@renderer/store/slices/teamSlice';
import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog'; import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog';
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility'; import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
@ -47,10 +45,8 @@ export const TeamGraphTab = ({
isPaneFocused = false, isPaneFocused = false,
}: TeamGraphTabProps): React.JSX.Element => { }: TeamGraphTabProps): React.JSX.Element => {
const graphData = useTeamGraphAdapter(teamName); const graphData = useTeamGraphAdapter(teamName);
const { openTeamPage, commitOwnerSlotDrop } = useTeamGraphSurfaceActions(teamName); const { openTeamPage, resetOwnerSlotAssignmentsToDefaults, commitOwnerSlotDrop } =
const resetTeamGraphSlotAssignmentsToDefaults = useStore( useTeamGraphSurfaceActions(teamName);
(s) => s.resetTeamGraphSlotAssignmentsToDefaults
);
const [fullscreen, setFullscreen] = useState(false); const [fullscreen, setFullscreen] = useState(false);
const { sidebarVisible, toggleSidebarVisible } = useGraphSidebarVisibility(); const { sidebarVisible, toggleSidebarVisible } = useGraphSidebarVisibility();
const { dialog: createTaskDialog, openCreateTaskDialog } = useGraphCreateTaskDialog(teamName); const { dialog: createTaskDialog, openCreateTaskDialog } = useGraphCreateTaskDialog(teamName);
@ -82,11 +78,11 @@ export const TeamGraphTab = ({
}, [openCreateTaskDialog]); }, [openCreateTaskDialog]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!isTeamGraphSlotPersistenceDisabled() || !isActive) { if (!isActive) {
return; return;
} }
resetTeamGraphSlotAssignmentsToDefaults(teamName); resetOwnerSlotAssignmentsToDefaults();
}, [isActive, resetTeamGraphSlotAssignmentsToDefaults, teamName]); }, [isActive, resetOwnerSlotAssignmentsToDefaults]);
// Task action dispatchers // Task action dispatchers
const dispatchTaskAction = useCallback( const dispatchTaskAction = useCallback(

View file

@ -1,7 +1,7 @@
import { import {
DASHBOARD_RECENT_PROJECTS_ROUTE, DASHBOARD_RECENT_PROJECTS_ROUTE,
normalizeDashboardRecentProjectsPayload,
type DashboardRecentProjectsPayload, type DashboardRecentProjectsPayload,
normalizeDashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts'; } from '@features/recent-projects/contracts';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';

View file

@ -1,3 +1,8 @@
import {
type DashboardRecentProjectsPayload,
normalizeDashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts';
import { ListDashboardRecentProjectsUseCase } from '../../core/application/use-cases/ListDashboardRecentProjectsUseCase'; import { ListDashboardRecentProjectsUseCase } from '../../core/application/use-cases/ListDashboardRecentProjectsUseCase';
import { DashboardRecentProjectsPresenter } from '../adapters/output/presenters/DashboardRecentProjectsPresenter'; import { DashboardRecentProjectsPresenter } from '../adapters/output/presenters/DashboardRecentProjectsPresenter';
import { ClaudeRecentProjectsSourceAdapter } from '../adapters/output/sources/ClaudeRecentProjectsSourceAdapter'; import { ClaudeRecentProjectsSourceAdapter } from '../adapters/output/sources/ClaudeRecentProjectsSourceAdapter';
@ -10,10 +15,6 @@ import { RecentProjectIdentityResolver } from '../infrastructure/identity/Recent
import type { ClockPort } from '../../core/application/ports/ClockPort'; import type { ClockPort } from '../../core/application/ports/ClockPort';
import type { LoggerPort } from '../../core/application/ports/LoggerPort'; import type { LoggerPort } from '../../core/application/ports/LoggerPort';
import {
normalizeDashboardRecentProjectsPayload,
type DashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts';
import type { ServiceContext } from '@main/services'; import type { ServiceContext } from '@main/services';
export interface RecentProjectsFeatureFacade { export interface RecentProjectsFeatureFacade {

View file

@ -1,9 +1,10 @@
import type {
DashboardRecentProjectsPayloadLike,
DashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts';
import { normalizeDashboardRecentProjectsPayload } from '@features/recent-projects/contracts'; import { normalizeDashboardRecentProjectsPayload } from '@features/recent-projects/contracts';
import type {
DashboardRecentProjectsPayload,
DashboardRecentProjectsPayloadLike,
} from '@features/recent-projects/contracts';
const RECENT_PROJECTS_CLIENT_CACHE_TTL_MS = 15_000; const RECENT_PROJECTS_CLIENT_CACHE_TTL_MS = 15_000;
const RECENT_PROJECTS_CLIENT_DEGRADED_CACHE_TTL_MS = 1_500; const RECENT_PROJECTS_CLIENT_DEGRADED_CACHE_TTL_MS = 1_500;

View file

@ -180,8 +180,8 @@ import type {
TeamSummary, TeamSummary,
TeamTask, TeamTask,
TeamTaskStatus, TeamTaskStatus,
TeamViewSnapshot,
TeamUpdateConfigRequest, TeamUpdateConfigRequest,
TeamViewSnapshot,
ToolApprovalFileContent, ToolApprovalFileContent,
ToolApprovalSettings, ToolApprovalSettings,
UpdateKanbanPatch, UpdateKanbanPatch,

View file

@ -50,8 +50,8 @@ import type {
CliInstallationStatus, CliInstallationStatus,
CliInstallerProgress, CliInstallerProgress,
CliPlatform, CliPlatform,
CliProviderModelAvailability,
CliProviderId, CliProviderId,
CliProviderModelAvailability,
CliProviderStatus, CliProviderStatus,
} from '@shared/types'; } from '@shared/types';
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
@ -596,7 +596,7 @@ export class CliInstallerService {
private updateLatestProviderStatus(providerStatus: CliProviderStatus): void { private updateLatestProviderStatus(providerStatus: CliProviderStatus): void {
if ( if (
providerStatus.modelVerificationState !== 'verifying' && providerStatus.modelVerificationState !== 'verifying' &&
!((providerStatus.modelAvailability?.length ?? 0) > 0) (providerStatus.modelAvailability?.length ?? 0) <= 0
) { ) {
this.latestProviderSignatures.set(providerStatus.providerId, null); this.latestProviderSignatures.set(providerStatus.providerId, null);
} }

View file

@ -47,8 +47,8 @@ import { TeamInboxWriter } from './TeamInboxWriter';
import { TeamKanbanManager } from './TeamKanbanManager'; import { TeamKanbanManager } from './TeamKanbanManager';
import { TeamMemberResolver } from './TeamMemberResolver'; import { TeamMemberResolver } from './TeamMemberResolver';
import { TeamMemberRuntimeAdvisoryService } from './TeamMemberRuntimeAdvisoryService'; import { TeamMemberRuntimeAdvisoryService } from './TeamMemberRuntimeAdvisoryService';
import { TeamMessageFeedService } from './TeamMessageFeedService';
import { TeamMembersMetaStore } from './TeamMembersMetaStore'; import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamMessageFeedService } from './TeamMessageFeedService';
import { TeamMetaStore } from './TeamMetaStore'; import { TeamMetaStore } from './TeamMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore'; import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal'; import { TeamTaskCommentNotificationJournal } from './TeamTaskCommentNotificationJournal';
@ -75,8 +75,8 @@ import type {
TaskRef, TaskRef,
TeamConfig, TeamConfig,
TeamCreateConfigRequest, TeamCreateConfigRequest,
TeamMemberActivityMeta,
TeamMember, TeamMember,
TeamMemberActivityMeta,
TeamProcess, TeamProcess,
TeamSummary, TeamSummary,
TeamTask, TeamTask,

View file

@ -1,9 +1,9 @@
import { getMemberColorByName } from '@shared/constants/memberColors';
import { import {
createCliAutoSuffixNameGuard, createCliAutoSuffixNameGuard,
createCliProvisionerNameGuard, createCliProvisionerNameGuard,
} from '@shared/utils/teamMemberName'; } from '@shared/utils/teamMemberName';
import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId'; import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId';
import { getMemberColorByName } from '@shared/constants/memberColors';
import type { TeamConfig, TeamMember, TeamMemberSnapshot, TeamTaskWithKanban } from '@shared/types'; import type { TeamConfig, TeamMember, TeamMemberSnapshot, TeamTaskWithKanban } from '@shared/types';

View file

@ -1,7 +1,6 @@
import { createHash } from 'crypto';
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics'; import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
import { createHash } from 'crypto';
import { getEffectiveInboxMessageId } from './inboxMessageIdentity'; import { getEffectiveInboxMessageId } from './inboxMessageIdentity';

View file

@ -33,6 +33,7 @@ import {
import { getMemberColorByName } from '@shared/constants/memberColors'; import { getMemberColorByName } from '@shared/constants/memberColors';
import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team'; import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team';
import { resolveLanguageName } from '@shared/utils/agentLanguage'; import { resolveLanguageName } from '@shared/utils/agentLanguage';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
import { parseCliArgs } from '@shared/utils/cliArgsParser'; import { parseCliArgs } from '@shared/utils/cliArgsParser';
import { import {
isInboxNoiseMessage, isInboxNoiseMessage,
@ -42,14 +43,13 @@ import {
} from '@shared/utils/inboxNoise'; } from '@shared/utils/inboxNoise';
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection'; import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults'; import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { import {
parseAllTeammateMessages, parseAllTeammateMessages,
type ParsedTeammateContent, type ParsedTeammateContent,
} from '@shared/utils/teammateMessageParser'; } from '@shared/utils/teammateMessageParser';
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName'; import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
import { import {
extractToolPreview, extractToolPreview,
@ -68,16 +68,16 @@ import {
type GeminiRuntimeAuthState, type GeminiRuntimeAuthState,
resolveGeminiRuntimeAuth, resolveGeminiRuntimeAuth,
} from '../runtime/geminiRuntimeAuth'; } from '../runtime/geminiRuntimeAuth';
import { buildProviderAwareCliEnv } from '../runtime/providerAwareCliEnv';
import { import {
buildProviderPreflightPingArgs,
buildProviderModelProbeArgs, buildProviderModelProbeArgs,
buildProviderPreflightPingArgs,
classifyProviderModelProbeFailure, classifyProviderModelProbeFailure,
getProviderModelProbeExpectedOutput, getProviderModelProbeExpectedOutput,
getProviderModelProbeTimeoutMs, getProviderModelProbeTimeoutMs,
isProviderModelProbeSuccessOutput, isProviderModelProbeSuccessOutput,
normalizeProviderModelProbeFailureReason, normalizeProviderModelProbeFailureReason,
} from '../runtime/providerModelProbe'; } from '../runtime/providerModelProbe';
import { buildProviderAwareCliEnv } from '../runtime/providerAwareCliEnv';
import { resolveTeamProviderId } from '../runtime/providerRuntimeEnv'; import { resolveTeamProviderId } from '../runtime/providerRuntimeEnv';
import { buildActionModeProtocol } from './actionModeInstructions'; import { buildActionModeProtocol } from './actionModeInstructions';

View file

@ -265,7 +265,6 @@ import type {
LeadContextUsageSnapshot, LeadContextUsageSnapshot,
MemberFullStats, MemberFullStats,
MemberLogSummary, MemberLogSummary,
TeamMemberActivityMeta,
MemberSpawnStatusesSnapshot, MemberSpawnStatusesSnapshot,
MessagesPage, MessagesPage,
NotificationTrigger, NotificationTrigger,
@ -297,14 +296,15 @@ import type {
TeamCreateResponse, TeamCreateResponse,
TeamLaunchRequest, TeamLaunchRequest,
TeamLaunchResponse, TeamLaunchResponse,
TeamMemberActivityMeta,
TeamMessageNotificationData, TeamMessageNotificationData,
TeamViewSnapshot,
TeamProvisioningPrepareResult, TeamProvisioningPrepareResult,
TeamProvisioningProgress, TeamProvisioningProgress,
TeamSummary, TeamSummary,
TeamTask, TeamTask,
TeamTaskStatus, TeamTaskStatus,
TeamUpdateConfigRequest, TeamUpdateConfigRequest,
TeamViewSnapshot,
ToolApprovalEvent, ToolApprovalEvent,
ToolApprovalFileContent, ToolApprovalFileContent,
ToolApprovalSettings, ToolApprovalSettings,

View file

@ -54,7 +54,6 @@ import type {
SshLastConnection, SshLastConnection,
SubagentDetail, SubagentDetail,
TeamChangeEvent, TeamChangeEvent,
UpdateSchedulePatch,
TeamClaudeLogsQuery, TeamClaudeLogsQuery,
TeamClaudeLogsResponse, TeamClaudeLogsResponse,
TeamCreateRequest, TeamCreateRequest,
@ -74,6 +73,7 @@ import type {
TriggerTestResult, TriggerTestResult,
UpdateKanbanPatch, UpdateKanbanPatch,
UpdaterAPI, UpdaterAPI,
UpdateSchedulePatch,
WaterfallData, WaterfallData,
WslClaudeRootCandidate, WslClaudeRootCandidate,
} from '@shared/types'; } from '@shared/types';

View file

@ -12,7 +12,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { api, isElectronMode } from '@renderer/api'; import { api, isElectronMode } from '@renderer/api';
import { confirm } from '@renderer/components/common/ConfirmDialog'; import { confirm } from '@renderer/components/common/ConfirmDialog';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
import { import {
formatProviderStatusText, formatProviderStatusText,
getProviderConnectionModeSummary, getProviderConnectionModeSummary,
@ -23,6 +22,7 @@ import {
isConnectionManagedRuntimeProvider, isConnectionManagedRuntimeProvider,
shouldShowProviderConnectAction, shouldShowProviderConnectAction,
} from '@renderer/components/runtime/providerConnectionUi'; } from '@renderer/components/runtime/providerConnectionUi';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector'; import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog'; import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
import { SettingsToggle } from '@renderer/components/settings/components'; import { SettingsToggle } from '@renderer/components/settings/components';

View file

@ -5,8 +5,8 @@
import { Badge } from '@renderer/components/ui/badge'; import { Badge } from '@renderer/components/ui/badge';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { import {
getInstallationSummaryLabel,
getCapabilityLabel, getCapabilityLabel,
getInstallationSummaryLabel,
hasInstallationInScope, hasInstallationInScope,
inferCapabilities, inferCapabilities,
normalizeCategory, normalizeCategory,

View file

@ -25,8 +25,8 @@ import {
} from '@renderer/components/ui/select'; } from '@renderer/components/ui/select';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { import {
getInstallationSummaryLabel,
getCapabilityLabel, getCapabilityLabel,
getInstallationSummaryLabel,
hasInstallationInScope, hasInstallationInScope,
inferCapabilities, inferCapabilities,
normalizeCategory, normalizeCategory,

View file

@ -1,8 +1,8 @@
import { cn } from '@renderer/lib/utils';
import { import {
getTeamModelBadgeLabel, getTeamModelBadgeLabel,
getVisibleTeamProviderModels, getVisibleTeamProviderModels,
} from '@renderer/utils/teamModelCatalog'; } from '@renderer/utils/teamModelCatalog';
import { cn } from '@renderer/lib/utils';
import type { import type {
CliProviderId, CliProviderId,
@ -43,7 +43,7 @@ function getAvailabilityChip(status: CliProviderModelAvailabilityStatus | null):
} }
} }
export function ProviderModelBadges({ export const ProviderModelBadges = ({
providerId, providerId,
models, models,
modelAvailability, modelAvailability,
@ -53,7 +53,7 @@ export function ProviderModelBadges({
readonly models: string[]; readonly models: string[];
readonly modelAvailability?: CliProviderModelAvailability[]; readonly modelAvailability?: CliProviderModelAvailability[];
readonly providerStatus?: Pick<CliProviderStatus, 'providerId' | 'authMethod' | 'backend'> | null; readonly providerStatus?: Pick<CliProviderStatus, 'providerId' | 'authMethod' | 'backend'> | null;
}): React.JSX.Element { }): React.JSX.Element => {
const visibleModels = getVisibleTeamProviderModels(providerId, models, providerStatus); const visibleModels = getVisibleTeamProviderModels(providerId, models, providerStatus);
return ( return (
@ -94,4 +94,4 @@ export function ProviderModelBadges({
})} })}
</div> </div>
); );
} };

View file

@ -10,7 +10,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { isElectronMode } from '@renderer/api'; import { isElectronMode } from '@renderer/api';
import { confirm } from '@renderer/components/common/ConfirmDialog'; import { confirm } from '@renderer/components/common/ConfirmDialog';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
import { import {
formatProviderStatusText, formatProviderStatusText,
getProviderConnectionModeSummary, getProviderConnectionModeSummary,
@ -21,6 +20,7 @@ import {
isConnectionManagedRuntimeProvider, isConnectionManagedRuntimeProvider,
shouldShowProviderConnectAction, shouldShowProviderConnectAction,
} from '@renderer/components/runtime/providerConnectionUi'; } from '@renderer/components/runtime/providerConnectionUi';
import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges';
import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector'; import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog'; import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog';
import { SettingsToggle } from '@renderer/components/settings/components'; import { SettingsToggle } from '@renderer/components/settings/components';

View file

@ -56,10 +56,10 @@ import {
import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog'; import type { ActiveTeamRef, TeamCopyData } from './dialogs/CreateTeamDialog';
import type { TeamListFilterState } from './TeamListFilterPopover'; import type { TeamListFilterState } from './TeamListFilterPopover';
import type { import type {
TeamMemberSnapshot,
ResolvedTeamMember, ResolvedTeamMember,
TeamCreateRequest, TeamCreateRequest,
TeamLaunchRequest, TeamLaunchRequest,
TeamMemberSnapshot,
TeamSummary, TeamSummary,
TeamSummaryMember, TeamSummaryMember,
} from '@shared/types'; } from '@shared/types';

View file

@ -43,6 +43,7 @@ import {
import { normalizePath } from '@renderer/utils/pathNormalize'; import { normalizePath } from '@renderer/utils/pathNormalize';
import { import {
getTeamModelSelectionError, getTeamModelSelectionError,
normalizeExplicitTeamModelForUi,
normalizeTeamModelForUi, normalizeTeamModelForUi,
} from '@renderer/utils/teamModelAvailability'; } from '@renderer/utils/teamModelAvailability';
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog'; import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
@ -53,22 +54,22 @@ import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
import { AdvancedCliSection } from './AdvancedCliSection'; import { AdvancedCliSection } from './AdvancedCliSection';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector'; import { ProjectPathSelector } from './ProjectPathSelector';
import {
getProviderPrepareCachedSnapshot,
type ProviderPrepareDiagnosticsModelResult,
runProviderPrepareDiagnostics,
} from './providerPrepareDiagnostics';
import { getProvisioningModelIssue } from './provisioningModelIssues';
import { import {
failIncompleteProviderChecks, failIncompleteProviderChecks,
getProvisioningFailureHint,
getPrimaryProvisioningFailureDetail, getPrimaryProvisioningFailureDetail,
getProvisioningFailureHint,
getProvisioningProviderBackendSummary, getProvisioningProviderBackendSummary,
type ProvisioningProviderCheck, type ProvisioningProviderCheck,
ProvisioningProviderStatusList, ProvisioningProviderStatusList,
shouldHideProvisioningProviderStatusList, shouldHideProvisioningProviderStatusList,
updateProviderCheck, updateProviderCheck,
} from './ProvisioningProviderStatusList'; } from './ProvisioningProviderStatusList';
import { getProvisioningModelIssue } from './provisioningModelIssues';
import {
getProviderPrepareCachedSnapshot,
runProviderPrepareDiagnostics,
type ProviderPrepareDiagnosticsModelResult,
} from './providerPrepareDiagnostics';
import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox'; import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox';
import { computeEffectiveTeamModel } from './TeamModelSelector'; import { computeEffectiveTeamModel } from './TeamModelSelector';
import { getNextSuggestedTeamName } from './teamNameSets'; import { getNextSuggestedTeamName } from './teamNameSets';
@ -108,7 +109,7 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
if (stored === null) { if (stored === null) {
return providerId === 'anthropic' ? 'opus' : ''; return providerId === 'anthropic' ? 'opus' : '';
} }
return normalizeTeamModelForUi(providerId, stored === '__default__' ? '' : stored); return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
} }
function isEphemeralRenderedProjectPath(projectPath: string | null | undefined): boolean { function isEphemeralRenderedProjectPath(projectPath: string | null | undefined): boolean {

View file

@ -48,6 +48,7 @@ import { normalizePath } from '@renderer/utils/pathNormalize';
import { nameColorSet } from '@renderer/utils/projectColor'; import { nameColorSet } from '@renderer/utils/projectColor';
import { import {
getTeamModelSelectionError, getTeamModelSelectionError,
normalizeExplicitTeamModelForUi,
normalizeTeamModelForUi, normalizeTeamModelForUi,
} from '@renderer/utils/teamModelAvailability'; } from '@renderer/utils/teamModelAvailability';
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog'; import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
@ -72,22 +73,22 @@ import { EffortLevelSelector } from './EffortLevelSelector';
import { resolveLaunchDialogPrefill } from './launchDialogPrefill'; import { resolveLaunchDialogPrefill } from './launchDialogPrefill';
import { OptionalSettingsSection } from './OptionalSettingsSection'; import { OptionalSettingsSection } from './OptionalSettingsSection';
import { ProjectPathSelector } from './ProjectPathSelector'; import { ProjectPathSelector } from './ProjectPathSelector';
import {
getProviderPrepareCachedSnapshot,
type ProviderPrepareDiagnosticsModelResult,
runProviderPrepareDiagnostics,
} from './providerPrepareDiagnostics';
import { getProvisioningModelIssue } from './provisioningModelIssues';
import { import {
failIncompleteProviderChecks, failIncompleteProviderChecks,
getProvisioningFailureHint,
getPrimaryProvisioningFailureDetail, getPrimaryProvisioningFailureDetail,
getProvisioningFailureHint,
getProvisioningProviderBackendSummary, getProvisioningProviderBackendSummary,
type ProvisioningProviderCheck, type ProvisioningProviderCheck,
ProvisioningProviderStatusList, ProvisioningProviderStatusList,
shouldHideProvisioningProviderStatusList, shouldHideProvisioningProviderStatusList,
updateProviderCheck, updateProviderCheck,
} from './ProvisioningProviderStatusList'; } from './ProvisioningProviderStatusList';
import { getProvisioningModelIssue } from './provisioningModelIssues';
import {
getProviderPrepareCachedSnapshot,
runProviderPrepareDiagnostics,
type ProviderPrepareDiagnosticsModelResult,
} from './providerPrepareDiagnostics';
import { import {
computeEffectiveTeamModel, computeEffectiveTeamModel,
formatTeamModelSummary, formatTeamModelSummary,
@ -195,7 +196,7 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
if (stored === null) { if (stored === null) {
return providerId === 'anthropic' ? 'opus' : ''; return providerId === 'anthropic' ? 'opus' : '';
} }
return normalizeTeamModelForUi(providerId, stored === '__default__' ? '' : stored); return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
} }
function getProviderLabel(providerId: TeamProviderId): string { function getProviderLabel(providerId: TeamProviderId): string {

View file

@ -11,7 +11,6 @@ import {
} from '@renderer/components/ui/tooltip'; } from '@renderer/components/ui/tooltip';
import { cn } from '@renderer/lib/utils'; import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
import { import {
GEMINI_UI_DISABLED_BADGE_LABEL, GEMINI_UI_DISABLED_BADGE_LABEL,
GEMINI_UI_DISABLED_REASON, GEMINI_UI_DISABLED_REASON,
@ -30,6 +29,7 @@ import {
getTeamProviderLabel as getCatalogTeamProviderLabel, getTeamProviderLabel as getCatalogTeamProviderLabel,
} from '@renderer/utils/teamModelCatalog'; } from '@renderer/utils/teamModelCatalog';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { getAnthropicDefaultTeamModel } from '@shared/utils/anthropicModelDefaults';
import { AlertTriangle, Info } from 'lucide-react'; import { AlertTriangle, Info } from 'lucide-react';
export { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog'; export { getProviderScopedTeamModelLabel } from '@renderer/utils/teamModelCatalog';

View file

@ -1,5 +1,5 @@
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze'; import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability'; import { normalizeExplicitTeamModelForUi } from '@renderer/utils/teamModelAvailability';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext'; import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
@ -102,7 +102,7 @@ export function resolveLaunchDialogPrefill({
return { return {
providerId, providerId,
model: matchingModel model: matchingModel
? normalizeTeamModelForUi(providerId, matchingModel) ? normalizeExplicitTeamModelForUi(providerId, matchingModel)
: getStoredModel(providerId), : getStoredModel(providerId),
effort, effort,
limitContext, limitContext,

View file

@ -5,15 +5,13 @@ import type { TeamProviderId, TeamProvisioningPrepareResult } from '@shared/type
export type ProviderPrepareCheckStatus = 'ready' | 'notes' | 'failed'; export type ProviderPrepareCheckStatus = 'ready' | 'notes' | 'failed';
interface PrepareProvisioningFn { type PrepareProvisioningFn = (
( cwd?: string,
cwd?: string, providerId?: TeamProviderId,
providerId?: TeamProviderId, providerIds?: TeamProviderId[],
providerIds?: TeamProviderId[], selectedModels?: string[],
selectedModels?: string[], limitContext?: boolean
limitContext?: boolean ) => Promise<TeamProvisioningPrepareResult>;
): Promise<TeamProvisioningPrepareResult>;
}
interface ProviderPrepareDiagnosticsProgress { interface ProviderPrepareDiagnosticsProgress {
details: string[]; details: string[];
@ -156,15 +154,15 @@ function normalizeModelReason(rawReason: string | null | undefined): string | nu
return 'Model verification timed out'; return 'Model verification timed out';
} }
const detailMatch = trimmed.match(/"detail":"((?:\\"|[^"])*)"/i); const detailMatch = /"detail":"((?:\\"|[^"])*)"/i.exec(trimmed);
if (detailMatch?.[1]) { if (detailMatch?.[1]) {
return normalizeModelReason(detailMatch[1].replace(/\\"/g, '"').trim()); return normalizeModelReason(detailMatch[1].replace(/\\"/g, '"').trim());
} }
const messageMatch = trimmed.match(/"message":"((?:\\"|[^"])*)"/i); const messageMatch = /"message":"((?:\\"|[^"])*)"/i.exec(trimmed);
if (messageMatch?.[1]) { if (messageMatch?.[1]) {
const decodedMessage = messageMatch[1].replace(/\\"/g, '"'); const decodedMessage = messageMatch[1].replace(/\\"/g, '"');
const nestedDetailMatch = decodedMessage.match(/"detail":"([^"]+)"/i); const nestedDetailMatch = /"detail":"([^"]+)"/i.exec(decodedMessage);
if (nestedDetailMatch?.[1]) { if (nestedDetailMatch?.[1]) {
return normalizeModelReason(nestedDetailMatch[1].trim()); return normalizeModelReason(nestedDetailMatch[1].trim());
} }

View file

@ -16,8 +16,8 @@ import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { reconcileChips, removeChipTokenFromText } from '@renderer/utils/chipUtils';
import { cn } from '@renderer/lib/utils'; import { cn } from '@renderer/lib/utils';
import { reconcileChips, removeChipTokenFromText } from '@renderer/utils/chipUtils';
import { getMemberColorByName } from '@shared/constants/memberColors'; import { getMemberColorByName } from '@shared/constants/memberColors';
import { AlertTriangle, ChevronDown, ChevronRight, Info, RotateCcw, Trash2 } from 'lucide-react'; import { AlertTriangle, ChevronDown, ChevronRight, Info, RotateCcw, Trash2 } from 'lucide-react';

View file

@ -1,7 +1,7 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { MemberCard } from './MemberCard'; import { MemberCard } from './MemberCard';

View file

@ -2,7 +2,7 @@ import { CUSTOM_ROLE, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRole
import { serializeChipsWithText } from '@renderer/types/inlineChip'; import { serializeChipsWithText } from '@renderer/types/inlineChip';
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze'; import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers'; import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability'; import { normalizeExplicitTeamModelForUi } from '@renderer/utils/teamModelAvailability';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
@ -39,7 +39,7 @@ export function createMemberDraft(initial?: Partial<MemberDraft>): MemberDraft {
customRole: initial?.customRole ?? '', customRole: initial?.customRole ?? '',
workflow: initial?.workflow, workflow: initial?.workflow,
providerId, providerId,
model: normalizeTeamModelForUi(providerId, initial?.model ?? ''), model: normalizeExplicitTeamModelForUi(providerId, initial?.model ?? ''),
effort: initial?.effort, effort: initial?.effort,
removedAt: initial?.removedAt, removedAt: initial?.removedAt,
}; };
@ -218,7 +218,7 @@ export function buildMembersFromDrafts(members: MemberDraft[]): TeamProvisioning
} }
const model = member.model?.trim(); const model = member.model?.trim();
if (model) { if (model) {
result.model = normalizeTeamModelForUi(providerId, model); result.model = normalizeExplicitTeamModelForUi(providerId, model);
} }
const effort = normalizeDraftEffort(member.effort); const effort = normalizeDraftEffort(member.effort);
if (effort) { if (effort) {

View file

@ -12,7 +12,6 @@ import { selectTeamMessages } from '@renderer/store/slices/teamSlice';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering'; import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { toMessageKey } from '@renderer/utils/teamMessageKey'; import { toMessageKey } from '@renderer/utils/teamMessageKey';
import { shouldExcludeInboxTextFromReplyCandidates } from '@shared/utils/idleNotificationSemantics'; import { shouldExcludeInboxTextFromReplyCandidates } from '@shared/utils/idleNotificationSemantics';
import { createLogger } from '@shared/utils/logger';
import { import {
CheckCheck, CheckCheck,
ChevronsDownUp, ChevronsDownUp,
@ -52,9 +51,6 @@ interface TimeWindow {
end: number; end: number;
} }
const logger = createLogger('Component:MessagesPanel');
const MESSAGES_PANEL_FILTER_WARN_MS = 8;
const MESSAGES_PANEL_EXPANDED_ITEM_WARN_MS = 6;
const BOTTOM_SHEET_HEADER_HEIGHT = 40; const BOTTOM_SHEET_HEADER_HEIGHT = 40;
const BOTTOM_SHEET_COLLAPSED_SNAP_INDEX = 1; const BOTTOM_SHEET_COLLAPSED_SNAP_INDEX = 1;
const BOTTOM_SHEET_COMPOSER_SNAP_INDEX = 2; const BOTTOM_SHEET_COMPOSER_SNAP_INDEX = 2;
@ -272,41 +268,21 @@ export const MessagesPanel = memo(function MessagesPanel({
}, [position, mountPoint]); }, [position, mountPoint]);
const filteredMessages = useMemo(() => { const filteredMessages = useMemo(() => {
const startedAt = performance.now(); return filterTeamMessages(effectiveMessages, {
const result = filterTeamMessages(effectiveMessages, {
timeWindow, timeWindow,
filter: messagesFilter, filter: messagesFilter,
searchQuery: messagesSearchQuery, searchQuery: messagesSearchQuery,
}); });
const ms = performance.now() - startedAt; }, [effectiveMessages, messagesFilter, messagesSearchQuery, timeWindow]);
if (ms >= MESSAGES_PANEL_FILTER_WARN_MS) {
logger.warn(
`[perf] filter team=${teamName} stage=messages ms=${ms.toFixed(1)} input=${effectiveMessages.length} output=${result.length} searchLen=${messagesSearchQuery.trim().length} noise=${
messagesFilter.showNoise ? 'on' : 'off'
}`
);
}
return result;
}, [effectiveMessages, messagesFilter, messagesSearchQuery, teamName, timeWindow]);
const activityTimelineMessages = useMemo(() => { const activityTimelineMessages = useMemo(() => {
const startedAt = performance.now(); return filterTeamMessages(effectiveMessages, {
const result = filterTeamMessages(effectiveMessages, {
includePassiveIdlePeerSummariesWhenNoiseHidden: true, includePassiveIdlePeerSummariesWhenNoiseHidden: true,
timeWindow, timeWindow,
filter: messagesFilter, filter: messagesFilter,
searchQuery: messagesSearchQuery, searchQuery: messagesSearchQuery,
}); });
const ms = performance.now() - startedAt; }, [effectiveMessages, messagesFilter, messagesSearchQuery, timeWindow]);
if (ms >= MESSAGES_PANEL_FILTER_WARN_MS) {
logger.warn(
`[perf] filter team=${teamName} stage=timeline ms=${ms.toFixed(1)} input=${effectiveMessages.length} output=${result.length} searchLen=${messagesSearchQuery.trim().length} noise=${
messagesFilter.showNoise ? 'on' : 'off'
}`
);
}
return result;
}, [effectiveMessages, messagesFilter, messagesSearchQuery, teamName, timeWindow]);
const replyCandidateMessages = useMemo( const replyCandidateMessages = useMemo(
() => () =>
@ -320,33 +296,21 @@ export const MessagesPanel = memo(function MessagesPanel({
// Resolve the expanded item from filtered messages // Resolve the expanded item from filtered messages
const expandedItem = useMemo<TimelineItem | null>(() => { const expandedItem = useMemo<TimelineItem | null>(() => {
const startedAt = performance.now(); if (!expandedItemKey) {
if (!expandedItemKey) return null; return null;
}
if (!expandedItemKey.startsWith('thoughts-')) { if (!expandedItemKey.startsWith('thoughts-')) {
const msg = activityTimelineMessages.find((m) => toMessageKey(m) === expandedItemKey); const msg = activityTimelineMessages.find((m) => toMessageKey(m) === expandedItemKey);
const result: TimelineItem | null = msg ? { type: 'message', message: msg } : null; return msg ? { type: 'message', message: msg } : null;
const ms = performance.now() - startedAt;
if (ms >= MESSAGES_PANEL_EXPANDED_ITEM_WARN_MS) {
logger.warn(
`[perf] expandedItem team=${teamName} ms=${ms.toFixed(1)} mode=message timelineMessages=${activityTimelineMessages.length}`
);
}
return result;
} }
const allItems = groupTimelineItems(activityTimelineMessages); const allItems = groupTimelineItems(activityTimelineMessages);
const result = return (
allItems.find( allItems.find(
(item) => (item) =>
item.type === 'lead-thoughts' && getThoughtGroupKey(item.group) === expandedItemKey item.type === 'lead-thoughts' && getThoughtGroupKey(item.group) === expandedItemKey
) ?? null; ) ?? null
const ms = performance.now() - startedAt; );
if (ms >= MESSAGES_PANEL_EXPANDED_ITEM_WARN_MS) { }, [expandedItemKey, activityTimelineMessages]);
logger.warn(
`[perf] expandedItem team=${teamName} ms=${ms.toFixed(1)} mode=thoughts timelineMessages=${activityTimelineMessages.length} groups=${allItems.length}`
);
}
return result;
}, [expandedItemKey, activityTimelineMessages, teamName]);
// Auto-clear stale expanded key // Auto-clear stale expanded key
useEffect(() => { useEffect(() => {

View file

@ -127,7 +127,7 @@ export interface ExtensionsSlice {
// Slice Creator // Slice Creator
// ============================================================================= // =============================================================================
let pluginFetchInFlight: { key: string; promise: Promise<void> } | null = null; let pluginFetchInFlight: { key: string; promise: Promise<void>; token: symbol } | null = null;
let pluginCatalogRequestSeq = 0; let pluginCatalogRequestSeq = 0;
const pluginSuccessResetTimers = new Map<string, ReturnType<typeof setTimeout>>(); const pluginSuccessResetTimers = new Map<string, ReturnType<typeof setTimeout>>();
let mcpDiagnosticsInFlight: Promise<void> | null = null; let mcpDiagnosticsInFlight: Promise<void> | null = null;
@ -286,6 +286,7 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
} }
const requestSeq = ++pluginCatalogRequestSeq; const requestSeq = ++pluginCatalogRequestSeq;
const requestToken = Symbol('pluginCatalogRequest');
set({ pluginCatalogLoading: true, pluginCatalogError: null }); set({ pluginCatalogLoading: true, pluginCatalogError: null });
const promise = (async () => { const promise = (async () => {
@ -344,13 +345,13 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
}; };
}); });
} finally { } finally {
if (pluginFetchInFlight?.promise === promise) { if (pluginFetchInFlight?.token === requestToken) {
pluginFetchInFlight = null; pluginFetchInFlight = null;
} }
} }
})(); })();
pluginFetchInFlight = { key: requestKey, promise }; pluginFetchInFlight = { key: requestKey, promise, token: requestToken };
await promise; await promise;
}, },

View file

@ -1,4 +1,5 @@
import { api } from '@renderer/api'; import { api } from '@renderer/api';
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
import { normalizePath } from '@renderer/utils/pathNormalize'; import { normalizePath } from '@renderer/utils/pathNormalize';
import { import {
buildTaskChangePresenceKey, buildTaskChangePresenceKey,
@ -6,10 +7,9 @@ import {
canDisplayTaskChangesForOptions, canDisplayTaskChangesForOptions,
type TaskChangeRequestOptions, type TaskChangeRequestOptions,
} from '@renderer/utils/taskChangeRequest'; } from '@renderer/utils/taskChangeRequest';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { toMessageKey } from '@renderer/utils/teamMessageKey'; import { toMessageKey } from '@renderer/utils/teamMessageKey';
import { extractProviderScopedBaseModel } from '@renderer/utils/teamModelContext';
import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc'; import { IpcError, unwrapIpc } from '@renderer/utils/unwrapIpc';
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team'; import { DEFAULT_TOOL_APPROVAL_SETTINGS } from '@shared/types/team';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
@ -111,10 +111,10 @@ type TeamGraphConfigMemberSeedInput = Pick<
NonNullable<TeamViewSnapshot['config']['members']>[number], NonNullable<TeamViewSnapshot['config']['members']>[number],
'name' | 'agentId' | 'removedAt' 'name' | 'agentId' | 'removedAt'
>; >;
type TeamGraphLayoutSessionState = { interface TeamGraphLayoutSessionState {
mode: 'default' | 'manual'; mode: 'default' | 'manual';
signature: string | null; signature: string | null;
}; }
export function isTeamDataRefreshPending(teamName: string): boolean { export function isTeamDataRefreshPending(teamName: string): boolean {
return ( return (
@ -3202,8 +3202,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
const existingOlderRequest = inFlightTeamMessagesOlderRequests.get(teamName); const existingOlderRequest = inFlightTeamMessagesOlderRequests.get(teamName);
if (existingOlderRequest) { if (existingOlderRequest) {
let queuedRequest: Promise<RefreshTeamMessagesHeadResult>; const queuedRequest: Promise<RefreshTeamMessagesHeadResult> = existingOlderRequest
queuedRequest = existingOlderRequest
.then(() => { .then(() => {
if (queuedTeamMessagesHeadRefreshesAfterOlder.get(teamName) === queuedRequest) { if (queuedTeamMessagesHeadRefreshesAfterOlder.get(teamName) === queuedRequest) {
queuedTeamMessagesHeadRefreshesAfterOlder.delete(teamName); queuedTeamMessagesHeadRefreshesAfterOlder.delete(teamName);

View file

@ -1,8 +1,8 @@
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector'; import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice'; import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
import type { MemberSpawnStatusEntry, ResolvedTeamMember, TeamProviderId } from '@shared/types'; import type { MemberSpawnStatusEntry, ResolvedTeamMember, TeamProviderId } from '@shared/types';
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
function isMemberLaunchPending(spawnEntry: MemberSpawnStatusEntry | undefined): boolean { function isMemberLaunchPending(spawnEntry: MemberSpawnStatusEntry | undefined): boolean {
if (!spawnEntry) { if (!spawnEntry) {

View file

@ -1,3 +1,23 @@
import {
getProviderScopedTeamModelLabel,
getRuntimeAwareTeamModelUiDisabledReason,
getTeamProviderLabel,
getTeamProviderModelOptions,
getVisibleTeamProviderModels,
GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON,
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
GPT_5_2_CODEX_UI_DISABLED_MODEL,
GPT_5_2_CODEX_UI_DISABLED_REASON,
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
normalizeTeamModelForUi as normalizeCatalogTeamModelForUi,
sortTeamProviderModels,
TEAM_MODEL_UI_DISABLED_BADGE_LABEL,
type TeamProviderModelOption,
} from './teamModelCatalog';
import { extractProviderScopedBaseModel } from './teamModelContext';
import type { import type {
CliProviderId, CliProviderId,
CliProviderModelAvailability, CliProviderModelAvailability,
@ -6,29 +26,10 @@ import type {
TeamProviderId, TeamProviderId,
} from '@shared/types'; } from '@shared/types';
import {
getProviderScopedTeamModelLabel,
getRuntimeAwareTeamModelUiDisabledReason,
getTeamProviderLabel,
getTeamProviderModelOptions,
sortTeamProviderModels,
getVisibleTeamProviderModels,
normalizeTeamModelForUi as normalizeCatalogTeamModelForUi,
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON,
GPT_5_2_CODEX_UI_DISABLED_MODEL,
GPT_5_2_CODEX_UI_DISABLED_REASON,
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
GPT_5_3_CODEX_SPARK_UI_DISABLED_REASON,
TEAM_MODEL_UI_DISABLED_BADGE_LABEL,
type TeamProviderModelOption,
} from './teamModelCatalog';
export { export {
GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON,
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL, GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON, GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
GPT_5_1_CODEX_MAX_CHATGPT_UI_DISABLED_REASON,
GPT_5_2_CODEX_UI_DISABLED_MODEL, GPT_5_2_CODEX_UI_DISABLED_MODEL,
GPT_5_2_CODEX_UI_DISABLED_REASON, GPT_5_2_CODEX_UI_DISABLED_REASON,
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL, GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
@ -236,6 +237,14 @@ export function isTeamModelAvailableForUi(
return getRuntimeModelAvailability(providerId, trimmed, providerStatus) === 'available'; return getRuntimeModelAvailability(providerId, trimmed, providerStatus) === 'available';
} }
export function normalizeExplicitTeamModelForUi(
providerId: SupportedProviderId | undefined,
model: string | undefined
): string {
const normalized = extractProviderScopedBaseModel(model, providerId) ?? '';
return normalizeCatalogTeamModelForUi(providerId, normalized).trim();
}
export function normalizeTeamModelForUi( export function normalizeTeamModelForUi(
providerId: SupportedProviderId | undefined, providerId: SupportedProviderId | undefined,
model: string | undefined, model: string | undefined,

View file

@ -1,4 +1,3 @@
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
import { import {
filterVisibleProviderRuntimeModels, filterVisibleProviderRuntimeModels,
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL, GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
@ -6,6 +5,8 @@ import {
GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL, GPT_5_3_CODEX_SPARK_UI_DISABLED_MODEL,
} from '@shared/utils/providerModelVisibility'; } from '@shared/utils/providerModelVisibility';
import type { CliProviderId, CliProviderStatus, TeamProviderId } from '@shared/types';
export { export {
GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL, GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL,
GPT_5_2_CODEX_UI_DISABLED_MODEL, GPT_5_2_CODEX_UI_DISABLED_MODEL,

View file

@ -53,7 +53,6 @@ import type {
KanbanColumnId, KanbanColumnId,
LeadActivitySnapshot, LeadActivitySnapshot,
LeadContextUsageSnapshot, LeadContextUsageSnapshot,
TeamMemberActivityMeta,
MemberFullStats, MemberFullStats,
MemberLogSummary, MemberLogSummary,
MemberSpawnStatusesSnapshot, MemberSpawnStatusesSnapshot,
@ -74,14 +73,15 @@ import type {
TeamCreateResponse, TeamCreateResponse,
TeamLaunchRequest, TeamLaunchRequest,
TeamLaunchResponse, TeamLaunchResponse,
TeamMemberActivityMeta,
TeamMessageNotificationData, TeamMessageNotificationData,
TeamViewSnapshot,
TeamProvisioningPrepareResult, TeamProvisioningPrepareResult,
TeamProvisioningProgress, TeamProvisioningProgress,
TeamSummary, TeamSummary,
TeamTask, TeamTask,
TeamTaskStatus, TeamTaskStatus,
TeamUpdateConfigRequest, TeamUpdateConfigRequest,
TeamViewSnapshot,
ToolApprovalEvent, ToolApprovalEvent,
ToolApprovalFileContent, ToolApprovalFileContent,
ToolApprovalSettings, ToolApprovalSettings,

View file

@ -4,8 +4,8 @@
import type { import type {
CliInstallationStatus, CliInstallationStatus,
InstallScope,
InstalledPluginEntry, InstalledPluginEntry,
InstallScope,
PluginCapability, PluginCapability,
PluginCatalogItem, PluginCatalogItem,
} from '@shared/types'; } from '@shared/types';

View file

@ -288,11 +288,11 @@ describe('CliInstallerService', () => {
expect(status.providers.find((provider) => provider.providerId === 'codex')?.modelAvailability).toEqual([]); expect(status.providers.find((provider) => provider.providerId === 'codex')?.modelAvailability).toEqual([]);
const verifiedProvider = await service.verifyProviderModels('codex'); const verifiedProvider = await service.verifyProviderModels('codex');
expect(verifiedProvider?.modelAvailability).toEqual( expect(verifiedProvider?.modelAvailability).toEqual([
expect.arrayContaining([ expect.objectContaining({ modelId: 'gpt-5.4', status: 'checking' }),
expect.objectContaining({ modelId: 'gpt-5.4', status: 'checking' }), ]);
expect.objectContaining({ modelId: 'gpt-5.2-codex', status: 'checking' }), expect(verifiedProvider?.modelAvailability).not.toEqual(
]) expect.arrayContaining([expect.objectContaining({ modelId: 'gpt-5.2-codex' })])
); );
await vi.waitFor(() => { await vi.waitFor(() => {
@ -302,13 +302,15 @@ describe('CliInstallerService', () => {
expect(latestCodexProvider?.modelAvailability).toEqual([ expect(latestCodexProvider?.modelAvailability).toEqual([
expect.objectContaining({ modelId: 'gpt-5.4', status: 'available' }), expect.objectContaining({ modelId: 'gpt-5.4', status: 'available' }),
expect.objectContaining({
modelId: 'gpt-5.2-codex',
status: 'unavailable',
}),
]); ]);
}); });
expect(execCli).not.toHaveBeenCalledWith(
'/usr/local/bin/claude',
expect.arrayContaining(['--model', 'gpt-5.2-codex']),
expect.anything()
);
const statusEvents = mockWindow.webContents.send.mock.calls const statusEvents = mockWindow.webContents.send.mock.calls
.filter((call: unknown[]) => call[0] === 'cliInstaller:progress') .filter((call: unknown[]) => call[0] === 'cliInstaller:progress')
.map((call: unknown[]) => call[1] as { type?: string; status?: { providers?: unknown[] } }) .map((call: unknown[]) => call[1] as { type?: string; status?: { providers?: unknown[] } })
@ -325,12 +327,29 @@ describe('CliInstallerService', () => {
'modelAvailability' in provider && 'modelAvailability' in provider &&
(provider as { providerId?: string }).providerId === 'codex' && (provider as { providerId?: string }).providerId === 'codex' &&
Array.isArray((provider as { modelAvailability?: unknown[] }).modelAvailability) && Array.isArray((provider as { modelAvailability?: unknown[] }).modelAvailability) &&
(provider as { modelAvailability: Array<{ status?: string }> }).modelAvailability.some( (provider as { modelAvailability: Array<{ modelId?: string; status?: string }> })
(item) => item.status === 'unavailable' .modelAvailability.some(
) (item) => item.modelId === 'gpt-5.4' && item.status === 'available'
)
) )
) )
).toBe(true); ).toBe(true);
expect(
statusEvents.some((event) =>
event.status?.providers?.some(
(provider) =>
typeof provider === 'object' &&
provider !== null &&
'providerId' in provider &&
'modelAvailability' in provider &&
(provider as { providerId?: string }).providerId === 'codex' &&
Array.isArray((provider as { modelAvailability?: unknown[] }).modelAvailability) &&
(provider as { modelAvailability: Array<{ modelId?: string }> }).modelAvailability.some(
(item) => item.modelId === 'gpt-5.2-codex'
)
)
)
).toBe(false);
}); });
}); });

View file

@ -72,18 +72,20 @@ describe('CliProviderModelAvailabilityService', () => {
expect(execCliMock).toHaveBeenCalledTimes(2); expect(execCliMock).toHaveBeenCalledTimes(2);
}); });
it('marks unsupported models as unavailable with the runtime reason', async () => { it('marks visible unsupported models as unavailable with the runtime reason', async () => {
buildProviderAwareCliEnvMock.mockResolvedValue({ buildProviderAwareCliEnvMock.mockResolvedValue({
env: { HOME: '/Users/tester' }, env: { HOME: '/Users/tester' },
connectionIssues: {}, connectionIssues: {},
}); });
execCliMock.mockRejectedValue( execCliMock.mockRejectedValue(
new Error("The 'gpt-5.2-codex' model is not supported when using Codex with a ChatGPT account.") new Error(
"The 'gpt-5.1-codex-max' model is not supported when using Codex with a ChatGPT account."
)
); );
const onUpdate = vi.fn(); const onUpdate = vi.fn();
const service = new CliProviderModelAvailabilityService(onUpdate); const service = new CliProviderModelAvailabilityService(onUpdate);
service.getSnapshot(createContext(['gpt-5.2-codex'])); service.getSnapshot(createContext(['gpt-5.1-codex-max']));
await vi.waitFor(() => { await vi.waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith( expect(onUpdate).toHaveBeenCalledWith(
@ -92,7 +94,7 @@ describe('CliProviderModelAvailabilityService', () => {
expect.objectContaining({ expect.objectContaining({
modelAvailability: [ modelAvailability: [
expect.objectContaining({ expect.objectContaining({
modelId: 'gpt-5.2-codex', modelId: 'gpt-5.1-codex-max',
status: 'unavailable', status: 'unavailable',
reason: 'Not available with Codex ChatGPT subscription', reason: 'Not available with Codex ChatGPT subscription',
}), }),

View file

@ -49,6 +49,13 @@ function createDeferred<T>(): Deferred<T> {
return { promise, resolve, reject }; return { promise, resolve, reject };
} }
function createLogsFinderMock(listAttributedMemberFiles: ReturnType<typeof vi.fn>) {
return {
listAttributedMemberFiles,
listAttributedSubagentFiles: listAttributedMemberFiles,
} as never;
}
describe('TeammateToolTracker', () => { describe('TeammateToolTracker', () => {
const tempDirs: string[] = []; const tempDirs: string[] = [];
@ -90,7 +97,7 @@ describe('TeammateToolTracker', () => {
const events: TeamChangeEvent[] = []; const events: TeamChangeEvent[] = [];
const tracker = new TeammateToolTracker( const tracker = new TeammateToolTracker(
{ listAttributedSubagentFiles } as never, createLogsFinderMock(listAttributedSubagentFiles),
{ enableTracking, disableTracking } as never, { enableTracking, disableTracking } as never,
(event) => events.push(event) (event) => events.push(event)
); );
@ -136,7 +143,7 @@ describe('TeammateToolTracker', () => {
]); ]);
const events: TeamChangeEvent[] = []; const events: TeamChangeEvent[] = [];
const tracker = new TeammateToolTracker( const tracker = new TeammateToolTracker(
{ listAttributedSubagentFiles } as never, createLogsFinderMock(listAttributedSubagentFiles),
{ enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never, { enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never,
(event) => events.push(event) (event) => events.push(event)
); );
@ -193,7 +200,7 @@ describe('TeammateToolTracker', () => {
const listAttributedSubagentFiles = vi.fn(async () => [...attributedFiles]); const listAttributedSubagentFiles = vi.fn(async () => [...attributedFiles]);
const events: TeamChangeEvent[] = []; const events: TeamChangeEvent[] = [];
const tracker = new TeammateToolTracker( const tracker = new TeammateToolTracker(
{ listAttributedSubagentFiles } as never, createLogsFinderMock(listAttributedSubagentFiles),
{ enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never, { enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never,
(event) => events.push(event) (event) => events.push(event)
); );
@ -242,7 +249,7 @@ describe('TeammateToolTracker', () => {
]); ]);
const events: TeamChangeEvent[] = []; const events: TeamChangeEvent[] = [];
const tracker = new TeammateToolTracker( const tracker = new TeammateToolTracker(
{ listAttributedSubagentFiles } as never, createLogsFinderMock(listAttributedSubagentFiles),
{ enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never, { enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never,
(event) => events.push(event) (event) => events.push(event)
); );
@ -299,7 +306,7 @@ describe('TeammateToolTracker', () => {
]); ]);
const events: TeamChangeEvent[] = []; const events: TeamChangeEvent[] = [];
const tracker = new TeammateToolTracker( const tracker = new TeammateToolTracker(
{ listAttributedSubagentFiles } as never, createLogsFinderMock(listAttributedSubagentFiles),
{ enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never, { enableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })), disableTracking: vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })) } as never,
(event) => events.push(event) (event) => events.push(event)
); );
@ -353,7 +360,7 @@ describe('TeammateToolTracker', () => {
const disableTracking = vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null })); const disableTracking = vi.fn(async () => ({ projectFingerprint: null, logSourceGeneration: null }));
const events: TeamChangeEvent[] = []; const events: TeamChangeEvent[] = [];
const tracker = new TeammateToolTracker( const tracker = new TeammateToolTracker(
{ listAttributedSubagentFiles } as never, createLogsFinderMock(listAttributedSubagentFiles),
{ enableTracking, disableTracking } as never, { enableTracking, disableTracking } as never,
(event) => events.push(event) (event) => events.push(event)
); );

View file

@ -61,7 +61,7 @@ describe('MemberCard starting-state visuals', () => {
document.body.innerHTML = ''; document.body.innerHTML = '';
}); });
it('shows starting skeleton treatment even after provisioning is no longer active', async () => { it('shows runtime summary while keeping the starting treatment after provisioning stops', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div'); const host = document.createElement('div');
document.body.appendChild(host); document.body.appendChild(host);
@ -84,8 +84,9 @@ describe('MemberCard starting-state visuals', () => {
}); });
expect(host.textContent).toContain('starting'); expect(host.textContent).toContain('starting');
expect(host.textContent).toContain('Anthropic · haiku · Medium');
expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull(); expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull();
expect(host.querySelectorAll('.skeleton-shimmer').length).toBeGreaterThan(0); expect(host.querySelectorAll('.skeleton-shimmer').length).toBe(0);
await act(async () => { await act(async () => {
root.unmount(); root.unmount();
@ -173,7 +174,7 @@ describe('MemberCard starting-state visuals', () => {
}); });
}); });
it('keeps the starting skeleton visible while a runtime is alive but still joining', async () => { it('keeps the starting treatment and runtime summary visible while a runtime is still joining', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div'); const host = document.createElement('div');
document.body.appendChild(host); document.body.appendChild(host);
@ -197,9 +198,10 @@ describe('MemberCard starting-state visuals', () => {
}); });
expect(host.textContent).toContain('starting'); expect(host.textContent).toContain('starting');
expect(host.textContent).toContain('Anthropic · sonnet · Medium');
expect(host.textContent).not.toContain('online'); expect(host.textContent).not.toContain('online');
expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull(); expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull();
expect(host.querySelectorAll('.skeleton-shimmer').length).toBeGreaterThan(0); expect(host.querySelectorAll('.skeleton-shimmer').length).toBe(0);
await act(async () => { await act(async () => {
root.unmount(); root.unmount();