Merge pull request #1391 from pikasTech/codex/1389-fast-new-session

加速 Workbench 新建会话切换
This commit is contained in:
Lyon
2026-06-17 16:44:00 +08:00
committed by GitHub
+105 -17
View File
@@ -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<WorkspaceRecord | null>(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<string>();
const terminalRealtimeRefreshInFlight = new Set<string>();
const pendingSessionCreates = new Map<string, Promise<SessionCreatePersistenceResult>>();
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<void> {
@@ -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<SessionCreatePersistenceResult> {
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<SessionCreatePersistenceResult> {
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)))];
}