mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(chat): in-chat run overview side panel for live plan progress and sub-agent status
This commit is contained in:
parent
982c6c048c
commit
47dc5373d1
275
mateclaw-ui/src/components/chat/RunOverviewPanel.vue
Normal file
275
mateclaw-ui/src/components/chat/RunOverviewPanel.vue
Normal file
@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Connection, Loading, Select, CloseBold, WarningFilled, Clock, ArrowDown, Expand, Fold } from '@element-plus/icons-vue'
|
||||
import { useToolLabel } from '@/composables/useToolLabel'
|
||||
import type { Message, MessageSegment, DelegationNode, PlanMeta } from '@/types'
|
||||
import PlanStepsPanel from './PlanStepsPanel.vue'
|
||||
import DelegationNodeView from './DelegationNodeView.vue'
|
||||
|
||||
/**
|
||||
* Always-on "run overview" rail mounted beside the chat thread. It surfaces the
|
||||
* data the agent already streams — the execution plan and the live sub-agent
|
||||
* delegation tree — so long-running tasks can be tracked without scrolling back
|
||||
* through the message history.
|
||||
*
|
||||
* Purely derived from the reactive chat messages: the plan lives on the latest
|
||||
* assistant message's metadata, and each delegation timeline segment becomes a
|
||||
* top-level node in the sub-agent tree. No extra API calls — the sub-agent
|
||||
* REST surface is admin-gated, but the same state arrives over the chat stream.
|
||||
*/
|
||||
const props = defineProps<{
|
||||
messages: Message[]
|
||||
isGenerating: boolean
|
||||
}>()
|
||||
|
||||
const { getToolLabel } = useToolLabel()
|
||||
|
||||
const collapsed = ref(false)
|
||||
|
||||
/** The most recent assistant message — the turn whose progress we track. */
|
||||
const latestAssistant = computed<Message | undefined>(() => {
|
||||
for (let i = props.messages.length - 1; i >= 0; i--) {
|
||||
if (props.messages[i].role === 'assistant') return props.messages[i]
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const currentPlan = computed<PlanMeta | undefined>(() => latestAssistant.value?.metadata?.plan)
|
||||
|
||||
/** Delegation segments carry a "→" tool-name prefix injected upstream. */
|
||||
function isDelegationSegment(seg: MessageSegment): boolean {
|
||||
return seg.type === 'tool_call' && (seg.toolName || '').startsWith('→')
|
||||
}
|
||||
|
||||
function toNode(seg: MessageSegment): DelegationNode {
|
||||
const raw = (seg.toolName || '').replace(/^→\s*/, '').trim()
|
||||
const status: DelegationNode['status'] =
|
||||
seg.status === 'error' ? 'error' : seg.status === 'completed' ? 'completed' : 'running'
|
||||
return {
|
||||
subagentId: seg.subagentId || seg.id,
|
||||
agentName: getToolLabel(raw) || raw,
|
||||
status,
|
||||
depth: 1,
|
||||
task: seg.toolArgs,
|
||||
plan: seg.childTimeline?.plan,
|
||||
tools: seg.childTimeline?.tools,
|
||||
result: seg.toolResult,
|
||||
stale: seg.delegationStale,
|
||||
async: seg.delegationAsync,
|
||||
children: seg.childTimeline?.children || [],
|
||||
}
|
||||
}
|
||||
|
||||
const subagentNodes = computed<DelegationNode[]>(() =>
|
||||
(latestAssistant.value?.metadata?.segments || []).filter(isDelegationSegment).map(toNode)
|
||||
)
|
||||
|
||||
const runningSubagents = computed(() => subagentNodes.value.filter(n => n.status === 'running').length)
|
||||
|
||||
const planProgress = computed(() => {
|
||||
const p = currentPlan.value
|
||||
if (!p?.steps?.length) return ''
|
||||
const done = p.stepResults?.filter(r => r?.status === 'completed').length || 0
|
||||
return `${done}/${p.steps.length}`
|
||||
})
|
||||
|
||||
const hasContent = computed(() => !!currentPlan.value || subagentNodes.value.length > 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
v-if="hasContent"
|
||||
class="run-overview"
|
||||
:class="{ 'is-collapsed': collapsed }"
|
||||
>
|
||||
<!-- Collapsed rail: badges only -->
|
||||
<button
|
||||
v-if="collapsed"
|
||||
class="run-overview__rail"
|
||||
:title="$t('chat.runOverview.expand')"
|
||||
@click="collapsed = false"
|
||||
>
|
||||
<el-icon :size="16"><Expand /></el-icon>
|
||||
<span v-if="planProgress" class="run-overview__rail-badge">{{ planProgress }}</span>
|
||||
<span v-if="subagentNodes.length" class="run-overview__rail-badge is-sub">
|
||||
<el-icon :size="12"><Connection /></el-icon>{{ subagentNodes.length }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<template v-else>
|
||||
<header class="run-overview__header">
|
||||
<span class="run-overview__title">{{ $t('chat.runOverview.title') }}</span>
|
||||
<el-icon
|
||||
v-if="isGenerating"
|
||||
class="run-overview__live is-loading"
|
||||
:title="$t('chat.runOverview.running')"
|
||||
:size="13"
|
||||
><Loading /></el-icon>
|
||||
<button
|
||||
class="run-overview__collapse"
|
||||
:title="$t('chat.runOverview.collapse')"
|
||||
@click="collapsed = true"
|
||||
><el-icon :size="15"><Fold /></el-icon></button>
|
||||
</header>
|
||||
|
||||
<div class="run-overview__body">
|
||||
<!-- Plan progress -->
|
||||
<section class="run-overview__section">
|
||||
<div class="run-overview__section-title">{{ $t('chat.runOverview.plan') }}</div>
|
||||
<PlanStepsPanel
|
||||
v-if="currentPlan"
|
||||
:plan="currentPlan"
|
||||
:is-generating="isGenerating"
|
||||
/>
|
||||
<p v-else class="run-overview__empty">{{ $t('chat.runOverview.noPlan') }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Sub-agents -->
|
||||
<section class="run-overview__section">
|
||||
<div class="run-overview__section-title">
|
||||
{{ $t('chat.runOverview.subagents') }}
|
||||
<span v-if="runningSubagents" class="run-overview__count">
|
||||
{{ runningSubagents }} {{ $t('chat.runOverview.running') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="subagentNodes.length" class="run-overview__subagents">
|
||||
<DelegationNodeView
|
||||
v-for="node in subagentNodes"
|
||||
:key="node.subagentId"
|
||||
:node="node"
|
||||
/>
|
||||
</div>
|
||||
<p v-else class="run-overview__empty">{{ $t('chat.runOverview.noSubagents') }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.run-overview {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid var(--mc-border-light);
|
||||
background: var(--mc-bg-muted, #f9f7f5);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
.run-overview.is-collapsed {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.run-overview__rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
.run-overview__rail:hover {
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
.run-overview__rail-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--mc-text-secondary);
|
||||
background: var(--mc-bg-sunken, #f3f0ed);
|
||||
border: 1px solid var(--mc-border-light);
|
||||
border-radius: 10px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.run-overview__rail-badge.is-sub {
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
|
||||
.run-overview__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--mc-border-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.run-overview__title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
.run-overview__live {
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
.run-overview__collapse {
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--mc-text-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.run-overview__collapse:hover {
|
||||
color: var(--mc-text-primary);
|
||||
background: var(--mc-bg-hover, #f0ece8);
|
||||
}
|
||||
|
||||
.run-overview__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.run-overview__section + .run-overview__section {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.run-overview__section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mc-text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.run-overview__count {
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
|
||||
.run-overview__empty {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
font-size: 12px;
|
||||
color: var(--mc-text-quaternary, #c0bfbc);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.run-overview__subagents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.run-overview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -97,6 +97,16 @@ export default {
|
||||
subagentAsync: 'Running in background — result via task_output',
|
||||
executionPlan: 'Execution Plan',
|
||||
planDone: 'done',
|
||||
runOverview: {
|
||||
title: 'Run Overview',
|
||||
plan: 'Plan Progress',
|
||||
subagents: 'Sub-agents',
|
||||
noPlan: 'No execution plan yet',
|
||||
noSubagents: 'No sub-agents yet',
|
||||
collapse: 'Collapse',
|
||||
expand: 'Expand',
|
||||
running: 'running',
|
||||
},
|
||||
detail: {
|
||||
viewDetail: 'View details',
|
||||
request: 'Request',
|
||||
|
||||
@ -97,6 +97,16 @@ export default {
|
||||
subagentAsync: '后台运行中,结果稍后获取',
|
||||
executionPlan: '执行计划',
|
||||
planDone: '已完成',
|
||||
runOverview: {
|
||||
title: '运行总览',
|
||||
plan: '计划进度',
|
||||
subagents: '子 Agent',
|
||||
noPlan: '暂无执行计划',
|
||||
noSubagents: '暂无子 Agent',
|
||||
collapse: '收起总览',
|
||||
expand: '展开总览',
|
||||
running: '运行中',
|
||||
},
|
||||
detail: {
|
||||
viewDetail: '查看详情',
|
||||
request: '请求参数',
|
||||
|
||||
@ -243,6 +243,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 运行总览侧栏:计划进度 + 子 Agent 实时状态 -->
|
||||
<RunOverviewPanel
|
||||
:messages="messages"
|
||||
:is-generating="isGenerating"
|
||||
/>
|
||||
|
||||
<!-- Talk Mode 覆盖层 -->
|
||||
<TalkMode
|
||||
v-if="showTalkMode"
|
||||
@ -268,6 +274,7 @@ import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { useFileDrop } from '@/composables/useFileDrop'
|
||||
import { useIsMobile, useMediaQuery, BREAKPOINTS } from '@/composables/useBreakpoint'
|
||||
import { useChat } from '@/composables/chat/useChat'
|
||||
import RunOverviewPanel from '@/components/chat/RunOverviewPanel.vue'
|
||||
import { reconstructErrorInfo } from '@/types/chatError'
|
||||
import { reconcileMessages, extractMessages } from '@/utils/messageReconcile'
|
||||
import type { Conversation, Agent, ModelConfig, ProviderInfo, ActiveModelsInfo, ChatAttachment, MessageContentPart, Message, ToolCallMeta, StreamPhase } from '@/types'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user