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';
import { useShallow } from 'zustand/react/shallow';
import type { TeamSummary } from '@shared/types/team';
import type { TeamGraphData } from '../adapters/TeamGraphAdapter';
import type { TeamSummary } from '@shared/types/team';
export function useGraphActivityContext(teamName: string): {
teamData: TeamGraphData | null;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import {
DASHBOARD_RECENT_PROJECTS_ROUTE,
normalizeDashboardRecentProjectsPayload,
type DashboardRecentProjectsPayload,
normalizeDashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts';
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 { DashboardRecentProjectsPresenter } from '../adapters/output/presenters/DashboardRecentProjectsPresenter';
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 { LoggerPort } from '../../core/application/ports/LoggerPort';
import {
normalizeDashboardRecentProjectsPayload,
type DashboardRecentProjectsPayload,
} from '@features/recent-projects/contracts';
import type { ServiceContext } from '@main/services';
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 type {
DashboardRecentProjectsPayload,
DashboardRecentProjectsPayloadLike,
} from '@features/recent-projects/contracts';
const RECENT_PROJECTS_CLIENT_CACHE_TTL_MS = 15_000;
const RECENT_PROJECTS_CLIENT_DEGRADED_CACHE_TTL_MS = 1_500;

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
import { getMemberColorByName } from '@shared/constants/memberColors';
import {
createCliAutoSuffixNameGuard,
createCliProvisionerNameGuard,
} from '@shared/utils/teamMemberName';
import { getStableTeamOwnerId } from '@shared/utils/teamStableOwnerId';
import { getMemberColorByName } from '@shared/constants/memberColors';
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 { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
import { createHash } from 'crypto';
import { getEffectiveInboxMessageId } from './inboxMessageIdentity';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 { isLeadMember } from '@shared/utils/leadDetection';
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
@ -102,7 +102,7 @@ export function resolveLaunchDialogPrefill({
return {
providerId,
model: matchingModel
? normalizeTeamModelForUi(providerId, matchingModel)
? normalizeExplicitTeamModelForUi(providerId, matchingModel)
: getStoredModel(providerId),
effort,
limitContext,

View file

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

View file

@ -16,8 +16,8 @@ import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
import { useTheme } from '@renderer/hooks/useTheme';
import { reconcileChips, removeChipTokenFromText } from '@renderer/utils/chipUtils';
import { cn } from '@renderer/lib/utils';
import { reconcileChips, removeChipTokenFromText } from '@renderer/utils/chipUtils';
import { getMemberColorByName } from '@shared/constants/memberColors';
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 { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
import { isLeadMember } from '@shared/utils/leadDetection';
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 { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
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 { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
@ -39,7 +39,7 @@ export function createMemberDraft(initial?: Partial<MemberDraft>): MemberDraft {
customRole: initial?.customRole ?? '',
workflow: initial?.workflow,
providerId,
model: normalizeTeamModelForUi(providerId, initial?.model ?? ''),
model: normalizeExplicitTeamModelForUi(providerId, initial?.model ?? ''),
effort: initial?.effort,
removedAt: initial?.removedAt,
};
@ -218,7 +218,7 @@ export function buildMembersFromDrafts(members: MemberDraft[]): TeamProvisioning
}
const model = member.model?.trim();
if (model) {
result.model = normalizeTeamModelForUi(providerId, model);
result.model = normalizeExplicitTeamModelForUi(providerId, model);
}
const effort = normalizeDraftEffort(member.effort);
if (effort) {

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
import type { MemberSpawnStatusEntry, ResolvedTeamMember, TeamProviderId } from '@shared/types';
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
function isMemberLaunchPending(spawnEntry: MemberSpawnStatusEntry | undefined): boolean {
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 {
CliProviderId,
CliProviderModelAvailability,
@ -6,29 +26,10 @@ import type {
TeamProviderId,
} 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 {
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_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,
@ -236,6 +237,14 @@ export function isTeamModelAvailableForUi(
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(
providerId: SupportedProviderId | undefined,
model: string | undefined,

View file

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

View file

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

View file

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

View file

@ -288,11 +288,11 @@ describe('CliInstallerService', () => {
expect(status.providers.find((provider) => provider.providerId === 'codex')?.modelAvailability).toEqual([]);
const verifiedProvider = await service.verifyProviderModels('codex');
expect(verifiedProvider?.modelAvailability).toEqual(
expect.arrayContaining([
expect.objectContaining({ modelId: 'gpt-5.4', status: 'checking' }),
expect.objectContaining({ modelId: 'gpt-5.2-codex', status: 'checking' }),
])
expect(verifiedProvider?.modelAvailability).toEqual([
expect.objectContaining({ modelId: 'gpt-5.4', status: 'checking' }),
]);
expect(verifiedProvider?.modelAvailability).not.toEqual(
expect.arrayContaining([expect.objectContaining({ modelId: 'gpt-5.2-codex' })])
);
await vi.waitFor(() => {
@ -302,13 +302,15 @@ describe('CliInstallerService', () => {
expect(latestCodexProvider?.modelAvailability).toEqual([
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
.filter((call: unknown[]) => call[0] === 'cliInstaller:progress')
.map((call: unknown[]) => call[1] as { type?: string; status?: { providers?: unknown[] } })
@ -325,12 +327,29 @@ describe('CliInstallerService', () => {
'modelAvailability' in provider &&
(provider as { providerId?: string }).providerId === 'codex' &&
Array.isArray((provider as { modelAvailability?: unknown[] }).modelAvailability) &&
(provider as { modelAvailability: Array<{ status?: string }> }).modelAvailability.some(
(item) => item.status === 'unavailable'
)
(provider as { modelAvailability: Array<{ modelId?: string; status?: string }> })
.modelAvailability.some(
(item) => item.modelId === 'gpt-5.4' && item.status === 'available'
)
)
)
).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);
});
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({
env: { HOME: '/Users/tester' },
connectionIssues: {},
});
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 service = new CliProviderModelAvailabilityService(onUpdate);
service.getSnapshot(createContext(['gpt-5.2-codex']));
service.getSnapshot(createContext(['gpt-5.1-codex-max']));
await vi.waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(
@ -92,7 +94,7 @@ describe('CliProviderModelAvailabilityService', () => {
expect.objectContaining({
modelAvailability: [
expect.objectContaining({
modelId: 'gpt-5.2-codex',
modelId: 'gpt-5.1-codex-max',
status: 'unavailable',
reason: 'Not available with Codex ChatGPT subscription',
}),

View file

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

View file

@ -61,7 +61,7 @@ describe('MemberCard starting-state visuals', () => {
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);
const host = document.createElement('div');
document.body.appendChild(host);
@ -84,8 +84,9 @@ describe('MemberCard starting-state visuals', () => {
});
expect(host.textContent).toContain('starting');
expect(host.textContent).toContain('Anthropic · haiku · Medium');
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 () => {
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);
const host = document.createElement('div');
document.body.appendChild(host);
@ -197,9 +198,10 @@ describe('MemberCard starting-state visuals', () => {
});
expect(host.textContent).toContain('starting');
expect(host.textContent).toContain('Anthropic · sonnet · Medium');
expect(host.textContent).not.toContain('online');
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 () => {
root.unmount();