fix(ci): restore workspace validation

This commit is contained in:
777genius 2026-04-18 12:20:37 +03:00
parent 0eb38387a3
commit f536e7e715
28 changed files with 86 additions and 86 deletions

View file

@ -115,9 +115,7 @@ export function useTeamGraphAdapter(teamName: string): GraphDataPort {
const currentAssignment = slotAssignments[stableOwnerId];
const defaultAssignment = defaultSeed.assignments[stableOwnerId];
return (
currentAssignment &&
defaultAssignment &&
currentAssignment.ringIndex === defaultAssignment.ringIndex &&
currentAssignment?.ringIndex === defaultAssignment?.ringIndex &&
currentAssignment.sectorIndex === defaultAssignment.sectorIndex
);
});

View file

@ -5,6 +5,7 @@
* into ui/, hooks/, or core/ directly.
*/
export type { InlineActivityEntry } from '../core/domain/buildInlineActivityEntries';
export { buildInlineActivityEntries } from '../core/domain/buildInlineActivityEntries';
export { buildGraphMemberNodeIdForMember } from '../core/domain/graphOwnerIdentity';
export { TeamGraphAdapter } from './adapters/TeamGraphAdapter';

View file

@ -157,7 +157,7 @@ function resolveHistoryOpenedAt(lookup: HistoryLookup, projectPath: string): num
}
const foldedMatch = lookup.folded.get(foldHistoryPath(normalizedPath));
if (!foldedMatch || foldedMatch.exactPaths.size !== 1) {
if (foldedMatch?.exactPaths.size !== 1) {
return 0;
}

View file

@ -471,7 +471,7 @@ export class TmuxWslService {
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', POWERSHELL_FEATURE_QUERY],
6_000
);
if (!result || result.exitCode !== 0 || !result.stdout.trim()) {
if (result?.exitCode !== 0 || !result.stdout.trim()) {
return null;
}

View file

@ -88,6 +88,7 @@ import {
} from './services/extensions';
import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor';
import { HttpServer } from './services/infrastructure/HttpServer';
import { clearAutoResumeService } from './services/team/AutoResumeService';
import {
buildTeamControlApiBaseUrl,
clearTeamControlApiState,
@ -100,7 +101,6 @@ import {
type TeamReconcileTrigger,
} from './services/team/TeamReconcileDrainScheduler';
import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore';
import { clearAutoResumeService } from './services/team/AutoResumeService';
import { getAppIconPath } from './utils/appIcon';
import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder';
import {

View file

@ -839,7 +839,7 @@ async function handleGetData(
const projectPath = data.config.projectPath;
const live = provisioning.getLiveLeadProcessMessages(tn);
const durableMessages = Array.isArray((data as { messages?: unknown }).messages)
? (((data as { messages?: typeof live }).messages ?? []) as typeof live)
? ((data as { messages?: typeof live }).messages ?? [])
: [];
if (live.length === 0) {

View file

@ -88,7 +88,7 @@ export class ApiKeyService {
);
}
if (!request.value) throw new Error('Key value is required');
if (request.scope === 'project' && (!request.projectPath || !request.projectPath.trim())) {
if (request.scope === 'project' && !request.projectPath?.trim()) {
throw new Error('Project-scoped API keys require a project path');
}

View file

@ -161,7 +161,7 @@ function classifyFailedProbe(
export class CliProviderModelAvailabilityService {
private readonly cache = new Map<string, ProviderModelAvailabilityCacheEntry>();
private readonly queue: Array<() => void> = [];
private readonly queue: (() => void)[] = [];
private activeProbeCount = 0;
constructor(private readonly onUpdate?: ProviderAvailabilityUpdateHandler) {}

View file

@ -223,8 +223,7 @@ export function createPersistedLaunchSnapshot(params: {
for (const name of expectedMembers) {
const member = members[name];
if (
member &&
member.launchState === 'starting' &&
member?.launchState === 'starting' &&
!member.agentToolAccepted &&
!member.runtimeAlive &&
!member.bootstrapConfirmed &&

View file

@ -390,7 +390,7 @@ export class TeamMessageFeedService {
const feedRevision = toFeedRevision(messages);
const nextEntry =
cached && cached.feedRevision === feedRevision
cached?.feedRevision === feedRevision
? cached
: {
feedRevision,

View file

@ -82,6 +82,7 @@ import { resolveTeamProviderId } from '../runtime/providerRuntimeEnv';
import { buildActionModeProtocol } from './actionModeInstructions';
import { atomicWriteAsync } from './atomicWrite';
import { peekAutoResumeService } from './AutoResumeService';
import { ClaudeBinaryResolver } from './ClaudeBinaryResolver';
import { withFileLock } from './fileLock';
import {
@ -112,7 +113,6 @@ import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamMetaStore } from './TeamMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskReader } from './TeamTaskReader';
import { peekAutoResumeService } from './AutoResumeService';
/**
* Kill a team CLI process using SIGKILL (uncatchable).

View file

@ -1,3 +1,4 @@
import { atomicWriteAsync } from '@main/utils/atomicWrite';
import { extractCwd } from '@main/utils/jsonl';
import {
encodePath,
@ -5,7 +6,6 @@ import {
getProjectsBasePath,
getTeamsBasePath,
} from '@main/utils/pathDecoder';
import { atomicWriteAsync } from '@main/utils/atomicWrite';
import { isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger';
import { createReadStream, type Dirent } from 'fs';
@ -219,7 +219,8 @@ export class TeamTranscriptProjectResolver {
...config,
projectPath: resolution.effectiveProjectPath,
projectPathHistory: this.buildRepairedProjectPathHistory(
config,
config.projectPath,
config.projectPathHistory,
resolution.effectiveProjectPath
),
}
@ -598,25 +599,11 @@ export class TeamTranscriptProjectResolver {
parsed.projectPath = normalizedNextPath;
const history: string[] = [];
const seen = new Set<string>();
const pushHistory = (value: unknown): void => {
const normalized = normalizeProjectPathCandidate(value);
if (!normalized || normalized === normalizedNextPath || seen.has(normalized)) {
return;
}
seen.add(normalized);
history.push(normalized);
};
if (Array.isArray(parsed.projectPathHistory)) {
for (const value of parsed.projectPathHistory) {
pushHistory(value);
}
}
pushHistory(rawProjectPath);
parsed.projectPathHistory = history.slice(-500);
parsed.projectPathHistory = this.buildRepairedProjectPathHistory(
rawProjectPath,
parsed.projectPathHistory,
normalizedNextPath
);
await atomicWriteAsync(configPath, JSON.stringify(parsed, null, 2));
logger.info(
`[${teamName}] Repaired transcript projectPath via exact session match: ${normalizedNextPath}`
@ -663,7 +650,11 @@ export class TeamTranscriptProjectResolver {
return orderedSessionIds;
}
private buildRepairedProjectPathHistory(config: TeamConfig, nextProjectPath: string): string[] {
private buildRepairedProjectPathHistory(
currentProjectPath: unknown,
rawProjectPathHistory: unknown,
nextProjectPath: string
): string[] {
const normalizedNextPath = normalizeProjectPathCandidate(nextProjectPath);
const history: string[] = [];
const seen = new Set<string>();
@ -676,12 +667,12 @@ export class TeamTranscriptProjectResolver {
history.push(normalized);
};
if (Array.isArray(config.projectPathHistory)) {
for (const value of config.projectPathHistory) {
if (Array.isArray(rawProjectPathHistory)) {
for (const value of rawProjectPathHistory) {
pushHistory(value);
}
}
pushHistory(config.projectPath);
pushHistory(currentProjectPath);
return history.slice(-500);
}

View file

@ -1,3 +1,9 @@
export {
AutoResumeService,
clearAutoResumeService,
getAutoResumeService,
initializeAutoResumeService,
} from './AutoResumeService';
export { BranchStatusService } from './BranchStatusService';
export { CascadeGuard } from './CascadeGuard';
export { ChangeExtractorService } from './ChangeExtractorService';
@ -16,12 +22,6 @@ export { BoardTaskActivityService } from './taskLogs/activity/BoardTaskActivityS
export { BoardTaskExactLogDetailService } from './taskLogs/exact/BoardTaskExactLogDetailService';
export { BoardTaskExactLogsService } from './taskLogs/exact/BoardTaskExactLogsService';
export { BoardTaskLogStreamService } from './taskLogs/stream/BoardTaskLogStreamService';
export {
AutoResumeService,
clearAutoResumeService,
getAutoResumeService,
initializeAutoResumeService,
} from './AutoResumeService';
export { TeamAttachmentStore } from './TeamAttachmentStore';
export { TeamBackupService } from './TeamBackupService';
export { TeamConfigReader } from './TeamConfigReader';

View file

@ -11,12 +11,12 @@ import { Button } from '@renderer/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store';
import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters';
import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
import {
getMcpInstallationSummaryLabel,
getMcpOperationKey,
sanitizeMcpServerName,
} from '@shared/utils/extensionNormalizers';
import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react';
import { Github as GithubIcon } from 'lucide-react';

View file

@ -25,18 +25,18 @@ import {
SelectValue,
} from '@renderer/components/ui/select';
import { useStore } from '@renderer/store';
import {
getDefaultMcpSharedScope,
getMcpScopeLabel,
isProjectScopedMcpScope,
isSharedMcpScope,
} from '@shared/utils/mcpScopes';
import {
getMcpInstallationSummaryLabel,
getMcpOperationKey,
getPreferredMcpInstallationEntry,
sanitizeMcpServerName,
} from '@shared/utils/extensionNormalizers';
import {
getDefaultMcpSharedScope,
getMcpScopeLabel,
isProjectScopedMcpScope,
isSharedMcpScope,
} from '@shared/utils/mcpScopes';
import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react';
import { InstallButton } from '../common/InstallButton';

View file

@ -16,8 +16,8 @@ import {
SelectValue,
} from '@renderer/components/ui/select';
import { useStore } from '@renderer/store';
import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities';
import { inferCapabilities, normalizeCategory } from '@shared/utils/extensionNormalizers';
import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities';
import { ArrowUpDown, Filter, Puzzle, Search } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';

View file

@ -41,8 +41,8 @@ import { validateSkillFolderName } from './skillValidationUtils';
import type {
SkillDetail,
SkillInvocationMode,
SkillRootKind,
SkillReviewPreview,
SkillRootKind,
} from '@shared/types/extensions';
type EditorMode = 'create' | 'edit';

View file

@ -127,7 +127,7 @@ function formatRuntimeAudienceLabel(providerNames: readonly string[]): string {
return 'the configured runtime';
}
if (providerNames.length === 1) {
return providerNames[0]!;
return providerNames[0];
}
if (providerNames.length === 2) {
return `${providerNames[0]} and ${providerNames[1]}`;

View file

@ -72,8 +72,8 @@ import { AdvancedCliSection } from './AdvancedCliSection';
import { EffortLevelSelector } from './EffortLevelSelector';
import { resolveLaunchDialogPrefill } from './launchDialogPrefill';
import { OptionalSettingsSection } from './OptionalSettingsSection';
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
import { ProjectPathSelector } from './ProjectPathSelector';
import { buildProviderPrepareModelCacheKey } from './providerPrepareCacheKey';
import {
buildReusableProviderPrepareModelResults,
getProviderPrepareCachedSnapshot,

View file

@ -111,7 +111,8 @@ export const MemberCard = ({
!isRemoved &&
presenceLabel === 'starting' &&
spawnLaunchState !== 'failed_to_start' &&
!activityTask;
!activityTask &&
!runtimeSummary;
const showStartingBadge = !isRemoved && presenceLabel === 'starting' && !activityTask;
const showRuntimeAdvisoryBadge =
!isRemoved &&

View file

@ -9,8 +9,8 @@ import { selectMemberMessagesForTeamMember } from '@renderer/store/slices/teamSl
import { isLeadMember } from '@shared/utils/leadDetection';
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
import { MemberDetailHeader } from './MemberDetailHeader';
import { buildMemberActivityEntries } from './memberActivityEntries';
import { MemberDetailHeader } from './MemberDetailHeader';
import { MemberDetailStats } from './MemberDetailStats';
import { type MemberActivityFilter, type MemberDetailTab } from './memberDetailTypes';
import { MemberLogsTab } from './MemberLogsTab';

View file

@ -2,7 +2,7 @@ import { buildInlineActivityEntries } from '@features/agent-graph/renderer';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { isLeadMember } from '@shared/utils/leadDetection';
import type { InlineActivityEntry } from '@features/agent-graph/core/domain/buildInlineActivityEntries';
import type { InlineActivityEntry } from '@features/agent-graph/renderer';
import type { InboxMessage, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
export function buildMemberActivityEntries({

View file

@ -267,7 +267,7 @@ export function initializeNotificationListeners(): () => void {
const headResult = await current.refreshTeamMessagesHead(teamName);
const latest = useStore.getState();
const meta = latest.memberActivityMetaByTeam[teamName];
if (headResult.feedChanged || !meta || meta.feedRevision !== headResult.feedRevision) {
if (headResult.feedChanged || meta?.feedRevision !== headResult.feedRevision) {
await latest.refreshMemberActivityMeta(teamName);
}
} catch {

View file

@ -1656,7 +1656,7 @@ export function selectResolvedMembersForTeamName(
const meta = state.memberActivityMetaByTeam[teamName];
const metaMembers = meta?.members;
const cached = resolvedMembersSelectorCache.get(teamName);
if (cached && cached.snapshotRef === snapshot.members && cached.metaMembersRef === metaMembers) {
if (cached?.snapshotRef === snapshot.members && cached.metaMembersRef === metaMembers) {
return cached.result;
}
@ -1690,7 +1690,7 @@ export function selectResolvedMemberForTeamName(
const metaEntry = state.memberActivityMetaByTeam[teamName]?.members[memberName];
const cacheKey = `${teamName}:${memberName}`;
const cached = resolvedMemberSelectorCache.get(cacheKey);
if (cached && cached.snapshotMemberRef === snapshotMember && cached.metaEntryRef === metaEntry) {
if (cached?.snapshotMemberRef === snapshotMember && cached.metaEntryRef === metaEntry) {
return cached.result;
}
@ -1735,8 +1735,7 @@ export function selectTeamMessages(
const entry = getTeamMessagesCacheEntry(state, teamName);
const cached = mergedMessagesSelectorCache.get(teamName);
if (
cached &&
cached.canonicalRef === entry.canonicalMessages &&
cached?.canonicalRef === entry.canonicalMessages &&
cached.optimisticRef === entry.optimisticMessages
) {
return cached.result;
@ -1763,7 +1762,7 @@ export function selectMemberMessagesForTeamMember(
const messages = selectTeamMessages(state, teamName);
const cacheKey = `${teamName}:${memberName}`;
const cached = memberMessagesSelectorCache.get(cacheKey);
if (cached && cached.messagesRef === messages) {
if (cached?.messagesRef === messages) {
return cached.result;
}
@ -3530,8 +3529,10 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
return queuedRequest;
}
let request!: Promise<RefreshTeamMessagesHeadResult>;
request = (async (): Promise<RefreshTeamMessagesHeadResult> => {
const requestRef: { current: Promise<RefreshTeamMessagesHeadResult> | null } = {
current: null,
};
requestRef.current = (async (): Promise<RefreshTeamMessagesHeadResult> => {
const teamStateEpoch = captureTeamLocalStateEpoch(teamName);
set((state) => ({
teamMessagesByName: {
@ -3621,7 +3622,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
}));
throw error;
} finally {
if (inFlightTeamMessagesHeadRequests.get(teamName) === request) {
if (inFlightTeamMessagesHeadRequests.get(teamName) === requestRef.current) {
inFlightTeamMessagesHeadRequests.delete(teamName);
if (pendingFreshTeamMessagesHeadRefreshes.delete(teamName)) {
void get().refreshTeamMessagesHead(teamName);
@ -3630,6 +3631,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
}
})();
const request = requestRef.current;
inFlightTeamMessagesHeadRequests.set(teamName, request);
return request;
},
@ -3662,8 +3664,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
return;
}
let request!: Promise<void>;
request = (async (): Promise<void> => {
const requestRef: { current: Promise<void> | null } = { current: null };
requestRef.current = (async (): Promise<void> => {
const teamStateEpoch = captureTeamLocalStateEpoch(teamName);
set((state) => ({
teamMessagesByName: {
@ -3747,12 +3749,13 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
},
}));
} finally {
if (inFlightTeamMessagesOlderRequests.get(teamName) === request) {
if (inFlightTeamMessagesOlderRequests.get(teamName) === requestRef.current) {
inFlightTeamMessagesOlderRequests.delete(teamName);
}
}
})();
const request = requestRef.current;
inFlightTeamMessagesOlderRequests.set(teamName, request);
return request;
},
@ -3769,8 +3772,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
return existingRequest;
}
let request!: Promise<void>;
request = (async (): Promise<void> => {
const requestRef: { current: Promise<void> | null } = { current: null };
requestRef.current = (async (): Promise<void> => {
const teamStateEpoch = captureTeamLocalStateEpoch(teamName);
try {
const meta = await unwrapIpc('team:getMemberActivityMeta', () =>
@ -3786,7 +3789,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
return {};
}
const existing = state.memberActivityMetaByTeam[teamName];
if (existing && existing.feedRevision === meta.feedRevision) {
if (existing?.feedRevision === meta.feedRevision) {
return {};
}
const sharedMembers = structurallyShareMemberActivityFacts(
@ -3794,8 +3797,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
meta.members
);
const nextMeta =
existing &&
existing.members === sharedMembers &&
existing?.members === sharedMembers &&
existing.feedRevision === meta.feedRevision &&
existing.computedAt === meta.computedAt
? existing
@ -3816,7 +3818,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
}
throw error;
} finally {
if (inFlightTeamMemberActivityMetaRequests.get(teamName) === request) {
if (inFlightTeamMemberActivityMetaRequests.get(teamName) === requestRef.current) {
inFlightTeamMemberActivityMetaRequests.delete(teamName);
if (pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName)) {
void get().refreshMemberActivityMeta(teamName);
@ -3825,6 +3827,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
}
})();
const request = requestRef.current;
inFlightTeamMemberActivityMetaRequests.set(teamName, request);
return request;
},

View file

@ -148,11 +148,7 @@ export function buildDisplayItems(
// Build display items
for (const step of steps) {
// Skip the last output step
if (
lastOutputStepRef &&
step.id === lastOutputStepRef.id &&
step.type === lastOutputStepRef.type
) {
if (step.id === lastOutputStepRef?.id && step.type === lastOutputStepRef.type) {
continue;
}

View file

@ -104,10 +104,10 @@ function parseRelativeResetDuration(text: string): number | null {
const match = LEADING_TIME_VALUE_RE.exec(tail);
if (!match) return null;
const amount = Number.parseFloat(match[1]!);
const amount = Number.parseFloat(match[1]);
if (!Number.isFinite(amount) || amount < 0) return null;
const unit = match[2]!.toLowerCase();
const unit = match[2].toLowerCase();
if (['second', 'seconds', 'sec', 'secs', 's'].includes(unit)) {
return Math.round(amount * 1000);
}
@ -157,7 +157,7 @@ function parseAbsoluteResetClockTime(text: string, now: Date): Date | null {
const afterMatch = tail.slice(tzTokenLength);
if (DAY_SHIFT_QUALIFIER_RE.test(afterMatch)) return null;
const hourRaw = Number.parseInt(match[1]!, 10);
const hourRaw = Number.parseInt(match[1], 10);
const minuteRaw = match[2] ? Number.parseInt(match[2], 10) : 0;
const ampm = match[3]?.toLowerCase() ?? null;
const parenthesizedTz = parenthesizedTzMatch?.[1]?.toUpperCase() ?? '';

View file

@ -15,7 +15,7 @@ export interface TeamGraphDefaultLayoutSeed {
assignments: Record<string, GraphOwnerSlotAssignment>;
}
const SMALL_TEAM_CARDINAL_SLOT_PRESETS: ReadonlyArray<ReadonlyArray<GraphOwnerSlotAssignment>> = [
const SMALL_TEAM_CARDINAL_SLOT_PRESETS: readonly (readonly GraphOwnerSlotAssignment[])[] = [
[],
[{ ringIndex: 0, sectorIndex: 0 }],
[
@ -83,7 +83,7 @@ export function buildTeamGraphDefaultLayoutSeed(
const preset = SMALL_TEAM_CARDINAL_SLOT_PRESETS[orderedVisibleOwnerIds.length];
const assignments: Record<string, GraphOwnerSlotAssignment> = {};
if (preset && preset.length === orderedVisibleOwnerIds.length) {
if (preset?.length === orderedVisibleOwnerIds.length) {
orderedVisibleOwnerIds.forEach((stableOwnerId, index) => {
assignments[stableOwnerId] = preset[index]!;
});

View file

@ -308,6 +308,17 @@ describe('MessagesPanel idle summary invariants', () => {
];
await act(async () => {
storeState.teamMessagesByName['atlas-hq'] = {
canonicalMessages: messages,
optimisticMessages: [],
feedRevision: 'rev-1',
nextCursor: null,
hasMore: false,
lastFetchedAt: Date.now(),
loadingHead: false,
loadingOlder: false,
headHydrated: true,
};
root.render(
React.createElement(MessagesPanel, {
teamName: 'atlas-hq',