mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-01-31 13:57:34 +00:00
fix: API would be stringified twice. That is now fixed.
This commit is contained in:
@@ -398,10 +398,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
// Send session-created event only once for new sessions
|
// Send session-created event only once for new sessions
|
||||||
if (!sessionId && !sessionCreatedSent) {
|
if (!sessionId && !sessionCreatedSent) {
|
||||||
sessionCreatedSent = true;
|
sessionCreatedSent = true;
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'session-created',
|
type: 'session-created',
|
||||||
sessionId: capturedSessionId
|
sessionId: capturedSessionId
|
||||||
}));
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
|
console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
|
||||||
}
|
}
|
||||||
@@ -411,20 +411,20 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
|
|
||||||
// Transform and send message to WebSocket
|
// Transform and send message to WebSocket
|
||||||
const transformedMessage = transformMessage(message);
|
const transformedMessage = transformMessage(message);
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'claude-response',
|
type: 'claude-response',
|
||||||
data: transformedMessage
|
data: transformedMessage
|
||||||
}));
|
});
|
||||||
|
|
||||||
// Extract and send token budget updates from result messages
|
// Extract and send token budget updates from result messages
|
||||||
if (message.type === 'result') {
|
if (message.type === 'result') {
|
||||||
const tokenBudget = extractTokenBudget(message);
|
const tokenBudget = extractTokenBudget(message);
|
||||||
if (tokenBudget) {
|
if (tokenBudget) {
|
||||||
console.log('Token budget from modelUsage:', tokenBudget);
|
console.log('Token budget from modelUsage:', tokenBudget);
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'token-budget',
|
type: 'token-budget',
|
||||||
data: tokenBudget
|
data: tokenBudget
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,12 +439,12 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
|
|
||||||
// Send completion event
|
// Send completion event
|
||||||
console.log('Streaming complete, sending claude-complete event');
|
console.log('Streaming complete, sending claude-complete event');
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'claude-complete',
|
type: 'claude-complete',
|
||||||
sessionId: capturedSessionId,
|
sessionId: capturedSessionId,
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
isNewSession: !sessionId && !!command
|
isNewSession: !sessionId && !!command
|
||||||
}));
|
});
|
||||||
console.log('claude-complete event sent');
|
console.log('claude-complete event sent');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -459,10 +459,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
await cleanupTempFiles(tempImagePaths, tempDir);
|
await cleanupTempFiles(tempImagePaths, tempDir);
|
||||||
|
|
||||||
// Send error to WebSocket
|
// Send error to WebSocket
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'claude-error',
|
type: 'claude-error',
|
||||||
error: error.message
|
error: error.message
|
||||||
}));
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,29 +102,29 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// Send session-created event only once for new sessions
|
// Send session-created event only once for new sessions
|
||||||
if (!sessionId && !sessionCreatedSent) {
|
if (!sessionId && !sessionCreatedSent) {
|
||||||
sessionCreatedSent = true;
|
sessionCreatedSent = true;
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'session-created',
|
type: 'session-created',
|
||||||
sessionId: capturedSessionId,
|
sessionId: capturedSessionId,
|
||||||
model: response.model,
|
model: response.model,
|
||||||
cwd: response.cwd
|
cwd: response.cwd
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send system info to frontend
|
// Send system info to frontend
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-system',
|
type: 'cursor-system',
|
||||||
data: response
|
data: response
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user':
|
case 'user':
|
||||||
// Forward user message
|
// Forward user message
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-user',
|
type: 'cursor-user',
|
||||||
data: response
|
data: response
|
||||||
}));
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'assistant':
|
case 'assistant':
|
||||||
@@ -134,7 +134,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
messageBuffer += textContent;
|
messageBuffer += textContent;
|
||||||
|
|
||||||
// Send as Claude-compatible format for frontend
|
// Send as Claude-compatible format for frontend
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'claude-response',
|
type: 'claude-response',
|
||||||
data: {
|
data: {
|
||||||
type: 'content_block_delta',
|
type: 'content_block_delta',
|
||||||
@@ -143,7 +143,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
text: textContent
|
text: textContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -153,37 +153,37 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
|
|
||||||
// Send final message if we have buffered content
|
// Send final message if we have buffered content
|
||||||
if (messageBuffer) {
|
if (messageBuffer) {
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'claude-response',
|
type: 'claude-response',
|
||||||
data: {
|
data: {
|
||||||
type: 'content_block_stop'
|
type: 'content_block_stop'
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send completion event
|
// Send completion event
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-result',
|
type: 'cursor-result',
|
||||||
sessionId: capturedSessionId || sessionId,
|
sessionId: capturedSessionId || sessionId,
|
||||||
data: response,
|
data: response,
|
||||||
success: response.subtype === 'success'
|
success: response.subtype === 'success'
|
||||||
}));
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Forward any other message types
|
// Forward any other message types
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-response',
|
type: 'cursor-response',
|
||||||
data: response
|
data: response
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.log('📄 Non-JSON response:', line);
|
console.log('📄 Non-JSON response:', line);
|
||||||
// If not JSON, send as raw text
|
// If not JSON, send as raw text
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-output',
|
type: 'cursor-output',
|
||||||
data: line
|
data: line
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,10 +191,10 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// Handle stderr
|
// Handle stderr
|
||||||
cursorProcess.stderr.on('data', (data) => {
|
cursorProcess.stderr.on('data', (data) => {
|
||||||
console.error('Cursor CLI stderr:', data.toString());
|
console.error('Cursor CLI stderr:', data.toString());
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-error',
|
type: 'cursor-error',
|
||||||
error: data.toString()
|
error: data.toString()
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle process completion
|
// Handle process completion
|
||||||
@@ -205,12 +205,12 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
const finalSessionId = capturedSessionId || sessionId || processKey;
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
||||||
activeCursorProcesses.delete(finalSessionId);
|
activeCursorProcesses.delete(finalSessionId);
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'claude-complete',
|
type: 'claude-complete',
|
||||||
sessionId: finalSessionId,
|
sessionId: finalSessionId,
|
||||||
exitCode: code,
|
exitCode: code,
|
||||||
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
|
isNewSession: !sessionId && !!command // Flag to indicate this was a new session
|
||||||
}));
|
});
|
||||||
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -226,12 +226,12 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
// Clean up process reference on error
|
// Clean up process reference on error
|
||||||
const finalSessionId = capturedSessionId || sessionId || processKey;
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
||||||
activeCursorProcesses.delete(finalSessionId);
|
activeCursorProcesses.delete(finalSessionId);
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
ws.send({
|
||||||
type: 'cursor-error',
|
type: 'cursor-error',
|
||||||
error: error.message
|
error: error.message
|
||||||
}));
|
});
|
||||||
|
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -717,6 +717,32 @@ wss.on('connection', (ws, request) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
|
||||||
|
*/
|
||||||
|
class WebSocketWriter {
|
||||||
|
constructor(ws) {
|
||||||
|
this.ws = ws;
|
||||||
|
this.sessionId = null;
|
||||||
|
this.isWebSocketWriter = true; // Marker for transport detection
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
if (this.ws.readyState === 1) { // WebSocket.OPEN
|
||||||
|
// Providers send raw objects, we stringify for WebSocket
|
||||||
|
this.ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionId(sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionId() {
|
||||||
|
return this.sessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle chat WebSocket connections
|
// Handle chat WebSocket connections
|
||||||
function handleChatConnection(ws) {
|
function handleChatConnection(ws) {
|
||||||
console.log('[INFO] Chat WebSocket connected');
|
console.log('[INFO] Chat WebSocket connected');
|
||||||
@@ -724,6 +750,9 @@ function handleChatConnection(ws) {
|
|||||||
// Add to connected clients for project updates
|
// Add to connected clients for project updates
|
||||||
connectedClients.add(ws);
|
connectedClients.add(ws);
|
||||||
|
|
||||||
|
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
||||||
|
const writer = new WebSocketWriter(ws);
|
||||||
|
|
||||||
ws.on('message', async (message) => {
|
ws.on('message', async (message) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(message);
|
const data = JSON.parse(message);
|
||||||
@@ -734,19 +763,19 @@ function handleChatConnection(ws) {
|
|||||||
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
||||||
|
|
||||||
// Use Claude Agents SDK
|
// Use Claude Agents SDK
|
||||||
await queryClaudeSDK(data.command, data.options, ws);
|
await queryClaudeSDK(data.command, data.options, writer);
|
||||||
} else if (data.type === 'cursor-command') {
|
} else if (data.type === 'cursor-command') {
|
||||||
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
|
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
|
||||||
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
||||||
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
||||||
console.log('🤖 Model:', data.options?.model || 'default');
|
console.log('🤖 Model:', data.options?.model || 'default');
|
||||||
await spawnCursor(data.command, data.options, ws);
|
await spawnCursor(data.command, data.options, writer);
|
||||||
} else if (data.type === 'codex-command') {
|
} else if (data.type === 'codex-command') {
|
||||||
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
||||||
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
||||||
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
||||||
console.log('🤖 Model:', data.options?.model || 'default');
|
console.log('🤖 Model:', data.options?.model || 'default');
|
||||||
await queryCodex(data.command, data.options, ws);
|
await queryCodex(data.command, data.options, writer);
|
||||||
} else if (data.type === 'cursor-resume') {
|
} else if (data.type === 'cursor-resume') {
|
||||||
// Backward compatibility: treat as cursor-command with resume and no prompt
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
||||||
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
||||||
@@ -754,7 +783,7 @@ function handleChatConnection(ws) {
|
|||||||
sessionId: data.sessionId,
|
sessionId: data.sessionId,
|
||||||
resume: true,
|
resume: true,
|
||||||
cwd: data.options?.cwd
|
cwd: data.options?.cwd
|
||||||
}, ws);
|
}, writer);
|
||||||
} else if (data.type === 'abort-session') {
|
} else if (data.type === 'abort-session') {
|
||||||
console.log('[DEBUG] Abort session request:', data.sessionId);
|
console.log('[DEBUG] Abort session request:', data.sessionId);
|
||||||
const provider = data.provider || 'claude';
|
const provider = data.provider || 'claude';
|
||||||
@@ -769,21 +798,21 @@ function handleChatConnection(ws) {
|
|||||||
success = await abortClaudeSDKSession(data.sessionId);
|
success = await abortClaudeSDKSession(data.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
writer.send({
|
||||||
type: 'session-aborted',
|
type: 'session-aborted',
|
||||||
sessionId: data.sessionId,
|
sessionId: data.sessionId,
|
||||||
provider,
|
provider,
|
||||||
success
|
success
|
||||||
}));
|
});
|
||||||
} else if (data.type === 'cursor-abort') {
|
} else if (data.type === 'cursor-abort') {
|
||||||
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
||||||
const success = abortCursorSession(data.sessionId);
|
const success = abortCursorSession(data.sessionId);
|
||||||
ws.send(JSON.stringify({
|
writer.send({
|
||||||
type: 'session-aborted',
|
type: 'session-aborted',
|
||||||
sessionId: data.sessionId,
|
sessionId: data.sessionId,
|
||||||
provider: 'cursor',
|
provider: 'cursor',
|
||||||
success
|
success
|
||||||
}));
|
});
|
||||||
} else if (data.type === 'check-session-status') {
|
} else if (data.type === 'check-session-status') {
|
||||||
// Check if a specific session is currently processing
|
// Check if a specific session is currently processing
|
||||||
const provider = data.provider || 'claude';
|
const provider = data.provider || 'claude';
|
||||||
@@ -799,12 +828,12 @@ function handleChatConnection(ws) {
|
|||||||
isActive = isClaudeSDKSessionActive(sessionId);
|
isActive = isClaudeSDKSessionActive(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
writer.send({
|
||||||
type: 'session-status',
|
type: 'session-status',
|
||||||
sessionId,
|
sessionId,
|
||||||
provider,
|
provider,
|
||||||
isProcessing: isActive
|
isProcessing: isActive
|
||||||
}));
|
});
|
||||||
} else if (data.type === 'get-active-sessions') {
|
} else if (data.type === 'get-active-sessions') {
|
||||||
// Get all currently active sessions
|
// Get all currently active sessions
|
||||||
const activeSessions = {
|
const activeSessions = {
|
||||||
@@ -812,17 +841,17 @@ function handleChatConnection(ws) {
|
|||||||
cursor: getActiveCursorSessions(),
|
cursor: getActiveCursorSessions(),
|
||||||
codex: getActiveCodexSessions()
|
codex: getActiveCodexSessions()
|
||||||
};
|
};
|
||||||
ws.send(JSON.stringify({
|
writer.send({
|
||||||
type: 'active-sessions',
|
type: 'active-sessions',
|
||||||
sessions: activeSessions
|
sessions: activeSessions
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ERROR] Chat WebSocket error:', error.message);
|
console.error('[ERROR] Chat WebSocket error:', error.message);
|
||||||
ws.send(JSON.stringify({
|
writer.send({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
error: error.message
|
error: error.message
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -360,12 +360,12 @@ export function getActiveCodexSessions() {
|
|||||||
*/
|
*/
|
||||||
function sendMessage(ws, data) {
|
function sendMessage(ws, data) {
|
||||||
try {
|
try {
|
||||||
if (typeof ws.send === 'function') {
|
if (ws.isSSEStreamWriter || ws.isWebSocketWriter) {
|
||||||
// WebSocket
|
// Writer handles stringification (SSEStreamWriter or WebSocketWriter)
|
||||||
|
ws.send(data);
|
||||||
|
} else if (typeof ws.send === 'function') {
|
||||||
|
// Raw WebSocket - stringify here
|
||||||
ws.send(JSON.stringify(data));
|
ws.send(JSON.stringify(data));
|
||||||
} else if (typeof ws.write === 'function') {
|
|
||||||
// SSE writer (for agent API)
|
|
||||||
ws.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Codex] Error sending message:', error);
|
console.error('[Codex] Error sending message:', error);
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ class SSEStreamWriter {
|
|||||||
constructor(res) {
|
constructor(res) {
|
||||||
this.res = res;
|
this.res = res;
|
||||||
this.sessionId = null;
|
this.sessionId = null;
|
||||||
|
this.isSSEStreamWriter = true; // Marker for transport detection
|
||||||
}
|
}
|
||||||
|
|
||||||
send(data) {
|
send(data) {
|
||||||
@@ -458,7 +459,7 @@ class SSEStreamWriter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format as SSE
|
// Format as SSE - providers send raw objects, we stringify
|
||||||
this.res.write(`data: ${JSON.stringify(data)}\n\n`);
|
this.res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user