@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.
- package/README.md +8 -0
- package/dist/_virtual/_build-info.js +1 -1
- package/dist/browserPage.d.ts +1 -1
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +29 -1
- package/dist/browserPage.js.map +1 -1
- package/dist/commands/acceptCommand.d.ts +1 -0
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +3 -2
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/artifactCommand.d.ts +1 -0
- package/dist/commands/artifactCommand.d.ts.map +1 -1
- package/dist/commands/checkStabilityCommand.d.ts +1 -0
- package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
- package/dist/commands/checkStabilityCommand.js +3 -2
- package/dist/commands/checkStabilityCommand.js.map +1 -1
- package/dist/commands/compareCommand.d.ts +1 -0
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +3 -2
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +3 -0
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +175 -43
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/renderCommand.d.ts +1 -0
- package/dist/commands/renderCommand.d.ts.map +1 -1
- package/dist/commands/renderCommand.js +6 -3
- package/dist/commands/renderCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +2 -0
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +65 -36
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/serviceDiffCommitsCommand.d.ts +16 -0
- package/dist/commands/serviceDiffCommitsCommand.d.ts.map +1 -0
- package/dist/commands/serviceDiffCommitsCommand.js +229 -0
- package/dist/commands/serviceDiffCommitsCommand.js.map +1 -0
- package/dist/commands/watchCommand.d.ts +1 -0
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +4 -3
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/componentExplorer.d.ts +3 -0
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +84 -50
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.js +5 -5
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/client.d.ts +5 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +7 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/inProcessClient.d.ts +11 -0
- package/dist/daemon/inProcessClient.d.ts.map +1 -0
- package/dist/daemon/inProcessClient.js +35 -0
- package/dist/daemon/inProcessClient.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +6 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +12 -2
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts +2 -0
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +21 -3
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeName.d.ts +6 -0
- package/dist/daemon/pipeName.d.ts.map +1 -1
- package/dist/daemon/pipeName.js +8 -3
- package/dist/daemon/pipeName.js.map +1 -1
- package/dist/daemon/pipeServer.d.ts.map +1 -1
- package/dist/daemon/pipeServer.js +9 -3
- package/dist/daemon/pipeServer.js.map +1 -1
- package/dist/evaluateFn.d.ts +21 -0
- package/dist/evaluateFn.d.ts.map +1 -0
- package/dist/evaluateFn.js +17 -0
- package/dist/evaluateFn.js.map +1 -0
- package/dist/git/gitUtils.d.ts +1 -0
- package/dist/git/gitUtils.d.ts.map +1 -1
- package/dist/git/gitUtils.js +5 -1
- package/dist/git/gitUtils.js.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +12 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +35 -2
- package/dist/logger.js.map +1 -1
- package/dist/mcp/DaemonAccessor.d.ts +22 -0
- package/dist/mcp/DaemonAccessor.d.ts.map +1 -0
- package/dist/mcp/McpServer.d.ts +5 -19
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +228 -223
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/packages/simple-api/dist/{chunk-3R7GHWBM.js → chunk-FJ7AVNQE.js} +2 -1
- package/dist/packages/simple-api/dist/chunk-FJ7AVNQE.js.map +1 -0
- package/dist/packages/simple-api/dist/{chunk-SGBCNXYH.js → chunk-TTRCY65Z.js} +4 -1
- package/dist/packages/simple-api/dist/chunk-TTRCY65Z.js.map +1 -0
- package/dist/packages/simple-api/dist/{chunk-TAEFVNPN.js → chunk-WNXMRXWV.js} +2 -1
- package/dist/packages/simple-api/dist/chunk-WNXMRXWV.js.map +1 -0
- package/dist/packages/simple-api/dist/express.js +1 -1
- package/dist/packages/simple-api/dist/express.js.map +1 -1
- package/dist/processTree.d.ts +9 -0
- package/dist/processTree.d.ts.map +1 -0
- package/dist/processTree.js +41 -0
- package/dist/processTree.js.map +1 -0
- package/dist/screenshotServiceClient.d.ts +31 -0
- package/dist/screenshotServiceClient.d.ts.map +1 -0
- package/dist/screenshotServiceClient.js +38 -0
- package/dist/screenshotServiceClient.js.map +1 -0
- package/dist/server/httpServer.d.ts.map +1 -1
- package/dist/server/httpServer.js +20 -18
- package/dist/server/httpServer.js.map +1 -1
- package/dist/server/viteServer.js +5 -5
- package/dist/server/viteServer.js.map +1 -1
- package/dist/utils.d.ts +11 -6
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +7 -6
- package/dist/utils.js.map +1 -1
- package/package.json +20 -17
- package/dist/commands/screenshotCommand.d.ts +0 -18
- package/dist/commands/screenshotCommand.d.ts.map +0 -1
- package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +0 -1
package/dist/mcp/McpServer.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
34
|
+
constructor(_daemon, options) {
|
|
142
35
|
super();
|
|
143
|
-
this.
|
|
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
|
|
154
|
-
await this._onDaemonChanged(
|
|
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.
|
|
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.
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 (
|
|
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
|
|
1099
|
-
'
|
|
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.
|
|
1103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1375
|
+
export { ComponentExplorerMcpServer };
|
|
1371
1376
|
//# sourceMappingURL=McpServer.js.map
|