Преглед изворни кода

WIP: 将上下文压缩归类为折叠时间线事件

[gstack-context]
Decisions: role=user 的 CONTEXT COMPACTION 和 display kind compaction 统一转系统事件;摘要沿用子任务折叠样式;展开保留完整压缩信息
Remaining: 全量测试、真实压缩会话验证、重载 8800
Skill: /frontend-skill
[/gstack-context]
samien пре 2 месеци
родитељ
комит
d6ba482653

+ 11 - 0
src/client/components/messages/MessageTimeline.vue

@@ -83,6 +83,11 @@ function systemSummary(content: string): string {
   return '系统信息'
 }
 
+function isCompactionEvent(message: UiMessage): boolean {
+  return message.timelineMetadata?.eventKind === 'compaction'
+    || message.content.trim().toLocaleLowerCase().startsWith('[context compaction')
+}
+
 function onScroll() {
   const el = scroller.value
   if (!el) return
@@ -198,6 +203,12 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
                 <MarkdownContent v-if="message.content" :content="message.content" />
               </details>
             </template>
+            <template v-else-if="message.timelineKind === 'system' && isCompactionEvent(message)">
+              <details class="delegation-event compaction-event">
+                <summary><AppIcon name="archive" :size="14" /><span><strong>上下文已压缩</strong><small>压缩摘要已归档 · 点击查看</small></span></summary>
+                <MarkdownContent v-if="message.content" :content="message.content" />
+              </details>
+            </template>
             <template v-else-if="message.timelineKind === 'system'">
               <details class="system-event">
                 <summary><AppIcon name="settings" :size="13" /><span>{{ systemSummary(message.content) }}</span></summary>

+ 12 - 2
src/client/components/workspace/viewModels.ts

@@ -83,8 +83,15 @@ function timelineMetadata(value: unknown): Record<string, unknown> | undefined {
   return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined
 }
 
+const CONTEXT_COMPACTION_PREFIX = '[context compaction'
+
+function isContextCompactionMessage(message: ChatMessage): boolean {
+  return message.content.trim().toLocaleLowerCase().startsWith(CONTEXT_COMPACTION_PREFIX)
+    || message.displayKind?.toLocaleLowerCase().includes('compaction') === true
+}
+
 function isSystemTimelineMessage(message: ChatMessage): boolean {
-  return Boolean(message.displayKind) || /^\[system:/i.test(message.content.trim())
+  return Boolean(message.displayKind) || /^\[system:/i.test(message.content.trim()) || isContextCompactionMessage(message)
 }
 
 export function chatMessageToUi(message: ChatMessage, agentNameFor?: (profile?: string) => string | undefined): UiMessage {
@@ -143,7 +150,10 @@ export function chatMessagesToUi(messages: ChatMessage[], agentNameFor?: (profil
     if (isSystemTimelineMessage(message)) {
       ui.role = 'system'
       ui.timelineKind = message.displayKind === 'async_delegation_complete' ? 'delegation-complete' : 'system'
-      ui.timelineMetadata = timelineMetadata(message.displayMetadata)
+      ui.timelineMetadata = {
+        ...(timelineMetadata(message.displayMetadata) ?? {}),
+        eventKind: isContextCompactionMessage(message) ? 'compaction' : message.displayKind,
+      }
     }
     result.push(ui)
     if (message.role === 'assistant') {

+ 10 - 0
tests/client/toolTimeline.test.ts

@@ -41,6 +41,16 @@ describe('ordinary chat tool timeline', () => {
     })
   })
 
+  it('classifies a persisted context compaction envelope as a system timeline event', () => {
+    const compacted = normalizeChatMessage({
+      id: 'compacted', role: 'user', content: '[CONTEXT COMPACTION — REFERENCE ONLY]\n\n## Historical Task Snapshot', timestamp: 1,
+    }, 'session-1', 'default')
+    const [timeline] = chatMessagesToUi([compacted])
+    expect(timeline).toMatchObject({
+      id: 'compacted', role: 'system', timelineKind: 'system', timelineMetadata: { eventKind: 'compaction' },
+    })
+  })
+
   it('groups one turn of reasoning and tools before its visible assistant reply', () => {
     const rows = buildMessageTimelineRows([
       { id: 'user', role: 'user', content: '检查一下' },

+ 15 - 2
tests/e2e/workspace.spec.ts

@@ -206,8 +206,21 @@ test('folds tool result rows into their expandable tool call', async ({ page })
 test('renders subtask completion as a collapsed timeline event', async ({ page }) => {
   await page.goto('/chat/session-demo')
   await expect(page.getByText('子任务已完成', { exact: true })).toBeVisible()
-  await expect(page.locator('.delegation-event')).toContainText('2 个子任务 · 2 已完成')
-  await expect(page.locator('.delegation-event')).not.toHaveAttribute('open', '')
+  const delegation = page.locator('.delegation-event').filter({ hasText: '子任务已完成' })
+  await expect(delegation).toContainText('2 个子任务 · 2 已完成')
+  await expect(delegation).not.toHaveAttribute('open', '')
+})
+
+test('renders context compaction as a collapsed timeline event', async ({ page }) => {
+  await page.goto('/chat/session-demo')
+  const compaction = page.locator('[data-message-id="message-compaction"] .compaction-event')
+  await expect(compaction).toContainText('上下文已压缩')
+  await expect(compaction).toContainText('压缩摘要已归档 · 点击查看')
+  await expect(compaction).not.toHaveAttribute('open', '')
+  await expect(compaction.getByText('Historical Task Snapshot', { exact: true })).toBeHidden()
+  await compaction.locator('summary').click()
+  await expect(compaction.getByText('Historical Task Snapshot', { exact: true })).toBeVisible()
+  await expect(page.locator('[data-message-id="message-compaction"]')).toHaveClass(/message--system/)
 })
 
 test('renders model changes as a collapsed system event', async ({ page }) => {

+ 1 - 0
tests/fixtures/fake-hermes.mjs

@@ -50,6 +50,7 @@ const messages = [
   { id: 'message-image-second', role: 'assistant', content: '第二张:![夭夭 Logo 2](/brand/AppIcon-1024.png?variant=2)', timestamp: now() - 155 },
   { id: 'message-legacy-media', role: 'assistant', content: '历史兼容:MEDIA:/brand/AppIcon-1024.png', timestamp: now() - 150 },
   { id: 'message-local-link', role: 'assistant', content: '[方案草稿.md](/Users/samien/Agents/方案草稿.md)', timestamp: now() - 145 },
+  { id: 'message-compaction', role: 'user', content: '[CONTEXT COMPACTION — REFERENCE ONLY]\n\n## Historical Task Snapshot\n\n- 已完成普通聊天验收\n- 保留当前会话上下文', timestamp: now() - 143 },
   { id: 'message-delegation', role: 'user', content: '[ASYNC DELEGATION BATCH COMPLETE]\n后台子任务已经完成。', display_kind: 'async_delegation_complete', display_metadata: { task_count: 2, completed_count: 2, failed_count: 0, duration_seconds: 71 }, timestamp: now() - 140 },
   { id: 'message-system', role: 'user', content: '[System: The active model for this chat has changed to gpt-5.6-terra via provider openai.]', timestamp: now() - 130 },
 ]