From 6fe6eed331b1a38b55c573ab7209937d602739cd Mon Sep 17 00:00:00 2001 From: lyon Date: Wed, 17 Jun 2026 16:42:47 +0800 Subject: [PATCH] fix: speed up workbench session creation --- web/hwlab-cloud-web/src/stores/workbench.ts | 122 +++++++++++++++++--- 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index f0eef238..928f9e94 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -23,6 +23,11 @@ interface HydrateOptions { conversationId?: string | null; } +interface SessionCreatePersistenceResult { + ok: boolean; + error: string | null; +} + export const useWorkbenchStore = defineStore("workbench", () => { const projectId = ref(resolveInitialWorkbenchProjectId()); const workspace = ref(window.HWLAB_CLOUD_WEB_WORKSPACE_BOOTSTRAP?.workspace ?? null); @@ -50,6 +55,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { const traceAuthorityById = computed(() => selectTraceAuthorityById(serverState.value)); const traceHydrationInFlight = new Set(); const terminalRealtimeRefreshInFlight = new Set(); + const pendingSessionCreates = new Map>(); let realtimeStream: WorkbenchEventStream | null = null; let realtimeKey = ""; let realtimeGapTimer: number | null = null; @@ -154,24 +160,24 @@ export const useWorkbenchStore = defineStore("workbench", () => { const current = await ensureWorkspace(); if (!current) return; const requestEpoch = beginWorkspaceSelection(); - loading.value = true; - const response = await api.workbench.selectConversation(current.workspaceId, { projectId: activeProjectId.value, create: true, updatedByClient: "cloud-web-vue" }); + const selectedProjectId = activeProjectId.value; + const optimisticConversation = makeOptimisticConversation(selectedProjectId); + setActiveConversationSelection(optimisticConversation.conversationId); + conversations.value = mergeConversationIntoList(conversations.value, optimisticConversation); + rememberConversationDetail(optimisticConversation); + conversationsReady.value = true; + workspace.value = workspaceWithSelectedConversation(current, optimisticConversation, selectedProjectId); + rememberWorkspaceSnapshot(selectedProjectId, workspace.value); + messages.value = []; + currentRequest.value = null; loading.value = false; - if (response.ok && isCurrentWorkspaceSelection(requestEpoch)) { - workspace.value = response.data?.workspace ?? current; - rememberWorkspaceSnapshot(activeProjectId.value, workspace.value); - const selectedConversationId = selectedConversationIdFromWorkspace(workspace.value); - const selectedConversation = workspace.value?.selectedConversation ?? null; - setActiveConversationSelection(selectedConversationId); - rememberConversationDetail(selectedConversation); - messages.value = restoreMessagesTraceAuthority(messagesFromSelectedConversation(selectedConversation, selectedConversationId, null, []), messages.value); - currentRequest.value = null; - void hydrateTurnStatusAuthority(messages.value); - void hydrateTraceEvents(messages.value); - await refreshSelectedSessionStatus(); - await refreshConversations(selectedConversationIdFromWorkspace(workspace.value)); - restartRealtime("create-session"); - } + error.value = null; + restartRealtime("create-session-optimistic"); + const persistence = persistOptimisticSession(optimisticConversation, selectedProjectId, current.workspaceId, requestEpoch); + pendingSessionCreates.set(optimisticConversation.conversationId, persistence); + void persistence.then((result) => { + if (result.ok && pendingSessionCreates.get(optimisticConversation.conversationId) === persistence) pendingSessionCreates.delete(optimisticConversation.conversationId); + }); } async function selectConversation(conversation: ConversationRecord): Promise { @@ -295,6 +301,54 @@ export const useWorkbenchStore = defineStore("workbench", () => { reduceServerState({ type: "conversation.list", conversations: source }); } + async function persistOptimisticSession(conversation: ConversationRecord, selectedProjectId: string, workspaceId: string, requestEpoch: number): Promise { + try { + const response = await api.workbench.selectConversation(workspaceId, { + projectId: selectedProjectId, + create: true, + conversationId: conversation.conversationId, + sessionId: conversation.sessionId, + threadId: conversation.threadId, + updatedByClient: "cloud-web-vue-fast-create" + }); + if (!response.ok || !response.data?.workspace) { + if (activeConversationId.value === conversation.conversationId) error.value = response.error ?? "session create failed"; + return { ok: false, error: response.error ?? "session create failed" }; + } + const persistedWorkspace = response.data.workspace; + const persistedConversation = persistedWorkspace.selectedConversation?.conversationId === conversation.conversationId + ? persistedWorkspace.selectedConversation + : conversation; + rememberConversationDetail(persistedConversation); + conversations.value = mergeConversationIntoList(conversations.value, persistedConversation); + if (activeConversationId.value === conversation.conversationId) { + const persistedProjectId = conversationProjectId(persistedConversation, selectedProjectId); + workspace.value = workspaceWithSelectedConversation(persistedWorkspace, persistedConversation, persistedProjectId); + rememberWorkspaceSnapshot(persistedProjectId, workspace.value); + error.value = null; + if (messages.value.length === 0) { + messages.value = restoreMessagesTraceAuthority(messagesFromSelectedConversation(persistedConversation, conversation.conversationId, null, []), messages.value); + } + void hydrateTurnStatusAuthority(messages.value); + void hydrateTraceEvents(messages.value); + void refreshSelectedSessionStatus(persistedConversation); + void refreshConversations(conversation.conversationId); + restartRealtime(requestEpoch === workspaceSelectionEpoch.value ? "create-session" : "create-session-persisted"); + } + return { ok: true, error: null }; + } catch (cause) { + const message = cause instanceof Error ? cause.message : String(cause); + if (activeConversationId.value === conversation.conversationId) error.value = message; + return { ok: false, error: message }; + } + } + + async function waitForPendingSessionCreate(conversationId: string): Promise { + const pending = pendingSessionCreates.get(conversationId); + if (!pending) return { ok: true, error: null }; + return pending; + } + function setActiveConversationSelection(conversationId: string | null | undefined): void { explicitConversationId.value = nextExplicitWorkbenchConversationId(explicitConversationId.value, conversationId); } @@ -424,6 +478,13 @@ export const useWorkbenchStore = defineStore("workbench", () => { currentRequest.value = { traceId, conversationId, sessionId, threadId, status: "running" }; rememberRecentDraft(value); recordActivity("submit"); + const sessionCreate = await waitForPendingSessionCreate(conversationId); + if (!sessionCreate.ok) { + markMessage(traceId, { status: "failed", text: `新会话尚未准备好,消息未提交:${sessionCreate.error ?? "session create failed"}` }); + chatPending.value = false; + currentRequest.value = null; + return; + } const payload = { projectId: activeProjectId.value, message: value, prompt: value, conversationId, sessionId, threadId, traceId, providerProfile: providerProfile.value, gatewayShellTimeoutMs: gatewayShellTimeoutMs.value, workspaceId: workspace.value?.workspaceId, workspaceRevision: workspace.value?.revision, targetTraceId: composer.value.targetTraceId, steerTraceId }; const route = steerMode ? api.agent.steerAgentMessage : api.agent.sendAgentMessage; const response = await route(payload, codeAgentTimeoutMs.value, () => activityRef.value); @@ -1030,6 +1091,33 @@ function sessionIdFromConversation(conversation: ConversationRecord | null | und return firstNonEmptyString(conversation?.sessionId, conversation?.session?.sessionId) ?? null; } +function makeOptimisticConversation(projectId: string, now = new Date()): ConversationRecord { + const token = nextProtocolId("cnv").slice(4); + const title = `新会话 ${shortSessionTimestamp(now)}`; + const createdAt = now.toISOString(); + const sessionId = `ses_${token}`; + const threadId = `thr_${token}`; + return { + conversationId: `cnv_${token}`, + projectId, + sessionId, + threadId, + status: "active", + title, + name: title, + startedAt: createdAt, + updatedAt: createdAt, + messageCount: 0, + session: { sessionId, threadId, status: "active" }, + messages: [] + }; +} + +function shortSessionTimestamp(value: Date): string { + const pad = (number: number): string => String(number).padStart(2, "0"); + return `${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`; +} + function uniqueSessionIds(conversations: ConversationRecord[]): string[] { return [...new Set(conversations.map((conversation) => sessionIdFromConversation(conversation)).filter((sessionId): sessionId is string => Boolean(sessionId)))]; }