@vscode/component-explorer-cli 0.1.1-6 → 0.1.1-8

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 (77) hide show
  1. package/README.md +4 -5
  2. package/dist/commands/compareCommand.d.ts +0 -3
  3. package/dist/commands/compareCommand.d.ts.map +1 -1
  4. package/dist/commands/compareCommand.js +18 -210
  5. package/dist/commands/compareCommand.js.map +1 -1
  6. package/dist/commands/mcpCommand.d.ts +2 -0
  7. package/dist/commands/mcpCommand.d.ts.map +1 -1
  8. package/dist/commands/mcpCommand.js +35 -6
  9. package/dist/commands/mcpCommand.js.map +1 -1
  10. package/dist/commands/screenshotCommand.d.ts.map +1 -1
  11. package/dist/commands/screenshotCommand.js +21 -75
  12. package/dist/commands/screenshotCommand.js.map +1 -1
  13. package/dist/commands/serveCommand.d.ts +3 -0
  14. package/dist/commands/serveCommand.d.ts.map +1 -1
  15. package/dist/commands/serveCommand.js +52 -3
  16. package/dist/commands/serveCommand.js.map +1 -1
  17. package/dist/comparison.d.ts +60 -0
  18. package/dist/comparison.d.ts.map +1 -0
  19. package/dist/comparison.js +250 -0
  20. package/dist/comparison.js.map +1 -0
  21. package/dist/component-explorer-config.schema.json +183 -0
  22. package/dist/componentExplorer.d.ts +13 -0
  23. package/dist/componentExplorer.d.ts.map +1 -1
  24. package/dist/componentExplorer.js +65 -2
  25. package/dist/componentExplorer.js.map +1 -1
  26. package/dist/daemon/DaemonService.d.ts +55 -4
  27. package/dist/daemon/DaemonService.d.ts.map +1 -1
  28. package/dist/daemon/DaemonService.js +139 -8
  29. package/dist/daemon/DaemonService.js.map +1 -1
  30. package/dist/daemon/lifecycle.js +3 -3
  31. package/dist/daemon/lifecycle.js.map +1 -1
  32. package/dist/daemon/pipeClient.js +3 -3
  33. package/dist/daemon/pipeClient.js.map +1 -1
  34. package/dist/daemon/pipeServer.js +2 -2
  35. package/dist/daemon/pipeServer.js.map +1 -1
  36. package/dist/dependencyInstaller.js +1 -1
  37. package/dist/dependencyInstaller.js.map +1 -1
  38. package/dist/external/vscode-observables/observables/dist/disposables.js +24 -1
  39. package/dist/external/vscode-observables/observables/dist/disposables.js.map +1 -1
  40. package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js +1 -4
  41. package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +1 -1
  42. package/dist/external/vscode-observables/observables/dist/observableInternal/index.js +2 -5
  43. package/dist/external/vscode-observables/observables/dist/observableInternal/index.js.map +1 -1
  44. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js +30 -6
  45. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +1 -1
  46. package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js +1 -1
  47. package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +1 -1
  48. package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js +12 -1
  49. package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +1 -1
  50. package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js +55 -0
  51. package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +1 -0
  52. package/dist/git/gitUtils.js +1 -1
  53. package/dist/git/gitUtils.js.map +1 -1
  54. package/dist/mcp/McpServer.d.ts +30 -4
  55. package/dist/mcp/McpServer.d.ts.map +1 -1
  56. package/dist/mcp/McpServer.js +435 -80
  57. package/dist/mcp/McpServer.js.map +1 -1
  58. package/dist/packages/simple-api/dist/{chunk-A5PE72HI.js → chunk-Q24JOMNK.js} +7 -1
  59. package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js.map +1 -0
  60. package/dist/utils.d.ts +24 -0
  61. package/dist/utils.d.ts.map +1 -0
  62. package/dist/utils.js +50 -0
  63. package/dist/utils.js.map +1 -0
  64. package/dist/watchConfig.d.ts +36 -0
  65. package/dist/watchConfig.d.ts.map +1 -1
  66. package/dist/watchConfig.js +32 -22
  67. package/dist/watchConfig.js.map +1 -1
  68. package/package.json +6 -4
  69. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/debuggerRpc.js +0 -72
  70. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/debuggerRpc.js.map +0 -1
  71. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/devToolsLogger.js +0 -447
  72. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/devToolsLogger.js.map +0 -1
  73. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/rpc.js +0 -64
  74. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/rpc.js.map +0 -1
  75. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/utils.js +0 -52
  76. package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/utils.js.map +0 -1
  77. package/dist/packages/simple-api/dist/chunk-A5PE72HI.js.map +0 -1
@@ -1,6 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import '../external/vscode-observables/observables/dist/observableInternal/index.js';
5
+ import { Disposable } from '../external/vscode-observables/observables/dist/disposables.js';
6
+ import '../external/vscode-observables/observables/dist/observableInternal/debugLocation.js';
7
+ import { autorun } from '../external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js';
8
+ import '../external/vscode-observables/observables/dist/observableInternal/observables/derived.js';
9
+ import '../external/vscode-observables/observables/dist/observableInternal/utils/utils.js';
10
+ import '../external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
11
+ import { buildExplorerUrl } from '../utils.js';
4
12
 
5
13
  // ---------------------------------------------------------------------------
6
14
  // Client-local state
@@ -40,43 +48,141 @@ class WatchList {
40
48
  };
41
49
  }
42
50
  }
51
+ function noDaemonError(hint) {
52
+ let text = 'Error: No daemon is currently running.';
53
+ if (hint) {
54
+ text += ` ${hint}`;
55
+ }
56
+ else {
57
+ text += ' Please start the Component Explorer daemon first by running:\n\n' +
58
+ ' component-explorer serve --project <config.json>\n\n' +
59
+ 'Or start it in the background:\n\n' +
60
+ ' component-explorer serve --project <config.json> --background\n\n' +
61
+ 'The daemon manages dev servers and enables fixture screenshots.';
62
+ }
63
+ return {
64
+ content: [{ type: 'text', text }],
65
+ isError: true,
66
+ };
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // DaemonConnection - wrapper to avoid Proxy issues with observables
70
+ // ---------------------------------------------------------------------------
71
+ class DaemonConnection {
72
+ client;
73
+ constructor(client) {
74
+ this.client = client;
75
+ }
76
+ }
43
77
  // ---------------------------------------------------------------------------
44
78
  // ComponentExplorerMcpServer
45
79
  // ---------------------------------------------------------------------------
46
- class ComponentExplorerMcpServer {
47
- _daemon;
80
+ class ComponentExplorerMcpServer extends Disposable {
81
+ _daemonConnection;
82
+ static async create(daemon, options) {
83
+ const server = new ComponentExplorerMcpServer(daemon, options ?? {});
84
+ const transport = new StdioServerTransport();
85
+ await server._mcp.connect(transport);
86
+ return server;
87
+ }
48
88
  _mcp;
49
89
  _watchList = new WatchList();
90
+ _pollFn;
91
+ _noAutostartHint;
92
+ _multiSessionTools = [];
50
93
  _sessions = [];
51
- constructor(_daemon) {
52
- this._daemon = _daemon;
94
+ _eventStreamAbortController;
95
+ constructor(_daemonConnection, options) {
96
+ super();
97
+ this._daemonConnection = _daemonConnection;
98
+ this._pollFn = options.pollFn;
99
+ this._noAutostartHint = options.noAutostartHint;
53
100
  this._mcp = new McpServer({
54
101
  name: 'component-explorer',
55
102
  version: '0.1.0',
56
103
  });
57
104
  this._registerTools();
105
+ this._store.add(autorun(async (reader) => {
106
+ const conn = this._daemonConnection.read(reader);
107
+ await this._onDaemonChanged(conn?.client);
108
+ }));
58
109
  }
59
- async connect() {
60
- this._sessions = await this._daemon.methods.sessions();
61
- this._startEventListener();
62
- const transport = new StdioServerTransport();
63
- await this._mcp.connect(transport);
110
+ async _onDaemonChanged(daemon) {
111
+ // Cancel any existing event stream
112
+ this._eventStreamAbortController?.abort();
113
+ this._eventStreamAbortController = undefined;
114
+ if (!daemon) {
115
+ this._sessions = [];
116
+ this._log('info', { type: 'daemon-disconnected' });
117
+ return;
118
+ }
119
+ // Fetch sessions and start event listener
120
+ try {
121
+ this._sessions = await daemon.methods.sessions();
122
+ this._log('info', { type: 'daemon-connected', sessions: this._sessions.length });
123
+ this._updateMultiSessionToolVisibility();
124
+ this._startEventListener(daemon);
125
+ }
126
+ catch (e) {
127
+ this._log('info', { type: 'daemon-error', error: String(e) });
128
+ this._sessions = [];
129
+ }
130
+ }
131
+ _getDaemon() {
132
+ return this._daemonConnection.get()?.client;
133
+ }
134
+ /**
135
+ * If no daemon is connected and we have a pollFn, poll immediately and wait
136
+ * up to 3 seconds for the daemon to become available.
137
+ */
138
+ async _waitForDaemon() {
139
+ let daemon = this._getDaemon();
140
+ if (daemon || !this._pollFn) {
141
+ return daemon;
142
+ }
143
+ // Poll immediately and wait up to 3 seconds
144
+ const startTime = Date.now();
145
+ const timeout = 3000;
146
+ while (Date.now() - startTime < timeout) {
147
+ await this._pollFn();
148
+ daemon = this._getDaemon();
149
+ if (daemon) {
150
+ return daemon;
151
+ }
152
+ // Wait 200ms before next poll
153
+ await new Promise(resolve => setTimeout(resolve, 200));
154
+ }
155
+ return undefined;
156
+ }
157
+ _noDaemonError() {
158
+ return noDaemonError(this._noAutostartHint);
64
159
  }
65
160
  _log(level, data) {
66
161
  this._mcp.sendLoggingMessage({ level, logger: 'daemon', data }).catch(() => { });
67
162
  }
68
- async _startEventListener() {
69
- const stream = await this._daemon.methods.events();
163
+ _startEventListener(daemon) {
164
+ const controller = new AbortController();
165
+ this._eventStreamAbortController = controller;
70
166
  (async () => {
71
- for await (const raw of stream) {
72
- const event = raw;
73
- if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
74
- this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
167
+ try {
168
+ const stream = await daemon.methods.events();
169
+ for await (const raw of stream) {
170
+ if (controller.signal.aborted)
171
+ break;
172
+ const event = raw;
173
+ if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
174
+ this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
175
+ }
176
+ if (event.type === 'ref-change') {
177
+ await this._refreshSessions();
178
+ }
179
+ this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
75
180
  }
76
- if (event.type === 'ref-change') {
77
- await this._refreshSessions();
181
+ }
182
+ catch (e) {
183
+ if (!controller.signal.aborted) {
184
+ this._log('info', { type: 'event-stream-error', error: String(e) });
78
185
  }
79
- this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
80
186
  }
81
187
  })();
82
188
  }
@@ -94,7 +200,7 @@ class ComponentExplorerMcpServer {
94
200
  }
95
201
  _sourceTreeId(sessionName) {
96
202
  const s = this._sessions.find(s => s.name === sessionName);
97
- return s?.sourceTreeId ?? '';
203
+ return s && !s.isLoading ? s.sourceTreeId : '';
98
204
  }
99
205
  _updateSessionSourceTreeId(sessionName, sourceTreeId) {
100
206
  const s = this._sessions.find(s => s.name === sessionName);
@@ -103,7 +209,25 @@ class ComponentExplorerMcpServer {
103
209
  }
104
210
  }
105
211
  async _refreshSessions() {
106
- this._sessions = await this._daemon.methods.sessions();
212
+ const daemon = this._getDaemon();
213
+ if (daemon) {
214
+ const prevCount = this._sessions.length;
215
+ this._sessions = await daemon.methods.sessions();
216
+ if (this._sessions.length !== prevCount) {
217
+ this._updateMultiSessionToolVisibility();
218
+ }
219
+ }
220
+ }
221
+ _updateMultiSessionToolVisibility() {
222
+ const isMultiSession = this._sessions.length > 1;
223
+ for (const tool of this._multiSessionTools) {
224
+ if (isMultiSession) {
225
+ tool.enable();
226
+ }
227
+ else {
228
+ tool.disable();
229
+ }
230
+ }
107
231
  }
108
232
  async _withSourceTreeRetry(fn) {
109
233
  try {
@@ -124,12 +248,15 @@ class ComponentExplorerMcpServer {
124
248
  this._registerScreenshot();
125
249
  this._registerCompareScreenshot();
126
250
  this._registerApproveDiff();
251
+ this._registerEvaluateJs();
252
+ this._registerDebugReloadPage();
127
253
  this._registerWatchAdd();
128
254
  this._registerWatchRemove();
129
255
  this._registerWatchSet();
130
256
  this._registerWatchCompare();
131
257
  this._registerWaitForUpdate();
132
258
  this._registerSessions();
259
+ this._registerGetUrl();
133
260
  }
134
261
  _registerListFixtures() {
135
262
  this._mcp.registerTool('list_fixtures', {
@@ -138,11 +265,16 @@ class ComponentExplorerMcpServer {
138
265
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
139
266
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
140
267
  },
268
+ annotations: { readOnlyHint: true },
141
269
  }, async (args) => {
270
+ const daemon = await this._waitForDaemon();
271
+ if (!daemon) {
272
+ return this._noDaemonError();
273
+ }
142
274
  const sessionName = args.sessionName ?? this._defaultSessionName();
143
275
  return this._withSourceTreeRetry(async () => {
144
276
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
145
- const fixtures = await this._daemon.methods.fixtures.list({ sessionName, sourceTreeId });
277
+ const fixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
146
278
  return {
147
279
  content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
148
280
  };
@@ -151,21 +283,29 @@ class ComponentExplorerMcpServer {
151
283
  }
152
284
  _registerScreenshot() {
153
285
  this._mcp.registerTool('screenshot', {
154
- description: 'Take a screenshot of a single fixture',
286
+ description: 'Take a screenshot of a single fixture. ' +
287
+ 'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ',
155
288
  inputSchema: {
156
289
  fixtureId: z.string().describe('The fixture ID'),
157
290
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
158
291
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
292
+ 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..'),
159
293
  },
294
+ annotations: { readOnlyHint: true },
160
295
  }, async (args) => {
296
+ const daemon = await this._waitForDaemon();
297
+ if (!daemon) {
298
+ return this._noDaemonError();
299
+ }
161
300
  const sessionName = args.sessionName ?? this._defaultSessionName();
162
301
  return this._withSourceTreeRetry(async () => {
163
302
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
164
- const result = await this._daemon.methods.screenshots.take({
303
+ const result = await daemon.methods.screenshots.take({
165
304
  fixtureId: args.fixtureId,
166
305
  sessionName,
167
306
  sourceTreeId,
168
307
  includeImage: true,
308
+ stabilityCheck: args.stabilityCheck,
169
309
  });
170
310
  const r = result;
171
311
  this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
@@ -176,18 +316,34 @@ class ComponentExplorerMcpServer {
176
316
  if (r.errors && r.errors.length > 0) {
177
317
  info.errors = r.errors;
178
318
  }
179
- const content = [
180
- { type: 'text', text: JSON.stringify(info, null, 2) },
181
- ];
182
- if (r.image) {
319
+ if (r.isStable !== undefined) {
320
+ info.isStable = r.isStable;
321
+ }
322
+ const content = [];
323
+ if (r.isStable === false && r.stabilityScreenshots) {
324
+ // Not stable: return all distinct screenshots
325
+ const seenHashes = new Set();
326
+ const screenshotDetails = [];
327
+ for (const s of r.stabilityScreenshots) {
328
+ screenshotDetails.push({ hash: s.hash, delayMs: s.delayMs });
329
+ if (!seenHashes.has(s.hash) && s.image) {
330
+ seenHashes.add(s.hash);
331
+ content.push({ type: 'image', data: s.image, mimeType: 'image/png' });
332
+ }
333
+ }
334
+ info.stabilityScreenshots = screenshotDetails;
335
+ }
336
+ else if (r.image) {
337
+ // Stable or no stability check: return single image
183
338
  content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
184
339
  }
340
+ content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
185
341
  return { content };
186
342
  });
187
343
  });
188
344
  }
189
345
  _registerCompareScreenshot() {
190
- this._mcp.registerTool('compare_screenshot', {
346
+ const tool = this._mcp.registerTool('compare_screenshot', {
191
347
  description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
192
348
  inputSchema: {
193
349
  fixtureId: z.string().describe('The fixture ID'),
@@ -196,13 +352,18 @@ class ComponentExplorerMcpServer {
196
352
  baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
197
353
  currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
198
354
  },
355
+ annotations: { readOnlyHint: true },
199
356
  }, async (args) => {
357
+ const daemon = await this._waitForDaemon();
358
+ if (!daemon) {
359
+ return this._noDaemonError();
360
+ }
200
361
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
201
362
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
202
363
  return this._withSourceTreeRetry(async () => {
203
364
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
204
365
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
205
- const result = await this._daemon.methods.screenshots.compare({
366
+ const result = await daemon.methods.screenshots.compare({
206
367
  fixtureId: args.fixtureId,
207
368
  baselineSessionName,
208
369
  baselineSourceTreeId,
@@ -237,9 +398,11 @@ class ComponentExplorerMcpServer {
237
398
  return { content };
238
399
  });
239
400
  });
401
+ tool.disable();
402
+ this._multiSessionTools.push(tool);
240
403
  }
241
404
  _registerApproveDiff() {
242
- this._mcp.registerTool('approve_diff', {
405
+ const tool = this._mcp.registerTool('approve_diff', {
243
406
  description: 'Approve a visual diff so it won\'t require re-inspection next time',
244
407
  inputSchema: {
245
408
  fixtureId: z.string(),
@@ -248,7 +411,11 @@ class ComponentExplorerMcpServer {
248
411
  comment: z.string().describe('Reason for approving this diff'),
249
412
  },
250
413
  }, async (args) => {
251
- await this._daemon.methods.approvals.approve(args);
414
+ const daemon = await this._waitForDaemon();
415
+ if (!daemon) {
416
+ return this._noDaemonError();
417
+ }
418
+ await daemon.methods.approvals.approve(args);
252
419
  return {
253
420
  content: [{
254
421
  type: 'text',
@@ -256,6 +423,74 @@ class ComponentExplorerMcpServer {
256
423
  }],
257
424
  };
258
425
  });
426
+ tool.disable();
427
+ this._multiSessionTools.push(tool);
428
+ }
429
+ _registerEvaluateJs() {
430
+ this._mcp.registerTool('evaluate_js', {
431
+ description: 'Evaluate a JavaScript expression in the browser page where fixtures are rendered, for debugging purposes. ' +
432
+ 'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
433
+ 'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
434
+ 'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
435
+ inputSchema: {
436
+ expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
437
+ fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
438
+ sessionName: z.string().optional().describe('Session name (defaults to first session)'),
439
+ sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
440
+ },
441
+ }, async (args) => {
442
+ const daemon = await this._waitForDaemon();
443
+ if (!daemon) {
444
+ return this._noDaemonError();
445
+ }
446
+ const sessionName = args.sessionName ?? this._defaultSessionName();
447
+ return this._withSourceTreeRetry(async () => {
448
+ const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
449
+ const result = await daemon.methods.evaluate({
450
+ sessionName,
451
+ sourceTreeId,
452
+ expression: args.expression,
453
+ fixtureId: args.fixtureId,
454
+ });
455
+ const r = result;
456
+ let text;
457
+ try {
458
+ text = JSON.stringify(r.result, null, 2) ?? 'undefined';
459
+ }
460
+ catch {
461
+ text = String(r.result);
462
+ }
463
+ return {
464
+ content: [{ type: 'text', text }],
465
+ };
466
+ });
467
+ });
468
+ }
469
+ _registerDebugReloadPage() {
470
+ this._mcp.registerTool('debug_reload_page', {
471
+ description: 'Force-reload the browser page used for rendering fixtures. ' +
472
+ 'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
473
+ 'that persist after source changes. Normal HMR updates should handle most cases automatically.',
474
+ inputSchema: {
475
+ sessionName: z.string().optional().describe('Session name (defaults to first session)'),
476
+ },
477
+ annotations: { destructiveHint: true },
478
+ }, async (args) => {
479
+ const daemon = await this._waitForDaemon();
480
+ if (!daemon) {
481
+ return this._noDaemonError();
482
+ }
483
+ const sessionName = args.sessionName ?? this._defaultSessionName();
484
+ const sourceTreeId = this._sourceTreeId(sessionName);
485
+ await daemon.methods.evaluate({
486
+ sessionName,
487
+ sourceTreeId,
488
+ expression: 'location.reload()',
489
+ });
490
+ return {
491
+ content: [{ type: 'text', text: `Reloaded page for session '${sessionName}'.` }],
492
+ };
493
+ });
259
494
  }
260
495
  _registerWatchAdd() {
261
496
  this._mcp.registerTool('watch_add', {
@@ -306,7 +541,7 @@ class ComponentExplorerMcpServer {
306
541
  });
307
542
  }
308
543
  _registerWatchCompare() {
309
- this._mcp.registerTool('watch_compare', {
544
+ const tool = this._mcp.registerTool('watch_compare', {
310
545
  description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
311
546
  inputSchema: {
312
547
  baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
@@ -314,7 +549,12 @@ class ComponentExplorerMcpServer {
314
549
  baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
315
550
  currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
316
551
  },
552
+ annotations: { readOnlyHint: true },
317
553
  }, async (args) => {
554
+ const daemon = await this._waitForDaemon();
555
+ if (!daemon) {
556
+ return this._noDaemonError();
557
+ }
318
558
  const ids = [...this._watchList.fixtureIds];
319
559
  if (ids.length === 0) {
320
560
  return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
@@ -325,12 +565,12 @@ class ComponentExplorerMcpServer {
325
565
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
326
566
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
327
567
  const [baselineResult, currentResult] = await Promise.all([
328
- this._daemon.methods.screenshots.takeBatch({
568
+ daemon.methods.screenshots.takeBatch({
329
569
  fixtureIds: ids,
330
570
  sessionName: baselineSessionName,
331
571
  sourceTreeId: baselineSourceTreeId,
332
572
  }),
333
- this._daemon.methods.screenshots.takeBatch({
573
+ daemon.methods.screenshots.takeBatch({
334
574
  fixtureIds: ids,
335
575
  sessionName: currentSessionName,
336
576
  sourceTreeId: currentSourceTreeId,
@@ -355,7 +595,7 @@ class ComponentExplorerMcpServer {
355
595
  for (const entry of entries) {
356
596
  let approval = undefined;
357
597
  if (!entry.match && entry.baselineHash && entry.currentHash) {
358
- approval = await this._daemon.methods.approvals.lookup({
598
+ approval = await daemon.methods.approvals.lookup({
359
599
  fixtureId: entry.fixtureId,
360
600
  originalHash: entry.baselineHash,
361
601
  modifiedHash: entry.currentHash,
@@ -368,72 +608,187 @@ class ComponentExplorerMcpServer {
368
608
  };
369
609
  });
370
610
  });
611
+ tool.disable();
612
+ this._multiSessionTools.push(tool);
371
613
  }
372
614
  _registerWaitForUpdate() {
373
615
  this._mcp.registerTool('wait_for_update', {
374
- description: 'Block until the next source change or ref change event. If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
375
- }, async () => {
376
- const events = await this._daemon.methods.events();
377
- const iterator = events[Symbol.asyncIterator]();
378
- const { value: event, done } = await iterator.next();
379
- // Close the stream after consuming one event
380
- await iterator.return?.();
381
- if (done || !event) {
382
- return { content: [{ type: 'text', text: 'Event stream ended.' }] };
383
- }
384
- const ev = event;
385
- // Update cached session info
386
- if (ev.type === 'source-change' && ev.sourceTreeId) {
387
- this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
616
+ description: 'Block until the source tree changes from the given sourceTreeId. ' +
617
+ 'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
618
+ 'otherwise waits for a source-change or ref-change event. ' +
619
+ 'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
620
+ inputSchema: {
621
+ sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
622
+ sessionName: z.string().optional().describe('Session name (defaults to first session)'),
623
+ },
624
+ annotations: { readOnlyHint: true },
625
+ }, async (args) => {
626
+ const daemon = await this._waitForDaemon();
627
+ if (!daemon) {
628
+ return this._noDaemonError();
388
629
  }
389
- if (ev.type === 'ref-change') {
390
- await this._refreshSessions();
630
+ const sessionName = args.sessionName ?? this._defaultSessionName();
631
+ const knownSourceTreeId = args.sourceTreeId;
632
+ // Check if already changed
633
+ const currentSourceTreeId = this._sourceTreeId(sessionName);
634
+ if (currentSourceTreeId && currentSourceTreeId !== knownSourceTreeId) {
635
+ return this._waitForUpdateResult(daemon, sessionName, currentSourceTreeId);
391
636
  }
392
- // If there are watched fixtures and this is a source-change, re-screenshot them
393
- const watchedIds = [...this._watchList.fixtureIds];
394
- if (ev.type === 'source-change' && watchedIds.length > 0 && ev.sourceTreeId) {
395
- const batchResult = await this._daemon.methods.screenshots.takeBatch({
396
- fixtureIds: watchedIds,
397
- sessionName: ev.sessionName,
398
- sourceTreeId: ev.sourceTreeId,
399
- });
400
- const br = batchResult;
401
- const changes = [];
402
- for (const s of br.screenshots) {
403
- const prevHash = this._watchList.getHash(s.fixtureId);
404
- const changed = prevHash !== undefined && prevHash !== s.hash;
405
- this._watchList.setHash(s.fixtureId, s.hash);
406
- if (changed) {
407
- changes.push({ fixtureId: s.fixtureId, previousHash: prevHash, hash: s.hash });
637
+ // Wait for an event that changes the source tree (max 5s)
638
+ const events = await daemon.methods.events();
639
+ const iterator = events[Symbol.asyncIterator]();
640
+ try {
641
+ const timeout = new Promise(resolve => setTimeout(() => resolve('timeout'), 5000));
642
+ while (true) {
643
+ const next = iterator.next();
644
+ const result = await Promise.race([next, timeout]);
645
+ if (result === 'timeout') {
646
+ return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
647
+ }
648
+ const { value: event, done } = result;
649
+ if (done || !event) {
650
+ return { content: [{ type: 'text', text: 'Event stream ended.' }] };
651
+ }
652
+ const ev = event;
653
+ if (ev.type === 'source-change' && ev.sourceTreeId) {
654
+ this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
655
+ }
656
+ if (ev.type === 'ref-change') {
657
+ await this._refreshSessions();
658
+ }
659
+ const newSourceTreeId = this._sourceTreeId(sessionName);
660
+ if (newSourceTreeId && newSourceTreeId !== knownSourceTreeId) {
661
+ return this._waitForUpdateResult(daemon, sessionName, newSourceTreeId);
408
662
  }
409
663
  }
410
- return {
411
- content: [{
412
- type: 'text',
413
- text: JSON.stringify({
414
- event: ev,
415
- watchedFixtures: br.screenshots.length,
416
- changed: changes,
417
- }, null, 2),
418
- }],
419
- };
664
+ }
665
+ finally {
666
+ await iterator.return?.();
667
+ }
668
+ });
669
+ }
670
+ async _waitForUpdateResult(daemon, sessionName, sourceTreeId) {
671
+ const watchedIds = [...this._watchList.fixtureIds];
672
+ if (watchedIds.length > 0) {
673
+ const batchResult = await daemon.methods.screenshots.takeBatch({
674
+ fixtureIds: watchedIds,
675
+ sessionName,
676
+ sourceTreeId,
677
+ });
678
+ const br = batchResult;
679
+ const changes = [];
680
+ for (const s of br.screenshots) {
681
+ const prevHash = this._watchList.getHash(s.fixtureId);
682
+ const changed = prevHash !== undefined && prevHash !== s.hash;
683
+ this._watchList.setHash(s.fixtureId, s.hash);
684
+ if (changed) {
685
+ changes.push({ fixtureId: s.fixtureId, previousHash: prevHash, hash: s.hash });
686
+ }
420
687
  }
421
688
  return {
422
- content: [{ type: 'text', text: JSON.stringify(ev, null, 2) }],
689
+ content: [{
690
+ type: 'text',
691
+ text: JSON.stringify({
692
+ sourceTreeId,
693
+ sessionName,
694
+ watchedFixtures: br.screenshots.length,
695
+ changed: changes,
696
+ }, null, 2),
697
+ }],
423
698
  };
424
- });
699
+ }
700
+ return {
701
+ content: [{
702
+ type: 'text',
703
+ text: JSON.stringify({ sourceTreeId, sessionName }, null, 2),
704
+ }],
705
+ };
425
706
  }
426
707
  _registerSessions() {
427
708
  this._mcp.registerTool('sessions', {
428
709
  description: 'List active sessions with their names, URLs, and current sourceTreeIds',
710
+ annotations: { readOnlyHint: true },
429
711
  }, async () => {
712
+ const daemon = await this._waitForDaemon();
713
+ if (!daemon) {
714
+ return this._noDaemonError();
715
+ }
430
716
  await this._refreshSessions();
431
717
  return {
432
718
  content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
433
719
  };
434
720
  });
435
721
  }
722
+ _registerGetUrl() {
723
+ this._mcp.registerTool('get_url', {
724
+ description: 'Get URL(s) for viewing fixtures. Returns the Component Explorer UI URL by default, ' +
725
+ 'or the raw render URL for embedding/screenshots when useRawDirectRenderingWithoutExplorerUi is true.',
726
+ inputSchema: {
727
+ sessionName: z.string().optional().describe('Session name (defaults to first session)'),
728
+ fixtureId: z.string().optional().describe('Specific fixture ID. If omitted, returns URL for the explorer root or all fixtures.'),
729
+ useRawDirectRenderingWithoutExplorerUi: z.boolean().optional().describe('If true, returns the raw rendering URL (for embedding or screenshots) instead of the Explorer UI URL. ' +
730
+ 'The raw URL renders only the fixture without the explorer chrome. Default: false.'),
731
+ },
732
+ annotations: { readOnlyHint: true },
733
+ }, async (args) => {
734
+ const sessionName = args.sessionName ?? this._defaultSessionName();
735
+ const session = this._sessions.find(s => s.name === sessionName);
736
+ if (!session) {
737
+ // Try to refresh sessions if we don't have the requested session
738
+ const daemon = await this._waitForDaemon();
739
+ if (daemon) {
740
+ await this._refreshSessions();
741
+ }
742
+ const refreshedSession = this._sessions.find(s => s.name === sessionName);
743
+ if (!refreshedSession) {
744
+ return {
745
+ content: [{
746
+ type: 'text',
747
+ text: `Error: Session '${sessionName}' not found. Available sessions: ${this._sessions.map(s => s.name).join(', ') || '(none)'}`,
748
+ }],
749
+ isError: true,
750
+ };
751
+ }
752
+ }
753
+ const resolved = session ?? this._sessions.find(s => s.name === sessionName);
754
+ const baseUrl = resolved && !resolved.isLoading ? resolved.serverUrl : undefined;
755
+ if (!baseUrl) {
756
+ return {
757
+ content: [{
758
+ type: 'text',
759
+ text: `Error: Session '${sessionName}' is still loading.`,
760
+ }],
761
+ isError: true,
762
+ };
763
+ }
764
+ const useRaw = args.useRawDirectRenderingWithoutExplorerUi ?? false;
765
+ const url = buildExplorerUrl({
766
+ baseUrl,
767
+ rawRender: useRaw,
768
+ fixtureId: args.fixtureId,
769
+ });
770
+ const result = {
771
+ url,
772
+ sessionName,
773
+ };
774
+ if (args.fixtureId) {
775
+ result.fixtureId = args.fixtureId;
776
+ }
777
+ if (useRaw) {
778
+ result.mode = 'raw-render';
779
+ }
780
+ else {
781
+ result.mode = 'explorer';
782
+ if (args.fixtureId) {
783
+ result.note = 'Fixture selection in the Explorer UI is not URL-based. Navigate to the fixture manually in the tree view.';
784
+ }
785
+ }
786
+ return {
787
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
788
+ };
789
+ });
790
+ }
436
791
  }
437
792
 
438
- export { ComponentExplorerMcpServer };
793
+ export { ComponentExplorerMcpServer, DaemonConnection };
439
794
  //# sourceMappingURL=McpServer.js.map