@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.
Files changed (59) 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 +445 -15
  55. package/dist/support-tools/mcp-server/tools.js.map +1 -1
  56. package/dist/support-tools/mcp-server/types.d.ts +20 -0
  57. package/dist/updater/sdk-manager.js +80 -36
  58. package/dist/updater/sdk-manager.js.map +1 -1
  59. 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) !== ((_k = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _k === void 0 ? void 0 : _k._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: ${(_m = (_l = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _l !== void 0 ? _l : initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData._hid) !== null && _m !== void 0 ? _m : 'none'} → ${(_r = (_p = (_o = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _o === void 0 ? void 0 : _o.id) !== null && _p !== void 0 ? _p : (_q = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _q === void 0 ? void 0 : _q._hid) !== null && _r !== void 0 ? _r : 'none'}`);
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: ${(_t = (_s = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _s !== void 0 ? _s : initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData._hid) !== null && _t !== void 0 ? _t : 'none'}`);
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 = ((_u = step.focusBefore) === null || _u === void 0 ? void 0 : _u.id) || ((_v = step.focusBefore) === null || _v === void 0 ? void 0 : _v._hid) || 'none';
4050
- const toId = ((_w = step.focusAfter) === null || _w === void 0 ? void 0 : _w.id) || ((_x = step.focusAfter) === null || _x === void 0 ? void 0 : _x._hid) || 'none';
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:** ${(_y = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _y !== void 0 ? _y : 'none'} → ${(_0 = (_z = lastStep === null || lastStep === void 0 ? void 0 : lastStep.focusAfter) === null || _z === void 0 ? void 0 : _z.id) !== null && _0 !== void 0 ? _0 : 'none'}`);
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:** ${(_1 = initialFocusData === null || initialFocusData === void 0 ? void 0 : initialFocusData.id) !== null && _1 !== void 0 ? _1 : 'none'}`);
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 = ((_2 = step.focusBefore) === null || _2 === void 0 ? void 0 : _2.id) || ((_3 = step.focusBefore) === null || _3 === void 0 ? void 0 : _3._hid) || 'none';
4119
- const toId = ((_4 = step.focusAfter) === null || _4 === void 0 ? void 0 : _4.id) || ((_5 = step.focusAfter) === null || _5 === void 0 ? void 0 : _5._hid) || 'none';
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.getAppLogs({ level, sinceMs, limit, sessionId, appId });
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 matches = client.searchAppLogs(pattern, caseSensitive, appId).slice(-limit);
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.clearAppLogs(appId);
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
  };