Merge pull request #1468 from pikasTech/fix/1422-user-visible-probe

fix: separate Workbench user-visible performance probe
This commit is contained in:
Lyon
2026-06-18 11:39:33 +08:00
committed by GitHub
3 changed files with 76 additions and 4 deletions
+33 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// SPEC: PJ2026-010401 Web工作台 - Workbench 可见性与性能探针
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0; PJ2026-010401 Web工作台 - Workbench 可见性与性能探针
import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import fs from "node:fs";
@@ -936,7 +936,27 @@ async function collectTraceTimelineSample(page, args, index) {
const trace = latestAgent?.querySelector(".trace-timeline") ?? null;
const details = trace?.querySelector("details") ?? null;
const rows = latestAgent ? [...latestAgent.querySelectorAll(".trace-render-row")] : [];
const rowPreview = (row) => row?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 240) ?? null;
const rowSummary = (row) => row ? {
rowId: row.getAttribute("data-row-id") ?? null,
kind: row.getAttribute("data-row-kind") ?? null,
terminal: row.getAttribute("data-terminal") === "true",
status: row.getAttribute("data-event-status") ?? null,
preview: rowPreview(row)
} : null;
const isUserVisibleOutputRow = (row) => {
const rowId = row.getAttribute("data-row-id") ?? "";
const kind = row.getAttribute("data-row-kind") ?? "";
const text = row.textContent?.replace(/\s+/gu, " ").trim() ?? "";
if (kind === "tool" || rowId.startsWith("tool:")) return true;
if (rowId.startsWith("trace-final-response:")) return true;
if (/助手(?:最终)?消息|assistant (?:final )?message|final response/iu.test(text)) return true;
if (row.getAttribute("data-terminal") === "true" && row.querySelector(".trace-row-body")?.textContent?.trim()) return true;
return false;
};
const latestRow = rows.at(-1) ?? null;
const userVisibleRows = rows.filter(isUserVisibleOutputRow);
const latestUserVisibleRow = userVisibleRows.at(-1) ?? null;
const empty = latestAgent?.querySelector(".trace-empty") ?? null;
const finalResponse = latestAgent?.querySelector(".message-final-response") ?? null;
return {
@@ -949,7 +969,11 @@ async function collectTraceTimelineSample(page, args, index) {
traceOpen: details instanceof HTMLDetailsElement ? details.open : null,
rowCount: rows.length,
emptyLabel: empty?.textContent?.replace(/\s+/gu, " ").trim() ?? null,
latestRowPreview: latestRow?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 240) ?? null,
latestRowPreview: rowPreview(latestRow),
latestRow: rowSummary(latestRow),
userVisibleRowCount: userVisibleRows.length,
latestUserVisibleRowPreview: rowPreview(latestUserVisibleRow),
latestUserVisibleRow: rowSummary(latestUserVisibleRow),
finalTextChars: finalResponse?.textContent?.trim().length ?? 0
};
}, index);
@@ -1057,6 +1081,7 @@ function compactSessionState(state) {
function summarizeTraceRun(samples, dom, sessionSummary) {
const firstDomRow = samples.find((sample) => Number(sample.rowCount ?? 0) > 0) ?? null;
const firstUserVisibleRow = samples.find((sample) => Number(sample.userVisibleRowCount ?? 0) > 0) ?? null;
const firstRestEvent = samples.find((sample) => Number(sample.traceFetch?.eventCount ?? 0) > 0) ?? null;
const last = samples.at(-1) ?? null;
const failedFetches = samples.filter((sample) => sample.traceFetch && sample.traceFetch.ok !== true);
@@ -1069,8 +1094,12 @@ function summarizeTraceRun(samples, dom, sessionSummary) {
finalAgentStatus: last?.agentStatus ?? null,
finalTraceStatus: last?.traceStatus ?? null,
finalDomRowCount: last?.rowCount ?? null,
finalUserVisibleDomRowCount: last?.userVisibleRowCount ?? null,
firstDomRowVisibleAt: firstDomRow?.sampledAt ?? null,
firstDomRowPreview: firstDomRow?.latestRowPreview ?? null,
firstUserVisibleRowAt: firstUserVisibleRow?.sampledAt ?? null,
firstUserVisibleRowPreview: firstUserVisibleRow?.latestUserVisibleRowPreview ?? null,
firstUserVisibleRow: firstUserVisibleRow?.latestUserVisibleRow ?? null,
firstRestEventVisibleAt: firstRestEvent?.sampledAt ?? null,
firstRestEventCount: firstRestEvent?.traceFetch?.eventCount ?? null,
restTraceOk: samples.some((sample) => sample.traceFetch?.ok === true),
@@ -1087,8 +1116,10 @@ function summarizeProbePerformance(actions, startedAt, samples, traceSummary) {
startedAt,
submittedAt,
firstDomTraceRowAt: traceSummary?.firstDomRowVisibleAt ?? null,
firstUserVisibleTraceRowAt: traceSummary?.firstUserVisibleRowAt ?? null,
firstRestTraceEventAt: traceSummary?.firstRestEventVisibleAt ?? null,
submitToFirstDomTraceRowMs: submittedAt && traceSummary?.firstDomRowVisibleAt ? Math.max(0, new Date(traceSummary.firstDomRowVisibleAt).getTime() - new Date(submittedAt).getTime()) : null,
submitToFirstUserVisibleTraceRowMs: submittedAt && traceSummary?.firstUserVisibleRowAt ? Math.max(0, new Date(traceSummary.firstUserVisibleRowAt).getTime() - new Date(submittedAt).getTime()) : null,
submitToFirstRestTraceEventMs: submittedAt && traceSummary?.firstRestEventVisibleAt ? Math.max(0, new Date(traceSummary.firstRestEventVisibleAt).getTime() - new Date(submittedAt).getTime()) : null,
totalSampleWindowMs: samples.length >= 2 ? Math.max(0, new Date(samples.at(-1).sampledAt).getTime() - new Date(samples[0].sampledAt).getTime()) : 0
};
@@ -120,7 +120,7 @@ function terminalTraceEmptyLabel(status: unknown): string {
</summary>
<div class="trace-disclosure-body">
<ol ref="listRef" @scroll="onScroll">
<li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'">
<li v-for="(row, index) in readableRows" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'" :data-row-kind="rowIsTool(row) ? 'tool' : 'message'" :data-row-id="row.rowId">
<details v-if="rowIsTool(row)" class="trace-tool-details">
<summary>
<code>{{ row.header }}</code>
+42 -1
View File
@@ -370,7 +370,48 @@ export const useWorkbenchStore = defineStore("workbench", () => {
async function waitForPendingSessionCreate(conversationId: string): Promise<SessionCreatePersistenceResult> {
const pending = pendingSessionCreates.get(conversationId);
if (!pending) return { ok: true, error: null };
return pending;
const result = await pending;
if (result.ok) {
if (pendingSessionCreates.get(conversationId) === pending) pendingSessionCreates.delete(conversationId);
return result;
}
const recovered = await recoverPendingSessionCreate(conversationId, result.error);
if (recovered.ok && pendingSessionCreates.get(conversationId) === pending) pendingSessionCreates.delete(conversationId);
return recovered;
}
async function recoverPendingSessionCreate(conversationId: string, originalError: string | null): Promise<SessionCreatePersistenceResult> {
const normalized = normalizeWorkbenchConversationId(conversationId);
if (!normalized) return { ok: false, error: originalError ?? "session create failed" };
const selectedProjectId = activeProjectId.value;
const [workspaceResult, conversationResult] = await Promise.all([
api.workbench.workspace(selectedProjectId),
api.workbench.conversation(normalized, { projectId: selectedProjectId })
]);
const workspaceSnapshot = workspaceResult.ok ? workspaceResult.data?.workspace ?? null : null;
const workspaceSelectedId = selectedConversationIdFromWorkspace(workspaceSnapshot);
const workspaceConversation = workspaceSnapshot?.selectedConversation?.conversationId === normalized ? workspaceSnapshot.selectedConversation : null;
const conversation = conversationResult.ok && !isArchivedConversation(conversationResult.data?.conversation) ? conversationResult.data?.conversation ?? null : null;
if (workspaceSelectedId !== normalized && !workspaceConversation && !conversation) {
return { ok: false, error: originalError ?? workspaceResult.error ?? conversationResult.error ?? "session create failed" };
}
const authoritativeConversation = conversation ?? workspaceConversation ?? visibleConversations.value.find((item) => item.conversationId === normalized) ?? null;
if (authoritativeConversation) {
rememberConversationDetail(authoritativeConversation);
conversations.value = mergeConversationIntoList(conversations.value, authoritativeConversation);
}
if (workspaceSnapshot && activeConversationId.value === normalized) {
const persistedProjectId = conversationProjectId(authoritativeConversation, selectedProjectId);
workspace.value = authoritativeConversation ? workspaceWithSelectedConversation(workspaceSnapshot, authoritativeConversation, persistedProjectId) : workspaceSnapshot;
rememberWorkspaceSnapshot(persistedProjectId, workspace.value);
error.value = null;
void hydrateTurnStatusAuthority(messages.value);
void hydrateTraceEvents(messages.value);
if (authoritativeConversation) void refreshSelectedSessionStatus(authoritativeConversation);
void refreshConversations(normalized);
restartRealtime("create-session-recovered");
}
return { ok: true, error: null };
}
function setActiveConversationSelection(conversationId: string | null | undefined): void {