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 currentAssignment = slotAssignments[stableOwnerId];
const defaultAssignment = defaultSeed.assignments[stableOwnerId]; const defaultAssignment = defaultSeed.assignments[stableOwnerId];
return ( return (
currentAssignment && currentAssignment?.ringIndex === defaultAssignment?.ringIndex &&
defaultAssignment &&
currentAssignment.ringIndex === defaultAssignment.ringIndex &&
currentAssignment.sectorIndex === defaultAssignment.sectorIndex currentAssignment.sectorIndex === defaultAssignment.sectorIndex
); );
}); });

View file

@ -5,6 +5,7 @@
* into ui/, hooks/, or core/ directly. * into ui/, hooks/, or core/ directly.
*/ */
export type { InlineActivityEntry } from '../core/domain/buildInlineActivityEntries';
export { buildInlineActivityEntries } from '../core/domain/buildInlineActivityEntries'; export { buildInlineActivityEntries } from '../core/domain/buildInlineActivityEntries';
export { buildGraphMemberNodeIdForMember } from '../core/domain/graphOwnerIdentity'; export { buildGraphMemberNodeIdForMember } from '../core/domain/graphOwnerIdentity';
export { TeamGraphAdapter } from './adapters/TeamGraphAdapter'; 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)); const foldedMatch = lookup.folded.get(foldHistoryPath(normalizedPath));
if (!foldedMatch || foldedMatch.exactPaths.size !== 1) { if (foldedMatch?.exactPaths.size !== 1) {
return 0; return 0;
} }

View file

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

View file

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

View file

@ -839,7 +839,7 @@ async function handleGetData(
const projectPath = data.config.projectPath; const projectPath = data.config.projectPath;
const live = provisioning.getLiveLeadProcessMessages(tn); const live = provisioning.getLiveLeadProcessMessages(tn);
const durableMessages = Array.isArray((data as { messages?: unknown }).messages) 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) { 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.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'); throw new Error('Project-scoped API keys require a project path');
} }

View file

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

View file

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

View file

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

View file

@ -82,6 +82,7 @@ import { resolveTeamProviderId } from '../runtime/providerRuntimeEnv';
import { buildActionModeProtocol } from './actionModeInstructions'; import { buildActionModeProtocol } from './actionModeInstructions';
import { atomicWriteAsync } from './atomicWrite'; import { atomicWriteAsync } from './atomicWrite';
import { peekAutoResumeService } from './AutoResumeService';
import { ClaudeBinaryResolver } from './ClaudeBinaryResolver'; import { ClaudeBinaryResolver } from './ClaudeBinaryResolver';
import { withFileLock } from './fileLock'; import { withFileLock } from './fileLock';
import { import {
@ -112,7 +113,6 @@ import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamMetaStore } from './TeamMetaStore'; import { TeamMetaStore } from './TeamMetaStore';
import { TeamSentMessagesStore } from './TeamSentMessagesStore'; import { TeamSentMessagesStore } from './TeamSentMessagesStore';
import { TeamTaskReader } from './TeamTaskReader'; import { TeamTaskReader } from './TeamTaskReader';
import { peekAutoResumeService } from './AutoResumeService';
/** /**
* Kill a team CLI process using SIGKILL (uncatchable). * 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 { extractCwd } from '@main/utils/jsonl';
import { import {
encodePath, encodePath,
@ -5,7 +6,6 @@ import {
getProjectsBasePath, getProjectsBasePath,
getTeamsBasePath, getTeamsBasePath,
} from '@main/utils/pathDecoder'; } from '@main/utils/pathDecoder';
import { atomicWriteAsync } from '@main/utils/atomicWrite';
import { isLeadMember } from '@shared/utils/leadDetection'; import { isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { createReadStream, type Dirent } from 'fs'; import { createReadStream, type Dirent } from 'fs';
@ -219,7 +219,8 @@ export class TeamTranscriptProjectResolver {
...config, ...config,
projectPath: resolution.effectiveProjectPath, projectPath: resolution.effectiveProjectPath,
projectPathHistory: this.buildRepairedProjectPathHistory( projectPathHistory: this.buildRepairedProjectPathHistory(
config, config.projectPath,
config.projectPathHistory,
resolution.effectiveProjectPath resolution.effectiveProjectPath
), ),
} }
@ -598,25 +599,11 @@ export class TeamTranscriptProjectResolver {
parsed.projectPath = normalizedNextPath; parsed.projectPath = normalizedNextPath;
const history: string[] = []; parsed.projectPathHistory = this.buildRepairedProjectPathHistory(
const seen = new Set<string>(); rawProjectPath,
const pushHistory = (value: unknown): void => { parsed.projectPathHistory,
const normalized = normalizeProjectPathCandidate(value); normalizedNextPath
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);
await atomicWriteAsync(configPath, JSON.stringify(parsed, null, 2)); await atomicWriteAsync(configPath, JSON.stringify(parsed, null, 2));
logger.info( logger.info(
`[${teamName}] Repaired transcript projectPath via exact session match: ${normalizedNextPath}` `[${teamName}] Repaired transcript projectPath via exact session match: ${normalizedNextPath}`
@ -663,7 +650,11 @@ export class TeamTranscriptProjectResolver {
return orderedSessionIds; return orderedSessionIds;
} }
private buildRepairedProjectPathHistory(config: TeamConfig, nextProjectPath: string): string[] { private buildRepairedProjectPathHistory(
currentProjectPath: unknown,
rawProjectPathHistory: unknown,
nextProjectPath: string
): string[] {
const normalizedNextPath = normalizeProjectPathCandidate(nextProjectPath); const normalizedNextPath = normalizeProjectPathCandidate(nextProjectPath);
const history: string[] = []; const history: string[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
@ -676,12 +667,12 @@ export class TeamTranscriptProjectResolver {
history.push(normalized); history.push(normalized);
}; };
if (Array.isArray(config.projectPathHistory)) { if (Array.isArray(rawProjectPathHistory)) {
for (const value of config.projectPathHistory) { for (const value of rawProjectPathHistory) {
pushHistory(value); pushHistory(value);
} }
} }
pushHistory(config.projectPath); pushHistory(currentProjectPath);
return history.slice(-500); return history.slice(-500);
} }

View file

@ -1,3 +1,9 @@
export {
AutoResumeService,
clearAutoResumeService,
getAutoResumeService,
initializeAutoResumeService,
} from './AutoResumeService';
export { BranchStatusService } from './BranchStatusService'; export { BranchStatusService } from './BranchStatusService';
export { CascadeGuard } from './CascadeGuard'; export { CascadeGuard } from './CascadeGuard';
export { ChangeExtractorService } from './ChangeExtractorService'; export { ChangeExtractorService } from './ChangeExtractorService';
@ -16,12 +22,6 @@ export { BoardTaskActivityService } from './taskLogs/activity/BoardTaskActivityS
export { BoardTaskExactLogDetailService } from './taskLogs/exact/BoardTaskExactLogDetailService'; export { BoardTaskExactLogDetailService } from './taskLogs/exact/BoardTaskExactLogDetailService';
export { BoardTaskExactLogsService } from './taskLogs/exact/BoardTaskExactLogsService'; export { BoardTaskExactLogsService } from './taskLogs/exact/BoardTaskExactLogsService';
export { BoardTaskLogStreamService } from './taskLogs/stream/BoardTaskLogStreamService'; export { BoardTaskLogStreamService } from './taskLogs/stream/BoardTaskLogStreamService';
export {
AutoResumeService,
clearAutoResumeService,
getAutoResumeService,
initializeAutoResumeService,
} from './AutoResumeService';
export { TeamAttachmentStore } from './TeamAttachmentStore'; export { TeamAttachmentStore } from './TeamAttachmentStore';
export { TeamBackupService } from './TeamBackupService'; export { TeamBackupService } from './TeamBackupService';
export { TeamConfigReader } from './TeamConfigReader'; 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 { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters'; import { formatCompactNumber, formatRelativeTime } from '@renderer/utils/formatters';
import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
import { import {
getMcpInstallationSummaryLabel, getMcpInstallationSummaryLabel,
getMcpOperationKey, getMcpOperationKey,
sanitizeMcpServerName, sanitizeMcpServerName,
} from '@shared/utils/extensionNormalizers'; } from '@shared/utils/extensionNormalizers';
import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react'; import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react';
import { Github as GithubIcon } from 'lucide-react'; import { Github as GithubIcon } from 'lucide-react';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import { buildInlineActivityEntries } from '@features/agent-graph/renderer';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering'; import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { isLeadMember } from '@shared/utils/leadDetection'; 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'; import type { InboxMessage, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
export function buildMemberActivityEntries({ export function buildMemberActivityEntries({

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -308,6 +308,17 @@ describe('MessagesPanel idle summary invariants', () => {
]; ];
await act(async () => { 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( root.render(
React.createElement(MessagesPanel, { React.createElement(MessagesPanel, {
teamName: 'atlas-hq', teamName: 'atlas-hq',