fix: unify session tab status authority (#1336)
This commit is contained in:
@@ -694,6 +694,17 @@ function firstNonEmptyValue(...values) {
|
||||
function submitCodeAgentChatTurn({ params, options, traceId }) {
|
||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
|
||||
const acceptedPayload = {
|
||||
accepted: true,
|
||||
status: "running",
|
||||
traceId,
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: safeSessionId(params.sessionId) || null,
|
||||
threadId: safeOpaqueId(params.threadId) || null,
|
||||
projectId: params.projectId ?? null,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
void recordCodeAgentSessionOwner({ payload: acceptedPayload, params, options, status: "running" });
|
||||
if (codeAgentAgentRunAdapterEnabled(options.env ?? process.env)) {
|
||||
const initial = withCodeAgentBillingReservation(initialAgentRunChatResult({ params, options, traceId }), params);
|
||||
results.set(traceId, annotateOwner(initial, params));
|
||||
@@ -744,14 +755,7 @@ function submitCodeAgentChatTurn({ params, options, traceId }) {
|
||||
setImmediate(() => { run(); });
|
||||
return;
|
||||
}
|
||||
results.set(traceId, {
|
||||
accepted: true,
|
||||
status: "running",
|
||||
traceId,
|
||||
conversationId: safeConversationId(params.conversationId) || null,
|
||||
sessionId: safeSessionId(params.sessionId) || null,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
results.set(traceId, acceptedPayload);
|
||||
traceStore.ensure(traceId, {
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
workspace: options.workspace ?? options.env?.HWLAB_CODE_AGENT_CODEX_WORKSPACE ?? options.env?.HWLAB_CODE_AGENT_WORKSPACE ?? null,
|
||||
|
||||
@@ -146,12 +146,12 @@ test("R2 running session tabs expose animation state without rewriting label", (
|
||||
agentMessage({ conversationId: "cnv_running", status: "running", updatedAt: "2026-01-04T00:01:00.000Z" })
|
||||
]
|
||||
}
|
||||
], null);
|
||||
], null, { ses_running: { sessionId: "ses_running", status: "running" } });
|
||||
assert.equal(tabs[0]?.label, "保持原始标题");
|
||||
assert.equal(tabs[0]?.running, true);
|
||||
});
|
||||
|
||||
test("R2 selected workspace stubs cannot clear a running conversation tab", () => {
|
||||
test("R2 session tabs do not infer running state from workspace stubs", () => {
|
||||
const conversations: ConversationRecord[] = [{
|
||||
conversationId: "cnv_r2",
|
||||
sessionId: "ses_r2",
|
||||
@@ -166,13 +166,16 @@ test("R2 selected workspace stubs cannot clear a running conversation tab", () =
|
||||
const workspace = workspaceRecord({ activeTraceId: null, sessionStatus: "idle" });
|
||||
const merged = mergeSelectedConversation(conversations, workspace);
|
||||
const tabs = sortSessionTabs(merged, "cnv_r2");
|
||||
assert.equal(tabs[0]?.status, "running");
|
||||
assert.equal(tabs[0]?.running, true);
|
||||
assert.equal(tabs[0]?.status, "unknown");
|
||||
assert.equal(tabs[0]?.running, false);
|
||||
assert.equal(tabs[0]?.messageCount, 2);
|
||||
assert.equal(tabs[0]?.label, "保持原始标题");
|
||||
const authorityTabs = sortSessionTabs(merged, "cnv_r2", { ses_r2: { sessionId: "ses_r2", status: "running" } });
|
||||
assert.equal(authorityTabs[0]?.status, "running");
|
||||
assert.equal(authorityTabs[0]?.running, true);
|
||||
});
|
||||
|
||||
test("R2 active in-flight turn updates the selected session tab before list refresh catches up", () => {
|
||||
test("R2 active in-flight turn updates tab content but not status without session authority", () => {
|
||||
const messages: ChatMessage[] = [
|
||||
userMessage({ conversationId: "cnv_live", sessionId: "ses_live", text: "立即显示左侧运行态", createdAt: "2026-01-04T00:00:00.000Z" }),
|
||||
agentMessage({ conversationId: "cnv_live", sessionId: "ses_live", status: "running", traceId: "trc_live", updatedAt: "2026-01-04T00:00:01.000Z" })
|
||||
@@ -184,13 +187,16 @@ test("R2 active in-flight turn updates the selected session tab before list refr
|
||||
chatPending: true
|
||||
});
|
||||
const tabs = sortSessionTabs(merged, "cnv_live");
|
||||
assert.equal(tabs[0]?.status, "running");
|
||||
assert.equal(tabs[0]?.running, true);
|
||||
assert.equal(tabs[0]?.status, "unknown");
|
||||
assert.equal(tabs[0]?.running, false);
|
||||
assert.equal(tabs[0]?.label, "立即显示左侧运行态");
|
||||
assert.equal(tabs[0]?.messageCount, 2);
|
||||
const authorityTabs = sortSessionTabs(merged, "cnv_live", { ses_live: { sessionId: "ses_live", status: "running" } });
|
||||
assert.equal(authorityTabs[0]?.status, "running");
|
||||
assert.equal(authorityTabs[0]?.running, true);
|
||||
});
|
||||
|
||||
test("R2 session tabs keep restored running message ahead of stale idle conversation status", () => {
|
||||
test("R2 session tabs keep trace metadata but status comes only from session authority", () => {
|
||||
const conversation: ConversationRecord = {
|
||||
conversationId: "cnv_stale_snapshot",
|
||||
sessionId: "ses_stale_snapshot",
|
||||
@@ -200,8 +206,12 @@ test("R2 session tabs keep restored running message ahead of stale idle conversa
|
||||
};
|
||||
assert.equal(resolveConversationSessionStatus(conversation), "running");
|
||||
const tabs = sortSessionTabs([conversation], "cnv_stale_snapshot");
|
||||
assert.equal(tabs[0]?.running, true);
|
||||
assert.equal(tabs[0]?.status, "unknown");
|
||||
assert.equal(tabs[0]?.running, false);
|
||||
assert.equal(tabs[0]?.lastTraceId, "trc_stale");
|
||||
const authorityTabs = sortSessionTabs([conversation], "cnv_stale_snapshot", { ses_stale_snapshot: { sessionId: "ses_stale_snapshot", status: "idle" } });
|
||||
assert.equal(authorityTabs[0]?.status, "idle");
|
||||
assert.equal(authorityTabs[0]?.running, false);
|
||||
});
|
||||
|
||||
test("R2 agent message titles drop transient running wording", () => {
|
||||
|
||||
@@ -5,5 +5,6 @@ export const agentAPI = {
|
||||
sendAgentMessage: (payload: Record<string, unknown>, timeoutMs: number, activityRef?: ApiRequestOptions["activityRef"]): Promise<ApiResult<AgentChatResponse>> => fetchJson("/v1/agent/chat", { method: "POST", body: JSON.stringify(payload), timeoutMs, timeoutName: "Code Agent", activityRef }),
|
||||
steerAgentMessage: (payload: Record<string, unknown>, timeoutMs: number, activityRef?: ApiRequestOptions["activityRef"]): Promise<ApiResult<AgentChatResponse>> => fetchJson("/v1/agent/chat/steer", { method: "POST", body: JSON.stringify(payload), timeoutMs, timeoutName: "Code Agent steer", activityRef }),
|
||||
getAgentChatResult: (resultUrl: string, timeoutMs = 8000, activityRef?: ApiRequestOptions["activityRef"]): Promise<ApiResult<AgentChatResultResponse>> => fetchJson(resultUrl, { timeoutMs, timeoutName: "Code Agent result", activityRef }),
|
||||
getAgentSession: (sessionId: string): Promise<ApiResult<{ session?: Record<string, unknown> }>> => fetchJson(`/v1/agent/sessions/${encodeURIComponent(sessionId)}`, { timeoutMs: 8000, timeoutName: "Code Agent session" }),
|
||||
cancelAgentMessage: (payload: Record<string, unknown>): Promise<ApiResult<{ ok?: boolean; canceled?: boolean; error?: { code?: string; message?: string; userMessage?: string } }>> => fetchJson("/v1/agent/chat/cancel", { method: "POST", body: JSON.stringify(payload), timeoutMs: 30000, timeoutName: "Code Agent cancel" })
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@ function formatSessionUpdatedTime(value: string | null | undefined): string {
|
||||
<div class="session-list" id="session-tabs" :data-loading="showSessionListLoading">
|
||||
<LoadingState v-if="showSessionListLoading" class="session-list-loading" label="加载中" />
|
||||
<template v-else>
|
||||
<button v-for="tab in workbench.sessionTabs" :key="tab.conversationId" class="session-tab" :data-active="tab.active" :data-running="tab.running" type="button" :title="tab.label || tab.conversationId" @click="workbench.selectConversation(tab)">
|
||||
<button v-for="tab in workbench.sessionTabs" :key="tab.conversationId" class="session-tab" :data-active="tab.active" :data-running="tab.running" :data-status="tab.status || undefined" :data-session-id="tab.sessionId || undefined" :data-conversation-id="tab.conversationId" type="button" :title="tab.label || tab.conversationId" @click="workbench.selectConversation(tab)">
|
||||
<span class="session-tab-title">{{ tab.label || tab.conversationId }}</span>
|
||||
<time class="session-tab-time" :datetime="tab.updatedAt || tab.startedAt || undefined">{{ formatSessionUpdatedTime(tab.updatedAt || tab.startedAt) }}</time>
|
||||
</button>
|
||||
|
||||
@@ -20,6 +20,16 @@ export interface SessionTab extends ConversationRecord {
|
||||
running: boolean;
|
||||
}
|
||||
|
||||
export interface SessionStatusAuthority {
|
||||
sessionId: string;
|
||||
status: string | null;
|
||||
updatedAt?: string | null;
|
||||
lastTraceId?: string | null;
|
||||
loadedAt?: string | null;
|
||||
}
|
||||
|
||||
export type SessionStatusAuthorityMap = Readonly<Record<string, SessionStatusAuthority | undefined>>;
|
||||
|
||||
export interface DraftEntry {
|
||||
text: string;
|
||||
ts: string;
|
||||
@@ -143,11 +153,11 @@ export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | nul
|
||||
};
|
||||
}
|
||||
|
||||
export function conversationToSessionTab(conversation: ConversationRecord, activeConversationId: string | null): SessionTab {
|
||||
export function conversationToSessionTab(conversation: ConversationRecord, activeConversationId: string | null, sessionStatusAuthority: SessionStatusAuthorityMap = {}): SessionTab {
|
||||
const sessionId = firstNonEmptyString(conversation.sessionId, conversation.session?.sessionId);
|
||||
const conversationId = conversation.conversationId;
|
||||
const updatedAt = conversationDisplayUpdatedAt(conversation);
|
||||
const status = resolveConversationSessionStatus(conversation);
|
||||
const status = normalizeSessionStatus(sessionId ? sessionStatusAuthority[sessionId]?.status : null) ?? "unknown";
|
||||
const trace = firstNonEmptyString(latestAgentMessage(conversation.messages)?.traceId, conversation.messages?.at(-1)?.traceId, conversation.lastTraceId);
|
||||
const userMessage = conversation.messages?.find((message) => message.role === "user");
|
||||
const running = isActiveStatus(status);
|
||||
@@ -164,7 +174,7 @@ export function conversationToSessionTab(conversation: ConversationRecord, activ
|
||||
...conversation,
|
||||
key: sessionId ?? conversationId,
|
||||
label: preview ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`,
|
||||
subtitle: `${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"} · ${conversation.messageCount ?? conversation.messages?.length ?? 0} 条`,
|
||||
subtitle: `${running ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : status === "unknown" ? "状态同步中" : "未开始"} · ${conversation.messageCount ?? conversation.messages?.length ?? 0} 条`,
|
||||
sessionId,
|
||||
threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId),
|
||||
lastTraceId: trace,
|
||||
@@ -225,8 +235,8 @@ export function normalizeWorkbenchMessageTitle(role: ChatMessage["role"], title:
|
||||
return text || fallback;
|
||||
}
|
||||
|
||||
export function sortSessionTabs(conversations: ConversationRecord[], activeConversationId: string | null): SessionTab[] {
|
||||
return conversations.map((conversation) => conversationToSessionTab(conversation, activeConversationId)).sort((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt));
|
||||
export function sortSessionTabs(conversations: ConversationRecord[], activeConversationId: string | null, sessionStatusAuthority: SessionStatusAuthorityMap = {}): SessionTab[] {
|
||||
return conversations.map((conversation) => conversationToSessionTab(conversation, activeConversationId, sessionStatusAuthority)).sort((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt));
|
||||
}
|
||||
|
||||
export function mergeActiveConversationActivity(conversations: ConversationRecord[], input: ActiveConversationActivityInput): ConversationRecord[] {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { api } from "@/api";
|
||||
import { mergeRunnerTrace, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "@/composables/useTraceSubscription";
|
||||
import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, TraceEvent, WorkspaceRecord } from "@/types";
|
||||
import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, normalizeWorkbenchConversationId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, conversationDisplayUpdatedAt, defaultProviderProfileOptions, latestUserMessageAtFromMessages, mergeActiveConversationActivity, mergeSelectedConversation, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, selectedConversationIdFromWorkspace, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption } from "./workbench-session";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, conversationDisplayUpdatedAt, defaultProviderProfileOptions, latestUserMessageAtFromMessages, mergeSelectedConversation, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, selectedConversationIdFromWorkspace, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption, type SessionStatusAuthority } from "./workbench-session";
|
||||
|
||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
||||
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
||||
@@ -29,13 +29,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const activeAbort = ref<AbortController | null>(null);
|
||||
const currentRequest = ref<{ traceId: string; conversationId: string | null; sessionId: string | null; threadId: string | null; status?: string | null } | null>(null);
|
||||
const workspaceSelectionEpoch = ref(0);
|
||||
const sessionStatusAuthority = ref<Record<string, SessionStatusAuthority>>({});
|
||||
|
||||
const visibleConversations = computed(() => mergeSelectedConversation(conversations.value, workspace.value));
|
||||
const activeConversationId = computed(() => firstNonEmptyString(switchingConversationId.value, workspace.value?.selectedConversationId, workspace.value?.workspace?.selectedConversationId, messages.value.find((message) => message.conversationId)?.conversationId));
|
||||
const selectedSessionId = computed(() => firstNonEmptyString(workspace.value?.selectedAgentSessionId, workspace.value?.workspace?.selectedAgentSessionId, activeConversation.value?.sessionId));
|
||||
const selectedThreadId = computed(() => firstNonEmptyString(workspace.value?.workspace?.threadId, activeConversation.value?.threadId));
|
||||
const activeConversation = computed(() => visibleConversations.value.find((item) => item.conversationId === activeConversationId.value) ?? null);
|
||||
const sessionTabs = computed(() => sortSessionTabs(mergeActiveConversationActivity(visibleConversations.value, { activeConversationId: activeConversationId.value, messages: messages.value, currentRequest: currentRequest.value, chatPending: chatPending.value }), activeConversationId.value));
|
||||
const sessionTabs = computed(() => sortSessionTabs(visibleConversations.value, activeConversationId.value, sessionStatusAuthority.value));
|
||||
const sessionListLoading = computed(() => shouldShowSessionListLoading({ loading: loading.value, conversationsReady: conversationsReady.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 }));
|
||||
@@ -60,6 +61,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const nextWorkspace = workspaceResult.data?.workspace ?? null;
|
||||
const nextConversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : [];
|
||||
conversations.value = nextConversations;
|
||||
void refreshSessionStatusAuthority(mergeSelectedConversation(nextConversations, nextWorkspace));
|
||||
if (shouldApplyWorkspaceSnapshot({ requestEpoch, currentEpoch: workspaceSelectionEpoch.value, currentConversationId: activeConversationId.value, workspace: nextWorkspace })) {
|
||||
workspace.value = nextWorkspace;
|
||||
providerProfile.value = firstNonEmptyString(workspace.value?.providerProfile, workspace.value?.workspace?.providerProfile, providerProfile.value) ?? providerProfile.value;
|
||||
@@ -95,6 +97,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
workspace.value = response.data?.workspace ?? current;
|
||||
messages.value = messagesFromWorkspace(workspace.value);
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus();
|
||||
await refreshConversations();
|
||||
}
|
||||
}
|
||||
@@ -107,6 +110,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
switchingConversationId.value = conversation.conversationId;
|
||||
workspace.value = optimisticWorkspaceSelection(current, conversation, tabProjectId);
|
||||
messages.value = messagesFromConversation(conversation);
|
||||
void refreshSessionStatusById(sessionIdFromConversation(conversation));
|
||||
loading.value = true;
|
||||
const response = await api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" });
|
||||
loading.value = false;
|
||||
@@ -118,6 +122,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
void hydrateTerminalMessageDiagnostics();
|
||||
reattachRestoredActiveTrace();
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus(conversation);
|
||||
return;
|
||||
}
|
||||
if (response.status === 404 && await retrySelectConversationWithFreshWorkspace(conversation, tabProjectId, requestEpoch)) return;
|
||||
@@ -148,6 +153,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return false;
|
||||
}
|
||||
conversations.value = [conversation, ...conversations.value.filter((item) => item.conversationId !== normalized)];
|
||||
void refreshSessionStatusById(sessionIdFromConversation(conversation));
|
||||
await selectConversation(conversation);
|
||||
return activeConversationId.value === normalized;
|
||||
}
|
||||
@@ -167,7 +173,48 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
async function refreshConversations(): Promise<void> {
|
||||
const response = await api.workbench.conversations(activeProjectId.value);
|
||||
conversationsReady.value = true;
|
||||
if (response.ok) conversations.value = response.data?.conversations ?? [];
|
||||
if (response.ok) {
|
||||
conversations.value = response.data?.conversations ?? [];
|
||||
await refreshSessionStatusAuthority(visibleConversations.value);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSessionStatusAuthority(source: ConversationRecord[] = visibleConversations.value): Promise<void> {
|
||||
await Promise.all(uniqueSessionIds(source).map((sessionId) => refreshSessionStatusById(sessionId)));
|
||||
}
|
||||
|
||||
async function refreshSelectedSessionStatus(fallbackConversation?: ConversationRecord | null): Promise<void> {
|
||||
await refreshSessionStatusById(firstNonEmptyString(
|
||||
fallbackConversation ? sessionIdFromConversation(fallbackConversation) : null,
|
||||
selectedSessionId.value,
|
||||
workspace.value?.selectedAgentSessionId,
|
||||
workspace.value?.workspace?.selectedAgentSessionId
|
||||
));
|
||||
}
|
||||
|
||||
async function refreshSessionStatusById(sessionId: string | null | undefined): Promise<void> {
|
||||
const id = firstNonEmptyString(sessionId);
|
||||
if (!id) return;
|
||||
const response = await api.agent.getAgentSession(id);
|
||||
const session = response.data?.session && typeof response.data.session === "object" ? response.data.session : null;
|
||||
if (!response.ok || !session) {
|
||||
const next = { ...sessionStatusAuthority.value };
|
||||
delete next[id];
|
||||
sessionStatusAuthority.value = next;
|
||||
return;
|
||||
}
|
||||
const returnedSessionId = firstNonEmptyString(session.sessionId, id);
|
||||
if (returnedSessionId !== id) return;
|
||||
sessionStatusAuthority.value = {
|
||||
...sessionStatusAuthority.value,
|
||||
[id]: {
|
||||
sessionId: id,
|
||||
status: firstNonEmptyString(session.status) ?? null,
|
||||
updatedAt: firstNonEmptyString(session.updatedAt) ?? null,
|
||||
lastTraceId: firstNonEmptyString(session.lastTraceId) ?? null,
|
||||
loadedAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function bumpConversationActivity(input: { conversationId?: string | null; sessionId?: string | null; threadId?: string | null; traceId?: string | null; status?: string | null; updatedAt?: string | null }): void {
|
||||
@@ -228,6 +275,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const pending = makeMessage("agent", "", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent", retryInput: value });
|
||||
messages.value.push(user, pending);
|
||||
bumpConversationActivity({ conversationId, sessionId, threadId, traceId, status: "running" });
|
||||
void refreshSessionStatusById(sessionId);
|
||||
chatPending.value = true;
|
||||
currentRequest.value = { traceId, conversationId, sessionId, threadId, status: "running" };
|
||||
rememberRecentDraft(value);
|
||||
@@ -240,8 +288,10 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
if (steerMode && response.status === 404) void clearActiveTrace(traceId, "steer-trace-not-found");
|
||||
chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void refreshSessionStatusById(sessionId);
|
||||
return;
|
||||
}
|
||||
void refreshSessionStatusById(sessionId);
|
||||
subscribe(traceId, response.data);
|
||||
}
|
||||
|
||||
@@ -254,6 +304,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
void clearActiveTrace(traceId, "cancel-agent-message", canceledStatus);
|
||||
if (message.status === "running") chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void refreshSessionStatusById(message.sessionId ?? selectedSessionId.value);
|
||||
}
|
||||
|
||||
async function cancelRunningTrace(): Promise<void> {
|
||||
@@ -373,6 +424,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const trace = snapshotToRunnerTrace(snapshot);
|
||||
messages.value = messages.value.map((message) => message.traceId === traceId ? { ...message, runnerTrace: mergeRunnerTrace(message.runnerTrace, trace), updatedAt: new Date().toISOString() } : message);
|
||||
bumpConversationActivity({ traceId, sessionId: trace.sessionId, threadId: trace.threadId, status: firstNonEmptyString(trace.status), updatedAt: firstNonEmptyString(trace.updatedAt) });
|
||||
void refreshSessionStatusById(trace.sessionId ?? selectedSessionId.value);
|
||||
}
|
||||
|
||||
function completeTrace(traceId: string, result: AgentChatResultResponse): void {
|
||||
@@ -389,6 +441,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void clearActiveTrace(traceId, "trace-terminal", workspaceSessionStatusFromChatStatus(terminalStatus));
|
||||
void refreshSessionStatusById(result.sessionId ?? selectedSessionId.value);
|
||||
}
|
||||
|
||||
async function hydrateTerminalMessageDiagnostics(): Promise<void> {
|
||||
@@ -412,6 +465,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return { ...message, status, title: normalizeWorkbenchMessageTitle(message.role, message.title), runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
|
||||
});
|
||||
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: statusFromResult(result.status), updatedAt: resultActivityUpdatedAt(result) });
|
||||
void refreshSessionStatusById(result.sessionId ?? selectedSessionId.value);
|
||||
}
|
||||
|
||||
function failTrace(traceId: string, message: string): void {
|
||||
@@ -419,6 +473,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void clearActiveTrace(traceId, "trace-infrastructure-error", "failed");
|
||||
void refreshSelectedSessionStatus();
|
||||
}
|
||||
|
||||
function markMessage(traceId: string, patch: Partial<ChatMessage>): void {
|
||||
@@ -484,6 +539,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
void hydrateTerminalMessageDiagnostics();
|
||||
reattachRestoredActiveTrace();
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus(conversation);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -495,6 +551,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
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, replayAgentTrace, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearConversation, recordActivity };
|
||||
});
|
||||
|
||||
function sessionIdFromConversation(conversation: ConversationRecord | null | undefined): string | null {
|
||||
return firstNonEmptyString(conversation?.sessionId, conversation?.session?.sessionId) ?? null;
|
||||
}
|
||||
|
||||
function uniqueSessionIds(conversations: ConversationRecord[]): string[] {
|
||||
return [...new Set(conversations.map((conversation) => sessionIdFromConversation(conversation)).filter((sessionId): sessionId is string => Boolean(sessionId)))];
|
||||
}
|
||||
|
||||
function optimisticWorkspaceSelection(current: WorkspaceRecord, conversation: ConversationRecord, projectId: string): WorkspaceRecord {
|
||||
return {
|
||||
...current,
|
||||
|
||||
Reference in New Issue
Block a user