From 412aa3c035c331b7209a2adae4ec2987584e536e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 18 Jul 2025 00:11:07 +0200 Subject: [PATCH] Fix server crash when Claude status contains regex special characters (#398) --- web/src/server/utils/activity-detector.ts | 49 +++++++++++++------- web/src/test/utils/activity-detector.test.ts | 31 +++++++++++++ 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/web/src/server/utils/activity-detector.ts b/web/src/server/utils/activity-detector.ts index 26d38de3..ee52dabe 100644 --- a/web/src/server/utils/activity-detector.ts +++ b/web/src/server/utils/activity-detector.ts @@ -24,6 +24,13 @@ function superDebug(message: string, ...args: unknown[]): void { // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes need control characters const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]/g; +/** + * Escape special regex characters in a string + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** * Activity status returned by app-specific parsers */ @@ -169,7 +176,10 @@ function parseClaudeStatus(data: string): ActivityStatus | null { originalPos + fullMatch.length + 50 ); // Look for the status pattern in the middle section - const statusPattern = new RegExp(`[^\n]*${indicator}[^\n]*to\\s+interrupt[^\n]*`, 'gi'); + const statusPattern = new RegExp( + `[^\n]*${escapeRegex(indicator)}[^\n]*to\\s+interrupt[^\n]*`, + 'gi' + ); const cleanedMiddle = middle.replace(statusPattern, ''); filteredData = before + cleanedMiddle + after; } @@ -271,23 +281,28 @@ export class ActivityDetector { // Try app-specific detection first if (this.detector) { - const status = this.detector.parseStatus(data); - if (status) { - this.currentStatus = status; - this.lastStatusTime = Date.now(); - // Always update activity time for app-specific status - this.lastActivityTime = Date.now(); - return { - filteredData: status.filteredData, - activity: { - isActive: true, - lastActivityTime: this.lastActivityTime, - specificStatus: { - app: this.detector.name, - status: status.displayText, + try { + const status = this.detector.parseStatus(data); + if (status) { + this.currentStatus = status; + this.lastStatusTime = Date.now(); + // Always update activity time for app-specific status + this.lastActivityTime = Date.now(); + return { + filteredData: status.filteredData, + activity: { + isActive: true, + lastActivityTime: this.lastActivityTime, + specificStatus: { + app: this.detector.name, + status: status.displayText, + }, }, - }, - }; + }; + } + } catch (error) { + logger.error(`Error in ${this.detector.name} status parser:`, error); + // Continue with unfiltered data if parsing fails } } diff --git a/web/src/test/utils/activity-detector.test.ts b/web/src/test/utils/activity-detector.test.ts index b38bc00f..084cc89f 100644 --- a/web/src/test/utils/activity-detector.test.ts +++ b/web/src/test/utils/activity-detector.test.ts @@ -130,6 +130,37 @@ describe('Activity Detector', () => { expect(result.filteredData).toBe(claudeOutput); expect(result.activity.specificStatus).toBeUndefined(); }); + + it('should handle regex special characters in status indicators', () => { + const detector = new ActivityDetector(['claude']); + + // Test with * which is a regex special character that was causing crashes + const statusWithStar = '* Processing… (42s · ↑ 1.2k tokens · esc to interrupt)\n'; + const result1 = detector.processOutput(statusWithStar); + expect(result1.activity.specificStatus?.status).toBe('Processing (42s, ↑1.2k)'); + expect(result1.filteredData).toBe('\n'); + + // Test with other regex special characters + const specialChars = ['*', '+', '?', '.', '^', '$', '|', '(', ')', '[', ']', '{', '}', '\\']; + for (const char of specialChars) { + const statusWithSpecialChar = `${char} Testing… (10s · ↑ 1.0k tokens · esc to interrupt)\n`; + const result = detector.processOutput(statusWithSpecialChar); + expect(result.activity.specificStatus?.status).toBe('Testing (10s, ↑1.0k)'); + expect(result.filteredData).toBe('\n'); + } + }); + + it('should not crash when parsing fails', () => { + const detector = new ActivityDetector(['claude']); + + // Even if something unexpected happens, it should not crash + const malformedOutput = 'Some output that might cause issues\n'; + expect(() => { + const result = detector.processOutput(malformedOutput); + expect(result.filteredData).toBe(malformedOutput); + expect(result.activity.specificStatus).toBeUndefined(); + }).not.toThrow(); + }); }); describe('registerDetector', () => {