fix(team): surface provider retries in reply UI

This commit is contained in:
iliya 2026-04-07 12:42:37 +03:00
parent 8ebde439a8
commit 7f737f985b
4 changed files with 50 additions and 12 deletions

View file

@ -12,7 +12,7 @@ import {
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import { nameColorSet } from '@renderer/utils/projectColor'; import { nameColorSet } from '@renderer/utils/projectColor';
import { formatDistanceToNowStrict } from 'date-fns'; import { formatDistanceToNowStrict } from 'date-fns';
import { ShieldQuestion, Users } from 'lucide-react'; import { Loader2, ShieldQuestion, Users } from 'lucide-react';
import type { ResolvedTeamMember } from '@shared/types'; import type { ResolvedTeamMember } from '@shared/types';
@ -83,6 +83,7 @@ export const PendingRepliesBlock = ({
); );
const advisoryLabel = getMemberRuntimeAdvisoryLabel(member.runtimeAdvisory); const advisoryLabel = getMemberRuntimeAdvisoryLabel(member.runtimeAdvisory);
const advisoryTitle = getMemberRuntimeAdvisoryTitle(member.runtimeAdvisory); const advisoryTitle = getMemberRuntimeAdvisoryTitle(member.runtimeAdvisory);
const isRetrying = advisoryLabel !== null;
return ( return (
<article <article
@ -103,8 +104,12 @@ export const PendingRepliesBlock = ({
loading="lazy" loading="lazy"
/> />
<span className="absolute -bottom-0.5 -right-0.5 flex size-2.5"> <span className="absolute -bottom-0.5 -right-0.5 flex size-2.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" /> <span
<span className="relative inline-flex size-full rounded-full bg-emerald-500" /> className={`absolute inline-flex size-full animate-ping rounded-full opacity-70 ${isRetrying ? 'bg-amber-400' : 'bg-emerald-400'}`}
/>
<span
className={`relative inline-flex size-full rounded-full ${isRetrying ? 'bg-amber-500' : 'bg-emerald-500'}`}
/>
</span> </span>
</span> </span>
{onMemberClick ? ( {onMemberClick ? (
@ -139,12 +144,15 @@ export const PendingRepliesBlock = ({
</span> </span>
) : null} ) : null}
<span <span
className="min-w-0 flex-1 truncate text-[10px]" className={`min-w-0 flex-1 truncate text-[10px] ${isRetrying ? 'text-amber-300' : ''}`}
style={{ color: CARD_ICON_MUTED }} style={isRetrying ? undefined : { color: CARD_ICON_MUTED }}
title={advisoryTitle ?? 'Message sent, awaiting reply'} title={advisoryTitle ?? 'Message sent, awaiting reply'}
> >
{advisoryLabel ?? 'awaiting reply'} {advisoryLabel ?? 'awaiting reply'}
</span> </span>
{isRetrying ? (
<Loader2 className="size-3 shrink-0 animate-spin text-amber-400" />
) : null}
<span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}> <span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{since} {since}
</span> </span>

View file

@ -179,11 +179,11 @@ export const MemberCard = ({
{!activityTask && isAwaitingReply ? ( {!activityTask && isAwaitingReply ? (
<> <>
<Loader2 <Loader2
className="size-3 shrink-0 animate-spin" className={`size-3 shrink-0 animate-spin ${runtimeAdvisoryLabel ? 'text-amber-400' : ''}`}
style={{ color: colors.border }} style={runtimeAdvisoryLabel ? undefined : { color: colors.border }}
/> />
<span <span
className="shrink-0 text-[10px] text-[var(--color-text-muted)]" className={`shrink-0 text-[10px] ${runtimeAdvisoryLabel ? 'text-amber-300' : 'text-[var(--color-text-muted)]'}`}
title={runtimeAdvisoryTitle ?? 'Message sent, awaiting reply'} title={runtimeAdvisoryTitle ?? 'Message sent, awaiting reply'}
> >
{runtimeAdvisoryLabel ?? 'awaiting reply'} {runtimeAdvisoryLabel ?? 'awaiting reply'}

View file

@ -214,13 +214,13 @@ export function getMemberRuntimeAdvisoryLabel(
} }
const retryUntilMs = Date.parse(advisory.retryUntil); const retryUntilMs = Date.parse(advisory.retryUntil);
if (!Number.isFinite(retryUntilMs)) { if (!Number.isFinite(retryUntilMs)) {
return 'SDK retrying'; return 'retrying now';
} }
const remainingMs = retryUntilMs - nowMs; const remainingMs = retryUntilMs - nowMs;
if (remainingMs <= 0) { if (remainingMs <= 0) {
return 'SDK retrying'; return 'retrying now';
} }
return `SDK retrying · ${formatRetryCountdown(remainingMs)}`; return `retrying now · ${formatRetryCountdown(remainingMs)}`;
} }
export function getMemberRuntimeAdvisoryTitle( export function getMemberRuntimeAdvisoryTitle(
@ -229,7 +229,10 @@ export function getMemberRuntimeAdvisoryTitle(
if (!advisory || advisory.kind !== 'sdk_retrying') { if (!advisory || advisory.kind !== 'sdk_retrying') {
return undefined; return undefined;
} }
return advisory.message?.trim() || 'The SDK is retrying after a provider error.'; return (
advisory.message?.trim() ||
'The SDK is retrying this request after a provider or backend error.'
);
} }
export const TASK_STATUS_STYLES: Record<TeamTaskStatus, { bg: string; text: string }> = { export const TASK_STATUS_STYLES: Record<TeamTaskStatus, { bg: string; text: string }> = {

View file

@ -1,6 +1,8 @@
import { import {
getSpawnAwareDotClass, getSpawnAwareDotClass,
getSpawnAwarePresenceLabel, getSpawnAwarePresenceLabel,
getMemberRuntimeAdvisoryLabel,
getMemberRuntimeAdvisoryTitle,
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import type { ResolvedTeamMember } from '@shared/types'; import type { ResolvedTeamMember } from '@shared/types';
@ -59,4 +61,29 @@ describe('memberHelpers spawn-aware presence', () => {
) )
).toBe('starting'); ).toBe('starting');
}); });
it('renders unified retry advisory labels for provider retries', () => {
expect(
getMemberRuntimeAdvisoryLabel(
{
kind: 'sdk_retrying',
observedAt: '2026-04-07T09:00:00.000Z',
retryUntil: '2026-04-07T09:00:45.000Z',
retryDelayMs: 45_000,
message: 'Gemini cli backend error: capacity exceeded.',
},
Date.parse('2026-04-07T09:00:00.000Z')
)
).toBe('retrying now · 45s');
expect(
getMemberRuntimeAdvisoryTitle({
kind: 'sdk_retrying',
observedAt: '2026-04-07T09:00:00.000Z',
retryUntil: '2026-04-07T09:00:45.000Z',
retryDelayMs: 45_000,
message: 'Gemini cli backend error: capacity exceeded.',
})
).toContain('capacity exceeded');
});
}); });