Skip to content

Commit aad515d

Browse files
masamaru0513eneko0513
authored andcommitted
feat: add lastId to grouped console messages and match by argCount
Extend console message grouping to include lastId so that consumers can access the last message in a group via get_console_message. Also require argCount to match for grouping, preventing false grouping of messages with different argument counts. - Add lastId field to grouped output (string and JSON) - Add argCount equality check in groupConsecutive() - Add unit tests for grouping, toStringGrouped, toJSONGrouped Fixes #904
1 parent e0fa7ae commit aad515d

3 files changed

Lines changed: 161 additions & 10 deletions

File tree

src/McpResponse.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,16 +1039,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
10391039
structuredContent.pagination = paginationData.pagination;
10401040
response.push(...paginationData.info);
10411041
response.push(
1042-
...paginationData.items.map(({message, count}) =>
1042+
...paginationData.items.map(({message, count, lastId}) =>
10431043
message instanceof ConsoleFormatter
1044-
? message.toStringGrouped(count)
1044+
? message.toStringGrouped(count, lastId)
10451045
: message.toString(),
10461046
),
10471047
);
10481048
structuredContent.consoleMessages = paginationData.items.map(
1049-
({message, count}) =>
1049+
({message, count, lastId}) =>
10501050
message instanceof ConsoleFormatter
1051-
? message.toJSONGrouped(count)
1051+
? message.toJSONGrouped(count, lastId)
10521052
: message.toJSON(),
10531053
);
10541054
} else {

src/formatters/ConsoleFormatter.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface ConsoleMessageConcise {
3535
argsCount: number;
3636
id: number;
3737
count?: number;
38+
lastId?: number;
3839
}
3940

4041
interface ConsoleMessageDetailed extends ConsoleMessageConcise {
@@ -179,10 +180,13 @@ export class ConsoleFormatter {
179180
}
180181

181182
// The short format with a repeat count.
182-
toStringGrouped(count: number): string {
183+
toStringGrouped(count: number, lastId?: number): string {
183184
const json = this.toJSON();
184185
if (count > 1) {
185186
json.count = count;
187+
if (lastId !== undefined) {
188+
json.lastId = lastId;
189+
}
186190
}
187191
return convertConsoleMessageConciseToString(json);
188192
}
@@ -213,24 +217,28 @@ export class ConsoleFormatter {
213217
};
214218
}
215219

216-
toJSONGrouped(count: number): ConsoleMessageConcise {
220+
toJSONGrouped(count: number, lastId?: number): ConsoleMessageConcise {
217221
const json = this.toJSON();
218222
if (count > 1) {
219223
json.count = count;
224+
if (lastId !== undefined) {
225+
json.lastId = lastId;
226+
}
220227
}
221228
return json;
222229
}
223230

224231
/**
225-
* Groups consecutive messages with the same type and text.
232+
* Groups consecutive messages with the same type, text, and argument count.
226233
* Similar to Chrome DevTools' console grouping behavior.
227234
*/
228235
static groupConsecutive(
229236
messages: Array<ConsoleFormatter | IssueFormatter>,
230-
): Array<{message: ConsoleFormatter | IssueFormatter; count: number}> {
237+
): Array<{message: ConsoleFormatter | IssueFormatter; count: number; lastId?: number}> {
231238
const grouped: Array<{
232239
message: ConsoleFormatter | IssueFormatter;
233240
count: number;
241+
lastId?: number;
234242
}> = [];
235243
for (const msg of messages) {
236244
const prev = grouped[grouped.length - 1];
@@ -239,9 +247,11 @@ export class ConsoleFormatter {
239247
prev.message instanceof ConsoleFormatter &&
240248
msg instanceof ConsoleFormatter &&
241249
prev.message.#type === msg.#type &&
242-
prev.message.#text === msg.#text
250+
prev.message.#text === msg.#text &&
251+
prev.message.#argCount === msg.#argCount
243252
) {
244253
prev.count++;
254+
prev.lastId = msg.#id;
245255
} else {
246256
grouped.push({message: msg, count: 1});
247257
}
@@ -264,7 +274,9 @@ export class ConsoleFormatter {
264274
}
265275

266276
function convertConsoleMessageConciseToString(msg: ConsoleMessageConcise) {
267-
const countSuffix = msg.count && msg.count > 1 ? ` [${msg.count} times]` : '';
277+
const countSuffix = msg.count && msg.count > 1
278+
? ` [${msg.count} times${msg.lastId ? `, last msgid=${msg.lastId}` : ''}]`
279+
: '';
268280
return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)${countSuffix}`;
269281
}
270282

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it} from 'node:test';
9+
10+
import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js';
11+
import type {ConsoleMessage} from '../../src/third_party/index.js';
12+
13+
const createMockMessage = (
14+
type: string,
15+
text: string,
16+
argsCount = 0,
17+
): ConsoleMessage => {
18+
const args = Array.from({length: argsCount}, () => ({
19+
jsonValue: async () => 'val',
20+
remoteObject: () => ({type: 'string'}),
21+
}));
22+
return {
23+
type: () => type,
24+
text: () => text,
25+
args: () => args,
26+
} as unknown as ConsoleMessage;
27+
};
28+
29+
const makeFormatter = (id: number, type: string, text: string, argsCount = 0) =>
30+
ConsoleFormatter.from(createMockMessage(type, text, argsCount), {id});
31+
32+
describe('ConsoleFormatter grouping', () => {
33+
describe('groupConsecutive', () => {
34+
it('groups identical consecutive messages', async () => {
35+
const msgs = await Promise.all([
36+
makeFormatter(1, 'log', 'hello'),
37+
makeFormatter(2, 'log', 'hello'),
38+
makeFormatter(3, 'log', 'hello'),
39+
]);
40+
const grouped = ConsoleFormatter.groupConsecutive(msgs);
41+
assert.strictEqual(grouped.length, 1);
42+
assert.strictEqual(grouped[0].count, 3);
43+
assert.strictEqual(grouped[0].lastId, 3);
44+
});
45+
46+
it('does not group different messages', async () => {
47+
const msgs = await Promise.all([
48+
makeFormatter(1, 'log', 'aaa'),
49+
makeFormatter(2, 'log', 'bbb'),
50+
makeFormatter(3, 'log', 'ccc'),
51+
]);
52+
const grouped = ConsoleFormatter.groupConsecutive(msgs);
53+
assert.strictEqual(grouped.length, 3);
54+
for (const g of grouped) {
55+
assert.strictEqual(g.count, 1);
56+
assert.strictEqual(g.lastId, undefined);
57+
}
58+
});
59+
60+
it('groups A,A,B,A,A correctly', async () => {
61+
const msgs = await Promise.all([
62+
makeFormatter(1, 'log', 'A'),
63+
makeFormatter(2, 'log', 'A'),
64+
makeFormatter(3, 'log', 'B'),
65+
makeFormatter(4, 'log', 'A'),
66+
makeFormatter(5, 'log', 'A'),
67+
]);
68+
const grouped = ConsoleFormatter.groupConsecutive(msgs);
69+
assert.strictEqual(grouped.length, 3);
70+
assert.strictEqual(grouped[0].count, 2);
71+
assert.strictEqual(grouped[0].lastId, 2);
72+
assert.strictEqual(grouped[1].count, 1);
73+
assert.strictEqual(grouped[1].lastId, undefined);
74+
assert.strictEqual(grouped[2].count, 2);
75+
assert.strictEqual(grouped[2].lastId, 5);
76+
});
77+
78+
it('does not group messages with different types', async () => {
79+
const msgs = await Promise.all([
80+
makeFormatter(1, 'log', 'hello'),
81+
makeFormatter(2, 'error', 'hello'),
82+
]);
83+
const grouped = ConsoleFormatter.groupConsecutive(msgs);
84+
assert.strictEqual(grouped.length, 2);
85+
});
86+
87+
it('does not group messages with different argsCount', async () => {
88+
const msgs = await Promise.all([
89+
makeFormatter(1, 'log', 'hello', 1),
90+
makeFormatter(2, 'log', 'hello', 2),
91+
]);
92+
const grouped = ConsoleFormatter.groupConsecutive(msgs);
93+
assert.strictEqual(grouped.length, 2);
94+
});
95+
96+
it('returns empty array for empty input', () => {
97+
const grouped = ConsoleFormatter.groupConsecutive([]);
98+
assert.strictEqual(grouped.length, 0);
99+
});
100+
101+
it('handles single message', async () => {
102+
const msgs = await Promise.all([makeFormatter(1, 'log', 'solo')]);
103+
const grouped = ConsoleFormatter.groupConsecutive(msgs);
104+
assert.strictEqual(grouped.length, 1);
105+
assert.strictEqual(grouped[0].count, 1);
106+
assert.strictEqual(grouped[0].lastId, undefined);
107+
});
108+
});
109+
110+
describe('toStringGrouped', () => {
111+
it('appends count and lastId suffix when count > 1', async () => {
112+
const f = await makeFormatter(1, 'log', 'hello');
113+
const str = f.toStringGrouped(5, 5);
114+
assert.ok(str.includes('[5 times, last msgid=5]'), `expected [5 times, last msgid=5] in: ${str}`);
115+
});
116+
117+
it('does not append count suffix when count is 1', async () => {
118+
const f = await makeFormatter(1, 'log', 'hello');
119+
const str = f.toStringGrouped(1);
120+
assert.ok(!str.includes('times'), `unexpected times in: ${str}`);
121+
});
122+
});
123+
124+
describe('toJSONGrouped', () => {
125+
it('includes count and lastId when count > 1', async () => {
126+
const f = await makeFormatter(1, 'log', 'hello');
127+
const json = f.toJSONGrouped(3, 3);
128+
assert.strictEqual(json.count, 3);
129+
assert.strictEqual(json.lastId, 3);
130+
});
131+
132+
it('does not include count or lastId when count is 1', async () => {
133+
const f = await makeFormatter(1, 'log', 'hello');
134+
const json = f.toJSONGrouped(1);
135+
assert.strictEqual(json.count, undefined);
136+
assert.strictEqual(json.lastId, undefined);
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)