Spaces:
Running
Running
| import type { WebSocketMessage } from "./websocket"; | |
| import { chatStore } from "../stores/chat-store"; | |
| import { contentManager } from "./content-manager"; | |
| import type { MessageSegment } from "../models/chat-data"; | |
| export class MessageHandler { | |
| private currentMessageId: string | null = null; | |
| private currentSegments: Map<string, MessageSegment> = new Map(); | |
| private completedSegments: MessageSegment[] = []; | |
| private latestTodoSegmentId: string | null = null; | |
| private updateTimer: ReturnType<typeof setTimeout> | null = null; | |
| handleMessage(message: WebSocketMessage): void { | |
| switch (message.type) { | |
| case "status": | |
| this.handleStatus(message); | |
| break; | |
| case "stream_start": | |
| this.handleStreamStart(message); | |
| break; | |
| case "stream_token": | |
| this.handleStreamToken(message); | |
| break; | |
| case "stream_end": | |
| this.handleStreamEnd(); | |
| break; | |
| case "chat": | |
| this.handleChat(message); | |
| break; | |
| case "error": | |
| this.handleError(message); | |
| break; | |
| case "editor_update": | |
| this.handleEditorUpdate(message); | |
| break; | |
| case "segment_start": | |
| this.handleSegmentStart(message); | |
| break; | |
| case "segment_token": | |
| this.handleSegmentToken(message); | |
| break; | |
| case "segment_end": | |
| this.handleSegmentEnd(message); | |
| break; | |
| case "tool_start": | |
| case "tool_end": | |
| break; | |
| } | |
| } | |
| private handleStatus(message: WebSocketMessage): void { | |
| const { processing, connected } = message.payload; | |
| if (processing !== undefined) { | |
| chatStore.setProcessing(processing as boolean); | |
| } | |
| if (connected !== undefined) { | |
| chatStore.setConnected(connected as boolean); | |
| } | |
| } | |
| private handleStreamStart(message: WebSocketMessage): void { | |
| const messageId = | |
| (message.payload.messageId as string) || `assistant_${Date.now()}`; | |
| this.currentMessageId = messageId; | |
| this.currentSegments.clear(); | |
| this.completedSegments = []; | |
| this.latestTodoSegmentId = null; | |
| chatStore.addMessage({ | |
| id: messageId, | |
| role: "assistant", | |
| content: "", | |
| timestamp: Date.now(), | |
| segments: [], | |
| }); | |
| } | |
| private handleStreamToken(message: WebSocketMessage): void { | |
| const token = (message.payload.token as string) || ""; | |
| if (this.currentMessageId && token) { | |
| chatStore.appendToLastMessage(token); | |
| } | |
| } | |
| private handleStreamEnd(): void { | |
| if (this.currentMessageId) { | |
| this.updateMessageSegments(); | |
| } | |
| this.currentMessageId = null; | |
| this.currentSegments.clear(); | |
| this.completedSegments = []; | |
| } | |
| private handleChat(message: WebSocketMessage): void { | |
| const { content } = message.payload; | |
| if (content) { | |
| const messageId = `assistant_${Date.now()}`; | |
| chatStore.addMessage({ | |
| id: messageId, | |
| role: "assistant", | |
| content: content as string, | |
| timestamp: Date.now(), | |
| }); | |
| } | |
| } | |
| private handleError(message: WebSocketMessage): void { | |
| chatStore.setError((message.payload.error as string) || null); | |
| } | |
| private handleEditorUpdate(message: WebSocketMessage): void { | |
| const content = message.payload.content as string; | |
| if (content) { | |
| contentManager.updateFromAgent(content); | |
| } | |
| } | |
| private handleSegmentStart(message: WebSocketMessage): void { | |
| const { segmentId, segmentType, toolName, toolArgs } = message.payload; | |
| if (!segmentId || !this.currentMessageId) return; | |
| const segment: MessageSegment = { | |
| id: segmentId as string, | |
| type: segmentType as MessageSegment["type"], | |
| content: "", | |
| toolName: toolName as string | undefined, | |
| toolArgs: toolArgs as Record<string, unknown> | undefined, | |
| startTime: Date.now(), | |
| streaming: segmentType === "text", | |
| }; | |
| this.currentSegments.set(segmentId as string, segment); | |
| this.updateMessageSegments(); | |
| } | |
| private handleSegmentToken(message: WebSocketMessage): void { | |
| const { segmentId, token } = message.payload; | |
| if (!segmentId || !token) return; | |
| const segment = this.currentSegments.get(segmentId as string); | |
| if (segment) { | |
| const updatedSegment = { | |
| ...segment, | |
| content: segment.content + (token as string), | |
| }; | |
| this.currentSegments.set(segmentId as string, updatedSegment); | |
| // Throttle updates during streaming to avoid excessive re-renders | |
| if (this.updateTimer) { | |
| clearTimeout(this.updateTimer); | |
| } | |
| this.updateTimer = setTimeout(() => { | |
| this.updateMessageSegments(); | |
| this.updateTimer = null; | |
| }, 50); | |
| } | |
| } | |
| private handleSegmentEnd(message: WebSocketMessage): void { | |
| const { | |
| segmentId, | |
| content, | |
| toolStatus, | |
| toolOutput, | |
| toolError, | |
| consoleOutput, | |
| } = message.payload; | |
| if (!segmentId) return; | |
| // Clear any pending update timer for immediate final update | |
| if (this.updateTimer) { | |
| clearTimeout(this.updateTimer); | |
| this.updateTimer = null; | |
| } | |
| const segment = this.currentSegments.get(segmentId as string); | |
| if (segment) { | |
| const completedSegment: MessageSegment = { | |
| ...segment, | |
| content: content ? (content as string) : segment.content, | |
| toolStatus: toolStatus | |
| ? (toolStatus as MessageSegment["toolStatus"]) | |
| : segment.toolStatus, | |
| toolOutput: toolOutput ? (toolOutput as string) : segment.toolOutput, | |
| toolError: toolError ? (toolError as string) : segment.toolError, | |
| consoleOutput: consoleOutput | |
| ? (consoleOutput as string[]) | |
| : segment.consoleOutput, | |
| endTime: Date.now(), | |
| streaming: false, | |
| }; | |
| // Special handling for todo tools - merge with existing | |
| if (this.isTodoTool(completedSegment)) { | |
| this.handleTodoSegment(completedSegment); | |
| } else { | |
| this.completedSegments.push(completedSegment); | |
| } | |
| this.currentSegments.delete(segmentId as string); | |
| this.updateMessageSegments(); | |
| } | |
| } | |
| private isTodoTool(segment: MessageSegment): boolean { | |
| return !!segment.toolName?.includes("task"); | |
| } | |
| private handleTodoSegment(segment: MessageSegment): void { | |
| // Remove previous todo segment and replace with new one | |
| if (this.latestTodoSegmentId) { | |
| this.completedSegments = this.completedSegments.filter( | |
| (s) => s.id !== this.latestTodoSegmentId, | |
| ); | |
| } | |
| this.latestTodoSegmentId = segment.id; | |
| this.completedSegments.push(segment); | |
| } | |
| private updateMessageSegments(): void { | |
| if (!this.currentMessageId) return; | |
| const allSegments = [ | |
| ...this.completedSegments, | |
| ...Array.from(this.currentSegments.values()), | |
| ]; | |
| const content = this.buildContentFromSegments(allSegments); | |
| chatStore.setLastMessageContent(content); | |
| chatStore.setLastMessageSegments(allSegments); | |
| } | |
| private buildContentFromSegments(segments: MessageSegment[]): string { | |
| let content = ""; | |
| let lastWasText = false; | |
| for (const segment of segments) { | |
| if (segment.type === "text" && segment.content) { | |
| // Add spacing between text segments if needed | |
| if (lastWasText && content && !content.endsWith("\n")) { | |
| content += " "; | |
| } | |
| content += segment.content; | |
| lastWasText = true; | |
| } else { | |
| // Tool segments are handled in UI, but reset text flag | |
| lastWasText = false; | |
| } | |
| } | |
| return content.trim(); | |
| } | |
| } | |
| export const messageHandler = new MessageHandler(); | |