fix(ui): clarify work status hierarchy and MCP loading

This commit is contained in:
Bohdan Triapitsyn
2026-08-18 18:54:06 +03:00
parent bf0dfc4e6b
commit f6a4492527
4 changed files with 25 additions and 12 deletions
@@ -97,6 +97,7 @@ export const WorkStatusMcpSection: React.FC<Props> = ({ directory }) => {
> >
{mcpServers.map(([name, entry]) => { {mcpServers.map(([name, entry]) => {
const connected = entry?.status === 'connected'; const connected = entry?.status === 'connected';
const busy = busyServer === name;
const needsAuth = entry?.status === 'needs_auth' || entry?.status === 'needs_client_registration'; const needsAuth = entry?.status === 'needs_auth' || entry?.status === 'needs_client_registration';
const failed = entry?.status === 'failed'; const failed = entry?.status === 'failed';
return ( return (
@@ -105,8 +106,9 @@ export const WorkStatusMcpSection: React.FC<Props> = ({ directory }) => {
leading={( leading={(
<Switch <Switch
checked={connected} checked={connected}
disabled={busyServer === name} disabled={busy}
className="scale-75 data-[checked]:bg-status-info" loading={busy}
className="scale-75 disabled:opacity-100 data-[checked]:bg-status-info"
aria-label={t('chat.workStatus.mcp.toggle', { name })} aria-label={t('chat.workStatus.mcp.toggle', { name })}
onCheckedChange={(checked) => { void handleToggle(name, checked); }} onCheckedChange={(checked) => { void handleToggle(name, checked); }}
/> />
@@ -118,7 +120,7 @@ export const WorkStatusMcpSection: React.FC<Props> = ({ directory }) => {
value={needsAuth ? ( value={needsAuth ? (
<WorkStatusRowAction <WorkStatusRowAction
tone="warning" tone="warning"
disabled={busyServer === name} disabled={busy}
onClick={() => { void handleAuthorize(name); }} onClick={() => { void handleAuthorize(name); }}
> >
{t('chat.workStatus.mcp.needsAuth')} {t('chat.workStatus.mcp.needsAuth')}
@@ -126,7 +128,7 @@ export const WorkStatusMcpSection: React.FC<Props> = ({ directory }) => {
) : failed ? ( ) : failed ? (
<WorkStatusRowAction <WorkStatusRowAction
tone="error" tone="error"
disabled={busyServer === name} disabled={busy}
onClick={() => { void handleToggle(name, true); }} onClick={() => { void handleToggle(name, true); }}
> >
{t('chat.workStatus.mcp.failed')} {t('chat.workStatus.mcp.failed')}
@@ -25,7 +25,7 @@ const SECTION_CLASS = cn(
'[&:not(:first-child)]:border-[var(--interactive-border)] [&:not(:first-child)]:pt-3', '[&:not(:first-child)]:border-[var(--interactive-border)] [&:not(:first-child)]:pt-3',
); );
const HEADING_CLASS = 'text-xs font-normal text-muted-foreground'; const HEADING_CLASS = 'text-xs font-semibold text-foreground';
export const WorkStatusSection: React.FC<{ export const WorkStatusSection: React.FC<{
title: string; title: string;
@@ -158,7 +158,10 @@ export const WorkStatusRow: React.FC<RowProps> = ({
</> </>
); );
const shared = cn('flex h-7 w-full items-center gap-2 rounded-md px-1 text-left', className); const shared = cn(
'flex h-7 w-full items-center gap-2 rounded-md px-1 text-left text-muted-foreground',
className,
);
if (!onClick) return <div className={shared}>{body}</div>; if (!onClick) return <div className={shared}>{body}</div>;
@@ -124,8 +124,7 @@ export const WorkStatusUsageSection: React.FC = () => {
<React.Fragment key={group.providerId}> <React.Fragment key={group.providerId}>
<WorkStatusRow <WorkStatusRow
leading={<ProviderLogo providerId={group.providerId} className="size-4 shrink-0" />} leading={<ProviderLogo providerId={group.providerId} className="size-4 shrink-0" />}
label={group.providerName} label={<span className="font-semibold text-foreground">{group.providerName}</span>}
muted
value={group.status && group.rows.length === 0 ? ( value={group.status && group.rows.length === 0 ? (
<WorkStatusValue tone="muted">{group.status}</WorkStatusValue> <WorkStatusValue tone="muted">{group.status}</WorkStatusValue>
) : undefined} ) : undefined}
+13 -4
View File
@@ -1,12 +1,17 @@
import * as React from 'react'; import * as React from 'react';
import { Switch as BaseSwitch } from '@base-ui/react/switch'; import { Switch as BaseSwitch } from '@base-ui/react/switch';
import { Icon } from '@/components/icon/Icon';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
type SwitchProps = React.ComponentPropsWithoutRef<typeof BaseSwitch.Root> & {
loading?: boolean;
};
const Switch = React.forwardRef< const Switch = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof BaseSwitch.Root> SwitchProps
>(({ className, ...props }, ref) => ( >(({ className, loading = false, ...props }, ref) => (
<BaseSwitch.Root <BaseSwitch.Root
className={cn( className={cn(
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-primary data-[unchecked]:bg-[var(--interactive-border)]', 'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-primary data-[unchecked]:bg-[var(--interactive-border)]',
@@ -14,14 +19,18 @@ const Switch = React.forwardRef<
)} )}
style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }} style={{ width: '36px', height: '20px', minWidth: '36px', minHeight: '20px' }}
{...props} {...props}
aria-busy={loading || undefined}
ref={ref} ref={ref}
> >
<BaseSwitch.Thumb <BaseSwitch.Thumb
className={cn( className={cn(
'pointer-events-none block rounded-full bg-background shadow-none ring-0 transition-transform data-[checked]:translate-x-4 data-[unchecked]:translate-x-0' 'pointer-events-none flex items-center justify-center rounded-full bg-background shadow-none ring-0 transition-transform data-[checked]:translate-x-4 data-[unchecked]:translate-x-0',
loading && 'bg-status-warning text-background',
)} )}
style={{ width: '16px', height: '16px', minWidth: '16px', minHeight: '16px' }} style={{ width: '16px', height: '16px', minWidth: '16px', minHeight: '16px' }}
/> >
{loading ? <Icon name="loader" className="size-3 animate-spin" /> : null}
</BaseSwitch.Thumb>
</BaseSwitch.Root> </BaseSwitch.Root>
)); ));
Switch.displayName = 'Switch'; Switch.displayName = 'Switch';