diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index a5cc8fcf5..1072189ec 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -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(null); + const directHistoryLoadingRef = useRef(false); const [pendingCommand, setPendingCommand] = useState( 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( - directCodexProductRuntime - ? 'read_direct_project_conversation' - : 'read_local_conversation', - directCodexProductRuntime - ? { projectPath: nextProjectPath } - : { projectPath: nextProjectPath, agentId: null }, - ); + let loadedDirectHistoryHasMore = false; + const projectConversation = directCodexProductRuntime + ? (() => { + return invoke( + '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('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( + '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), ); diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/directThreadEvents.ts b/apps/ai-game-creator-shell/src/features/project-workspace/directThreadEvents.ts index 27430c970..fc7b8f5b4 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/directThreadEvents.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/directThreadEvents.ts @@ -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; + 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;