fix: tighten web trace reading
This commit is contained in:
@@ -1616,7 +1616,10 @@ test("hwlab-cli client agent trace can render with the Web trace row path", asyn
|
||||
assert.equal(result.payload.body.sourceEventCount, 3);
|
||||
assert.ok(result.payload.body.renderedRowCount >= 1);
|
||||
assert.ok(result.payload.body.rows.some((row: any) => /助手最终消息/u.test(row.header)));
|
||||
assert.equal(JSON.stringify(result.payload.body.rows).includes("request accepted"), false);
|
||||
assert.equal(JSON.stringify(result.payload.body.rows).includes("request:accepted"), false);
|
||||
assert.equal(JSON.stringify(result.payload.body.rows).includes("session:reused"), false);
|
||||
assert.equal(result.payload.body.rows[0].body, "当前有两个 HWPOD 可用。");
|
||||
assert.equal(result.payload.traceStatus, "completed");
|
||||
assert.equal(result.payload.rendered.traceStatus, "completed");
|
||||
assert.equal(result.payload.rendered.render, "web");
|
||||
@@ -1710,6 +1713,8 @@ test("hwlab-cli Web trace render does not treat AgentRun result-ready as final a
|
||||
assert.equal(text.includes("助手最终消息"), false);
|
||||
assert.equal(text.includes("助手消息"), false);
|
||||
assert.match(text, /轮次完成/u);
|
||||
assert.match(text, /总耗时 00:00:02/u);
|
||||
assert.equal(text.includes("总耗时 00:00:00"), false);
|
||||
assert.equal(text.includes("AgentRun result is ready for HWLAB short-connection polling"), false);
|
||||
});
|
||||
|
||||
@@ -1785,6 +1790,9 @@ test("hwlab-cli Web trace render keeps AgentRun middle assistant message", async
|
||||
assert.match(text, /我先检查当前 HWPOD 列表/u);
|
||||
assert.match(text, /当前有 2 个 HWPOD 可用/u);
|
||||
assert.match(text, /助手最终消息/u);
|
||||
assert.match(text, /总耗时 00:00:03/u);
|
||||
assert.equal(text.includes("agentrun:request:accepted"), false);
|
||||
assert.equal(text.includes("总耗时 00:00:00"), false);
|
||||
});
|
||||
|
||||
test("hwlab-cli Web trace render keeps the full final-response body without truncating long agent summaries", async () => {
|
||||
|
||||
@@ -11,12 +11,11 @@ export interface TraceEventRow {
|
||||
type TraceEvent = Record<string, unknown>;
|
||||
|
||||
export function traceDisplayRows(trace: Record<string, unknown> = {}, events: TraceEvent[] = []): TraceEventRow[] {
|
||||
const effectiveTrace = traceWithInferredStart(trace, events);
|
||||
const rows: TraceEventRow[] = [];
|
||||
const renderedSourceEvents = new Set<string>();
|
||||
const renderedToolIdentities = new Set<string>();
|
||||
const assistantRows: AssistantRowState[] = [];
|
||||
let requestRendered = false;
|
||||
let setupRendered = false;
|
||||
let lastAssistantRowIndex = -1;
|
||||
let completionEvent: TraceEvent | null = null;
|
||||
for (let index = 0; index < events.length; index += 1) {
|
||||
@@ -34,30 +33,22 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
|
||||
if (renderedToolIdentities.has(identity)) continue;
|
||||
renderedToolIdentities.add(identity);
|
||||
}
|
||||
rows.push(traceToolCallRow(trace, event));
|
||||
rows.push(traceToolCallRow(effectiveTrace, event));
|
||||
continue;
|
||||
}
|
||||
if (isRequestTraceEvent(event)) {
|
||||
if (!requestRendered) {
|
||||
rows.push(traceRequestSummaryRow(event));
|
||||
requestRendered = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isSetupTraceEvent(event)) {
|
||||
if (!setupRendered) {
|
||||
rows.push(traceSetupSummaryRow(event));
|
||||
setupRendered = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (isTerminalAssistantTraceEvent(event)) {
|
||||
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, trace, event, { terminal: true });
|
||||
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, effectiveTrace, event, { terminal: true });
|
||||
if (assistantRowIndex >= 0) lastAssistantRowIndex = assistantRowIndex;
|
||||
continue;
|
||||
}
|
||||
if (isAssistantTraceEvent(event)) {
|
||||
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, trace, event, { terminal: false });
|
||||
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, effectiveTrace, event, { terminal: false });
|
||||
if (assistantRowIndex >= 0) lastAssistantRowIndex = assistantRowIndex;
|
||||
continue;
|
||||
}
|
||||
@@ -65,21 +56,21 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
|
||||
completionEvent = event;
|
||||
continue;
|
||||
}
|
||||
rows.push(traceDisplayRow(trace, event));
|
||||
rows.push(traceDisplayRow(effectiveTrace, event));
|
||||
}
|
||||
if (completionEvent) {
|
||||
const finalResponseText = traceFinalResponseText(trace);
|
||||
const finalResponseText = traceFinalResponseText(effectiveTrace);
|
||||
if (finalResponseText) {
|
||||
lastAssistantRowIndex = upsertAuthoritativeFinalResponseRow(rows, assistantRows, completionEvent, finalResponseText);
|
||||
}
|
||||
const lastAssistantRow = lastAssistantRowIndex >= 0 ? rows[lastAssistantRowIndex] : undefined;
|
||||
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(trace, lastAssistantRow, completionEvent);
|
||||
else rows.push(traceCompletionSummaryRow(trace, completionEvent));
|
||||
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(effectiveTrace, lastAssistantRow, completionEvent);
|
||||
else rows.push(traceCompletionSummaryRow(effectiveTrace, completionEvent));
|
||||
}
|
||||
if (rows.length > 0) return rows;
|
||||
if (events.length === 0) return [];
|
||||
if (traceNoiseEventCount(events) === events.length) return [traceNoiseSummaryRow(trace, events)];
|
||||
return events.filter((event) => !isNoisyTraceEvent(event)).map((event) => traceDisplayRow(trace, event));
|
||||
if (traceNoiseEventCount(events) === events.length) return [traceNoiseSummaryRow(effectiveTrace, events)];
|
||||
return events.filter((event) => !isNoisyTraceEvent(event) && !isRequestTraceEvent(event) && !isSetupTraceEvent(event)).map((event) => traceDisplayRow(effectiveTrace, event));
|
||||
}
|
||||
|
||||
export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string {
|
||||
@@ -140,7 +131,7 @@ function traceToolCallRow(trace: Record<string, unknown>, event: TraceEvent): Tr
|
||||
rowId: `tool:${event.itemId ?? event.seq ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceClock(event.createdAt)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(),
|
||||
header: `${traceEventClock(event)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(),
|
||||
body,
|
||||
bodyFormat: "text"
|
||||
};
|
||||
@@ -161,33 +152,6 @@ export function traceNoiseEventCount(events: TraceEvent[] = []): number {
|
||||
return Array.isArray(events) ? events.filter((event) => isNoisyTraceEvent(event)).length : 0;
|
||||
}
|
||||
|
||||
function traceRequestSummaryRow(event: TraceEvent): TraceEventRow {
|
||||
const prompt = cleanTraceText(event.promptSummary ?? event.prompt ?? "");
|
||||
return {
|
||||
rowId: `trace-request:${event.seq ?? "accepted"}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceClock(event.createdAt)} 请求接受${prompt ? `,提示词:"${compactTraceOneLine(prompt, 120)}"` : ""}`,
|
||||
body: null
|
||||
};
|
||||
}
|
||||
|
||||
function traceSetupSummaryRow(event: TraceEvent): TraceEventRow {
|
||||
const label = String(event.label ?? "");
|
||||
const parts = [
|
||||
/session:reused|thread\/resume/iu.test(label) ? "会话复用" : "会话就绪",
|
||||
/prompt:sent|sent prompt/iu.test(label) ? "提示词已发送" : null,
|
||||
/turn[:/]start|turn:started|turn\/start/iu.test(label) ? "轮次开始" : null
|
||||
].filter(Boolean).join(",");
|
||||
return {
|
||||
rowId: `trace-setup:${event.seq ?? "session-turn"}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
tone: "source",
|
||||
header: `${traceClock(event.createdAt)} ${parts || "会话准备完成"}`,
|
||||
body: null
|
||||
};
|
||||
}
|
||||
|
||||
function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEvent, { terminal }: { terminal: boolean }): TraceEventRow {
|
||||
const text = cleanTraceText(event.message ?? event.outputSummary ?? assistantStreamText(trace, event) ?? "");
|
||||
const index = Number.isInteger(event.messageIndex) ? Number(event.messageIndex) : null;
|
||||
@@ -196,7 +160,7 @@ function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEv
|
||||
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
tone: "ok",
|
||||
header: `${traceClock(event.createdAt)} ${terminal ? "助手最终消息" : "助手消息"}${index ? ` ${index}${count ? `/${count}` : ""}` : ""}`,
|
||||
header: `${traceEventClock(event)} ${terminal ? "助手最终消息" : "助手消息"}${index ? ` ${index}${count ? `/${count}` : ""}` : ""}`,
|
||||
terminal: terminal ? true : undefined,
|
||||
body: text || null,
|
||||
bodyFormat: "markdown"
|
||||
@@ -274,7 +238,7 @@ function upsertAuthoritativeFinalResponseRow(rows: TraceEventRow[], assistantRow
|
||||
body: finalText,
|
||||
terminal: true,
|
||||
tone: "ok",
|
||||
header: `${traceClock(completionEvent.createdAt)} 助手最终消息`
|
||||
header: `${traceEventClock(completionEvent)} 助手最终消息`
|
||||
};
|
||||
matchedState.comparableText = comparableText;
|
||||
matchedState.derivedSnapshotSuffix = false;
|
||||
@@ -285,7 +249,7 @@ function upsertAuthoritativeFinalResponseRow(rows: TraceEventRow[], assistantRow
|
||||
rowId: `trace-final-response:${completionEvent.seq ?? "completed"}`,
|
||||
seq: numberOrNull(completionEvent.seq),
|
||||
tone: "ok",
|
||||
header: `${traceClock(completionEvent.createdAt)} 助手最终消息`,
|
||||
header: `${traceEventClock(completionEvent)} 助手最终消息`,
|
||||
terminal: true,
|
||||
body: finalText,
|
||||
bodyFormat: "markdown"
|
||||
@@ -348,7 +312,7 @@ function traceCompletionSummaryRow(trace: Record<string, unknown>, event: TraceE
|
||||
rowId: `trace-completion:${event.seq ?? "turn"}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceClock(event.createdAt)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))})`,
|
||||
header: `${traceEventClock(event)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))})`,
|
||||
body: null
|
||||
};
|
||||
}
|
||||
@@ -361,7 +325,7 @@ function traceNoiseSummaryRow(trace: Record<string, unknown>, events: TraceEvent
|
||||
rowId: "trace-noise-summary",
|
||||
seq: numberOrNull(lastEvent?.seq),
|
||||
tone: lastEvent ? traceEventTone(lastEvent) : "source",
|
||||
header: `${traceClock(lastEvent?.createdAt)} Trace ${status},等待可读事件`,
|
||||
header: `${traceEventClock(lastEvent)} Trace ${status},等待可读事件`,
|
||||
body: `已隐藏 ${events.length} 条 AgentRun backend 状态事件。最新原始事件:${lastLabel}。`,
|
||||
bodyFormat: "text"
|
||||
};
|
||||
@@ -373,7 +337,7 @@ function traceDisplayRow(trace: Record<string, unknown>, event: TraceEvent): Tra
|
||||
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceClock(event.createdAt)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${traceStatusToken(event)} ${label}`.trim(),
|
||||
header: `${traceEventClock(event)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${traceStatusToken(event)} ${label}`.trim(),
|
||||
body: traceDisplayBody(event)
|
||||
};
|
||||
}
|
||||
@@ -561,10 +525,25 @@ function traceClock(value: unknown): string {
|
||||
return Number.isNaN(date.getTime()) ? "--:--:--" : date.toISOString().slice(11, 19);
|
||||
}
|
||||
|
||||
function traceEventClock(event: TraceEvent | null | undefined): string {
|
||||
return traceClock(traceEventTimestamp(event));
|
||||
}
|
||||
|
||||
function traceWithInferredStart(trace: Record<string, unknown>, events: TraceEvent[]): Record<string, unknown> {
|
||||
if (nonEmptyString(trace.startedAt ?? trace.createdAt)) return trace;
|
||||
const firstTimestamp = events.map((event) => traceEventTimestamp(event)).find(Boolean);
|
||||
return firstTimestamp ? { ...trace, startedAt: firstTimestamp } : trace;
|
||||
}
|
||||
|
||||
function traceEventTimestamp(event: TraceEvent | null | undefined): string | null {
|
||||
if (!event) return null;
|
||||
return nonEmptyString(event.createdAt ?? event.ts ?? event.timestamp ?? event.observedAt);
|
||||
}
|
||||
|
||||
function traceRelativeMs(trace: Record<string, unknown>, event: TraceEvent): number {
|
||||
if (typeof event.elapsedMs === "number") return event.elapsedMs;
|
||||
const start = Date.parse(String(trace.startedAt ?? trace.createdAt ?? ""));
|
||||
const current = Date.parse(String(event.createdAt ?? ""));
|
||||
const current = Date.parse(String(traceEventTimestamp(event) ?? ""));
|
||||
return Number.isNaN(start) || Number.isNaN(current) ? 0 : Math.max(0, current - start);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,13 @@ assertIncludes(appSource, "activityRef: () => activityRef.value", "Workbench mus
|
||||
assertIncludes(appSource, "getAgentChatResult(resultUrl, inactivityTimeoutMs, activityRef)", "Trace result polling must use activityRef-backed inactivity timeout");
|
||||
assertIncludes(appSource, "for (;;) ", "trace subscription must keep unbounded polling without total timeout");
|
||||
assertIncludes(appSource, "/v1/agent/chat/trace/", "trace replay must use Cloud Web same-origin trace API");
|
||||
assertIncludes(appSource, "trace-meta-details", "Trace identity and raw-event controls must stay behind a compact details disclosure");
|
||||
assertIncludes(appSource, "grid-template-columns: minmax(0, 1fr);", "Trace rows must give the readable body the full content width");
|
||||
assertIncludes(appSource, "const primaryAction = computed", "Workbench composer must derive a single primary send/cancel/steer action");
|
||||
assertIncludes(appSource, "primaryAction.value === \"cancel\"", "Composer primary button must switch to cancel while a turn is running and the draft is empty");
|
||||
assertIncludes(appSource, "primaryAction.value === \"steer\"", "Composer primary button must switch to steer while a turn is running and the draft has text");
|
||||
assertIncludes(appSource, ":data-action=\"primaryAction\"", "Composer primary button must expose the active turn/steer/cancel action");
|
||||
assert.doesNotMatch(appSource, /取消运行中 Trace/u, "Workbench must not render a separate stale cancel-running-trace button");
|
||||
assertIncludes(appSource, "session_required", "explicit session policy must be represented");
|
||||
assertIncludes(appSource, "DEV-LIVE", "evidence labels must remain visible");
|
||||
assertIncludes(appSource, "CodeWorkbenchView", "Code Workbench must be a route view, not the app shell");
|
||||
|
||||
@@ -21,6 +21,7 @@ const noiseCount = computed(() => traceNoiseEventCount(events.value as Record<st
|
||||
const lastEvent = computed(() => events.value.at(-1) ?? null);
|
||||
const lastReadableRow = computed(() => readableRows.value.at(-1) ?? null);
|
||||
const lastEventLabel = computed(() => firstNonEmptyString(lastReadableRow.value?.header, props.trace?.lastEventLabel, lastEvent.value?.label, lastEvent.value?.type, lastEvent.value?.kind) ?? "未观测");
|
||||
const detailTitle = computed(() => props.trace?.traceId ? `运行详情 ${props.trace.traceId}` : "运行详情");
|
||||
const summaryItems = computed(() => {
|
||||
const agentRun = recordValue(props.trace?.agentRun);
|
||||
return [
|
||||
@@ -83,19 +84,28 @@ function recordValue(value: unknown): Record<string, unknown> | null {
|
||||
<header class="trace-toolbar">
|
||||
<button class="trace-toggle" type="button" @click="expanded = !expanded">{{ expanded ? "收起" : "展开" }}</button>
|
||||
<strong>Trace</strong>
|
||||
<span class="trace-id">{{ trace.traceId || "pending" }}</span>
|
||||
<small>{{ readableRows.length }} readable / {{ eventCount }} events</small>
|
||||
<small v-if="noiseCount > 0">hidden={{ noiseCount }}</small>
|
||||
<small>last={{ lastEventLabel }}</small>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="following = !following">{{ following ? "跟随" : "暂停" }}</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="jumpToBottom">到底部</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="rawMode = !rawMode">{{ rawMode ? "可读视图" : "原始事件" }}</button>
|
||||
</header>
|
||||
<div v-if="summaryItems.length > 0" class="trace-summary-strip" aria-label="Trace identity summary">
|
||||
<span v-for="item in summaryItems" :key="item.label" class="trace-summary-chip"><b>{{ item.label }}</b>{{ item.value }}</span>
|
||||
</div>
|
||||
<p v-if="trace.eventsCompacted" class="trace-warning">当前 result 返回被压缩,已通过 trace replay 入口请求完整事件;完成前不要把压缩窗口当完整 trace。</p>
|
||||
<p v-if="noiseCount > 0 && !rawMode" class="trace-warning">已按共享 trace renderer 聚合隐藏 {{ noiseCount }} 条 backend/chunk/token 噪声事件;需要审计原始 payload 时可切换到原始事件。</p>
|
||||
<details class="trace-meta-details">
|
||||
<summary>{{ detailTitle }}</summary>
|
||||
<div class="trace-meta-panel">
|
||||
<dl class="trace-meta-grid">
|
||||
<div><dt>trace</dt><dd>{{ trace.traceId || "pending" }}</dd></div>
|
||||
<div><dt>rows</dt><dd>{{ readableRows.length }} / {{ eventCount }}</dd></div>
|
||||
<div v-if="noiseCount > 0"><dt>hidden</dt><dd>{{ noiseCount }}</dd></div>
|
||||
<div><dt>last</dt><dd>{{ lastEventLabel }}</dd></div>
|
||||
</dl>
|
||||
<div v-if="summaryItems.length > 0" class="trace-summary-strip" aria-label="Trace identity summary">
|
||||
<span v-for="item in summaryItems" :key="item.label" class="trace-summary-chip"><b>{{ item.label }}</b>{{ item.value }}</span>
|
||||
</div>
|
||||
<p v-if="trace.eventsCompacted" class="trace-warning">当前 result 返回被压缩,已通过 trace replay 入口请求完整事件;完成前不要把压缩窗口当完整 trace。</p>
|
||||
<p v-if="noiseCount > 0 && !rawMode" class="trace-warning">已聚合隐藏 {{ noiseCount }} 条 backend/chunk/token 噪声事件;原始 payload 仍可在原始事件中审计。</p>
|
||||
<div class="trace-meta-actions">
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="following = !following">{{ following ? "跟随" : "暂停" }}</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="jumpToBottom">到底部</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="rawMode = !rawMode">{{ rawMode ? "可读视图" : "原始事件" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<ol v-show="expanded" ref="listRef" @scroll="onScroll">
|
||||
<li v-for="(row, index) in readableRows" v-if="!rawMode" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'">
|
||||
<code>{{ row.header }}</code>
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useWorkbenchStore } from "@/stores/workbench";
|
||||
|
||||
const workbench = useWorkbenchStore();
|
||||
const draft = ref("");
|
||||
const draftsOpen = ref(false);
|
||||
const draftText = computed(() => draft.value.trim());
|
||||
const primaryAction = computed<"turn" | "steer" | "cancel">(() => {
|
||||
if (workbench.composer.submitMode === "steer") return draftText.value ? "steer" : "cancel";
|
||||
return "turn";
|
||||
});
|
||||
const primaryLabel = computed(() => primaryAction.value === "cancel" ? "取消" : primaryAction.value === "steer" ? "Steer" : "发送");
|
||||
const primaryModeLabel = computed(() => {
|
||||
if (workbench.composer.disabledReason) return workbench.composer.disabledReason;
|
||||
if (primaryAction.value === "cancel") return `运行中 ${workbench.composer.targetTraceId ?? "trace"}`;
|
||||
if (primaryAction.value === "steer") return `steer ${workbench.composer.targetTraceId}`;
|
||||
return "turn / 新请求";
|
||||
});
|
||||
const primaryDisabled = computed(() => {
|
||||
if (primaryAction.value === "cancel") return workbench.composer.disabled || !workbench.composer.targetTraceId;
|
||||
return workbench.composer.disabled || !draftText.value;
|
||||
});
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
const value = draft.value;
|
||||
if (primaryAction.value === "cancel") {
|
||||
if (primaryDisabled.value) return;
|
||||
await workbench.cancelRunningTrace();
|
||||
return;
|
||||
}
|
||||
if (!value.trim() || workbench.composer.disabled) return;
|
||||
draft.value = "";
|
||||
await workbench.submitMessage(value);
|
||||
}
|
||||
|
||||
async function submitFromInput(): Promise<void> {
|
||||
if (!draftText.value) return;
|
||||
await submit();
|
||||
}
|
||||
|
||||
function pickDraft(text: string): void {
|
||||
draft.value = text;
|
||||
draftsOpen.value = false;
|
||||
@@ -21,7 +47,7 @@ function pickDraft(text: string): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form id="command-form" class="command-composer" :title="workbench.composer.disabledReason || (workbench.composer.submitMode === 'steer' ? `引导运行中的 turn: ${workbench.composer.targetTraceId}` : '发送新 turn')" @submit.prevent="submit">
|
||||
<form id="command-form" class="command-composer" :title="workbench.composer.disabledReason || (primaryAction === 'cancel' ? `取消运行中的 turn: ${workbench.composer.targetTraceId}` : primaryAction === 'steer' ? `Steer 运行中的 turn: ${workbench.composer.targetTraceId}` : '发送新 turn')" @submit.prevent="submit">
|
||||
<div class="composer-toolbar">
|
||||
<label class="composer-field" for="code-agent-provider-profile">
|
||||
<span>模型通道</span>
|
||||
@@ -47,10 +73,9 @@ function pickDraft(text: string): void {
|
||||
</section>
|
||||
<div class="composer-actions">
|
||||
<span v-if="workbench.composer.disabledReason" class="composer-warning">{{ workbench.composer.disabledReason }}</span>
|
||||
<span v-else class="composer-mode" :data-mode="workbench.composer.submitMode">{{ workbench.composer.submitMode === 'steer' ? `steer ${workbench.composer.targetTraceId}` : 'turn / 新请求' }}</span>
|
||||
<button class="btn btn-secondary" type="button" @click="workbench.cancelRunningTrace">取消运行中 Trace</button>
|
||||
<button id="command-send" class="btn btn-primary" type="submit" :disabled="!draft.trim() || workbench.composer.disabled">{{ workbench.composer.submitMode === 'steer' ? '引导' : '发送' }}</button>
|
||||
<span v-else class="composer-mode" :data-mode="primaryAction">{{ primaryModeLabel }}</span>
|
||||
<button id="command-send" class="btn btn-primary" type="submit" :data-action="primaryAction" :disabled="primaryDisabled">{{ primaryLabel }}</button>
|
||||
</div>
|
||||
<textarea id="command-input" v-model="draft" rows="3" placeholder="输入任务,Enter 发送,Shift+Enter 换行" :disabled="false" @input="workbench.recordActivity('typing')" @keydown.enter.exact.prevent="submit" />
|
||||
<textarea id="command-input" v-model="draft" rows="3" placeholder="输入任务,Enter 发送,Shift+Enter 换行" :disabled="false" @input="workbench.recordActivity('typing')" @keydown.enter.exact.prevent="submitFromInput" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -636,8 +636,9 @@
|
||||
.message-card {
|
||||
display: grid;
|
||||
align-self: start;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
gap: 6px;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.message-card[data-role="agent"] {
|
||||
@@ -711,7 +712,7 @@
|
||||
|
||||
.message-markdown pre,
|
||||
.message-markdown code {
|
||||
border-radius: 6px;
|
||||
border-radius: 3px;
|
||||
background: #0f172a;
|
||||
color: #dbeafe;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
@@ -742,14 +743,74 @@
|
||||
|
||||
.trace-timeline {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
gap: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
background: #f8fafc;
|
||||
padding: 10px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.trace-toolbar {
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.trace-meta-details {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.trace-meta-details summary {
|
||||
cursor: pointer;
|
||||
padding: 5px 7px;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.trace-meta-panel {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.trace-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 4px 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.trace-meta-grid div {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.trace-meta-grid dt {
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.trace-meta-grid dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #334155;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.trace-meta-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.trace-summary-strip {
|
||||
@@ -765,10 +826,10 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 8px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
padding: 5px 8px;
|
||||
padding: 4px 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -795,12 +856,12 @@
|
||||
}
|
||||
|
||||
.trace-toggle {
|
||||
min-height: 28px;
|
||||
min-height: 24px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
padding: 0 9px;
|
||||
padding: 0 7px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -808,8 +869,8 @@
|
||||
.trace-warning,
|
||||
.trace-empty {
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -828,8 +889,8 @@
|
||||
|
||||
.trace-timeline ol {
|
||||
display: grid;
|
||||
max-height: 170px;
|
||||
gap: 6px;
|
||||
max-height: 320px;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
@@ -838,18 +899,26 @@
|
||||
|
||||
.trace-timeline li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 280px) minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 3px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding: 5px 0 0;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trace-timeline li:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.trace-timeline li[data-terminal="true"] {
|
||||
border-left: 3px solid #22c55e;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.trace-render-row .trace-row-body {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
@@ -866,13 +935,13 @@
|
||||
}
|
||||
|
||||
.trace-render-row pre.trace-row-body {
|
||||
max-height: 120px;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 8px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
@@ -963,6 +1032,11 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trace-timeline code {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.command-composer {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
Reference in New Issue
Block a user