feat(web): AgentEventCard component (per-method rendering)

This commit is contained in:
2026-05-07 17:52:02 +08:00
parent fdd6fd7111
commit fa43d2ed32
+148
View File
@@ -0,0 +1,148 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { AcpEvent } from '@/api/acp'
const props = defineProps<{ event: AcpEvent }>()
// Backend may serialize payload as a string in some edge paths; defensively parse.
const payload = computed<Record<string, unknown>>(() => {
if (typeof props.event.payload === 'string') {
try {
return JSON.parse(props.event.payload) as Record<string, unknown>
} catch {
return {}
}
}
return (props.event.payload as Record<string, unknown>) ?? {}
})
const eventType = computed(() => {
if (props.event.kind === 'session_terminated') return 'session_terminated'
if (props.event.kind === 'client_error') return 'client_error'
const m = props.event.method ?? ''
if (m === 'session/prompt') return 'user_prompt'
if (m === 'session/cancel') return 'user_cancel'
if (m === 'fs/read_text_file') return 'tool_fs_read'
if (m === 'fs/write_text_file') return 'tool_fs_write'
if (m === 'session/request_permission') return 'tool_permission'
if (m === 'session/update') {
const params = payload.value?.params as Record<string, unknown> | undefined
const update = params?.update as Record<string, unknown> | undefined
const su = update?.sessionUpdate
if (su === 'agent_message_chunk') return 'agent_text'
if (su === 'agent_thought_chunk') return 'agent_thought'
if (su === 'tool_call') return 'tool_call_update'
return 'agent_update'
}
if (m === 'initialize' || m === 'session/new') return 'protocol'
return 'other'
})
const thoughtCollapsed = ref(true)
function chunkText(): string {
const params = payload.value?.params as Record<string, unknown> | undefined
const update = params?.update as Record<string, unknown> | undefined
const content = update?.content
if (Array.isArray(content)) {
return content.map((c: { text?: string }) => c.text ?? '').join('')
}
return ''
}
function userPromptText(): string {
const params = payload.value?.params as Record<string, unknown> | undefined
const prompt = params?.prompt
if (Array.isArray(prompt)) {
return prompt.map((p: { text?: string }) => p.text ?? '').join('')
}
return ''
}
function fsPath(): string {
const params = payload.value?.params as Record<string, unknown> | undefined
return (params?.path as string) ?? ''
}
function fsWriteSize(): number {
const params = payload.value?.params as Record<string, unknown> | undefined
const c = params?.content
return typeof c === 'string' ? c.length : 0
}
</script>
<template>
<div class="py-1">
<div v-if="eventType === 'user_prompt'" class="flex gap-2">
<span class="text-xs text-gray-500 mt-0.5 w-12">user</span>
<div class="flex-1 px-2 py-1 bg-blue-50 rounded text-sm">{{ userPromptText() }}</div>
</div>
<div v-else-if="eventType === 'user_cancel'" class="flex gap-2 text-xs text-gray-500">
<span class="w-12">user</span>
<span>· cancel</span>
</div>
<div v-else-if="eventType === 'agent_text'" class="flex gap-2">
<span class="text-xs text-gray-500 mt-0.5 w-12">agent</span>
<div class="flex-1 text-sm whitespace-pre-wrap">{{ chunkText() }}</div>
</div>
<div v-else-if="eventType === 'agent_thought'" class="flex gap-2">
<span class="text-xs text-gray-500 mt-0.5 w-12">thought</span>
<div class="flex-1">
<button
class="text-xs text-gray-500 hover:text-gray-700"
@click="thoughtCollapsed = !thoughtCollapsed"
>
{{ thoughtCollapsed ? '▶' : '▼' }} thinking...
</button>
<div
v-if="!thoughtCollapsed"
class="mt-1 px-2 py-1 bg-gray-50 text-sm text-gray-700 italic whitespace-pre-wrap"
>
{{ chunkText() }}
</div>
</div>
</div>
<div v-else-if="eventType === 'tool_fs_read'" class="flex gap-2 text-sm">
<span class="text-xs text-gray-500 mt-0.5 w-12">tool</span>
<div class="flex-1 px-2 py-0.5 bg-yellow-50 rounded">
read <code class="text-xs">{{ fsPath() }}</code>
</div>
</div>
<div v-else-if="eventType === 'tool_fs_write'" class="flex gap-2 text-sm">
<span class="text-xs text-gray-500 mt-0.5 w-12">tool</span>
<div class="flex-1 px-2 py-0.5 bg-orange-50 rounded">
write <code class="text-xs">{{ fsPath() }}</code> ({{ fsWriteSize() }} bytes)
</div>
</div>
<div v-else-if="eventType === 'tool_permission'" class="flex gap-2 text-sm">
<span class="text-xs text-gray-500 mt-0.5 w-12">perm</span>
<div class="flex-1 px-2 py-0.5 bg-purple-50 rounded">
permission requested (auto-rejected)
</div>
</div>
<div
v-else-if="eventType === 'session_terminated'"
class="text-center text-xs text-gray-500 py-2"
>
session terminated
</div>
<div v-else-if="eventType === 'client_error'" class="flex gap-2 text-sm text-red-600">
<span class="text-xs mt-0.5 w-12">error</span>
<div class="flex-1">{{ (payload as { message?: string }).message }}</div>
</div>
<div v-else class="flex gap-2 text-xs text-gray-400">
<span class="w-12">{{ eventType }}</span>
<code class="flex-1 truncate">{{ event.method }}</code>
</div>
</div>
</template>