@tantawowa/hosanna-tools 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -30
- package/dist/build-info.json +3 -3
- package/dist/cli.js +42 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/lib/env-info.d.ts +2 -0
- package/dist/lib/env-info.js +53 -26
- package/dist/lib/env-info.js.map +1 -1
- package/dist/lib/spawn-env.d.ts +1 -0
- package/dist/lib/spawn-env.js +36 -0
- package/dist/lib/spawn-env.js.map +1 -0
- package/dist/native/native-project.js +2 -2
- package/dist/native/native-project.js.map +1 -1
- package/dist/run/android-emulator.js +23 -8
- package/dist/run/android-emulator.js.map +1 -1
- package/dist/run/apple-tv.js +1 -1
- package/dist/run/apple-tv.js.map +1 -1
- package/dist/run/executor.d.ts +6 -0
- package/dist/run/executor.js +49 -8
- package/dist/run/executor.js.map +1 -1
- package/dist/run/hsconfig-static-assets.d.ts +20 -0
- package/dist/run/hsconfig-static-assets.js +306 -0
- package/dist/run/hsconfig-static-assets.js.map +1 -0
- package/dist/run/ios-simulator.js +1 -1
- package/dist/run/ios-simulator.js.map +1 -1
- package/dist/run/planner.d.ts +3 -1
- package/dist/run/planner.js +115 -45
- package/dist/run/planner.js.map +1 -1
- package/dist/run/run-config.d.ts +1 -0
- package/dist/run/run-config.js +4 -4
- package/dist/run/run-config.js.map +1 -1
- package/dist/run/run-controller.js +44 -50
- package/dist/run/run-controller.js.map +1 -1
- package/dist/run/targets.d.ts +1 -1
- package/dist/run/targets.js +6 -14
- package/dist/run/targets.js.map +1 -1
- package/dist/run/types.d.ts +2 -2
- package/dist/run/types.js +6 -6
- package/dist/run/types.js.map +1 -1
- package/dist/support-tools/android-metro-build.d.ts +1 -0
- package/dist/support-tools/android-metro-build.js +2 -0
- package/dist/support-tools/android-metro-build.js.map +1 -1
- package/dist/support-tools/apple-bundler.d.ts +1 -0
- package/dist/support-tools/apple-bundler.js +20 -4
- package/dist/support-tools/apple-bundler.js.map +1 -1
- package/dist/support-tools/build-config-resolver.js +13 -4
- package/dist/support-tools/build-config-resolver.js.map +1 -1
- package/dist/support-tools/command-debugger.d.ts +8 -0
- package/dist/support-tools/command-debugger.js +86 -0
- package/dist/support-tools/command-debugger.js.map +1 -1
- package/dist/support-tools/mcp-server/client.d.ts +23 -0
- package/dist/support-tools/mcp-server/client.js +91 -11
- package/dist/support-tools/mcp-server/client.js.map +1 -1
- package/dist/support-tools/mcp-server/tools.js +445 -15
- package/dist/support-tools/mcp-server/tools.js.map +1 -1
- package/dist/support-tools/mcp-server/types.d.ts +20 -0
- package/dist/updater/sdk-manager.js +80 -36
- package/dist/updater/sdk-manager.js.map +1 -1
- package/package.json +1 -1
|
@@ -67,6 +67,11 @@ const token_tracker_js_1 = require("./token-tracker.js");
|
|
|
67
67
|
const roku_deploy_tool_js_1 = require("./roku-deploy-tool.js");
|
|
68
68
|
const cv_recording_manager_js_1 = require("./cv-recording-manager.js");
|
|
69
69
|
const cv_diagnostic_engine_js_1 = require("./cv-diagnostic-engine.js");
|
|
70
|
+
function normalizeToolLimit(limit, defaultLimit) {
|
|
71
|
+
return typeof limit === 'number' && Number.isFinite(limit) && limit > 0
|
|
72
|
+
? Math.floor(limit)
|
|
73
|
+
: defaultLimit;
|
|
74
|
+
}
|
|
70
75
|
// Test recordings storage directory for the current app/project workspace.
|
|
71
76
|
const RECORDINGS_DIR = (0, test_runner_js_1.getRecordingsDir)();
|
|
72
77
|
/**
|
|
@@ -74,6 +79,323 @@ const RECORDINGS_DIR = (0, test_runner_js_1.getRecordingsDir)();
|
|
|
74
79
|
* 12s keeps worst-case wall clock under ~40s for the default three commands; use include_collection_view:false for ~24s max.
|
|
75
80
|
*/
|
|
76
81
|
const GET_STATE_COMMAND_TIMEOUT_MS = 3000;
|
|
82
|
+
function parseScrollJankValue(value) {
|
|
83
|
+
if (value === 'true') {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (value === 'false') {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (value !== '' && /^-?\d+(\.\d+)?$/.test(value)) {
|
|
90
|
+
return Number(value);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function parseScrollJankDetails(details) {
|
|
95
|
+
const fields = {};
|
|
96
|
+
for (const token of details.split(/\s+/)) {
|
|
97
|
+
const separatorIndex = token.indexOf('=');
|
|
98
|
+
if (separatorIndex <= 0) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
fields[token.slice(0, separatorIndex)] = parseScrollJankValue(token.slice(separatorIndex + 1));
|
|
102
|
+
}
|
|
103
|
+
return fields;
|
|
104
|
+
}
|
|
105
|
+
function parseScrollJankLog(log) {
|
|
106
|
+
var _a;
|
|
107
|
+
const rawData = (_a = log.data) !== null && _a !== void 0 ? _a : {};
|
|
108
|
+
const nestedData = rawData.data && typeof rawData.data === 'object' && !Array.isArray(rawData.data)
|
|
109
|
+
? rawData.data
|
|
110
|
+
: undefined;
|
|
111
|
+
const data = (nestedData === null || nestedData === void 0 ? void 0 : nestedData.source) === 'scroll-jank' ? nestedData : rawData;
|
|
112
|
+
const source = data.source;
|
|
113
|
+
const dataEvent = typeof data.event === 'string' ? data.event : undefined;
|
|
114
|
+
const dataDetails = typeof data.details === 'string' ? data.details : undefined;
|
|
115
|
+
const dataFields = data.fields && typeof data.fields === 'object' && !Array.isArray(data.fields)
|
|
116
|
+
? data.fields
|
|
117
|
+
: undefined;
|
|
118
|
+
if (source === 'scroll-jank' && dataEvent) {
|
|
119
|
+
const details = dataDetails !== null && dataDetails !== void 0 ? dataDetails : '';
|
|
120
|
+
return {
|
|
121
|
+
timestamp: log.timestamp,
|
|
122
|
+
time: new Date(log.timestamp).toISOString(),
|
|
123
|
+
appId: log.appId,
|
|
124
|
+
level: log.level,
|
|
125
|
+
event: dataEvent,
|
|
126
|
+
details,
|
|
127
|
+
fields: dataFields !== null && dataFields !== void 0 ? dataFields : parseScrollJankDetails(details),
|
|
128
|
+
message: log.message,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const marker = '[SCROLL-JANK] ';
|
|
132
|
+
const markerIndex = log.message.indexOf(marker);
|
|
133
|
+
if (markerIndex < 0) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const line = log.message.slice(markerIndex + marker.length);
|
|
137
|
+
const firstSpace = line.indexOf(' ');
|
|
138
|
+
const event = firstSpace >= 0 ? line.slice(0, firstSpace) : line;
|
|
139
|
+
if (!/^[a-z0-9-]+$/i.test(event)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const details = firstSpace >= 0 ? line.slice(firstSpace + 1) : '';
|
|
143
|
+
return {
|
|
144
|
+
timestamp: log.timestamp,
|
|
145
|
+
time: new Date(log.timestamp).toISOString(),
|
|
146
|
+
appId: log.appId,
|
|
147
|
+
level: log.level,
|
|
148
|
+
event,
|
|
149
|
+
details,
|
|
150
|
+
fields: parseScrollJankDetails(details),
|
|
151
|
+
message: log.message,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function summarizeScrollJankEvents(events) {
|
|
155
|
+
var _a, _b, _c, _d, _e, _f;
|
|
156
|
+
var _g;
|
|
157
|
+
const byEvent = {};
|
|
158
|
+
const durationsByEvent = {};
|
|
159
|
+
const rowCounts = {};
|
|
160
|
+
const durationsByRow = {};
|
|
161
|
+
for (const event of events) {
|
|
162
|
+
byEvent[event.event] = ((_a = byEvent[event.event]) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
163
|
+
const duration = event.fields.durationMs;
|
|
164
|
+
if (typeof duration === 'number') {
|
|
165
|
+
((_b = durationsByEvent[_g = event.event]) !== null && _b !== void 0 ? _b : (durationsByEvent[_g] = [])).push(duration);
|
|
166
|
+
}
|
|
167
|
+
const rowIndex = event.fields.rowIndex;
|
|
168
|
+
if (typeof rowIndex === 'number') {
|
|
169
|
+
const key = String(rowIndex);
|
|
170
|
+
rowCounts[key] = ((_c = rowCounts[key]) !== null && _c !== void 0 ? _c : 0) + 1;
|
|
171
|
+
if (typeof duration === 'number') {
|
|
172
|
+
((_d = durationsByRow[key]) !== null && _d !== void 0 ? _d : (durationsByRow[key] = [])).push(duration);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const summarizeDurations = (durations) => {
|
|
177
|
+
var _a, _b;
|
|
178
|
+
const sorted = [...durations].sort((a, b) => a - b);
|
|
179
|
+
const sum = sorted.reduce((acc, value) => acc + value, 0);
|
|
180
|
+
const percentile = (p) => { var _a; return (_a = sorted[Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * p))]) !== null && _a !== void 0 ? _a : 0; };
|
|
181
|
+
return {
|
|
182
|
+
count: sorted.length,
|
|
183
|
+
min: (_a = sorted[0]) !== null && _a !== void 0 ? _a : 0,
|
|
184
|
+
p50: percentile(0.5),
|
|
185
|
+
p95: percentile(0.95),
|
|
186
|
+
max: (_b = sorted[sorted.length - 1]) !== null && _b !== void 0 ? _b : 0,
|
|
187
|
+
avg: sorted.length ? Number((sum / sorted.length).toFixed(2)) : 0,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const durationSummary = Object.fromEntries(Object.entries(durationsByEvent).map(([event, durations]) => [event, summarizeDurations(durations)]));
|
|
191
|
+
const rowDurationSummary = Object.fromEntries(Object.entries(durationsByRow).map(([row, durations]) => [row, summarizeDurations(durations)]));
|
|
192
|
+
const slowest = events
|
|
193
|
+
.filter((event) => typeof event.fields.durationMs === 'number')
|
|
194
|
+
.sort((a, b) => (Number(b.fields.durationMs) || 0) - (Number(a.fields.durationMs) || 0))
|
|
195
|
+
.slice(0, 20);
|
|
196
|
+
return {
|
|
197
|
+
total: events.length,
|
|
198
|
+
firstTimestamp: (_e = events[0]) === null || _e === void 0 ? void 0 : _e.time,
|
|
199
|
+
lastTimestamp: (_f = events[events.length - 1]) === null || _f === void 0 ? void 0 : _f.time,
|
|
200
|
+
byEvent,
|
|
201
|
+
durationSummary,
|
|
202
|
+
rowCounts,
|
|
203
|
+
rowDurationSummary,
|
|
204
|
+
slowest,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function markdownTable(headers, rows) {
|
|
208
|
+
if (rows.length === 0) {
|
|
209
|
+
return '_No rows._';
|
|
210
|
+
}
|
|
211
|
+
const escapeCell = (value) => String(value !== null && value !== void 0 ? value : '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
212
|
+
return [
|
|
213
|
+
`| ${headers.map(escapeCell).join(' | ')} |`,
|
|
214
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
215
|
+
...rows.map((row) => `| ${row.map(escapeCell).join(' | ')} |`),
|
|
216
|
+
].join('\n');
|
|
217
|
+
}
|
|
218
|
+
function formatScrollJankMs(value) {
|
|
219
|
+
return typeof value === 'number' ? `${value}ms` : '';
|
|
220
|
+
}
|
|
221
|
+
function classifyScrollJankDuration(durationMs) {
|
|
222
|
+
if (typeof durationMs !== 'number') {
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
if (durationMs >= 100) {
|
|
226
|
+
return 'SEVERE';
|
|
227
|
+
}
|
|
228
|
+
if (durationMs >= 33) {
|
|
229
|
+
return 'BAD';
|
|
230
|
+
}
|
|
231
|
+
if (durationMs >= 16) {
|
|
232
|
+
return 'WARN';
|
|
233
|
+
}
|
|
234
|
+
return 'OK';
|
|
235
|
+
}
|
|
236
|
+
function getScrollJankRowKey(fields) {
|
|
237
|
+
const rowIndex = fields.rowIndex;
|
|
238
|
+
return typeof rowIndex === 'number' ? String(rowIndex) : undefined;
|
|
239
|
+
}
|
|
240
|
+
function getScrollJankRowLabel(fields) {
|
|
241
|
+
for (const key of ['rowLabel', 'rowTitle', 'rowId']) {
|
|
242
|
+
const value = fields[key];
|
|
243
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return '';
|
|
248
|
+
}
|
|
249
|
+
function renderScrollJankTopSuspects(summary, events) {
|
|
250
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
251
|
+
const suspects = [];
|
|
252
|
+
const slowest = summary.slowest[0];
|
|
253
|
+
if (slowest) {
|
|
254
|
+
suspects.push(`${classifyScrollJankDuration(slowest.fields.durationMs)}: worst event is ${slowest.event} at ${formatScrollJankMs(slowest.fields.durationMs)}${typeof slowest.fields.targetRow === 'number' ? ` targeting row ${slowest.fields.targetRow}` : ''}.`);
|
|
255
|
+
}
|
|
256
|
+
const calcEvents = events.filter((entry) => entry.event === 'calculate-visible-cells-end');
|
|
257
|
+
const slowCalc = calcEvents
|
|
258
|
+
.filter((entry) => typeof entry.fields.durationMs === 'number')
|
|
259
|
+
.sort((a, b) => Number(b.fields.durationMs) - Number(a.fields.durationMs))[0];
|
|
260
|
+
if (slowCalc) {
|
|
261
|
+
const row = getScrollJankRowKey(slowCalc.fields);
|
|
262
|
+
const label = getScrollJankRowLabel(slowCalc.fields);
|
|
263
|
+
suspects.push(`${classifyScrollJankDuration(slowCalc.fields.durationMs)}: slowest visible-cell calculation took ${formatScrollJankMs(slowCalc.fields.durationMs)} on row ${row !== null && row !== void 0 ? row : '?'}${label ? ` (${label})` : ''}; new=${String((_a = slowCalc.fields.new) !== null && _a !== void 0 ? _a : '')}, reused=${String((_b = slowCalc.fields.reused) !== null && _b !== void 0 ? _b : '')}, rendered=${String((_c = slowCalc.fields.rendered) !== null && _c !== void 0 ? _c : '')}.`);
|
|
264
|
+
}
|
|
265
|
+
const coldRows = calcEvents
|
|
266
|
+
.filter((entry) => { var _a; return typeof entry.fields.new === 'number' && Number(entry.fields.new) > 0 && Number((_a = entry.fields.reused) !== null && _a !== void 0 ? _a : 0) === 0; })
|
|
267
|
+
.sort((a, b) => { var _a, _b; return Number((_a = b.fields.durationMs) !== null && _a !== void 0 ? _a : 0) - Number((_b = a.fields.durationMs) !== null && _b !== void 0 ? _b : 0); })
|
|
268
|
+
.slice(0, 3);
|
|
269
|
+
for (const entry of coldRows) {
|
|
270
|
+
const row = getScrollJankRowKey(entry.fields);
|
|
271
|
+
const label = getScrollJankRowLabel(entry.fields);
|
|
272
|
+
suspects.push(`${classifyScrollJankDuration(entry.fields.durationMs)}: cold row work on row ${row !== null && row !== void 0 ? row : '?'}${label ? ` (${label})` : ''}; created ${String(entry.fields.new)} cells, reused ${String((_d = entry.fields.reused) !== null && _d !== void 0 ? _d : 0)}, duration ${formatScrollJankMs(entry.fields.durationMs)}.`);
|
|
273
|
+
}
|
|
274
|
+
const fragmentMax = (_f = (_e = summary.durationSummary['fragment-add-end']) === null || _e === void 0 ? void 0 : _e.max) !== null && _f !== void 0 ? _f : (_g = summary.durationSummary['fragment-create-end']) === null || _g === void 0 ? void 0 : _g.max;
|
|
275
|
+
if (typeof fragmentMax === 'number') {
|
|
276
|
+
suspects.push(`${classifyScrollJankDuration(fragmentMax)}: fragment add/create max is ${formatScrollJankMs(fragmentMax)}.`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
suspects.push('OK: no fragment add/create duration hotspot was captured in this window.');
|
|
280
|
+
}
|
|
281
|
+
return suspects;
|
|
282
|
+
}
|
|
283
|
+
function renderScrollJankTelemetryReport(summary, events, options) {
|
|
284
|
+
var _a, _b, _c;
|
|
285
|
+
const durationRows = Object.entries(summary.durationSummary)
|
|
286
|
+
.sort(([, a], [, b]) => b.max - a.max)
|
|
287
|
+
.slice(0, 15)
|
|
288
|
+
.map(([name, stats]) => [
|
|
289
|
+
classifyScrollJankDuration(stats.max),
|
|
290
|
+
name,
|
|
291
|
+
stats.count,
|
|
292
|
+
formatScrollJankMs(stats.p50),
|
|
293
|
+
formatScrollJankMs(stats.p95),
|
|
294
|
+
formatScrollJankMs(stats.max),
|
|
295
|
+
formatScrollJankMs(stats.avg),
|
|
296
|
+
]);
|
|
297
|
+
const eventRows = Object.entries(summary.byEvent)
|
|
298
|
+
.sort(([, a], [, b]) => b - a)
|
|
299
|
+
.slice(0, 20)
|
|
300
|
+
.map(([name, count]) => [name, count]);
|
|
301
|
+
const rowRows = Object.entries(summary.rowDurationSummary)
|
|
302
|
+
.sort(([, a], [, b]) => b.max - a.max)
|
|
303
|
+
.slice(0, 15)
|
|
304
|
+
.map(([row, stats]) => {
|
|
305
|
+
var _a;
|
|
306
|
+
return [
|
|
307
|
+
classifyScrollJankDuration(stats.max),
|
|
308
|
+
row,
|
|
309
|
+
(_a = summary.rowCounts[row]) !== null && _a !== void 0 ? _a : 0,
|
|
310
|
+
stats.count,
|
|
311
|
+
formatScrollJankMs(stats.p50),
|
|
312
|
+
formatScrollJankMs(stats.p95),
|
|
313
|
+
formatScrollJankMs(stats.max),
|
|
314
|
+
formatScrollJankMs(stats.avg),
|
|
315
|
+
];
|
|
316
|
+
});
|
|
317
|
+
const slowestRows = summary.slowest.slice(0, 20).map((entry) => [
|
|
318
|
+
classifyScrollJankDuration(entry.fields.durationMs),
|
|
319
|
+
new Date(entry.timestamp).toISOString().slice(11, 23),
|
|
320
|
+
entry.event,
|
|
321
|
+
formatScrollJankMs(entry.fields.durationMs),
|
|
322
|
+
entry.fields.rowIndex,
|
|
323
|
+
getScrollJankRowLabel(entry.fields),
|
|
324
|
+
entry.fields.cellIndex,
|
|
325
|
+
entry.fields.targetRow,
|
|
326
|
+
entry.fields.new,
|
|
327
|
+
entry.fields.reused,
|
|
328
|
+
entry.fields.rendered,
|
|
329
|
+
entry.fields.virtualY,
|
|
330
|
+
entry.fields.targetY,
|
|
331
|
+
]);
|
|
332
|
+
const rowWorkRows = events
|
|
333
|
+
.filter((entry) => entry.event === 'calculate-visible-cells-end')
|
|
334
|
+
.sort((a, b) => { var _a, _b; return Number((_a = b.fields.durationMs) !== null && _a !== void 0 ? _a : 0) - Number((_b = a.fields.durationMs) !== null && _b !== void 0 ? _b : 0); })
|
|
335
|
+
.slice(0, 20)
|
|
336
|
+
.map((entry) => {
|
|
337
|
+
var _a, _b;
|
|
338
|
+
return [
|
|
339
|
+
classifyScrollJankDuration(entry.fields.durationMs),
|
|
340
|
+
entry.fields.rowIndex,
|
|
341
|
+
getScrollJankRowLabel(entry.fields),
|
|
342
|
+
formatScrollJankMs(entry.fields.durationMs),
|
|
343
|
+
entry.fields.existingCells,
|
|
344
|
+
entry.fields.configured,
|
|
345
|
+
entry.fields.new,
|
|
346
|
+
entry.fields.reused,
|
|
347
|
+
entry.fields.removed,
|
|
348
|
+
entry.fields.rendered,
|
|
349
|
+
Number((_a = entry.fields.new) !== null && _a !== void 0 ? _a : 0) > 0 && Number((_b = entry.fields.reused) !== null && _b !== void 0 ? _b : 0) === 0 ? 'cold row' : '',
|
|
350
|
+
];
|
|
351
|
+
});
|
|
352
|
+
const clearVerdict = summary.slowest.length === 0
|
|
353
|
+
? 'No duration-bearing scroll-jank events were captured.'
|
|
354
|
+
: `Worst event: ${summary.slowest[0].event} at ${formatScrollJankMs(summary.slowest[0].fields.durationMs)} (${classifyScrollJankDuration(summary.slowest[0].fields.durationMs)}).`;
|
|
355
|
+
const suspects = renderScrollJankTopSuspects(summary, events);
|
|
356
|
+
const sequenceLines = ((_a = options.sequence) === null || _a === void 0 ? void 0 : _a.length)
|
|
357
|
+
? ['## Sequence', markdownTable(['step'], options.sequence.map((step) => [step])), '']
|
|
358
|
+
: [];
|
|
359
|
+
const title = options.format === 'table' ? 'Scroll-Jank Telemetry Report' : '# Scroll-Jank Telemetry Report';
|
|
360
|
+
return [
|
|
361
|
+
title,
|
|
362
|
+
'',
|
|
363
|
+
`App: ${(_b = options.appId) !== null && _b !== void 0 ? _b : 'all apps'}`,
|
|
364
|
+
`Filter: ${(_c = options.event) !== null && _c !== void 0 ? _c : 'all events'}`,
|
|
365
|
+
`Returned events: ${events.length} / requested limit ${options.limit}`,
|
|
366
|
+
`Window: ${summary.firstTimestamp ? `${summary.firstTimestamp} -> ${summary.lastTimestamp}` : 'empty'}`,
|
|
367
|
+
`Verdict hint: ${clearVerdict}`,
|
|
368
|
+
'',
|
|
369
|
+
'## Severity Legend',
|
|
370
|
+
markdownTable(['severity', 'duration', 'meaning'], [
|
|
371
|
+
['OK', '< 16ms', 'fits inside one 60fps frame'],
|
|
372
|
+
['WARN', '16-32ms', 'misses one frame or is close to doing so'],
|
|
373
|
+
['BAD', '33-99ms', 'visible jank; two or more frames at 60fps'],
|
|
374
|
+
['SEVERE', '>= 100ms', 'major hitch likely visible to users'],
|
|
375
|
+
]),
|
|
376
|
+
'',
|
|
377
|
+
...sequenceLines,
|
|
378
|
+
'## Top Suspects',
|
|
379
|
+
suspects.length ? suspects.map((suspect) => `- ${suspect}`).join('\n') : '_No suspects._',
|
|
380
|
+
'',
|
|
381
|
+
'## Duration Hotspots',
|
|
382
|
+
markdownTable(['severity', 'event', 'count', 'p50', 'p95', 'max', 'avg'], durationRows),
|
|
383
|
+
'',
|
|
384
|
+
'## Slowest Events',
|
|
385
|
+
markdownTable(['severity', 'time', 'event', 'duration', 'row', 'rowLabel', 'cell', 'targetRow', 'new', 'reused', 'rendered', 'virtualY', 'targetY'], slowestRows),
|
|
386
|
+
'',
|
|
387
|
+
'## Visible Cell Work',
|
|
388
|
+
markdownTable(['severity', 'row', 'rowLabel', 'duration', 'existingCells', 'configured', 'new', 'reused', 'removed', 'rendered', 'note'], rowWorkRows),
|
|
389
|
+
'',
|
|
390
|
+
'## Row Hotspots',
|
|
391
|
+
markdownTable(['severity', 'row', 'events', 'timedEvents', 'p50', 'p95', 'max', 'avg'], rowRows),
|
|
392
|
+
'',
|
|
393
|
+
'## Event Counts',
|
|
394
|
+
markdownTable(['event', 'count'], eventRows),
|
|
395
|
+
'',
|
|
396
|
+
'Use `format: "json"` when you need the raw event payloads for scripts or deeper diffing.',
|
|
397
|
+
].join('\n');
|
|
398
|
+
}
|
|
77
399
|
/**
|
|
78
400
|
* Build a tool response and track token usage.
|
|
79
401
|
*/
|
|
@@ -3926,23 +4248,27 @@ Shows:
|
|
|
3926
4248
|
- nextFocusMap hits/misses with target IDs and failure reasons
|
|
3927
4249
|
- Bubble-up chain when focus bubbles to parent views
|
|
3928
4250
|
- Per-key timeline when timeline=true
|
|
4251
|
+
- With capture_origins=true: PROGRAMMATIC setFocus origins — i.e. WHO called setFocus (e.g. an app onInputEvent handler doing setFocusedSubview('x')), with the JS call stack. Directional resolution alone cannot show this; use it when focus "bounces" to a view that no nextFocusMap/group navigation explains (the classic "menu opens unexpectedly after Back" class of bug).
|
|
3929
4252
|
|
|
3930
4253
|
Use when:
|
|
3931
4254
|
- Focus goes to unexpected view
|
|
3932
4255
|
- nextFocusMap doesn't work
|
|
3933
4256
|
- Focus gets trapped
|
|
4257
|
+
- A view opens/activates unexpectedly and you need WHO set focus there (capture_origins)
|
|
3934
4258
|
- Need to understand focus resolution logic
|
|
3935
4259
|
|
|
3936
|
-
Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in debug-flags.dev.json
|
|
4260
|
+
Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in debug-flags.dev.json.
|
|
4261
|
+
Note: capture_origins requires the framework to emit origins — FocusManager.setFocus must push { targetHid, targetId, source, callStack: new Error().stack } into the focus-trace buffer when SetFocusTracing was enabled with captureOrigins:true. Until that emit lands, origins will be empty (the tool degrades gracefully).`, {
|
|
3937
4262
|
keys: zod_1.z.array(zod_1.z.string()).describe('Keys to send (e.g., ["Right", "Down"])'),
|
|
3938
4263
|
trace_depth: zod_1.z.number().optional().default(5).describe('Max bubble-up levels to trace (default: 5)'),
|
|
3939
4264
|
show_candidates: zod_1.z.boolean().optional().default(false).describe('Include rejected focus candidates'),
|
|
4265
|
+
capture_origins: zod_1.z.boolean().optional().default(false).describe('Capture PROGRAMMATIC setFocus origins (who called setFocus, with call stack) — surfaces app handlers doing setFocusedSubview(...), which directional resolution does not. Requires the framework focus-origin emit.'),
|
|
3940
4266
|
timeline: zod_1.z.boolean().optional().default(true).describe('Show per-key breakdown (default: true)'),
|
|
3941
4267
|
delay_ms: zod_1.z.number().optional().default(300).describe('Delay after each key in ms (default: 300)'),
|
|
3942
4268
|
format: zod_1.z.enum(['json', 'table', 'markdown']).optional().default('table').describe('Output format'),
|
|
3943
4269
|
appId: zod_1.z.string().optional().describe('Target app ID'),
|
|
3944
|
-
}, async ({ keys, trace_depth = 5, show_candidates = false, timeline = true, delay_ms = 300, format = 'table', appId }) => {
|
|
3945
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
|
|
4270
|
+
}, async ({ keys, trace_depth = 5, show_candidates = false, capture_origins = false, timeline = true, delay_ms = 300, format = 'table', appId }) => {
|
|
4271
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6;
|
|
3946
4272
|
const targetAppId = appId !== null && appId !== void 0 ? appId : (_a = (await client.getFirstConnectedApp())) === null || _a === void 0 ? void 0 : _a.appId;
|
|
3947
4273
|
if (!targetAppId) {
|
|
3948
4274
|
return {
|
|
@@ -3961,6 +4287,7 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
3961
4287
|
await client.executeCommand(targetAppId, types_js_1.RemoteDebugCommand.SetFocusTracing, {
|
|
3962
4288
|
enabled: true,
|
|
3963
4289
|
recordCandidates: show_candidates,
|
|
4290
|
+
captureOrigins: capture_origins,
|
|
3964
4291
|
});
|
|
3965
4292
|
try {
|
|
3966
4293
|
// Process each key
|
|
@@ -3997,6 +4324,7 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
3997
4324
|
focusMoved,
|
|
3998
4325
|
trace: (_h = traceData === null || traceData === void 0 ? void 0 : traceData.trace) !== null && _h !== void 0 ? _h : null,
|
|
3999
4326
|
context: (_j = traceData === null || traceData === void 0 ? void 0 : traceData.context) !== null && _j !== void 0 ? _j : null,
|
|
4327
|
+
programmaticOrigins: (_k = traceData === null || traceData === void 0 ? void 0 : traceData.programmaticOrigins) !== null && _k !== void 0 ? _k : null,
|
|
4000
4328
|
});
|
|
4001
4329
|
}
|
|
4002
4330
|
}
|
|
@@ -4008,7 +4336,7 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4008
4336
|
// Format output
|
|
4009
4337
|
// ─────────────────────────────────────────────────────────────────────────
|
|
4010
4338
|
const lastStep = steps[steps.length - 1];
|
|
4011
|
-
const overallMoved = (initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData._hid) !== ((
|
|
4339
|
+
const overallMoved = (initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData._hid) !== ((_l = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _l === void 0 ? void 0 : _l._hid);
|
|
4012
4340
|
// Table format
|
|
4013
4341
|
if (format === 'table') {
|
|
4014
4342
|
const lines = [];
|
|
@@ -4029,10 +4357,10 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4029
4357
|
lines.push(`╚${'═'.repeat(74)}╝`);
|
|
4030
4358
|
// Summary
|
|
4031
4359
|
if (overallMoved) {
|
|
4032
|
-
lines.push(`${C.green}✓${C.reset} Focus changed: ${(
|
|
4360
|
+
lines.push(`${C.green}✓${C.reset} Focus changed: ${(_o = (_m = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _m !== void 0 ? _m : initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData._hid) !== null && _o !== void 0 ? _o : 'none'} → ${(_s = (_q = (_p = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _p === void 0 ? void 0 : _p.id) !== null && _q !== void 0 ? _q : (_r = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _r === void 0 ? void 0 : _r._hid) !== null && _s !== void 0 ? _s : 'none'}`);
|
|
4033
4361
|
}
|
|
4034
4362
|
else {
|
|
4035
|
-
lines.push(`${C.yellow}○${C.reset} Focus unchanged: ${(
|
|
4363
|
+
lines.push(`${C.yellow}○${C.reset} Focus unchanged: ${(_u = (_t = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _t !== void 0 ? _t : initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData._hid) !== null && _u !== void 0 ? _u : 'none'}`);
|
|
4036
4364
|
}
|
|
4037
4365
|
lines.push('');
|
|
4038
4366
|
// Per-key breakdown
|
|
@@ -4046,8 +4374,8 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4046
4374
|
const moveMark = step.focusMoved ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`;
|
|
4047
4375
|
lines.push(`${moveMark} [${step.step}] ${C.bold}${step.key}${C.reset} ${C.cyan}${methodTag}${C.reset} +${step.timestamp}ms`);
|
|
4048
4376
|
// Focus transition
|
|
4049
|
-
const fromId = ((
|
|
4050
|
-
const toId = ((
|
|
4377
|
+
const fromId = ((_v = step.focusBefore) === null || _v === void 0 ? void 0 : _v.id) || ((_w = step.focusBefore) === null || _w === void 0 ? void 0 : _w._hid) || 'none';
|
|
4378
|
+
const toId = ((_x = step.focusAfter) === null || _x === void 0 ? void 0 : _x.id) || ((_y = step.focusAfter) === null || _y === void 0 ? void 0 : _y._hid) || 'none';
|
|
4051
4379
|
if (step.focusMoved) {
|
|
4052
4380
|
lines.push(` ${fromId} → ${C.bold}${toId}${C.reset}`);
|
|
4053
4381
|
}
|
|
@@ -4076,6 +4404,18 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4076
4404
|
lines.push(` ${C.dim}bubble: ${chainStr}${C.reset}`);
|
|
4077
4405
|
}
|
|
4078
4406
|
}
|
|
4407
|
+
// Programmatic setFocus origins — "who called setFocus" (the gap directional resolution can't show)
|
|
4408
|
+
if (step.programmaticOrigins && step.programmaticOrigins.length > 0) {
|
|
4409
|
+
for (const origin of step.programmaticOrigins) {
|
|
4410
|
+
lines.push(` ${C.yellow}setFocus→${origin.targetId || origin.targetHid} (source=${origin.source})${C.reset}`);
|
|
4411
|
+
if (origin.callStack) {
|
|
4412
|
+
const frames = origin.callStack.split('\n').map(s => s.trim()).filter(s => s.startsWith('at ')).slice(0, trace_depth);
|
|
4413
|
+
for (const f of frames) {
|
|
4414
|
+
lines.push(` ${C.dim}${f}${C.reset}`);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4079
4419
|
lines.push('');
|
|
4080
4420
|
}
|
|
4081
4421
|
// Siblings at final focus
|
|
@@ -4099,10 +4439,10 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4099
4439
|
lines.push(`## diff_focus: ${keys.join(' → ')}`);
|
|
4100
4440
|
lines.push('');
|
|
4101
4441
|
if (overallMoved) {
|
|
4102
|
-
lines.push(`✅ **Focus changed:** ${(
|
|
4442
|
+
lines.push(`✅ **Focus changed:** ${(_z = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _z !== void 0 ? _z : 'none'} → ${(_1 = (_0 = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _0 === void 0 ? void 0 : _0.id) !== null && _1 !== void 0 ? _1 : 'none'}`);
|
|
4103
4443
|
}
|
|
4104
4444
|
else {
|
|
4105
|
-
lines.push(`⚪ **Focus unchanged:** ${(
|
|
4445
|
+
lines.push(`⚪ **Focus unchanged:** ${(_2 = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _2 !== void 0 ? _2 : 'none'}`);
|
|
4106
4446
|
}
|
|
4107
4447
|
lines.push('');
|
|
4108
4448
|
for (const step of steps) {
|
|
@@ -4115,8 +4455,8 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4115
4455
|
const moveMark = step.focusMoved ? '✅' : '⚪';
|
|
4116
4456
|
lines.push(`### ${moveMark} [${step.step}] **${step.key}** ${methodTag}`);
|
|
4117
4457
|
lines.push('');
|
|
4118
|
-
const fromId = ((
|
|
4119
|
-
const toId = ((
|
|
4458
|
+
const fromId = ((_3 = step.focusBefore) === null || _3 === void 0 ? void 0 : _3.id) || ((_4 = step.focusBefore) === null || _4 === void 0 ? void 0 : _4._hid) || 'none';
|
|
4459
|
+
const toId = ((_5 = step.focusAfter) === null || _5 === void 0 ? void 0 : _5.id) || ((_6 = step.focusAfter) === null || _6 === void 0 ? void 0 : _6._hid) || 'none';
|
|
4120
4460
|
if (step.focusMoved) {
|
|
4121
4461
|
lines.push(`${fromId} → **${toId}**`);
|
|
4122
4462
|
}
|
|
@@ -4141,6 +4481,21 @@ Note: Focus tracing can also be enabled permanently via mcp.focusTracing=true in
|
|
|
4141
4481
|
const chainStr = chain.map(e => `${e.type}:${e.returned}`).join(' → ');
|
|
4142
4482
|
lines.push(`*bubble:* ${chainStr}`);
|
|
4143
4483
|
}
|
|
4484
|
+
// Programmatic setFocus origins — "who called setFocus"
|
|
4485
|
+
if (step.programmaticOrigins && step.programmaticOrigins.length > 0) {
|
|
4486
|
+
for (const origin of step.programmaticOrigins) {
|
|
4487
|
+
lines.push(`**setFocus → ${origin.targetId || origin.targetHid}** *(source=${origin.source})*`);
|
|
4488
|
+
if (origin.callStack) {
|
|
4489
|
+
const frames = origin.callStack.split('\n').map(s => s.trim()).filter(s => s.startsWith('at ')).slice(0, trace_depth);
|
|
4490
|
+
if (frames.length > 0) {
|
|
4491
|
+
lines.push('```');
|
|
4492
|
+
for (const f of frames)
|
|
4493
|
+
lines.push(f);
|
|
4494
|
+
lines.push('```');
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4144
4499
|
lines.push('');
|
|
4145
4500
|
}
|
|
4146
4501
|
const finalContext = lastStep === null || lastStep === void 0 ? void 0 : lastStep.context;
|
|
@@ -5897,7 +6252,7 @@ If the app has more than one CollectionView, pass collection_view_hid from get_c
|
|
|
5897
6252
|
sinceMs: zod_1.z.number().optional().describe('Only logs from the last N milliseconds'),
|
|
5898
6253
|
sessionId: zod_1.z.string().optional().describe('Filter by debug session ID (matches data.sid field)'),
|
|
5899
6254
|
}, async ({ appId, level, limit = 100, sinceMs, sessionId }) => {
|
|
5900
|
-
const logs = client.
|
|
6255
|
+
const logs = await client.getAllAppLogs({ level, sinceMs, limit, sessionId, appId });
|
|
5901
6256
|
if (logs.length === 0) {
|
|
5902
6257
|
return {
|
|
5903
6258
|
content: [{ type: 'text', text: sessionId ? `No app logs matching session "${sessionId}".` : 'No app logs captured yet.' }],
|
|
@@ -5925,7 +6280,20 @@ If the app has more than one CollectionView, pass collection_view_hid from get_c
|
|
|
5925
6280
|
caseSensitive: zod_1.z.boolean().optional().describe('Case-sensitive search (default: false)'),
|
|
5926
6281
|
limit: zod_1.z.number().optional().describe('Maximum results (default: 50)'),
|
|
5927
6282
|
}, async ({ pattern, appId, caseSensitive = false, limit = 50 }) => {
|
|
5928
|
-
const
|
|
6283
|
+
const flags = caseSensitive ? '' : 'i';
|
|
6284
|
+
let regex;
|
|
6285
|
+
try {
|
|
6286
|
+
regex = new RegExp(pattern, flags);
|
|
6287
|
+
}
|
|
6288
|
+
catch (error) {
|
|
6289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6290
|
+
return {
|
|
6291
|
+
content: [{ type: 'text', text: `Invalid regex pattern "${pattern}": ${message}` }],
|
|
6292
|
+
};
|
|
6293
|
+
}
|
|
6294
|
+
const resultLimit = normalizeToolLimit(limit, 50);
|
|
6295
|
+
const logs = await client.getAllAppLogs({ appId, limit: resultLimit * 3 });
|
|
6296
|
+
const matches = logs.filter(l => regex.test(l.message) || regex.test(l.raw)).slice(-resultLimit);
|
|
5929
6297
|
if (matches.length === 0) {
|
|
5930
6298
|
return {
|
|
5931
6299
|
content: [{ type: 'text', text: `No app logs matching "${pattern}"` }],
|
|
@@ -5944,10 +6312,72 @@ If the app has more than one CollectionView, pass collection_view_hid from get_c
|
|
|
5944
6312
|
}],
|
|
5945
6313
|
};
|
|
5946
6314
|
});
|
|
6315
|
+
server.tool('get_scroll_jank_telemetry', 'Get structured scroll-jank telemetry emitted by Hosanna UI through remote.log(). Use after driving CollectionView scrolling to identify fragment churn, cell recalculation, SG add/remove, and animation duration hotspots.', {
|
|
6316
|
+
appId: zod_1.z.string().optional().describe('Filter telemetry to a specific app by appId'),
|
|
6317
|
+
sinceMs: zod_1.z.number().optional().describe('Only include telemetry from the last N milliseconds'),
|
|
6318
|
+
limit: zod_1.z.number().optional().describe('Maximum number of raw telemetry events to return (default: 500)'),
|
|
6319
|
+
event: zod_1.z.string().optional().describe('Optional exact event name filter, e.g. calculate-visible-cells-end'),
|
|
6320
|
+
clear: zod_1.z.boolean().optional().describe('Clear app logs for the app after reading telemetry (default: false)'),
|
|
6321
|
+
sequence: zod_1.z.array(zod_1.z.string()).optional().describe('Optional human-readable key sequence steps to include in table/markdown reports'),
|
|
6322
|
+
format: zod_1.z.enum(['table', 'markdown', 'json', 'summary']).optional()
|
|
6323
|
+
.describe('Output format: "table" or "markdown" for human review, "json" for raw events, or "summary" for concise text (default: table)'),
|
|
6324
|
+
}, async ({ appId, sinceMs, limit = 500, event, clear = false, sequence, format = 'table' }) => {
|
|
6325
|
+
const resultLimit = normalizeToolLimit(limit, 500);
|
|
6326
|
+
const logs = await client.getAllAppLogs({ sinceMs, limit: resultLimit * 3, appId });
|
|
6327
|
+
let events = logs
|
|
6328
|
+
.map(parseScrollJankLog)
|
|
6329
|
+
.filter((entry) => entry !== undefined);
|
|
6330
|
+
if (event) {
|
|
6331
|
+
events = events.filter((entry) => entry.event === event);
|
|
6332
|
+
}
|
|
6333
|
+
events = events.slice(-resultLimit);
|
|
6334
|
+
const summary = summarizeScrollJankEvents(events);
|
|
6335
|
+
if (clear) {
|
|
6336
|
+
await client.clearAllAppLogs(appId);
|
|
6337
|
+
}
|
|
6338
|
+
if (format === 'summary') {
|
|
6339
|
+
const durationLines = Object.entries(summary.durationSummary)
|
|
6340
|
+
.sort(([, a], [, b]) => b.max - a.max)
|
|
6341
|
+
.slice(0, 12)
|
|
6342
|
+
.map(([name, stats]) => `${name}: count=${stats.count} p50=${stats.p50}ms p95=${stats.p95}ms max=${stats.max}ms avg=${stats.avg}ms`);
|
|
6343
|
+
const eventLines = Object.entries(summary.byEvent)
|
|
6344
|
+
.sort(([, a], [, b]) => b - a)
|
|
6345
|
+
.slice(0, 20)
|
|
6346
|
+
.map(([name, count]) => `${name}: ${count}`);
|
|
6347
|
+
const slowestLines = summary.slowest.slice(0, 10).map((entry) => {
|
|
6348
|
+
const row = typeof entry.fields.rowIndex === 'number' ? ` row=${entry.fields.rowIndex}` : '';
|
|
6349
|
+
const cell = typeof entry.fields.cellIndex === 'number' ? ` cell=${entry.fields.cellIndex}` : '';
|
|
6350
|
+
return `${entry.event}: ${String(entry.fields.durationMs)}ms${row}${cell} ${entry.details}`;
|
|
6351
|
+
});
|
|
6352
|
+
return tracked('get_scroll_jank_telemetry', [
|
|
6353
|
+
`Scroll-jank telemetry: ${summary.total} event(s)`,
|
|
6354
|
+
summary.firstTimestamp ? `Window: ${summary.firstTimestamp} -> ${summary.lastTimestamp}` : 'Window: empty',
|
|
6355
|
+
'',
|
|
6356
|
+
'Event counts:',
|
|
6357
|
+
eventLines.length ? eventLines.join('\n') : '(none)',
|
|
6358
|
+
'',
|
|
6359
|
+
'Duration hotspots:',
|
|
6360
|
+
durationLines.length ? durationLines.join('\n') : '(none)',
|
|
6361
|
+
'',
|
|
6362
|
+
'Slowest events:',
|
|
6363
|
+
slowestLines.length ? slowestLines.join('\n') : '(none)',
|
|
6364
|
+
].join('\n'));
|
|
6365
|
+
}
|
|
6366
|
+
if (format === 'table' || format === 'markdown') {
|
|
6367
|
+
return tracked('get_scroll_jank_telemetry', renderScrollJankTelemetryReport(summary, events, {
|
|
6368
|
+
appId,
|
|
6369
|
+
event,
|
|
6370
|
+
limit,
|
|
6371
|
+
format,
|
|
6372
|
+
sequence,
|
|
6373
|
+
}));
|
|
6374
|
+
}
|
|
6375
|
+
return tracked('get_scroll_jank_telemetry', JSON.stringify({ summary, events }, null, 2));
|
|
6376
|
+
});
|
|
5947
6377
|
server.tool('clear_app_logs', 'Clear the buffered app logs. Pass appId to clear logs for a single app only.', {
|
|
5948
6378
|
appId: zod_1.z.string().optional().describe('Clear logs only for this appId (clears all if omitted)'),
|
|
5949
6379
|
}, async ({ appId }) => {
|
|
5950
|
-
client.
|
|
6380
|
+
await client.clearAllAppLogs(appId);
|
|
5951
6381
|
return {
|
|
5952
6382
|
content: [{ type: 'text', text: appId ? `App log buffer cleared for "${appId}".` : 'App log buffer cleared.' }],
|
|
5953
6383
|
};
|