mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(chat): add execution-plan & tool-call detail viewer (closes #246)
This commit is contained in:
parent
48611a6f4d
commit
bb2cf12f49
251
mateclaw-ui/src/components/chat/ExecutionDetailDialog.vue
Normal file
251
mateclaw-ui/src/components/chat/ExecutionDetailDialog.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import JsonView from './JsonView.vue'
|
||||
|
||||
/**
|
||||
* Full-detail viewer for an execution step or a tool call.
|
||||
* Shows the complete request payload and response output without truncation,
|
||||
* so users can audit exactly what an agent ran and what came back.
|
||||
*/
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
title: string
|
||||
status?: 'running' | 'completed' | 'error' | 'pending'
|
||||
/** Raw request payload (typically JSON arguments). Optional — plan steps have none. */
|
||||
request?: string
|
||||
/** Raw response / output text. */
|
||||
response?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v),
|
||||
})
|
||||
|
||||
const hasRequest = computed(() => !!(props.request || '').trim())
|
||||
const hasResponse = computed(() => !!(props.response || '').trim())
|
||||
|
||||
const statusLabel = computed(() => t(`chat.detail.status.${props.status || 'pending'}`))
|
||||
|
||||
async function copy(text?: string) {
|
||||
if (!text) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success(t('chat.detail.copied'))
|
||||
} catch {
|
||||
ElMessage.error(t('chat.detail.copyFailed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
width="700px"
|
||||
append-to-body
|
||||
align-center
|
||||
:show-close="false"
|
||||
class="exec-detail-dialog"
|
||||
modal-class="exec-detail-overlay"
|
||||
>
|
||||
<template #header>
|
||||
<div class="exec-detail__head">
|
||||
<span class="exec-detail__dot" :class="`is-${status || 'pending'}`" />
|
||||
<span class="exec-detail__title">{{ title }}</span>
|
||||
<span v-if="status" class="exec-detail__badge" :class="`is-${status}`">{{ statusLabel }}</span>
|
||||
<button class="exec-detail__close" :aria-label="$t('common.close')" @click="visible = false">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="exec-detail__body">
|
||||
<section v-if="hasRequest" class="exec-detail__section">
|
||||
<div class="exec-detail__label">
|
||||
<span>{{ $t('chat.detail.request') }}</span>
|
||||
<button class="exec-detail__copy" :title="$t('chat.detail.copy')" @click="copy(request)">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<JsonView :raw="request" />
|
||||
</section>
|
||||
|
||||
<section class="exec-detail__section">
|
||||
<div class="exec-detail__label">
|
||||
<span>{{ $t('chat.detail.response') }}</span>
|
||||
<button v-if="hasResponse" class="exec-detail__copy" :title="$t('chat.detail.copy')" @click="copy(response)">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<JsonView v-if="hasResponse" :raw="response" />
|
||||
<div v-else class="exec-detail__empty">{{ $t('chat.detail.empty') }}</div>
|
||||
</section>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.exec-detail__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.exec-detail__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: var(--mc-text-quaternary, #c0bfbc);
|
||||
}
|
||||
.exec-detail__dot.is-completed { background: var(--mc-success, #67c23a); }
|
||||
.exec-detail__dot.is-error { background: var(--mc-danger, #f56c6c); }
|
||||
.exec-detail__dot.is-running { background: var(--mc-primary, #d96d46); }
|
||||
|
||||
.exec-detail__title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: var(--mc-text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.exec-detail__badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
padding: 0 8px;
|
||||
border-radius: 9px;
|
||||
font-weight: 500;
|
||||
color: var(--mc-text-tertiary);
|
||||
background: var(--mc-bg-muted, #f1ece8);
|
||||
}
|
||||
.exec-detail__badge.is-completed {
|
||||
color: var(--mc-success, #4f9a3f);
|
||||
background: rgba(103, 194, 58, 0.12);
|
||||
}
|
||||
.exec-detail__badge.is-error {
|
||||
color: var(--mc-danger, #d9533f);
|
||||
background: rgba(245, 108, 108, 0.12);
|
||||
}
|
||||
.exec-detail__badge.is-running {
|
||||
color: var(--mc-primary, #d96d46);
|
||||
background: var(--mc-primary-bg, rgba(217, 109, 70, 0.1));
|
||||
}
|
||||
|
||||
.exec-detail__close {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--mc-text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.exec-detail__close:hover {
|
||||
background: var(--mc-bg-muted, #f1ece8);
|
||||
color: var(--mc-text-primary);
|
||||
}
|
||||
|
||||
.exec-detail__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.exec-detail__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.exec-detail__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
.exec-detail__copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: var(--mc-text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.exec-detail__copy:hover {
|
||||
background: var(--mc-bg-muted, #f1ece8);
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
.exec-detail__empty {
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--mc-text-tertiary);
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px dashed var(--mc-border-light);
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Frosted-glass dialog shell. Non-scoped because el-dialog teleports to body,
|
||||
out of this component's scoped style reach. -->
|
||||
<style>
|
||||
.exec-detail-overlay {
|
||||
background: rgba(28, 20, 16, 0.28) !important;
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
}
|
||||
.exec-detail-dialog.el-dialog {
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.55);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 56px rgba(28, 20, 16, 0.22);
|
||||
overflow: hidden;
|
||||
}
|
||||
.exec-detail-dialog .el-dialog__header {
|
||||
margin: 0;
|
||||
padding: 16px 18px 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.exec-detail-dialog .el-dialog__body {
|
||||
padding: 14px 18px 20px;
|
||||
}
|
||||
|
||||
html.dark .exec-detail-overlay {
|
||||
background: rgba(0, 0, 0, 0.42) !important;
|
||||
}
|
||||
html.dark .exec-detail-dialog.el-dialog {
|
||||
background: rgba(34, 27, 23, 0.6);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 16px 56px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
html.dark .exec-detail-dialog .el-dialog__header {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
</style>
|
||||
95
mateclaw-ui/src/components/chat/JsonView.vue
Normal file
95
mateclaw-ui/src/components/chat/JsonView.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
/**
|
||||
* Lightweight, dependency-free JSON viewer with syntax highlighting.
|
||||
* Parses the raw string as JSON and pretty-prints it with token colors;
|
||||
* when the input is not valid JSON (e.g. plain terminal output), it falls
|
||||
* back to rendering the raw text verbatim without highlighting.
|
||||
*/
|
||||
const props = defineProps<{
|
||||
raw?: string
|
||||
}>()
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
/** Wrap JSON tokens (keys, strings, numbers, booleans, null) in colored spans. */
|
||||
function highlight(jsonStr: string): string {
|
||||
return escapeHtml(jsonStr).replace(
|
||||
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
|
||||
(match) => {
|
||||
let cls = 'jv-number'
|
||||
if (/^"/.test(match)) {
|
||||
cls = /:$/.test(match) ? 'jv-key' : 'jv-string'
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'jv-boolean'
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'jv-null'
|
||||
}
|
||||
return `<span class="${cls}">${match}</span>`
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = computed(() => {
|
||||
const s = (props.raw || '').trim()
|
||||
if (!s) return { empty: true, isJson: false, html: '' }
|
||||
try {
|
||||
const pretty = JSON.stringify(JSON.parse(s), null, 2)
|
||||
return { empty: false, isJson: true, html: highlight(pretty) }
|
||||
} catch {
|
||||
return { empty: false, isJson: false, html: escapeHtml(props.raw || '') }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pre class="json-view" :class="{ 'is-plain': !parsed.isJson }"><code v-html="parsed.html" /></pre>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.json-view {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
/* Translucent surface so the dialog's frosted-glass blur shows through */
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
border: 1px solid var(--mc-border-light);
|
||||
border-radius: 10px;
|
||||
font-family: var(--mc-font-mono, 'SF Mono', 'Menlo', 'Consolas', monospace);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.65;
|
||||
color: var(--mc-code-text, #1c1410);
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
tab-size: 2;
|
||||
}
|
||||
.json-view code {
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Token palette lives in a non-scoped block: the colored spans are produced
|
||||
via v-html, so they carry no scoped data-attribute. -->
|
||||
<style>
|
||||
.json-view .jv-key { color: #e45649; }
|
||||
.json-view .jv-string { color: #50a14f; }
|
||||
.json-view .jv-number { color: #c18401; }
|
||||
.json-view .jv-boolean { color: #a626a4; }
|
||||
.json-view .jv-null { color: #a626a4; }
|
||||
|
||||
html.dark .json-view {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
html.dark .json-view .jv-key { color: #e06c75; }
|
||||
html.dark .json-view .jv-string { color: #98c379; }
|
||||
html.dark .json-view .jv-number { color: #d19a66; }
|
||||
html.dark .json-view .jv-boolean { color: #c678dd; }
|
||||
html.dark .json-view .jv-null { color: #c678dd; }
|
||||
</style>
|
||||
@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Loading, Select, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { Loading, Select, ArrowDown, View } from '@element-plus/icons-vue'
|
||||
import type { PlanMeta } from '@/types'
|
||||
import ExecutionDetailDialog from './ExecutionDetailDialog.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
plan: PlanMeta
|
||||
@ -45,6 +46,27 @@ function truncateResult(text: string, max: number): string {
|
||||
if (!text || text.length <= max) return text
|
||||
return text.slice(0, max) + '...'
|
||||
}
|
||||
|
||||
// Full step-result viewer — the inline preview is capped at 500 chars; this opens
|
||||
// the complete result so plan execution stays fully auditable.
|
||||
const detailVisible = ref(false)
|
||||
const detailIndex = ref(-1)
|
||||
const detailResponse = computed(() => props.plan.stepResults?.[detailIndex.value]?.result || '')
|
||||
const detailTitle = computed(() => {
|
||||
const i = detailIndex.value
|
||||
return i >= 0 ? `${i + 1}. ${props.plan.steps[i] || ''}` : ''
|
||||
})
|
||||
const detailStatus = computed<'completed' | 'error' | 'running'>(() => {
|
||||
const st = props.plan.stepResults?.[detailIndex.value]?.status
|
||||
if (st === 'failed' || st === 'error') return 'error'
|
||||
if (st === 'completed') return 'completed'
|
||||
return 'running'
|
||||
})
|
||||
|
||||
function openDetail(index: number) {
|
||||
detailIndex.value = index
|
||||
detailVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -55,10 +77,8 @@ function truncateResult(text: string, max: number): string {
|
||||
<el-icon v-if="isGenerating && !allDone" class="is-loading" :size="14"><Loading /></el-icon>
|
||||
<el-icon v-else :size="14"><Select /></el-icon>
|
||||
</span>
|
||||
<span class="plan-panel__title">
|
||||
Plan
|
||||
</span>
|
||||
<span class="plan-panel__progress">{{ completedCount }}/{{ plan.steps.length }}</span>
|
||||
<span class="plan-panel__title">{{ $t('chat.executionPlan') }}</span>
|
||||
<span class="plan-panel__progress">({{ completedCount }}/{{ plan.steps.length }} {{ $t('chat.planDone') }})</span>
|
||||
<el-icon
|
||||
class="plan-panel__arrow"
|
||||
:class="{ 'is-open': !collapsed }"
|
||||
@ -88,6 +108,13 @@ function truncateResult(text: string, max: number): string {
|
||||
</span>
|
||||
<span class="plan-step__index">{{ i + 1 }}.</span>
|
||||
<span class="plan-step__text">{{ step }}</span>
|
||||
<el-icon
|
||||
v-if="plan.stepResults?.[i]?.result"
|
||||
class="plan-step__detail"
|
||||
:title="$t('chat.detail.viewDetail')"
|
||||
:size="12"
|
||||
@click.stop="openDetail(i)"
|
||||
><View /></el-icon>
|
||||
<el-icon
|
||||
v-if="plan.stepResults?.[i]?.result"
|
||||
class="plan-step__arrow"
|
||||
@ -96,7 +123,7 @@ function truncateResult(text: string, max: number): string {
|
||||
><ArrowDown /></el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 步骤结果(可展开) -->
|
||||
<!-- 步骤结果(可展开预览,完整内容见详情弹层) -->
|
||||
<Transition name="plan-slide">
|
||||
<div v-if="expandedSteps.has(i) && plan.stepResults?.[i]?.result" class="plan-step__result">
|
||||
<pre>{{ truncateResult(plan.stepResults[i].result, 500) }}</pre>
|
||||
@ -105,6 +132,13 @@ function truncateResult(text: string, max: number): string {
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<ExecutionDetailDialog
|
||||
v-model="detailVisible"
|
||||
:title="detailTitle"
|
||||
:status="detailStatus"
|
||||
:response="detailResponse"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -226,11 +260,21 @@ function truncateResult(text: string, max: number): string {
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
|
||||
.plan-step__detail {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
color: var(--mc-text-quaternary);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.plan-step__detail:hover {
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
|
||||
.plan-step__arrow {
|
||||
flex-shrink: 0;
|
||||
color: var(--mc-text-quaternary);
|
||||
transition: transform 0.2s;
|
||||
margin-left: auto;
|
||||
}
|
||||
.plan-step__arrow.is-open {
|
||||
transform: rotate(180deg);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Loading, Select, CloseBold, ArrowDown, Document, Setting, Connection, WarningFilled, Clock } from '@element-plus/icons-vue'
|
||||
import { Loading, Select, CloseBold, ArrowDown, Document, Setting, Connection, WarningFilled, Clock, View } from '@element-plus/icons-vue'
|
||||
import { useToolLabel } from '@/composables/useToolLabel'
|
||||
import type { MessageSegment } from '@/types'
|
||||
import DelegationNodeView from './DelegationNodeView.vue'
|
||||
import ExecutionDetailDialog from './ExecutionDetailDialog.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
segment: MessageSegment
|
||||
@ -97,6 +98,19 @@ const childProgress = computed(() => {
|
||||
const n = childTools.value.length
|
||||
return n ? `${n} ${n === 1 ? 'tool' : 'tools'}` : ''
|
||||
})
|
||||
|
||||
// Full request/response detail viewer. Available for regular tool calls (not
|
||||
// delegation timelines) that carry arguments or a result — lets users audit the
|
||||
// complete payload that the inline card truncates.
|
||||
const detailVisible = ref(false)
|
||||
const canViewDetail = computed(() =>
|
||||
!isDelegation.value && (!!props.segment.toolArgs || !!props.segment.toolResult)
|
||||
)
|
||||
const detailStatus = computed<'running' | 'completed' | 'error'>(() => {
|
||||
if (isError.value) return 'error'
|
||||
if (isRunning.value) return 'running'
|
||||
return 'completed'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -117,12 +131,21 @@ const childProgress = computed(() => {
|
||||
<span v-if="isDelegation && childProgress" class="seg-tool__badge">{{ childProgress }}</span>
|
||||
<el-icon v-if="isStalled" class="seg-tool__stale" :title="$t('chat.subagentStalled')" :size="12"><WarningFilled /></el-icon>
|
||||
<span v-if="truncatedArgs" class="seg-tool__args">{{ truncatedArgs }}</span>
|
||||
<el-icon
|
||||
v-if="hasBody"
|
||||
class="seg-tool__arrow"
|
||||
:class="{ 'is-open': expanded }"
|
||||
:size="11"
|
||||
><ArrowDown /></el-icon>
|
||||
<span class="seg-tool__actions">
|
||||
<el-icon
|
||||
v-if="canViewDetail"
|
||||
class="seg-tool__detail"
|
||||
:title="$t('chat.detail.viewDetail')"
|
||||
:size="13"
|
||||
@click.stop="detailVisible = true"
|
||||
><View /></el-icon>
|
||||
<el-icon
|
||||
v-if="hasBody"
|
||||
class="seg-tool__arrow"
|
||||
:class="{ 'is-open': expanded }"
|
||||
:size="11"
|
||||
><ArrowDown /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<Transition name="seg-slide">
|
||||
<div v-if="expanded && hasBody" class="seg-tool__body">
|
||||
@ -164,6 +187,15 @@ const childProgress = computed(() => {
|
||||
<pre v-if="segment.toolResult">{{ resultPreview }}</pre>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<ExecutionDetailDialog
|
||||
v-if="canViewDetail"
|
||||
v-model="detailVisible"
|
||||
:title="displayName"
|
||||
:status="detailStatus"
|
||||
:request="segment.toolArgs"
|
||||
:response="segment.toolResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -245,11 +277,30 @@ const childProgress = computed(() => {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Trailing controls pinned to the right edge as a single group, so the
|
||||
detail icon and chevron stay together regardless of whether args render. */
|
||||
.seg-tool__actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.seg-tool__detail {
|
||||
flex-shrink: 0;
|
||||
color: var(--mc-text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.seg-tool__detail:hover {
|
||||
color: var(--mc-primary);
|
||||
}
|
||||
|
||||
.seg-tool__arrow {
|
||||
flex-shrink: 0;
|
||||
color: var(--mc-text-tertiary);
|
||||
transition: transform 0.2s;
|
||||
margin-left: auto;
|
||||
}
|
||||
.seg-tool__arrow.is-open {
|
||||
transform: rotate(180deg);
|
||||
|
||||
@ -95,6 +95,23 @@ export default {
|
||||
interrupted: 'Interrupted',
|
||||
subagentStalled: 'Subagent stalled — no progress',
|
||||
subagentAsync: 'Running in background — result via task_output',
|
||||
executionPlan: 'Execution Plan',
|
||||
planDone: 'done',
|
||||
detail: {
|
||||
viewDetail: 'View details',
|
||||
request: 'Request',
|
||||
response: 'Response',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
copyFailed: 'Copy failed',
|
||||
empty: 'No content',
|
||||
status: {
|
||||
running: 'In progress',
|
||||
completed: 'Completed',
|
||||
error: 'Failed',
|
||||
pending: 'Pending',
|
||||
},
|
||||
},
|
||||
expandLines: 'Show more ({hidden} more lines)',
|
||||
collapse: 'Show less',
|
||||
failed: 'Generation failed',
|
||||
|
||||
@ -95,6 +95,23 @@ export default {
|
||||
interrupted: '已中断',
|
||||
subagentStalled: '子 Agent 无进展',
|
||||
subagentAsync: '后台运行中,结果稍后获取',
|
||||
executionPlan: '执行计划',
|
||||
planDone: '已完成',
|
||||
detail: {
|
||||
viewDetail: '查看详情',
|
||||
request: '请求参数',
|
||||
response: '响应输出',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
copyFailed: '复制失败',
|
||||
empty: '无内容',
|
||||
status: {
|
||||
running: '进行中',
|
||||
completed: '已完成',
|
||||
error: '失败',
|
||||
pending: '待处理',
|
||||
},
|
||||
},
|
||||
expandLines: '展开(还有 {hidden} 行)',
|
||||
collapse: '收起',
|
||||
failed: '生成失败',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user