@tantawowa/hosanna-tools 3.2.0 → 3.3.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.
Files changed (58) hide show
  1. package/README.md +26 -30
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +42 -30
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/lib/env-info.d.ts +2 -0
  7. package/dist/lib/env-info.js +53 -26
  8. package/dist/lib/env-info.js.map +1 -1
  9. package/dist/lib/spawn-env.d.ts +1 -0
  10. package/dist/lib/spawn-env.js +36 -0
  11. package/dist/lib/spawn-env.js.map +1 -0
  12. package/dist/native/native-project.js +2 -2
  13. package/dist/native/native-project.js.map +1 -1
  14. package/dist/run/android-emulator.js +23 -8
  15. package/dist/run/android-emulator.js.map +1 -1
  16. package/dist/run/apple-tv.js +1 -1
  17. package/dist/run/apple-tv.js.map +1 -1
  18. package/dist/run/executor.d.ts +6 -0
  19. package/dist/run/executor.js +49 -8
  20. package/dist/run/executor.js.map +1 -1
  21. package/dist/run/hsconfig-static-assets.d.ts +20 -0
  22. package/dist/run/hsconfig-static-assets.js +306 -0
  23. package/dist/run/hsconfig-static-assets.js.map +1 -0
  24. package/dist/run/ios-simulator.js +1 -1
  25. package/dist/run/ios-simulator.js.map +1 -1
  26. package/dist/run/planner.d.ts +3 -1
  27. package/dist/run/planner.js +115 -45
  28. package/dist/run/planner.js.map +1 -1
  29. package/dist/run/run-config.d.ts +1 -0
  30. package/dist/run/run-config.js +4 -4
  31. package/dist/run/run-config.js.map +1 -1
  32. package/dist/run/run-controller.js +44 -50
  33. package/dist/run/run-controller.js.map +1 -1
  34. package/dist/run/targets.d.ts +1 -1
  35. package/dist/run/targets.js +6 -14
  36. package/dist/run/targets.js.map +1 -1
  37. package/dist/run/types.d.ts +2 -2
  38. package/dist/run/types.js +6 -6
  39. package/dist/run/types.js.map +1 -1
  40. package/dist/support-tools/android-metro-build.d.ts +1 -0
  41. package/dist/support-tools/android-metro-build.js +2 -0
  42. package/dist/support-tools/android-metro-build.js.map +1 -1
  43. package/dist/support-tools/apple-bundler.d.ts +1 -0
  44. package/dist/support-tools/apple-bundler.js +20 -4
  45. package/dist/support-tools/apple-bundler.js.map +1 -1
  46. package/dist/support-tools/build-config-resolver.js +13 -4
  47. package/dist/support-tools/build-config-resolver.js.map +1 -1
  48. package/dist/support-tools/command-debugger.d.ts +8 -0
  49. package/dist/support-tools/command-debugger.js +86 -0
  50. package/dist/support-tools/command-debugger.js.map +1 -1
  51. package/dist/support-tools/mcp-server/client.d.ts +23 -0
  52. package/dist/support-tools/mcp-server/client.js +91 -11
  53. package/dist/support-tools/mcp-server/client.js.map +1 -1
  54. package/dist/support-tools/mcp-server/tools.js +400 -3
  55. package/dist/support-tools/mcp-server/tools.js.map +1 -1
  56. package/dist/updater/sdk-manager.js +28 -5
  57. package/dist/updater/sdk-manager.js.map +1 -1
  58. 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
  */
@@ -5897,7 +6219,7 @@ If the app has more than one CollectionView, pass collection_view_hid from get_c
5897
6219
  sinceMs: zod_1.z.number().optional().describe('Only logs from the last N milliseconds'),
5898
6220
  sessionId: zod_1.z.string().optional().describe('Filter by debug session ID (matches data.sid field)'),
5899
6221
  }, async ({ appId, level, limit = 100, sinceMs, sessionId }) => {
5900
- const logs = client.getAppLogs({ level, sinceMs, limit, sessionId, appId });
6222
+ const logs = await client.getAllAppLogs({ level, sinceMs, limit, sessionId, appId });
5901
6223
  if (logs.length === 0) {
5902
6224
  return {
5903
6225
  content: [{ type: 'text', text: sessionId ? `No app logs matching session "${sessionId}".` : 'No app logs captured yet.' }],
@@ -5925,7 +6247,20 @@ If the app has more than one CollectionView, pass collection_view_hid from get_c
5925
6247
  caseSensitive: zod_1.z.boolean().optional().describe('Case-sensitive search (default: false)'),
5926
6248
  limit: zod_1.z.number().optional().describe('Maximum results (default: 50)'),
5927
6249
  }, async ({ pattern, appId, caseSensitive = false, limit = 50 }) => {
5928
- const matches = client.searchAppLogs(pattern, caseSensitive, appId).slice(-limit);
6250
+ const flags = caseSensitive ? '' : 'i';
6251
+ let regex;
6252
+ try {
6253
+ regex = new RegExp(pattern, flags);
6254
+ }
6255
+ catch (error) {
6256
+ const message = error instanceof Error ? error.message : String(error);
6257
+ return {
6258
+ content: [{ type: 'text', text: `Invalid regex pattern "${pattern}": ${message}` }],
6259
+ };
6260
+ }
6261
+ const resultLimit = normalizeToolLimit(limit, 50);
6262
+ const logs = await client.getAllAppLogs({ appId, limit: resultLimit * 3 });
6263
+ const matches = logs.filter(l => regex.test(l.message) || regex.test(l.raw)).slice(-resultLimit);
5929
6264
  if (matches.length === 0) {
5930
6265
  return {
5931
6266
  content: [{ type: 'text', text: `No app logs matching "${pattern}"` }],
@@ -5944,10 +6279,72 @@ If the app has more than one CollectionView, pass collection_view_hid from get_c
5944
6279
  }],
5945
6280
  };
5946
6281
  });
6282
+ 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.', {
6283
+ appId: zod_1.z.string().optional().describe('Filter telemetry to a specific app by appId'),
6284
+ sinceMs: zod_1.z.number().optional().describe('Only include telemetry from the last N milliseconds'),
6285
+ limit: zod_1.z.number().optional().describe('Maximum number of raw telemetry events to return (default: 500)'),
6286
+ event: zod_1.z.string().optional().describe('Optional exact event name filter, e.g. calculate-visible-cells-end'),
6287
+ clear: zod_1.z.boolean().optional().describe('Clear app logs for the app after reading telemetry (default: false)'),
6288
+ sequence: zod_1.z.array(zod_1.z.string()).optional().describe('Optional human-readable key sequence steps to include in table/markdown reports'),
6289
+ format: zod_1.z.enum(['table', 'markdown', 'json', 'summary']).optional()
6290
+ .describe('Output format: "table" or "markdown" for human review, "json" for raw events, or "summary" for concise text (default: table)'),
6291
+ }, async ({ appId, sinceMs, limit = 500, event, clear = false, sequence, format = 'table' }) => {
6292
+ const resultLimit = normalizeToolLimit(limit, 500);
6293
+ const logs = await client.getAllAppLogs({ sinceMs, limit: resultLimit * 3, appId });
6294
+ let events = logs
6295
+ .map(parseScrollJankLog)
6296
+ .filter((entry) => entry !== undefined);
6297
+ if (event) {
6298
+ events = events.filter((entry) => entry.event === event);
6299
+ }
6300
+ events = events.slice(-resultLimit);
6301
+ const summary = summarizeScrollJankEvents(events);
6302
+ if (clear) {
6303
+ await client.clearAllAppLogs(appId);
6304
+ }
6305
+ if (format === 'summary') {
6306
+ const durationLines = Object.entries(summary.durationSummary)
6307
+ .sort(([, a], [, b]) => b.max - a.max)
6308
+ .slice(0, 12)
6309
+ .map(([name, stats]) => `${name}: count=${stats.count} p50=${stats.p50}ms p95=${stats.p95}ms max=${stats.max}ms avg=${stats.avg}ms`);
6310
+ const eventLines = Object.entries(summary.byEvent)
6311
+ .sort(([, a], [, b]) => b - a)
6312
+ .slice(0, 20)
6313
+ .map(([name, count]) => `${name}: ${count}`);
6314
+ const slowestLines = summary.slowest.slice(0, 10).map((entry) => {
6315
+ const row = typeof entry.fields.rowIndex === 'number' ? ` row=${entry.fields.rowIndex}` : '';
6316
+ const cell = typeof entry.fields.cellIndex === 'number' ? ` cell=${entry.fields.cellIndex}` : '';
6317
+ return `${entry.event}: ${String(entry.fields.durationMs)}ms${row}${cell} ${entry.details}`;
6318
+ });
6319
+ return tracked('get_scroll_jank_telemetry', [
6320
+ `Scroll-jank telemetry: ${summary.total} event(s)`,
6321
+ summary.firstTimestamp ? `Window: ${summary.firstTimestamp} -> ${summary.lastTimestamp}` : 'Window: empty',
6322
+ '',
6323
+ 'Event counts:',
6324
+ eventLines.length ? eventLines.join('\n') : '(none)',
6325
+ '',
6326
+ 'Duration hotspots:',
6327
+ durationLines.length ? durationLines.join('\n') : '(none)',
6328
+ '',
6329
+ 'Slowest events:',
6330
+ slowestLines.length ? slowestLines.join('\n') : '(none)',
6331
+ ].join('\n'));
6332
+ }
6333
+ if (format === 'table' || format === 'markdown') {
6334
+ return tracked('get_scroll_jank_telemetry', renderScrollJankTelemetryReport(summary, events, {
6335
+ appId,
6336
+ event,
6337
+ limit,
6338
+ format,
6339
+ sequence,
6340
+ }));
6341
+ }
6342
+ return tracked('get_scroll_jank_telemetry', JSON.stringify({ summary, events }, null, 2));
6343
+ });
5947
6344
  server.tool('clear_app_logs', 'Clear the buffered app logs. Pass appId to clear logs for a single app only.', {
5948
6345
  appId: zod_1.z.string().optional().describe('Clear logs only for this appId (clears all if omitted)'),
5949
6346
  }, async ({ appId }) => {
5950
- client.clearAppLogs(appId);
6347
+ await client.clearAllAppLogs(appId);
5951
6348
  return {
5952
6349
  content: [{ type: 'text', text: appId ? `App log buffer cleared for "${appId}".` : 'App log buffer cleared.' }],
5953
6350
  };