Align goal UI with memory activity blocks

Goal status previously used a standalone success-card treatment, which made /goal look visually detached from the transcript. Rework the active goal strip and goal event cards to use the same compact memory/tool-result visual language.

Constraint: Preserve /goal state visibility while reducing alert-like chrome.

Rejected: Continue tuning only goal colors | the layout still read as a separate alert instead of transcript state.

Confidence: high

Scope-risk: narrow

Tested: cd desktop && bun run test -- --run src/components/chat/MessageList.test.tsx src/pages/ActiveSession.test.tsx src/theme/globals.test.ts

Tested: cd desktop && bun run test -- --run src/components/chat/MessageList.test.tsx src/pages/ActiveSession.test.tsx

Tested: cd desktop && bun run lint

Tested: cd desktop && bun run build
This commit is contained in:
程序员阿江(Relakkes) 2026-05-16 02:28:14 +08:00
parent d76c09c3d6
commit 40852a804d
2 changed files with 93 additions and 65 deletions

View File

@ -1,5 +1,5 @@
import { useRef, useEffect, useMemo, memo, useState, useCallback, useLayoutEffect, type ReactNode } from 'react'
import { ArrowDown, BookMarked, Settings, Target } from 'lucide-react'
import { ArrowDown, BookMarked, ChevronDown, ChevronRight, Settings, Target } from 'lucide-react'
import { ApiError } from '../../api/client'
import { sessionsApi, type SessionTurnCheckpoint } from '../../api/sessions'
import { useChatStore } from '../../stores/chatStore'
@ -151,6 +151,7 @@ function ChatSelectionMenu({
function GoalEventCard({ message }: { message: GoalEvent }) {
const t = useTranslation()
const [expanded, setExpanded] = useState(true)
const titleKey = `chat.goalEvent.${message.action === 'status' ? 'statusTitle' : message.action}` as TranslationKey
const title = t(titleKey) === titleKey ? t('chat.goalEvent.message') : t(titleKey)
const metaDetails = [
@ -160,35 +161,60 @@ function GoalEventCard({ message }: { message: GoalEvent }) {
].filter((detail): detail is string => detail !== null)
return (
<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>
) : 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 className="mb-2">
<div
data-testid="goal-event-card"
className="overflow-hidden rounded-lg border border-[var(--color-memory-border)] bg-[var(--color-memory-surface)]"
>
<button
type="button"
onClick={() => setExpanded((value) => !value)}
className="flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-hover)]/50"
>
{expanded ? (
<ChevronDown size={15} className="shrink-0 text-[var(--color-text-tertiary)]" aria-hidden="true" />
) : (
<ChevronRight size={15} className="shrink-0 text-[var(--color-text-tertiary)]" aria-hidden="true" />
)}
</div>
<Target size={15} className="shrink-0 text-[var(--color-memory-accent)]" strokeWidth={2.25} aria-hidden="true" />
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-[var(--color-text-primary)]">
{title}
</span>
{message.status ? (
<span className="inline-flex shrink-0 items-center gap-1 text-[12px] text-[var(--color-text-tertiary)]">
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-memory-accent)]" aria-hidden="true" />
{message.status}
</span>
) : null}
</button>
{expanded ? (
<div className="border-t border-[var(--color-border)]/55 px-3 py-2.5">
<div className="space-y-1.5">
{message.objective ? (
<div className="line-clamp-2 rounded-md px-2 py-1 text-[12px] leading-5 text-[var(--color-text-secondary)]">
{t('chat.goalEvent.objective', { value: message.objective })}
</div>
) : message.message ? (
<div className="whitespace-pre-wrap rounded-md px-2 py-1 text-[12px] leading-5 text-[var(--color-text-secondary)]">
{message.message}
</div>
) : null}
{metaDetails.length > 0 && (
<div className="flex flex-wrap items-center gap-1.5 px-2 pt-0.5">
{metaDetails.map((detail) => (
<span
key={detail}
className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface)] px-1.5 py-0.5 text-[11px] font-medium text-[var(--color-text-secondary)]"
>
{detail}
</span>
))}
</div>
)}
</div>
</div>
) : null}
</div>
</div>
)

View File

@ -218,6 +218,7 @@ function GoalStatusPanel({
: isRunning && goal.status !== 'complete'
? t('chat.activeGoal.running')
: t('chat.activeGoal.active')
const hasMeta = Boolean(goal.budget || goal.continuations || goal.elapsed)
return (
<div
@ -225,48 +226,49 @@ function GoalStatusPanel({
className={
compact
? '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'
: 'mx-auto w-full max-w-[960px] px-6 py-2.5'
}
>
<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" />
<div className="flex min-w-0 items-center gap-2 overflow-hidden rounded-lg border border-[var(--color-memory-border)] bg-[var(--color-memory-surface)] px-3 py-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center text-[var(--color-memory-accent)]">
<Target size={15} strokeWidth={2.25} 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 className="shrink-0 text-[13px] font-medium text-[var(--color-text-primary)]">
{t('chat.activeGoal.title')}
</span>
<span className="inline-flex shrink-0 items-center gap-1 text-[12px] text-[var(--color-text-tertiary)]">
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-memory-accent)]" aria-hidden="true" />
{stateLabel}
</span>
{goal.objective && (
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-[var(--color-text-primary)]">
{goal.objective}
</span>
)}
{hasMeta && (
<div className="hidden shrink-0 items-center gap-1.5 md:flex">
{goal.budget && (
<span className="rounded-[var(--radius-sm)] border border-[var(--color-border)] bg-[var(--color-surface)] 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-border)] bg-[var(--color-surface)] 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-border)] bg-[var(--color-surface)] 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-[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>
)}
</div>
)}
{hasMeta && goal.budget ? (
<span className="ml-auto shrink-0 text-[11px] font-medium text-[var(--color-text-tertiary)] md:hidden">
{t('chat.activeGoal.budget', { value: goal.budget })}
</span>
) : null}
</div>
</div>
)