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,22 +1202,8 @@ export const MemberCard = memo(function MemberCard({
) : null} ) : null}
</div> </div>
) : null} ) : 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}
>
<span>
{renderLinkifiedText(launchFailureReason, {
linkClassName: 'underline underline-offset-2 hover:text-red-200',
stopPropagation: true,
getLinkLabel: getLaunchFailureLinkLabel,
})}
</span>
</div>
) : null}
</div> </div>
<div className="flex shrink-0 items-center gap-2.5 justify-self-end">
{showLaunchBadge ? ( {showLaunchBadge ? (
<span <span
className="flex shrink-0 items-center gap-1" className="flex shrink-0 items-center gap-1"
@ -1244,7 +1231,9 @@ export const MemberCard = memo(function MemberCard({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel} 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" 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} disabled={retryingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -1283,6 +1272,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}
{canSkipFailedLaunch ? ( {canSkipFailedLaunch ? (
@ -1313,7 +1303,9 @@ export const MemberCard = memo(function MemberCard({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel} 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" 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} disabled={retryingLaunch || skippingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -1355,7 +1347,9 @@ export const MemberCard = memo(function MemberCard({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel} 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" 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} disabled={retryingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -1406,7 +1400,9 @@ export const MemberCard = memo(function MemberCard({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
aria-label={retryingLaunch ? restartActionBusyLabel : restartActionIdleLabel} 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" 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} disabled={retryingLaunch}
onClick={handleRestartMember} onClick={handleRestartMember}
@ -1428,6 +1424,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}
</span> </span>
@ -1533,6 +1530,22 @@ export const MemberCard = memo(function MemberCard({
</Tooltip> </Tooltip>
) : null} ) : null}
</div> </div>
{launchFailureReason ? (
<div
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}
>
<span>
{renderLinkifiedText(launchFailureReason, {
linkClassName: 'underline underline-offset-2 hover:text-red-200',
stopPropagation: true,
getLinkLabel: getLaunchFailureLinkLabel,
})}
</span>
</div>
) : null}
</div>
</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');