Merge pull request #1234 from pikasTech/fix/issue-1232-hwpod-panel-noise
fix: reduce v0.3 workbench layout noise
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user