You've already forked agentic-coding-workflow
feat(web): AgentEventCard component (per-method rendering)
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user