feat: add CaseRun task overview
This commit is contained in:
@@ -7,7 +7,7 @@ const reviewPane = read("../src/components/caserun/CaseRunReviewPane.vue");
|
||||
const diffViewer = read("../src/components/caserun/CaseRunDiffViewer.vue");
|
||||
const navigation = read("../src/components/layout/AppNavigation.vue");
|
||||
|
||||
test("CaseRun uses the bounded three-tab review workspace", () => {
|
||||
test("CaseRun uses the bounded four-tab review workspace", () => {
|
||||
assert.doesNotMatch(view, /PageCommandBar/u);
|
||||
assert.match(view, /<CaseRunReviewPane/u);
|
||||
assert.match(view, /class="run-actions"/u);
|
||||
@@ -16,6 +16,13 @@ test("CaseRun uses the bounded three-tab review workspace", () => {
|
||||
assert.match(view, /platform-content:has\(> \.caserun-page\)/u);
|
||||
assert.match(view, /<StatusStrip[^>]*class="caserun-status-footer"/u);
|
||||
assert.match(reviewPane, /label: "Trace"/u);
|
||||
assert.match(reviewPane, /id: "overview", label: "概览"/u);
|
||||
assert.match(reviewPane, /activeTab = ref<ReviewTab>\("overview"\)/u);
|
||||
assert.match(reviewPane, /id="caserun-review-overview"/u);
|
||||
assert.match(reviewPane, /class="task-context"/u);
|
||||
assert.match(reviewPane, /查看过程记录/u);
|
||||
assert.match(reviewPane, /class="overview-pane final-response"/u);
|
||||
assert.doesNotMatch(reviewPane, /<section class="final-response"/u);
|
||||
assert.match(reviewPane, /class="trace-kind-filter"/u);
|
||||
assert.match(reviewPane, /class="trace-search"/u);
|
||||
assert.match(reviewPane, /<details :open="traceRowOpen\(row, index\)"/u);
|
||||
@@ -25,6 +32,9 @@ test("CaseRun uses the bounded three-tab review workspace", () => {
|
||||
assert.doesNotMatch(reviewPane, /background: #111719/u);
|
||||
assert.match(reviewPane, /label: "Diff"/u);
|
||||
assert.match(reviewPane, /<CaseRunDiffViewer/u);
|
||||
assert.doesNotMatch(reviewPane, /class="diff-summary"/u);
|
||||
assert.match(reviewPane, /:line-count="diff\?\.lineCount"/u);
|
||||
assert.match(diffViewer, /props\.truncated \? "已截断" : "完整"/u);
|
||||
assert.match(diffViewer, /parseUnifiedDiff/u);
|
||||
assert.match(diffViewer, /data-kind="line\.kind"/u);
|
||||
assert.match(reviewPane, /label: "硬件操作"/u);
|
||||
|
||||
@@ -18,7 +18,7 @@ interface DiffFile {
|
||||
lines: DiffLine[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ text?: string; filename?: string }>();
|
||||
const props = defineProps<{ text?: string; filename?: string; lineCount?: number; bytes?: number; truncated?: boolean }>();
|
||||
const query = ref("");
|
||||
const wrap = ref(false);
|
||||
const copied = ref(false);
|
||||
@@ -99,7 +99,7 @@ function parseUnifiedDiff(text: string): DiffFile[] {
|
||||
<template>
|
||||
<section class="diff-viewer" aria-label="Agent Diff 代码审查">
|
||||
<header class="diff-toolbar">
|
||||
<div class="diff-title"><FileDiff :size="15" aria-hidden="true" /><strong>代码变更</strong><span>{{ files.length }} 个文件</span></div>
|
||||
<div class="diff-title"><FileDiff :size="15" aria-hidden="true" /><strong>代码变更</strong><span>{{ files.length }} 文件</span><span>{{ props.lineCount ?? source.split("\n").length }} 行</span><span>{{ props.bytes ?? source.length }} B</span><span>{{ props.truncated ? "已截断" : "完整" }}</span></div>
|
||||
<label class="diff-search"><Search :size="14" aria-hidden="true" /><input v-model="query" type="search" placeholder="搜索 Diff" aria-label="搜索 Diff" /></label>
|
||||
<button type="button" :aria-pressed="wrap" :title="wrap ? '关闭自动换行' : '开启自动换行'" @click="wrap = !wrap"><WrapText :size="15" aria-hidden="true" /></button>
|
||||
<button type="button" :title="copied ? '已复制' : '复制 Diff'" @click="copyDiff"><Check v-if="copied" :size="15" aria-hidden="true" /><Copy v-else :size="15" aria-hidden="true" /></button>
|
||||
@@ -125,7 +125,7 @@ function parseUnifiedDiff(text: string): DiffFile[] {
|
||||
<style scoped>
|
||||
.diff-viewer { display: grid; min-height: 360px; grid-template-rows: 38px auto minmax(0, 1fr); overflow: hidden; border: 1px solid var(--console-border); background: white; }
|
||||
.diff-toolbar { display: grid; height: 38px; grid-template-columns: minmax(150px, 1fr) minmax(160px, 260px) repeat(3, 30px); align-items: center; gap: 5px; border-bottom: 1px solid var(--console-border); background: var(--console-surface-muted); padding: 0 7px 0 10px; }
|
||||
.diff-title { display: flex; min-width: 0; align-items: center; gap: 7px; color: var(--console-cyan-700); }.diff-title strong { color: var(--console-graphite-900); font-size: 10px; }.diff-title span { color: var(--console-graphite-500); font-size: 8px; }
|
||||
.diff-title { display: flex; min-width: 0; align-items: center; gap: 7px; overflow: hidden; color: var(--console-cyan-700); white-space: nowrap; }.diff-title strong { color: var(--console-graphite-900); font-size: 10px; }.diff-title span { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 8px; }.diff-title span + span::before { margin-right: 7px; color: var(--console-graphite-300); content: "·"; }
|
||||
.diff-search { display: grid; min-width: 0; grid-template-columns: 16px minmax(0, 1fr); align-items: center; gap: 4px; border: 1px solid var(--console-border); border-radius: 3px; background: white; padding: 0 7px; color: var(--console-graphite-400); }.diff-search input { min-width: 0; height: 27px; border: 0; outline: 0; font-size: 9px; }
|
||||
.diff-toolbar button { display: grid; width: 30px; height: 28px; place-items: center; border: 1px solid var(--console-border); border-radius: 3px; background: white; color: var(--console-graphite-600); cursor: pointer; }.diff-toolbar button:hover, .diff-toolbar button[aria-pressed="true"] { border-color: var(--console-cyan-500); color: var(--console-cyan-700); }
|
||||
.diff-file-nav { display: flex; gap: 4px; overflow-x: auto; border-bottom: 1px solid var(--console-border); padding: 5px 7px; }.diff-file-nav a { display: inline-flex; max-width: 240px; flex: 0 0 auto; align-items: center; gap: 5px; overflow: hidden; border: 1px solid var(--console-border); border-radius: 3px; background: white; padding: 4px 6px; color: var(--console-graphite-700); font-family: var(--console-font-mono); font-size: 8px; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { Bot, CheckCircle2, ChevronDown, CircleAlert, CircuitBoard, Code2, FileDiff, MessageSquare, Search, TerminalSquare } from "lucide-vue-next";
|
||||
import { ArrowRight, Bot, CheckCircle2, ChevronDown, CircleAlert, CircuitBoard, ClipboardList, Code2, FileDiff, LayoutDashboard, MessageSquare, Search, TerminalSquare } from "lucide-vue-next";
|
||||
import CaseRunDiffViewer from "@/components/caserun/CaseRunDiffViewer.vue";
|
||||
import ContentViewer from "@/components/common/ContentViewer.vue";
|
||||
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
|
||||
@@ -8,9 +8,10 @@ import type { CaseRunAnalysisReview, CaseRunReviewHardwareOperation, CaseRunRevi
|
||||
import { traceToolAccessibilityLabel, traceToolSummary, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts";
|
||||
|
||||
const props = defineProps<{ review?: CaseRunAnalysisReview | null; analysisAvailable?: boolean; runId?: string }>();
|
||||
type ReviewTab = "trace" | "diff" | "hardware";
|
||||
const activeTab = ref<ReviewTab>("trace");
|
||||
type ReviewTab = "overview" | "trace" | "diff" | "hardware";
|
||||
const activeTab = ref<ReviewTab>("overview");
|
||||
const tabs: Array<{ id: ReviewTab; label: string; icon: typeof Bot }> = [
|
||||
{ id: "overview", label: "概览", icon: LayoutDashboard },
|
||||
{ id: "trace", label: "Trace", icon: Bot },
|
||||
{ id: "diff", label: "Diff", icon: FileDiff },
|
||||
{ id: "hardware", label: "硬件操作", icon: CircuitBoard },
|
||||
@@ -19,6 +20,19 @@ const trace = computed(() => props.review?.trace);
|
||||
const diff = computed(() => props.review?.diff);
|
||||
const hardware = computed(() => props.review?.hardware);
|
||||
const operations = computed(() => hardware.value?.operations ?? []);
|
||||
const taskPrompt = computed(() => (trace.value?.rows ?? []).find((row) => /agentrun:user:message/u.test(row.header || ""))?.body || "");
|
||||
const taskContext = computed(() => {
|
||||
const prompt = taskPrompt.value;
|
||||
const facts: Array<{ label: string; value: string; mono?: boolean }> = [];
|
||||
if (/gitbundle/iu.test(prompt)) facts.push({ label: "资源包", value: "gitbundle", mono: true });
|
||||
const skills = [...new Set(prompt.match(/[A-Za-z0-9_-]+-skill/gu) ?? [])];
|
||||
if (skills.length) facts.push({ label: "Skill", value: skills.join(" · "), mono: true });
|
||||
const hwpodId = prompt.match(/hwpodId:\s*([^\s]+)/u)?.[1];
|
||||
if (hwpodId) facts.push({ label: "HWPOD", value: hwpodId, mono: true });
|
||||
const workspace = prompt.match(/主体隔离工作区路径:\s*([^\n]+)/u)?.[1];
|
||||
if (workspace) facts.push({ label: "工作区", value: workspace, mono: true });
|
||||
return facts;
|
||||
});
|
||||
const traceQuery = ref("");
|
||||
const traceKind = ref<"all" | "message" | "command">("all");
|
||||
const filteredTraceRows = computed(() => {
|
||||
@@ -89,7 +103,7 @@ function duration(value?: number): string {
|
||||
{{ tab.label }}
|
||||
<span v-if="tab.id === 'trace'">{{ trace?.returnedRowCount ?? 0 }}</span>
|
||||
<span v-else-if="tab.id === 'diff'">{{ diff?.files?.length ?? 0 }}</span>
|
||||
<span v-else>{{ operations.length }}</span>
|
||||
<span v-else-if="tab.id === 'hardware'">{{ operations.length }}</span>
|
||||
</button>
|
||||
<div class="review-source">
|
||||
<span>{{ props.review ? "离线分析投影" : "等待分析" }}</span>
|
||||
@@ -103,12 +117,24 @@ function duration(value?: number): string {
|
||||
<p>该页面只读取已有 capture 的分析结果,不会重跑 Agent 或 HWPOD。</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'overview'" class="overview-review" id="caserun-review-overview">
|
||||
<header class="overview-toolbar"><div><LayoutDashboard :size="16" aria-hidden="true" /><strong>任务概览</strong><span>采集与分析分离的只读投影</span></div><button type="button" @click="activeTab = 'trace'">查看过程记录<ArrowRight :size="14" aria-hidden="true" /></button></header>
|
||||
<div class="overview-grid">
|
||||
<section class="overview-pane task-overview">
|
||||
<div class="overview-heading"><div><ClipboardList :size="16" aria-hidden="true" /><strong>任务 Prompt</strong></div><span>{{ taskPrompt.length }} 字符</span></div>
|
||||
<dl v-if="taskContext.length" class="task-context">
|
||||
<div v-for="fact in taskContext" :key="`${fact.label}-${fact.value}`"><dt>{{ fact.label }}</dt><dd :class="{ mono: fact.mono }">{{ fact.value }}</dd></div>
|
||||
</dl>
|
||||
<div class="overview-document"><MessageMarkdown v-if="taskPrompt" :source="taskPrompt" /><p v-else class="inline-empty">capture 中没有任务 Prompt。</p></div>
|
||||
</section>
|
||||
<section class="overview-pane final-response">
|
||||
<div class="overview-heading"><div><Bot :size="16" aria-hidden="true" /><strong>Agent 最终答复</strong></div><span>{{ trace?.finalResponse?.bytes ?? 0 }} B</span></div>
|
||||
<div class="overview-document"><MessageMarkdown v-if="trace?.finalResponse?.text" :source="trace.finalResponse.text" /><p v-else class="inline-empty">capture 中没有最终答复。</p></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'trace'" class="review-scroll trace-review" id="caserun-review-trace">
|
||||
<section class="final-response">
|
||||
<div class="review-section-heading"><div><Bot :size="17" aria-hidden="true" /><strong>Agent 最终答复</strong></div><span>{{ trace?.finalResponse?.bytes ?? 0 }} B</span></div>
|
||||
<MessageMarkdown v-if="trace?.finalResponse?.text" :source="trace.finalResponse.text" />
|
||||
<p v-else class="inline-empty">capture 中没有最终答复。</p>
|
||||
</section>
|
||||
<section class="trace-stream">
|
||||
<div class="trace-toolbar">
|
||||
<div class="trace-heading"><Code2 :size="17" aria-hidden="true" /><div><strong>运行日志</strong><span>{{ trace?.returnedRowCount ?? 0 }} / {{ trace?.sourceRowCount ?? 0 }} 条</span></div></div>
|
||||
@@ -148,13 +174,7 @@ function duration(value?: number): string {
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'diff'" class="review-scroll diff-review" id="caserun-review-diff">
|
||||
<div class="diff-summary">
|
||||
<div><span>变更文件</span><strong>{{ diff?.files?.length ?? 0 }}</strong></div>
|
||||
<div><span>补丁行数</span><strong>{{ diff?.lineCount ?? 0 }}</strong></div>
|
||||
<div><span>补丁大小</span><strong>{{ diff?.bytes ?? 0 }} B</strong></div>
|
||||
<div><span>完整性</span><strong>{{ diff?.truncated ? "已截断" : "完整" }}</strong></div>
|
||||
</div>
|
||||
<CaseRunDiffViewer :text="diff?.text || ''" filename="agent-diff.patch" />
|
||||
<CaseRunDiffViewer :text="diff?.text || ''" filename="agent-diff.patch" :line-count="diff?.lineCount" :bytes="diff?.bytes" :truncated="diff?.truncated" />
|
||||
</div>
|
||||
|
||||
<div v-else class="review-scroll hardware-review" id="caserun-review-hardware">
|
||||
@@ -201,12 +221,12 @@ function duration(value?: number): string {
|
||||
.review-empty strong { color: var(--console-graphite-800); font-size: 14px; }
|
||||
.review-empty p, .inline-empty { margin: 0; color: var(--console-graphite-500); font-size: 11px; line-height: 1.55; }
|
||||
.review-empty.compact { min-height: 180px; }
|
||||
.final-response { border-bottom: 1px solid var(--console-border); background: color-mix(in srgb, var(--console-green-100) 46%, white); padding: 18px 22px; }
|
||||
.review-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; }
|
||||
.review-section-heading > div { display: flex; align-items: center; gap: 8px; color: var(--console-cyan-700); }
|
||||
.review-section-heading strong { color: var(--console-graphite-950); font-size: 12px; }
|
||||
.review-section-heading > span { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 9px; }
|
||||
.final-response :deep(.message-markdown) { color: var(--console-graphite-800); font-size: 12px; line-height: 1.65; }
|
||||
.overview-review { display: grid; min-height: 0; grid-template-rows: 38px minmax(0, 1fr); overflow: hidden; background: var(--console-surface-raised); }
|
||||
.overview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--console-border); background: var(--console-surface-muted); padding: 0 12px; }.overview-toolbar > div { display: flex; min-width: 0; align-items: center; gap: 7px; color: var(--console-cyan-700); }.overview-toolbar strong { color: var(--console-graphite-900); font-size: 10px; }.overview-toolbar span { overflow: hidden; color: var(--console-graphite-500); font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }.overview-toolbar button { display: inline-flex; height: 28px; flex: 0 0 auto; align-items: center; gap: 5px; border: 1px solid var(--console-border); border-radius: 3px; background: white; padding: 0 8px; color: var(--console-cyan-700); font-size: 9px; cursor: pointer; }
|
||||
.overview-grid { display: grid; min-height: 0; grid-template-columns: minmax(300px, 44%) minmax(0, 1fr); overflow: hidden; }.overview-pane { display: grid; min-width: 0; min-height: 0; grid-template-rows: 38px auto minmax(0, 1fr); overflow: hidden; }.overview-pane + .overview-pane { border-left: 1px solid var(--console-border); }.overview-pane.final-response { grid-template-rows: 38px minmax(0, 1fr); background: color-mix(in srgb, var(--console-green-100) 28%, white); }
|
||||
.overview-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; border-bottom: 1px solid var(--console-border); padding: 0 12px; }.overview-heading > div { display: flex; align-items: center; gap: 7px; color: var(--console-cyan-700); }.overview-heading strong { color: var(--console-graphite-900); font-size: 10px; }.overview-heading > span { color: var(--console-graphite-500); font-family: var(--console-font-mono); font-size: 8px; }
|
||||
.task-context { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-bottom: 1px solid var(--console-border); background: #f8fafc; }.task-context div { min-width: 0; border-right: 1px solid var(--console-border); border-bottom: 1px solid var(--console-border); padding: 7px 10px; }.task-context div:nth-child(2n) { border-right: 0; }.task-context dt { color: var(--console-graphite-500); font-size: 8px; }.task-context dd { margin: 3px 0 0; overflow: hidden; color: var(--console-graphite-800); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }.task-context dd.mono { font-family: var(--console-font-mono); }
|
||||
.overview-document { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 14px 16px 24px; }.overview-document :deep(.message-markdown) { color: var(--console-graphite-800); font-size: 10px; line-height: 1.62; }
|
||||
.trace-stream { min-height: 0; background: var(--console-surface-raised); }
|
||||
.trace-toolbar { position: sticky; top: 0; z-index: 2; display: grid; grid-template-columns: auto auto minmax(150px, 240px); align-items: center; gap: 12px; border-bottom: 1px solid var(--console-border); background: rgb(250 252 252 / 96%); padding: 9px 14px; backdrop-filter: blur(8px); }
|
||||
.trace-heading { display: flex; align-items: center; gap: 8px; color: var(--console-cyan-700); }
|
||||
@@ -244,12 +264,7 @@ function duration(value?: number): string {
|
||||
.trace-tool-call { display: grid; gap: 8px; }.trace-tool-command { display: grid; min-width: 0; grid-template-columns: 16px auto minmax(0, 1fr); align-items: start; gap: 6px; color: var(--console-amber-700); }.trace-tool-command strong { color: var(--console-graphite-700); font-size: 9px; }.trace-tool-command code { min-width: 0; color: var(--console-graphite-900); font-size: 9px; white-space: pre-wrap; overflow-wrap: anywhere; }.trace-tool-facts { display: flex; flex-wrap: wrap; gap: 5px; }.trace-tool-facts span { border: 1px solid var(--console-border); border-radius: 3px; background: white; padding: 3px 6px; color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 8px; }.trace-tool-output { border-left: 2px solid var(--console-graphite-300); background: white; padding: 7px 9px; }.trace-tool-empty { margin: 0; color: var(--console-graphite-500); font-size: 9px; }
|
||||
.trace-row-id { display: block; margin-top: 10px; color: var(--console-graphite-400); font-size: 8px; overflow-wrap: anywhere; }
|
||||
.trace-no-results { padding: 28px 14px; color: var(--console-graphite-500); font-size: 10px; text-align: center; }
|
||||
.diff-review { display: grid; align-content: start; gap: 12px; padding: 16px; }
|
||||
.diff-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--console-border); }
|
||||
.diff-summary div { display: grid; gap: 4px; border-right: 1px solid var(--console-border); padding: 10px 12px; }
|
||||
.diff-summary div:last-child { border-right: 0; }
|
||||
.diff-summary span { color: var(--console-graphite-500); font-size: 9px; }
|
||||
.diff-summary strong { font-family: var(--console-font-mono); font-size: 12px; }
|
||||
.diff-review { display: grid; align-content: start; padding: 12px; }
|
||||
.operation-record :deep(.content-viewer) { min-height: 300px; border-radius: 4px; box-shadow: none; }
|
||||
.hardware-review { padding: 16px; }
|
||||
.hardware-summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--console-border); padding: 0 2px 12px; }
|
||||
@@ -273,5 +288,5 @@ function duration(value?: number): string {
|
||||
.operation-command code { min-width: 0; color: var(--console-graphite-700); font-size: 9px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.operation-artifacts { display: flex; flex-wrap: wrap; gap: 5px; }
|
||||
.operation-artifacts span { display: inline-flex; max-width: 100%; align-items: center; gap: 5px; border: 1px solid var(--console-border); border-radius: 4px; background: var(--console-surface-muted); padding: 5px 7px; color: var(--console-graphite-700); font-family: var(--console-font-mono); font-size: 9px; overflow-wrap: anywhere; }
|
||||
@media (max-width: 820px) { .review-source { display: none; } .review-tabs button { min-width: 0; flex: 1 1 0; padding-inline: 6px; } .trace-toolbar { grid-template-columns: 1fr auto; }.trace-search { grid-column: 1 / -1; }.trace-list summary { grid-template-columns: 24px 38px minmax(0, 1fr) 18px; }.trace-list time { display: none; }.trace-body { padding-left: 14px; }.trace-body::before { display: none; }.diff-summary, .operation-record dl { grid-template-columns: repeat(2, minmax(0, 1fr)); } .diff-summary div:nth-child(2), .operation-record dl div:nth-child(2) { border-right: 0; } }
|
||||
@media (max-width: 820px) { .review-source { display: none; } .review-tabs button { min-width: 0; flex: 1 1 0; padding-inline: 6px; }.overview-review { overflow: auto; }.overview-grid { grid-template-columns: 1fr; overflow: visible; }.overview-pane { min-height: 420px; }.overview-pane + .overview-pane { border-top: 1px solid var(--console-border); border-left: 0; } .trace-toolbar { grid-template-columns: 1fr auto; }.trace-search { grid-column: 1 / -1; }.trace-list summary { grid-template-columns: 24px 38px minmax(0, 1fr) 18px; }.trace-list time { display: none; }.trace-body { padding-left: 14px; }.trace-body::before { display: none; }.operation-record dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }.operation-record dl div:nth-child(2) { border-right: 0; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user