Refine goal UI around memory theme tokens

Goal status needed to read like persistent agent state, not a success alert. Reuse the Memory surface language and expose goal-specific aliases so the visual treatment follows each supported desktop theme.\n\nConstraint: Must keep /goal visible in both active-session chrome and transcript events.\nRejected: Continue using success tokens | they made the UI look like a warning/success card and clashed with white/dark themes.\nConfidence: high\nScope-risk: narrow\nTested: cd desktop && bun run test -- --run src/pages/ActiveSession.test.tsx src/components/chat/MessageList.test.tsx src/theme/globals.test.ts\nTested: cd desktop && bun run lint\nTested: cd desktop && bun run build\nTested: Chrome screenshot fixture for light, white, and dark goal UI tokens.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-16 01:44:42 +08:00
parent 929ddd0ebe
commit 7dc0b59784
4 changed files with 88 additions and 78 deletions

View File

@ -153,52 +153,41 @@ function GoalEventCard({ message }: { message: GoalEvent }) {
const t = useTranslation()
const titleKey = `chat.goalEvent.${message.action === 'status' ? 'statusTitle' : message.action}` as TranslationKey
const title = t(titleKey) === titleKey ? t('chat.goalEvent.message') : t(titleKey)
const objective = message.objective ? t('chat.goalEvent.objective', { value: message.objective }) : null
const details = [
const metaDetails = [
message.status ? t('chat.goalEvent.statusValue', { value: message.status }) : null,
message.budget ? t('chat.goalEvent.budget', { value: message.budget }) : null,
message.continuations ? t('chat.goalEvent.continuations', { value: message.continuations }) : null,
].filter((detail): detail is string => detail !== null)
return (
<div className="mb-3 flex justify-start">
<div className="group relative w-full max-w-[760px] rounded-[8px] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-3 py-2.5 text-left shadow-[0_1px_2px_rgba(15,23,42,0.04)]">
<div className="absolute inset-y-2 left-0 w-[3px] rounded-r-full bg-[var(--color-success)]" aria-hidden="true" />
<div className="flex min-w-0 items-start gap-2.5 pl-2">
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-[7px] border border-[var(--color-success)]/20 bg-[var(--color-success)]/8 text-[var(--color-success)]">
<Target size={15} strokeWidth={2.25} aria-hidden="true" />
</div>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<div className="truncate text-[13px] font-semibold text-[var(--color-text-primary)]">{title}</div>
{message.status && (
<span className="shrink-0 rounded-[6px] border border-[var(--color-success)]/20 bg-[var(--color-success)]/7 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-normal text-[var(--color-success)]">
{message.status}
</span>
)}
<div className="mb-3 flex justify-center">
<div className="flex w-full max-w-[680px] items-start gap-3 rounded-lg border border-[var(--color-goal-border)] bg-[var(--color-goal-surface)] px-3 py-2.5 text-left">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-[var(--color-goal-border)] bg-[var(--color-goal-icon-bg)] text-[var(--color-goal-accent)]">
<Target size={15} strokeWidth={2.25} aria-hidden="true" />
</div>
<div className="min-w-0">
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{title}</div>
{message.objective ? (
<div className="mt-1 line-clamp-2 text-xs leading-5 text-[var(--color-text-secondary)]">
{t('chat.goalEvent.objective', { value: message.objective })}
</div>
{objective ? (
<div
className="mt-1 overflow-hidden text-[12px] leading-5 text-[var(--color-text-secondary)]"
style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}
>
{objective}
</div>
) : message.message ? (
<div className="mt-1 whitespace-pre-wrap text-[12px] leading-5 text-[var(--color-text-secondary)]">
{message.message}
</div>
) : null}
{details.length > 0 && (
<div className="mt-2 flex min-w-0 flex-wrap items-center gap-1.5">
{details.map((detail) => (
<span key={detail} className="rounded-[6px] bg-[var(--color-surface-container-high)] px-2 py-1 text-[11px] leading-none text-[var(--color-text-tertiary)]">
{detail}
</span>
))}
</div>
)}
</div>
) : message.message ? (
<div className="mt-1 whitespace-pre-wrap text-xs leading-5 text-[var(--color-text-secondary)]">
{message.message}
</div>
) : null}
{metaDetails.length > 0 && (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{metaDetails.map((detail) => (
<span
key={detail}
className="rounded-[var(--radius-sm)] border border-[var(--color-goal-chip-border)] bg-[var(--color-goal-chip-bg)] px-1.5 py-0.5 text-[11px] font-medium text-[var(--color-text-secondary)]"
>
{detail}
</span>
))}
</div>
)}
</div>
</div>
</div>

View File

@ -224,51 +224,48 @@ function GoalStatusPanel({
data-testid="active-goal-panel"
className={
compact
? 'border-b border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-2'
: 'mx-auto w-full max-w-[920px] border-b border-[var(--color-border)] px-8 py-2'
? 'border-b border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-4 py-2'
: 'mx-auto w-full max-w-[900px] px-8 py-2.5'
}
>
<div className="relative overflow-hidden rounded-[8px] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-3 py-2 shadow-[0_1px_2px_rgba(15,23,42,0.04)]">
<div className="absolute inset-y-2 left-0 w-[3px] rounded-r-full bg-[var(--color-success)]" aria-hidden="true" />
<div className="flex min-w-0 items-center gap-3 pl-2">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[8px] border border-[var(--color-success)]/20 bg-[var(--color-success)]/8 text-[var(--color-success)]">
<Target size={17} strokeWidth={2.2} aria-hidden="true" />
<div className="flex min-w-0 items-center gap-3 rounded-lg border border-[var(--color-goal-border)] bg-[var(--color-goal-surface)] px-3 py-2.5">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-[var(--color-goal-border)] bg-[var(--color-goal-icon-bg)] text-[var(--color-goal-accent)]">
<Target size={17} strokeWidth={2.2} aria-hidden="true" />
</span>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<span className="shrink-0 text-[12px] font-semibold text-[var(--color-text-primary)]">
{t('chat.activeGoal.title')}
</span>
<span className="inline-flex shrink-0 items-center gap-1 rounded-[var(--radius-sm)] border border-[var(--color-goal-border)] bg-[var(--color-goal-chip-bg)] px-1.5 py-0.5 text-[11px] font-semibold text-[var(--color-goal-accent)]">
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-goal-accent)]" aria-hidden="true" />
{stateLabel}
</span>
{goal.objective && (
<span className="truncate text-sm font-semibold text-[var(--color-text-primary)]">
{goal.objective}
</span>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<span className="shrink-0 text-[11px] font-semibold uppercase tracking-normal text-[var(--color-text-secondary)]">
{t('chat.activeGoal.title')}
</span>
<span className="inline-flex shrink-0 items-center gap-1 rounded-[6px] border border-[var(--color-success)]/20 bg-[var(--color-success)]/7 px-1.5 py-0.5 text-[10px] font-semibold text-[var(--color-success)]">
<span className="h-1.5 w-1.5 rounded-full bg-current" aria-hidden="true" />
{stateLabel}
</span>
{goal.objective && (
<span className="truncate text-[13px] font-semibold text-[var(--color-text-primary)]">
{goal.objective}
{(goal.budget || goal.continuations || goal.elapsed) && (
<div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
{goal.budget && (
<span className="rounded-[var(--radius-sm)] border border-[var(--color-goal-chip-border)] bg-[var(--color-goal-chip-bg)] px-1.5 py-0.5 text-[11px] font-medium text-[var(--color-text-secondary)]">
{t('chat.activeGoal.budget', { value: goal.budget })}
</span>
)}
{goal.continuations && (
<span className="rounded-[var(--radius-sm)] border border-[var(--color-goal-chip-border)] bg-[var(--color-goal-chip-bg)] px-1.5 py-0.5 text-[11px] font-medium text-[var(--color-text-secondary)]">
{t('chat.activeGoal.continuations', { value: goal.continuations })}
</span>
)}
{goal.elapsed && (
<span className="rounded-[var(--radius-sm)] border border-[var(--color-goal-chip-border)] bg-[var(--color-goal-chip-bg)] px-1.5 py-0.5 text-[11px] font-medium text-[var(--color-text-secondary)]">
{t('chat.activeGoal.elapsed', { value: goal.elapsed })}
</span>
)}
</div>
{(goal.budget || goal.continuations || goal.elapsed) && (
<div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
{goal.budget && (
<span className="rounded-[6px] bg-[var(--color-surface-container-high)] px-2 py-1 text-[11px] leading-none text-[var(--color-text-tertiary)]">
{t('chat.activeGoal.budget', { value: goal.budget })}
</span>
)}
{goal.continuations && (
<span className="rounded-[6px] bg-[var(--color-surface-container-high)] px-2 py-1 text-[11px] leading-none text-[var(--color-text-tertiary)]">
{t('chat.activeGoal.continuations', { value: goal.continuations })}
</span>
)}
{goal.elapsed && (
<span className="rounded-[6px] bg-[var(--color-surface-container-high)] px-2 py-1 text-[11px] leading-none text-[var(--color-text-tertiary)]">
{t('chat.activeGoal.elapsed', { value: goal.elapsed })}
</span>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>

View File

@ -370,6 +370,12 @@
--color-memory-surface: #F6FAF8;
--color-memory-border: #BCCDC8;
--color-memory-icon-bg: #F3F8F7;
--color-goal-accent: var(--color-memory-accent);
--color-goal-surface: var(--color-memory-surface);
--color-goal-border: var(--color-memory-border);
--color-goal-icon-bg: var(--color-memory-icon-bg);
--color-goal-chip-bg: var(--color-surface);
--color-goal-chip-border: var(--color-border);
--color-model-option-selected-bg: var(--color-primary-fixed);
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
--color-activity-heat-0: var(--color-surface-container);
@ -571,6 +577,12 @@
--color-memory-surface: #F3F8F7;
--color-memory-border: #AECBC9;
--color-memory-icon-bg: #EDF7F6;
--color-goal-accent: var(--color-memory-accent);
--color-goal-surface: var(--color-memory-surface);
--color-goal-border: var(--color-memory-border);
--color-goal-icon-bg: var(--color-memory-icon-bg);
--color-goal-chip-bg: var(--color-surface);
--color-goal-chip-border: var(--color-border);
--color-model-option-selected-bg: #FFF0EA;
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
--color-activity-heat-0: #EEF2F6;
@ -749,6 +761,12 @@
--color-memory-surface: #1D2423;
--color-memory-border: #334743;
--color-memory-icon-bg: #22302E;
--color-goal-accent: var(--color-memory-accent);
--color-goal-surface: var(--color-memory-surface);
--color-goal-border: var(--color-memory-border);
--color-goal-icon-bg: var(--color-memory-icon-bg);
--color-goal-chip-bg: var(--color-surface-container-low);
--color-goal-chip-border: var(--color-border);
--color-model-option-selected-bg: rgba(255, 181, 159, 0.13);
--color-model-option-selected-border: rgba(255, 181, 159, 0.34);
--color-activity-heat-0: #242322;

View File

@ -49,6 +49,12 @@ describe('desktop theme tokens', () => {
'--color-info',
'--color-info-container',
'--color-warning-container',
'--color-goal-accent',
'--color-goal-surface',
'--color-goal-border',
'--color-goal-icon-bg',
'--color-goal-chip-bg',
'--color-goal-chip-border',
'--color-text-secondary-a72',
'--color-text-secondary-a68',
'--color-text-primary-a88',