diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 4ba22c37..b3840e04 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -153,32 +153,52 @@ 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 = [ - message.objective ? t('chat.goalEvent.objective', { value: message.objective }) : null, 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 ( -
-
-
-
-
-
{title}
- {details.length > 0 ? ( -
- {details.map((detail) => ( -
{detail}
- ))} +
+
+
diff --git a/desktop/src/pages/ActiveSession.tsx b/desktop/src/pages/ActiveSession.tsx index 1bac497a..93ee709d 100644 --- a/desktop/src/pages/ActiveSession.tsx +++ b/desktop/src/pages/ActiveSession.tsx @@ -29,6 +29,7 @@ import type { SessionListItem } from '../types/session' import { useMobileViewport } from '../hooks/useMobileViewport' import { isTauriRuntime } from '../lib/desktopRuntime' import type { ActiveGoalState } from '../types/chat' +import { Target } from 'lucide-react' const TASK_POLL_INTERVAL_MS = 1000 const WORKSPACE_RESIZE_STEP = 32 @@ -223,33 +224,51 @@ function GoalStatusPanel({ data-testid="active-goal-panel" className={ compact - ? 'border-b border-[var(--color-success)]/20 bg-[var(--color-success)]/6 px-4 py-2' - : 'mx-auto w-full max-w-[860px] border-b border-[var(--color-success)]/20 px-8 py-2' + ? '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' } > -
- track_changes -
-
- - {t('chat.activeGoal.title')} - - - {stateLabel} - - {goal.objective && ( - - {goal.objective} +
+
diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts index 18c6116a..ab784960 100644 --- a/src/server/__tests__/sessions.test.ts +++ b/src/server/__tests__/sessions.test.ts @@ -1429,6 +1429,31 @@ describe('SessionService', () => { expect(detail!.title).toBe('/frontend-design @website 重新设计首页') }) + it('should keep a goal creation title instead of later goal status titles', async () => { + const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' + await writeSessionFile('-tmp-project', sessionId, [ + makeSnapshotEntry(), + { + parentUuid: null, + isSidechain: false, + type: 'system', + subtype: 'local_command', + content: '/goal\ngoal\nship the actual objective', + level: 'info', + timestamp: '2026-01-01T00:00:01.000Z', + uuid: 'goal-command', + }, + { + type: 'ai-title', + aiTitle: '/goal status', + timestamp: '2026-01-01T00:02:00.000Z', + }, + ]) + + const detail = await service.getSession(sessionId) + expect(detail!.title).toBe('/goal ship the actual objective') + }) + it('should display stored AI titles without internal XML tags', async () => { const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' await writeSessionFile('-tmp-project', sessionId, [ diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index cc6196d7..d416eb6f 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -601,6 +601,25 @@ export class SessionService { } } + private goalCreationCommandTitle(entry: RawEntry): string | null { + if ( + entry.type !== 'system' || + entry.subtype !== 'local_command' || + typeof entry.content !== 'string' + ) { + return null + } + + const commandName = this.readXmlTag(entry.content, 'command-name')?.replace(/^\//, '') + if (commandName !== 'goal') return null + + const args = this.readXmlTag(entry.content, 'command-args')?.trim() + if (!args || /^(status|pause|resume|complete|clear)\b/i.test(args)) return null + + const title = cleanSessionTitleSource(`/goal ${args}`) + return title ? title.length > 80 ? title.slice(0, 80) + '...' : title : null + } + private extractAgentToolUseId(entry: RawEntry): string | undefined { const content = entry.message?.content if (!Array.isArray(content)) return undefined @@ -825,7 +844,13 @@ export class SessionService { } } - // 2. Look for AI-generated title (written by titleService) + // 2. Goal sessions should keep the original objective as the stable title. + for (const e of entries) { + const goalTitle = this.goalCreationCommandTitle(e) + if (goalTitle) return goalTitle + } + + // 3. Look for AI-generated title (written by titleService) for (let i = entries.length - 1; i >= 0; i--) { const e = entries[i]! if (e.type === 'ai-title' && e.aiTitle) { @@ -834,7 +859,7 @@ export class SessionService { } } - // 3. Look for first non-meta user message as title + // 4. Look for first non-meta user message as title for (const e of entries) { if (e.type === 'user' && !e.isMeta && e.message?.role === 'user') { const content = e.message.content