LSamien 2 месяцев назад
Родитель
Сommit
a8305d66d5

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

@@ -97,6 +97,14 @@ function systemSummary(content: string): string {
   return '系统信息'
 }
 
+function backgroundProcessSummary(metadata?: Record<string, unknown>): string {
+  const exitCode = Number(metadata?.exit_code)
+  const signal = typeof metadata?.signal === 'string' ? metadata.signal : ''
+  if (exitCode === 0) return '后台子任务已完成'
+  if (signal) return `后台子任务已终止 · ${signal}`
+  return Number.isFinite(exitCode) ? `后台子任务失败 · 退出码 ${exitCode}` : '后台子任务已结束'
+}
+
 function isCompactionEvent(message: UiMessage): boolean {
   return message.timelineMetadata?.eventKind === 'compaction'
     || message.content.trim().toLocaleLowerCase().startsWith('[context compaction')
@@ -269,6 +277,12 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
                 <MarkdownContent v-if="message.content" :content="message.content" />
               </details>
             </template>
+            <template v-else-if="message.timelineKind === 'background-process'">
+              <details class="system-event background-process-event">
+                <summary><AppIcon name="tools" :size="13" /><span>{{ backgroundProcessSummary(message.timelineMetadata) }}</span></summary>
+                <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>

+ 1 - 1
src/client/components/messages/types.ts

@@ -33,7 +33,7 @@ export type UiMessage = {
   attachments?: UiMessageAttachment[]
   tools?: UiToolCall[]
   profile?: string
-  timelineKind?: 'delegation-complete' | 'system'
+  timelineKind?: 'delegation-complete' | 'background-process' | 'system'
   timelineMetadata?: Record<string, unknown>
   metadata?: string
 }

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

@@ -84,14 +84,28 @@ function timelineMetadata(value: unknown): Record<string, unknown> | undefined {
 }
 
 const CONTEXT_COMPACTION_PREFIX = '[context compaction'
+const BACKGROUND_PROCESS_NOTICE = /^\[IMPORTANT:\s*Background process\s+(\S+)\s+exited\s+\(exit code\s+(-?\d+)(?:,\s*([^)]+))?\)\.?\]?(?:\r?\n|$)/i
 
 function isContextCompactionMessage(message: ChatMessage): boolean {
   return message.content.trim().toLocaleLowerCase().startsWith(CONTEXT_COMPACTION_PREFIX)
     || message.displayKind?.toLocaleLowerCase().includes('compaction') === true
 }
 
+function backgroundProcessMetadata(content: string): Record<string, unknown> | undefined {
+  const match = content.trim().match(BACKGROUND_PROCESS_NOTICE)
+  if (!match) return undefined
+  return {
+    process_id: match[1],
+    exit_code: Number(match[2]),
+    ...(match[3]?.trim() ? { signal: match[3].trim() } : {}),
+  }
+}
+
 function isSystemTimelineMessage(message: ChatMessage): boolean {
-  return Boolean(message.displayKind) || /^\[system:/i.test(message.content.trim()) || isContextCompactionMessage(message)
+  return Boolean(message.displayKind)
+    || /^\[system:/i.test(message.content.trim())
+    || isContextCompactionMessage(message)
+    || Boolean(backgroundProcessMetadata(message.content))
 }
 
 export function chatMessageToUi(message: ChatMessage, agentNameFor?: (profile?: string) => string | undefined): UiMessage {
@@ -148,10 +162,16 @@ export function chatMessagesToUi(messages: ChatMessage[], agentNameFor?: (profil
     if (message.role === 'system' && !isSystemTimelineMessage(message)) continue
     const ui = chatMessageToUi(message, agentNameFor)
     if (isSystemTimelineMessage(message)) {
+      const backgroundProcess = backgroundProcessMetadata(message.content)
       ui.role = 'system'
-      ui.timelineKind = message.displayKind === 'async_delegation_complete' ? 'delegation-complete' : 'system'
+      ui.timelineKind = message.displayKind === 'async_delegation_complete'
+        ? 'delegation-complete'
+        : backgroundProcess
+          ? 'background-process'
+          : 'system'
       ui.timelineMetadata = {
         ...(timelineMetadata(message.displayMetadata) ?? {}),
+        ...(backgroundProcess ?? {}),
         eventKind: isContextCompactionMessage(message) ? 'compaction' : message.displayKind,
       }
     }

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

@@ -41,6 +41,19 @@ describe('ordinary chat tool timeline', () => {
     })
   })
 
+  it('classifies background process exits as collapsed system timeline events', () => {
+    const notice = normalizeChatMessage({
+      id: 'background-process', role: 'user',
+      content: '[IMPORTANT: Background process proc_6be40e6c3864 exited (exit code 143, SIGTERM).\nCommand: ./run_mac.sh\nOutput:\nmodel loaded',
+      timestamp: 1,
+    }, 'session-1', 'default')
+    const [timeline] = chatMessagesToUi([notice])
+    expect(timeline).toMatchObject({
+      id: 'background-process', role: 'system', timelineKind: 'background-process',
+      timelineMetadata: { process_id: 'proc_6be40e6c3864', exit_code: 143, signal: 'SIGTERM' },
+    })
+  })
+
   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,

+ 13 - 0
tests/e2e/workspace.spec.ts

@@ -684,6 +684,19 @@ test('renders subtask completion as a collapsed timeline event', async ({ page }
   await expect(delegation).not.toHaveAttribute('open', '')
 })
 
+test('renders background process exits as collapsed system events', async ({ page }) => {
+  await page.goto('/chat/session-demo')
+  const event = page.locator('[data-message-id="message-background-process"] .background-process-event')
+  const detail = event.locator('.markdown')
+  await expect(event).toContainText('后台子任务已终止 · SIGTERM')
+  await expect(event).not.toHaveAttribute('open', '')
+  await expect(detail).toBeHidden()
+  await event.locator('summary').click()
+  await expect(detail).toBeVisible()
+  await expect(detail).toContainText('Command: ./run_mac.sh')
+  await expect(page.locator('[data-message-id="message-background-process"]')).toHaveClass(/message--system/)
+})
+
 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')

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

@@ -67,6 +67,7 @@ const messages = [
   { 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-background-process', role: 'user', content: '[IMPORTANT: Background process proc_6be40e6c3864 exited (exit code 143, SIGTERM).\nCommand: ./run_mac.sh\nOutput:\nmodel loaded', timestamp: now() - 135 },
   { 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 },
 ]
 const groupAgent = { id: agentId, roomId, profile: 'yaoyao', displayName: '夭夭', description: '主 Agent', enabled: true, replyWithoutMention: true, isHost: true, model: 'gpt-5.6', provider: 'openai', reasoningEffort: 'high', fastMode: true, status: 'idle', createdAt: now() - 2000, updatedAt: now() }