fix: repair webui refresh trace layout (#938)
Co-authored-by: Codex Agent <codex@hwlab.local>
This commit is contained in:
@@ -55,3 +55,23 @@ test("message trace panel exposes running status for live trace animation", () =
|
||||
assert.match(html, /data-trace-status="running"/u);
|
||||
assert.match(html, /open=""/u);
|
||||
});
|
||||
|
||||
test("message trace panel renders persisted fallback for expired historical trace", () => {
|
||||
const trace: RunnerTrace = {
|
||||
traceId: "trc_issue932_expired",
|
||||
status: "expired",
|
||||
traceStatus: "expired",
|
||||
events: [],
|
||||
eventCount: 31,
|
||||
fallback: { available: true, source: "agent-session-snapshot" },
|
||||
traceSummary: { sourceEventCount: 31, terminalStatus: "completed", source: "agent-session-snapshot" },
|
||||
finalResponse: { text: "历史最终回复" }
|
||||
};
|
||||
|
||||
const html = renderToStaticMarkup(<MessageTracePanel trace={trace} defaultOpen />);
|
||||
|
||||
assert.match(html, /历史 trace 已过期/u);
|
||||
assert.match(html, /历史最终回复/u);
|
||||
assert.match(html, /sourceEventCount=31/u);
|
||||
assert.doesNotMatch(html, /等待后端事件/u);
|
||||
});
|
||||
|
||||
@@ -46,7 +46,10 @@ export function MessageTracePanel({ trace, defaultOpen, storageKey, markdownUpgr
|
||||
|
||||
const events = Array.isArray(trace.events) ? trace.events : [];
|
||||
const last = summarizeLastEvent(trace);
|
||||
const rows = useMemo<TraceEventRow[]>(() => traceDisplayRows(trace, events), [trace, events]);
|
||||
const rows = useMemo<TraceEventRow[]>(() => {
|
||||
const eventRows = traceDisplayRows(trace, events);
|
||||
return eventRows.length > 0 ? eventRows : persistentTraceFallbackRows(trace);
|
||||
}, [trace, events]);
|
||||
const noiseCount = useMemo<number>(() => traceNoiseEventCount(events), [events]);
|
||||
const lastUpdatedAt = traceUpdatedAt(trace, events, last.ts);
|
||||
|
||||
@@ -113,6 +116,42 @@ function traceCountText(trace: RunnerTrace, events: number, rows: number, noiseC
|
||||
return `${rows}/${events}/${rawTotal} 行${noise}`;
|
||||
}
|
||||
|
||||
function persistentTraceFallbackRows(trace: RunnerTrace): TraceEventRow[] {
|
||||
const fallback = objectOrNull(trace.fallback);
|
||||
const summary = objectOrNull(trace.traceSummary);
|
||||
const response = objectOrNull(trace.finalResponse);
|
||||
const text = firstNonEmptyTraceText(response?.text, response?.content, response?.message, summary?.finalText, summary?.text);
|
||||
const sourceCount = positiveInteger(summary?.sourceEventCount ?? summary?.eventCount ?? trace.eventCount);
|
||||
if (!text && fallback?.available !== true && sourceCount === 0) return [];
|
||||
const status = firstNonEmptyTraceText(trace.traceStatus, trace.status, summary?.terminalStatus, objectOrNull(trace.agentRun)?.terminalStatus) ?? "expired";
|
||||
const header = `${trace.traceId ?? "trace"} 历史 trace 已过期,显示持久化摘要`;
|
||||
const meta = [
|
||||
`traceStatus=${status}`,
|
||||
sourceCount > 0 ? `sourceEventCount=${sourceCount}` : null,
|
||||
firstNonEmptyTraceText(fallback?.source, summary?.source) ? `source=${firstNonEmptyTraceText(fallback?.source, summary?.source)}` : null
|
||||
].filter(Boolean).join("\n");
|
||||
const body = [meta, text].filter(Boolean).join("\n\n") || "后端没有保留原始 event,但已返回持久化 trace 摘要。";
|
||||
return [{ rowId: `fallback:${trace.traceId ?? "trace"}`, seq: null, tone: "source", header, body, terminal: true, bodyFormat: text ? "markdown" : "text" }];
|
||||
}
|
||||
|
||||
function objectOrNull(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function firstNonEmptyTraceText(...values: unknown[]): string | null {
|
||||
for (const value of values) {
|
||||
if (typeof value !== "string") continue;
|
||||
const text = value.trim();
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function isRunningTrace(trace: RunnerTrace): boolean {
|
||||
return String(trace.status ?? "").toLowerCase() === "running";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { mergeConversationMessages } from "./conversation";
|
||||
import type { ChatMessage } from "../types/domain";
|
||||
|
||||
test("mergeConversationMessages replaces a refreshed agent message instead of appending a duplicate", () => {
|
||||
const base: ChatMessage[] = [
|
||||
message({ id: "msg_user", role: "user", text: "你好", status: "sent" }),
|
||||
message({ id: "msg_agent", role: "agent", text: "处理中", status: "running" })
|
||||
];
|
||||
|
||||
const merged = mergeConversationMessages(base, [
|
||||
message({ id: "msg_agent", role: "agent", text: "完成回复", status: "completed", updatedAt: "2026-06-05T10:00:00.000Z" })
|
||||
]);
|
||||
|
||||
assert.equal(merged.length, 2);
|
||||
const agent = merged[1];
|
||||
assert.ok(agent);
|
||||
assert.equal(agent.text, "完成回复");
|
||||
assert.equal(agent.status, "completed");
|
||||
});
|
||||
|
||||
test("mergeConversationMessages dedupes same trace and role when a refresh generated a new local id", () => {
|
||||
const merged = mergeConversationMessages([
|
||||
message({ id: "msg_agent_old", role: "agent", text: "旧回复", status: "running" })
|
||||
], [
|
||||
message({ id: "msg_agent_new", role: "agent", text: "最终回复", status: "completed" })
|
||||
]);
|
||||
|
||||
assert.equal(merged.length, 1);
|
||||
const agent = merged[0];
|
||||
assert.ok(agent);
|
||||
assert.equal(agent.id, "msg_agent_new");
|
||||
assert.equal(agent.text, "最终回复");
|
||||
});
|
||||
|
||||
function message(overrides: Partial<ChatMessage>): ChatMessage {
|
||||
return {
|
||||
id: "msg_default",
|
||||
role: "agent",
|
||||
title: "Code Agent",
|
||||
text: "",
|
||||
status: "running",
|
||||
traceId: "trc_issue932_refresh",
|
||||
conversationId: "cnv_issue932_refresh",
|
||||
sessionId: "ses_issue932_refresh",
|
||||
threadId: "thread-issue932-refresh",
|
||||
createdAt: "2026-06-05T09:59:00.000Z",
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,36 @@ export function makeMessage(role: "user" | "agent", text: string, status: ChatMe
|
||||
return { id: nextProtocolId("msg"), role, title: meta.title ?? (role === "user" ? "用户" : "Code Agent"), text, status, traceId: meta.traceId, conversationId: meta.conversationId, sessionId: meta.sessionId, threadId: meta.threadId, createdAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
export function mergeConversationMessages(existing: ChatMessage[], incoming: ChatMessage[] = []): ChatMessage[] {
|
||||
const merged: ChatMessage[] = [];
|
||||
for (const message of [...existing, ...incoming]) {
|
||||
const index = merged.findIndex((item) => sameConversationMessage(item, message));
|
||||
const current = index >= 0 ? merged[index] : null;
|
||||
if (current) merged[index] = mergeConversationMessage(current, message);
|
||||
else merged.push(message);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeConversationMessage(existing: ChatMessage, next: ChatMessage): ChatMessage {
|
||||
return {
|
||||
...existing,
|
||||
...next,
|
||||
title: firstNonEmptyString(next.title, existing.title) ?? next.title,
|
||||
text: firstNonEmptyString(next.text, existing.text) ?? next.text,
|
||||
runnerTrace: next.runnerTrace ?? existing.runnerTrace,
|
||||
traceEvents: next.traceEvents ?? existing.traceEvents,
|
||||
error: next.error ?? existing.error,
|
||||
updatedAt: next.updatedAt ?? existing.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
function sameConversationMessage(left: ChatMessage, right: ChatMessage): boolean {
|
||||
if (left.id && right.id && left.id === right.id) return true;
|
||||
if (!left.traceId || !right.traceId || left.traceId !== right.traceId || left.role !== right.role) return false;
|
||||
return !left.conversationId || !right.conversationId || left.conversationId === right.conversationId;
|
||||
}
|
||||
|
||||
export function messageFromAgentResponse(messageId: string, pending: ChatMessage, response: AgentChatResponse | AgentChatResultResponse): ChatMessage {
|
||||
const errorObject = typeof response.error === "object" && response.error ? response.error : null;
|
||||
const failed = Boolean(response.error) || ["failed", "blocked", "timeout", "cancelled"].includes(String(response.status ?? ""));
|
||||
@@ -96,15 +126,17 @@ export async function ensureWorkspace(current: WorkspaceRecord | null): Promise<
|
||||
}
|
||||
|
||||
export async function persistConversation(input: { workspace: WorkspaceRecord | null; conversationId: string; sessionId: string | null; threadId: string | null; messages: ChatMessage[] }): Promise<WorkspaceRecord | null> {
|
||||
const messages = mergeConversationMessages(input.messages);
|
||||
const latest = messages.at(-1);
|
||||
await api.saveConversation(input.conversationId, {
|
||||
projectId: WORKBENCH_PROJECT_ID,
|
||||
conversationId: input.conversationId,
|
||||
sessionId: input.sessionId,
|
||||
threadId: input.threadId,
|
||||
sessionStatus: input.messages.at(-1)?.status ?? "source",
|
||||
lastTraceId: input.messages.at(-1)?.traceId,
|
||||
messages: input.messages,
|
||||
snapshot: { source: "cloud-web-react-account-sync", updatedAt: new Date().toISOString(), sessionStatus: input.messages.at(-1)?.status ?? "source" }
|
||||
sessionStatus: latest?.status ?? "source",
|
||||
lastTraceId: latest?.traceId,
|
||||
messages,
|
||||
snapshot: { source: "cloud-web-react-account-sync", updatedAt: new Date().toISOString(), sessionStatus: latest?.status ?? "source" }
|
||||
});
|
||||
if (input.workspace?.workspaceId) {
|
||||
const response = await api.updateWorkspace(input.workspace.workspaceId, {
|
||||
@@ -113,8 +145,8 @@ export async function persistConversation(input: { workspace: WorkspaceRecord |
|
||||
selectedConversationId: input.conversationId,
|
||||
selectedAgentSessionId: input.sessionId,
|
||||
threadId: input.threadId,
|
||||
activeTraceId: input.messages.findLast((message) => message.status === "running")?.traceId ?? null,
|
||||
messages: input.messages,
|
||||
activeTraceId: messages.findLast((message) => message.status === "running")?.traceId ?? null,
|
||||
messages,
|
||||
workspace: { source: "cloud-web-react-workspace-sync", updatedAt: new Date().toISOString() },
|
||||
updatedByClient: "cloud-web-react"
|
||||
});
|
||||
|
||||
@@ -29,8 +29,9 @@ test("issue 853 submit failure persists the same trace before refresh", () => {
|
||||
|
||||
test("issue 853 left sidebar collapse only removes the left columns", () => {
|
||||
const css = fs.readFileSync(path.join(srcRoot, "styles/workbench.css"), "utf8");
|
||||
assert.match(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(0, 1fr\) var\(--resize-handle-width\) var\(--right-sidebar-width\); \}/u);
|
||||
assert.doesNotMatch(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(420px, 1fr\)/u);
|
||||
assert.match(css, /--right-sidebar-left-collapsed-width: min\(var\(--right-sidebar-width\), 46vw\);/u);
|
||||
assert.match(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(360px, 1fr\) var\(--resize-handle-width\) minmax\(min\(var\(--right-sidebar-min-width\), 46vw\), var\(--right-sidebar-left-collapsed-width\)\); \}/u);
|
||||
assert.doesNotMatch(css, /\.workbench-shell\.is-left-sidebar-collapsed \.workspace-body \{ grid-template-columns: 0 0 minmax\(0, 1fr\) var\(--resize-handle-width\) var\(--right-sidebar-width\); \}/u);
|
||||
});
|
||||
|
||||
test("issue 853 completed trace panels keep stored open state instead of closing on refresh", () => {
|
||||
|
||||
@@ -379,6 +379,36 @@ test("fetchTraceSnapshot retries until fast-fail trace events are readable", asy
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchTraceSnapshot accepts expired trace fallback without waiting for raw events", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetches: string[] = [];
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
fetches.push(url);
|
||||
return jsonResponse({
|
||||
status: "completed",
|
||||
traceStatus: "expired",
|
||||
traceId: "trc_expired_fallback",
|
||||
events: [],
|
||||
eventCount: 0,
|
||||
fallback: { available: true, source: "agent-session-snapshot" },
|
||||
traceSummary: { sourceEventCount: 31, terminalStatus: "completed" },
|
||||
finalResponse: { text: "过期 trace 的最终回复" }
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const snapshot = await fetchTraceSnapshot("trc_expired_fallback", "trc_expired_fallback", 1000);
|
||||
assert.equal(snapshot?.status, "expired");
|
||||
assert.equal(snapshot?.eventCount, 31);
|
||||
assert.equal((snapshot?.fallback as { source?: string })?.source, "agent-session-snapshot");
|
||||
assert.equal((snapshot?.finalResponse as { text?: string })?.text, "过期 trace 的最终回复");
|
||||
assert.equal(fetches.length, 1);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
function jsonResponse(payload: unknown): Response {
|
||||
return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface TraceSnapshot {
|
||||
eventCount?: number;
|
||||
eventsCompacted?: boolean;
|
||||
agentRun?: AgentRunProvenance;
|
||||
traceStatus?: string;
|
||||
retention?: unknown;
|
||||
fallback?: unknown;
|
||||
finalResponse?: unknown;
|
||||
traceSummary?: unknown;
|
||||
lastEventLabel?: string;
|
||||
waitingFor?: string;
|
||||
updatedAt?: string;
|
||||
@@ -67,6 +72,12 @@ export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable<Chat
|
||||
events,
|
||||
eventCount: snapshot.eventCount ?? events.length,
|
||||
eventsCompacted: snapshot.eventsCompacted,
|
||||
traceStatus: snapshot.traceStatus,
|
||||
retention: snapshot.retention,
|
||||
fallback: snapshot.fallback,
|
||||
finalResponse: snapshot.finalResponse,
|
||||
traceSummary: snapshot.traceSummary,
|
||||
agentRun: snapshot.agentRun,
|
||||
runnerKind: snapshot.agentRun?.adapter,
|
||||
sessionMode: snapshot.agentRun?.backendProfile,
|
||||
lastEventLabel: snapshot.lastEventLabel ?? events.at(-1)?.label ?? events.at(-1)?.type,
|
||||
@@ -95,6 +106,11 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
traceEvents: terminal.traceEvents ?? events,
|
||||
events,
|
||||
eventCount: trace.eventCount ?? events.length,
|
||||
traceStatus: trace.traceStatus ?? terminal.traceStatus,
|
||||
retention: trace.retention ?? terminal.retention,
|
||||
fallback: trace.fallback ?? terminal.fallback,
|
||||
finalResponse: trace.finalResponse ?? terminal.finalResponse,
|
||||
traceSummary: trace.traceSummary ?? terminal.traceSummary,
|
||||
lastEventLabel: trace.lastEventLabel ?? terminal.lastEventLabel ?? events.at(-1)?.label
|
||||
} as AgentChatResultResponse;
|
||||
}
|
||||
@@ -264,6 +280,11 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
eventCount,
|
||||
eventsCompacted: false,
|
||||
agentRun: tracePolled.data.agentRun,
|
||||
traceStatus: typeof tracePolled.data.traceStatus === "string" ? tracePolled.data.traceStatus : undefined,
|
||||
retention: tracePolled.data.retention,
|
||||
fallback: tracePolled.data.fallback,
|
||||
finalResponse: tracePolled.data.finalResponse,
|
||||
traceSummary: tracePolled.data.traceSummary,
|
||||
lastEventLabel: String(lastEvent?.label ?? lastEvent?.type ?? tracePolled.data.lastEventLabel ?? ""),
|
||||
waitingFor: String(tracePolled.data.waitingFor ?? ""),
|
||||
updatedAt: updatedAt ?? new Date().toISOString()
|
||||
@@ -330,7 +351,7 @@ export async function fetchTraceSnapshot(traceId: string, traceUrlOrId: string,
|
||||
if (signal?.aborted) return null;
|
||||
if (tracePolled.ok && tracePolled.data) {
|
||||
const snapshot = snapshotFromTracePoll(traceId, tracePolled.data);
|
||||
if (hasTraceEvents(snapshot)) return snapshot;
|
||||
if (hasTraceEvents(snapshot) || hasPersistedTraceFallback(snapshot)) return snapshot;
|
||||
}
|
||||
if (attempts >= maxAttempts || Date.now() - startedAt >= totalTimeoutMs) break;
|
||||
await new Promise<void>((resolve) => window.setTimeout(resolve, Math.min(TRACE_SNAPSHOT_RETRY_INTERVAL_MS, totalTimeoutMs - (Date.now() - startedAt))));
|
||||
@@ -345,7 +366,7 @@ function snapshotFromTracePoll(traceId: string, result: AgentChatResultResponse)
|
||||
...result,
|
||||
traceId,
|
||||
events,
|
||||
eventCount: Number(result.eventCount ?? events.length) || 0,
|
||||
eventCount: traceSnapshotEventCount(result, events),
|
||||
lastEventLabel: String(last?.label ?? last?.type ?? result.lastEventLabel ?? "")
|
||||
} as AgentChatResultResponse);
|
||||
}
|
||||
@@ -354,15 +375,31 @@ function hasTraceEvents(snapshot: TraceSnapshot): boolean {
|
||||
return Array.isArray(snapshot.events) && snapshot.events.length > 0;
|
||||
}
|
||||
|
||||
function hasPersistedTraceFallback(snapshot: TraceSnapshot): boolean {
|
||||
const fallback = objectOrNull(snapshot.fallback);
|
||||
const summary = objectOrNull(snapshot.traceSummary);
|
||||
return Boolean(
|
||||
fallback?.available === true ||
|
||||
finalResponseText(snapshot.finalResponse) ||
|
||||
positiveNumber(summary?.sourceEventCount ?? summary?.eventCount) > 0
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotToTraceSnapshot(result: AgentChatResultResponse): TraceSnapshot {
|
||||
const events = Array.isArray((result as { events?: TraceEvent[] }).events) ? ((result as { events?: TraceEvent[] }).events ?? []) : [];
|
||||
const traceStatus = firstNonEmptyString(result.traceStatus, result.status);
|
||||
return {
|
||||
traceId: result.traceId,
|
||||
status: result.status,
|
||||
status: traceStatus ?? result.status,
|
||||
sessionId: result.sessionId ?? null,
|
||||
threadId: result.threadId ?? null,
|
||||
events,
|
||||
eventCount: Number(result.eventCount ?? events.length) || 0,
|
||||
eventCount: traceSnapshotEventCount(result, events),
|
||||
traceStatus: typeof result.traceStatus === "string" ? result.traceStatus : undefined,
|
||||
retention: result.retention,
|
||||
fallback: result.fallback,
|
||||
finalResponse: result.finalResponse,
|
||||
traceSummary: result.traceSummary,
|
||||
agentRun: result.agentRun,
|
||||
lastEventLabel: typeof result.lastEventLabel === "string" ? result.lastEventLabel : String(events.at(-1)?.label ?? events.at(-1)?.type ?? ""),
|
||||
updatedAt: typeof result.updatedAt === "string" ? result.updatedAt : new Date().toISOString()
|
||||
@@ -378,15 +415,26 @@ export async function replayFullTrace(traceId: string, totalTimeoutMs = 15000):
|
||||
polled = await api.getAgentChatResult(url, Math.max(1000, totalTimeoutMs - (Date.now() - startedAt)));
|
||||
}
|
||||
if (!polled.ok || !polled.data) return null;
|
||||
const events = Array.isArray(polled.data.events) ? polled.data.events : [];
|
||||
const lastEvent = events[events.length - 1];
|
||||
return {
|
||||
traceId,
|
||||
status: String(polled.data.status ?? "completed"),
|
||||
events,
|
||||
eventCount: Number(polled.data.eventCount ?? events.length) || 0,
|
||||
eventsCompacted: true,
|
||||
lastEventLabel: String(lastEvent?.label ?? lastEvent?.type ?? ""),
|
||||
updatedAt: String(polled.data.updatedAt ?? new Date().toISOString())
|
||||
};
|
||||
return { ...snapshotFromTracePoll(traceId, polled.data), eventsCompacted: true };
|
||||
}
|
||||
|
||||
function traceSnapshotEventCount(result: AgentChatResultResponse, events: TraceEvent[]): number {
|
||||
const explicit = positiveNumber(result.eventCount);
|
||||
if (explicit > 0 || events.length > 0) return explicit || events.length;
|
||||
const summary = objectOrNull(result.traceSummary);
|
||||
return positiveNumber(summary?.sourceEventCount ?? summary?.eventCount) || 0;
|
||||
}
|
||||
|
||||
function positiveNumber(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function objectOrNull(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function finalResponseText(value: unknown): string | null {
|
||||
const item = objectOrNull(value);
|
||||
return firstNonEmptyString(item?.text, item?.content, item?.message);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { firstNonEmptyString, nextProtocolId } from "../utils";
|
||||
import { makeMessage, messageFromAgentResponse, persistConversation } from "./conversation";
|
||||
import { makeMessage, mergeConversationMessages, messageFromAgentResponse, persistConversation } from "./conversation";
|
||||
import { mergeTraceResults, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "./runner-trace";
|
||||
import type { Action } from "./workbench-reducer";
|
||||
import type { WorkbenchState } from "./workbench-state";
|
||||
@@ -50,13 +50,13 @@ export function useTraceReattach(config: UseTraceReattachConfig): void {
|
||||
const stub = messages.find((m) => m.id === placeholderId) ?? ({ id: placeholderId, role: "agent" as const, text: "", status: "running" as const, traceId: activeTraceId, conversationId, sessionId, threadId, createdAt: new Date().toISOString() } as ChatMessage);
|
||||
const completed = messageFromAgentResponse(placeholderId, stub, mergeTraceResults(result, lastSnapshot));
|
||||
onDispatch({ type: "message:complete", messageId: placeholderId, message: completed, availability: result.availability ?? null, workspace: ws ?? undefined });
|
||||
void onPersist({ workspace: ws, conversationId, sessionId: result.sessionId ?? sessionId, threadId: result.threadId ?? threadId, messages: [...messages, completed] });
|
||||
void onPersist({ workspace: ws, conversationId, sessionId: result.sessionId ?? sessionId, threadId: result.threadId ?? threadId, messages: mergeConversationMessages(messages, [completed]) });
|
||||
controller.abort();
|
||||
},
|
||||
onInfrastructureError: (error) => {
|
||||
const failed = makeMessage("agent", `Code Agent re-attach ${codeAgentTimeoutMs}ms 无活动(${error})`, "failed", { traceId: activeTraceId, conversationId, sessionId, threadId, title: "Code Agent 超时" });
|
||||
onDispatch({ type: "message:fail", messageId: placeholderId, message: failed });
|
||||
void onPersist({ workspace: ws, conversationId, sessionId, threadId, messages: [...messages, failed] });
|
||||
void onPersist({ workspace: ws, conversationId, sessionId, threadId, messages: mergeConversationMessages(messages, [failed]) });
|
||||
controller.abort();
|
||||
}
|
||||
}).finally(() => controller.abort());
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
--right-sidebar-width: 620px;
|
||||
--right-sidebar-min-width: 420px;
|
||||
--right-sidebar-max-width: 680px;
|
||||
--right-sidebar-left-collapsed-width: min(var(--right-sidebar-width), 46vw);
|
||||
--right-collapsed-width: 46px;
|
||||
--resize-handle-width: 8px;
|
||||
}
|
||||
@@ -55,7 +56,7 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||
.status-chip-label { flex: 0 0 auto; }
|
||||
.status-chip-meta { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
|
||||
.workspace-body { min-width: 0; min-height: 0; display: grid; grid-template-columns: var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) var(--resize-handle-width) var(--right-sidebar-width); grid-template-rows: minmax(0, 1fr); overflow: hidden; }
|
||||
.workbench-shell.is-left-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(0, 1fr) var(--resize-handle-width) var(--right-sidebar-width); }
|
||||
.workbench-shell.is-left-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(360px, 1fr) var(--resize-handle-width) minmax(min(var(--right-sidebar-min-width), 46vw), var(--right-sidebar-left-collapsed-width)); }
|
||||
.workbench-shell.is-right-sidebar-collapsed .workspace-body { grid-template-columns: var(--session-sidebar-width) var(--resize-handle-width) minmax(420px, 1fr) 0 0; }
|
||||
.workbench-shell.is-left-sidebar-collapsed.is-right-sidebar-collapsed .workspace-body { grid-template-columns: 0 0 minmax(0, 1fr) 0 0; }
|
||||
.workspace-body.is-route-without-sidebars { grid-template-columns: minmax(0, 1fr); }
|
||||
|
||||
Reference in New Issue
Block a user