@vscode/component-explorer-cli 0.2.1-25 → 0.2.1-27

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 (72) hide show
  1. package/README.md +6 -6
  2. package/dist/_virtual/_build-info.js +1 -1
  3. package/dist/browserPage.d.ts +49 -0
  4. package/dist/browserPage.d.ts.map +1 -1
  5. package/dist/browserPage.js +75 -0
  6. package/dist/browserPage.js.map +1 -1
  7. package/dist/commands/acceptCommand.d.ts +1 -1
  8. package/dist/commands/acceptCommand.d.ts.map +1 -1
  9. package/dist/commands/acceptCommand.js +12 -5
  10. package/dist/commands/acceptCommand.js.map +1 -1
  11. package/dist/commands/artifactCommand.d.ts +3 -1
  12. package/dist/commands/artifactCommand.d.ts.map +1 -1
  13. package/dist/commands/checkStabilityCommand.d.ts +2 -0
  14. package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
  15. package/dist/commands/checkStabilityCommand.js +17 -1
  16. package/dist/commands/checkStabilityCommand.js.map +1 -1
  17. package/dist/commands/compareCommand.d.ts +1 -1
  18. package/dist/commands/compareCommand.d.ts.map +1 -1
  19. package/dist/commands/compareCommand.js +12 -4
  20. package/dist/commands/compareCommand.js.map +1 -1
  21. package/dist/commands/inputArg.d.ts +33 -0
  22. package/dist/commands/inputArg.d.ts.map +1 -0
  23. package/dist/commands/inputArg.js +103 -0
  24. package/dist/commands/inputArg.js.map +1 -0
  25. package/dist/commands/renderCommand.d.ts +9 -1
  26. package/dist/commands/renderCommand.d.ts.map +1 -1
  27. package/dist/commands/renderCommand.js +300 -15
  28. package/dist/commands/renderCommand.js.map +1 -1
  29. package/dist/commands/watchCommand.d.ts +1 -0
  30. package/dist/commands/watchCommand.d.ts.map +1 -1
  31. package/dist/commands/watchCommand.js +22 -12
  32. package/dist/commands/watchCommand.js.map +1 -1
  33. package/dist/component-explorer-config.schema.json +53 -0
  34. package/dist/componentExplorer.d.ts +98 -7
  35. package/dist/componentExplorer.d.ts.map +1 -1
  36. package/dist/componentExplorer.js +133 -8
  37. package/dist/componentExplorer.js.map +1 -1
  38. package/dist/config.d.ts +27 -0
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +42 -5
  41. package/dist/config.js.map +1 -1
  42. package/dist/coverage.d.ts +63 -0
  43. package/dist/coverage.d.ts.map +1 -0
  44. package/dist/coverage.js +212 -0
  45. package/dist/coverage.js.map +1 -0
  46. package/dist/daemon/DaemonService.d.ts +14 -5
  47. package/dist/daemon/DaemonService.d.ts.map +1 -1
  48. package/dist/daemon/DaemonService.js +32 -9
  49. package/dist/daemon/DaemonService.js.map +1 -1
  50. package/dist/daemon/version.d.ts +1 -1
  51. package/dist/daemon/version.js +1 -1
  52. package/dist/index.js +10 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/manifest.schema.json +38 -16
  55. package/dist/mcp/McpServer.d.ts.map +1 -1
  56. package/dist/mcp/McpServer.js +143 -72
  57. package/dist/mcp/McpServer.js.map +1 -1
  58. package/dist/packages/common/dist/renderManifest.js +26 -5
  59. package/dist/packages/common/dist/renderManifest.js.map +1 -1
  60. package/dist/server/serverConfig.d.ts +17 -0
  61. package/dist/server/serverConfig.d.ts.map +1 -1
  62. package/dist/server/serverConfig.js +2 -0
  63. package/dist/server/serverConfig.js.map +1 -1
  64. package/dist/server/viteServer.d.ts +15 -0
  65. package/dist/server/viteServer.d.ts.map +1 -1
  66. package/dist/server/viteServer.js +83 -0
  67. package/dist/server/viteServer.js.map +1 -1
  68. package/dist/utils.d.ts +0 -1
  69. package/dist/utils.d.ts.map +1 -1
  70. package/dist/utils.js +1 -5
  71. package/dist/utils.js.map +1 -1
  72. package/package.json +6 -1
@@ -235,12 +235,12 @@ class ComponentExplorerMcpServer extends Disposable {
235
235
  _registerListFixtures() {
236
236
  this._mcp.registerTool('list_fixtures', {
237
237
  description: 'List all fixtures from a session',
238
- inputSchema: {
238
+ inputSchema: z.strictObject({
239
239
  fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
240
240
  labelRegex: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
241
241
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
242
242
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
243
- },
243
+ }),
244
244
  annotations: { readOnlyHint: true },
245
245
  }, async (args) => this._withDaemon(async (daemon) => {
246
246
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -264,14 +264,22 @@ class ComponentExplorerMcpServer extends Disposable {
264
264
  _registerScreenshot() {
265
265
  this._mcp.registerTool('screenshot', {
266
266
  description: 'Take a screenshot of a single fixture. ' +
267
- 'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ',
268
- inputSchema: {
267
+ 'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ' +
268
+ 'By default the fixture stays mounted after the screenshot (so a follow-up `evaluate_js` call can still operate on it); ' +
269
+ 'pass `disposeAfter: true` to dispose immediately and surface any teardown errors as `currentDispose` in the result. ' +
270
+ 'Errors / events captured while disposing the previously-mounted fixture (the implicit dispose at the start of every render) are reported as `previousDispose`.',
271
+ inputSchema: z.strictObject({
269
272
  fixtureId: z.string().describe('The fixture ID'),
270
273
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
271
274
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
272
275
  stabilityCheck: z.boolean().optional().describe('If true, takes three screenshots (at render time, ~500ms, ~3000ms) to check visual stability. Expensive (~3s extra) — only use it when you need to verify rendering stability..'),
273
276
  includeConsoleLog: z.boolean().optional().describe('If true, include console.log events in the returned events array. Defaults to false — console.log entries are filtered out to reduce noise.'),
274
- },
277
+ input: z.unknown().optional().describe('Arbitrary JSON-serializable data passed to the fixture as `RenderContext.input`. ' +
278
+ 'The fixture decides how to interpret it (e.g. theme switch, scenario selection, mock data).'),
279
+ disposeAfter: z.boolean().optional().describe('If true, dispose the fixture after taking the screenshot. ' +
280
+ 'Default: false — the fixture stays mounted until the next render, so `evaluate_js` can still operate on it. ' +
281
+ 'Use this when validating fixture teardown: dispose errors / events appear in `currentDispose`.'),
282
+ }),
275
283
  annotations: { readOnlyHint: true },
276
284
  }, async (args) => this._withDaemon(async (daemon) => {
277
285
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -285,6 +293,8 @@ class ComponentExplorerMcpServer extends Disposable {
285
293
  sourceTreeId,
286
294
  includeImage: true,
287
295
  stabilityCheck: args.stabilityCheck,
296
+ input: args.input,
297
+ disposeAfter: args.disposeAfter,
288
298
  });
289
299
  const r = result;
290
300
  this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
@@ -292,6 +302,7 @@ class ComponentExplorerMcpServer extends Disposable {
292
302
  if (r.hash && r.image) {
293
303
  this._imageLru.put(r.hash, r.image);
294
304
  }
305
+ const filterEvents = (events) => args.includeConsoleLog ? events : events.filter(e => e.type !== 'console.log');
295
306
  const info = {
296
307
  hash: r.hash,
297
308
  sourceTreeId: r.sourceTreeId,
@@ -303,19 +314,37 @@ class ComponentExplorerMcpServer extends Disposable {
303
314
  info.error = r.error;
304
315
  }
305
316
  if (r.events && r.events.length > 0) {
306
- const filtered = args.includeConsoleLog
307
- ? r.events
308
- : r.events.filter(e => e.type !== 'console.log');
317
+ const filtered = filterEvents(r.events);
309
318
  if (filtered.length > 0) {
310
319
  info.events = filtered;
311
320
  }
312
321
  }
313
- if (r.resultData !== undefined) {
314
- info.resultData = r.resultData;
322
+ if (r.output !== undefined) {
323
+ info.output = r.output;
315
324
  }
316
325
  if (r.isStable !== undefined) {
317
326
  info.isStable = r.isStable;
318
327
  }
328
+ if (r.previousDispose) {
329
+ const filtered = filterEvents(r.previousDispose.events);
330
+ if (r.previousDispose.hasError || r.previousDispose.errors.length > 0 || filtered.length > 0) {
331
+ info.previousDispose = {
332
+ hasError: r.previousDispose.hasError,
333
+ errors: r.previousDispose.errors,
334
+ events: filtered,
335
+ };
336
+ }
337
+ }
338
+ if (r.currentDispose) {
339
+ const filtered = filterEvents(r.currentDispose.events);
340
+ if (r.currentDispose.hasError || r.currentDispose.errors.length > 0 || filtered.length > 0) {
341
+ info.currentDispose = {
342
+ hasError: r.currentDispose.hasError,
343
+ errors: r.currentDispose.errors,
344
+ events: filtered,
345
+ };
346
+ }
347
+ }
319
348
  // Visual review status
320
349
  if (r.hash) {
321
350
  try {
@@ -359,13 +388,13 @@ class ComponentExplorerMcpServer extends Disposable {
359
388
  _registerCompareScreenshot() {
360
389
  const tool = this._mcp.registerTool('compare_screenshot', {
361
390
  description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
362
- inputSchema: {
391
+ inputSchema: z.strictObject({
363
392
  fixtureId: z.string().describe('The fixture ID'),
364
393
  baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
365
394
  currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
366
395
  baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
367
396
  currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
368
- },
397
+ }),
369
398
  annotations: { readOnlyHint: true },
370
399
  }, async (args) => this._withDaemon(async (daemon) => {
371
400
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
@@ -399,8 +428,8 @@ class ComponentExplorerMcpServer extends Disposable {
399
428
  if (r.baselineEvents && r.baselineEvents.length > 0) {
400
429
  info.baselineEvents = r.baselineEvents;
401
430
  }
402
- if (r.baselineResultData !== undefined) {
403
- info.baselineResultData = r.baselineResultData;
431
+ if (r.baselineOutput !== undefined) {
432
+ info.baselineOutput = r.baselineOutput;
404
433
  }
405
434
  if (r.currentHasError) {
406
435
  info.currentHasError = true;
@@ -411,8 +440,8 @@ class ComponentExplorerMcpServer extends Disposable {
411
440
  if (r.currentEvents && r.currentEvents.length > 0) {
412
441
  info.currentEvents = r.currentEvents;
413
442
  }
414
- if (r.currentResultData !== undefined) {
415
- info.currentResultData = r.currentResultData;
443
+ if (r.currentOutput !== undefined) {
444
+ info.currentOutput = r.currentOutput;
416
445
  }
417
446
  if (r.approval) {
418
447
  info.approval = r.approval;
@@ -435,12 +464,12 @@ class ComponentExplorerMcpServer extends Disposable {
435
464
  _registerApproveDiff() {
436
465
  const tool = this._mcp.registerTool('approve_diff', {
437
466
  description: 'Approve a visual diff so it won\'t require re-inspection next time',
438
- inputSchema: {
467
+ inputSchema: z.strictObject({
439
468
  fixtureId: z.string(),
440
469
  originalHash: z.string(),
441
470
  modifiedHash: z.string(),
442
471
  comment: z.string().describe('Reason for approving this diff'),
443
- },
472
+ }),
444
473
  }, async (args) => this._withDaemon(async (daemon) => {
445
474
  await daemon.methods.approvals.approve(args);
446
475
  return {
@@ -458,14 +487,14 @@ class ComponentExplorerMcpServer extends Disposable {
458
487
  description: 'Approve or reject a fixture\'s screenshot based on its expectedVisualDescriptions. ' +
459
488
  'You must take a screenshot first and pass the resulting hash. ' +
460
489
  'On approve, caches (expectedVisualDescriptions, screenshotHash) so future runs auto-approve.',
461
- inputSchema: {
490
+ inputSchema: z.strictObject({
462
491
  fixtureId: z.string().describe('The fixture ID'),
463
492
  screenshotHash: z.string().describe('The screenshot hash (from a prior screenshot tool call)'),
464
493
  verdict: z.enum(['approve', 'reject']).describe('Whether the visual matches expectations'),
465
494
  comment: z.string().describe('Reason for the verdict'),
466
495
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
467
496
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
468
- },
497
+ }),
469
498
  }, async (args) => this._withDaemon(async (daemon) => {
470
499
  const sessionName = args.sessionName ?? this._defaultSessionName();
471
500
  this._log('debug', { type: 'tool-call', tool: 'review_visual', fixtureId: args.fixtureId, verdict: args.verdict });
@@ -511,12 +540,12 @@ class ComponentExplorerMcpServer extends Disposable {
511
540
  this._mcp.registerTool('check_visuals', {
512
541
  description: 'Batch check visual review status for fixtures that have expectedVisualDescription. ' +
513
542
  'Returns lists of approved, needs-review, and no-expectation fixtures.',
514
- inputSchema: {
543
+ inputSchema: z.strictObject({
515
544
  fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
516
545
  labelRegex: z.string().optional().describe('RegExp to filter fixtures by label'),
517
546
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
518
547
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
519
- },
548
+ }),
520
549
  annotations: { readOnlyHint: true },
521
550
  }, async (args) => this._withDaemon(async (daemon) => {
522
551
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -585,12 +614,12 @@ class ComponentExplorerMcpServer extends Disposable {
585
614
  'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
586
615
  'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
587
616
  'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
588
- inputSchema: {
617
+ inputSchema: z.strictObject({
589
618
  expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
590
619
  fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
591
620
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
592
621
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
593
- },
622
+ }),
594
623
  }, async (args) => this._withDaemon(async (daemon) => {
595
624
  const sessionName = args.sessionName ?? this._defaultSessionName();
596
625
  this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
@@ -622,9 +651,9 @@ class ComponentExplorerMcpServer extends Disposable {
622
651
  description: 'Force-reload the browser page used for rendering fixtures. ' +
623
652
  'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
624
653
  'that persist after source changes. Normal HMR updates should handle most cases automatically.',
625
- inputSchema: {
654
+ inputSchema: z.strictObject({
626
655
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
627
- },
656
+ }),
628
657
  annotations: { destructiveHint: true },
629
658
  }, async (args) => this._withDaemon(async (daemon) => {
630
659
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -642,9 +671,9 @@ class ComponentExplorerMcpServer extends Disposable {
642
671
  _registerWatchAdd() {
643
672
  this._mcp.registerTool('watch_add', {
644
673
  description: 'Add fixtures to the watch list. Watched fixtures are automatically re-screenshotted when source changes.',
645
- inputSchema: {
674
+ inputSchema: z.strictObject({
646
675
  fixtureIds: z.array(z.string()).describe('Fixture IDs to add'),
647
- },
676
+ }),
648
677
  }, async (args) => {
649
678
  this._watchList.add(args.fixtureIds);
650
679
  return {
@@ -658,9 +687,9 @@ class ComponentExplorerMcpServer extends Disposable {
658
687
  _registerWatchRemove() {
659
688
  this._mcp.registerTool('watch_remove', {
660
689
  description: 'Remove fixtures from the watch list',
661
- inputSchema: {
690
+ inputSchema: z.strictObject({
662
691
  fixtureIds: z.array(z.string()).describe('Fixture IDs to remove'),
663
- },
692
+ }),
664
693
  }, async (args) => {
665
694
  this._watchList.remove(args.fixtureIds);
666
695
  return {
@@ -674,9 +703,9 @@ class ComponentExplorerMcpServer extends Disposable {
674
703
  _registerWatchSet() {
675
704
  this._mcp.registerTool('watch_set', {
676
705
  description: 'Replace the watch list entirely',
677
- inputSchema: {
706
+ inputSchema: z.strictObject({
678
707
  fixtureIds: z.array(z.string()).describe('Fixture IDs to watch'),
679
- },
708
+ }),
680
709
  }, async (args) => {
681
710
  this._watchList.set(args.fixtureIds);
682
711
  return {
@@ -690,12 +719,12 @@ class ComponentExplorerMcpServer extends Disposable {
690
719
  _registerWatchCompare() {
691
720
  const tool = this._mcp.registerTool('watch_compare', {
692
721
  description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
693
- inputSchema: {
722
+ inputSchema: z.strictObject({
694
723
  baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
695
724
  currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
696
725
  baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
697
726
  currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
698
- },
727
+ }),
699
728
  annotations: { readOnlyHint: true },
700
729
  }, async (args) => this._withDaemon(async (daemon) => {
701
730
  const ids = [...this._watchList.fixtureIds];
@@ -760,10 +789,10 @@ class ComponentExplorerMcpServer extends Disposable {
760
789
  'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
761
790
  'otherwise waits for a source-change or ref-change event. ' +
762
791
  'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
763
- inputSchema: {
792
+ inputSchema: z.strictObject({
764
793
  sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
765
794
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
766
- },
795
+ }),
767
796
  annotations: { readOnlyHint: true },
768
797
  }, async (args) => this._withDaemon(async (daemon) => {
769
798
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -865,9 +894,9 @@ class ComponentExplorerMcpServer extends Disposable {
865
894
  this._mcp.registerTool('restart_session', {
866
895
  description: 'Restart a session by disposing its browser page and dev server, then recreating them. ' +
867
896
  'Use this when a session appears stuck (e.g. after a timeout).',
868
- inputSchema: {
897
+ inputSchema: z.strictObject({
869
898
  sessionName: z.string().optional().describe('Session name to restart (defaults to first session)'),
870
- },
899
+ }),
871
900
  annotations: { destructiveHint: true },
872
901
  }, async (args) => this._withDaemon(async (daemon) => {
873
902
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -885,10 +914,10 @@ class ComponentExplorerMcpServer extends Disposable {
885
914
  'The ref can be a branch name, tag, commit SHA, or the special value "INDEX" to snapshot staged changes. ' +
886
915
  'The daemon allocates a reusable worktree slot from a fixed pool (max configured in component-explorer.json). ' +
887
916
  'Returns the updated session list on success.',
888
- inputSchema: {
917
+ inputSchema: z.strictObject({
889
918
  name: z.string().describe('Unique session name (e.g. "baseline", "bisect")'),
890
919
  ref: z.string().describe('Git ref: branch, tag, commit SHA, or "INDEX" for staged changes'),
891
- },
920
+ }),
892
921
  }, async (args) => this._withDaemon(async (daemon) => {
893
922
  this._log('info', { type: 'tool-call', tool: 'open_session', name: args.name, ref: args.ref });
894
923
  const result = await daemon.methods.openSession({ name: args.name, ref: args.ref });
@@ -906,9 +935,9 @@ class ComponentExplorerMcpServer extends Disposable {
906
935
  this._mcp.registerTool('close_session', {
907
936
  description: 'Close a dynamic worktree session and release its worktree slot back to the pool. ' +
908
937
  'Cannot close static sessions configured in component-explorer.json.',
909
- inputSchema: {
938
+ inputSchema: z.strictObject({
910
939
  name: z.string().describe('Session name to close'),
911
- },
940
+ }),
912
941
  annotations: { destructiveHint: true },
913
942
  }, async (args) => this._withDaemon(async (daemon) => {
914
943
  this._log('info', { type: 'tool-call', tool: 'close_session', name: args.name });
@@ -929,10 +958,10 @@ class ComponentExplorerMcpServer extends Disposable {
929
958
  'The worktree is checked out to the new ref and Vite\'s HMR handles the incremental update (no server restart). ' +
930
959
  'Fails if the worktree has uncommitted changes — the error will list the dirty files. ' +
931
960
  'The ref can be a branch, tag, commit SHA, or "INDEX" for staged changes.',
932
- inputSchema: {
961
+ inputSchema: z.strictObject({
933
962
  name: z.string().describe('Session name to update'),
934
963
  ref: z.string().describe('New git ref: branch, tag, commit SHA, or "INDEX"'),
935
- },
964
+ }),
936
965
  }, async (args) => this._withDaemon(async (daemon) => {
937
966
  this._log('info', { type: 'tool-call', tool: 'update_session_ref', name: args.name, ref: args.ref });
938
967
  const result = await daemon.methods.updateSessionRef({ name: args.name, ref: args.ref });
@@ -949,11 +978,11 @@ class ComponentExplorerMcpServer extends Disposable {
949
978
  this._mcp.registerTool('get_url', {
950
979
  description: 'Get URL for viewing fixtures. Returns the full Component Explorer UI by default. ' +
951
980
  'Use `embedded: true` for a minimal single-fixture view (requires fixtureId).',
952
- inputSchema: {
981
+ inputSchema: z.strictObject({
953
982
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
954
983
  fixtureId: z.string().optional().describe('Specific fixture ID to view. In explorer mode, pre-selects this fixture. In embedded mode (required), shows only this fixture.'),
955
984
  embedded: z.boolean().optional().describe('If true, returns an embedded single-fixture URL (minimal UI, requires fixtureId). Default: false (full explorer UI).'),
956
- },
985
+ }),
957
986
  annotations: { readOnlyHint: true },
958
987
  }, async (args) => this._withDaemon(async (daemon) => {
959
988
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -1013,14 +1042,23 @@ class ComponentExplorerMcpServer extends Disposable {
1013
1042
  }
1014
1043
  _registerCheckFixtureErrors() {
1015
1044
  this._mcp.registerTool('check_fixture_errors', {
1016
- description: 'Render fixtures and check for errors. Returns each fixture\'s error status. ' +
1045
+ description: 'Render fixtures and check for errors. Each fixture is rendered and then disposed; ' +
1046
+ 'render exceptions appear as `error`, dispose exceptions as `disposeError`, and console / window ' +
1047
+ 'events from both phases are collected in `events` (each tagged with `phase: "render" | "dispose"`). ' +
1048
+ 'A fixture is considered errored if either the render or the dispose phase reports `hasError` ' +
1049
+ '(i.e. an exception was thrown or an event of type `console.error` / `window.error` / `window.unhandledrejection` was captured). ' +
1050
+ 'Only errored fixtures are listed; successful fixtures are summarised via the `summary` counts. ' +
1017
1051
  'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task.',
1018
- inputSchema: {
1052
+ inputSchema: z.strictObject({
1019
1053
  fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
1020
1054
  labelRegex: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
1021
1055
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
1022
1056
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
1023
- },
1057
+ reloadBetweenFixtures: z.boolean().optional().describe('If true, reload the browser page between fixtures. Provides cleaner isolation but is slower. ' +
1058
+ 'Default: false (the page is only reloaded after a fixture errors).'),
1059
+ input: z.unknown().optional().describe('Arbitrary JSON object passed to every fixture as `RenderContext.input`. ' +
1060
+ 'See `screenshot` for details.'),
1061
+ }),
1024
1062
  annotations: { readOnlyHint: true },
1025
1063
  }, async (args) => this._withDaemon(async (daemon) => {
1026
1064
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -1037,8 +1075,12 @@ class ComponentExplorerMcpServer extends Disposable {
1037
1075
  }
1038
1076
  const fixtures = filtered.fixtures;
1039
1077
  const task = this._taskManager.startTask(async (report, signal) => {
1040
- const results = [];
1041
- report({ completed: 0, total: fixtures.length, partialResult: results });
1078
+ // Only errored fixtures are stored/reported; successful fixtures would just bloat the response.
1079
+ // The summary below still surfaces the total/ok/errored counts.
1080
+ const erroredResults = [];
1081
+ let okCount = 0;
1082
+ let processed = 0;
1083
+ report({ completed: 0, total: fixtures.length, partialResult: erroredResults });
1042
1084
  for (let i = 0; i < fixtures.length; i++) {
1043
1085
  if (signal.aborted) {
1044
1086
  break;
@@ -1050,30 +1092,52 @@ class ComponentExplorerMcpServer extends Disposable {
1050
1092
  sessionName,
1051
1093
  sourceTreeId,
1052
1094
  includeImage: false,
1095
+ // Skip the reload before the very first fixture — the page is already fresh.
1096
+ reloadBeforeRender: args.reloadBetweenFixtures && i > 0,
1097
+ input: args.input,
1098
+ // Always dispose after rendering so teardown errors / events are observable for every fixture
1099
+ // (including the last one). Because we explicitly dispose here, the implicit
1100
+ // `previousDispose` at the start of the next render is a no-op and need not be inspected.
1101
+ disposeAfter: true,
1053
1102
  });
1054
1103
  const r = result;
1055
- const entry = { fixtureId: fixture.fixtureId, hasError: r.hasError };
1056
- if (r.error) {
1057
- entry.error = r.error;
1104
+ const renderEvents = (r.events ?? []).map(e => ({ phase: 'render', ...e }));
1105
+ const disposeEvents = (r.currentDispose?.events ?? []).map(e => ({ phase: 'dispose', ...e }));
1106
+ const events = [...renderEvents, ...disposeEvents];
1107
+ const disposeError = r.currentDispose?.errors[0];
1108
+ // Trust the upstream `hasError` flags so this stays in sync with the render-report rules
1109
+ // (exception OR an event of type console.error / window.error / window.unhandledrejection).
1110
+ const hasError = r.hasError || !!r.currentDispose?.hasError;
1111
+ if (hasError) {
1112
+ const entry = { fixtureId: fixture.fixtureId, hasError: true };
1113
+ if (r.error) {
1114
+ entry.error = r.error;
1115
+ }
1116
+ if (disposeError) {
1117
+ entry.disposeError = disposeError;
1118
+ }
1119
+ if (events.length > 0) {
1120
+ entry.events = events;
1121
+ }
1122
+ erroredResults.push(entry);
1058
1123
  }
1059
- if (r.events && r.events.length > 0) {
1060
- entry.events = r.events;
1124
+ else {
1125
+ okCount++;
1061
1126
  }
1062
- results.push(entry);
1063
1127
  }
1064
1128
  catch (e) {
1065
- results.push({
1129
+ erroredResults.push({
1066
1130
  fixtureId: fixture.fixtureId,
1067
1131
  hasError: true,
1068
1132
  error: { message: e instanceof Error ? e.message : String(e) },
1069
1133
  });
1070
1134
  }
1071
- report({ completed: i + 1, total: fixtures.length, partialResult: results });
1135
+ processed++;
1136
+ report({ completed: i + 1, total: fixtures.length, partialResult: erroredResults });
1072
1137
  }
1073
- const withErrors = results.filter(r => r.hasError);
1074
1138
  return {
1075
- fixtures: results,
1076
- summary: { total: results.length, ok: results.length - withErrors.length, errored: withErrors.length },
1139
+ fixtures: erroredResults,
1140
+ summary: { total: processed, ok: okCount, errored: erroredResults.length },
1077
1141
  };
1078
1142
  });
1079
1143
  const waited = await this._taskManager.waitForTask(task.id, 10_000);
@@ -1107,12 +1171,16 @@ class ComponentExplorerMcpServer extends Disposable {
1107
1171
  description: 'Check rendering stability of fixtures. Each fixture is unmounted, re-mounted, and screenshotted 3 times (~3s per fixture). ' +
1108
1172
  'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task. ' +
1109
1173
  'When returning a taskId, includes partial results collected so far.',
1110
- inputSchema: {
1174
+ inputSchema: z.strictObject({
1111
1175
  fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
1112
1176
  labelRegex: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
1113
1177
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
1114
1178
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
1115
- },
1179
+ reloadBetweenFixtures: z.boolean().optional().describe('If true, reload the browser page between fixtures. Provides cleaner isolation but is slower. ' +
1180
+ 'Default: false (the page is only reloaded after a fixture errors).'),
1181
+ input: z.unknown().optional().describe('Arbitrary JSON object passed to every fixture as `RenderContext.input`. ' +
1182
+ 'See `screenshot` for details.'),
1183
+ }),
1116
1184
  annotations: { readOnlyHint: true },
1117
1185
  }, async (args) => this._withDaemon(async (daemon) => {
1118
1186
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -1144,6 +1212,9 @@ class ComponentExplorerMcpServer extends Disposable {
1144
1212
  sourceTreeId,
1145
1213
  includeImage: false,
1146
1214
  stabilityCheck: true,
1215
+ // Skip the reload before the very first fixture — the page is already fresh.
1216
+ reloadBeforeRender: args.reloadBetweenFixtures && i > 0,
1217
+ input: args.input,
1147
1218
  });
1148
1219
  const r = result;
1149
1220
  results.push({
@@ -1188,9 +1259,9 @@ class ComponentExplorerMcpServer extends Disposable {
1188
1259
  _registerCheckTask() {
1189
1260
  this._mcp.registerTool('check_task', {
1190
1261
  description: 'Check on a running task. Waits up to ~2s for completion; if still running, returns progress and new results since last check.',
1191
- inputSchema: {
1262
+ inputSchema: z.strictObject({
1192
1263
  taskId: z.string().describe('The task ID returned by a previous tool call'),
1193
- },
1264
+ }),
1194
1265
  annotations: { readOnlyHint: true },
1195
1266
  }, async (args) => {
1196
1267
  const waited = await this._taskManager.waitForTask(args.taskId, 2_000);
@@ -1238,9 +1309,9 @@ class ComponentExplorerMcpServer extends Disposable {
1238
1309
  _registerCancelTask() {
1239
1310
  this._mcp.registerTool('cancel_task', {
1240
1311
  description: 'Cancel a running task',
1241
- inputSchema: {
1312
+ inputSchema: z.strictObject({
1242
1313
  taskId: z.string().describe('The task ID to cancel'),
1243
- },
1314
+ }),
1244
1315
  }, async (args) => {
1245
1316
  const task = this._taskManager.getTask(args.taskId);
1246
1317
  if (!task) {
@@ -1260,9 +1331,9 @@ class ComponentExplorerMcpServer extends Disposable {
1260
1331
  description: 'Retrieve a recently-taken screenshot image by its hash. ' +
1261
1332
  'Keeps the last ~10 images in an LRU cache. ' +
1262
1333
  'Useful for debugging when screenshot hashes behave unexpectedly.',
1263
- inputSchema: {
1334
+ inputSchema: z.strictObject({
1264
1335
  hash: z.string().describe('The screenshot hash to look up'),
1265
- },
1336
+ }),
1266
1337
  annotations: { readOnlyHint: true },
1267
1338
  }, async (args) => {
1268
1339
  const image = this._imageLru.get(args.hash);
@@ -1286,9 +1357,9 @@ class ComponentExplorerMcpServer extends Disposable {
1286
1357
  'Only use this tool when the user explicitly asks to show or hide the browser. ' +
1287
1358
  'Do not call this tool automatically or as part of other workflows. ' +
1288
1359
  'Note: changing visibility closes the current browser instance, so the next screenshot or evaluate_js call will relaunch it.',
1289
- inputSchema: {
1360
+ inputSchema: z.strictObject({
1290
1361
  visible: z.boolean().describe('true to show the browser window (headed mode), false to hide it (headless mode)'),
1291
- },
1362
+ }),
1292
1363
  annotations: { destructiveHint: true },
1293
1364
  }, async (args) => this._withDaemon(async (daemon) => {
1294
1365
  await daemon.methods.setBrowserVisibility({ visible: args.visible });