fix(team-ui): polish launch diagnostics controls

This commit is contained in:
777genius 2026-05-22 15:43:36 +03:00
parent 7b99a3713b
commit 7e6ebce093
8 changed files with 411 additions and 432 deletions

View file

@ -206,10 +206,8 @@ export const PluginsPanel = ({
return ( return (
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-sm text-amber-300"> <div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-sm text-amber-300">
In the multimodel runtime, plugins are currently guaranteed only for Anthropic Plugin support is currently guaranteed for Anthropic (Claude) sessions only.
sessions. We are actively building broader plugin support for all agents, including We&apos;re working to support plugins across all agents.
both universal plugins and agent-specific plugins.
{capability.reason ? ` ${capability.reason}` : ''}
</div> </div>
); );
})()} })()}

View file

@ -41,8 +41,8 @@ export const SkipPermissionsCheckbox: React.FC<SkipPermissionsCheckboxProps> = (
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<Info className="mt-0.5 size-3.5 shrink-0 text-blue-400" /> <Info className="mt-0.5 size-3.5 shrink-0 text-blue-400" />
<p> <p>
Unleash Claude&apos;s full power no interruptions asking for permission. Autonomous Autonomous mode: team tools execute without confirmation. Be cautious with untrusted
mode all tools execute without confirmation. Be cautious with untrusted code. code.
</p> </p>
</div> </div>
</div> </div>
@ -57,7 +57,7 @@ export const SkipPermissionsCheckbox: React.FC<SkipPermissionsCheckboxProps> = (
> >
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<Info className="mt-0.5 size-3.5 shrink-0 text-blue-400" /> <Info className="mt-0.5 size-3.5 shrink-0 text-blue-400" />
<p>Manual mode you&apos;ll approve or deny each tool call in real-time.</p> <p>Manual mode: you&apos;ll approve or deny each tool call in real time.</p>
</div> </div>
</div> </div>
)} )}

View file

@ -1043,7 +1043,7 @@ export const MemberCard = memo(function MemberCard({
<MemberRuntimeTelemetryStrip runtimeEntry={runtimeEntry} scale={runtimeTelemetryScale} /> <MemberRuntimeTelemetryStrip runtimeEntry={runtimeEntry} scale={runtimeTelemetryScale} />
) : null} ) : null}
<div className="pointer-events-none absolute inset-0 z-10 rounded transition-colors group-hover:bg-white/5" /> <div className="pointer-events-none absolute inset-0 z-10 rounded transition-colors group-hover:bg-white/5" />
<div className="relative z-20 flex items-center gap-2.5"> <div className="relative z-20 grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2.5 gap-y-1">
<div className="relative shrink-0"> <div className="relative shrink-0">
<div <div
className="rounded-full border-2 p-px" className="rounded-full border-2 p-px"
@ -1166,6 +1166,7 @@ export const MemberCard = memo(function MemberCard({
<MemberLaunchDiagnosticsButton <MemberLaunchDiagnosticsButton
payload={launchDiagnosticsPayload} payload={launchDiagnosticsPayload}
className="size-auto rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200" className="size-auto rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200"
attention
/> />
) : null} ) : null}
</> </>
@ -1201,336 +1202,348 @@ export const MemberCard = memo(function MemberCard({
) : null} ) : null}
</div> </div>
) : null} ) : null}
{launchFailureReason ? ( </div>
<div <div className="flex shrink-0 items-center gap-2.5 justify-self-end">
data-testid="member-launch-failure-reason" {showLaunchBadge ? (
className="mt-1 min-w-0 whitespace-pre-wrap break-words text-[10px] font-medium leading-snug text-red-300/90" <span
title={rawLaunchFailureReason} className="flex shrink-0 items-center gap-1"
title={runtimeEntry?.runtimeDiagnostic}
> >
<span> {launchVisualState === 'starting_stale' ? (
{renderLinkifiedText(launchFailureReason, { <AlertTriangle
linkClassName: 'underline underline-offset-2 hover:text-red-200', className="size-3.5 shrink-0 text-amber-400"
stopPropagation: true, aria-label={launchBadgeLabel}
getLinkLabel: getLaunchFailureLinkLabel, />
})} ) : (
</span> <SyncedLoader2
className="size-3.5 shrink-0 text-[var(--color-text-muted)]"
aria-label={launchBadgeLabel}
/>
)}
<Badge
variant="secondary"
className="shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]"
>
{launchBadgeLabel}
</Badge>
{canRelaunchOpenCode ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-amber-300 transition-colors hover:bg-amber-500/10 hover:text-amber-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
</span>
) : showFailedLaunchBadge ? (
<span className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1">
<AlertTriangle className="size-3.5 shrink-0 text-red-400" />
<Badge
variant="secondary"
className="shrink-0 bg-red-500/15 px-1.5 py-0.5 text-[10px] font-normal leading-none text-red-400"
>
{displayPresenceLabel}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">{spawnError ?? 'Spawn failed'}</TooltipContent>
</Tooltip>
{showCopyDiagnostics ? (
<MemberLaunchDiagnosticsButton
payload={launchDiagnosticsPayload}
className="size-auto rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200"
attention
/>
) : null}
{canSkipFailedLaunch ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={skippingLaunch ? 'Skipping teammate' : 'Skip for this launch'}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={skippingLaunch || retryingLaunch}
onClick={handleSkipFailedLaunch}
>
{skippingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<Ban className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{skipLaunchError ??
(skippingLaunch ? 'Skipping teammate...' : 'Skip for this launch')}
</TooltipContent>
</Tooltip>
) : null}
{canRetryLaunch ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch || skippingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? `${restartActionBusyLabel}...` : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
</span>
) : showSkippedLaunchBadge ? (
<span className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1">
<Ban className="size-3.5 shrink-0 text-zinc-400" />
<Badge
variant="secondary"
className="shrink-0 bg-zinc-500/15 px-1.5 py-0.5 text-[10px] font-normal leading-none text-zinc-300"
>
{displayPresenceLabel}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{spawnEntry?.skipReason ?? 'Skipped for this launch'}
</TooltipContent>
</Tooltip>
{canRetryLaunch ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-zinc-300 transition-colors hover:bg-zinc-500/10 hover:text-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? `${restartActionBusyLabel}...` : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
</span>
) : showRuntimeAdvisoryBadge ? (
<span className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1">
<AlertTriangle
className={`size-3.5 shrink-0 ${
runtimeAdvisoryTone === 'error' ? 'text-red-400' : 'text-amber-400'
}`}
/>
<Badge
variant="secondary"
className={`shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none ${
runtimeAdvisoryTone === 'error'
? 'bg-red-500/15 text-red-300'
: 'bg-amber-500/15 text-amber-300'
}`}
title={runtimeAdvisoryTitle}
>
{runtimeAdvisoryLabel}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{runtimeAdvisoryTitle ?? runtimeAdvisoryLabel}
</TooltipContent>
</Tooltip>
{canRelaunchRuntimeAdvisoryOpenCode ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={
retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel
}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? `${restartActionBusyLabel}...` : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
{showRuntimeAdvisoryDiagnostics ? (
<MemberLaunchDiagnosticsButton
payload={launchDiagnosticsPayload}
className="size-auto rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200"
attention
/>
) : null}
</span>
) : !activityTask ? (
<Badge
variant="secondary"
className={`shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none ${isRemoved ? 'bg-zinc-600 text-zinc-300' : 'text-[var(--color-text-muted)]'}`}
title={isRemoved ? 'This member has been removed' : activityTitle}
>
{isRemoved ? 'removed' : displayPresenceLabel}
</Badge>
) : null}
{showStartingSkeleton ? (
<div className="shrink-0" aria-hidden="true">
<div
className="skeleton-shimmer h-[18px] w-[62px] rounded-full border"
style={{
backgroundColor: 'var(--skeleton-base-dim)',
borderColor: 'var(--color-border)',
}}
/>
<div
className="skeleton-shimmer mx-1 mt-1 h-[2px] w-10 rounded-full"
style={{ backgroundColor: 'var(--skeleton-base)' }}
/>
</div> </div>
) : (
<div
className="shrink-0"
title={totalTasks > 0 ? `${completed}/${totalTasks} completed` : undefined}
>
<Badge
variant="secondary"
className="shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none"
>
{member.taskCount} {member.taskCount === 1 ? 'task' : 'tasks'}
</Badge>
{totalTasks > 0 && (
<div className="mx-0.5 mt-0.5 h-[2px] rounded-full bg-[var(--color-border)]">
<div
className="h-full rounded-full bg-emerald-500 transition-all duration-500"
style={{ width: `${progressPercent}%` }}
/>
</div>
)}
{/* NOTE: lead context bar disabled — usage formula is inaccurate */}
</div>
)}
{!isRemoved && (
<div className="flex shrink-0 items-center gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
onClick={(e) => {
e.stopPropagation();
onSendMessage?.();
}}
>
<MessageSquare size={13} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Send message</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
onClick={(e) => {
e.stopPropagation();
onAssignTask?.();
}}
>
<Plus size={13} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Assign task</TooltipContent>
</Tooltip>
</div>
)}
{canRestoreMember ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={restoringMember ? 'Restoring teammate' : 'Restore teammate'}
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-60"
disabled={restoringMember}
onClick={handleRestoreMember}
>
{restoringMember ? (
<SyncedLoader2 className="size-3.5" />
) : (
<Undo2 className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{restoreMemberError ?? (restoringMember ? 'Restoring teammate...' : 'Restore')}
</TooltipContent>
</Tooltip>
) : null} ) : null}
</div> </div>
{showLaunchBadge ? ( {launchFailureReason ? (
<span
className="flex shrink-0 items-center gap-1"
title={runtimeEntry?.runtimeDiagnostic}
>
{launchVisualState === 'starting_stale' ? (
<AlertTriangle
className="size-3.5 shrink-0 text-amber-400"
aria-label={launchBadgeLabel}
/>
) : (
<SyncedLoader2
className="size-3.5 shrink-0 text-[var(--color-text-muted)]"
aria-label={launchBadgeLabel}
/>
)}
<Badge
variant="secondary"
className="shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]"
>
{launchBadgeLabel}
</Badge>
{canRelaunchOpenCode ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
className="rounded p-1 text-amber-300 transition-colors hover:bg-amber-500/10 hover:text-amber-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
</span>
) : showFailedLaunchBadge ? (
<span className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1">
<AlertTriangle className="size-3.5 shrink-0 text-red-400" />
<Badge
variant="secondary"
className="shrink-0 bg-red-500/15 px-1.5 py-0.5 text-[10px] font-normal leading-none text-red-400"
>
{displayPresenceLabel}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">{spawnError ?? 'Spawn failed'}</TooltipContent>
</Tooltip>
{showCopyDiagnostics ? (
<MemberLaunchDiagnosticsButton
payload={launchDiagnosticsPayload}
className="size-auto rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200"
/>
) : null}
{canSkipFailedLaunch ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={skippingLaunch ? 'Skipping teammate' : 'Skip for this launch'}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={skippingLaunch || retryingLaunch}
onClick={handleSkipFailedLaunch}
>
{skippingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<Ban className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{skipLaunchError ??
(skippingLaunch ? 'Skipping teammate...' : 'Skip for this launch')}
</TooltipContent>
</Tooltip>
) : null}
{canRetryLaunch ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch || skippingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? `${restartActionBusyLabel}...` : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
</span>
) : showSkippedLaunchBadge ? (
<span className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1">
<Ban className="size-3.5 shrink-0 text-zinc-400" />
<Badge
variant="secondary"
className="shrink-0 bg-zinc-500/15 px-1.5 py-0.5 text-[10px] font-normal leading-none text-zinc-300"
>
{displayPresenceLabel}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{spawnEntry?.skipReason ?? 'Skipped for this launch'}
</TooltipContent>
</Tooltip>
{canRetryLaunch ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
className="rounded p-1 text-zinc-300 transition-colors hover:bg-zinc-500/10 hover:text-zinc-100 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? `${restartActionBusyLabel}...` : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
</span>
) : showRuntimeAdvisoryBadge ? (
<span className="flex shrink-0 items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1">
<AlertTriangle
className={`size-3.5 shrink-0 ${
runtimeAdvisoryTone === 'error' ? 'text-red-400' : 'text-amber-400'
}`}
/>
<Badge
variant="secondary"
className={`shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none ${
runtimeAdvisoryTone === 'error'
? 'bg-red-500/15 text-red-300'
: 'bg-amber-500/15 text-amber-300'
}`}
title={runtimeAdvisoryTitle}
>
{runtimeAdvisoryLabel}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
{runtimeAdvisoryTitle ?? runtimeAdvisoryLabel}
</TooltipContent>
</Tooltip>
{canRelaunchRuntimeAdvisoryOpenCode ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel}
className="rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={retryingLaunch}
onClick={handleRestartMember}
>
{retryingLaunch ? (
<SyncedLoader2 className="size-3.5" />
) : (
<RotateCcw className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{retryLaunchError ??
(retryingLaunch ? `${restartActionBusyLabel}...` : restartActionIdleLabel)}
</TooltipContent>
</Tooltip>
) : null}
{showRuntimeAdvisoryDiagnostics ? (
<MemberLaunchDiagnosticsButton
payload={launchDiagnosticsPayload}
className="size-auto rounded p-1 text-red-300 transition-colors hover:bg-red-500/10 hover:text-red-200"
/>
) : null}
</span>
) : !activityTask ? (
<Badge
variant="secondary"
className={`shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none ${isRemoved ? 'bg-zinc-600 text-zinc-300' : 'text-[var(--color-text-muted)]'}`}
title={isRemoved ? 'This member has been removed' : activityTitle}
>
{isRemoved ? 'removed' : displayPresenceLabel}
</Badge>
) : null}
{showStartingSkeleton ? (
<div className="shrink-0" aria-hidden="true">
<div
className="skeleton-shimmer h-[18px] w-[62px] rounded-full border"
style={{
backgroundColor: 'var(--skeleton-base-dim)',
borderColor: 'var(--color-border)',
}}
/>
<div
className="skeleton-shimmer mx-1 mt-1 h-[2px] w-10 rounded-full"
style={{ backgroundColor: 'var(--skeleton-base)' }}
/>
</div>
) : (
<div <div
className="shrink-0" data-testid="member-launch-failure-reason"
title={totalTasks > 0 ? `${completed}/${totalTasks} completed` : undefined} className="col-span-2 col-start-2 min-w-0 whitespace-pre-wrap break-words text-[10px] font-medium leading-snug text-red-300/90"
title={rawLaunchFailureReason}
> >
<Badge <span>
variant="secondary" {renderLinkifiedText(launchFailureReason, {
className="shrink-0 px-1.5 py-0.5 text-[10px] font-normal leading-none" linkClassName: 'underline underline-offset-2 hover:text-red-200',
> stopPropagation: true,
{member.taskCount} {member.taskCount === 1 ? 'task' : 'tasks'} getLinkLabel: getLaunchFailureLinkLabel,
</Badge> })}
{totalTasks > 0 && ( </span>
<div className="mx-0.5 mt-0.5 h-[2px] rounded-full bg-[var(--color-border)]">
<div
className="h-full rounded-full bg-emerald-500 transition-all duration-500"
style={{ width: `${progressPercent}%` }}
/>
</div>
)}
{/* NOTE: lead context bar disabled — usage formula is inaccurate */}
</div> </div>
)}
{!isRemoved && (
<div className="flex shrink-0 items-center gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
onClick={(e) => {
e.stopPropagation();
onSendMessage?.();
}}
>
<MessageSquare size={13} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Send message</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)]"
onClick={(e) => {
e.stopPropagation();
onAssignTask?.();
}}
>
<Plus size={13} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">Assign task</TooltipContent>
</Tooltip>
</div>
)}
{canRestoreMember ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={restoringMember ? 'Restoring teammate' : 'Restore teammate'}
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-60"
disabled={restoringMember}
onClick={handleRestoreMember}
>
{restoringMember ? (
<SyncedLoader2 className="size-3.5" />
) : (
<Undo2 className="size-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
{restoreMemberError ?? (restoringMember ? 'Restoring teammate...' : 'Restore')}
</TooltipContent>
</Tooltip>
) : null} ) : null}
</div> </div>
</div> </div>
@ -1549,7 +1562,7 @@ export const MemberCard = memo(function MemberCard({
> >
<TooltipTrigger asChild>{cardContent}</TooltipTrigger> <TooltipTrigger asChild>{cardContent}</TooltipTrigger>
<TooltipContent <TooltipContent
side="top" side="left"
align="start" align="start"
sideOffset={8} sideOffset={8}
className="border-blue-400/20 bg-[var(--color-surface)] p-3 shadow-xl shadow-black/30" className="border-blue-400/20 bg-[var(--color-surface)] p-3 shadow-xl shadow-black/30"

View file

@ -2,6 +2,7 @@ import { useState } from 'react';
import { Button } from '@renderer/components/ui/button'; 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 { cn } from '@renderer/lib/utils';
import { import {
formatMemberLaunchDiagnosticsPayload, formatMemberLaunchDiagnosticsPayload,
type MemberLaunchDiagnosticsPayload, type MemberLaunchDiagnosticsPayload,
@ -13,6 +14,7 @@ interface MemberLaunchDiagnosticsButtonProps {
label?: string; label?: string;
className?: string; className?: string;
size?: 'icon' | 'sm'; size?: 'icon' | 'sm';
attention?: boolean;
} }
export const MemberLaunchDiagnosticsButton = ({ export const MemberLaunchDiagnosticsButton = ({
@ -20,6 +22,7 @@ export const MemberLaunchDiagnosticsButton = ({
label, label,
className, className,
size = label ? 'sm' : 'icon', size = label ? 'sm' : 'icon',
attention = false,
}: MemberLaunchDiagnosticsButtonProps): React.JSX.Element => { }: MemberLaunchDiagnosticsButtonProps): React.JSX.Element => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -45,7 +48,7 @@ export const MemberLaunchDiagnosticsButton = ({
type="button" type="button"
variant="ghost" variant="ghost"
size={size} size={size}
className={className} className={cn(className, attention && !copied && 'member-launch-diagnostics-pulse')}
title={tooltip} title={tooltip}
aria-label={tooltip} aria-label={tooltip}
onClick={copyDiagnostics} onClick={copyDiagnostics}

View file

@ -1056,6 +1056,44 @@ a[href],
} }
} }
@keyframes member-diagnostics-attention-fill {
0%,
100% {
background-color: rgba(239, 68, 68, 0.1);
}
50% {
background-color: rgba(239, 68, 68, 0.22);
}
}
@keyframes member-diagnostics-attention-ring {
0% {
opacity: 0.7;
transform: scale(0.92);
}
70%,
100% {
opacity: 0;
transform: scale(1.38);
}
}
.member-launch-diagnostics-pulse {
position: relative;
overflow: visible;
animation: member-diagnostics-attention-fill 1.7s ease-in-out infinite;
}
.member-launch-diagnostics-pulse::after {
content: '';
position: absolute;
inset: -2px;
border: 1px solid rgba(248, 113, 113, 0.55);
border-radius: inherit;
pointer-events: none;
animation: member-diagnostics-attention-ring 1.7s ease-out infinite;
}
/* Skeleton-style shimmer for waiting members: a translucent light sweep */ /* Skeleton-style shimmer for waiting members: a translucent light sweep */
.member-waiting-shimmer { .member-waiting-shimmer {
position: relative; position: relative;
@ -1653,6 +1691,8 @@ a[href],
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.kanban-comment-badge-pulse, .kanban-comment-badge-pulse,
.member-launch-diagnostics-pulse,
.member-launch-diagnostics-pulse::after,
.message-composer-orbit-path, .message-composer-orbit-path,
.message-composer-orbit-glow { .message-composer-orbit-glow {
animation: none; animation: none;

View file

@ -8,97 +8,6 @@
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="./favicon.png" /> <link rel="icon" type="image/png" href="./favicon.png" />
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/01.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/02.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/03.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/04.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/05.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/06.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/07.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/08.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/09.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/10.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/11.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/12.png"
fetchpriority="high"
/>
<link
rel="preload"
as="image"
type="image/png"
href="./assets/participant-avatars/13.png"
fetchpriority="high"
/>
<title>Agent Teams AI</title> <title>Agent Teams AI</title>
<style> <style>
/* Splash: animated gradient background */ /* Splash: animated gradient background */

View file

@ -1,5 +1,6 @@
import React, { act } from 'react'; import React, { act } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { CliInstallationStatus } from '@shared/types'; import type { CliInstallationStatus } from '@shared/types';
@ -214,7 +215,7 @@ describe('PluginsPanel effective runtime status', () => {
}); });
expect(host.textContent).not.toContain( expect(host.textContent).not.toContain(
'In the multimodel runtime, plugins currently apply only to Anthropic sessions.' 'Plugin support is currently guaranteed for Anthropic (Claude) sessions only.'
); );
expect(host.textContent).not.toContain('Codex bootstrap placeholder'); expect(host.textContent).not.toContain('Codex bootstrap placeholder');
@ -224,7 +225,7 @@ describe('PluginsPanel effective runtime status', () => {
}); });
}); });
it('explains that broader plugin support for all agents is actively being built when Codex plugins are not supported yet', async () => { it('explains that plugin support is guaranteed only for Anthropic sessions when Codex plugins are not supported yet', async () => {
const host = document.createElement('div'); const host = document.createElement('div');
document.body.appendChild(host); document.body.appendChild(host);
const root = createRoot(host); const root = createRoot(host);
@ -257,12 +258,10 @@ describe('PluginsPanel effective runtime status', () => {
}); });
expect(host.textContent).toContain( expect(host.textContent).toContain(
'plugins are currently guaranteed only for Anthropic sessions' "Plugin support is currently guaranteed for Anthropic (Claude) sessions only. We're working to support plugins across all agents."
); );
expect(host.textContent).toContain( expect(host.textContent).not.toContain('multimodel runtime');
'We are actively building broader plugin support for all agents' expect(host.textContent).not.toContain('Codex bootstrap placeholder');
);
expect(host.textContent).toContain('universal plugins and agent-specific plugins');
await act(async () => { await act(async () => {
root.unmount(); root.unmount();

View file

@ -73,11 +73,20 @@ vi.mock('@renderer/components/ui/tooltip', () => ({
React.createElement(React.Fragment, null, children), React.createElement(React.Fragment, null, children),
TooltipContent: ({ TooltipContent: ({
children, children,
side,
align,
className, className,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
side?: string;
align?: string;
className?: string; className?: string;
}) => React.createElement('div', { className, 'data-testid': 'tooltip-content' }, children), }) =>
React.createElement(
'div',
{ className, 'data-align': align, 'data-side': side, 'data-testid': 'tooltip-content' },
children
),
})); }));
vi.mock('@renderer/hooks/useTheme', () => ({ vi.mock('@renderer/hooks/useTheme', () => ({
@ -866,6 +875,10 @@ describe('MemberCard starting-state visuals', () => {
expect( expect(
host.querySelector('[data-testid="tooltip-root"][data-delay-duration="0"]') host.querySelector('[data-testid="tooltip-root"][data-delay-duration="0"]')
).not.toBeNull(); ).not.toBeNull();
const runtimeTooltipContent = Array.from(
host.querySelectorAll('[data-testid="tooltip-content"]')
).find((content) => content.className.includes('border-blue-400/20'));
expect(runtimeTooltipContent?.getAttribute('data-side')).toBe('left');
expect(host.querySelector('[data-testid="tooltip-root"]')?.getAttribute('data-open')).toBe( expect(host.querySelector('[data-testid="tooltip-root"]')?.getAttribute('data-open')).toBe(
'false' 'false'
); );
@ -1148,6 +1161,7 @@ describe('MemberCard starting-state visuals', () => {
const button = host.querySelector('[aria-label="Copy diagnostics"]') as HTMLButtonElement; const button = host.querySelector('[aria-label="Copy diagnostics"]') as HTMLButtonElement;
expect(button).not.toBeNull(); expect(button).not.toBeNull();
expect(button.className).toContain('member-launch-diagnostics-pulse');
await act(async () => { await act(async () => {
button.click(); button.click();
@ -1163,6 +1177,7 @@ describe('MemberCard starting-state visuals', () => {
expect(payload.runId).toBe('run-42'); expect(payload.runId).toBe('run-42');
expect(payload.livenessKind).toBe('not_found'); expect(payload.livenessKind).toBe('not_found');
expect(payload.processCommand).toContain('--token [redacted]'); expect(payload.processCommand).toContain('--token [redacted]');
expect(button.className).not.toContain('member-launch-diagnostics-pulse');
await act(async () => { await act(async () => {
root.unmount(); root.unmount();
@ -1233,6 +1248,8 @@ describe('MemberCard starting-state visuals', () => {
}); });
const failureReason = host.querySelector('[data-testid="member-launch-failure-reason"]'); const failureReason = host.querySelector('[data-testid="member-launch-failure-reason"]');
expect(failureReason?.className).toContain('col-start-2');
expect(failureReason?.className).toContain('col-span-2');
expect(failureReason?.textContent).toContain('Insufficient credits'); expect(failureReason?.textContent).toContain('Insufficient credits');
expect(failureReason?.textContent).toContain('OpenRouter credits'); expect(failureReason?.textContent).toContain('OpenRouter credits');
expect(failureReason?.textContent).not.toContain('Latest assistant message'); expect(failureReason?.textContent).not.toContain('Latest assistant message');