feat(frontend): 第二阶段前端 Agent Trace / 主题包 / Mermaid 能力
实现第二阶段分工表中吉海燕负责的 P0/P1 前端能力。
- Agent Trace 可视化:新增 traceService 将扁平事件流折叠为树
(ModelCallStarted 区间内的工具/文本事件挂为子节点,运行级事件保持顶层),
TraceTimeline 支持时间线/树两种视图、耗时统计与引用跳转。
- 主题包:新增 themePackageService(Web Mock Adapter),
校验 manifest 必填字段与 theme_id 格式,拒绝远程 css_entry;
CSS 侧拒绝 @import / expression() / javascript:,
未通过校验的 CSS 不会注入页面。内置主题走 data-theme=light|dark|sepia,
自定义主题走 data-theme={theme_id} + 独立 style 节点。
ThemesView 增加“已安装/社区主题”两个标签页与导入、预览、卸载流程。
- Mermaid:新增 mermaidService(securityLevel: strict)与 MermaidBlock,
markdown 渲染管线识别 mermaid 代码块;MarkdownContent 随亮/暗主题重渲染
(SVG 配色在渲染时烘焙,无法靠 CSS 变量事后调整)。
- 插件贡献 UI:PluginsView 增加“概览/命令/设置”标签页,
PluginSettingsPanel 按 Schema 动态生成表单;
secret 字段只写不读,仅展示 configured 状态,不进 store 也不回显。
与 main 上队友成果的整合(rebase 时处理):
- 命令面板保留队友基于真实后端的实现(when 条件求值、效果白名单、
参数命令跳详情页),仅叠加我新增的主题/任务两条内置命令。
- 删除我先前的 pluginContributionService(mock 版),
统一改用队友已落地的 pluginService 真实接口;
相应修正表单以匹配真实契约(options 为 string[]、min/max 可空、无 placeholder)。
- 移除 contracts 中与队友重复的 PluginHostStatus / PluginCommand /
PluginSettingField / PluginSettingsSchema 声明,以队友版本为准。
- PluginsView 概览页保留队友的 PluginMcpPanel,并补回被我改写时丢掉的空状态。
顺带修复:
- 开启 skipLibCheck —— mermaid 11.17 把 type-fest 泄漏进了发布产物的
.d.ts,但只声明为自身 devDependency,vue-tsc -b 会因此报错。
验证:pnpm test 26 文件 / 113 测试通过(新增 traceService、
themePackageService 两个测试文件共 22 项);pnpm build 通过。
This commit is contained in:
@@ -4,8 +4,9 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useProviderStore } from '@/stores/provider'
|
||||
import { useSkillStore } from '@/stores/skill'
|
||||
import TraceTimeline from './TraceTimeline.vue'
|
||||
import type { AgentEvent } from '@/contracts'
|
||||
import { eventLabel, localizeDetails, permissionLabel, runStatusLabel, toolLabel } from './labels'
|
||||
import { localizeDetails, permissionLabel, runStatusLabel, toolLabel } from './labels'
|
||||
import ToolOption from './ToolOption.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -95,15 +96,29 @@ function eventText(event: AgentEvent) {
|
||||
</form>
|
||||
|
||||
<div v-else class="trace-layout">
|
||||
<div class="panel run-summary"><div><span class="badge info">{{ runStatusLabel(agentStore.activeRun?.status) }}</span><h2>{{ agentStore.activeRunId }}</h2></div><div class="inline-actions"><span>步骤 {{ agentStore.currentStep }} / {{ agentStore.activeRun?.max_steps }}</span><button v-if="agentStore.isRunning" class="button-danger" @click="agentStore.cancelRun(agentStore.activeRunId!)">取消运行</button></div></div>
|
||||
<div class="timeline">
|
||||
<article v-for="event in agentStore.events" :key="event.sequence" class="event-card item-card">
|
||||
<div class="event-head"><span class="badge" :class="{ success: event.event === 'RunCompleted', error: event.event === 'RunFailed', warning: event.event === 'PermissionRequired' }">{{ eventLabel(event.event) }}</span><span>第 {{ event.sequence }} 条 · {{ new Date(event.timestamp).toLocaleTimeString() }}</span></div>
|
||||
<p v-if="eventText(event)" class="event-text">{{ eventText(event) }}</p>
|
||||
<pre v-if="['ToolCall', 'ToolResult', 'Citation', 'Usage'].includes(event.event)">{{ JSON.stringify(localizeDetails(event.data), null, 2) }}</pre>
|
||||
</article>
|
||||
<div v-if="!agentStore.events.length" class="empty-state"><div><strong>等待执行轨迹</strong><p>事件连接建立后将在这里实时显示。</p></div></div>
|
||||
<div class="panel run-summary">
|
||||
<div>
|
||||
<span class="badge" :class="{
|
||||
success: agentStore.activeRun?.status === 'completed',
|
||||
error: agentStore.activeRun?.status === 'failed',
|
||||
warning: agentStore.activeRun?.status === 'waiting_permission',
|
||||
info: agentStore.activeRun?.status === 'running' || agentStore.activeRun?.status === 'queued',
|
||||
}">{{ runStatusLabel(agentStore.activeRun?.status) }}</span>
|
||||
<h2>{{ agentStore.activeRun?.run_id ?? agentStore.activeRunId }}</h2>
|
||||
<p v-if="agentStore.activeRun" class="run-meta">
|
||||
<span>步骤 {{ agentStore.currentStep }} / {{ agentStore.activeRun.max_steps }}</span>
|
||||
<span>·</span>
|
||||
<span>Token: {{ agentStore.activeRun.token_usage?.total_tokens ?? 0 }}</span>
|
||||
<span v-if="agentStore.activeRun.started_at">·</span>
|
||||
<span v-if="agentStore.activeRun.started_at">开始: {{ new Date(agentStore.activeRun.started_at).toLocaleString() }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="inline-actions">
|
||||
<button v-if="agentStore.isRunning" class="button-danger" @click="agentStore.cancelRun(agentStore.activeRunId!)">取消运行</button>
|
||||
<button class="button-secondary" @click="agentStore.loadRun(agentStore.activeRunId!)">重新加载</button>
|
||||
</div>
|
||||
</div>
|
||||
<TraceTimeline :events="agentStore.events" :run-status="agentStore.activeRun?.status" />
|
||||
</div>
|
||||
|
||||
<div v-if="agentStore.permissionRequest" class="modal-backdrop">
|
||||
@@ -118,14 +133,24 @@ function eventText(event: AgentEvent) {
|
||||
.tool-grid { display: grid; align-items: start; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--space-sm); }
|
||||
.network { display: flex; gap: var(--space-sm); }
|
||||
.trace-layout { display: grid; gap: var(--space-lg); }
|
||||
.run-summary, .event-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); }
|
||||
.run-summary h2 { margin-top: var(--space-sm); font-family: var(--font-ui-mono); font-size: var(--font-size-lg); }
|
||||
.timeline { position: relative; display: grid; gap: var(--space-md); padding-left: var(--space-md); }
|
||||
.timeline::before { content: ''; position: absolute; top: 10px; bottom: 10px; left: 1px; width: 2px; border-radius: var(--radius-full); background: var(--color-border-default); }
|
||||
.event-card { position: relative; }
|
||||
.event-card::before { content: ''; position: absolute; top: 20px; left: calc(-1 * var(--space-md) - 5px); width: 8px; height: 8px; border: 2px solid var(--color-surface-primary); border-radius: var(--radius-full); background: var(--color-accent-primary); box-shadow: 0 0 0 1px var(--color-accent-secondary); }
|
||||
.event-head { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
|
||||
.event-text { margin-top: var(--space-md); white-space: pre-wrap; line-height: var(--line-height-relaxed); }
|
||||
pre { margin-top: var(--space-md); max-height: 260px; overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-background-secondary); font-family: var(--font-ui-mono); font-size: var(--font-size-xs); white-space: pre-wrap; user-select: text; }
|
||||
.run-summary {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
.run-summary h2 {
|
||||
margin-top: var(--space-sm);
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-lg);
|
||||
word-break: break-all;
|
||||
}
|
||||
.run-meta {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
margin-top: var(--space-xs);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.permission-actions { margin-top: var(--space-lg); }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,661 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { TraceNode, AgentEvent } from '@/contracts'
|
||||
import { buildTraceNodes, getToolCallsFromEvents, getTotalDuration } from '@/services/traceService'
|
||||
import { eventLabel } from './labels'
|
||||
|
||||
const props = defineProps<{
|
||||
events: AgentEvent[]
|
||||
runStatus?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-citation', data: Record<string, unknown>): void
|
||||
}>()
|
||||
|
||||
const expandedNodes = ref<Set<string>>(new Set())
|
||||
const viewMode = ref<'timeline' | 'tree'>('timeline')
|
||||
const showDetails = ref(true)
|
||||
|
||||
const traceNodes = computed(() => buildTraceNodes(props.events))
|
||||
const toolCalls = computed(() => getToolCallsFromEvents(props.events))
|
||||
const totalDuration = computed(() => getTotalDuration(props.events))
|
||||
|
||||
const summaryStats = computed(() => {
|
||||
const events = props.events
|
||||
return {
|
||||
totalEvents: events.length,
|
||||
modelCalls: events.filter((e) => e.event === 'ModelCallStarted').length,
|
||||
toolCalls: events.filter((e) => e.event === 'ToolCall').length,
|
||||
citations: events.filter((e) => e.event === 'Citation').length,
|
||||
errors: events.filter((e) => e.event.endsWith('Failed') || e.event === 'RunFailed').length,
|
||||
}
|
||||
})
|
||||
|
||||
function toggleExpand(nodeId: string) {
|
||||
if (expandedNodes.value.has(nodeId)) {
|
||||
expandedNodes.value.delete(nodeId)
|
||||
} else {
|
||||
expandedNodes.value.add(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
function isExpanded(nodeId: string): boolean {
|
||||
return expandedNodes.value.has(nodeId)
|
||||
}
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
return d.toLocaleTimeString('zh-CN', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3, '0')
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
||||
return `${(ms / 60000).toFixed(1)}m`
|
||||
}
|
||||
|
||||
function getNodeIcon(type: TraceNode['type']): string {
|
||||
const icons: Record<TraceNode['type'], string> = {
|
||||
run: '▶',
|
||||
model_call: '🤖',
|
||||
tool_call: '🔧',
|
||||
tool_result: '✅',
|
||||
text: '💬',
|
||||
thinking: '🧠',
|
||||
citation: '📚',
|
||||
usage: '📊',
|
||||
permission: '🔒',
|
||||
error: '❌',
|
||||
complete: '🏁',
|
||||
}
|
||||
return icons[type] ?? '•'
|
||||
}
|
||||
|
||||
function getNodeStatusClass(node: TraceNode): string {
|
||||
switch (node.status) {
|
||||
case 'running': return 'status-running'
|
||||
case 'completed': return 'status-completed'
|
||||
case 'error': return 'status-error'
|
||||
case 'pending': return 'status-pending'
|
||||
case 'cancelled': return 'status-cancelled'
|
||||
default: return 'status-completed'
|
||||
}
|
||||
}
|
||||
|
||||
function prettyData(data: Record<string, unknown>): string {
|
||||
const filtered = { ...data }
|
||||
if (typeof filtered.output === 'string' && filtered.output.length > 500) {
|
||||
filtered.output = filtered.output.slice(0, 500) + '...'
|
||||
}
|
||||
return JSON.stringify(filtered, null, 2)
|
||||
}
|
||||
|
||||
function flatNodes(nodes: TraceNode[], depth = 0): Array<{ node: TraceNode; depth: number }> {
|
||||
const result: Array<{ node: TraceNode; depth: number }> = []
|
||||
for (const node of nodes) {
|
||||
result.push({ node, depth })
|
||||
if (node.children.length > 0 && isExpanded(node.id)) {
|
||||
result.push(...flatNodes(node.children, depth + 1))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const flatTrace = computed(() => flatNodes(traceNodes.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="trace-visualization">
|
||||
<div class="trace-header">
|
||||
<div class="trace-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ summaryStats.totalEvents }}</span>
|
||||
<span class="stat-label">事件</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ summaryStats.modelCalls }}</span>
|
||||
<span class="stat-label">模型调用</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ summaryStats.toolCalls }}</span>
|
||||
<span class="stat-label">工具调用</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ summaryStats.citations }}</span>
|
||||
<span class="stat-label">引用</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value duration">{{ totalDuration > 0 ? formatDuration(totalDuration) : '-' }}</span>
|
||||
<span class="stat-label">总耗时</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-controls">
|
||||
<div class="view-toggle">
|
||||
<button :class="{ active: viewMode === 'timeline' }" @click="viewMode = 'timeline'">时间线</button>
|
||||
<button :class="{ active: viewMode === 'tree' }" @click="viewMode = 'tree'">树形</button>
|
||||
</div>
|
||||
<button class="detail-toggle" @click="showDetails = !showDetails">
|
||||
{{ showDetails ? '隐藏详情' : '显示详情' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'timeline'" class="timeline-view">
|
||||
<div class="timeline">
|
||||
<article
|
||||
v-for="event in events"
|
||||
:key="event.sequence"
|
||||
class="event-card"
|
||||
:class="{ expanded: isExpanded(`event-${event.sequence}`) }"
|
||||
>
|
||||
<div class="event-dot" :class="`dot-${event.event}`"></div>
|
||||
<div class="event-content" @click="toggleExpand(`event-${event.sequence}`)">
|
||||
<div class="event-header">
|
||||
<span class="event-badge" :class="{
|
||||
success: event.event === 'RunCompleted' || event.event === 'ModelCallCompleted',
|
||||
error: event.event.endsWith('Failed') || event.event === 'RunFailed',
|
||||
warning: event.event === 'PermissionRequired',
|
||||
info: event.event === 'ToolCall' || event.event === 'ModelCallStarted',
|
||||
}">{{ eventLabel(event.event as any) }}</span>
|
||||
<span class="event-time">{{ formatTime(event.timestamp) }}</span>
|
||||
</div>
|
||||
<div v-if="event.data.text || event.data.message" class="event-text">
|
||||
{{ (event.data.text || event.data.message) as string }}
|
||||
</div>
|
||||
<div v-else-if="event.data.name" class="event-name">
|
||||
<code>{{ event.data.name as string }}</code>
|
||||
<span v-if="event.data.duration_ms != null" class="event-duration">
|
||||
{{ formatDuration(event.data.duration_ms as number) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="event.event === 'Usage'" class="event-usage">
|
||||
<span>输入: {{ event.data.input_tokens ?? '-' }} tokens</span>
|
||||
<span>输出: {{ event.data.output_tokens ?? '-' }} tokens</span>
|
||||
<span class="total">总计: {{ event.data.total_tokens ?? '-' }} tokens</span>
|
||||
</div>
|
||||
<div v-if="event.event === 'Citation'" class="event-citation" @click.stop="emit('open-citation', event.data)">
|
||||
<span class="cite-icon">📎</span>
|
||||
<span>{{ (event.data.heading_path || event.data.note_title || event.data.file_path) as string }}</span>
|
||||
</div>
|
||||
<div v-if="event.event === 'PermissionRequired'" class="event-permission">
|
||||
<span class="perm-label">权限:</span>
|
||||
<code>{{ event.data.permission as string }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isExpanded(`event-${event.sequence}`) && showDetails" class="event-detail">
|
||||
<details open>
|
||||
<summary>完整数据</summary>
|
||||
<pre>{{ prettyData(event.data) }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div v-if="!events.length" class="empty-state">
|
||||
<div><strong>等待执行轨迹</strong><p>事件连接建立后将在这里实时显示。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="tree-view">
|
||||
<div v-for="item in flatTrace" :key="item.node.id" class="tree-node" :style="{ paddingLeft: `${item.depth * 24 + 8}px` }">
|
||||
<div class="node-row" :class="getNodeStatusClass(item.node)" @click="item.node.children.length && toggleExpand(item.node.id)">
|
||||
<span v-if="item.node.children.length" class="expand-icon">
|
||||
{{ isExpanded(item.node.id) ? '▼' : '▶' }}
|
||||
</span>
|
||||
<span v-else class="expand-icon placeholder"></span>
|
||||
<span class="node-icon">{{ getNodeIcon(item.node.type) }}</span>
|
||||
<span class="node-title">{{ item.node.title }}</span>
|
||||
<span v-if="item.node.subtitle" class="node-subtitle">{{ item.node.subtitle }}</span>
|
||||
<span v-if="item.node.duration_ms != null" class="node-duration">
|
||||
{{ formatDuration(item.node.duration_ms) }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isExpanded(item.node.id) && item.node.children.length === 0 && showDetails" class="node-detail">
|
||||
<pre>{{ prettyData(item.node.data) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!traceNodes.length" class="empty-state">
|
||||
<div><strong>暂无树形数据</strong><p>运行开始后将展示调用树。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="toolCalls.length > 0 && viewMode === 'timeline'" class="tool-calls-summary panel">
|
||||
<h3 class="panel-title">工具调用统计</h3>
|
||||
<div class="tool-call-list">
|
||||
<div v-for="call in toolCalls" :key="call.tool_call_id" class="tool-call-item" :class="call.status">
|
||||
<span class="tool-status-dot"></span>
|
||||
<code class="tool-name">{{ call.name }}</code>
|
||||
<span v-if="call.duration_ms != null" class="tool-duration">
|
||||
{{ formatDuration(call.duration_ms) }}
|
||||
</span>
|
||||
<span class="tool-status-badge" :class="call.status">
|
||||
{{ call.status === 'completed' ? '成功' : call.status === 'error' ? '失败' : call.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.trace-visualization {
|
||||
display: grid;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.trace-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.trace-stats {
|
||||
display: flex;
|
||||
gap: var(--space-lg);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stat-value.duration {
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.trace-controls {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view-toggle button {
|
||||
padding: 4px 12px;
|
||||
background: var(--color-surface-primary);
|
||||
border: none;
|
||||
border-right: 1px solid var(--color-border-default);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--motion-fast);
|
||||
}
|
||||
|
||||
.view-toggle button:last-child { border-right: none; }
|
||||
.view-toggle button.active {
|
||||
background: var(--color-accent-primary);
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.detail-toggle {
|
||||
padding: 4px 12px;
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detail-toggle:hover { border-color: var(--color-accent-secondary); }
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: var(--space-sm);
|
||||
padding-left: var(--space-md);
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
left: 7px;
|
||||
width: 2px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-border-default);
|
||||
}
|
||||
|
||||
.event-card {
|
||||
position: relative;
|
||||
padding: var(--space-md);
|
||||
background: var(--color-surface-primary);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
|
||||
}
|
||||
|
||||
.event-card:hover {
|
||||
border-color: var(--color-accent-secondary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: -22px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent-primary);
|
||||
border: 2px solid var(--color-surface-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-border-default);
|
||||
}
|
||||
|
||||
.dot-RunStarted, .dot-ModelCallStarted { background: var(--color-accent-primary); }
|
||||
.dot-RunCompleted, .dot-ModelCallCompleted, .dot-ToolResult { background: var(--color-success); }
|
||||
.dot-RunFailed, .dot-ModelCallFailed { background: var(--color-error); }
|
||||
.dot-ToolCall { background: var(--color-info); }
|
||||
.dot-PermissionRequired { background: var(--color-warning); }
|
||||
.dot-ThinkingDelta { background: var(--color-text-tertiary); }
|
||||
.dot-TextDelta { background: var(--color-text-secondary); }
|
||||
.dot-Citation { background: var(--color-accent-secondary); }
|
||||
.dot-Usage { background: var(--color-text-tertiary); }
|
||||
|
||||
.event-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.event-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
background: var(--color-background-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.event-badge.success {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
.event-badge.error {
|
||||
background: var(--color-error-soft);
|
||||
color: var(--color-error);
|
||||
}
|
||||
.event-badge.warning {
|
||||
background: var(--color-warning-soft);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.event-badge.info {
|
||||
background: var(--color-info-soft);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: var(--font-ui-mono);
|
||||
}
|
||||
|
||||
.event-text {
|
||||
white-space: pre-wrap;
|
||||
line-height: var(--line-height-relaxed);
|
||||
color: var(--color-text-primary);
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.event-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.event-name code {
|
||||
padding: 2px 6px;
|
||||
background: var(--color-background-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.event-duration {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: var(--font-ui-mono);
|
||||
}
|
||||
|
||||
.event-usage {
|
||||
display: flex;
|
||||
gap: var(--space-md);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-family: var(--font-ui-mono);
|
||||
}
|
||||
|
||||
.event-usage .total {
|
||||
color: var(--color-accent-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.event-citation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--color-accent-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-accent-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.event-citation:hover { text-decoration: underline; }
|
||||
|
||||
.event-permission {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.event-permission code {
|
||||
padding: 2px 6px;
|
||||
background: var(--color-warning-soft);
|
||||
color: var(--color-warning);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-ui-mono);
|
||||
}
|
||||
|
||||
.event-detail {
|
||||
margin-top: var(--space-sm);
|
||||
padding-top: var(--space-sm);
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
.event-detail details summary {
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.event-detail pre {
|
||||
margin-top: var(--space-sm);
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
padding: var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-background-secondary);
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tree-view {
|
||||
padding: var(--space-sm) 0;
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-primary);
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.tree-node:last-child { border-bottom: none; }
|
||||
|
||||
.node-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: 8px 12px;
|
||||
cursor: default;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: background-color var(--motion-fast);
|
||||
}
|
||||
|
||||
.node-row:hover { background: var(--color-background-hover); }
|
||||
|
||||
.node-row.status-running {
|
||||
background: var(--color-info-soft);
|
||||
}
|
||||
|
||||
.node-row.status-error {
|
||||
background: var(--color-error-soft);
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
width: 16px;
|
||||
font-size: 10px;
|
||||
color: var(--color-text-tertiary);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-icon.placeholder { visibility: hidden; }
|
||||
|
||||
.node-icon {
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
flex: 1;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.node-duration {
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.node-detail {
|
||||
padding: 8px 12px 12px 36px;
|
||||
}
|
||||
|
||||
.node-detail pre {
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
padding: var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-background-secondary);
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tool-calls-summary { margin-top: var(--space-md); }
|
||||
.tool-call-list {
|
||||
display: grid;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.tool-call-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-background-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.tool-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.tool-call-item.completed .tool-status-dot { background: var(--color-success); }
|
||||
.tool-call-item.error .tool-status-dot { background: var(--color-error); }
|
||||
.tool-call-item.running .tool-status-dot { background: var(--color-info); }
|
||||
|
||||
.tool-name {
|
||||
flex: 1;
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.tool-duration {
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: var(--font-ui-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.tool-status-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tool-status-badge.completed { background: var(--color-success-soft); color: var(--color-success); }
|
||||
.tool-status-badge.error { background: var(--color-error-soft); color: var(--color-error); }
|
||||
.tool-status-badge.running { background: var(--color-info-soft); color: var(--color-info); }
|
||||
|
||||
.empty-state {
|
||||
padding: var(--space-3xl);
|
||||
text-align: center;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.empty-state strong {
|
||||
display: block;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user