diff --git a/app/lib/runtime/message-parser.spec.ts b/app/lib/runtime/message-parser.spec.ts
index ef4ef8e421..94132c8350 100644
--- a/app/lib/runtime/message-parser.spec.ts
+++ b/app/lib/runtime/message-parser.spec.ts
@@ -157,6 +157,58 @@ describe('StreamingMessageParser', () => {
runTest(input, expected);
});
});
+
+ describe('file action with missing filePath', () => {
+ it('should not crash when filePath attribute is missing from file action', () => {
+ const callbacks = {
+ onArtifactOpen: vi.fn(),
+ onArtifactClose: vi.fn(),
+ onActionOpen: vi.fn(),
+ onActionClose: vi.fn(),
+ };
+
+ const parser = new StreamingMessageParser({
+ artifactElement: () => '',
+ callbacks,
+ });
+
+ // This input has a file action without filePath - previously caused
+ // "TypeError: Cannot read properties of undefined (reading 'endsWith')"
+ const input =
+ 'Before some file content After';
+
+ expect(() => parser.parse('message_1', input)).not.toThrow();
+
+ expect(callbacks.onActionOpen).toHaveBeenCalledTimes(1);
+ expect(callbacks.onActionClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not crash during streaming when filePath attribute is missing', () => {
+ const callbacks = {
+ onArtifactOpen: vi.fn(),
+ onArtifactClose: vi.fn(),
+ onActionOpen: vi.fn(),
+ onActionStream: vi.fn(),
+ onActionClose: vi.fn(),
+ };
+
+ const parser = new StreamingMessageParser({
+ artifactElement: () => '',
+ callbacks,
+ });
+
+ // Simulate streaming: first chunk has the action open but no close tag yet
+ const chunk1 =
+ 'Before partial content';
+
+ expect(() => parser.parse('message_1', chunk1)).not.toThrow();
+
+ // Second chunk completes the action
+ const chunk2 = chunk1 + ' more content After';
+
+ expect(() => parser.parse('message_1', chunk2)).not.toThrow();
+ });
+ });
});
describe('EnhancedStreamingMessageParser', () => {
diff --git a/app/lib/runtime/message-parser.ts b/app/lib/runtime/message-parser.ts
index 47769e2589..9d0f060ffd 100644
--- a/app/lib/runtime/message-parser.ts
+++ b/app/lib/runtime/message-parser.ts
@@ -150,7 +150,7 @@ export class StreamingMessageParser {
if ('type' in currentAction && currentAction.type === 'file') {
// Remove markdown code block syntax if present and file is not markdown
- if (!currentAction.filePath.endsWith('.md')) {
+ if (!currentAction.filePath?.endsWith('.md')) {
content = cleanoutMarkdownSyntax(content);
content = cleanEscapedTags(content);
}
@@ -182,7 +182,7 @@ export class StreamingMessageParser {
if ('type' in currentAction && currentAction.type === 'file') {
let content = input.slice(i);
- if (!currentAction.filePath.endsWith('.md')) {
+ if (!currentAction.filePath?.endsWith('.md')) {
content = cleanoutMarkdownSyntax(content);
content = cleanEscapedTags(content);
}
@@ -366,7 +366,7 @@ export class StreamingMessageParser {
(actionAttributes as SupabaseAction).filePath = filePath;
}
} else if (actionType === 'file') {
- const filePath = this.#extractAttribute(actionTag, 'filePath') as string;
+ const filePath = this.#extractAttribute(actionTag, 'filePath') ?? '';
if (!filePath) {
logger.debug('File path not specified');