App 聊天历史分页改走连拉循环

首屏与「显示更早」共用 readDirectHistoryPages,不再各自单发一页
首屏基线取空并在守卫后一次性并入聊天 reducer,失败仍按原有语义抛出
「显示更早」按当前视图条目判断新回合,取到多页一次性并入并推进锚点
连拉中途失败保留已取到的页,报错统一走同一个出口
This commit is contained in:
2026-09-17 16:39:24 +08:00
parent 1ae6c6cd06
commit 37ae7508b8
+60 -30
View File
@@ -232,17 +232,19 @@ import {
} from './features/project-workspace/chatComposerQueue';
import { DeveloperProjectPanels } from './features/project-workspace/DeveloperProjectPanels';
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
import { readDirectHistoryPages } from './features/project-workspace/directHistoryPaging';
import {
applyDirectThreadConsumeResult,
type DirectThreadChatState,
emptyDirectThreadChatState,
mergeDirectThreadHistorySlice,
mergeDirectHistoryItems,
resolveDirectThreadBootstrap,
selectDirectChatEntries,
} from './features/project-workspace/directThreadChat';
import {
type DirectThreadConsumeResult,
type DirectThreadHistorySlice,
type DirectThreadItem,
type DirectThreadSubscriptionBootstrap,
} from './features/project-workspace/directThreadEvents';
import type { DirectCodexUserContentPart } from './features/project-workspace/generated';
@@ -3491,21 +3493,38 @@ export function App({
let runtimeError = '';
let loadedDirectHistoryHasMore = false;
let loadedDirectHistoryFirstItemId: string | null = null;
// 首屏切片先暂存,待下方 staleness 守卫通过后再并入聊天 reducer:
// 首屏连拉到的条目先暂存,待下方 staleness 守卫通过后再并入聊天 reducer:
// 迟到的切片属于已经切走的项目,不能在守卫之前就写进全局聊天状态。
let loadedDirectHistorySlice: DirectThreadHistorySlice | null = null;
let loadedDirectHistoryItems: DirectThreadItem[] = [];
const projectConversation = directCodexProductRuntime
? (() => {
return invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
{
projectPath: nextProjectPath,
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
},
).then((slice) => {
loadedDirectHistoryHasMore = slice.hasMore;
loadedDirectHistoryFirstItemId = slice.firstItemId;
loadedDirectHistorySlice = slice;
// 首屏铺的就是这份视图的基线,"新回合"比较没有意义:判据退化为"这一页得有能渲染
// 的条目",所以基线取空,免得拿上一份对话的残留状态去比。首屏失败仍按原有语义
// 抛出,交给调用方的 catch 处理。
return readDirectHistoryPages({
existingEntries: [],
beforeItemId: null,
readSlice: (beforeItemId) =>
invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
beforeItemId
? {
projectPath: nextProjectPath,
beforeItemId,
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
}
: {
projectPath: nextProjectPath,
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
},
),
}).then((pages) => {
if (pages.error) {
throw pages.error;
}
loadedDirectHistoryHasMore = pages.hasMore;
loadedDirectHistoryFirstItemId = pages.firstItemId;
loadedDirectHistoryItems = pages.items;
return {
path: nextProjectPath,
agentId: null,
@@ -3579,12 +3598,10 @@ export function App({
if (directCodexProductRuntime) {
setDirectHistoryHasMore(loadedDirectHistoryHasMore);
directHistoryOldestItemIdRef.current = loadedDirectHistoryFirstItemId;
if (loadedDirectHistorySlice) {
const slice = loadedDirectHistorySlice;
if (loadedDirectHistoryItems.length > 0) {
const items = loadedDirectHistoryItems;
// 首屏历史进聊天 reducer`messages` 在 DirectProject 下只剩运行期本地消息。
setDirectThreadChat((state) =>
mergeDirectThreadHistorySlice(state, slice),
);
setDirectThreadChat((state) => mergeDirectHistoryItems(state, items));
}
}
setMessages((current) => {
@@ -11807,24 +11824,37 @@ export function App({
if (invoke && projectPath && !directHistoryLoadingRef.current) {
directHistoryLoadingRef.current = true;
try {
const slice = await invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
{
projectPath,
beforeItemId: directHistoryOldestItemIdRef.current,
limit: CONVERSATION_VISIBLE_STEP,
},
);
// 一次点击连拉,直到出现新回合或取不动为止:一屏全是工具卡片 / 思考文本时,
// 单发一页会让用户点了"显示更早"却看不到任何变化。
const pages = await readDirectHistoryPages({
existingEntries: selectDirectChatEntries(directThreadChat),
beforeItemId: directHistoryOldestItemIdRef.current,
readSlice: (beforeItemId) =>
invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
beforeItemId
? {
projectPath,
beforeItemId,
limit: CONVERSATION_VISIBLE_STEP,
}
: { projectPath, limit: CONVERSATION_VISIBLE_STEP },
),
});
if (localProjectPathRef.current !== projectPath) {
return;
}
// 更早的一屏历史进聊天 reducer:与运行态同形、同身份,重复读取只补不重。
// 连拉到的页一次性进聊天 reducer:与运行态同形、同身份,重复读取只补不重。
setDirectThreadChat((state) =>
mergeDirectThreadHistorySlice(state, slice),
mergeDirectHistoryItems(state, pages.items),
);
setDirectHistoryHasMore(slice.hasMore);
setDirectHistoryHasMore(pages.hasMore);
directHistoryOldestItemIdRef.current =
slice.firstItemId ?? directHistoryOldestItemIdRef.current;
pages.firstItemId ?? directHistoryOldestItemIdRef.current;
if (pages.error) {
// 已经取到的页照常进视图,失败只走下面同一个报错出口。
throw pages.error;
}
} catch (error) {
setWorkspaceStatus(
`读取更早的对话历史失败:${error instanceof Error ? error.message : String(error)}`,