-
-
- {t('chat.activeGoal.title')}
-
-
- {stateLabel}
-
- {goal.objective && (
-
- {goal.objective}
+
+
+
+
+
+
+
+
+
+ {t('chat.activeGoal.title')}
+
+
+ {stateLabel}
+
+ {goal.objective && (
+
+ {goal.objective}
+
+ )}
+
+ {(goal.budget || goal.continuations || goal.elapsed) && (
+
+ {goal.budget && (
+
+ {t('chat.activeGoal.budget', { value: goal.budget })}
+
+ )}
+ {goal.continuations && (
+
+ {t('chat.activeGoal.continuations', { value: goal.continuations })}
+
+ )}
+ {goal.elapsed && (
+
+ {t('chat.activeGoal.elapsed', { value: goal.elapsed })}
+
+ )}
+
)}
- {(goal.budget || goal.continuations || goal.elapsed) && (
-
- {goal.budget && {t('chat.activeGoal.budget', { value: goal.budget })}}
- {goal.continuations && {t('chat.activeGoal.continuations', { value: goal.continuations })}}
- {goal.elapsed && {t('chat.activeGoal.elapsed', { value: goal.elapsed })}}
-
- )}
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\n
goal\n
ship 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