Merge pull request #1234 from pikasTech/fix/issue-1232-hwpod-panel-noise

fix: reduce v0.3 workbench layout noise
This commit is contained in:
Lyon
2026-06-15 09:59:10 +08:00
committed by GitHub
4 changed files with 117 additions and 49 deletions
@@ -1,14 +1,15 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from "vue";
import type { RunnerTrace, TraceEvent } from "@/types";
import { isTerminalStatus } from "@/composables/useTraceSubscription";
import { firstNonEmptyString } from "@/utils";
import { traceEventBody, traceEventLabel, traceEventMeta, visibleTraceEvents } from "@/components/workbench/message-rendering";
const props = defineProps<{ trace?: RunnerTrace | null }>();
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string }>();
const listRef = ref<HTMLOListElement | null>(null);
const following = ref(true);
const expanded = ref(true);
const expanded = ref(false);
const compact = ref(true);
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
@@ -24,6 +25,15 @@ watch(() => [eventCount.value, props.trace?.status, following.value, expanded.va
jumpToBottom();
});
watch(() => [props.trace?.traceId, props.trace?.status, props.defaultExpanded] as const, () => {
expanded.value = props.defaultExpanded ?? !isTerminalStatus(props.trace?.status);
}, { immediate: true });
watch(expanded, (value) => {
if (!props.storageKey) return;
try { window.localStorage.setItem(props.storageKey, value ? "1" : "0"); } catch { /* ignore */ }
});
function jumpToBottom(): void {
const list = listRef.value;
if (!list) return;
@@ -8,6 +8,7 @@ import { useWorkbenchStore } from "@/stores/workbench";
const hwpod = useHwpodStore();
const workbench = useWorkbenchStore();
const selectedDetail = ref<HwpodDetail | null>(null);
const diagnosticsOpen = ref(false);
const follow = ref(true);
const scrollRef = ref<HTMLDivElement | null>(null);
const summary = computed(() => summarizeHwpodNodeOps({ live: workbench.live, specs: hwpod.specs, nodeOps: hwpod.nodeOps, error: hwpod.error }));
@@ -39,42 +40,11 @@ function onStreamScroll(): void {
<p class="page-eyebrow">HWPOD</p>
<h2>Node Ops</h2>
</div>
<StatusBadge :status="summary.tone" :label="`node-ops ${summary.status}`" />
</div>
<div class="evidence-grid" aria-label="HWPOD evidence level">
<span>DEV-LIVE</span>
<span>SOURCE</span>
<span>LOCAL</span>
<span>DRY-RUN</span>
<span>BLOCKED</span>
</div>
<section class="hwpod-node-status" aria-label="HWPOD node-ops status">
<div class="hwpod-node-summary" id="hwpod-node-summary">
<button v-for="tile in summary.tiles" :key="tile.key" class="summary-tile" type="button" :data-hwpod-detail="tile.key" :data-hwpod-title="tile.detail.title" @click="selectedDetail = tile.detail">
<span>{{ tile.label }}</span>
<strong>{{ tile.value }}</strong>
</button>
<div class="hwpod-panel-actions">
<StatusBadge :status="summary.tone" :label="`node-ops ${summary.status}`" />
<button class="hwpod-diagnostics-button" type="button" aria-label="HWPOD 诊断" aria-haspopup="dialog" @click="diagnosticsOpen = true">!</button>
</div>
<dl class="hwpod-node-meta">
<div><dt>hwpod</dt><dd>{{ summary.hwpodName }}</dd></div>
<div><dt>node</dt><dd>{{ summary.nodeId }}</dd></div>
<div><dt>workspace</dt><dd>{{ summary.workspacePath }}</dd></div>
<div><dt>case</dt><dd>{{ summary.caseId }}</dd></div>
<div><dt>route</dt><dd>{{ summary.route }}</dd></div>
<div><dt>updated</dt><dd>{{ formatTimestamp(summary.updatedAt) }}</dd></div>
<div><dt>ws</dt><dd>{{ summary.connectedCount }} connected / {{ summary.pendingCount }} pending</dd></div>
<div><dt>supported ops</dt><dd>{{ summary.supportedOpsCount }}</dd></div>
</dl>
</section>
<section v-if="summary.errors.length" class="hwpod-errors" aria-label="HWPOD blockers">
<strong>阻塞聚合</strong>
<ul>
<li v-for="error in summary.errors" :key="error.key">
<code>{{ error.code }}</code>
<span>{{ error.message }}</span>
</li>
</ul>
</section>
</div>
<section class="hwpod-event-panel" aria-label="node-ops ">
<header>
<span>node-ops</span>
@@ -87,6 +57,51 @@ function onStreamScroll(): void {
<pre id="hwpod-event-text">{{ summary.streamLines.join('\n') || '等待 HWPOD node-ops。' }}</pre>
</div>
</section>
<div v-if="diagnosticsOpen" class="workbench-dialog-backdrop" role="presentation" @click.self="diagnosticsOpen = false">
<section id="hwpod-diagnostics-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="hwpod-diagnostics-title">
<header>
<div class="dialog-title-stack">
<h2 id="hwpod-diagnostics-title">HWPOD 诊断</h2>
<p>{{ summary.hwpodName }} / {{ summary.nodeId }} / {{ formatTimestamp(summary.updatedAt) }}</p>
</div>
<button type="button" class="dialog-close" aria-label="关闭" @click="diagnosticsOpen = false">x</button>
</header>
<div class="evidence-grid" aria-label="HWPOD evidence level">
<span>DEV-LIVE</span>
<span>SOURCE</span>
<span>LOCAL</span>
<span>DRY-RUN</span>
<span>BLOCKED</span>
</div>
<section class="hwpod-node-status" aria-label="HWPOD node-ops status">
<div class="hwpod-node-summary" id="hwpod-node-summary">
<button v-for="tile in summary.tiles" :key="tile.key" class="summary-tile" type="button" :data-hwpod-detail="tile.key" :data-hwpod-title="tile.detail.title" @click="selectedDetail = tile.detail">
<span>{{ tile.label }}</span>
<strong>{{ tile.value }}</strong>
</button>
</div>
<dl class="hwpod-node-meta">
<div><dt>hwpod</dt><dd>{{ summary.hwpodName }}</dd></div>
<div><dt>node</dt><dd>{{ summary.nodeId }}</dd></div>
<div><dt>workspace</dt><dd>{{ summary.workspacePath }}</dd></div>
<div><dt>case</dt><dd>{{ summary.caseId }}</dd></div>
<div><dt>route</dt><dd>{{ summary.route }}</dd></div>
<div><dt>updated</dt><dd>{{ formatTimestamp(summary.updatedAt) }}</dd></div>
<div><dt>ws</dt><dd>{{ summary.connectedCount }} connected / {{ summary.pendingCount }} pending</dd></div>
<div><dt>supported ops</dt><dd>{{ summary.supportedOpsCount }}</dd></div>
</dl>
</section>
<section v-if="summary.errors.length" class="hwpod-errors" aria-label="HWPOD blockers">
<strong>阻塞聚合</strong>
<ul>
<li v-for="error in summary.errors" :key="error.key">
<code>{{ error.code }}</code>
<span>{{ error.message }}</span>
</li>
</ul>
</section>
</section>
</div>
<div v-if="selectedDetail" class="workbench-dialog-backdrop" role="presentation" @click.self="selectedDetail = null">
<section id="hwpod-detail-dialog" class="workbench-dialog" role="dialog" aria-modal="true" aria-labelledby="hwpod-detail-title">
<header>
@@ -6,6 +6,7 @@ import TraceTimeline from "@/components/agent/TraceTimeline.vue";
import CodeAgentStatusSummary from "@/components/workbench/CodeAgentStatusSummary.vue";
import MessageActions from "@/components/workbench/MessageActions.vue";
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
import { isTerminalStatus } from "@/composables/useTraceSubscription";
import { useClipboard } from "@/composables/useClipboard";
import { useWorkbenchStore } from "@/stores/workbench";
import { traceIdentityText } from "./message-rendering";
@@ -18,6 +19,14 @@ const detailMessage = computed(() => workbench.messages.find((message) => messag
function openDetails(message: ChatMessage): void {
detailMessageId.value = message.id;
}
function traceDefaultExpanded(message: ChatMessage): boolean {
return !isTerminalStatus(message.runnerTrace?.status ?? message.status);
}
function traceStorageKey(message: ChatMessage): string {
return `hwlab.workbench.trace-open.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`;
}
</script>
<template>
@@ -30,12 +39,10 @@ function openDetails(message: ChatMessage): void {
<button v-if="message.role === 'agent'" class="message-detail-button" type="button" aria-label="消息详情" title="消息详情" @click="openDetails(message)">i</button>
</div>
</header>
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :default-expanded="traceDefaultExpanded(message)" :storage-key="traceStorageKey(message)" />
<MessageMarkdown class="message-text" :source="message.text" />
</article>
<article v-if="workbench.messages.length === 0" class="message-card empty-message">
<header><strong>等待会话</strong><StatusBadge status="source" label="SOURCE" /></header>
<MessageMarkdown class="message-text" source="请先显式新建或选择 session。Code Agent 不会自动滚动新 session 掩盖失败。" />
</article>
<div v-if="workbench.messages.length === 0" class="conversation-empty-hint">发起对话或从左侧选择 session</div>
<div v-if="detailMessage" class="workbench-dialog-backdrop" role="presentation" @click.self="detailMessageId = null">
<section id="message-detail-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="message-detail-title">
<header>
@@ -45,7 +52,6 @@ function openDetails(message: ChatMessage): void {
</div>
<button type="button" class="dialog-close" aria-label="关闭" @click="detailMessageId = null">x</button>
</header>
<TraceTimeline :trace="detailMessage.runnerTrace" />
<CodeAgentStatusSummary :message="detailMessage" />
<MessageActions
:message="detailMessage"
+44 -7
View File
@@ -48,10 +48,9 @@
}
.platform-content {
display: grid;
min-height: 0;
min-width: 0;
overflow: hidden;
overflow: auto;
padding: 22px;
}
@@ -108,7 +107,8 @@
}
.route-stack {
overflow: auto;
align-content: start;
overflow: visible;
}
.workbench-route {
@@ -558,7 +558,10 @@
}
.hwpod-panel {
grid-template-rows: auto auto minmax(0, auto) auto minmax(0, 1fr);
align-content: stretch;
grid-template-rows: auto minmax(220px, 1fr);
overflow: auto;
overscroll-behavior: contain;
}
.panel-header {
@@ -623,6 +626,13 @@
padding: 14px;
}
.conversation-empty-hint {
justify-self: center;
margin-top: 24px;
color: #64748b;
font-size: 13px;
}
.message-card {
display: grid;
align-self: start;
@@ -630,6 +640,10 @@
padding: 12px;
}
.message-card[data-role="agent"] {
width: 100%;
}
.message-card[data-role="user"] {
justify-self: end;
width: max-content;
@@ -1045,6 +1059,28 @@
font-weight: 750;
}
.hwpod-panel-actions {
display: inline-flex;
min-width: 0;
flex: 0 0 auto;
align-items: center;
gap: 8px;
}
.hwpod-diagnostics-button {
display: inline-flex;
width: 28px;
height: 28px;
align-items: center;
justify-content: center;
border: 1px solid #cbd5e1;
border-radius: 999px;
background: white;
color: #0c4a6e;
font-size: 13px;
font-weight: 900;
}
.hwpod-node-status,
.hwpod-event-panel {
display: grid;
@@ -1058,6 +1094,7 @@
.hwpod-event-panel {
grid-template-rows: auto minmax(0, 1fr);
min-height: 0;
}
.hwpod-node-summary {
@@ -1164,7 +1201,6 @@
.hwpod-event-scroll {
min-height: 0;
max-height: none;
overflow: auto;
border: 1px solid #d8e1eb;
border-radius: 8px;
@@ -1197,6 +1233,7 @@
.empty-state {
display: grid;
align-self: start;
justify-items: start;
gap: 8px;
}
@@ -1895,7 +1932,7 @@
}
.workbench-route {
height: auto;
min-height: calc(100dvh - 102px);
overflow: auto;
}
@@ -1903,7 +1940,7 @@
.session-rail,
.workbench-center,
.hwpod-panel {
height: auto;
min-height: 0;
}
.hwpod-event-scroll {