Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions app/shared/src/styles/tokens-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,16 @@
}

/* ── Global Reset ──────────────────────────────────────────────────── */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
/* Layered so Tailwind utilities (h-, p-, m-, gap-…) can override the reset.
An unlayered reset silently wins over every `@layer utilities` rule. */
@layer base {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}

html,
Expand Down
19 changes: 3 additions & 16 deletions app/workbench/src/ComposerContextParts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import {
ComposerMainchainStrip,
ComposerMentionChips,
ComposerQuoteBar,
ComposerReplyBar,
Expand Down Expand Up @@ -59,20 +58,8 @@ describe('ComposerContextParts', () => {
expect(onRemove).toHaveBeenCalledWith('profile-builder');
});

it('renders mainchain and status strips', () => {
render(
<ComposerMainchainStrip
mainchainTask="draft required"
selectedAgentLabel="@Builder"
selectedTargetLabel={undefined}
targetSelected={false}
/>,
);
expect(screen.getByText('Agent @Builder')).toBeInTheDocument();
expect(screen.getByText('目标未选')).toBeInTheDocument();
expect(screen.getByText('需填写内容')).toBeInTheDocument();

render(<ComposerStatusStrip statusItems={['数据:真实数据']} />);
expect(screen.getByText('数据:真实数据')).toBeInTheDocument();
it('renders the status strip as one quiet line', () => {
render(<ComposerStatusStrip statusItems={['数据:真实数据', '目标:就绪']} />);
expect(screen.getByRole('status')).toHaveTextContent('数据:真实数据 · 目标:就绪');
});
});
63 changes: 5 additions & 58 deletions app/workbench/src/ComposerContextParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@ import { Button } from '@shared/shadcn/ui/button';
import { cn } from '@shared/lib/utils';
import { DesignNavIcon } from './designIcons';
import {
formatMainchainAgentLabel,
formatMainchainTargetLabel,
formatMainchainTaskLabel,
formatMentionChipLabel,
formatReplyToLabel,
mainchainDataState,
} from './ComposerPartsHelpers';
import { formatQuotePreview } from './unifiedComposerHelpers';

Expand Down Expand Up @@ -153,67 +149,18 @@ export function ComposerMentionChips({
);
}

export function ComposerMainchainStrip({
selectedAgentLabel,
selectedTargetLabel,
targetSelected,
mainchainTask,
}: {
selectedAgentLabel: string;
selectedTargetLabel: string | undefined;
targetSelected: boolean;
mainchainTask: 'ready' | 'draft required';
}): React.ReactElement {
const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE);
return (
<div
aria-label={t('aria.agentMainChain')}
className="flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground"
>
<span
className="inline-flex items-center rounded-full border border-primary/20 bg-primary/10 px-2.5 py-1 font-medium text-foreground"
data-state="selected"
>
{formatMainchainAgentLabel(selectedAgentLabel, t)}
</span>
<span
className={cn(
'inline-flex items-center rounded-full border border-line bg-foreground/5 px-2.5 py-1',
mainchainDataState(targetSelected) === 'missing' && 'border-warning/40 bg-warning/10 text-warning',
)}
data-state={mainchainDataState(targetSelected)}
>
{formatMainchainTargetLabel(selectedTargetLabel, t)}
</span>
<span
className={cn(
'inline-flex items-center rounded-full border border-line bg-foreground/5 px-2.5 py-1',
mainchainDataState(mainchainTask === 'ready') === 'missing' && 'border-warning/40 bg-warning/10 text-warning',
)}
data-state={mainchainDataState(mainchainTask === 'ready')}
>
{formatMainchainTaskLabel(mainchainTask, t)}
</span>
</div>
);
}

export function ComposerStatusStrip({
statusItems,
}: {
statusItems: string[];
}): React.ReactElement {
return (
<div
className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-1 px-1 text-[11px] leading-4 text-muted-foreground/80"
<p
className="w-full min-w-0 truncate px-1 text-[11px] leading-4 text-muted-foreground/60"
role="status"
title={statusItems.join(' · ')}
>
{statusItems.map((item) => (
<span className="inline-flex min-w-0 items-center gap-1.5" key={item}>
<span aria-hidden="true" className="size-1 shrink-0 rounded-full bg-current opacity-50" />
<span className="truncate">{item}</span>
</span>
))}
</div>
{statusItems.join(' · ')}
</p>
);
}
53 changes: 25 additions & 28 deletions app/workbench/src/ComposerControlParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,28 @@ export function ComposerAgentPicker({
onSelect: (agentId: string) => void;
}): React.ReactElement {
const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE);
const placeholder = agentPickerPlaceholder(availableMentionOptions.length, t);
return (
<label className="group/picker relative inline-flex h-8 min-w-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-within:bg-muted focus-within:text-foreground has-[:disabled]:opacity-50">
<DesignNavIcon className="shrink-0" name="agent" size={14} />
<span className="sr-only">{t('composer.agentPicker')}</span>
<label className="group/picker relative inline-flex h-7 min-w-0 items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-within:bg-muted/60 focus-within:text-foreground has-[:disabled]:opacity-40">
<DesignNavIcon className="shrink-0" name="agent" size={13} />
<span aria-hidden="true" className="max-w-[9.5rem] truncate">
{placeholder}
</span>
<DesignNavIcon className="shrink-0 opacity-50" name="chevron" size={10} />
<select
aria-label={t('aria.atAgent')}
className="h-8 min-w-0 max-w-[9.5rem] cursor-pointer appearance-none truncate bg-transparent pr-4 text-xs font-medium text-inherit outline-none disabled:cursor-not-allowed"
className="absolute inset-0 size-full cursor-pointer appearance-none opacity-0 disabled:cursor-not-allowed"
disabled={isSubmitting || availableMentionOptions.length === 0}
onChange={(event) => onSelect(event.target.value)}
value=""
>
<option value="">
{agentPickerPlaceholder(availableMentionOptions.length, t)}
</option>
<option value="">{placeholder}</option>
{availableMentionOptions.map((agent) => (
<option key={agent.id} value={agent.id}>
{formatAgentOptionLabel(agent)}
</option>
))}
</select>
<DesignNavIcon
className="pointer-events-none absolute right-1.5 opacity-60"
name="chevron"
size={12}
/>
</label>
);
}
Expand All @@ -77,31 +74,31 @@ export function ComposerTargetPicker({
onChange: (executionTargetId: string) => void;
}): React.ReactElement {
const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE);
const selectedLabel = executionTargets.find(
(target) => target.id === executionTargetId,
)?.label;
const placeholder = selectedLabel ?? targetPickerPlaceholder(executionTargets.length, t);
return (
<label className="group/picker relative inline-flex h-8 min-w-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-within:bg-muted focus-within:text-foreground has-[:disabled]:opacity-50">
<DesignNavIcon className="shrink-0" name="laptop" size={14} />
<span className="sr-only">{t('composer.targetPicker')}</span>
<label className="group/picker relative inline-flex h-7 min-w-0 max-w-[14rem] items-center gap-1.5 rounded-md px-2 text-xs text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-within:bg-muted/60 focus-within:text-foreground has-[:disabled]:opacity-40">
<DesignNavIcon className="shrink-0" name="laptop" size={13} />
<span aria-hidden="true" className="truncate">
{placeholder}
</span>
<DesignNavIcon className="shrink-0 opacity-50" name="chevron" size={10} />
<select
aria-label={t('aria.target')}
className="h-8 min-w-0 max-w-[11rem] cursor-pointer appearance-none truncate bg-transparent pr-4 text-xs font-medium text-inherit outline-none disabled:cursor-not-allowed"
className="absolute inset-0 size-full cursor-pointer appearance-none opacity-0 disabled:cursor-not-allowed"
disabled={isSubmitting || executionTargets.length === 0}
onChange={(event) => onChange(event.target.value)}
value={executionTargetId}
>
<option value="">
{targetPickerPlaceholder(executionTargets.length, t)}
</option>
<option value="">{targetPickerPlaceholder(executionTargets.length, t)}</option>
{executionTargets.map((target) => (
<option key={target.id} value={target.id}>
{target.label}
</option>
))}
</select>
<DesignNavIcon
className="pointer-events-none absolute right-1.5 opacity-60"
name="chevron"
size={12}
/>
</label>
);
}
Expand All @@ -127,7 +124,7 @@ export function ComposerSendButton({
return (
<Button
aria-label={t('action.stopRun')}
className="size-9 shrink-0 rounded-full shadow-sm"
className="size-8 shrink-0 rounded-full"
data-running="true"
size="icon-lg"
type="button"
Expand All @@ -141,7 +138,7 @@ export function ComposerSendButton({
return (
<Button
aria-label={hasMentions ? t('action.startAgentTask') : t('profile.sendMessage')}
className="size-9 shrink-0 rounded-full shadow-sm"
className="size-8 shrink-0 rounded-full"
disabled={submitDisabled}
size="icon-lg"
type="submit"
Expand All @@ -162,7 +159,7 @@ export function ComposerAttachButton({
return (
<Button
aria-label={t('aria.addAttachment')}
className="size-8 shrink-0 rounded-full text-muted-foreground"
className="size-7 shrink-0 rounded-md text-muted-foreground"
disabled={isSubmitting}
size="icon-sm"
onClick={onClick}
Expand Down Expand Up @@ -207,7 +204,7 @@ export function ComposerApprovalModePicker({
options={options}
disabled={isSubmitting}
className="shrink-0"
triggerClassName="max-w-[11rem] rounded-full bg-transparent! text-xs! font-medium! text-muted-foreground! hover:bg-muted! hover:text-foreground! aria-expanded:bg-muted! aria-expanded:text-foreground!"
triggerClassName="max-w-[11rem] rounded-md bg-transparent! text-xs! text-muted-foreground! hover:bg-muted/60! hover:text-foreground! aria-expanded:bg-muted/60! aria-expanded:text-foreground!"
onChange={(value) => {
const mode = pickerValueToApprovalMode(value);
if (mode) onChange(mode);
Expand Down
13 changes: 1 addition & 12 deletions app/workbench/src/ComposerPartsHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@ import {
agentPickerPlaceholder,
buildAttachmentChipViewModel,
formatAgentOptionLabel,
formatMainchainAgentLabel,
formatMainchainTargetLabel,
formatMainchainTaskLabel,
formatMentionChipLabel,
formatReplyToLabel,
isMediaAttachment,
mainchainDataState,
resolveAttachmentPreviewKind,
resolveAttachmentUploadProgress,
targetPickerPlaceholder,
Expand All @@ -32,18 +28,11 @@ const textAttachment: ComposerAttachment = {
};

describe('ComposerPartsHelpers', () => {
it('formats reply, mention, and mainchain labels without UX drift', () => {
it('formats reply and mention labels without UX drift', () => {
expect(formatReplyToLabel({ author: 'Ada', preview: 'hello' })).toBe(
'回复至 Ada: hello',
);
expect(formatMentionChipLabel({ id: 'a1', label: 'Builder' })).toBe('@Builder');
expect(formatMainchainAgentLabel('@Builder')).toBe('Agent @Builder');
expect(formatMainchainTargetLabel(undefined)).toBe('目标未选');
expect(formatMainchainTargetLabel('Desktop')).toBe('目标 Desktop');
expect(formatMainchainTaskLabel('ready')).toBe('任务就绪');
expect(formatMainchainTaskLabel('draft required')).toBe('需填写内容');
expect(mainchainDataState(true)).toBe('selected');
expect(mainchainDataState(false)).toBe('missing');
});

it('builds picker placeholders and agent option labels', () => {
Expand Down
32 changes: 0 additions & 32 deletions app/workbench/src/ComposerPartsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,6 @@ export function formatMentionChipLabel(mention: ComposerMention): string {
return `@${mention.label}`;
}

export function formatMainchainAgentLabel(selectedAgentLabel: string, t?: TFunction): string {
const label = selectedAgentLabel.trim();
if (!label) return t?.('composer.mainchain.agentUnselected') ?? 'Agent 未选';
return label.startsWith('@')
? (t?.('composer.mainchain.agentPlain', { label }) ?? `Agent ${label}`)
: (t?.('composer.mainchain.agentAt', { label }) ?? `Agent @${label}`);
}

export function formatMainchainTargetLabel(
selectedTargetLabel: string | undefined,
t?: TFunction,
): string {
return selectedTargetLabel
? (t?.('composer.mainchain.targetSelected', { label: selectedTargetLabel }) ?? `目标 ${selectedTargetLabel}`)
: (t?.('composer.mainchain.targetUnselected') ?? '目标未选');
}

export function formatMainchainTaskLabel(
mainchainTask: 'ready' | 'draft required',
t?: TFunction,
): string {
return mainchainTask === 'ready'
? (t?.('composer.mainchain.taskReady') ?? '任务就绪')
: (t?.('composer.mainchain.taskDraftRequired') ?? '需填写内容');
}

export function mainchainDataState(
selected: boolean,
): 'selected' | 'missing' {
return selected ? 'selected' : 'missing';
}

export function agentPickerPlaceholder(
availableCount: number,
t?: TFunction,
Expand Down
Loading
Loading