fix(workbench): 增量显示会话加载进度

This commit is contained in:
root
2026-07-21 10:15:12 +02:00
parent 38990c4e81
commit 01f627ba93
5 changed files with 216 additions and 8 deletions
@@ -0,0 +1,98 @@
// @vitest-environment jsdom
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ConversationPanel from "./ConversationPanel.vue";
const store = vi.hoisted(() => ({
activeSessionId: "ses_loading_progress",
activeMessages: [] as Array<Record<string, unknown>>,
error: null as string | null,
sessionDetailLoading: false,
sessionDetailLoadingProgress: {
active: false,
blocking: false,
phase: "idle",
label: ""
},
cancelAgentMessage: vi.fn(),
retryAgentMessage: vi.fn()
}));
vi.mock("@/stores/workbench", () => ({ useWorkbenchStore: () => store }));
vi.mock("@/composables/useWorkbenchScrollRuntime", () => ({
useWorkbenchBottomFollowScroll: () => ({
following: { value: true },
keepBottomAfterUpdate: vi.fn(),
onScroll: vi.fn(),
scrollToBottom: vi.fn()
})
}));
vi.mock("@/utils/workbench-performance", () => ({ acknowledgeWorkbenchVisibleAfterPaint: vi.fn() }));
function mountPanel() {
return mount(ConversationPanel, {
global: {
stubs: {
LoadingState: { props: ["label"], template: "<div class='loading-state'>{{ label }}</div>" },
WorkbenchMessageCard: { props: ["message"], template: "<article class='message-card'>{{ message.text }}</article>" },
ApiErrorDiagnostic: { template: "<div />" },
CodeAgentStatusSummary: { template: "<div />" },
MessageActions: { template: "<div />" },
MessageTraceDebugPanel: { template: "<div />" }
}
}
});
}
describe("ConversationPanel progressive session loading", () => {
beforeEach(() => {
store.activeMessages = [];
store.error = null;
store.sessionDetailLoading = false;
store.sessionDetailLoadingProgress = { active: false, blocking: false, phase: "idle", label: "" };
});
it("renders replayed messages while the SSE replay continues at the panel edge", () => {
store.activeMessages = [{
id: "msg_loading_progress",
messageId: "msg_loading_progress",
role: "user",
title: "用户",
text: "已经到达的回放消息",
status: "sent",
sessionId: store.activeSessionId,
traceId: "trc_loading_progress",
createdAt: "2026-07-21T12:00:00.000Z"
}];
store.sessionDetailLoadingProgress = {
active: true,
blocking: false,
phase: "replaying",
label: "实时流已连接,已显示 1 条,继续回放..."
};
const wrapper = mountPanel();
expect(wrapper.get(".message-card").text()).toContain("已经到达的回放消息");
expect(wrapper.get(".conversation-load-progress").text()).toContain("实时流已连接,已显示 1 条,继续回放...");
expect(wrapper.find(".conversation-detail-loading").exists()).toBe(false);
wrapper.unmount();
});
it("keeps the centered phase label before the first replay message", () => {
store.sessionDetailLoading = true;
store.sessionDetailLoadingProgress = {
active: true,
blocking: true,
phase: "selecting",
label: "正在选择会话并建立实时连接..."
};
const wrapper = mountPanel();
expect(wrapper.get(".conversation-detail-loading").text()).toContain("正在选择会话并建立实时连接...");
expect(wrapper.find(".conversation-empty-hint").exists()).toBe(false);
expect(wrapper.find(".conversation-load-progress").exists()).toBe(false);
wrapper.unmount();
});
});
@@ -24,7 +24,7 @@ const panelRef = ref<HTMLElement | null>(null);
const scrollSessionKey = computed(() => workbench.activeSessionId ?? "workbench");
const { following, keepBottomAfterUpdate, onScroll: onPanelScroll, scrollToBottom } = useWorkbenchBottomFollowScroll(panelRef, { threshold: 56, sessionKey: scrollSessionKey, tabKey: "conversation" });
const detailMessageId = ref<string | null>(null);
const visibleMessages = computed(() => workbench.sessionDetailLoading ? [] : workbench.activeMessages);
const visibleMessages = computed(() => workbench.activeMessages);
const visibleTimelineRows = computed(() => buildWorkbenchTimelineRows(visibleMessages.value));
const detailMessage = computed(() => workbench.activeMessages.find((message) => message.id === detailMessageId.value) ?? null);
const scrollSignature = computed(() => workbenchTimelineSignature(visibleTimelineRows.value));
@@ -64,7 +64,10 @@ function acknowledgeVisibleMessages(): void {
<template>
<section id="conversation-list" ref="panelRef" class="conversation-panel" :data-following="following ? 'true' : 'false'" @scroll="onPanelScroll">
<LoadingState v-if="workbench.sessionDetailLoading" class="conversation-detail-loading" label="加载中" />
<LoadingState v-if="workbench.sessionDetailLoadingProgress.blocking" class="conversation-detail-loading" :label="workbench.sessionDetailLoadingProgress.label" />
<div v-else-if="workbench.sessionDetailLoadingProgress.active" class="conversation-load-progress" :data-phase="workbench.sessionDetailLoadingProgress.phase">
<LoadingState :label="workbench.sessionDetailLoadingProgress.label" compact />
</div>
<template v-for="row in visibleTimelineRows" :key="row.key">
<div v-if="row.type === 'TurnGap'" class="timeline-row timeline-turn-gap" aria-hidden="true" />
<div v-else-if="row.type === 'Thinking'" class="timeline-row timeline-thinking" :data-session-id="row.sessionId || undefined" :data-trace-id="row.traceId || undefined">
@@ -74,8 +77,8 @@ function acknowledgeVisibleMessages(): void {
<div v-else-if="row.type === 'Error'" class="timeline-row timeline-error" :data-session-id="row.sessionId || undefined" :data-trace-id="row.traceId || undefined">{{ row.text }}</div>
<WorkbenchMessageCard v-else-if="row.message" :message="row.message" @details="openDetails" />
</template>
<div v-if="!workbench.sessionDetailLoading && workbench.activeMessages.length === 0 && workbench.error" class="conversation-empty-hint conversation-error-hint">加载失败{{ workbench.error }}</div>
<div v-else-if="!workbench.sessionDetailLoading && workbench.activeMessages.length === 0" class="conversation-empty-hint">发起对话或从左侧选择 session</div>
<div v-if="!workbench.sessionDetailLoadingProgress.blocking && workbench.activeMessages.length === 0 && workbench.error" class="conversation-empty-hint conversation-error-hint">加载失败{{ workbench.error }}</div>
<div v-else-if="!workbench.sessionDetailLoadingProgress.active && workbench.activeMessages.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>
@@ -98,3 +101,17 @@ function acknowledgeVisibleMessages(): void {
</div>
</section>
</template>
<style scoped>
.conversation-load-progress {
position: sticky;
z-index: 2;
top: 0;
align-self: flex-end;
border: 1px solid #bae6fd;
border-radius: 3px;
background: #f8fafc;
padding: 5px 8px;
box-shadow: 0 1px 2px rgb(15 23 42 / 8%);
}
</style>