diff --git a/src/McpResponse.ts b/src/McpResponse.ts index dbdcd9f2b..c76685378 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -36,6 +36,51 @@ interface TraceInsightData { insightName: InsightName; } +class GroupedConsoleMessage { + readonly key: string; + #first: ConsoleFormatter; + #count = 0; + #msgids: number[] = []; + + constructor(first: ConsoleFormatter) { + this.#first = first; + this.key = GroupedConsoleMessage.keyFor(first); + this.add(first); + } + + static keyFor(message: ConsoleFormatter): string { + const json = message.toJSON(); + return JSON.stringify({ + type: json.type, + text: json.text, + argsCount: json.argsCount, + }); + } + + add(message: ConsoleFormatter): void { + const json = message.toJSON(); + this.#count++; + this.#msgids.push(json.id); + } + + toString(): string { + const base = this.#first.toString(); + if (this.#count <= 1) { + return base; + } + return `${base} [${this.#count} times]`; + } + + toJSON(): object { + const base = this.#first.toJSON(); + return { + ...base, + count: this.#count, + msgids: this.#msgids, + }; + } +} + export class McpResponse implements Response { #includePages = false; #snapshotParams?: SnapshotParams; @@ -612,7 +657,7 @@ Call ${handleDialog.name} to handle it before continuing.`); } if (this.#consoleDataOptions?.include) { - const messages = data.consoleMessages ?? []; + const messages = this.#groupConsoleMessages(data.consoleMessages ?? []); response.push('## Console messages'); if (messages.length) { @@ -650,6 +695,41 @@ Call ${handleDialog.name} to handle it before continuing.`); }; } + #groupConsoleMessages( + messages: Array, + ): Array { + const result: Array = []; + + let activeGroup: GroupedConsoleMessage | null = null; + + const flush = () => { + if (activeGroup) { + result.push(activeGroup); + activeGroup = null; + } + }; + + for (const message of messages) { + if (!(message instanceof ConsoleFormatter)) { + flush(); + result.push(message); + continue; + } + + const key = GroupedConsoleMessage.keyFor(message); + if (activeGroup && activeGroup.key === key) { + activeGroup.add(message); + continue; + } + + flush(); + activeGroup = new GroupedConsoleMessage(message); + } + + flush(); + return result; + } + #dataWithPagination(data: T[], pagination?: PaginationOptions) { const response = []; const paginationResult = paginate(data, pagination); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 64ed12dce..82002b034 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -42,6 +42,30 @@ describe('console', () => { }); }); + it('groups identical messages', async () => { + await withMcpContext(async (response, context) => { + const page = await context.newPage(); + await page.setContent(` + + `); + await listConsoleMessages.handler({params: {}}, response, context); + const formattedResponse = await response.handle('test', context); + const textContent = getTextContent(formattedResponse.content[0]); + assert.ok( + textContent.includes('msgid=1 [log] spam (1 args) [5 times]'), + textContent, + ); + assert.ok( + !textContent.includes('msgid=2 [log] spam'), + 'Should collapse subsequent identical messages', + ); + }); + }); + it('lists error objects', async t => { await withMcpContext(async (response, context) => { const page = await context.newPage();