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 (
<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
sessions. We are actively building broader plugin support for all agents, including
both universal plugins and agent-specific plugins.
{capability.reason ? ` ${capability.reason}` : ''}
Plugin support is currently guaranteed for Anthropic (Claude) sessions only.
We&apos;re working to support plugins across all agents.
</div>
);
})()}

View file

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

View file

@ -1043,7 +1043,7 @@ export const MemberCard = memo(function MemberCard({
<MemberRuntimeTelemetryStrip runtimeEntry={runtimeEntry} scale={runtimeTelemetryScale} />
) : null}
<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="rounded-full border-2 p-px"
@ -1166,6 +1166,7 @@ export const MemberCard = memo(function MemberCard({
<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}
</>
@ -1201,336 +1202,348 @@ export const MemberCard = memo(function MemberCard({
) : null}
</div>
) : null}
{launchFailureReason ? (
<div
data-testid="member-launch-failure-reason"
className="mt-1 min-w-0 whitespace-pre-wrap break-words text-[10px] font-medium leading-snug text-red-300/90"
title={rawLaunchFailureReason}
</div>
<div className="flex shrink-0 items-center gap-2.5 justify-self-end">
{showLaunchBadge ? (
<span
className="flex shrink-0 items-center gap-1"
title={runtimeEntry?.runtimeDiagnostic}
>
<span>
{renderLinkifiedText(launchFailureReason, {
linkClassName: 'underline underline-offset-2 hover:text-red-200',
stopPropagation: true,
getLinkLabel: getLaunchFailureLinkLabel,
})}
</span>
{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"
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
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}
</div>
{showLaunchBadge ? (
<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>
) : (
{launchFailureReason ? (
<div
className="shrink-0"
title={totalTasks > 0 ? `${completed}/${totalTasks} completed` : undefined}
data-testid="member-launch-failure-reason"
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
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 */}
<span>
{renderLinkifiedText(launchFailureReason, {
linkClassName: 'underline underline-offset-2 hover:text-red-200',
stopPropagation: true,
getLinkLabel: getLaunchFailureLinkLabel,
})}
</span>
</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}
</div>
</div>
@ -1549,7 +1562,7 @@ export const MemberCard = memo(function MemberCard({
>
<TooltipTrigger asChild>{cardContent}</TooltipTrigger>
<TooltipContent
side="top"
side="left"
align="start"
sideOffset={8}
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 { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { cn } from '@renderer/lib/utils';
import {
formatMemberLaunchDiagnosticsPayload,
type MemberLaunchDiagnosticsPayload,
@ -13,6 +14,7 @@ interface MemberLaunchDiagnosticsButtonProps {
label?: string;
className?: string;
size?: 'icon' | 'sm';
attention?: boolean;
}
export const MemberLaunchDiagnosticsButton = ({
@ -20,6 +22,7 @@ export const MemberLaunchDiagnosticsButton = ({
label,
className,
size = label ? 'sm' : 'icon',
attention = false,
}: MemberLaunchDiagnosticsButtonProps): React.JSX.Element => {
const [copied, setCopied] = useState(false);
@ -45,7 +48,7 @@ export const MemberLaunchDiagnosticsButton = ({
type="button"
variant="ghost"
size={size}
className={className}
className={cn(className, attention && !copied && 'member-launch-diagnostics-pulse')}
title={tooltip}
aria-label={tooltip}
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 */
.member-waiting-shimmer {
position: relative;
@ -1653,6 +1691,8 @@ a[href],
@media (prefers-reduced-motion: reduce) {
.kanban-comment-badge-pulse,
.member-launch-diagnostics-pulse,
.member-launch-diagnostics-pulse::after,
.message-composer-orbit-path,
.message-composer-orbit-glow {
animation: none;

View file

@ -8,97 +8,6 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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>
<style>
/* Splash: animated gradient background */

View file

@ -1,5 +1,6 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { CliInstallationStatus } from '@shared/types';
@ -214,7 +215,7 @@ describe('PluginsPanel effective runtime status', () => {
});
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');
@ -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');
document.body.appendChild(host);
const root = createRoot(host);
@ -257,12 +258,10 @@ describe('PluginsPanel effective runtime status', () => {
});
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(
'We are actively building broader plugin support for all agents'
);
expect(host.textContent).toContain('universal plugins and agent-specific plugins');
expect(host.textContent).not.toContain('multimodel runtime');
expect(host.textContent).not.toContain('Codex bootstrap placeholder');
await act(async () => {
root.unmount();

View file

@ -73,11 +73,20 @@ vi.mock('@renderer/components/ui/tooltip', () => ({
React.createElement(React.Fragment, null, children),
TooltipContent: ({
children,
side,
align,
className,
}: {
children: React.ReactNode;
side?: string;
align?: 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', () => ({
@ -866,6 +875,10 @@ describe('MemberCard starting-state visuals', () => {
expect(
host.querySelector('[data-testid="tooltip-root"][data-delay-duration="0"]')
).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(
'false'
);
@ -1148,6 +1161,7 @@ describe('MemberCard starting-state visuals', () => {
const button = host.querySelector('[aria-label="Copy diagnostics"]') as HTMLButtonElement;
expect(button).not.toBeNull();
expect(button.className).toContain('member-launch-diagnostics-pulse');
await act(async () => {
button.click();
@ -1163,6 +1177,7 @@ describe('MemberCard starting-state visuals', () => {
expect(payload.runId).toBe('run-42');
expect(payload.livenessKind).toBe('not_found');
expect(payload.processCommand).toContain('--token [redacted]');
expect(button.className).not.toContain('member-launch-diagnostics-pulse');
await act(async () => {
root.unmount();
@ -1233,6 +1248,8 @@ describe('MemberCard starting-state visuals', () => {
});
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('OpenRouter credits');
expect(failureReason?.textContent).not.toContain('Latest assistant message');