Merge pull request #1370 from pikasTech/fix/issue1368-1369-workbench-trace-timeout
fix(v03): 稳定 Workbench Trace 加载与超时链路
This commit is contained in:
@@ -433,7 +433,7 @@ function isAgentRunCommandAlreadyClaimed(error) {
|
||||
return statusCode === 409 && /command\s+[^\s]+\s+is not pending:/u.test(message);
|
||||
}
|
||||
|
||||
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true }) {
|
||||
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true, forceResultSync = false }) {
|
||||
const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
|
||||
const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial;
|
||||
if (!mapped?.agentRun?.runId || !mapped?.agentRun?.commandId) return { result: currentResult, runnerTrace: traceStore.snapshot(traceId), found: Boolean(currentResult) };
|
||||
@@ -443,6 +443,21 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
|
||||
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
|
||||
const eventsResponse = refreshEvents ? await fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, env, mapping: { ...mapped.agentRun, traceSummary: mapped.traceSummary } }) : null;
|
||||
if (eventsResponse) appendAgentRunEventsToTrace(traceStore, traceId, eventsResponse.events, mapped.agentRun);
|
||||
if (!forceResultSync && !agentRunCommandResultSyncRequired(mapped, eventsResponse)) {
|
||||
const nextMapping = withAgentRunRunnerJobCount({
|
||||
...mapped.agentRun,
|
||||
lastSeq: eventsResponse ? agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq) : mapped.agentRun.lastSeq,
|
||||
status: mapped.agentRun.status ?? "running",
|
||||
runStatus: mapped.agentRun.runStatus ?? null,
|
||||
commandState: mapped.agentRun.commandState ?? null,
|
||||
terminalStatus: null,
|
||||
updatedAt: nowIso(options.now),
|
||||
valuesPrinted: false
|
||||
});
|
||||
const payload = decorateAgentRunRunningResult({ base: { ...mapped, status: "running", agentRun: nextMapping, updatedAt: nowIso(options.now) }, mapping: nextMapping, traceStore, traceId });
|
||||
options.codeAgentChatResults?.set?.(traceId, payload);
|
||||
return { result: payload, runnerTrace: payload.runnerTrace ?? traceStore.snapshot(traceId), found: true };
|
||||
}
|
||||
const result = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/commands/${encodeURIComponent(mapped.agentRun.commandId)}/result`, {
|
||||
method: "GET",
|
||||
timeoutMs,
|
||||
@@ -465,6 +480,42 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
|
||||
return { result: payload, runnerTrace: payload.runnerTrace ?? traceStore.snapshot(traceId), found: true };
|
||||
}
|
||||
|
||||
function agentRunCommandResultSyncRequired(mapped = {}, eventsResponse = null) {
|
||||
const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : {};
|
||||
if (isAgentRunTerminalStatus(mapped?.status)) return true;
|
||||
if (isAgentRunTerminalStatus(agentRun.terminalStatus ?? agentRun.commandState ?? agentRun.status)) return true;
|
||||
return Boolean(agentRunTerminalStatusFromEvents(eventsResponse?.events));
|
||||
}
|
||||
|
||||
function agentRunTerminalStatusFromEvents(events = []) {
|
||||
if (!Array.isArray(events)) return null;
|
||||
for (const event of [...events].reverse()) {
|
||||
const payload = event?.payload && typeof event.payload === "object" ? event.payload : {};
|
||||
const candidates = [
|
||||
event?.type === "terminal_status" ? payload.terminalStatus : null,
|
||||
payload.phase === "command-terminal" ? payload.terminalStatus : null,
|
||||
payload.phase === "turn:completed" || payload.phase === "turn/steer:completed" ? "completed" : null,
|
||||
payload.terminalStatus,
|
||||
payload.status
|
||||
];
|
||||
for (const value of candidates) {
|
||||
const status = normalizeAgentRunStatus(value);
|
||||
if (isAgentRunTerminalStatus(status)) return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAgentRunTerminalStatus(value) {
|
||||
const status = normalizeAgentRunStatus(value);
|
||||
return status === "timeout" || TERMINAL_RUN_STATUSES.has(status);
|
||||
}
|
||||
|
||||
function normalizeAgentRunStatus(value) {
|
||||
const status = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
|
||||
return status === "cancelled" ? "canceled" : status;
|
||||
}
|
||||
|
||||
async function resolveAgentRunTraceCommandMapping({ traceId, mapped, options = {} }) {
|
||||
const safeId = safeTraceId(traceId);
|
||||
const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : null;
|
||||
@@ -1940,7 +1991,7 @@ async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body,
|
||||
return parsed?.ok === true && Object.hasOwn(parsed, "data") ? parsed.data : parsed;
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") {
|
||||
throw Object.assign(new Error(`AgentRun ${method} ${path} timed out after ${timeoutMs}ms`), { code: "agentrun_timeout", statusCode: 504 });
|
||||
throw Object.assign(new Error(`AgentRun ${method} ${path} timed out after ${timeoutMs}ms`), agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code: "agentrun_timeout", statusCode: 504, category: "upstream-timeout" }));
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -1948,6 +1999,27 @@ async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body,
|
||||
}
|
||||
}
|
||||
|
||||
function agentRunHttpErrorContext({ managerUrl, path, method, timeoutMs, code, statusCode, category }) {
|
||||
return {
|
||||
code,
|
||||
statusCode,
|
||||
layer: "agentrun",
|
||||
category,
|
||||
retryable: true,
|
||||
method,
|
||||
route: path,
|
||||
path,
|
||||
timeoutMs,
|
||||
timeoutStage: "agentrun-http",
|
||||
managerHost: safeUrlHost(managerUrl),
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function safeUrlHost(value) {
|
||||
try { return new URL(value).host; } catch { return null; }
|
||||
}
|
||||
|
||||
function resolveAgentRunManagerUrl(env = process.env, override = null) {
|
||||
const raw = firstNonEmpty(override, env.AGENTRUN_MGR_URL, env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL, DEFAULT_AGENTRUN_MGR_URL);
|
||||
const url = new URL(raw);
|
||||
|
||||
@@ -1105,7 +1105,13 @@ export async function handleCodeAgentChatResultHttp(request, response, url, opti
|
||||
label: "agentrun:result:poll-failed",
|
||||
errorCode: error?.code ?? "agentrun_result_poll_failed",
|
||||
message: error?.message ?? "AgentRun result polling failed",
|
||||
waitingFor: "agentrun-result"
|
||||
runId: result?.agentRun?.runId ?? null,
|
||||
commandId: result?.agentRun?.commandId ?? null,
|
||||
timeoutMs: numberOrNull(error?.timeoutMs),
|
||||
timeoutStage: textValue(error?.timeoutStage) || null,
|
||||
upstreamRoute: textValue(error?.route ?? error?.path) || null,
|
||||
waitingFor: "agentrun-result",
|
||||
valuesPrinted: false
|
||||
});
|
||||
if (result?.agentRun && result.status === "running") {
|
||||
sendJson(response, 202, {
|
||||
@@ -1117,7 +1123,7 @@ export async function handleCodeAgentChatResultHttp(request, response, url, opti
|
||||
runnerTrace: compactRunnerTraceForResult(traceStore.snapshot(traceId), resultTraceEventLimit(options)),
|
||||
waitingFor: "agentrun-result",
|
||||
degraded: true,
|
||||
error: { code: error?.code ?? "agentrun_result_poll_failed", message: error?.message ?? "AgentRun result polling failed" }
|
||||
error: codeAgentRefreshErrorPayload(error, traceId, result?.agentRun, "agentrun_result_poll_failed")
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1205,6 +1211,11 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
|
||||
label: "turn-status:result-sync-failed",
|
||||
errorCode: error?.code ?? "agentrun_result_poll_failed",
|
||||
message: error?.message ?? "AgentRun result polling failed",
|
||||
runId: result?.agentRun?.runId ?? null,
|
||||
commandId: result?.agentRun?.commandId ?? null,
|
||||
timeoutMs: numberOrNull(error?.timeoutMs),
|
||||
timeoutStage: textValue(error?.timeoutStage) || null,
|
||||
upstreamRoute: textValue(error?.route ?? error?.path) || null,
|
||||
waitingFor: "agentrun-result",
|
||||
valuesPrinted: false
|
||||
});
|
||||
@@ -1227,7 +1238,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) {
|
||||
const refreshedTrace = await refreshAgentRunTrace({ traceId, result: agentRunResult, options, traceStore });
|
||||
agentRunResult = options.codeAgentChatResults?.get?.(traceId) ?? agentRunResult;
|
||||
if (traceNeedsCommandResultSync(agentRunResult, refreshedTrace)) {
|
||||
const synced = await syncAgentRunChatResult({ traceId, currentResult: agentRunResult, options, traceStore, appendResultEvent: false, refreshEvents: false });
|
||||
const synced = await syncAgentRunChatResult({ traceId, currentResult: agentRunResult, options, traceStore, appendResultEvent: false, refreshEvents: false, forceResultSync: true });
|
||||
agentRunResult = synced.result ?? agentRunResult;
|
||||
}
|
||||
if (isTraceCommandTerminalStatus(agentRunResult?.status)) {
|
||||
@@ -1306,13 +1317,33 @@ function codeAgentTurnStatusPayload({ traceId, result, snapshot, resultPollError
|
||||
retention: snapshotObject?.retention ?? null,
|
||||
eventCount: numberOrNull(snapshotObject?.eventCount ?? runnerTrace?.eventCount ?? events.length),
|
||||
error: resultPollError || refreshError
|
||||
? { code: resultPollError?.code ?? refreshError?.code ?? "turn_status_degraded", message: resultPollError?.message ?? refreshError?.message ?? "Code Agent turn status refresh degraded", valuesPrinted: false }
|
||||
? codeAgentRefreshErrorPayload(resultPollError ?? refreshError, traceId, resultObject?.agentRun ?? snapshotObject?.agentRun, "turn_status_degraded")
|
||||
: resultObject?.error ?? snapshotObject?.error ?? null,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentRefreshErrorPayload(error, traceId, agentRun, fallbackCode) {
|
||||
return {
|
||||
code: error?.code ?? fallbackCode,
|
||||
layer: error?.layer ?? "agentrun",
|
||||
category: error?.category ?? (error?.code === "agentrun_timeout" ? "upstream-timeout" : "upstream-refresh-failed"),
|
||||
retryable: error?.retryable !== false,
|
||||
message: error?.message ?? "Code Agent turn status refresh degraded",
|
||||
traceId,
|
||||
runId: agentRun?.runId ?? error?.runId ?? null,
|
||||
commandId: agentRun?.commandId ?? error?.commandId ?? null,
|
||||
method: error?.method ?? null,
|
||||
route: error?.route ?? error?.path ?? null,
|
||||
timeoutMs: numberOrNull(error?.timeoutMs),
|
||||
timeoutStage: error?.timeoutStage ?? null,
|
||||
upstreamStatus: numberOrNull(error?.statusCode),
|
||||
managerHost: error?.managerHost ?? null,
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTurnStatus(...values) {
|
||||
for (const value of values) {
|
||||
const text = textValue(value).toLowerCase().replace(/_/gu, "-");
|
||||
@@ -2509,7 +2540,7 @@ export async function handleCodeAgentTraceHttp(request, response, url, options)
|
||||
const snapshot = await refreshAgentRunTrace({ traceId, result: agentRunResult, options, traceStore });
|
||||
agentRunResult = options.codeAgentChatResults?.get?.(traceId) ?? agentRunResult;
|
||||
if (traceNeedsCommandResultSync(agentRunResult, snapshot)) {
|
||||
const synced = await syncAgentRunChatResult({ traceId, currentResult: agentRunResult, options, traceStore, appendResultEvent: false, refreshEvents: false });
|
||||
const synced = await syncAgentRunChatResult({ traceId, currentResult: agentRunResult, options, traceStore, appendResultEvent: false, refreshEvents: false, forceResultSync: true });
|
||||
agentRunResult = synced.result ?? agentRunResult;
|
||||
}
|
||||
if (isTraceCommandTerminalStatus(agentRunResult?.status)) {
|
||||
|
||||
@@ -78,7 +78,8 @@ function traceStorageKey(message: ChatMessage): string {
|
||||
|
||||
<template>
|
||||
<section id="conversation-list" ref="panelRef" class="conversation-panel" :data-following="following ? 'true' : 'false'" @scroll="onPanelScroll">
|
||||
<article v-for="message in workbench.messages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
|
||||
<LoadingState v-if="workbench.conversationDetailLoading" class="conversation-detail-loading" label="加载中" />
|
||||
<article v-for="message in workbench.conversationDetailLoading ? [] : workbench.messages" :key="message.id" class="message-card" :data-role="message.role" :data-status="message.status">
|
||||
<header v-if="message.role !== 'user'">
|
||||
<strong>{{ message.title }}</strong>
|
||||
<div class="message-header-actions">
|
||||
@@ -91,7 +92,8 @@ function traceStorageKey(message: ChatMessage): string {
|
||||
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
|
||||
<MessageMarkdown v-if="showMessageText(message)" class="message-text" :source="message.text" />
|
||||
</article>
|
||||
<div v-if="workbench.messages.length === 0" class="conversation-empty-hint">发起对话,或从左侧选择 session。</div>
|
||||
<div v-if="!workbench.conversationDetailLoading && workbench.messages.length === 0 && workbench.error" class="conversation-empty-hint conversation-error-hint">加载失败:{{ workbench.error }}</div>
|
||||
<div v-else-if="!workbench.conversationDetailLoading && workbench.messages.length === 0" class="conversation-empty-hint">发起对话,或从左侧选择 session。</div>
|
||||
<div v-if="detailMessage" class="workbench-dialog-backdrop" role="presentation" @click.self="detailMessageId = null">
|
||||
<section id="message-detail-dialog" class="workbench-dialog wide" role="dialog" aria-modal="true" aria-labelledby="message-detail-title">
|
||||
<header>
|
||||
|
||||
@@ -95,7 +95,8 @@ export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: Non
|
||||
const mergeAuthoritativeEvents = nextAuthoritative && previousEvents.length > 0 && (next.hasMore === true || next.fullTraceLoaded !== true || previous.fullTraceLoaded !== true || nextEvents.length < previousEvents.length);
|
||||
const events = nextAuthoritative
|
||||
? mergeAuthoritativeEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents
|
||||
: keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
|
||||
: keepPreviousEvents && previousAuthoritative && next.eventsCompacted === true ? previousEvents
|
||||
: keepPreviousEvents ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents.length > previousEvents.length ? mergeTraceEvents(previousEvents, nextEvents) : nextEvents;
|
||||
const eventCount = keepPreviousEvents ? previous.eventCount ?? events.length : next.eventCount ?? previous.eventCount ?? events.length;
|
||||
return { ...previous, ...next, events, eventCount, updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString() };
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const loading = ref(false);
|
||||
const conversationsReady = ref(false);
|
||||
const switchingConversationId = ref<string | null>(null);
|
||||
const conversationDetailLoadingId = ref<string | null>(null);
|
||||
const chatPending = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const activityRef = ref({ lastActivityAt: Date.now(), lastActivityIso: new Date().toISOString(), waitingFor: "idle", lastEventLabel: null as string | null });
|
||||
@@ -49,6 +50,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const activeConversation = computed(() => visibleConversations.value.find((item) => item.conversationId === activeConversationId.value) ?? null);
|
||||
const sessionTabs = computed(() => sortSessionTabs(visibleConversations.value, activeConversationId.value, sessionStatusAuthority.value));
|
||||
const sessionListLoading = computed(() => shouldShowSessionListLoading({ loading: loading.value, conversationsReady: conversationsReady.value }));
|
||||
const conversationDetailLoading = computed(() => Boolean(conversationDetailLoadingId.value || switchingConversationId.value));
|
||||
const activeProjectId = computed(() => workspaceProjectId(workspace.value, projectId.value));
|
||||
const composer = computed(() => resolveComposerState({ workspace: workspace.value, messages: messages.value, conversations: visibleConversations.value, activeConversationId: activeConversationId.value, chatPending: chatPending.value, currentRequest: currentRequest.value, turnStatusAuthority: turnStatusAuthority.value }));
|
||||
|
||||
@@ -75,7 +77,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const routeConversationId = normalizeWorkbenchConversationId(options.conversationId);
|
||||
const selectedConversationId = routeConversationId ?? selectedConversationIdFromWorkspace(nextWorkspace);
|
||||
const conversationsPromise = api.workbench.conversations(nextProjectId, { includeConversationId: selectedConversationId });
|
||||
if (selectedConversationId) conversationDetailLoadingId.value = selectedConversationId;
|
||||
const selectedConversationResult = selectedConversationId ? await api.workbench.conversation(selectedConversationId, { projectId: nextProjectId }) : null;
|
||||
clearConversationDetailLoading(selectedConversationId);
|
||||
const selectedConversation = selectedConversationResult?.ok ? selectedConversationResult.data?.conversation ?? null : null;
|
||||
if (selectedConversationId && selectedConversationResult && !selectedConversationResult.ok) error.value = selectedConversationResult.error ?? "conversation unavailable";
|
||||
const workspaceSnapshot = selectedConversation
|
||||
@@ -148,6 +152,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const requestEpoch = beginWorkspaceSelection();
|
||||
const tabProjectId = conversation.projectId ?? activeProjectId.value;
|
||||
switchingConversationId.value = conversation.conversationId;
|
||||
conversationDetailLoadingId.value = conversation.conversationId;
|
||||
conversations.value = mergeConversationIntoList(conversations.value, conversation);
|
||||
conversationsReady.value = true;
|
||||
workspace.value = optimisticWorkspaceSelection(current, conversation, tabProjectId);
|
||||
@@ -157,6 +162,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const detailResponse = await api.workbench.conversation(conversation.conversationId, { projectId: tabProjectId });
|
||||
const response = await persistPromise;
|
||||
loading.value = false;
|
||||
clearConversationDetailLoading(conversation.conversationId);
|
||||
clearSwitchingConversation(conversation.conversationId);
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) return;
|
||||
const selectedConversation = detailResponse.ok ? detailResponse.data?.conversation ?? null : null;
|
||||
@@ -185,6 +191,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (activeConversationId.value === normalized && messages.value.length > 0) return true;
|
||||
const requestEpoch = beginWorkspaceSelection();
|
||||
switchingConversationId.value = normalized;
|
||||
conversationDetailLoadingId.value = normalized;
|
||||
const existing = visibleConversations.value.find((conversation) => conversation.conversationId === normalized) ?? null;
|
||||
if (existing && (existing.messages?.length ?? 0) > 0) {
|
||||
await selectConversation(existing);
|
||||
@@ -194,12 +201,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const conversation = response.data?.conversation ?? null;
|
||||
if (!response.ok || !conversation) {
|
||||
if (isCurrentWorkspaceSelection(requestEpoch, normalized)) clearSwitchingConversation(normalized);
|
||||
clearConversationDetailLoading(normalized);
|
||||
error.value = response.error ?? "session URL not found";
|
||||
return false;
|
||||
}
|
||||
const selectedProjectId = conversationProjectId(conversation, activeProjectId.value);
|
||||
applySelectedConversationDetail(conversation, current, selectedProjectId, null, messages.value);
|
||||
clearSwitchingConversation(normalized);
|
||||
clearConversationDetailLoading(normalized);
|
||||
error.value = null;
|
||||
void refreshSessionStatusById(sessionIdFromConversation(conversation));
|
||||
void persistSelectedConversation(conversation, selectedProjectId);
|
||||
@@ -684,6 +693,10 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (switchingConversationId.value === conversationId) switchingConversationId.value = null;
|
||||
}
|
||||
|
||||
function clearConversationDetailLoading(conversationId: string | null | undefined): void {
|
||||
if (!conversationId || conversationDetailLoadingId.value === conversationId) conversationDetailLoadingId.value = null;
|
||||
}
|
||||
|
||||
function applySelectedConversationDetail(conversation: ConversationRecord, baseWorkspace: WorkspaceRecord | null, selectedProjectId: string, previousConversationId: string | null, previousMessages: ChatMessage[]): void {
|
||||
workspace.value = workspaceWithSelectedConversation(baseWorkspace, conversation, selectedProjectId);
|
||||
messages.value = restoreMessagesTraceAuthority(messagesFromSelectedConversation(conversation, conversation.conversationId, previousConversationId, previousMessages), previousMessages);
|
||||
@@ -716,8 +729,12 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
}
|
||||
|
||||
async function retrySelectConversationWithFreshWorkspace(conversation: ConversationRecord, tabProjectId: string, requestEpoch: number): Promise<boolean> {
|
||||
conversationDetailLoadingId.value = conversation.conversationId;
|
||||
const fresh = await api.workbench.workspace(tabProjectId);
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) return true;
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) {
|
||||
clearConversationDetailLoading(conversation.conversationId);
|
||||
return true;
|
||||
}
|
||||
const freshWorkspace = fresh.data?.workspace;
|
||||
if (!fresh.ok || !freshWorkspace?.workspaceId) return false;
|
||||
workspace.value = optimisticWorkspaceSelection(freshWorkspace, conversation, tabProjectId);
|
||||
@@ -725,7 +742,11 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue-retry" }),
|
||||
api.workbench.conversation(conversation.conversationId, { projectId: tabProjectId })
|
||||
]);
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) return true;
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) {
|
||||
clearConversationDetailLoading(conversation.conversationId);
|
||||
return true;
|
||||
}
|
||||
clearConversationDetailLoading(conversation.conversationId);
|
||||
if (!retried.ok) {
|
||||
error.value = retried.error ?? "session switch failed";
|
||||
return false;
|
||||
@@ -755,7 +776,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (traceId) void validateAndReattachTrace(traceId);
|
||||
}
|
||||
|
||||
return { projectId, workspace, conversations, messages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, sessionListLoading, chatPending, error, activeConversationId, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectConversation, selectConversationById, deleteCurrentSession, refreshConversations, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearConversation, recordActivity };
|
||||
return { projectId, workspace, conversations, messages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, sessionListLoading, conversationDetailLoading, chatPending, error, activeConversationId, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectConversation, selectConversationById, deleteCurrentSession, refreshConversations, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearConversation, recordActivity };
|
||||
});
|
||||
|
||||
function sessionIdFromConversation(conversation: ConversationRecord | null | undefined): string | null {
|
||||
|
||||
@@ -714,6 +714,15 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.conversation-detail-loading {
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.conversation-error-hint {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user