接入DirectProject历史懒加载

进入项目先读取有限历史切片而非完整对话\n按最早item ID向前翻页并保留hasMore状态\n复用同一历史item投影构造聊天记录
This commit is contained in:
2026-09-15 18:23:00 +08:00
committed by kdletters
parent 9cd43b2ce8
commit 2bcf210552
2 changed files with 115 additions and 11 deletions
+74 -11
View File
@@ -224,6 +224,8 @@ import { DeveloperProjectPanels } from './features/project-workspace/DeveloperPr
import { DeveloperRuntimePanels } from './features/project-workspace/DeveloperRuntimePanels';
import {
type DirectThreadConsumeResult,
directThreadHistoryItemsToMessages,
type DirectThreadHistorySlice,
type DirectThreadSubscriptionBootstrap,
emptyDirectThreadReducerState,
reduceDirectThreadEvents,
@@ -1487,6 +1489,9 @@ export function App({
const [conversationVisibleCount, setConversationVisibleCount] = useState(
CONVERSATION_INITIAL_VISIBLE_COUNT,
);
const [directHistoryHasMore, setDirectHistoryHasMore] = useState(false);
const directHistoryOldestItemIdRef = useRef<string | null>(null);
const directHistoryLoadingRef = useRef(false);
const [pendingCommand, setPendingCommand] = useState<PendingCommand | null>(
null,
);
@@ -2725,7 +2730,7 @@ export function App({
agentId: null,
...(message.messageId ? { messageId: message.messageId } : {}),
message: {
role: message.role,
role: message.role === 'user' ? 'user' : 'assistant',
content: message.text,
agentId: null,
...(typeof message.updatedAt === 'number'
@@ -3614,14 +3619,29 @@ export function App({
? null
: await readProjectSupervisorActiveSession(invoke, nextProjectPath);
let runtimeError = '';
const projectConversation = await invoke<LocalConversationResult>(
directCodexProductRuntime
? 'read_direct_project_conversation'
: 'read_local_conversation',
directCodexProductRuntime
? { projectPath: nextProjectPath }
: { projectPath: nextProjectPath, agentId: null },
);
let loadedDirectHistoryHasMore = false;
const projectConversation = directCodexProductRuntime
? (() => {
return invoke<DirectThreadHistorySlice>(
'read_direct_project_history_slice',
{
projectPath: nextProjectPath,
limit: CONVERSATION_INITIAL_VISIBLE_COUNT,
},
).then((slice) => {
loadedDirectHistoryHasMore = slice.hasMore;
return {
path: nextProjectPath,
agentId: null,
messages: directThreadHistoryItemsToMessages(slice.items),
} satisfies LocalConversationResult;
});
})()
: invoke<LocalConversationResult>('read_local_conversation', {
projectPath: nextProjectPath,
agentId: null,
});
const resolvedProjectConversation = await projectConversation;
let supervisorConversation: LocalConversationResult | null = null;
let runtime: AgentRuntimeState | null = null;
let runtimeResponseStream: AgentRuntimeResponseStream | null = null;
@@ -3658,7 +3678,7 @@ export function App({
return;
}
const conversationMessages = mergeProjectSupervisorConversation(
projectConversation.messages,
resolvedProjectConversation.messages,
supervisorConversation?.messages ?? [],
);
if (
@@ -3679,6 +3699,12 @@ export function App({
setProjectSupervisorResponseStream(null);
}
setProjectSupervisorRuntimeError(runtimeError || resumeError);
if (directCodexProductRuntime) {
setDirectHistoryHasMore(loadedDirectHistoryHasMore);
directHistoryOldestItemIdRef.current =
conversationMessages.find((message) => message.messageId)
?.messageId ?? null;
}
setMessages((current) => {
const nextConversationMessages = conversationMessages;
const hasOnlyDefaultGreeting =
@@ -11890,7 +11916,44 @@ export function App({
)
: null;
function showEarlierConversationMessages() {
async function showEarlierConversationMessages() {
if (directCodexProductRuntime && directHistoryHasMore) {
const invoke = resolveTauriInvoke();
const projectPath = localProject?.projectPath;
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 older = directThreadHistoryItemsToMessages(slice.items).map(
(message) => ({
role:
message.role === 'user'
? ('user' as const)
: ('assistant' as const),
text: message.content,
runtimeOwned: true,
messageId: message.messageId,
updatedAt: message.updatedAt,
}),
);
setMessages((current) => [...older, ...current]);
setDirectHistoryHasMore(slice.hasMore);
directHistoryOldestItemIdRef.current =
older.find((message) => message.messageId)?.messageId ??
directHistoryOldestItemIdRef.current;
} finally {
directHistoryLoadingRef.current = false;
}
}
return;
}
setConversationVisibleCount((current) =>
Math.min(messages.length, current + CONVERSATION_VISIBLE_STEP),
);
@@ -16,6 +16,47 @@ export type DirectThreadConsumeResult = {
events: DirectThreadRawEvent[];
};
export type DirectThreadHistorySlice = {
items: unknown[];
hasMore: boolean;
};
import type { LocalConversationMessageRecord } from '../../app/types';
export function directThreadHistoryItemsToMessages(
items: unknown[],
): LocalConversationMessageRecord[] {
return items.flatMap((raw) => {
if (!raw || typeof raw !== 'object') return [];
const item = raw as Record<string, unknown>;
const role = item.role;
if (role !== 'user' && role !== 'assistant') return [];
const messageRole = role as 'user' | 'assistant';
const content = Array.isArray(item.content)
? item.content
.map((part) =>
part && typeof part === 'object' && 'text' in part
? (part as { text?: unknown }).text
: null,
)
.filter((text): text is string => typeof text === 'string')
.join('')
: '';
if (!content) return [];
const messageId = typeof item.id === 'string' ? item.id : undefined;
return [
{
schemaVersion: 'agc-direct-project-context.v1',
role: messageRole,
content,
agentId: null,
messageId,
updatedAt: 0,
},
];
});
}
export type DirectThreadReducerState = {
lastSeq: number;
turnId: string | null;