Merge pull request #1468 from pikasTech/fix/1422-user-visible-probe
fix: separate Workbench user-visible performance probe
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user