@vscode/component-explorer-cli 0.2.1-2 → 0.2.1-21

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 (120) hide show
  1. package/README.md +8 -0
  2. package/dist/_virtual/_build-info.js +1 -1
  3. package/dist/browserPage.d.ts +1 -1
  4. package/dist/browserPage.d.ts.map +1 -1
  5. package/dist/browserPage.js +29 -1
  6. package/dist/browserPage.js.map +1 -1
  7. package/dist/commands/acceptCommand.d.ts +1 -0
  8. package/dist/commands/acceptCommand.d.ts.map +1 -1
  9. package/dist/commands/acceptCommand.js +3 -2
  10. package/dist/commands/acceptCommand.js.map +1 -1
  11. package/dist/commands/artifactCommand.d.ts +1 -0
  12. package/dist/commands/artifactCommand.d.ts.map +1 -1
  13. package/dist/commands/checkStabilityCommand.d.ts +1 -0
  14. package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
  15. package/dist/commands/checkStabilityCommand.js +3 -2
  16. package/dist/commands/checkStabilityCommand.js.map +1 -1
  17. package/dist/commands/compareCommand.d.ts +1 -0
  18. package/dist/commands/compareCommand.d.ts.map +1 -1
  19. package/dist/commands/compareCommand.js +3 -2
  20. package/dist/commands/compareCommand.js.map +1 -1
  21. package/dist/commands/mcpCommand.d.ts +3 -0
  22. package/dist/commands/mcpCommand.d.ts.map +1 -1
  23. package/dist/commands/mcpCommand.js +175 -43
  24. package/dist/commands/mcpCommand.js.map +1 -1
  25. package/dist/commands/renderCommand.d.ts +1 -0
  26. package/dist/commands/renderCommand.d.ts.map +1 -1
  27. package/dist/commands/renderCommand.js +6 -3
  28. package/dist/commands/renderCommand.js.map +1 -1
  29. package/dist/commands/serveCommand.d.ts +2 -0
  30. package/dist/commands/serveCommand.d.ts.map +1 -1
  31. package/dist/commands/serveCommand.js +65 -36
  32. package/dist/commands/serveCommand.js.map +1 -1
  33. package/dist/commands/serviceDiffCommitsCommand.d.ts +16 -0
  34. package/dist/commands/serviceDiffCommitsCommand.d.ts.map +1 -0
  35. package/dist/commands/serviceDiffCommitsCommand.js +229 -0
  36. package/dist/commands/serviceDiffCommitsCommand.js.map +1 -0
  37. package/dist/commands/watchCommand.d.ts +1 -0
  38. package/dist/commands/watchCommand.d.ts.map +1 -1
  39. package/dist/commands/watchCommand.js +4 -3
  40. package/dist/commands/watchCommand.js.map +1 -1
  41. package/dist/componentExplorer.d.ts +3 -0
  42. package/dist/componentExplorer.d.ts.map +1 -1
  43. package/dist/componentExplorer.js +84 -50
  44. package/dist/componentExplorer.js.map +1 -1
  45. package/dist/daemon/DaemonService.js +5 -5
  46. package/dist/daemon/DaemonService.js.map +1 -1
  47. package/dist/daemon/client.d.ts +5 -0
  48. package/dist/daemon/client.d.ts.map +1 -0
  49. package/dist/daemon/client.js +7 -0
  50. package/dist/daemon/client.js.map +1 -0
  51. package/dist/daemon/inProcessClient.d.ts +11 -0
  52. package/dist/daemon/inProcessClient.d.ts.map +1 -0
  53. package/dist/daemon/inProcessClient.js +35 -0
  54. package/dist/daemon/inProcessClient.js.map +1 -0
  55. package/dist/daemon/lifecycle.d.ts +6 -0
  56. package/dist/daemon/lifecycle.d.ts.map +1 -1
  57. package/dist/daemon/lifecycle.js +12 -2
  58. package/dist/daemon/lifecycle.js.map +1 -1
  59. package/dist/daemon/pipeClient.d.ts +2 -0
  60. package/dist/daemon/pipeClient.d.ts.map +1 -1
  61. package/dist/daemon/pipeClient.js +21 -3
  62. package/dist/daemon/pipeClient.js.map +1 -1
  63. package/dist/daemon/pipeName.d.ts +6 -0
  64. package/dist/daemon/pipeName.d.ts.map +1 -1
  65. package/dist/daemon/pipeName.js +8 -3
  66. package/dist/daemon/pipeName.js.map +1 -1
  67. package/dist/daemon/pipeServer.d.ts.map +1 -1
  68. package/dist/daemon/pipeServer.js +9 -3
  69. package/dist/daemon/pipeServer.js.map +1 -1
  70. package/dist/evaluateFn.d.ts +21 -0
  71. package/dist/evaluateFn.d.ts.map +1 -0
  72. package/dist/evaluateFn.js +17 -0
  73. package/dist/evaluateFn.js.map +1 -0
  74. package/dist/git/gitUtils.d.ts +1 -0
  75. package/dist/git/gitUtils.d.ts.map +1 -1
  76. package/dist/git/gitUtils.js +5 -1
  77. package/dist/git/gitUtils.js.map +1 -1
  78. package/dist/index.js +9 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/logger.d.ts +12 -1
  81. package/dist/logger.d.ts.map +1 -1
  82. package/dist/logger.js +35 -2
  83. package/dist/logger.js.map +1 -1
  84. package/dist/mcp/DaemonAccessor.d.ts +22 -0
  85. package/dist/mcp/DaemonAccessor.d.ts.map +1 -0
  86. package/dist/mcp/McpServer.d.ts +5 -19
  87. package/dist/mcp/McpServer.d.ts.map +1 -1
  88. package/dist/mcp/McpServer.js +228 -223
  89. package/dist/mcp/McpServer.js.map +1 -1
  90. package/dist/packages/simple-api/dist/{chunk-3R7GHWBM.js → chunk-FJ7AVNQE.js} +2 -1
  91. package/dist/packages/simple-api/dist/chunk-FJ7AVNQE.js.map +1 -0
  92. package/dist/packages/simple-api/dist/{chunk-SGBCNXYH.js → chunk-TTRCY65Z.js} +4 -1
  93. package/dist/packages/simple-api/dist/chunk-TTRCY65Z.js.map +1 -0
  94. package/dist/packages/simple-api/dist/{chunk-TAEFVNPN.js → chunk-WNXMRXWV.js} +2 -1
  95. package/dist/packages/simple-api/dist/chunk-WNXMRXWV.js.map +1 -0
  96. package/dist/packages/simple-api/dist/express.js +1 -1
  97. package/dist/packages/simple-api/dist/express.js.map +1 -1
  98. package/dist/processTree.d.ts +9 -0
  99. package/dist/processTree.d.ts.map +1 -0
  100. package/dist/processTree.js +41 -0
  101. package/dist/processTree.js.map +1 -0
  102. package/dist/screenshotServiceClient.d.ts +31 -0
  103. package/dist/screenshotServiceClient.d.ts.map +1 -0
  104. package/dist/screenshotServiceClient.js +38 -0
  105. package/dist/screenshotServiceClient.js.map +1 -0
  106. package/dist/server/httpServer.d.ts.map +1 -1
  107. package/dist/server/httpServer.js +20 -18
  108. package/dist/server/httpServer.js.map +1 -1
  109. package/dist/server/viteServer.js +5 -5
  110. package/dist/server/viteServer.js.map +1 -1
  111. package/dist/utils.d.ts +11 -6
  112. package/dist/utils.d.ts.map +1 -1
  113. package/dist/utils.js +7 -6
  114. package/dist/utils.js.map +1 -1
  115. package/package.json +20 -17
  116. package/dist/commands/screenshotCommand.d.ts +0 -18
  117. package/dist/commands/screenshotCommand.d.ts.map +0 -1
  118. package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js.map +0 -1
  119. package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js.map +0 -1
  120. package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +0 -1
@@ -10,118 +10,13 @@ import '../external/vscode-observables/observables/dist/observableInternal/utils
10
10
  import '../external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
11
11
  import { buildExplorerUrl } from '../utils.js';
12
12
  import { TaskManager } from './TaskManager.js';
13
+ import { isPipeConnectionError } from '../daemon/pipeClient.js';
13
14
 
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
15
  // ---------------------------------------------------------------------------
121
16
  // ComponentExplorerMcpServer
122
17
  // ---------------------------------------------------------------------------
123
18
  class ComponentExplorerMcpServer extends Disposable {
124
- _daemonConnection;
19
+ _daemon;
125
20
  static async create(daemon, options) {
126
21
  const server = new ComponentExplorerMcpServer(daemon, options ?? {});
127
22
  const transport = new StdioServerTransport();
@@ -133,16 +28,12 @@ class ComponentExplorerMcpServer extends Disposable {
133
28
  _imageLru = new ImageLruCache(10);
134
29
  _taskManager = new TaskManager();
135
30
  _taskLastReportedIndex = new Map();
136
- _pollFn;
137
- _noAutostartHint;
138
31
  _multiSessionTools = [];
139
32
  _sessions = [];
140
33
  _eventStreamAbortController;
141
- constructor(_daemonConnection, options) {
34
+ constructor(_daemon, options) {
142
35
  super();
143
- this._daemonConnection = _daemonConnection;
144
- this._pollFn = options.pollFn;
145
- this._noAutostartHint = options.noAutostartHint;
36
+ this._daemon = _daemon;
146
37
  this._callTimeoutMs = options.callTimeoutMs ?? ComponentExplorerMcpServer._DEFAULT_CALL_TIMEOUT_MS;
147
38
  this._mcp = new McpServer({
148
39
  name: 'component-explorer',
@@ -150,8 +41,8 @@ class ComponentExplorerMcpServer extends Disposable {
150
41
  });
151
42
  this._registerTools();
152
43
  this._store.add(autorun(async (reader) => {
153
- const conn = this._daemonConnection.read(reader);
154
- await this._onDaemonChanged(conn?.client);
44
+ const client = this._daemon.connection.read(reader);
45
+ await this._onDaemonChanged(client);
155
46
  }));
156
47
  }
157
48
  async _onDaemonChanged(daemon) {
@@ -173,49 +64,13 @@ class ComponentExplorerMcpServer extends Disposable {
173
64
  this._sessions = [];
174
65
  }
175
66
  }
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
67
  _noDaemonError() {
213
- return noDaemonError(this._noAutostartHint);
68
+ return noDaemonError(this._daemon.noDaemonHint);
214
69
  }
215
70
  static _DEFAULT_CALL_TIMEOUT_MS = 15_000;
216
71
  _callTimeoutMs;
217
72
  async _withDaemon(fn, options) {
218
- const daemon = await this._waitForDaemon();
73
+ const daemon = await this._daemon.getDaemonOrStart();
219
74
  if (!daemon) {
220
75
  return this._noDaemonError();
221
76
  }
@@ -235,7 +90,6 @@ class ComponentExplorerMcpServer extends Disposable {
235
90
  }
236
91
  if (isPipeConnectionError(e)) {
237
92
  this._log('debug', { type: 'daemon-call-failed', error: String(e) });
238
- this._handleDisconnect();
239
93
  return this._noDaemonError();
240
94
  }
241
95
  throw e;
@@ -259,7 +113,7 @@ class ComponentExplorerMcpServer extends Disposable {
259
113
  this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
260
114
  }
261
115
  if (event.type === 'ref-change' || event.type === 'session-change') {
262
- await this._refreshSessions();
116
+ await this._refreshSessions(daemon);
263
117
  }
264
118
  this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
265
119
  }
@@ -293,24 +147,11 @@ class ComponentExplorerMcpServer extends Disposable {
293
147
  s.sourceTreeId = sourceTreeId;
294
148
  }
295
149
  }
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
- }
150
+ async _refreshSessions(daemon) {
151
+ const prevCount = this._sessions.length;
152
+ this._sessions = await daemon.methods.sessions();
153
+ if (this._sessions.length !== prevCount) {
154
+ this._updateMultiSessionToolVisibility();
314
155
  }
315
156
  }
316
157
  _updateMultiSessionToolVisibility() {
@@ -324,14 +165,14 @@ class ComponentExplorerMcpServer extends Disposable {
324
165
  }
325
166
  }
326
167
  }
327
- async _withSourceTreeRetry(fn) {
168
+ async _withSourceTreeRetry(daemon, fn) {
328
169
  try {
329
170
  return await fn();
330
171
  }
331
172
  catch (e) {
332
173
  const msg = e instanceof Error ? e.message : String(e);
333
174
  if (msg.includes('Source tree changed')) {
334
- await this._refreshSessions();
175
+ await this._refreshSessions(daemon);
335
176
  return await fn();
336
177
  }
337
178
  throw e;
@@ -371,10 +212,10 @@ class ComponentExplorerMcpServer extends Disposable {
371
212
  this._registerCheckVisuals();
372
213
  this._registerEvaluateJs();
373
214
  this._registerDebugReloadPage();
374
- this._registerWatchAdd();
375
- this._registerWatchRemove();
376
- this._registerWatchSet();
377
- this._registerWatchCompare();
215
+ //this._registerWatchAdd();
216
+ //this._registerWatchRemove();
217
+ //this._registerWatchSet();
218
+ //this._registerWatchCompare();
378
219
  this._registerWaitForUpdate();
379
220
  this._registerSessions();
380
221
  this._registerRestartSession();
@@ -382,6 +223,7 @@ class ComponentExplorerMcpServer extends Disposable {
382
223
  this._registerCloseSession();
383
224
  this._registerUpdateSessionRef();
384
225
  this._registerGetUrl();
226
+ this._registerCheckFixtureErrors();
385
227
  this._registerCheckStability();
386
228
  this._registerCheckTask();
387
229
  this._registerCancelTask();
@@ -401,7 +243,7 @@ class ComponentExplorerMcpServer extends Disposable {
401
243
  }, async (args) => this._withDaemon(async (daemon) => {
402
244
  const sessionName = args.sessionName ?? this._defaultSessionName();
403
245
  this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
404
- return this._withSourceTreeRetry(async () => {
246
+ return this._withSourceTreeRetry(daemon, async () => {
405
247
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
406
248
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
407
249
  const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdPattern, args.labelPattern);
@@ -432,7 +274,7 @@ class ComponentExplorerMcpServer extends Disposable {
432
274
  const sessionName = args.sessionName ?? this._defaultSessionName();
433
275
  this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
434
276
  this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
435
- return this._withSourceTreeRetry(async () => {
277
+ return this._withSourceTreeRetry(daemon, async () => {
436
278
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
437
279
  const result = await daemon.methods.screenshots.take({
438
280
  fixtureId: args.fixtureId,
@@ -521,7 +363,7 @@ class ComponentExplorerMcpServer extends Disposable {
521
363
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
522
364
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
523
365
  this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
524
- return this._withSourceTreeRetry(async () => {
366
+ return this._withSourceTreeRetry(daemon, async () => {
525
367
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
526
368
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
527
369
  const result = await daemon.methods.screenshots.compare({
@@ -619,7 +461,7 @@ class ComponentExplorerMcpServer extends Disposable {
619
461
  }, async (args) => this._withDaemon(async (daemon) => {
620
462
  const sessionName = args.sessionName ?? this._defaultSessionName();
621
463
  this._log('debug', { type: 'tool-call', tool: 'review_visual', fixtureId: args.fixtureId, verdict: args.verdict });
622
- return this._withSourceTreeRetry(async () => {
464
+ return this._withSourceTreeRetry(daemon, async () => {
623
465
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
624
466
  // Get fixture descriptions
625
467
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
@@ -669,7 +511,7 @@ class ComponentExplorerMcpServer extends Disposable {
669
511
  }, async (args) => this._withDaemon(async (daemon) => {
670
512
  const sessionName = args.sessionName ?? this._defaultSessionName();
671
513
  this._log('debug', { type: 'tool-call', tool: 'check_visuals', sessionName });
672
- return this._withSourceTreeRetry(async () => {
514
+ return this._withSourceTreeRetry(daemon, async () => {
673
515
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
674
516
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
675
517
  if (listResult.loadError) {
@@ -743,7 +585,7 @@ class ComponentExplorerMcpServer extends Disposable {
743
585
  const sessionName = args.sessionName ?? this._defaultSessionName();
744
586
  this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
745
587
  this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
746
- return this._withSourceTreeRetry(async () => {
588
+ return this._withSourceTreeRetry(daemon, async () => {
747
589
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
748
590
  const result = await daemon.methods.evaluate({
749
591
  sessionName,
@@ -852,7 +694,7 @@ class ComponentExplorerMcpServer extends Disposable {
852
694
  }
853
695
  const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
854
696
  const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
855
- return this._withSourceTreeRetry(async () => {
697
+ return this._withSourceTreeRetry(daemon, async () => {
856
698
  const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
857
699
  const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
858
700
  const [baselineResult, currentResult] = await Promise.all([
@@ -945,7 +787,7 @@ class ComponentExplorerMcpServer extends Disposable {
945
787
  this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
946
788
  }
947
789
  if (ev.type === 'ref-change') {
948
- const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
790
+ const refreshResult = await Promise.race([this._refreshSessions(daemon), timeout]);
949
791
  if (refreshResult === 'timeout') {
950
792
  return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
951
793
  }
@@ -1002,8 +844,8 @@ class ComponentExplorerMcpServer extends Disposable {
1002
844
  this._mcp.registerTool('sessions', {
1003
845
  description: 'List active sessions with their names, URLs, and current sourceTreeIds',
1004
846
  annotations: { readOnlyHint: true },
1005
- }, async () => this._withDaemon(async (_daemon) => {
1006
- await this._refreshSessions();
847
+ }, async () => this._withDaemon(async (daemon) => {
848
+ await this._refreshSessions(daemon);
1007
849
  return {
1008
850
  content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
1009
851
  };
@@ -1095,33 +937,19 @@ class ComponentExplorerMcpServer extends Disposable {
1095
937
  }
1096
938
  _registerGetUrl() {
1097
939
  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.',
940
+ description: 'Get URL for viewing fixtures. Returns the full Component Explorer UI by default. ' +
941
+ 'Use `embedded: true` for a minimal single-fixture view (requires fixtureId).',
1100
942
  inputSchema: {
1101
943
  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.'),
944
+ 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.'),
945
+ embedded: z.boolean().optional().describe('If true, returns an embedded single-fixture URL (minimal UI, requires fixtureId). Default: false (full explorer UI).'),
1105
946
  },
1106
947
  annotations: { readOnlyHint: true },
1107
- }, async (args) => {
948
+ }, async (args) => this._withDaemon(async (daemon) => {
1108
949
  const sessionName = args.sessionName ?? this._defaultSessionName();
1109
950
  let session = this._sessions.find(s => s.name === sessionName);
1110
951
  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
- }
952
+ await this._refreshSessions(daemon);
1125
953
  session = this._sessions.find(s => s.name === sessionName);
1126
954
  if (!session) {
1127
955
  return {
@@ -1143,10 +971,20 @@ class ComponentExplorerMcpServer extends Disposable {
1143
971
  isError: true,
1144
972
  };
1145
973
  }
1146
- const useRaw = args.useRawDirectRenderingWithoutExplorerUi ?? false;
974
+ // Validate embedded mode requires fixtureId
975
+ if (args.embedded && !args.fixtureId) {
976
+ return {
977
+ content: [{
978
+ type: 'text',
979
+ text: 'Error: embedded mode requires a fixtureId.',
980
+ }],
981
+ isError: true,
982
+ };
983
+ }
984
+ const mode = args.embedded ? 'embedded' : undefined;
1147
985
  const url = buildExplorerUrl({
1148
986
  baseUrl,
1149
- rawRender: useRaw,
987
+ mode,
1150
988
  fixtureId: args.fixtureId,
1151
989
  });
1152
990
  const result = {
@@ -1156,19 +994,102 @@ class ComponentExplorerMcpServer extends Disposable {
1156
994
  if (args.fixtureId) {
1157
995
  result.fixtureId = args.fixtureId;
1158
996
  }
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
- }
997
+ result.mode = args.embedded ? 'embedded' : 'explorer';
1168
998
  return {
1169
999
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1170
1000
  };
1171
- });
1001
+ }));
1002
+ }
1003
+ _registerCheckFixtureErrors() {
1004
+ this._mcp.registerTool('check_fixture_errors', {
1005
+ description: 'Render fixtures and check for errors. Returns each fixture\'s error status. ' +
1006
+ 'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task.',
1007
+ inputSchema: {
1008
+ fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
1009
+ labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
1010
+ sessionName: z.string().optional().describe('Session name (defaults to first session)'),
1011
+ sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
1012
+ },
1013
+ annotations: { readOnlyHint: true },
1014
+ }, async (args) => this._withDaemon(async (daemon) => {
1015
+ const sessionName = args.sessionName ?? this._defaultSessionName();
1016
+ this._log('debug', { type: 'tool-call', tool: 'check_fixture_errors', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
1017
+ return this._withSourceTreeRetry(daemon, async () => {
1018
+ const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
1019
+ const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
1020
+ if (listResult.loadError) {
1021
+ return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
1022
+ }
1023
+ const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdPattern, args.labelPattern);
1024
+ if ('error' in filtered) {
1025
+ return { content: [{ type: 'text', text: filtered.error }], isError: true };
1026
+ }
1027
+ const fixtures = filtered.fixtures;
1028
+ const task = this._taskManager.startTask(async (report, signal) => {
1029
+ const results = [];
1030
+ report({ completed: 0, total: fixtures.length, partialResult: results });
1031
+ for (let i = 0; i < fixtures.length; i++) {
1032
+ if (signal.aborted) {
1033
+ break;
1034
+ }
1035
+ const fixture = fixtures[i];
1036
+ try {
1037
+ const result = await daemon.methods.screenshots.take({
1038
+ fixtureId: fixture.fixtureId,
1039
+ sessionName,
1040
+ sourceTreeId,
1041
+ includeImage: false,
1042
+ });
1043
+ const r = result;
1044
+ const entry = { fixtureId: fixture.fixtureId, hasError: r.hasError };
1045
+ if (r.error) {
1046
+ entry.error = r.error;
1047
+ }
1048
+ if (r.events && r.events.length > 0) {
1049
+ entry.events = r.events;
1050
+ }
1051
+ results.push(entry);
1052
+ }
1053
+ catch (e) {
1054
+ results.push({
1055
+ fixtureId: fixture.fixtureId,
1056
+ hasError: true,
1057
+ error: { message: e instanceof Error ? e.message : String(e) },
1058
+ });
1059
+ }
1060
+ report({ completed: i + 1, total: fixtures.length, partialResult: results });
1061
+ }
1062
+ const withErrors = results.filter(r => r.hasError);
1063
+ return {
1064
+ fixtures: results,
1065
+ summary: { total: results.length, ok: results.length - withErrors.length, errored: withErrors.length },
1066
+ };
1067
+ });
1068
+ const waited = await this._taskManager.waitForTask(task.id, 10_000);
1069
+ if (!waited) {
1070
+ return { content: [{ type: 'text', text: 'Error: task disappeared' }], isError: true };
1071
+ }
1072
+ if (waited.done) {
1073
+ return {
1074
+ content: [{ type: 'text', text: JSON.stringify(waited.result, null, 2) }],
1075
+ };
1076
+ }
1077
+ const partial = waited.progress.partialResult;
1078
+ this._taskLastReportedIndex.set(task.id, partial.length);
1079
+ return {
1080
+ content: [{
1081
+ type: 'text',
1082
+ text: JSON.stringify({
1083
+ taskId: task.id,
1084
+ status: 'running',
1085
+ progress: { completed: waited.progress.completed, total: waited.progress.total },
1086
+ elapsedMs: waited.elapsedMs,
1087
+ results: partial,
1088
+ }, null, 2),
1089
+ }],
1090
+ };
1091
+ });
1092
+ }));
1172
1093
  }
1173
1094
  _registerCheckStability() {
1174
1095
  this._mcp.registerTool('check_stability', {
@@ -1185,7 +1106,7 @@ class ComponentExplorerMcpServer extends Disposable {
1185
1106
  }, async (args) => this._withDaemon(async (daemon) => {
1186
1107
  const sessionName = args.sessionName ?? this._defaultSessionName();
1187
1108
  this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
1188
- return this._withSourceTreeRetry(async () => {
1109
+ return this._withSourceTreeRetry(daemon, async () => {
1189
1110
  const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
1190
1111
  const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
1191
1112
  if (listResult.loadError) {
@@ -1366,6 +1287,90 @@ class ComponentExplorerMcpServer extends Disposable {
1366
1287
  }));
1367
1288
  }
1368
1289
  }
1290
+ // ---------------------------------------------------------------------------
1291
+ // Client-local state
1292
+ // ---------------------------------------------------------------------------
1293
+ class ImageLruCache {
1294
+ _maxSize;
1295
+ _entries = [];
1296
+ constructor(_maxSize = 10) {
1297
+ this._maxSize = _maxSize;
1298
+ }
1299
+ put(hash, image) {
1300
+ const idx = this._entries.findIndex(e => e.hash === hash);
1301
+ if (idx !== -1) {
1302
+ this._entries.splice(idx, 1);
1303
+ }
1304
+ this._entries.unshift({ hash, image });
1305
+ if (this._entries.length > this._maxSize) {
1306
+ this._entries.length = this._maxSize;
1307
+ }
1308
+ }
1309
+ get(hash) {
1310
+ const idx = this._entries.findIndex(e => e.hash === hash);
1311
+ if (idx === -1) {
1312
+ return undefined;
1313
+ }
1314
+ const [entry] = this._entries.splice(idx, 1);
1315
+ this._entries.unshift(entry);
1316
+ return entry.image;
1317
+ }
1318
+ keys() {
1319
+ return this._entries.map(e => e.hash);
1320
+ }
1321
+ }
1322
+ class WatchList {
1323
+ _fixtureIds = new Set();
1324
+ _hashes = new Map();
1325
+ get fixtureIds() { return this._fixtureIds; }
1326
+ add(ids) {
1327
+ for (const id of ids) {
1328
+ this._fixtureIds.add(id);
1329
+ }
1330
+ }
1331
+ remove(ids) {
1332
+ for (const id of ids) {
1333
+ this._fixtureIds.delete(id);
1334
+ this._hashes.delete(id);
1335
+ }
1336
+ }
1337
+ set(ids) {
1338
+ this._fixtureIds.clear();
1339
+ this._hashes.clear();
1340
+ for (const id of ids) {
1341
+ this._fixtureIds.add(id);
1342
+ }
1343
+ }
1344
+ getHash(fixtureId) {
1345
+ return this._hashes.get(fixtureId);
1346
+ }
1347
+ setHash(fixtureId, hash) {
1348
+ this._hashes.set(fixtureId, hash);
1349
+ }
1350
+ toJSON() {
1351
+ return {
1352
+ fixtureIds: [...this._fixtureIds],
1353
+ hashes: Object.fromEntries(this._hashes),
1354
+ };
1355
+ }
1356
+ }
1357
+ function noDaemonError(hint) {
1358
+ let text = 'Error: No daemon is currently running.';
1359
+ if (hint) {
1360
+ text += ` ${hint}`;
1361
+ }
1362
+ else {
1363
+ text += ' Please start the Component Explorer daemon first by running:\n\n' +
1364
+ ' component-explorer serve --project <config.json>\n\n' +
1365
+ 'Or start it in the background:\n\n' +
1366
+ ' component-explorer serve --project <config.json> --background\n\n' +
1367
+ 'The daemon manages dev servers and enables fixture screenshots.';
1368
+ }
1369
+ return {
1370
+ content: [{ type: 'text', text }],
1371
+ isError: true,
1372
+ };
1373
+ }
1369
1374
 
1370
- export { ComponentExplorerMcpServer, DaemonConnection };
1375
+ export { ComponentExplorerMcpServer };
1371
1376
  //# sourceMappingURL=McpServer.js.map