mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-20 21:37:25 +00:00
- Add provider adapter layer (server/providers/) with registry pattern
- Claude, Cursor, Codex, Gemini adapters normalize native formats to NormalizedMessage
- Shared types.js defines ProviderAdapter interface and message kinds
- Registry enables polymorphic provider lookup
- Add unified REST endpoint: GET /api/sessions/:id/messages?provider=...
- Replaces four provider-specific message endpoints with one
- Delegates to provider adapters via registry
- Add frontend session-keyed store (useSessionStore)
- Per-session Map with serverMessages/realtimeMessages/merged
- Dedup by ID, stale threshold for re-fetch, background session accumulation
- No localStorage for messages — backend JSONL is source of truth
- Add normalizedToChatMessages converter (useChatMessages)
- Converts NormalizedMessage[] to existing ChatMessage[] UI format
- Wire unified store into ChatInterface, useChatSessionState, useChatRealtimeHandlers
- Session switch uses store cache for instant render
- Background WebSocket messages routed to correct session slot
249 lines
7.8 KiB
JavaScript
249 lines
7.8 KiB
JavaScript
/**
|
|
* Codex (OpenAI) provider adapter.
|
|
*
|
|
* Normalizes Codex SDK session history into NormalizedMessage format.
|
|
* @module adapters/codex
|
|
*/
|
|
|
|
import { getCodexSessionMessages } from '../../projects.js';
|
|
import { createNormalizedMessage, generateMessageId } from '../types.js';
|
|
|
|
const PROVIDER = 'codex';
|
|
|
|
/**
|
|
* Normalize a raw Codex JSONL message into NormalizedMessage(s).
|
|
* @param {object} raw - A single parsed message from Codex JSONL
|
|
* @param {string} sessionId
|
|
* @returns {import('../types.js').NormalizedMessage[]}
|
|
*/
|
|
function normalizeCodexHistoryEntry(raw, sessionId) {
|
|
const ts = raw.timestamp || new Date().toISOString();
|
|
const baseId = raw.uuid || generateMessageId('codex');
|
|
|
|
// User message
|
|
if (raw.message?.role === 'user') {
|
|
const content = typeof raw.message.content === 'string'
|
|
? raw.message.content
|
|
: Array.isArray(raw.message.content)
|
|
? raw.message.content.map(p => typeof p === 'string' ? p : p?.text || '').filter(Boolean).join('\n')
|
|
: String(raw.message.content || '');
|
|
if (!content.trim()) return [];
|
|
return [createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'user',
|
|
content,
|
|
})];
|
|
}
|
|
|
|
// Assistant message
|
|
if (raw.message?.role === 'assistant') {
|
|
const content = typeof raw.message.content === 'string'
|
|
? raw.message.content
|
|
: Array.isArray(raw.message.content)
|
|
? raw.message.content.map(p => typeof p === 'string' ? p : p?.text || '').filter(Boolean).join('\n')
|
|
: '';
|
|
if (!content.trim()) return [];
|
|
return [createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'assistant',
|
|
content,
|
|
})];
|
|
}
|
|
|
|
// Thinking/reasoning
|
|
if (raw.type === 'thinking' || raw.isReasoning) {
|
|
return [createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'thinking',
|
|
content: raw.message?.content || '',
|
|
})];
|
|
}
|
|
|
|
// Tool use
|
|
if (raw.type === 'tool_use' || raw.toolName) {
|
|
return [createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'tool_use',
|
|
toolName: raw.toolName || 'Unknown',
|
|
toolInput: raw.toolInput,
|
|
toolId: raw.toolCallId || baseId,
|
|
})];
|
|
}
|
|
|
|
// Tool result
|
|
if (raw.type === 'tool_result') {
|
|
return [createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'tool_result',
|
|
toolId: raw.toolCallId || '',
|
|
content: raw.output || '',
|
|
isError: Boolean(raw.isError),
|
|
})];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Normalize a raw Codex event (history JSONL or transformed SDK event) into NormalizedMessage(s).
|
|
* @param {object} raw - A history entry (has raw.message.role) or transformed SDK event (has raw.type)
|
|
* @param {string} sessionId
|
|
* @returns {import('../types.js').NormalizedMessage[]}
|
|
*/
|
|
export function normalizeMessage(raw, sessionId) {
|
|
// History format: has message.role
|
|
if (raw.message?.role) {
|
|
return normalizeCodexHistoryEntry(raw, sessionId);
|
|
}
|
|
|
|
const ts = raw.timestamp || new Date().toISOString();
|
|
const baseId = raw.uuid || generateMessageId('codex');
|
|
|
|
// SDK event format (output of transformCodexEvent)
|
|
if (raw.type === 'item') {
|
|
switch (raw.itemType) {
|
|
case 'agent_message':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'text', role: 'assistant', content: raw.message?.content || '',
|
|
})];
|
|
case 'reasoning':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'thinking', content: raw.message?.content || '',
|
|
})];
|
|
case 'command_execution':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'tool_use', toolName: 'Bash', toolInput: { command: raw.command },
|
|
toolId: baseId,
|
|
output: raw.output, exitCode: raw.exitCode, status: raw.status,
|
|
})];
|
|
case 'file_change':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'tool_use', toolName: 'FileChanges', toolInput: raw.changes,
|
|
toolId: baseId, status: raw.status,
|
|
})];
|
|
case 'mcp_tool_call':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'tool_use', toolName: raw.tool || 'MCP', toolInput: raw.arguments,
|
|
toolId: baseId, server: raw.server, result: raw.result,
|
|
error: raw.error, status: raw.status,
|
|
})];
|
|
case 'web_search':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'tool_use', toolName: 'WebSearch', toolInput: { query: raw.query },
|
|
toolId: baseId,
|
|
})];
|
|
case 'todo_list':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'tool_use', toolName: 'TodoList', toolInput: { items: raw.items },
|
|
toolId: baseId,
|
|
})];
|
|
case 'error':
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'error', content: raw.message?.content || 'Unknown error',
|
|
})];
|
|
default:
|
|
// Unknown item type — pass through as generic tool_use
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'tool_use', toolName: raw.itemType || 'Unknown',
|
|
toolInput: raw.item || raw, toolId: baseId,
|
|
})];
|
|
}
|
|
}
|
|
|
|
if (raw.type === 'turn_complete') {
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'complete',
|
|
})];
|
|
}
|
|
if (raw.type === 'turn_failed') {
|
|
return [createNormalizedMessage({
|
|
id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
|
|
kind: 'error', content: raw.error?.message || 'Turn failed',
|
|
})];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @type {import('../types.js').ProviderAdapter}
|
|
*/
|
|
export const codexAdapter = {
|
|
normalizeMessage,
|
|
/**
|
|
* Fetch session history from Codex JSONL files.
|
|
*/
|
|
async fetchHistory(sessionId, opts = {}) {
|
|
const { limit = null, offset = 0 } = opts;
|
|
|
|
let result;
|
|
try {
|
|
result = await getCodexSessionMessages(sessionId, limit, offset);
|
|
} catch (error) {
|
|
console.warn(`[CodexAdapter] Failed to load session ${sessionId}:`, error.message);
|
|
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
}
|
|
|
|
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
|
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
|
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
|
const tokenUsage = result.tokenUsage || null;
|
|
|
|
const normalized = [];
|
|
for (const raw of rawMessages) {
|
|
const entries = normalizeCodexHistoryEntry(raw, sessionId);
|
|
normalized.push(...entries);
|
|
}
|
|
|
|
// Attach tool results to tool_use messages
|
|
const toolResultMap = new Map();
|
|
for (const msg of normalized) {
|
|
if (msg.kind === 'tool_result' && msg.toolId) {
|
|
toolResultMap.set(msg.toolId, msg);
|
|
}
|
|
}
|
|
for (const msg of normalized) {
|
|
if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
|
|
const tr = toolResultMap.get(msg.toolId);
|
|
msg.toolResult = { content: tr.content, isError: tr.isError };
|
|
}
|
|
}
|
|
|
|
return {
|
|
messages: normalized,
|
|
total,
|
|
hasMore,
|
|
offset,
|
|
limit,
|
|
tokenUsage,
|
|
};
|
|
},
|
|
};
|