diff --git a/app/lib/hooks/useMessageParser.ts b/app/lib/hooks/useMessageParser.ts index e08e77e91e..92e3a9673a 100644 --- a/app/lib/hooks/useMessageParser.ts +++ b/app/lib/hooks/useMessageParser.ts @@ -1,12 +1,12 @@ import type { Message } from 'ai'; import { useCallback, useState } from 'react'; -import { StreamingMessageParser } from '~/lib/runtime/message-parser'; +import { EnhancedStreamingMessageParser } from '~/lib/runtime/enhanced-message-parser'; import { workbenchStore } from '~/lib/stores/workbench'; import { createScopedLogger } from '~/utils/logger'; const logger = createScopedLogger('useMessageParser'); -const messageParser = new StreamingMessageParser({ +const messageParser = new EnhancedStreamingMessageParser({ callbacks: { onArtifactOpen: (data) => { logger.trace('onArtifactOpen', data); diff --git a/app/lib/runtime/enhanced-message-parser.ts b/app/lib/runtime/enhanced-message-parser.ts new file mode 100644 index 0000000000..9820302df0 --- /dev/null +++ b/app/lib/runtime/enhanced-message-parser.ts @@ -0,0 +1,300 @@ +import { createScopedLogger } from '~/utils/logger'; +import { StreamingMessageParser, type StreamingMessageParserOptions } from './message-parser'; + +const logger = createScopedLogger('EnhancedMessageParser'); + +/** + * Enhanced message parser that detects code blocks and file patterns + * even when AI models don't wrap them in proper artifact tags. + * Fixes issue #1797 where code outputs to chat instead of files. + */ +export class EnhancedStreamingMessageParser extends StreamingMessageParser { + private _processedCodeBlocks = new Map>(); + private _artifactCounter = 0; + + constructor(options: StreamingMessageParserOptions = {}) { + super(options); + } + + parse(messageId: string, input: string): string { + // First try the normal parsing + let output = super.parse(messageId, input); + + // If no artifacts were detected, check for code blocks that should be files + if (!this._hasDetectedArtifacts(input)) { + const enhancedInput = this._detectAndWrapCodeBlocks(messageId, input); + + if (enhancedInput !== input) { + // Reset and reparse with enhanced input + this.reset(); + output = super.parse(messageId, enhancedInput); + } + } + + return output; + } + + private _hasDetectedArtifacts(input: string): boolean { + return input.includes(''); + } + + private _detectAndWrapCodeBlocks(messageId: string, input: string): string { + // Initialize processed blocks for this message if not exists + if (!this._processedCodeBlocks.has(messageId)) { + this._processedCodeBlocks.set(messageId, new Set()); + } + + const processed = this._processedCodeBlocks.get(messageId)!; + + // Regex patterns for detecting code blocks with file indicators + const patterns = [ + // Pattern 1: Explicit file creation/modification mentions + /(?:create|update|modify|edit|write|add|generate|here'?s?|file:?)\s+(?:a\s+)?(?:new\s+)?(?:file\s+)?(?:called\s+)?[`'"]*([\/\w\-\.]+\.\w+)[`'"]*:?\s*\n+```(\w*)\n([\s\S]*?)```/gi, + + // Pattern 2: Code blocks with filename comments + /```(\w*)\n(?:\/\/|#|