Merge pull request #1269 from pikasTech/fix/issue-1268-trace-auto-toggle
修复 Web Trace 生命周期自动展开折叠
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { ChatMessage } from "../src/types/index.ts";
|
||||
import { traceLifecycleExpanded } from "../src/components/workbench/trace-lifecycle.ts";
|
||||
|
||||
test("Trace lifecycle expands only active agent turns", () => {
|
||||
assert.equal(traceLifecycleExpanded(agentMessage({ status: "running", runnerTrace: { status: "running" } })), true);
|
||||
assert.equal(traceLifecycleExpanded(agentMessage({ status: "running", runnerTrace: { status: "pending" } })), true);
|
||||
assert.equal(traceLifecycleExpanded(agentMessage({ status: "completed", runnerTrace: { status: "running" } })), false);
|
||||
assert.equal(traceLifecycleExpanded(agentMessage({ status: "running", runnerTrace: { status: "completed" } })), false);
|
||||
assert.equal(traceLifecycleExpanded(agentMessage({ status: "running", runnerTrace: { status: "running", finalResponse: "ok" } })), false);
|
||||
assert.equal(traceLifecycleExpanded(agentMessage({ status: "failed", runnerTrace: { status: "failed" } })), false);
|
||||
assert.equal(traceLifecycleExpanded({ ...agentMessage({ status: "running" }), role: "user", title: "用户" }), false);
|
||||
});
|
||||
|
||||
function agentMessage(patch: Partial<ChatMessage>): ChatMessage {
|
||||
return { id: "msg_trace_lifecycle", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-15T00:00:00.000Z", ...patch };
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
|
||||
import { traceEventBody, traceEventLabel, traceEventMeta } from "@/components/workbench/message-rendering";
|
||||
import { traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts";
|
||||
|
||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string }>();
|
||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string; autoExpanded?: boolean | null }>();
|
||||
|
||||
const listRef = ref<HTMLOListElement | null>(null);
|
||||
const following = ref(true);
|
||||
@@ -40,9 +40,15 @@ watch(() => [eventCount.value, props.trace?.status, following.value, expanded.va
|
||||
});
|
||||
|
||||
watch(() => [props.trace?.traceId, props.storageKey, props.defaultExpanded] as const, () => {
|
||||
if (typeof props.autoExpanded === "boolean") return;
|
||||
expanded.value = readStoredExpanded() ?? props.defaultExpanded ?? false;
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => [props.trace?.traceId, props.autoExpanded] as const, () => {
|
||||
if (typeof props.autoExpanded !== "boolean") return;
|
||||
expanded.value = props.autoExpanded;
|
||||
}, { immediate: true });
|
||||
|
||||
watch(expanded, (value) => {
|
||||
if (!props.storageKey) return;
|
||||
try { window.localStorage.setItem(props.storageKey, value ? "1" : "0"); } catch { /* ignore */ }
|
||||
|
||||
@@ -9,6 +9,7 @@ import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
|
||||
import { useClipboard } from "@/composables/useClipboard";
|
||||
import { useWorkbenchStore } from "@/stores/workbench";
|
||||
import { traceIdentityText } from "./message-rendering";
|
||||
import { traceLifecycleExpanded } from "./trace-lifecycle";
|
||||
|
||||
const workbench = useWorkbenchStore();
|
||||
const clipboard = useClipboard();
|
||||
@@ -60,11 +61,6 @@ function scrollToBottom(): void {
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
}
|
||||
|
||||
function traceDefaultExpanded(message: ChatMessage): boolean {
|
||||
void message;
|
||||
return false;
|
||||
}
|
||||
|
||||
function traceStorageKey(message: ChatMessage): string {
|
||||
return `hwlab.workbench.trace-open.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`;
|
||||
}
|
||||
@@ -81,7 +77,7 @@ function traceStorageKey(message: ChatMessage): string {
|
||||
<button v-if="message.role === 'agent'" class="message-detail-button" type="button" aria-label="运行详情" title="运行详情" @click="openDetails(message)">!</button>
|
||||
</div>
|
||||
</header>
|
||||
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :default-expanded="traceDefaultExpanded(message)" :storage-key="traceStorageKey(message)" />
|
||||
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :auto-expanded="traceLifecycleExpanded(message)" :storage-key="traceStorageKey(message)" />
|
||||
<MessageMarkdown class="message-text" :source="message.text" />
|
||||
</article>
|
||||
<div v-if="workbench.messages.length === 0" class="conversation-empty-hint">发起对话,或从左侧选择 session。</div>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ChatMessage } from "@/types";
|
||||
|
||||
export function traceLifecycleExpanded(message: ChatMessage): boolean {
|
||||
if (message.role !== "agent") return false;
|
||||
if (hasTraceTerminalSignal(message)) return false;
|
||||
return traceStatusValues(message).some(isTraceActiveStatus);
|
||||
}
|
||||
|
||||
function hasTraceTerminalSignal(message: ChatMessage): boolean {
|
||||
if (hasFinalResponseValue(message.finalResponse) || hasFinalResponseValue(message.runnerTrace?.finalResponse)) return true;
|
||||
return traceStatusValues(message).some(isTraceTerminalStatus);
|
||||
}
|
||||
|
||||
function traceStatusValues(message: ChatMessage): string[] {
|
||||
return [
|
||||
message.status,
|
||||
message.runnerTrace?.status,
|
||||
message.runnerTrace?.traceStatus
|
||||
].map(normalizeTraceStatus).filter((value): value is string => Boolean(value));
|
||||
}
|
||||
|
||||
function normalizeTraceStatus(value: unknown): string | null {
|
||||
const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function isTraceActiveStatus(status: string): boolean {
|
||||
return ["accepted", "pending", "processing", "running", "busy", "creating", "queued"].includes(status);
|
||||
}
|
||||
|
||||
function isTraceTerminalStatus(status: string): boolean {
|
||||
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "aborted", "error", "done"].includes(status);
|
||||
}
|
||||
|
||||
function hasFinalResponseValue(value: unknown): boolean {
|
||||
if (value == null) return false;
|
||||
if (typeof value === "string") return value.trim().length > 0;
|
||||
if (typeof value === "object") return Object.keys(value as Record<string, unknown>).length > 0;
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user