@vscode/component-explorer-cli 0.1.1-7 → 0.1.1-9

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