fix(ui): clarify work status hierarchy and MCP loading
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user