fix(team): clear delivered runtime message warnings

This commit is contained in:
777genius 2026-04-27 11:15:57 +03:00
parent b26f0b05ba
commit 068e473d2d
12 changed files with 386 additions and 46 deletions

View file

@ -3506,7 +3506,7 @@ function buildLaunchDiagnosticsFromRun(
memberName,
severity: 'warning',
code: 'runtime_not_found',
label: `${memberName} - no runtime found`,
label: `${memberName} - waiting for runtime`,
detail: entry.runtimeDiagnostic,
observedAt,
});
@ -15755,7 +15755,7 @@ export class TeamProvisioningService {
? `${launchSummary.runtimeCandidatePendingCount} process candidates`
: '',
launchSummary.noRuntimePendingCount
? `${launchSummary.noRuntimePendingCount} no runtime found`
? `${launchSummary.noRuntimePendingCount} waiting for runtime`
: '',
].filter(Boolean);
const diagnosticSuffix = diagnosticParts.length > 0 ? ` - ${diagnosticParts.join(', ')}` : '';

View file

@ -138,6 +138,7 @@ import type {
TeamAgentRuntimeEntry,
TeamCreateRequest,
TeamLaunchRequest,
TeamProviderId,
TeamTaskWithKanban,
} from '@shared/types';
import type { EditorSelectionAction } from '@shared/types/editor';
@ -338,9 +339,17 @@ interface LeadContextBridgeProps {
tabId: string | null;
projectId: string | null;
leadSessionId: string | null;
leadProviderId?: TeamProviderId;
fallbackProjectRoot?: string;
}
// Codex/OpenCode lead sessions do not expose the Claude-style context data this panel expects yet.
const LEAD_CONTEXT_UNSUPPORTED_PROVIDER_IDS = new Set<TeamProviderId>(['codex', 'opencode']);
function canShowLeadContextUi(providerId: TeamProviderId | undefined): boolean {
return providerId === undefined || !LEAD_CONTEXT_UNSUPPORTED_PROVIDER_IDS.has(providerId);
}
function buildMemberSpawnStatusMap(
memberSpawnStatuses: Record<string, MemberSpawnStatusEntry> | undefined
): Map<string, MemberSpawnStatusEntry> | undefined {
@ -547,6 +556,7 @@ const LeadContextBridge = memo(function LeadContextBridge({
tabId,
projectId,
leadSessionId,
leadProviderId,
fallbackProjectRoot,
}: LeadContextBridgeProps): React.JSX.Element | null {
const {
@ -686,8 +696,15 @@ const LeadContextBridge = memo(function LeadContextBridge({
contextMetrics.contextUsedPercentOfContextWindow ?? leadContextSnapshot?.contextUsedPercent;
return percent === null || percent === undefined ? null : `${percent.toFixed(1)}%`;
}, [contextMetrics.contextUsedPercentOfContextWindow, leadContextSnapshot?.contextUsedPercent]);
const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId);
if (!leadSessionId) {
useEffect(() => {
if (!shouldShowLeadContextUi && isContextPanelVisible) {
setContextPanelVisible(false);
}
}, [isContextPanelVisible, setContextPanelVisible, shouldShowLeadContextUi]);
if (!leadSessionId || !shouldShowLeadContextUi) {
return null;
}
@ -1677,6 +1694,14 @@ export const TeamDetailView = ({
() => activeMembers.filter((m) => !isLeadMember(m)).length,
[activeMembers]
);
const leadProviderId = useMemo<TeamProviderId | undefined>(() => {
const activeLeadProviderId = activeMembers.find(isLeadMember)?.providerId;
if (activeLeadProviderId) return activeLeadProviderId;
const configuredLeadProviderId = data?.config.members?.find(isLeadMember)?.providerId;
if (configuredLeadProviderId) return configuredLeadProviderId;
return launchParams?.providerId;
}, [activeMembers, data?.config.members, launchParams?.providerId]);
const shouldShowLeadContextUi = canShowLeadContextUi(leadProviderId);
const taskMap = useMemo(() => new Map((data?.tasks ?? []).map((t) => [t.id, t])), [data?.tasks]);
const taskMapRef = useRef(taskMap);
@ -2068,7 +2093,7 @@ export const TeamDetailView = ({
isThisTabActive={isThisTabActive}
/>
);
const leadContextWatcher = (
const leadContextWatcher = shouldShowLeadContextUi ? (
<LeadContextWatcher
teamName={teamName}
tabId={tabId}
@ -2080,7 +2105,7 @@ export const TeamDetailView = ({
sessions={sessions}
sessionsLoading={sessionsLoading}
/>
);
) : null;
const renderBody = (): React.JSX.Element => {
if ((loading && !data) || (data && data.teamName !== teamName)) {
@ -2190,6 +2215,7 @@ export const TeamDetailView = ({
tabId={tabId}
projectId={projectId}
leadSessionId={leadSessionId}
leadProviderId={leadProviderId}
fallbackProjectRoot={data.config.projectPath}
/>

View file

@ -545,7 +545,7 @@ export const SendMessageDialog = ({
</button>
}
footerRight={
<div className="flex items-center gap-2">
<div className="flex flex-col items-end gap-1">
{sendError ? (
<span className="inline-flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-[10px] text-red-400">
<AlertCircle size={10} className="shrink-0" />
@ -557,16 +557,18 @@ export const SendMessageDialog = ({
debugDetails={sendDebugDetails}
/>
) : null}
{remaining < 200 ? (
<span
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
>
{remaining} chars left
</span>
) : null}
{textDraft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null}
<div className="flex items-center gap-2">
{remaining < 200 ? (
<span
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
>
{remaining} chars left
</span>
) : null}
{textDraft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null}
</div>
</div>
}
/>

View file

@ -935,18 +935,20 @@ export const MessageComposer = ({
isCompactLayout ? (
compactFooterNotice
) : (
<div className="flex items-center gap-2">
<div className="flex flex-col items-end gap-1">
{compactFooterNotice}
{remaining < 200 ? (
<span
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
>
{remaining} chars left
</span>
) : null}
{draft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null}
<div className="flex items-center gap-2">
{remaining < 200 ? (
<span
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
>
{remaining} chars left
</span>
) : null}
{draft.isSaved ? (
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
) : null}
</div>
</div>
)
}

View file

@ -18,6 +18,7 @@ import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
import { useStore } from '@renderer/store';
import { selectTeamMessages } from '@renderer/store/slices/teamSlice';
import type { OpenCodeRuntimeDeliveryDebugDetails } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { toMessageKey } from '@renderer/utils/teamMessageKey';
import { shouldExcludeInboxTextFromReplyCandidates } from '@shared/utils/idleNotificationSemantics';
@ -38,7 +39,11 @@ import {
import { useShallow } from 'zustand/react/shallow';
import { ActivityTimeline, type TimelineViewport } from '../activity/ActivityTimeline';
import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup';
import {
getThoughtGroupKey,
groupTimelineItems,
isLeadThought,
} from '../activity/LeadThoughtsGroup';
import { MessageExpandDialog } from '../activity/MessageExpandDialog';
import { CollapsibleTeamSection } from '../CollapsibleTeamSection';
import {
@ -140,7 +145,9 @@ export function reconcilePendingRepliesByMember(
continue;
}
if (message.to === 'user') {
// Team lead often answers through visible lead thoughts, which do not carry `to: 'user'`.
// Count them as replies so the pending-reply badge clears after the lead responds.
if (message.to === 'user' || isLeadThought(message)) {
const previous = latestReplyToUserByMember.get(message.from);
if (previous == null || ts > previous) {
latestReplyToUserByMember.set(message.from, ts);
@ -164,6 +171,51 @@ export function reconcilePendingRepliesByMember(
return changed ? next : pendingRepliesByMember;
}
function normalizeMessageParticipant(value: unknown): string {
return typeof value === 'string' ? value.trim().toLowerCase() : '';
}
export function hasVisibleReplyForSendMessageDiagnostics(
debugDetails: OpenCodeRuntimeDeliveryDebugDetails | null | undefined,
messages: readonly InboxMessage[]
): boolean {
const messageId = debugDetails?.messageId;
if (!messageId) {
return false;
}
const sentMessage = messages.find((message) => message.messageId === messageId);
if (
!sentMessage ||
sentMessage.from !== 'user' ||
typeof sentMessage.to !== 'string' ||
sentMessage.to.length === 0
) {
return false;
}
const recipient = normalizeMessageParticipant(sentMessage.to);
const sentAt = Date.parse(sentMessage.timestamp);
if (!recipient || !Number.isFinite(sentAt)) {
return false;
}
return messages.some((message) => {
if (message.messageId === sentMessage.messageId) {
return false;
}
if (normalizeMessageParticipant(message.from) !== recipient || message.to !== 'user') {
return false;
}
if (message.relayOfMessageId === messageId) {
return true;
}
const replyAt = Date.parse(message.timestamp);
return Number.isFinite(replyAt) && replyAt > sentAt;
});
}
export const MessagesPanel = memo(function MessagesPanel({
teamName,
position,
@ -194,6 +246,7 @@ export const MessagesPanel = memo(function MessagesPanel({
sendMessageWarning,
sendMessageDebugDetails,
lastSendMessageResult,
clearSendMessageRuntimeDiagnostics,
teams,
openTeamTab,
messages,
@ -209,6 +262,7 @@ export const MessagesPanel = memo(function MessagesPanel({
sendMessageWarning: s.sendMessageWarning,
sendMessageDebugDetails: s.sendMessageDebugDetails,
lastSendMessageResult: s.lastSendMessageResult,
clearSendMessageRuntimeDiagnostics: s.clearSendMessageRuntimeDiagnostics,
teams: s.teams,
openTeamTab: s.openTeamTab,
messages: selectTeamMessages(s, teamName),
@ -442,6 +496,14 @@ export const MessagesPanel = memo(function MessagesPanel({
),
[effectiveMessages]
);
const sendMessageRuntimeReplyVisible = useMemo(
() => hasVisibleReplyForSendMessageDiagnostics(sendMessageDebugDetails, effectiveMessages),
[effectiveMessages, sendMessageDebugDetails]
);
const effectiveSendMessageWarning = sendMessageRuntimeReplyVisible ? null : sendMessageWarning;
const effectiveSendMessageDebugDetails = sendMessageRuntimeReplyVisible
? null
: sendMessageDebugDetails;
// Resolve the expanded item from filtered messages
const expandedItem = useMemo<TimelineItem | null>(() => {
@ -507,6 +569,15 @@ export const MessagesPanel = memo(function MessagesPanel({
if (next !== pendingRepliesByMember) onPendingReplyChange(() => next);
}, [onPendingReplyChange, pendingRepliesByMember, replyCandidateMessages]);
useEffect(() => {
if (!sendMessageRuntimeReplyVisible || !sendMessageDebugDetails?.messageId) return;
clearSendMessageRuntimeDiagnostics(sendMessageDebugDetails.messageId);
}, [
clearSendMessageRuntimeDiagnostics,
sendMessageDebugDetails?.messageId,
sendMessageRuntimeReplyVisible,
]);
const handleSend = useCallback(
(
member: string,
@ -696,8 +767,8 @@ export const MessagesPanel = memo(function MessagesPanel({
isTeamAlive={isTeamAlive}
sending={sendingMessage}
sendError={sendMessageError}
sendWarning={sendMessageWarning}
sendDebugDetails={sendMessageDebugDetails}
sendWarning={effectiveSendMessageWarning}
sendDebugDetails={effectiveSendMessageDebugDetails}
lastResult={lastSendMessageResult}
textareaRef={composerTextareaRef}
onSend={handleSend}
@ -883,8 +954,8 @@ export const MessagesPanel = memo(function MessagesPanel({
isTeamAlive={isTeamAlive}
sending={sendingMessage}
sendError={sendMessageError}
sendWarning={sendMessageWarning}
sendDebugDetails={sendMessageDebugDetails}
sendWarning={effectiveSendMessageWarning}
sendDebugDetails={effectiveSendMessageDebugDetails}
lastResult={lastSendMessageResult}
textareaRef={composerTextareaRef}
onSend={handleSend}
@ -1169,8 +1240,8 @@ export const MessagesPanel = memo(function MessagesPanel({
isTeamAlive={isTeamAlive}
sending={sendingMessage}
sendError={sendMessageError}
sendWarning={sendMessageWarning}
sendDebugDetails={sendMessageDebugDetails}
sendWarning={effectiveSendMessageWarning}
sendDebugDetails={effectiveSendMessageDebugDetails}
lastResult={lastSendMessageResult}
textareaRef={composerTextareaRef}
onSend={handleSend}

View file

@ -1964,6 +1964,7 @@ export interface TeamSlice {
sendMessageWarning: string | null;
sendMessageDebugDetails: OpenCodeRuntimeDeliveryDebugDetails | null;
lastSendMessageResult: SendMessageResult | null;
clearSendMessageRuntimeDiagnostics: (messageId?: string | null) => void;
reviewActionError: string | null;
provisioningRuns: Record<string, TeamProvisioningProgress>;
/** Synthetic TeamSummary snapshots for teams currently being provisioned (before config.json exists). */
@ -4023,6 +4024,21 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
}
},
clearSendMessageRuntimeDiagnostics: (messageId?: string | null) => {
set((state) => {
if (messageId && state.sendMessageDebugDetails?.messageId !== messageId) {
return {};
}
if (!state.sendMessageWarning && !state.sendMessageDebugDetails) {
return {};
}
return {
sendMessageWarning: null,
sendMessageDebugDetails: null,
};
});
},
fetchCrossTeamTargets: async () => {
set({ crossTeamTargetsLoading: true });
try {

View file

@ -259,7 +259,7 @@ function buildPendingDiagnosticPhrase({
formatNamedPendingDiagnostic('Waiting for bootstrap', groups.runtimeProcess),
formatNamedPendingDiagnostic('Process candidates', groups.runtimeCandidate),
formatNamedPendingDiagnostic('Awaiting permission', groups.permission),
formatNamedPendingDiagnostic('No runtime found', groups.noRuntime),
formatNamedPendingDiagnostic('Waiting for runtime', groups.noRuntime),
].filter(Boolean);
if (namedParts.length > 0) {
return namedParts.join(', ');
@ -272,7 +272,7 @@ function buildPendingDiagnosticPhrase({
formatCountPendingDiagnostic(summary.runtimeProcessPendingCount, 'waiting for bootstrap'),
formatCountPendingDiagnostic(summary.runtimeCandidatePendingCount, 'process candidates'),
formatCountPendingDiagnostic(summary.permissionPendingCount, 'awaiting permission'),
formatCountPendingDiagnostic(summary.noRuntimePendingCount, 'no runtime found'),
formatCountPendingDiagnostic(summary.noRuntimePendingCount, 'waiting for runtime'),
].filter(Boolean);
return countParts.length > 0 ? countParts.join(', ') : fallbackJoiningPhrase;
}

View file

@ -2691,7 +2691,7 @@ describe('TeamProvisioningService', () => {
});
expect(
(svc as any).buildPendingBootstrapStatusMessage('Finishing launch', run, launchSummary)
).toContain('1 no runtime found');
).toContain('1 waiting for runtime');
});
it('trusts persisted snapshot permission state for pure teams when live run statuses are absent', () => {

View file

@ -102,7 +102,7 @@ describe('ProvisioningProgressBlock', () => {
memberName: 'tom',
severity: 'warning',
code: 'runtime_not_found',
label: 'tom - no runtime found',
label: 'tom - waiting for runtime',
detail: 'registered runtime metadata without live process',
observedAt: '2026-04-24T12:00:01.000Z',
},
@ -136,7 +136,7 @@ describe('ProvisioningProgressBlock', () => {
expect(host.textContent).toContain('bob - shell only');
expect(host.textContent).toContain('tmux pane foreground command is zsh');
expect(host.textContent).toContain('tom - no runtime found');
expect(host.textContent).toContain('tom - waiting for runtime');
expect(host.textContent).toContain('registered runtime metadata without live process');
expect(host.textContent).toContain('jack - process table unavailable');
expect(host.textContent).toContain(

View file

@ -2,16 +2,18 @@ import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { OpenCodeRuntimeDeliveryDebugDetails } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
import type { InboxMessage } from '@shared/types';
const storeState = {
sendTeamMessage: vi.fn().mockResolvedValue(undefined),
sendCrossTeamMessage: vi.fn().mockResolvedValue(undefined),
sendingMessage: false,
sendMessageError: null,
sendMessageWarning: null,
sendMessageDebugDetails: null,
lastSendMessageResult: null,
sendMessageError: null as string | null,
sendMessageWarning: null as string | null,
sendMessageDebugDetails: null as OpenCodeRuntimeDeliveryDebugDetails | null,
lastSendMessageResult: null as unknown,
clearSendMessageRuntimeDiagnostics: vi.fn(),
teams: [],
openTeamTab: vi.fn(),
loadOlderTeamMessages: vi.fn().mockResolvedValue(undefined),
@ -169,6 +171,7 @@ vi.mock('react-modal-sheet', () => ({
}));
import {
hasVisibleReplyForSendMessageDiagnostics,
MessagesPanel,
reconcilePendingRepliesByMember,
} from '@renderer/components/team/messages/MessagesPanel';
@ -200,8 +203,14 @@ describe('MessagesPanel idle summary invariants', () => {
storeState.sendTeamMessage.mockClear();
storeState.sendCrossTeamMessage.mockClear();
storeState.openTeamTab.mockClear();
storeState.clearSendMessageRuntimeDiagnostics.mockClear();
storeState.loadOlderTeamMessages.mockClear();
storeState.refreshTeamMessagesHead.mockClear();
storeState.sendingMessage = false;
storeState.sendMessageError = null;
storeState.sendMessageWarning = null;
storeState.sendMessageDebugDetails = null;
storeState.lastSendMessageResult = null;
storeState.teamMessagesByName = {};
sidebarUiState.messagesSearchQuery = '';
sidebarUiState.messagesFilter = { from: new Set(), to: new Set(), showNoise: false };
@ -417,6 +426,186 @@ describe('MessagesPanel idle summary invariants', () => {
expect(reconcilePendingRepliesByMember({ forge: pendingSentAtMs }, messages)).toEqual({});
});
it('clears pending replies when the team lead answers through a visible lead thought', () => {
const pendingSentAtMs = Date.parse('2026-04-08T12:00:00.000Z');
const messages: InboxMessage[] = [
makeMessage({
messageId: 'lead-thought-reply',
from: 'lead',
to: undefined,
source: 'lead_session',
timestamp: '2026-04-08T12:00:05.000Z',
text: 'Да, команда на месте.',
}),
];
expect(reconcilePendingRepliesByMember({ lead: pendingSentAtMs }, messages)).toEqual({});
});
it('keeps pending replies when the lead thought is older than the user message', () => {
const pendingSentAtMs = Date.parse('2026-04-08T12:00:00.000Z');
const pending = { lead: pendingSentAtMs };
const messages: InboxMessage[] = [
makeMessage({
messageId: 'older-lead-thought',
from: 'lead',
to: undefined,
source: 'lead_session',
timestamp: '2026-04-08T11:59:59.000Z',
text: 'Предыдущий статус.',
}),
];
expect(reconcilePendingRepliesByMember(pending, messages)).toBe(pending);
});
it('detects a visible OpenCode reply for pending runtime diagnostics', () => {
const messages: InboxMessage[] = [
makeMessage({
messageId: 'user-send',
from: 'user',
to: 'tom',
source: 'user_sent',
timestamp: '2026-04-08T12:00:00.000Z',
text: 'Тут?',
}),
makeMessage({
messageId: 'tom-reply',
from: 'tom',
to: 'user',
relayOfMessageId: 'user-send',
timestamp: '2026-04-08T12:00:05.000Z',
text: 'Да, я тут.',
}),
];
expect(
hasVisibleReplyForSendMessageDiagnostics(
{
messageId: 'user-send',
providerId: 'opencode',
delivered: true,
responsePending: true,
responseState: 'pending',
ledgerStatus: 'accepted',
acceptanceUnknown: false,
reason: 'assistant_response_pending',
diagnostics: ['assistant_response_pending'],
},
messages
)
).toBe(true);
});
it('does not treat older member messages as OpenCode replies for pending diagnostics', () => {
const messages: InboxMessage[] = [
makeMessage({
messageId: 'tom-old-reply',
from: 'tom',
to: 'user',
timestamp: '2026-04-08T11:59:59.000Z',
text: 'Предыдущий ответ.',
}),
makeMessage({
messageId: 'user-send',
from: 'user',
to: 'tom',
source: 'user_sent',
timestamp: '2026-04-08T12:00:00.000Z',
text: 'Тут?',
}),
];
expect(
hasVisibleReplyForSendMessageDiagnostics(
{
messageId: 'user-send',
providerId: 'opencode',
delivered: true,
responsePending: true,
responseState: 'pending',
ledgerStatus: 'accepted',
acceptanceUnknown: false,
reason: 'assistant_response_pending',
diagnostics: ['assistant_response_pending'],
},
messages
)
).toBe(false);
});
it('clears stale OpenCode runtime diagnostics once the member reply is visible', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
const messages: InboxMessage[] = [
makeMessage({
messageId: 'user-send',
from: 'user',
to: 'tom',
source: 'user_sent',
timestamp: '2026-04-08T12:00:00.000Z',
text: 'Тут?',
}),
makeMessage({
messageId: 'tom-reply',
from: 'tom',
to: 'user',
timestamp: '2026-04-08T12:00:05.000Z',
text: 'Да, я тут.',
}),
];
storeState.sendMessageWarning =
'OpenCode runtime delivery is still being checked. Message was saved and will be retried if needed.';
storeState.sendMessageDebugDetails = {
messageId: 'user-send',
providerId: 'opencode',
delivered: true,
responsePending: true,
responseState: 'pending',
ledgerStatus: 'accepted',
acceptanceUnknown: false,
reason: 'assistant_response_pending',
diagnostics: ['assistant_response_pending'],
};
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',
position: 'sidebar',
onPositionChange: vi.fn(),
members: [],
tasks: [],
timeWindow: null,
pendingRepliesByMember: {},
onPendingReplyChange: vi.fn(),
})
);
await Promise.resolve();
});
expect(storeState.clearSendMessageRuntimeDiagnostics).toHaveBeenCalledWith('user-send');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('renders the bottom-sheet composer before the status block so input stays pinned near the header', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');

View file

@ -328,6 +328,40 @@ describe('teamSlice actions', () => {
});
});
it('clears OpenCode runtime diagnostics only for the matching message id', async () => {
const store = createSliceStore();
hoisted.sendMessage.mockResolvedValue({
deliveredToInbox: true,
messageId: 'm-opencode-pending',
runtimeDelivery: {
providerId: 'opencode',
attempted: true,
delivered: true,
responsePending: true,
responseState: 'pending',
ledgerStatus: 'accepted',
acceptanceUnknown: false,
reason: 'assistant_response_pending',
diagnostics: ['assistant_response_pending'],
},
});
await store.getState().sendTeamMessage('my-team', {
member: 'bob',
text: 'hello',
});
store.getState().clearSendMessageRuntimeDiagnostics('other-message');
expect(store.getState().sendMessageWarning).toBe(
'OpenCode runtime delivery is still being checked. Message was saved and will be retried if needed.'
);
expect(store.getState().sendMessageDebugDetails?.messageId).toBe('m-opencode-pending');
store.getState().clearSendMessageRuntimeDiagnostics('m-opencode-pending');
expect(store.getState().sendMessageWarning).toBeNull();
expect(store.getState().sendMessageDebugDetails).toBeNull();
});
it('clears OpenCode runtime diagnostics after normal success or send failure', async () => {
const store = createSliceStore();
hoisted.sendMessage

View file

@ -893,8 +893,8 @@ describe('buildTeamProvisioningPresentation', () => {
});
expect(presentation?.compactTitle).toBe('Finishing launch');
expect(presentation?.compactDetail).toBe('No runtime found: alice, bob');
expect(presentation?.panelMessage).toBe('No runtime found: alice, bob');
expect(presentation?.compactDetail).toBe('Waiting for runtime: alice, bob');
expect(presentation?.panelMessage).toBe('Waiting for runtime: alice, bob');
});
it('names live pending diagnostics without duplicating permission-blocked teammates', () => {