fix(team-ui): polish launch diagnostics controls
This commit is contained in:
parent
7b99a3713b
commit
7e6ebce093
8 changed files with 411 additions and 432 deletions
|
|
@ -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're working to support plugins across all agents.
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
|
|
|||
|
|
@ -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'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'll approve or deny each tool call in real-time.</p>
|
||||
<p>Manual mode: you'll approve or deny each tool call in real time.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue