From 515f1852136555996f5f4c24a2e6a5c7229ea347 Mon Sep 17 00:00:00 2001 From: lyon Date: Tue, 16 Jun 2026 08:01:33 +0800 Subject: [PATCH] fix: show session loading state before conversations load --- .../workbench-r2-session-parity.test.ts | 8 ++- .../src/components/common/LoadingState.vue | 52 +++++++++++++++++-- .../src/components/workbench/SessionRail.vue | 25 +++++---- .../src/stores/workbench-session.ts | 4 ++ web/hwlab-cloud-web/src/stores/workbench.ts | 9 +++- web/hwlab-cloud-web/src/styles/workbench.css | 10 ++++ 6 files changed, 92 insertions(+), 16 deletions(-) diff --git a/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts b/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts index 58203d45..9014f4d8 100644 --- a/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import type { ChatMessage, ConversationRecord, WorkspaceRecord } from "../src/types/index.ts"; -import { defaultProviderProfileOptions, mergeSelectedConversation, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, shouldApplyWorkspaceSnapshot, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts"; +import { defaultProviderProfileOptions, mergeSelectedConversation, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts"; test("R2 composer ignores stale workspace activeTraceId without verified active message", () => { const workspace = workspaceRecord({ activeTraceId: "trc_stale", sessionStatus: "running" }); @@ -45,6 +45,12 @@ test("R2 workspace snapshots cannot override a newer explicit session selection" }), true); }); +test("R2 session list shows loading until conversations are ready", () => { + assert.equal(shouldShowSessionListLoading({ loading: true, conversationsReady: false }), true); + assert.equal(shouldShowSessionListLoading({ loading: true, conversationsReady: true }), false); + assert.equal(shouldShowSessionListLoading({ loading: false, conversationsReady: false }), false); +}); + test("R2 drafts keep recent unique values and normalize corrupt storage", () => { const next = recordRecentDraft([{ text: "旧输入", ts: "2026-01-01T00:00:00.000Z" }], "新输入", "2026-01-02T00:00:00.000Z"); assert.deepEqual(next.map((item) => item.text), ["新输入", "旧输入"]); diff --git a/web/hwlab-cloud-web/src/components/common/LoadingState.vue b/web/hwlab-cloud-web/src/components/common/LoadingState.vue index 97452fef..09f36bb2 100644 --- a/web/hwlab-cloud-web/src/components/common/LoadingState.vue +++ b/web/hwlab-cloud-web/src/components/common/LoadingState.vue @@ -1,6 +1,52 @@ + + + + diff --git a/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue b/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue index 0df01f51..26a814da 100644 --- a/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue +++ b/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue @@ -2,6 +2,7 @@ import { computed, ref } from "vue"; import { useWorkbenchStore } from "@/stores/workbench"; import { useClipboard } from "@/composables/useClipboard"; +import LoadingState from "@/components/common/LoadingState.vue"; import { DEFAULT_WORKBENCH_PROJECT_ID, normalizeWorkbenchProjectId, workbenchSessionPath } from "@/utils"; const workbench = useWorkbenchStore(); @@ -9,6 +10,7 @@ const { copied, copy } = useClipboard(); const width = ref(readWidth()); const dragging = ref(false); const activeTab = computed(() => workbench.sessionTabs.find((tab) => tab.active) ?? null); +const showSessionListLoading = computed(() => workbench.sessionListLoading); function copyActive(): void { const tab = activeTab.value; @@ -82,21 +84,24 @@ function formatSessionUpdatedTime(value: string | null | undefined): string {
模型通道:{{ workbench.providerProfile }}
-
- -

没有 session。发送前必须显式新建或选择 session。

+
+ +
- - + +
{ const gatewayShellTimeoutMs = ref(readNumber("hwlab.workbench.gatewayShellTimeoutMs.v1", DEFAULT_GATEWAY_TIMEOUT_MS)); const live = ref(null); const loading = ref(false); + const conversationsReady = ref(false); const switchingConversationId = ref(null); const chatPending = ref(false); const error = ref(null); @@ -35,6 +36,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { 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(visibleConversations.value, activeConversationId.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 })); @@ -45,10 +47,12 @@ export const useWorkbenchStore = defineStore("workbench", () => { async function hydrate(): Promise { const requestEpoch = workspaceSelectionEpoch.value; + conversationsReady.value = conversations.value.length > 0; loading.value = true; error.value = null; const [workspaceResult, conversationsResult] = await Promise.all([api.workbench.workspace(projectId.value), api.workbench.conversations(projectId.value)]); loading.value = false; + conversationsReady.value = true; if (!workspaceResult.ok) { error.value = workspaceResult.error ?? "workspace unavailable"; return; @@ -158,6 +162,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { async function refreshConversations(): Promise { const response = await api.workbench.conversations(activeProjectId.value); + conversationsReady.value = true; if (response.ok) conversations.value = response.data?.conversations ?? []; } @@ -468,7 +473,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { return true; } - return { projectId, workspace, conversations, messages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, 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 }; + 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 optimisticWorkspaceSelection(current: WorkspaceRecord, conversation: ConversationRecord, projectId: string): WorkspaceRecord { diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css index b2184184..b308af13 100644 --- a/web/hwlab-cloud-web/src/styles/workbench.css +++ b/web/hwlab-cloud-web/src/styles/workbench.css @@ -594,6 +594,16 @@ overflow: auto; } +.session-list[data-loading="true"] { + align-content: center; + justify-items: center; + overflow: hidden; +} + +.session-list-loading { + min-height: 120px; +} + .conversation-panel { display: flex; min-height: 0;