@vscode/component-explorer-cli 0.2.1-3 → 0.2.1-31

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 (198) hide show
  1. package/README.md +14 -6
  2. package/dist/WorktreePool.d.ts +5 -4
  3. package/dist/WorktreePool.d.ts.map +1 -1
  4. package/dist/WorktreePool.js +3 -3
  5. package/dist/WorktreePool.js.map +1 -1
  6. package/dist/_virtual/_build-info.js +1 -1
  7. package/dist/browserPage.d.ts +51 -1
  8. package/dist/browserPage.d.ts.map +1 -1
  9. package/dist/browserPage.js +107 -1
  10. package/dist/browserPage.js.map +1 -1
  11. package/dist/commands/acceptCommand.d.ts +2 -1
  12. package/dist/commands/acceptCommand.d.ts.map +1 -1
  13. package/dist/commands/acceptCommand.js +18 -10
  14. package/dist/commands/acceptCommand.js.map +1 -1
  15. package/dist/commands/checkStabilityCommand.d.ts +3 -0
  16. package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
  17. package/dist/commands/checkStabilityCommand.js +22 -4
  18. package/dist/commands/checkStabilityCommand.js.map +1 -1
  19. package/dist/commands/compareCommand.d.ts +2 -1
  20. package/dist/commands/compareCommand.d.ts.map +1 -1
  21. package/dist/commands/compareCommand.js +27 -17
  22. package/dist/commands/compareCommand.js.map +1 -1
  23. package/dist/commands/inputArg.d.ts +33 -0
  24. package/dist/commands/inputArg.d.ts.map +1 -0
  25. package/dist/commands/inputArg.js +103 -0
  26. package/dist/commands/inputArg.js.map +1 -0
  27. package/dist/commands/mcpCommand.d.ts +3 -0
  28. package/dist/commands/mcpCommand.d.ts.map +1 -1
  29. package/dist/commands/mcpCommand.js +178 -45
  30. package/dist/commands/mcpCommand.js.map +1 -1
  31. package/dist/commands/renderCommand.d.ts +10 -1
  32. package/dist/commands/renderCommand.d.ts.map +1 -1
  33. package/dist/commands/renderCommand.js +311 -23
  34. package/dist/commands/renderCommand.js.map +1 -1
  35. package/dist/commands/serveCommand.d.ts +2 -0
  36. package/dist/commands/serveCommand.d.ts.map +1 -1
  37. package/dist/commands/serveCommand.js +73 -41
  38. package/dist/commands/serveCommand.js.map +1 -1
  39. package/dist/commands/serviceDiffCommitsCommand.d.ts +16 -0
  40. package/dist/commands/serviceDiffCommitsCommand.d.ts.map +1 -0
  41. package/dist/commands/serviceDiffCommitsCommand.js +230 -0
  42. package/dist/commands/serviceDiffCommitsCommand.js.map +1 -0
  43. package/dist/commands/watchCommand.d.ts +2 -0
  44. package/dist/commands/watchCommand.d.ts.map +1 -1
  45. package/dist/commands/watchCommand.js +28 -16
  46. package/dist/commands/watchCommand.js.map +1 -1
  47. package/dist/comparison.d.ts +4 -3
  48. package/dist/comparison.d.ts.map +1 -1
  49. package/dist/comparison.js +10 -10
  50. package/dist/comparison.js.map +1 -1
  51. package/dist/component-explorer-config.schema.json +55 -2
  52. package/dist/componentExplorer.d.ts +109 -11
  53. package/dist/componentExplorer.d.ts.map +1 -1
  54. package/dist/componentExplorer.js +235 -58
  55. package/dist/componentExplorer.js.map +1 -1
  56. package/dist/config.d.ts +33 -4
  57. package/dist/config.d.ts.map +1 -1
  58. package/dist/config.js +64 -27
  59. package/dist/config.js.map +1 -1
  60. package/dist/coverage.d.ts +64 -0
  61. package/dist/coverage.d.ts.map +1 -0
  62. package/dist/coverage.js +212 -0
  63. package/dist/coverage.js.map +1 -0
  64. package/dist/daemon/DaemonService.d.ts +14 -5
  65. package/dist/daemon/DaemonService.d.ts.map +1 -1
  66. package/dist/daemon/DaemonService.js +42 -18
  67. package/dist/daemon/DaemonService.js.map +1 -1
  68. package/dist/daemon/approvalStore.d.ts +2 -1
  69. package/dist/daemon/approvalStore.d.ts.map +1 -1
  70. package/dist/daemon/approvalStore.js +4 -3
  71. package/dist/daemon/approvalStore.js.map +1 -1
  72. package/dist/daemon/client.d.ts +5 -0
  73. package/dist/daemon/client.d.ts.map +1 -0
  74. package/dist/daemon/client.js +7 -0
  75. package/dist/daemon/client.js.map +1 -0
  76. package/dist/daemon/inProcessClient.d.ts +11 -0
  77. package/dist/daemon/inProcessClient.d.ts.map +1 -0
  78. package/dist/daemon/inProcessClient.js +35 -0
  79. package/dist/daemon/inProcessClient.js.map +1 -0
  80. package/dist/daemon/lifecycle.d.ts +8 -1
  81. package/dist/daemon/lifecycle.d.ts.map +1 -1
  82. package/dist/daemon/lifecycle.js +13 -3
  83. package/dist/daemon/lifecycle.js.map +1 -1
  84. package/dist/daemon/pipeClient.d.ts +2 -0
  85. package/dist/daemon/pipeClient.d.ts.map +1 -1
  86. package/dist/daemon/pipeClient.js +22 -3
  87. package/dist/daemon/pipeClient.js.map +1 -1
  88. package/dist/daemon/pipeName.d.ts +7 -1
  89. package/dist/daemon/pipeName.d.ts.map +1 -1
  90. package/dist/daemon/pipeName.js +8 -4
  91. package/dist/daemon/pipeName.js.map +1 -1
  92. package/dist/daemon/pipeServer.d.ts.map +1 -1
  93. package/dist/daemon/pipeServer.js +9 -3
  94. package/dist/daemon/pipeServer.js.map +1 -1
  95. package/dist/daemon/version.d.ts +1 -1
  96. package/dist/daemon/version.js +1 -1
  97. package/dist/dependencyInstaller.d.ts +2 -1
  98. package/dist/dependencyInstaller.d.ts.map +1 -1
  99. package/dist/dependencyInstaller.js +4 -4
  100. package/dist/dependencyInstaller.js.map +1 -1
  101. package/dist/evaluateFn.d.ts +21 -0
  102. package/dist/evaluateFn.d.ts.map +1 -0
  103. package/dist/evaluateFn.js +17 -0
  104. package/dist/evaluateFn.js.map +1 -0
  105. package/dist/git/gitCommitResolver.d.ts +2 -1
  106. package/dist/git/gitCommitResolver.d.ts.map +1 -1
  107. package/dist/git/gitCommitResolver.js +2 -2
  108. package/dist/git/gitCommitResolver.js.map +1 -1
  109. package/dist/git/gitIndexResolver.d.ts +2 -1
  110. package/dist/git/gitIndexResolver.d.ts.map +1 -1
  111. package/dist/git/gitIndexResolver.js +1 -1
  112. package/dist/git/gitIndexResolver.js.map +1 -1
  113. package/dist/git/gitService.d.ts +5 -4
  114. package/dist/git/gitService.d.ts.map +1 -1
  115. package/dist/git/gitService.js.map +1 -1
  116. package/dist/git/gitUtils.d.ts +6 -4
  117. package/dist/git/gitUtils.d.ts.map +1 -1
  118. package/dist/git/gitUtils.js +10 -5
  119. package/dist/git/gitUtils.js.map +1 -1
  120. package/dist/git/gitWorktreeManager.d.ts +15 -14
  121. package/dist/git/gitWorktreeManager.d.ts.map +1 -1
  122. package/dist/git/gitWorktreeManager.js +10 -11
  123. package/dist/git/gitWorktreeManager.js.map +1 -1
  124. package/dist/git/testUtils.d.ts +2 -1
  125. package/dist/git/testUtils.d.ts.map +1 -1
  126. package/dist/index.js +19 -1
  127. package/dist/index.js.map +1 -1
  128. package/dist/logger.d.ts +12 -1
  129. package/dist/logger.d.ts.map +1 -1
  130. package/dist/logger.js +35 -2
  131. package/dist/logger.js.map +1 -1
  132. package/dist/manifest.schema.json +38 -16
  133. package/dist/mcp/DaemonAccessor.d.ts +22 -0
  134. package/dist/mcp/DaemonAccessor.d.ts.map +1 -0
  135. package/dist/mcp/McpServer.d.ts +5 -19
  136. package/dist/mcp/McpServer.d.ts.map +1 -1
  137. package/dist/mcp/McpServer.js +384 -297
  138. package/dist/mcp/McpServer.js.map +1 -1
  139. package/dist/packages/common/dist/explorerUrl.js +21 -0
  140. package/dist/packages/common/dist/explorerUrl.js.map +1 -0
  141. package/dist/packages/common/dist/renderManifest.js +26 -5
  142. package/dist/packages/common/dist/renderManifest.js.map +1 -1
  143. package/dist/packages/simple-api/dist/{chunk-3R7GHWBM.js → chunk-FJ7AVNQE.js} +2 -1
  144. package/dist/packages/simple-api/dist/chunk-FJ7AVNQE.js.map +1 -0
  145. package/dist/packages/simple-api/dist/{chunk-SGBCNXYH.js → chunk-TTRCY65Z.js} +4 -1
  146. package/dist/packages/simple-api/dist/chunk-TTRCY65Z.js.map +1 -0
  147. package/dist/packages/simple-api/dist/{chunk-TAEFVNPN.js → chunk-WNXMRXWV.js} +2 -1
  148. package/dist/packages/simple-api/dist/chunk-WNXMRXWV.js.map +1 -0
  149. package/dist/packages/simple-api/dist/express.js +1 -1
  150. package/dist/packages/simple-api/dist/express.js.map +1 -1
  151. package/dist/path.d.ts +30 -0
  152. package/dist/path.d.ts.map +1 -0
  153. package/dist/path.js +78 -0
  154. package/dist/path.js.map +1 -0
  155. package/dist/processTree.d.ts +9 -0
  156. package/dist/processTree.d.ts.map +1 -0
  157. package/dist/processTree.js +41 -0
  158. package/dist/processTree.js.map +1 -0
  159. package/dist/screenshotServiceClient.d.ts +31 -0
  160. package/dist/screenshotServiceClient.d.ts.map +1 -0
  161. package/dist/screenshotServiceClient.js +38 -0
  162. package/dist/screenshotServiceClient.js.map +1 -0
  163. package/dist/server/httpServer.d.ts +1 -1
  164. package/dist/server/httpServer.d.ts.map +1 -1
  165. package/dist/server/httpServer.js +22 -20
  166. package/dist/server/httpServer.js.map +1 -1
  167. package/dist/server/serverConfig.d.ts +23 -6
  168. package/dist/server/serverConfig.d.ts.map +1 -1
  169. package/dist/server/serverConfig.js +11 -18
  170. package/dist/server/serverConfig.js.map +1 -1
  171. package/dist/server/viteServer.d.ts +16 -1
  172. package/dist/server/viteServer.d.ts.map +1 -1
  173. package/dist/server/viteServer.js +91 -10
  174. package/dist/server/viteServer.js.map +1 -1
  175. package/dist/storage.d.ts +2 -1
  176. package/dist/storage.d.ts.map +1 -1
  177. package/dist/storage.js +1 -1
  178. package/dist/storage.js.map +1 -1
  179. package/dist/utils.d.ts +3 -25
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +4 -23
  182. package/dist/utils.js.map +1 -1
  183. package/dist/visualCache.d.ts +2 -1
  184. package/dist/visualCache.d.ts.map +1 -1
  185. package/dist/visualCache.js +4 -3
  186. package/dist/visualCache.js.map +1 -1
  187. package/dist/viteProjectRef.d.ts +4 -12
  188. package/dist/viteProjectRef.d.ts.map +1 -1
  189. package/dist/viteProjectRef.js +9 -17
  190. package/dist/viteProjectRef.js.map +1 -1
  191. package/package.json +23 -19
  192. package/dist/commands/artifactCommand.d.ts +0 -13
  193. package/dist/commands/artifactCommand.d.ts.map +0 -1
  194. package/dist/commands/screenshotCommand.d.ts +0 -18
  195. package/dist/commands/screenshotCommand.d.ts.map +0 -1
  196. package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js.map +0 -1
  197. package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js.map +0 -1
  198. package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +0 -1
@@ -8,120 +8,17 @@ import { autorun } from '../external/vscode-observables/observables/dist/observa
8
8
  import '../external/vscode-observables/observables/dist/observableInternal/observables/derived.js';
9
9
  import '../external/vscode-observables/observables/dist/observableInternal/utils/utils.js';
10
10
  import '../external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
11
- import { buildExplorerUrl } from '../utils.js';
11
+ import '../packages/common/dist/renderManifest.js';
12
+ import { buildExplorerUrl } from '../packages/common/dist/explorerUrl.js';
13
+ import { EXPLORER_ROUTE } from '../utils.js';
12
14
  import { TaskManager } from './TaskManager.js';
15
+ import { isPipeConnectionError } from '../daemon/pipeClient.js';
13
16
 
14
- // ---------------------------------------------------------------------------
15
- // Client-local state
16
- // ---------------------------------------------------------------------------
17
- class ImageLruCache {
18
- _maxSize;
19
- _entries = [];
20
- constructor(_maxSize = 10) {
21
- this._maxSize = _maxSize;
22
- }
23
- put(hash, image) {
24
- const idx = this._entries.findIndex(e => e.hash === hash);
25
- if (idx !== -1) {
26
- this._entries.splice(idx, 1);
27
- }
28
- this._entries.unshift({ hash, image });
29
- if (this._entries.length > this._maxSize) {
30
- this._entries.length = this._maxSize;
31
- }
32
- }
33
- get(hash) {
34
- const idx = this._entries.findIndex(e => e.hash === hash);
35
- if (idx === -1) {
36
- return undefined;
37
- }
38
- const [entry] = this._entries.splice(idx, 1);
39
- this._entries.unshift(entry);
40
- return entry.image;
41
- }
42
- keys() {
43
- return this._entries.map(e => e.hash);
44
- }
45
- }
46
- class WatchList {
47
- _fixtureIds = new Set();
48
- _hashes = new Map();
49
- get fixtureIds() { return this._fixtureIds; }
50
- add(ids) {
51
- for (const id of ids) {
52
- this._fixtureIds.add(id);
53
- }
54
- }
55
- remove(ids) {
56
- for (const id of ids) {
57
- this._fixtureIds.delete(id);
58
- this._hashes.delete(id);
59
- }
60
- }
61
- set(ids) {
62
- this._fixtureIds.clear();
63
- this._hashes.clear();
64
- for (const id of ids) {
65
- this._fixtureIds.add(id);
66
- }
67
- }
68
- getHash(fixtureId) {
69
- return this._hashes.get(fixtureId);
70
- }
71
- setHash(fixtureId, hash) {
72
- this._hashes.set(fixtureId, hash);
73
- }
74
- toJSON() {
75
- return {
76
- fixtureIds: [...this._fixtureIds],
77
- hashes: Object.fromEntries(this._hashes),
78
- };
79
- }
80
- }
81
- function noDaemonError(hint) {
82
- let text = 'Error: No daemon is currently running.';
83
- if (hint) {
84
- text += ` ${hint}`;
85
- }
86
- else {
87
- text += ' Please start the Component Explorer daemon first by running:\n\n' +
88
- ' component-explorer serve --project <config.json>\n\n' +
89
- 'Or start it in the background:\n\n' +
90
- ' component-explorer serve --project <config.json> --background\n\n' +
91
- 'The daemon manages dev servers and enables fixture screenshots.';
92
- }
93
- return {
94
- content: [{ type: 'text', text }],
95
- isError: true,
96
- };
97
- }
98
- // ---------------------------------------------------------------------------
99
- // DaemonConnection - wrapper to avoid Proxy issues with observables
100
- // ---------------------------------------------------------------------------
101
- class DaemonConnection {
102
- client;
103
- _stale = false;
104
- constructor(client) {
105
- this.client = client;
106
- }
107
- get isStale() { return this._stale; }
108
- markStale() { this._stale = true; }
109
- }
110
- function isPipeConnectionError(e) {
111
- if (!(e instanceof Error)) {
112
- return false;
113
- }
114
- const code = e.code;
115
- if (code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EPIPE') {
116
- return true;
117
- }
118
- return /connect ENOENT|ECONNREFUSED|ECONNRESET|EPIPE/.test(e.message);
119
- }
120
17
  // ---------------------------------------------------------------------------
121
18
  // ComponentExplorerMcpServer
122
19
  // ---------------------------------------------------------------------------
123
20
  class ComponentExplorerMcpServer extends Disposable {
124
- _daemonConnection;
21
+ _daemon;
125
22
  static async create(daemon, options) {
126
23
  const server = new ComponentExplorerMcpServer(daemon, options ?? {});
127
24
  const transport = new StdioServerTransport();
@@ -133,16 +30,12 @@ class ComponentExplorerMcpServer extends Disposable {
133
30
  _imageLru = new ImageLruCache(10);
134
31
  _taskManager = new TaskManager();
135
32
  _taskLastReportedIndex = new Map();
136
- _pollFn;
137
- _noAutostartHint;
138
33
  _multiSessionTools = [];
139
34
  _sessions = [];
140
35
  _eventStreamAbortController;
141
- constructor(_daemonConnection, options) {
36
+ constructor(_daemon, options) {
142
37
  super();
143
- this._daemonConnection = _daemonConnection;
144
- this._pollFn = options.pollFn;
145
- this._noAutostartHint = options.noAutostartHint;
38
+ this._daemon = _daemon;
146
39
  this._callTimeoutMs = options.callTimeoutMs ?? ComponentExplorerMcpServer._DEFAULT_CALL_TIMEOUT_MS;
147
40
  this._mcp = new McpServer({
148
41
  name: 'component-explorer',
@@ -150,8 +43,8 @@ class ComponentExplorerMcpServer extends Disposable {
150
43
  });
151
44
  this._registerTools();
152
45
  this._store.add(autorun(async (reader) => {
153
- const conn = this._daemonConnection.read(reader);
154
- await this._onDaemonChanged(conn?.client);
46
+ const client = this._daemon.connection.read(reader);
47
+ await this._onDaemonChanged(client);
155
48
  }));
156
49
  }
157
50
  async _onDaemonChanged(daemon) {
@@ -173,49 +66,13 @@ class ComponentExplorerMcpServer extends Disposable {
173
66
  this._sessions = [];
174
67
  }
175
68
  }
176
- _getConnection() {
177
- const conn = this._daemonConnection.get();
178
- if (conn?.isStale) {
179
- return undefined;
180
- }
181
- return conn;
182
- }
183
- async _waitForDaemon() {
184
- let conn = this._getConnection();
185
- if (conn) {
186
- return conn.client;
187
- }
188
- if (!this._pollFn) {
189
- return undefined;
190
- }
191
- this._log('debug', { type: 'waiting-for-daemon' });
192
- const startTime = Date.now();
193
- const timeout = 3000;
194
- while (Date.now() - startTime < timeout) {
195
- await this._pollFn();
196
- conn = this._getConnection();
197
- if (conn) {
198
- return conn.client;
199
- }
200
- await new Promise(resolve => setTimeout(resolve, 200));
201
- }
202
- return undefined;
203
- }
204
- _handleDisconnect() {
205
- const conn = this._daemonConnection.get();
206
- if (conn && !conn.isStale) {
207
- conn.markStale();
208
- this._sessions = [];
209
- this._log('debug', { type: 'daemon-connection-lost' });
210
- }
211
- }
212
69
  _noDaemonError() {
213
- return noDaemonError(this._noAutostartHint);
70
+ return noDaemonError(this._daemon.noDaemonHint);
214
71
  }
215
72
  static _DEFAULT_CALL_TIMEOUT_MS = 15_000;
216
73
  _callTimeoutMs;
217
74
  async _withDaemon(fn, options) {
218
- const daemon = await this._waitForDaemon();
75
+ const daemon = await this._daemon.getDaemonOrStart();
219
76
  if (!daemon) {
220
77
  return this._noDaemonError();
221
78
  }
@@ -235,7 +92,6 @@ class ComponentExplorerMcpServer extends Disposable {
235
92
  }
236
93
  if (isPipeConnectionError(e)) {
237
94
  this._log('debug', { type: 'daemon-call-failed', error: String(e) });
238
- this._handleDisconnect();
239
95
  return this._noDaemonError();
240
96
  }
241
97
  throw e;
@@ -259,7 +115,7 @@ class ComponentExplorerMcpServer extends Disposable {
259
115
  this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
260
116
  }
261
117
  if (event.type === 'ref-change' || event.type === 'session-change') {
262
- await this._refreshSessions();
118
+ await this._refreshSessions(daemon);
263
119
  }
264
120
  this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
265
121
  }
@@ -293,24 +149,11 @@ class ComponentExplorerMcpServer extends Disposable {
293
149
  s.sourceTreeId = sourceTreeId;
294
150
  }
295
151
  }
296
- async _refreshSessions() {
297
- const conn = this._getConnection();
298
- if (conn) {
299
- try {
300
- const prevCount = this._sessions.length;
301
- this._sessions = await conn.client.methods.sessions();
302
- if (this._sessions.length !== prevCount) {
303
- this._updateMultiSessionToolVisibility();
304
- }
305
- }
306
- catch (e) {
307
- if (isPipeConnectionError(e)) {
308
- this._handleDisconnect();
309
- }
310
- else {
311
- throw e;
312
- }
313
- }
152
+ async _refreshSessions(daemon) {
153
+ const prevCount = this._sessions.length;
154
+ this._sessions = await daemon.methods.sessions();
155
+ if (this._sessions.length !== prevCount) {
156
+ this._updateMultiSessionToolVisibility();
314
157
  }
315
158
  }
316
159
  _updateMultiSessionToolVisibility() {
@@ -324,37 +167,37 @@ class ComponentExplorerMcpServer extends Disposable {
324
167
  }
325
168
  }
326
169
  }
327
- async _withSourceTreeRetry(fn) {
170
+ async _withSourceTreeRetry(daemon, fn) {
328
171
  try {
329
172
  return await fn();
330
173
  }
331
174
  catch (e) {
332
175
  const msg = e instanceof Error ? e.message : String(e);
333
176
  if (msg.includes('Source tree changed')) {
334
- await this._refreshSessions();
177
+ await this._refreshSessions(daemon);
335
178
  return await fn();
336
179
  }
337
180
  throw e;
338
181
  }
339
182
  }
340
183
  // -- Tool registration ---------------------------------------------------
341
- _filterFixtures(allFixtures, fixtureIdPattern, labelPattern) {
184
+ _filterFixtures(allFixtures, fixtureIdRegexStr, labelRegexStr) {
342
185
  let fixtureIdRegex;
343
- if (fixtureIdPattern) {
186
+ if (fixtureIdRegexStr) {
344
187
  try {
345
- fixtureIdRegex = new RegExp(fixtureIdPattern);
188
+ fixtureIdRegex = new RegExp(fixtureIdRegexStr);
346
189
  }
347
190
  catch {
348
- return { error: `Error: Invalid fixtureIdPattern: ${fixtureIdPattern}` };
191
+ return { error: `Error: Invalid fixtureIdRegex: ${fixtureIdRegexStr}` };
349
192
  }
350
193
  }
351
194
  let labelRegex;
352
- if (labelPattern) {
195
+ if (labelRegexStr) {
353
196
  try {
354
- labelRegex = new RegExp(labelPattern);
197
+ labelRegex = new RegExp(labelRegexStr);
355
198
  }
356
199
  catch {
357
- return { error: `Error: Invalid labelPattern: ${labelPattern}` };
200
+ return { error: `Error: Invalid labelRegex: ${labelRegexStr}` };
358
201
  }
359
202
  }
360
203
  return {
@@ -371,10 +214,10 @@ class ComponentExplorerMcpServer extends Disposable {
371
214
  this._registerCheckVisuals();
372
215
  this._registerEvaluateJs();
373
216
  this._registerDebugReloadPage();
374
- this._registerWatchAdd();
375
- this._registerWatchRemove();
376
- this._registerWatchSet();
377
- this._registerWatchCompare();
217
+ //this._registerWatchAdd();
218
+ //this._registerWatchRemove();
219
+ //this._registerWatchSet();
220
+ //this._registerWatchCompare();
378
221
  this._registerWaitForUpdate();
379
222
  this._registerSessions();
380
223
  this._registerRestartSession();
@@ -382,6 +225,7 @@ class ComponentExplorerMcpServer extends Disposable {
382
225
  this._registerCloseSession();
383
226
  this._registerUpdateSessionRef();
384
227
  this._registerGetUrl();
228
+ this._registerCheckFixtureErrors();
385
229
  this._registerCheckStability();
386
230
  this._registerCheckTask();
387
231
  this._registerCancelTask();
@@ -391,20 +235,20 @@ class ComponentExplorerMcpServer extends Disposable {
391
235
  _registerListFixtures() {
392
236
  this._mcp.registerTool('list_fixtures', {
393
237
  description: 'List all fixtures from a session',
394
- inputSchema: {
395
- fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
396
- labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
238
+ inputSchema: z.strictObject({
239
+ fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
240
+ labelRegex: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
397
241
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
398
242
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
399
- },
243
+ }),
400
244
  annotations: { readOnlyHint: true },
401
245
  }, async (args) => this._withDaemon(async (daemon) => {
402
246
  const sessionName = args.sessionName ?? this._defaultSessionName();
403
247
  this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
404
- return this._withSourceTreeRetry(async () => {
248
+ return this._withSourceTreeRetry(daemon, async () => {
405
249
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
406
250
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
407
- const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdPattern, args.labelPattern);
251
+ const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
408
252
  if ('error' in filtered) {
409
253
  return { content: [{ type: 'text', text: filtered.error }], isError: true };
410
254
  }
@@ -420,19 +264,28 @@ class ComponentExplorerMcpServer extends Disposable {
420
264
  _registerScreenshot() {
421
265
  this._mcp.registerTool('screenshot', {
422
266
  description: 'Take a screenshot of a single fixture. ' +
423
- 'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ',
424
- 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({
425
272
  fixtureId: z.string().describe('The fixture ID'),
426
273
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
427
274
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
428
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..'),
429
- },
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.'),
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
+ }),
430
283
  annotations: { readOnlyHint: true },
431
284
  }, async (args) => this._withDaemon(async (daemon) => {
432
285
  const sessionName = args.sessionName ?? this._defaultSessionName();
433
286
  this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
434
287
  this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
435
- return this._withSourceTreeRetry(async () => {
288
+ return this._withSourceTreeRetry(daemon, async () => {
436
289
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
437
290
  const result = await daemon.methods.screenshots.take({
438
291
  fixtureId: args.fixtureId,
@@ -440,6 +293,8 @@ class ComponentExplorerMcpServer extends Disposable {
440
293
  sourceTreeId,
441
294
  includeImage: true,
442
295
  stabilityCheck: args.stabilityCheck,
296
+ input: args.input,
297
+ disposeAfter: args.disposeAfter,
443
298
  });
444
299
  const r = result;
445
300
  this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
@@ -447,6 +302,7 @@ class ComponentExplorerMcpServer extends Disposable {
447
302
  if (r.hash && r.image) {
448
303
  this._imageLru.put(r.hash, r.image);
449
304
  }
305
+ const filterEvents = (events) => args.includeConsoleLog ? events : events.filter(e => e.type !== 'console.log');
450
306
  const info = {
451
307
  hash: r.hash,
452
308
  sourceTreeId: r.sourceTreeId,
@@ -458,14 +314,37 @@ class ComponentExplorerMcpServer extends Disposable {
458
314
  info.error = r.error;
459
315
  }
460
316
  if (r.events && r.events.length > 0) {
461
- info.events = r.events;
317
+ const filtered = filterEvents(r.events);
318
+ if (filtered.length > 0) {
319
+ info.events = filtered;
320
+ }
462
321
  }
463
- if (r.resultData !== undefined) {
464
- info.resultData = r.resultData;
322
+ if (r.output !== undefined) {
323
+ info.output = r.output;
465
324
  }
466
325
  if (r.isStable !== undefined) {
467
326
  info.isStable = r.isStable;
468
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
+ }
469
348
  // Visual review status
470
349
  if (r.hash) {
471
350
  try {
@@ -509,19 +388,19 @@ class ComponentExplorerMcpServer extends Disposable {
509
388
  _registerCompareScreenshot() {
510
389
  const tool = this._mcp.registerTool('compare_screenshot', {
511
390
  description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
512
- inputSchema: {
391
+ inputSchema: z.strictObject({
513
392
  fixtureId: z.string().describe('The fixture ID'),
514
393
  baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
515
394
  currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
516
395
  baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
517
396
  currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
518
- },
397
+ }),
519
398
  annotations: { readOnlyHint: true },
520
399
  }, async (args) => this._withDaemon(async (daemon) => {
521
400
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
522
401
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
523
402
  this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
524
- return this._withSourceTreeRetry(async () => {
403
+ return this._withSourceTreeRetry(daemon, async () => {
525
404
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
526
405
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
527
406
  const result = await daemon.methods.screenshots.compare({
@@ -549,8 +428,8 @@ class ComponentExplorerMcpServer extends Disposable {
549
428
  if (r.baselineEvents && r.baselineEvents.length > 0) {
550
429
  info.baselineEvents = r.baselineEvents;
551
430
  }
552
- if (r.baselineResultData !== undefined) {
553
- info.baselineResultData = r.baselineResultData;
431
+ if (r.baselineOutput !== undefined) {
432
+ info.baselineOutput = r.baselineOutput;
554
433
  }
555
434
  if (r.currentHasError) {
556
435
  info.currentHasError = true;
@@ -561,8 +440,8 @@ class ComponentExplorerMcpServer extends Disposable {
561
440
  if (r.currentEvents && r.currentEvents.length > 0) {
562
441
  info.currentEvents = r.currentEvents;
563
442
  }
564
- if (r.currentResultData !== undefined) {
565
- info.currentResultData = r.currentResultData;
443
+ if (r.currentOutput !== undefined) {
444
+ info.currentOutput = r.currentOutput;
566
445
  }
567
446
  if (r.approval) {
568
447
  info.approval = r.approval;
@@ -585,12 +464,12 @@ class ComponentExplorerMcpServer extends Disposable {
585
464
  _registerApproveDiff() {
586
465
  const tool = this._mcp.registerTool('approve_diff', {
587
466
  description: 'Approve a visual diff so it won\'t require re-inspection next time',
588
- inputSchema: {
467
+ inputSchema: z.strictObject({
589
468
  fixtureId: z.string(),
590
469
  originalHash: z.string(),
591
470
  modifiedHash: z.string(),
592
471
  comment: z.string().describe('Reason for approving this diff'),
593
- },
472
+ }),
594
473
  }, async (args) => this._withDaemon(async (daemon) => {
595
474
  await daemon.methods.approvals.approve(args);
596
475
  return {
@@ -608,18 +487,18 @@ class ComponentExplorerMcpServer extends Disposable {
608
487
  description: 'Approve or reject a fixture\'s screenshot based on its expectedVisualDescriptions. ' +
609
488
  'You must take a screenshot first and pass the resulting hash. ' +
610
489
  'On approve, caches (expectedVisualDescriptions, screenshotHash) so future runs auto-approve.',
611
- inputSchema: {
490
+ inputSchema: z.strictObject({
612
491
  fixtureId: z.string().describe('The fixture ID'),
613
492
  screenshotHash: z.string().describe('The screenshot hash (from a prior screenshot tool call)'),
614
493
  verdict: z.enum(['approve', 'reject']).describe('Whether the visual matches expectations'),
615
494
  comment: z.string().describe('Reason for the verdict'),
616
495
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
617
496
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
618
- },
497
+ }),
619
498
  }, async (args) => this._withDaemon(async (daemon) => {
620
499
  const sessionName = args.sessionName ?? this._defaultSessionName();
621
500
  this._log('debug', { type: 'tool-call', tool: 'review_visual', fixtureId: args.fixtureId, verdict: args.verdict });
622
- return this._withSourceTreeRetry(async () => {
501
+ return this._withSourceTreeRetry(daemon, async () => {
623
502
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
624
503
  // Get fixture descriptions
625
504
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
@@ -643,13 +522,15 @@ class ComponentExplorerMcpServer extends Disposable {
643
522
  }
644
523
  else {
645
524
  return {
646
- content: [{ type: 'text', text: JSON.stringify({
525
+ content: [{
526
+ type: 'text', text: JSON.stringify({
647
527
  fixtureId: args.fixtureId,
648
528
  verdict: 'rejected',
649
529
  comment: args.comment,
650
530
  screenshotHash: args.screenshotHash,
651
531
  expectedVisualDescriptions: fixture.expectedVisualDescriptions,
652
- }, null, 2) }],
532
+ }, null, 2)
533
+ }],
653
534
  };
654
535
  }
655
536
  });
@@ -659,23 +540,23 @@ class ComponentExplorerMcpServer extends Disposable {
659
540
  this._mcp.registerTool('check_visuals', {
660
541
  description: 'Batch check visual review status for fixtures that have expectedVisualDescription. ' +
661
542
  'Returns lists of approved, needs-review, and no-expectation fixtures.',
662
- inputSchema: {
663
- fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
664
- labelPattern: z.string().optional().describe('RegExp to filter fixtures by label'),
543
+ inputSchema: z.strictObject({
544
+ fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
545
+ labelRegex: z.string().optional().describe('RegExp to filter fixtures by label'),
665
546
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
666
547
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
667
- },
548
+ }),
668
549
  annotations: { readOnlyHint: true },
669
550
  }, async (args) => this._withDaemon(async (daemon) => {
670
551
  const sessionName = args.sessionName ?? this._defaultSessionName();
671
552
  this._log('debug', { type: 'tool-call', tool: 'check_visuals', sessionName });
672
- return this._withSourceTreeRetry(async () => {
553
+ return this._withSourceTreeRetry(daemon, async () => {
673
554
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
674
555
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
675
556
  if (listResult.loadError) {
676
557
  return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
677
558
  }
678
- const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdPattern, args.labelPattern);
559
+ const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
679
560
  if ('error' in filtered) {
680
561
  return { content: [{ type: 'text', text: filtered.error }], isError: true };
681
562
  }
@@ -733,17 +614,17 @@ class ComponentExplorerMcpServer extends Disposable {
733
614
  'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
734
615
  'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
735
616
  'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
736
- inputSchema: {
617
+ inputSchema: z.strictObject({
737
618
  expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
738
619
  fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
739
620
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
740
621
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
741
- },
622
+ }),
742
623
  }, async (args) => this._withDaemon(async (daemon) => {
743
624
  const sessionName = args.sessionName ?? this._defaultSessionName();
744
625
  this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
745
626
  this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
746
- return this._withSourceTreeRetry(async () => {
627
+ return this._withSourceTreeRetry(daemon, async () => {
747
628
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
748
629
  const result = await daemon.methods.evaluate({
749
630
  sessionName,
@@ -770,9 +651,9 @@ class ComponentExplorerMcpServer extends Disposable {
770
651
  description: 'Force-reload the browser page used for rendering fixtures. ' +
771
652
  'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
772
653
  'that persist after source changes. Normal HMR updates should handle most cases automatically.',
773
- inputSchema: {
654
+ inputSchema: z.strictObject({
774
655
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
775
- },
656
+ }),
776
657
  annotations: { destructiveHint: true },
777
658
  }, async (args) => this._withDaemon(async (daemon) => {
778
659
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -790,9 +671,9 @@ class ComponentExplorerMcpServer extends Disposable {
790
671
  _registerWatchAdd() {
791
672
  this._mcp.registerTool('watch_add', {
792
673
  description: 'Add fixtures to the watch list. Watched fixtures are automatically re-screenshotted when source changes.',
793
- inputSchema: {
674
+ inputSchema: z.strictObject({
794
675
  fixtureIds: z.array(z.string()).describe('Fixture IDs to add'),
795
- },
676
+ }),
796
677
  }, async (args) => {
797
678
  this._watchList.add(args.fixtureIds);
798
679
  return {
@@ -806,9 +687,9 @@ class ComponentExplorerMcpServer extends Disposable {
806
687
  _registerWatchRemove() {
807
688
  this._mcp.registerTool('watch_remove', {
808
689
  description: 'Remove fixtures from the watch list',
809
- inputSchema: {
690
+ inputSchema: z.strictObject({
810
691
  fixtureIds: z.array(z.string()).describe('Fixture IDs to remove'),
811
- },
692
+ }),
812
693
  }, async (args) => {
813
694
  this._watchList.remove(args.fixtureIds);
814
695
  return {
@@ -822,9 +703,9 @@ class ComponentExplorerMcpServer extends Disposable {
822
703
  _registerWatchSet() {
823
704
  this._mcp.registerTool('watch_set', {
824
705
  description: 'Replace the watch list entirely',
825
- inputSchema: {
706
+ inputSchema: z.strictObject({
826
707
  fixtureIds: z.array(z.string()).describe('Fixture IDs to watch'),
827
- },
708
+ }),
828
709
  }, async (args) => {
829
710
  this._watchList.set(args.fixtureIds);
830
711
  return {
@@ -838,12 +719,12 @@ class ComponentExplorerMcpServer extends Disposable {
838
719
  _registerWatchCompare() {
839
720
  const tool = this._mcp.registerTool('watch_compare', {
840
721
  description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
841
- inputSchema: {
722
+ inputSchema: z.strictObject({
842
723
  baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
843
724
  currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
844
725
  baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
845
726
  currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
846
- },
727
+ }),
847
728
  annotations: { readOnlyHint: true },
848
729
  }, async (args) => this._withDaemon(async (daemon) => {
849
730
  const ids = [...this._watchList.fixtureIds];
@@ -852,7 +733,7 @@ class ComponentExplorerMcpServer extends Disposable {
852
733
  }
853
734
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
854
735
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
855
- return this._withSourceTreeRetry(async () => {
736
+ return this._withSourceTreeRetry(daemon, async () => {
856
737
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
857
738
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
858
739
  const [baselineResult, currentResult] = await Promise.all([
@@ -908,10 +789,10 @@ class ComponentExplorerMcpServer extends Disposable {
908
789
  'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
909
790
  'otherwise waits for a source-change or ref-change event. ' +
910
791
  'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
911
- inputSchema: {
792
+ inputSchema: z.strictObject({
912
793
  sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
913
794
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
914
- },
795
+ }),
915
796
  annotations: { readOnlyHint: true },
916
797
  }, async (args) => this._withDaemon(async (daemon) => {
917
798
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -945,7 +826,7 @@ class ComponentExplorerMcpServer extends Disposable {
945
826
  this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
946
827
  }
947
828
  if (ev.type === 'ref-change') {
948
- const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
829
+ const refreshResult = await Promise.race([this._refreshSessions(daemon), timeout]);
949
830
  if (refreshResult === 'timeout') {
950
831
  return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
951
832
  }
@@ -1002,8 +883,8 @@ class ComponentExplorerMcpServer extends Disposable {
1002
883
  this._mcp.registerTool('sessions', {
1003
884
  description: 'List active sessions with their names, URLs, and current sourceTreeIds',
1004
885
  annotations: { readOnlyHint: true },
1005
- }, async () => this._withDaemon(async (_daemon) => {
1006
- await this._refreshSessions();
886
+ }, async () => this._withDaemon(async (daemon) => {
887
+ await this._refreshSessions(daemon);
1007
888
  return {
1008
889
  content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
1009
890
  };
@@ -1013,9 +894,9 @@ class ComponentExplorerMcpServer extends Disposable {
1013
894
  this._mcp.registerTool('restart_session', {
1014
895
  description: 'Restart a session by disposing its browser page and dev server, then recreating them. ' +
1015
896
  'Use this when a session appears stuck (e.g. after a timeout).',
1016
- inputSchema: {
897
+ inputSchema: z.strictObject({
1017
898
  sessionName: z.string().optional().describe('Session name to restart (defaults to first session)'),
1018
- },
899
+ }),
1019
900
  annotations: { destructiveHint: true },
1020
901
  }, async (args) => this._withDaemon(async (daemon) => {
1021
902
  const sessionName = args.sessionName ?? this._defaultSessionName();
@@ -1033,10 +914,10 @@ class ComponentExplorerMcpServer extends Disposable {
1033
914
  'The ref can be a branch name, tag, commit SHA, or the special value "INDEX" to snapshot staged changes. ' +
1034
915
  'The daemon allocates a reusable worktree slot from a fixed pool (max configured in component-explorer.json). ' +
1035
916
  'Returns the updated session list on success.',
1036
- inputSchema: {
917
+ inputSchema: z.strictObject({
1037
918
  name: z.string().describe('Unique session name (e.g. "baseline", "bisect")'),
1038
919
  ref: z.string().describe('Git ref: branch, tag, commit SHA, or "INDEX" for staged changes'),
1039
- },
920
+ }),
1040
921
  }, async (args) => this._withDaemon(async (daemon) => {
1041
922
  this._log('info', { type: 'tool-call', tool: 'open_session', name: args.name, ref: args.ref });
1042
923
  const result = await daemon.methods.openSession({ name: args.name, ref: args.ref });
@@ -1054,9 +935,9 @@ class ComponentExplorerMcpServer extends Disposable {
1054
935
  this._mcp.registerTool('close_session', {
1055
936
  description: 'Close a dynamic worktree session and release its worktree slot back to the pool. ' +
1056
937
  'Cannot close static sessions configured in component-explorer.json.',
1057
- inputSchema: {
938
+ inputSchema: z.strictObject({
1058
939
  name: z.string().describe('Session name to close'),
1059
- },
940
+ }),
1060
941
  annotations: { destructiveHint: true },
1061
942
  }, async (args) => this._withDaemon(async (daemon) => {
1062
943
  this._log('info', { type: 'tool-call', tool: 'close_session', name: args.name });
@@ -1077,10 +958,10 @@ class ComponentExplorerMcpServer extends Disposable {
1077
958
  'The worktree is checked out to the new ref and Vite\'s HMR handles the incremental update (no server restart). ' +
1078
959
  'Fails if the worktree has uncommitted changes — the error will list the dirty files. ' +
1079
960
  'The ref can be a branch, tag, commit SHA, or "INDEX" for staged changes.',
1080
- inputSchema: {
961
+ inputSchema: z.strictObject({
1081
962
  name: z.string().describe('Session name to update'),
1082
963
  ref: z.string().describe('New git ref: branch, tag, commit SHA, or "INDEX"'),
1083
- },
964
+ }),
1084
965
  }, async (args) => this._withDaemon(async (daemon) => {
1085
966
  this._log('info', { type: 'tool-call', tool: 'update_session_ref', name: args.name, ref: args.ref });
1086
967
  const result = await daemon.methods.updateSessionRef({ name: args.name, ref: args.ref });
@@ -1095,33 +976,19 @@ class ComponentExplorerMcpServer extends Disposable {
1095
976
  }
1096
977
  _registerGetUrl() {
1097
978
  this._mcp.registerTool('get_url', {
1098
- description: 'Get URL(s) for viewing fixtures. Returns the Component Explorer UI URL by default, ' +
1099
- 'or the raw render URL for embedding/screenshots when useRawDirectRenderingWithoutExplorerUi is true.',
1100
- inputSchema: {
979
+ description: 'Get URL for viewing fixtures. Returns the full Component Explorer UI by default. ' +
980
+ 'Use `embedded: true` for a minimal single-fixture view (requires fixtureId).',
981
+ inputSchema: z.strictObject({
1101
982
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
1102
- fixtureId: z.string().optional().describe('Specific fixture ID. If omitted, returns URL for the explorer root or all fixtures.'),
1103
- useRawDirectRenderingWithoutExplorerUi: z.boolean().optional().describe('If true, returns the raw rendering URL (for embedding or screenshots) instead of the Explorer UI URL. ' +
1104
- 'The raw URL renders only the fixture without the explorer chrome. Default: false.'),
1105
- },
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.'),
984
+ embedded: z.boolean().optional().describe('If true, returns an embedded single-fixture URL (minimal UI, requires fixtureId). Default: false (full explorer UI).'),
985
+ }),
1106
986
  annotations: { readOnlyHint: true },
1107
- }, async (args) => {
987
+ }, async (args) => this._withDaemon(async (daemon) => {
1108
988
  const sessionName = args.sessionName ?? this._defaultSessionName();
1109
989
  let session = this._sessions.find(s => s.name === sessionName);
1110
990
  if (!session) {
1111
- const daemon = await this._waitForDaemon();
1112
- if (daemon) {
1113
- try {
1114
- await this._refreshSessions();
1115
- }
1116
- catch (e) {
1117
- if (isPipeConnectionError(e)) {
1118
- this._handleDisconnect();
1119
- }
1120
- else {
1121
- throw e;
1122
- }
1123
- }
1124
- }
991
+ await this._refreshSessions(daemon);
1125
992
  session = this._sessions.find(s => s.name === sessionName);
1126
993
  if (!session) {
1127
994
  return {
@@ -1143,10 +1010,21 @@ class ComponentExplorerMcpServer extends Disposable {
1143
1010
  isError: true,
1144
1011
  };
1145
1012
  }
1146
- const useRaw = args.useRawDirectRenderingWithoutExplorerUi ?? false;
1013
+ // Validate embedded mode requires fixtureId
1014
+ if (args.embedded && !args.fixtureId) {
1015
+ return {
1016
+ content: [{
1017
+ type: 'text',
1018
+ text: 'Error: embedded mode requires a fixtureId.',
1019
+ }],
1020
+ isError: true,
1021
+ };
1022
+ }
1023
+ const mode = args.embedded ? 'embedded' : undefined;
1147
1024
  const url = buildExplorerUrl({
1148
1025
  baseUrl,
1149
- rawRender: useRaw,
1026
+ pathname: EXPLORER_ROUTE,
1027
+ mode,
1150
1028
  fixtureId: args.fixtureId,
1151
1029
  });
1152
1030
  const result = {
@@ -1156,42 +1034,164 @@ class ComponentExplorerMcpServer extends Disposable {
1156
1034
  if (args.fixtureId) {
1157
1035
  result.fixtureId = args.fixtureId;
1158
1036
  }
1159
- if (useRaw) {
1160
- result.mode = 'raw-render';
1161
- }
1162
- else {
1163
- result.mode = 'explorer';
1164
- if (args.fixtureId) {
1165
- result.note = 'Fixture selection in the Explorer UI is not URL-based. Navigate to the fixture manually in the tree view.';
1166
- }
1167
- }
1037
+ result.mode = args.embedded ? 'embedded' : 'explorer';
1168
1038
  return {
1169
1039
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1170
1040
  };
1171
- });
1041
+ }));
1042
+ }
1043
+ _registerCheckFixtureErrors() {
1044
+ this._mcp.registerTool('check_fixture_errors', {
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. ' +
1051
+ 'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task.',
1052
+ inputSchema: z.strictObject({
1053
+ fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
1054
+ labelRegex: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
1055
+ sessionName: z.string().optional().describe('Session name (defaults to first session)'),
1056
+ sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
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
+ }),
1062
+ annotations: { readOnlyHint: true },
1063
+ }, async (args) => this._withDaemon(async (daemon) => {
1064
+ const sessionName = args.sessionName ?? this._defaultSessionName();
1065
+ this._log('debug', { type: 'tool-call', tool: 'check_fixture_errors', sessionName, fixtureIdRegex: args.fixtureIdRegex, labelRegex: args.labelRegex });
1066
+ return this._withSourceTreeRetry(daemon, async () => {
1067
+ const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
1068
+ const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
1069
+ if (listResult.loadError) {
1070
+ return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
1071
+ }
1072
+ const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
1073
+ if ('error' in filtered) {
1074
+ return { content: [{ type: 'text', text: filtered.error }], isError: true };
1075
+ }
1076
+ const fixtures = filtered.fixtures;
1077
+ const task = this._taskManager.startTask(async (report, signal) => {
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 });
1084
+ for (let i = 0; i < fixtures.length; i++) {
1085
+ if (signal.aborted) {
1086
+ break;
1087
+ }
1088
+ const fixture = fixtures[i];
1089
+ try {
1090
+ const result = await daemon.methods.screenshots.take({
1091
+ fixtureId: fixture.fixtureId,
1092
+ sessionName,
1093
+ sourceTreeId,
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,
1102
+ });
1103
+ const r = result;
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);
1123
+ }
1124
+ else {
1125
+ okCount++;
1126
+ }
1127
+ }
1128
+ catch (e) {
1129
+ erroredResults.push({
1130
+ fixtureId: fixture.fixtureId,
1131
+ hasError: true,
1132
+ error: { message: e instanceof Error ? e.message : String(e) },
1133
+ });
1134
+ }
1135
+ processed++;
1136
+ report({ completed: i + 1, total: fixtures.length, partialResult: erroredResults });
1137
+ }
1138
+ return {
1139
+ fixtures: erroredResults,
1140
+ summary: { total: processed, ok: okCount, errored: erroredResults.length },
1141
+ };
1142
+ });
1143
+ const waited = await this._taskManager.waitForTask(task.id, 10_000);
1144
+ if (!waited) {
1145
+ return { content: [{ type: 'text', text: 'Error: task disappeared' }], isError: true };
1146
+ }
1147
+ if (waited.done) {
1148
+ return {
1149
+ content: [{ type: 'text', text: JSON.stringify(waited.result, null, 2) }],
1150
+ };
1151
+ }
1152
+ const partial = waited.progress.partialResult;
1153
+ this._taskLastReportedIndex.set(task.id, partial.length);
1154
+ return {
1155
+ content: [{
1156
+ type: 'text',
1157
+ text: JSON.stringify({
1158
+ taskId: task.id,
1159
+ status: 'running',
1160
+ progress: { completed: waited.progress.completed, total: waited.progress.total },
1161
+ elapsedMs: waited.elapsedMs,
1162
+ results: partial,
1163
+ }, null, 2),
1164
+ }],
1165
+ };
1166
+ });
1167
+ }));
1172
1168
  }
1173
1169
  _registerCheckStability() {
1174
1170
  this._mcp.registerTool('check_stability', {
1175
1171
  description: 'Check rendering stability of fixtures. Each fixture is unmounted, re-mounted, and screenshotted 3 times (~3s per fixture). ' +
1176
1172
  'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task. ' +
1177
1173
  'When returning a taskId, includes partial results collected so far.',
1178
- inputSchema: {
1179
- fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
1180
- labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
1174
+ inputSchema: z.strictObject({
1175
+ fixtureIdRegex: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
1176
+ labelRegex: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
1181
1177
  sessionName: z.string().optional().describe('Session name (defaults to first session)'),
1182
1178
  sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
1183
- },
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
+ }),
1184
1184
  annotations: { readOnlyHint: true },
1185
1185
  }, async (args) => this._withDaemon(async (daemon) => {
1186
1186
  const sessionName = args.sessionName ?? this._defaultSessionName();
1187
- this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
1188
- return this._withSourceTreeRetry(async () => {
1187
+ this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdRegex: args.fixtureIdRegex, labelRegex: args.labelRegex });
1188
+ return this._withSourceTreeRetry(daemon, async () => {
1189
1189
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
1190
1190
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
1191
1191
  if (listResult.loadError) {
1192
1192
  return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
1193
1193
  }
1194
- const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdPattern, args.labelPattern);
1194
+ const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
1195
1195
  if ('error' in filtered) {
1196
1196
  return { content: [{ type: 'text', text: filtered.error }], isError: true };
1197
1197
  }
@@ -1212,6 +1212,9 @@ class ComponentExplorerMcpServer extends Disposable {
1212
1212
  sourceTreeId,
1213
1213
  includeImage: false,
1214
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,
1215
1218
  });
1216
1219
  const r = result;
1217
1220
  results.push({
@@ -1256,9 +1259,9 @@ class ComponentExplorerMcpServer extends Disposable {
1256
1259
  _registerCheckTask() {
1257
1260
  this._mcp.registerTool('check_task', {
1258
1261
  description: 'Check on a running task. Waits up to ~2s for completion; if still running, returns progress and new results since last check.',
1259
- inputSchema: {
1262
+ inputSchema: z.strictObject({
1260
1263
  taskId: z.string().describe('The task ID returned by a previous tool call'),
1261
- },
1264
+ }),
1262
1265
  annotations: { readOnlyHint: true },
1263
1266
  }, async (args) => {
1264
1267
  const waited = await this._taskManager.waitForTask(args.taskId, 2_000);
@@ -1306,9 +1309,9 @@ class ComponentExplorerMcpServer extends Disposable {
1306
1309
  _registerCancelTask() {
1307
1310
  this._mcp.registerTool('cancel_task', {
1308
1311
  description: 'Cancel a running task',
1309
- inputSchema: {
1312
+ inputSchema: z.strictObject({
1310
1313
  taskId: z.string().describe('The task ID to cancel'),
1311
- },
1314
+ }),
1312
1315
  }, async (args) => {
1313
1316
  const task = this._taskManager.getTask(args.taskId);
1314
1317
  if (!task) {
@@ -1328,9 +1331,9 @@ class ComponentExplorerMcpServer extends Disposable {
1328
1331
  description: 'Retrieve a recently-taken screenshot image by its hash. ' +
1329
1332
  'Keeps the last ~10 images in an LRU cache. ' +
1330
1333
  'Useful for debugging when screenshot hashes behave unexpectedly.',
1331
- inputSchema: {
1334
+ inputSchema: z.strictObject({
1332
1335
  hash: z.string().describe('The screenshot hash to look up'),
1333
- },
1336
+ }),
1334
1337
  annotations: { readOnlyHint: true },
1335
1338
  }, async (args) => {
1336
1339
  const image = this._imageLru.get(args.hash);
@@ -1354,9 +1357,9 @@ class ComponentExplorerMcpServer extends Disposable {
1354
1357
  'Only use this tool when the user explicitly asks to show or hide the browser. ' +
1355
1358
  'Do not call this tool automatically or as part of other workflows. ' +
1356
1359
  'Note: changing visibility closes the current browser instance, so the next screenshot or evaluate_js call will relaunch it.',
1357
- inputSchema: {
1360
+ inputSchema: z.strictObject({
1358
1361
  visible: z.boolean().describe('true to show the browser window (headed mode), false to hide it (headless mode)'),
1359
- },
1362
+ }),
1360
1363
  annotations: { destructiveHint: true },
1361
1364
  }, async (args) => this._withDaemon(async (daemon) => {
1362
1365
  await daemon.methods.setBrowserVisibility({ visible: args.visible });
@@ -1366,6 +1369,90 @@ class ComponentExplorerMcpServer extends Disposable {
1366
1369
  }));
1367
1370
  }
1368
1371
  }
1372
+ // ---------------------------------------------------------------------------
1373
+ // Client-local state
1374
+ // ---------------------------------------------------------------------------
1375
+ class ImageLruCache {
1376
+ _maxSize;
1377
+ _entries = [];
1378
+ constructor(_maxSize = 10) {
1379
+ this._maxSize = _maxSize;
1380
+ }
1381
+ put(hash, image) {
1382
+ const idx = this._entries.findIndex(e => e.hash === hash);
1383
+ if (idx !== -1) {
1384
+ this._entries.splice(idx, 1);
1385
+ }
1386
+ this._entries.unshift({ hash, image });
1387
+ if (this._entries.length > this._maxSize) {
1388
+ this._entries.length = this._maxSize;
1389
+ }
1390
+ }
1391
+ get(hash) {
1392
+ const idx = this._entries.findIndex(e => e.hash === hash);
1393
+ if (idx === -1) {
1394
+ return undefined;
1395
+ }
1396
+ const [entry] = this._entries.splice(idx, 1);
1397
+ this._entries.unshift(entry);
1398
+ return entry.image;
1399
+ }
1400
+ keys() {
1401
+ return this._entries.map(e => e.hash);
1402
+ }
1403
+ }
1404
+ class WatchList {
1405
+ _fixtureIds = new Set();
1406
+ _hashes = new Map();
1407
+ get fixtureIds() { return this._fixtureIds; }
1408
+ add(ids) {
1409
+ for (const id of ids) {
1410
+ this._fixtureIds.add(id);
1411
+ }
1412
+ }
1413
+ remove(ids) {
1414
+ for (const id of ids) {
1415
+ this._fixtureIds.delete(id);
1416
+ this._hashes.delete(id);
1417
+ }
1418
+ }
1419
+ set(ids) {
1420
+ this._fixtureIds.clear();
1421
+ this._hashes.clear();
1422
+ for (const id of ids) {
1423
+ this._fixtureIds.add(id);
1424
+ }
1425
+ }
1426
+ getHash(fixtureId) {
1427
+ return this._hashes.get(fixtureId);
1428
+ }
1429
+ setHash(fixtureId, hash) {
1430
+ this._hashes.set(fixtureId, hash);
1431
+ }
1432
+ toJSON() {
1433
+ return {
1434
+ fixtureIds: [...this._fixtureIds],
1435
+ hashes: Object.fromEntries(this._hashes),
1436
+ };
1437
+ }
1438
+ }
1439
+ function noDaemonError(hint) {
1440
+ let text = 'Error: No daemon is currently running.';
1441
+ if (hint) {
1442
+ text += ` ${hint}`;
1443
+ }
1444
+ else {
1445
+ text += ' Please start the Component Explorer daemon first by running:\n\n' +
1446
+ ' component-explorer serve --project <config.json>\n\n' +
1447
+ 'Or start it in the background:\n\n' +
1448
+ ' component-explorer serve --project <config.json> --background\n\n' +
1449
+ 'The daemon manages dev servers and enables fixture screenshots.';
1450
+ }
1451
+ return {
1452
+ content: [{ type: 'text', text }],
1453
+ isError: true,
1454
+ };
1455
+ }
1369
1456
 
1370
- export { ComponentExplorerMcpServer, DaemonConnection };
1457
+ export { ComponentExplorerMcpServer };
1371
1458
  //# sourceMappingURL=McpServer.js.map