@vscode/component-explorer-cli 0.1.1-9 → 0.2.1-0
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/LICENSE +21 -0
- package/SECURITY.md +14 -0
- package/dist/WorktreePool.d.ts +22 -0
- package/dist/WorktreePool.d.ts.map +1 -0
- package/dist/WorktreePool.js +58 -0
- package/dist/WorktreePool.js.map +1 -0
- package/dist/WorktreePool.test.d.ts +2 -0
- package/dist/WorktreePool.test.d.ts.map +1 -0
- package/dist/_virtual/_build-info.js +4 -0
- package/dist/_virtual/_build-info.js.map +1 -0
- package/dist/browserPage.d.ts +15 -1
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +51 -7
- package/dist/browserPage.js.map +1 -1
- 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/checkStabilityCommand.d.ts +12 -0
- package/dist/commands/checkStabilityCommand.d.ts.map +1 -0
- package/dist/commands/checkStabilityCommand.js +84 -0
- package/dist/commands/checkStabilityCommand.js.map +1 -0
- package/dist/commands/compareCommand.d.ts +1 -0
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +25 -4
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +1 -0
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +23 -10
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts +2 -0
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +19 -4
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +4 -0
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +101 -26
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +2 -0
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +18 -66
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/comparison.d.ts +11 -1
- package/dist/comparison.d.ts.map +1 -1
- package/dist/comparison.js +25 -11
- package/dist/comparison.js.map +1 -1
- package/dist/component-explorer-config.schema.json +260 -55
- package/dist/componentExplorer.d.ts +21 -18
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +60 -19
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.d.ts +100 -11
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +512 -129
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/dynamicSessions.test.d.ts +2 -0
- package/dist/daemon/dynamicSessions.test.d.ts.map +1 -0
- package/dist/daemon/lifecycle.d.ts +2 -1
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +52 -30
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +81 -2
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeServer.d.ts +2 -1
- package/dist/daemon/pipeServer.d.ts.map +1 -1
- package/dist/daemon/pipeServer.js +59 -2
- package/dist/daemon/pipeServer.js.map +1 -1
- package/dist/daemon/version.d.ts +10 -0
- package/dist/daemon/version.d.ts.map +1 -0
- package/dist/daemon/version.js +17 -0
- package/dist/daemon/version.js.map +1 -0
- package/dist/dependencyInstaller.d.ts +2 -2
- package/dist/dependencyInstaller.d.ts.map +1 -1
- package/dist/dependencyInstaller.js.map +1 -1
- package/dist/explorerSession.d.ts +3 -3
- package/dist/explorerSession.d.ts.map +1 -1
- package/dist/explorerSession.js +26 -9
- package/dist/explorerSession.js.map +1 -1
- package/dist/git/gitIndexResolver.d.ts +25 -0
- package/dist/git/gitIndexResolver.d.ts.map +1 -0
- package/dist/git/gitIndexResolver.js +91 -0
- package/dist/git/gitIndexResolver.js.map +1 -0
- package/dist/git/gitIndexResolver.test.d.ts +2 -0
- package/dist/git/gitIndexResolver.test.d.ts.map +1 -0
- package/dist/git/gitService.d.ts +2 -0
- package/dist/git/gitService.d.ts.map +1 -1
- package/dist/git/gitService.js +6 -0
- package/dist/git/gitService.js.map +1 -1
- package/dist/git/gitWorktreeManager.d.ts +6 -0
- package/dist/git/gitWorktreeManager.d.ts.map +1 -1
- package/dist/git/gitWorktreeManager.js +42 -13
- package/dist/git/gitWorktreeManager.js.map +1 -1
- package/dist/git/gitWorktreeManager.test.d.ts +2 -0
- package/dist/git/gitWorktreeManager.test.d.ts.map +1 -0
- package/dist/git/testUtils.d.ts +13 -0
- package/dist/git/testUtils.d.ts.map +1 -0
- package/dist/httpServer.d.ts +18 -7
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +117 -18
- package/dist/httpServer.js.map +1 -1
- package/dist/httpServer.test.d.ts +2 -0
- package/dist/httpServer.test.d.ts.map +1 -0
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +7 -1
- package/dist/logger.js.map +1 -1
- package/dist/mcp/McpServer.d.ts +18 -0
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +555 -13
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/mcp/TaskManager.d.ts +28 -0
- package/dist/mcp/TaskManager.d.ts.map +1 -0
- package/dist/mcp/TaskManager.js +54 -0
- package/dist/mcp/TaskManager.js.map +1 -0
- package/dist/packages/simple-api/dist/{chunk-Q24JOMNK.js → chunk-TAEFVNPN.js} +1 -1
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +1 -0
- package/dist/packages/simple-api/dist/express.js +11 -3
- package/dist/packages/simple-api/dist/express.js.map +1 -1
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +6 -7
- package/dist/utils.js.map +1 -1
- package/dist/visualCache.d.ts +34 -0
- package/dist/visualCache.d.ts.map +1 -0
- package/dist/visualCache.js +90 -0
- package/dist/visualCache.js.map +1 -0
- package/dist/watchConfig.d.ts +68 -15
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +109 -65
- package/dist/watchConfig.js.map +1 -1
- package/package.json +21 -4
- package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js.map +0 -1
package/dist/mcp/McpServer.js
CHANGED
|
@@ -9,10 +9,40 @@ import '../external/vscode-observables/observables/dist/observableInternal/obser
|
|
|
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
11
|
import { buildExplorerUrl } from '../utils.js';
|
|
12
|
+
import { TaskManager } from './TaskManager.js';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Client-local state
|
|
15
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
|
+
}
|
|
16
46
|
class WatchList {
|
|
17
47
|
_fixtureIds = new Set();
|
|
18
48
|
_hashes = new Map();
|
|
@@ -100,6 +130,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
100
130
|
}
|
|
101
131
|
_mcp;
|
|
102
132
|
_watchList = new WatchList();
|
|
133
|
+
_imageLru = new ImageLruCache(10);
|
|
134
|
+
_taskManager = new TaskManager();
|
|
135
|
+
_taskLastReportedIndex = new Map();
|
|
103
136
|
_pollFn;
|
|
104
137
|
_noAutostartHint;
|
|
105
138
|
_multiSessionTools = [];
|
|
@@ -110,6 +143,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
110
143
|
this._daemonConnection = _daemonConnection;
|
|
111
144
|
this._pollFn = options.pollFn;
|
|
112
145
|
this._noAutostartHint = options.noAutostartHint;
|
|
146
|
+
this._callTimeoutMs = options.callTimeoutMs ?? ComponentExplorerMcpServer._DEFAULT_CALL_TIMEOUT_MS;
|
|
113
147
|
this._mcp = new McpServer({
|
|
114
148
|
name: 'component-explorer',
|
|
115
149
|
version: '0.1.0',
|
|
@@ -178,15 +212,27 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
178
212
|
_noDaemonError() {
|
|
179
213
|
return noDaemonError(this._noAutostartHint);
|
|
180
214
|
}
|
|
181
|
-
|
|
215
|
+
static _DEFAULT_CALL_TIMEOUT_MS = 15_000;
|
|
216
|
+
_callTimeoutMs;
|
|
217
|
+
async _withDaemon(fn, options) {
|
|
182
218
|
const daemon = await this._waitForDaemon();
|
|
183
219
|
if (!daemon) {
|
|
184
220
|
return this._noDaemonError();
|
|
185
221
|
}
|
|
186
222
|
try {
|
|
187
|
-
|
|
223
|
+
if (options?.noTimeout) {
|
|
224
|
+
return await fn(daemon);
|
|
225
|
+
}
|
|
226
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('__mcp_timeout__')), this._callTimeoutMs));
|
|
227
|
+
return await Promise.race([fn(daemon), timeout]);
|
|
188
228
|
}
|
|
189
229
|
catch (e) {
|
|
230
|
+
if (e instanceof Error && e.message === '__mcp_timeout__') {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text', text: `Error: Operation timed out after ${this._callTimeoutMs / 1000}s. Retry, if the error persists, restart the involved session using the restart_session tool and retry.` }],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
190
236
|
if (isPipeConnectionError(e)) {
|
|
191
237
|
this._log('debug', { type: 'daemon-call-failed', error: String(e) });
|
|
192
238
|
this._handleDisconnect();
|
|
@@ -212,7 +258,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
212
258
|
if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
|
|
213
259
|
this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
|
|
214
260
|
}
|
|
215
|
-
if (event.type === 'ref-change') {
|
|
261
|
+
if (event.type === 'ref-change' || event.type === 'session-change') {
|
|
216
262
|
await this._refreshSessions();
|
|
217
263
|
}
|
|
218
264
|
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
@@ -292,11 +338,37 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
292
338
|
}
|
|
293
339
|
}
|
|
294
340
|
// -- Tool registration ---------------------------------------------------
|
|
341
|
+
_filterFixtures(allFixtures, fixtureIdPattern, labelPattern) {
|
|
342
|
+
let fixtureIdRegex;
|
|
343
|
+
if (fixtureIdPattern) {
|
|
344
|
+
try {
|
|
345
|
+
fixtureIdRegex = new RegExp(fixtureIdPattern);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return { error: `Error: Invalid fixtureIdPattern: ${fixtureIdPattern}` };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
let labelRegex;
|
|
352
|
+
if (labelPattern) {
|
|
353
|
+
try {
|
|
354
|
+
labelRegex = new RegExp(labelPattern);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return { error: `Error: Invalid labelPattern: ${labelPattern}` };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
fixtures: allFixtures.filter(f => (!fixtureIdRegex || fixtureIdRegex.test(f.fixtureId)) &&
|
|
362
|
+
(!labelRegex || f.labels.some(l => labelRegex.test(l)))),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
295
365
|
_registerTools() {
|
|
296
366
|
this._registerListFixtures();
|
|
297
367
|
this._registerScreenshot();
|
|
298
368
|
this._registerCompareScreenshot();
|
|
299
369
|
this._registerApproveDiff();
|
|
370
|
+
this._registerReviewVisual();
|
|
371
|
+
this._registerCheckVisuals();
|
|
300
372
|
this._registerEvaluateJs();
|
|
301
373
|
this._registerDebugReloadPage();
|
|
302
374
|
this._registerWatchAdd();
|
|
@@ -305,12 +377,23 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
305
377
|
this._registerWatchCompare();
|
|
306
378
|
this._registerWaitForUpdate();
|
|
307
379
|
this._registerSessions();
|
|
380
|
+
this._registerRestartSession();
|
|
381
|
+
this._registerOpenSession();
|
|
382
|
+
this._registerCloseSession();
|
|
383
|
+
this._registerUpdateSessionRef();
|
|
308
384
|
this._registerGetUrl();
|
|
385
|
+
this._registerCheckStability();
|
|
386
|
+
this._registerCheckTask();
|
|
387
|
+
this._registerCancelTask();
|
|
388
|
+
this._registerDebugGetImageByHash();
|
|
389
|
+
this._registerDebugSetBrowserVisibility();
|
|
309
390
|
}
|
|
310
391
|
_registerListFixtures() {
|
|
311
392
|
this._mcp.registerTool('list_fixtures', {
|
|
312
393
|
description: 'List all fixtures from a session',
|
|
313
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)'),
|
|
314
397
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
315
398
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
316
399
|
},
|
|
@@ -320,9 +403,13 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
320
403
|
this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
|
|
321
404
|
return this._withSourceTreeRetry(async () => {
|
|
322
405
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
323
|
-
const
|
|
406
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
407
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
408
|
+
if ('error' in filtered) {
|
|
409
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
410
|
+
}
|
|
324
411
|
return {
|
|
325
|
-
content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
|
|
412
|
+
content: [{ type: 'text', text: JSON.stringify(filtered.fixtures, null, 2) }],
|
|
326
413
|
};
|
|
327
414
|
});
|
|
328
415
|
}));
|
|
@@ -353,16 +440,46 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
353
440
|
});
|
|
354
441
|
const r = result;
|
|
355
442
|
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
443
|
+
// Cache image for debug_get_image_by_hash
|
|
444
|
+
if (r.hash && r.image) {
|
|
445
|
+
this._imageLru.put(r.hash, r.image);
|
|
446
|
+
}
|
|
356
447
|
const info = {
|
|
357
448
|
hash: r.hash,
|
|
358
449
|
sourceTreeId: r.sourceTreeId,
|
|
359
450
|
};
|
|
360
|
-
if (r.
|
|
361
|
-
info.
|
|
451
|
+
if (r.hasError) {
|
|
452
|
+
info.hasError = true;
|
|
453
|
+
}
|
|
454
|
+
if (r.error) {
|
|
455
|
+
info.error = r.error;
|
|
456
|
+
}
|
|
457
|
+
if (r.events && r.events.length > 0) {
|
|
458
|
+
info.events = r.events;
|
|
459
|
+
}
|
|
460
|
+
if (r.resultData !== undefined) {
|
|
461
|
+
info.resultData = r.resultData;
|
|
362
462
|
}
|
|
363
463
|
if (r.isStable !== undefined) {
|
|
364
464
|
info.isStable = r.isStable;
|
|
365
465
|
}
|
|
466
|
+
// Visual review status
|
|
467
|
+
if (r.hash) {
|
|
468
|
+
try {
|
|
469
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
470
|
+
const fixture = allFixtures.find(f => f.fixtureId === args.fixtureId);
|
|
471
|
+
const descriptions = fixture?.expectedVisualDescriptions ?? [];
|
|
472
|
+
const review = await daemon.methods.visualReview.getStatus({
|
|
473
|
+
fixtureId: args.fixtureId,
|
|
474
|
+
expectedVisualDescriptions: [...descriptions],
|
|
475
|
+
screenshotHash: r.hash,
|
|
476
|
+
});
|
|
477
|
+
info.review = review;
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Visual review not available — ignore
|
|
481
|
+
}
|
|
482
|
+
}
|
|
366
483
|
const content = [];
|
|
367
484
|
if (r.isStable === false && r.stabilityScreenshots) {
|
|
368
485
|
// Not stable: return all distinct screenshots
|
|
@@ -417,12 +534,32 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
417
534
|
match: r.match,
|
|
418
535
|
baselineHash: r.baselineHash,
|
|
419
536
|
currentHash: r.currentHash,
|
|
537
|
+
baselineSourceTreeId,
|
|
538
|
+
currentSourceTreeId,
|
|
420
539
|
};
|
|
421
|
-
if (r.
|
|
422
|
-
info.
|
|
540
|
+
if (r.baselineHasError) {
|
|
541
|
+
info.baselineHasError = true;
|
|
542
|
+
}
|
|
543
|
+
if (r.baselineError) {
|
|
544
|
+
info.baselineError = r.baselineError;
|
|
545
|
+
}
|
|
546
|
+
if (r.baselineEvents && r.baselineEvents.length > 0) {
|
|
547
|
+
info.baselineEvents = r.baselineEvents;
|
|
548
|
+
}
|
|
549
|
+
if (r.baselineResultData !== undefined) {
|
|
550
|
+
info.baselineResultData = r.baselineResultData;
|
|
551
|
+
}
|
|
552
|
+
if (r.currentHasError) {
|
|
553
|
+
info.currentHasError = true;
|
|
423
554
|
}
|
|
424
|
-
if (r.
|
|
425
|
-
info.
|
|
555
|
+
if (r.currentError) {
|
|
556
|
+
info.currentError = r.currentError;
|
|
557
|
+
}
|
|
558
|
+
if (r.currentEvents && r.currentEvents.length > 0) {
|
|
559
|
+
info.currentEvents = r.currentEvents;
|
|
560
|
+
}
|
|
561
|
+
if (r.currentResultData !== undefined) {
|
|
562
|
+
info.currentResultData = r.currentResultData;
|
|
426
563
|
}
|
|
427
564
|
if (r.approval) {
|
|
428
565
|
info.approval = r.approval;
|
|
@@ -463,6 +600,127 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
463
600
|
tool.disable();
|
|
464
601
|
this._multiSessionTools.push(tool);
|
|
465
602
|
}
|
|
603
|
+
_registerReviewVisual() {
|
|
604
|
+
this._mcp.registerTool('review_visual', {
|
|
605
|
+
description: 'Approve or reject a fixture\'s screenshot based on its expectedVisualDescriptions. ' +
|
|
606
|
+
'You must take a screenshot first and pass the resulting hash. ' +
|
|
607
|
+
'On approve, caches (expectedVisualDescriptions, screenshotHash) so future runs auto-approve.',
|
|
608
|
+
inputSchema: {
|
|
609
|
+
fixtureId: z.string().describe('The fixture ID'),
|
|
610
|
+
screenshotHash: z.string().describe('The screenshot hash (from a prior screenshot tool call)'),
|
|
611
|
+
verdict: z.enum(['approve', 'reject']).describe('Whether the visual matches expectations'),
|
|
612
|
+
comment: z.string().describe('Reason for the verdict'),
|
|
613
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
614
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
615
|
+
},
|
|
616
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
617
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
618
|
+
this._log('debug', { type: 'tool-call', tool: 'review_visual', fixtureId: args.fixtureId, verdict: args.verdict });
|
|
619
|
+
return this._withSourceTreeRetry(async () => {
|
|
620
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
621
|
+
// Get fixture descriptions
|
|
622
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
623
|
+
const fixture = allFixtures.find(f => f.fixtureId === args.fixtureId);
|
|
624
|
+
if (!fixture) {
|
|
625
|
+
return { content: [{ type: 'text', text: `Fixture not found: ${args.fixtureId}` }], isError: true };
|
|
626
|
+
}
|
|
627
|
+
if (fixture.expectedVisualDescriptions.length === 0) {
|
|
628
|
+
return { content: [{ type: 'text', text: `Fixture ${args.fixtureId} has no expectedVisualDescriptions — nothing to review.` }], isError: true };
|
|
629
|
+
}
|
|
630
|
+
if (args.verdict === 'approve') {
|
|
631
|
+
await daemon.methods.visualReview.approve({
|
|
632
|
+
fixtureId: args.fixtureId,
|
|
633
|
+
expectedVisualDescriptions: [...fixture.expectedVisualDescriptions],
|
|
634
|
+
screenshotHash: args.screenshotHash,
|
|
635
|
+
comment: args.comment,
|
|
636
|
+
});
|
|
637
|
+
return {
|
|
638
|
+
content: [{ type: 'text', text: `Approved: ${args.fixtureId} (hash: ${args.screenshotHash})` }],
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
return {
|
|
643
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
644
|
+
fixtureId: args.fixtureId,
|
|
645
|
+
verdict: 'rejected',
|
|
646
|
+
comment: args.comment,
|
|
647
|
+
screenshotHash: args.screenshotHash,
|
|
648
|
+
expectedVisualDescriptions: fixture.expectedVisualDescriptions,
|
|
649
|
+
}, null, 2) }],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}));
|
|
654
|
+
}
|
|
655
|
+
_registerCheckVisuals() {
|
|
656
|
+
this._mcp.registerTool('check_visuals', {
|
|
657
|
+
description: 'Batch check visual review status for fixtures that have expectedVisualDescription. ' +
|
|
658
|
+
'Returns lists of approved, needs-review, and no-expectation fixtures.',
|
|
659
|
+
inputSchema: {
|
|
660
|
+
fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
|
|
661
|
+
labelPattern: z.string().optional().describe('RegExp to filter fixtures by label'),
|
|
662
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
663
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
664
|
+
},
|
|
665
|
+
annotations: { readOnlyHint: true },
|
|
666
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
667
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
668
|
+
this._log('debug', { type: 'tool-call', tool: 'check_visuals', sessionName });
|
|
669
|
+
return this._withSourceTreeRetry(async () => {
|
|
670
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
671
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
672
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
673
|
+
if ('error' in filtered) {
|
|
674
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
675
|
+
}
|
|
676
|
+
const approved = [];
|
|
677
|
+
const needsReview = [];
|
|
678
|
+
const noExpectation = [];
|
|
679
|
+
// Take screenshots for all fixtures with expectations
|
|
680
|
+
const withExpectations = filtered.fixtures.filter(f => f.expectedVisualDescriptions.length > 0);
|
|
681
|
+
const withoutExpectations = filtered.fixtures.filter(f => f.expectedVisualDescriptions.length === 0);
|
|
682
|
+
for (const f of withoutExpectations) {
|
|
683
|
+
noExpectation.push(f.fixtureId);
|
|
684
|
+
}
|
|
685
|
+
for (const f of withExpectations) {
|
|
686
|
+
try {
|
|
687
|
+
const screenshotResult = await daemon.methods.screenshots.take({
|
|
688
|
+
fixtureId: f.fixtureId, sessionName, sourceTreeId, includeImage: true,
|
|
689
|
+
});
|
|
690
|
+
const hash = screenshotResult.hash;
|
|
691
|
+
const image = screenshotResult.image;
|
|
692
|
+
if (hash && image) {
|
|
693
|
+
this._imageLru.put(hash, image);
|
|
694
|
+
}
|
|
695
|
+
if (!hash) {
|
|
696
|
+
needsReview.push({ fixtureId: f.fixtureId, reason: 'screenshot-failed' });
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const review = await daemon.methods.visualReview.getStatus({
|
|
700
|
+
fixtureId: f.fixtureId,
|
|
701
|
+
expectedVisualDescriptions: [...f.expectedVisualDescriptions],
|
|
702
|
+
screenshotHash: hash,
|
|
703
|
+
});
|
|
704
|
+
if (review === 'no-expectations') {
|
|
705
|
+
noExpectation.push(f.fixtureId);
|
|
706
|
+
}
|
|
707
|
+
else if (typeof review === 'object' && review.status === 'approved') {
|
|
708
|
+
approved.push({ fixtureId: f.fixtureId, screenshotHash: hash });
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
needsReview.push({ fixtureId: f.fixtureId, screenshotHash: hash, reason: 'needs-review' });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
needsReview.push({ fixtureId: f.fixtureId, reason: 'error' });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
content: [{ type: 'text', text: JSON.stringify({ approved, needsReview, noExpectation }, null, 2) }],
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
}));
|
|
723
|
+
}
|
|
466
724
|
_registerEvaluateJs() {
|
|
467
725
|
this._mcp.registerTool('evaluate_js', {
|
|
468
726
|
description: 'Evaluate a JavaScript expression in the browser page where fixtures are rendered, for debugging purposes. ' +
|
|
@@ -660,8 +918,13 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
660
918
|
const events = await daemon.methods.events();
|
|
661
919
|
const iterator = events[Symbol.asyncIterator]();
|
|
662
920
|
try {
|
|
663
|
-
const
|
|
921
|
+
const deadline = Date.now() + 5000;
|
|
664
922
|
while (true) {
|
|
923
|
+
const remaining = deadline - Date.now();
|
|
924
|
+
if (remaining <= 0) {
|
|
925
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
926
|
+
}
|
|
927
|
+
const timeout = new Promise(resolve => setTimeout(() => resolve('timeout'), remaining));
|
|
665
928
|
const next = iterator.next();
|
|
666
929
|
const result = await Promise.race([next, timeout]);
|
|
667
930
|
if (result === 'timeout') {
|
|
@@ -676,7 +939,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
676
939
|
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
677
940
|
}
|
|
678
941
|
if (ev.type === 'ref-change') {
|
|
679
|
-
await this._refreshSessions();
|
|
942
|
+
const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
|
|
943
|
+
if (refreshResult === 'timeout') {
|
|
944
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
945
|
+
}
|
|
680
946
|
}
|
|
681
947
|
const newSourceTreeId = this._sourceTreeId(sessionName);
|
|
682
948
|
if (newSourceTreeId && newSourceTreeId !== knownSourceTreeId) {
|
|
@@ -737,6 +1003,90 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
737
1003
|
};
|
|
738
1004
|
}));
|
|
739
1005
|
}
|
|
1006
|
+
_registerRestartSession() {
|
|
1007
|
+
this._mcp.registerTool('restart_session', {
|
|
1008
|
+
description: 'Restart a session by disposing its browser page and dev server, then recreating them. ' +
|
|
1009
|
+
'Use this when a session appears stuck (e.g. after a timeout).',
|
|
1010
|
+
inputSchema: {
|
|
1011
|
+
sessionName: z.string().optional().describe('Session name to restart (defaults to first session)'),
|
|
1012
|
+
},
|
|
1013
|
+
annotations: { destructiveHint: true },
|
|
1014
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1015
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
1016
|
+
this._log('info', { type: 'tool-call', tool: 'restart_session', sessionName });
|
|
1017
|
+
const sessions = await daemon.methods.restartSession({ sessionName });
|
|
1018
|
+
this._sessions = sessions;
|
|
1019
|
+
return {
|
|
1020
|
+
content: [{ type: 'text', text: `Session '${sessionName}' restarted.\n` + JSON.stringify(sessions, null, 2) }],
|
|
1021
|
+
};
|
|
1022
|
+
}));
|
|
1023
|
+
}
|
|
1024
|
+
_registerOpenSession() {
|
|
1025
|
+
this._mcp.registerTool('open_session', {
|
|
1026
|
+
description: 'Open a new worktree-backed session at a given git ref. ' +
|
|
1027
|
+
'The ref can be a branch name, tag, commit SHA, or the special value "INDEX" to snapshot staged changes. ' +
|
|
1028
|
+
'The daemon allocates a reusable worktree slot from a fixed pool (max configured in component-explorer.json). ' +
|
|
1029
|
+
'Returns the updated session list on success.',
|
|
1030
|
+
inputSchema: {
|
|
1031
|
+
name: z.string().describe('Unique session name (e.g. "baseline", "bisect")'),
|
|
1032
|
+
ref: z.string().describe('Git ref: branch, tag, commit SHA, or "INDEX" for staged changes'),
|
|
1033
|
+
},
|
|
1034
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1035
|
+
this._log('info', { type: 'tool-call', tool: 'open_session', name: args.name, ref: args.ref });
|
|
1036
|
+
const result = await daemon.methods.openSession({ name: args.name, ref: args.ref });
|
|
1037
|
+
if ('error' in result) {
|
|
1038
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
1039
|
+
}
|
|
1040
|
+
this._sessions = result.sessions;
|
|
1041
|
+
this._updateMultiSessionToolVisibility();
|
|
1042
|
+
return {
|
|
1043
|
+
content: [{ type: 'text', text: JSON.stringify(result.sessions, null, 2) }],
|
|
1044
|
+
};
|
|
1045
|
+
}, { noTimeout: true }));
|
|
1046
|
+
}
|
|
1047
|
+
_registerCloseSession() {
|
|
1048
|
+
this._mcp.registerTool('close_session', {
|
|
1049
|
+
description: 'Close a dynamic worktree session and release its worktree slot back to the pool. ' +
|
|
1050
|
+
'Cannot close static sessions configured in component-explorer.json.',
|
|
1051
|
+
inputSchema: {
|
|
1052
|
+
name: z.string().describe('Session name to close'),
|
|
1053
|
+
},
|
|
1054
|
+
annotations: { destructiveHint: true },
|
|
1055
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1056
|
+
this._log('info', { type: 'tool-call', tool: 'close_session', name: args.name });
|
|
1057
|
+
const result = await daemon.methods.closeSession({ name: args.name });
|
|
1058
|
+
if ('error' in result) {
|
|
1059
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
1060
|
+
}
|
|
1061
|
+
this._sessions = result.sessions;
|
|
1062
|
+
this._updateMultiSessionToolVisibility();
|
|
1063
|
+
return {
|
|
1064
|
+
content: [{ type: 'text', text: `Session '${args.name}' closed.\n` + JSON.stringify(result.sessions, null, 2) }],
|
|
1065
|
+
};
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
_registerUpdateSessionRef() {
|
|
1069
|
+
this._mcp.registerTool('update_session_ref', {
|
|
1070
|
+
description: 'Change the git ref of an existing dynamic session. ' +
|
|
1071
|
+
'The worktree is checked out to the new ref and Vite\'s HMR handles the incremental update (no server restart). ' +
|
|
1072
|
+
'Fails if the worktree has uncommitted changes — the error will list the dirty files. ' +
|
|
1073
|
+
'The ref can be a branch, tag, commit SHA, or "INDEX" for staged changes.',
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
name: z.string().describe('Session name to update'),
|
|
1076
|
+
ref: z.string().describe('New git ref: branch, tag, commit SHA, or "INDEX"'),
|
|
1077
|
+
},
|
|
1078
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1079
|
+
this._log('info', { type: 'tool-call', tool: 'update_session_ref', name: args.name, ref: args.ref });
|
|
1080
|
+
const result = await daemon.methods.updateSessionRef({ name: args.name, ref: args.ref });
|
|
1081
|
+
if ('error' in result) {
|
|
1082
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
1083
|
+
}
|
|
1084
|
+
this._sessions = result.sessions;
|
|
1085
|
+
return {
|
|
1086
|
+
content: [{ type: 'text', text: JSON.stringify(result.sessions, null, 2) }],
|
|
1087
|
+
};
|
|
1088
|
+
}, { noTimeout: true }));
|
|
1089
|
+
}
|
|
740
1090
|
_registerGetUrl() {
|
|
741
1091
|
this._mcp.registerTool('get_url', {
|
|
742
1092
|
description: 'Get URL(s) for viewing fixtures. Returns the Component Explorer UI URL by default, ' +
|
|
@@ -814,6 +1164,198 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
814
1164
|
};
|
|
815
1165
|
});
|
|
816
1166
|
}
|
|
1167
|
+
_registerCheckStability() {
|
|
1168
|
+
this._mcp.registerTool('check_stability', {
|
|
1169
|
+
description: 'Check rendering stability of fixtures. Each fixture is unmounted, re-mounted, and screenshotted 3 times (~3s per fixture). ' +
|
|
1170
|
+
'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task. ' +
|
|
1171
|
+
'When returning a taskId, includes partial results collected so far.',
|
|
1172
|
+
inputSchema: {
|
|
1173
|
+
fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
|
|
1174
|
+
labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
|
|
1175
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
1176
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
1177
|
+
},
|
|
1178
|
+
annotations: { readOnlyHint: true },
|
|
1179
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1180
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
1181
|
+
this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
|
|
1182
|
+
return this._withSourceTreeRetry(async () => {
|
|
1183
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
1184
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
1185
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
1186
|
+
if ('error' in filtered) {
|
|
1187
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
1188
|
+
}
|
|
1189
|
+
const fixtures = filtered.fixtures;
|
|
1190
|
+
this._log('info', { type: 'check-stability-start', total: fixtures.length, filtered: allFixtures.length - fixtures.length });
|
|
1191
|
+
const task = this._taskManager.startTask(async (report, signal) => {
|
|
1192
|
+
const results = [];
|
|
1193
|
+
report({ completed: 0, total: fixtures.length, partialResult: results });
|
|
1194
|
+
for (let i = 0; i < fixtures.length; i++) {
|
|
1195
|
+
if (signal.aborted) {
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
1198
|
+
const fixture = fixtures[i];
|
|
1199
|
+
this._log('info', { type: 'check-stability-progress', fixtureId: fixture.fixtureId, index: i + 1, total: fixtures.length });
|
|
1200
|
+
const result = await daemon.methods.screenshots.take({
|
|
1201
|
+
fixtureId: fixture.fixtureId,
|
|
1202
|
+
sessionName,
|
|
1203
|
+
sourceTreeId,
|
|
1204
|
+
includeImage: false,
|
|
1205
|
+
stabilityCheck: true,
|
|
1206
|
+
});
|
|
1207
|
+
const r = result;
|
|
1208
|
+
results.push({
|
|
1209
|
+
fixtureId: fixture.fixtureId,
|
|
1210
|
+
isStable: r.isStable ?? true,
|
|
1211
|
+
screenshots: r.stabilityScreenshots?.map(s => ({ hash: s.hash, delayMs: s.delayMs })) ?? [],
|
|
1212
|
+
});
|
|
1213
|
+
report({ completed: i + 1, total: fixtures.length, partialResult: results });
|
|
1214
|
+
}
|
|
1215
|
+
const stable = results.filter(r => r.isStable).length;
|
|
1216
|
+
return {
|
|
1217
|
+
fixtures: results,
|
|
1218
|
+
summary: { total: results.length, stable, unstable: results.length - stable },
|
|
1219
|
+
};
|
|
1220
|
+
});
|
|
1221
|
+
const waited = await this._taskManager.waitForTask(task.id, 10_000);
|
|
1222
|
+
if (!waited) {
|
|
1223
|
+
return { content: [{ type: 'text', text: 'Error: task disappeared' }], isError: true };
|
|
1224
|
+
}
|
|
1225
|
+
if (waited.done) {
|
|
1226
|
+
return {
|
|
1227
|
+
content: [{ type: 'text', text: JSON.stringify(waited.result, null, 2) }],
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const partial = waited.progress.partialResult;
|
|
1231
|
+
this._taskLastReportedIndex.set(task.id, partial.length);
|
|
1232
|
+
return {
|
|
1233
|
+
content: [{
|
|
1234
|
+
type: 'text',
|
|
1235
|
+
text: JSON.stringify({
|
|
1236
|
+
taskId: task.id,
|
|
1237
|
+
status: 'running',
|
|
1238
|
+
progress: { completed: waited.progress.completed, total: waited.progress.total },
|
|
1239
|
+
elapsedMs: waited.elapsedMs,
|
|
1240
|
+
results: partial,
|
|
1241
|
+
}, null, 2),
|
|
1242
|
+
}],
|
|
1243
|
+
};
|
|
1244
|
+
});
|
|
1245
|
+
}));
|
|
1246
|
+
}
|
|
1247
|
+
_registerCheckTask() {
|
|
1248
|
+
this._mcp.registerTool('check_task', {
|
|
1249
|
+
description: 'Check on a running task. Waits up to ~2s for completion; if still running, returns progress and new results since last check.',
|
|
1250
|
+
inputSchema: {
|
|
1251
|
+
taskId: z.string().describe('The task ID returned by a previous tool call'),
|
|
1252
|
+
},
|
|
1253
|
+
annotations: { readOnlyHint: true },
|
|
1254
|
+
}, async (args) => {
|
|
1255
|
+
const waited = await this._taskManager.waitForTask(args.taskId, 2_000);
|
|
1256
|
+
if (!waited) {
|
|
1257
|
+
this._taskLastReportedIndex.delete(args.taskId);
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: 'text', text: `Error: No task found with id '${args.taskId}'` }],
|
|
1260
|
+
isError: true,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
if (waited.done) {
|
|
1264
|
+
const lastIndex = this._taskLastReportedIndex.get(args.taskId) ?? 0;
|
|
1265
|
+
this._taskLastReportedIndex.delete(args.taskId);
|
|
1266
|
+
const fullResult = waited.result;
|
|
1267
|
+
const newResults = fullResult.fixtures.slice(lastIndex);
|
|
1268
|
+
return {
|
|
1269
|
+
content: [{
|
|
1270
|
+
type: 'text',
|
|
1271
|
+
text: JSON.stringify({
|
|
1272
|
+
status: 'done',
|
|
1273
|
+
newResults,
|
|
1274
|
+
summary: fullResult.summary,
|
|
1275
|
+
}, null, 2),
|
|
1276
|
+
}],
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
const partial = waited.progress.partialResult;
|
|
1280
|
+
const lastIndex = this._taskLastReportedIndex.get(args.taskId) ?? 0;
|
|
1281
|
+
const newResults = partial.slice(lastIndex);
|
|
1282
|
+
this._taskLastReportedIndex.set(args.taskId, partial.length);
|
|
1283
|
+
return {
|
|
1284
|
+
content: [{
|
|
1285
|
+
type: 'text',
|
|
1286
|
+
text: JSON.stringify({
|
|
1287
|
+
taskId: args.taskId,
|
|
1288
|
+
status: 'running',
|
|
1289
|
+
progress: { completed: waited.progress.completed, total: waited.progress.total },
|
|
1290
|
+
elapsedMs: waited.elapsedMs,
|
|
1291
|
+
newResults,
|
|
1292
|
+
}, null, 2),
|
|
1293
|
+
}],
|
|
1294
|
+
};
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
_registerCancelTask() {
|
|
1298
|
+
this._mcp.registerTool('cancel_task', {
|
|
1299
|
+
description: 'Cancel a running task',
|
|
1300
|
+
inputSchema: {
|
|
1301
|
+
taskId: z.string().describe('The task ID to cancel'),
|
|
1302
|
+
},
|
|
1303
|
+
}, async (args) => {
|
|
1304
|
+
const task = this._taskManager.getTask(args.taskId);
|
|
1305
|
+
if (!task) {
|
|
1306
|
+
return {
|
|
1307
|
+
content: [{ type: 'text', text: `Error: No task found with id '${args.taskId}'` }],
|
|
1308
|
+
isError: true,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
this._taskManager.removeTask(args.taskId);
|
|
1312
|
+
return {
|
|
1313
|
+
content: [{ type: 'text', text: `Task '${args.taskId}' cancelled.` }],
|
|
1314
|
+
};
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
_registerDebugGetImageByHash() {
|
|
1318
|
+
this._mcp.registerTool('debug_get_image_by_hash', {
|
|
1319
|
+
description: 'Retrieve a recently-taken screenshot image by its hash. ' +
|
|
1320
|
+
'Keeps the last ~10 images in an LRU cache. ' +
|
|
1321
|
+
'Useful for debugging when screenshot hashes behave unexpectedly.',
|
|
1322
|
+
inputSchema: {
|
|
1323
|
+
hash: z.string().describe('The screenshot hash to look up'),
|
|
1324
|
+
},
|
|
1325
|
+
annotations: { readOnlyHint: true },
|
|
1326
|
+
}, async (args) => {
|
|
1327
|
+
const image = this._imageLru.get(args.hash);
|
|
1328
|
+
if (!image) {
|
|
1329
|
+
return {
|
|
1330
|
+
content: [{ type: 'text', text: `No cached image for hash '${args.hash}'. Available hashes: ${this._imageLru.keys().join(', ') || '(none)'}` }],
|
|
1331
|
+
isError: true,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
content: [
|
|
1336
|
+
{ type: 'text', text: `Image for hash: ${args.hash}` },
|
|
1337
|
+
{ type: 'image', data: image, mimeType: 'image/png' },
|
|
1338
|
+
],
|
|
1339
|
+
};
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
_registerDebugSetBrowserVisibility() {
|
|
1343
|
+
this._mcp.registerTool('debug_set_browser_visibility', {
|
|
1344
|
+
description: 'Show or hide the browser window used for rendering fixtures. ' +
|
|
1345
|
+
'Only use this tool when the user explicitly asks to show or hide the browser. ' +
|
|
1346
|
+
'Do not call this tool automatically or as part of other workflows. ' +
|
|
1347
|
+
'Note: changing visibility closes the current browser instance, so the next screenshot or evaluate_js call will relaunch it.',
|
|
1348
|
+
inputSchema: {
|
|
1349
|
+
visible: z.boolean().describe('true to show the browser window (headed mode), false to hide it (headless mode)'),
|
|
1350
|
+
},
|
|
1351
|
+
annotations: { destructiveHint: true },
|
|
1352
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1353
|
+
await daemon.methods.setBrowserVisibility({ visible: args.visible });
|
|
1354
|
+
return {
|
|
1355
|
+
content: [{ type: 'text', text: `Browser is now ${args.visible ? 'visible (headed)' : 'hidden (headless)'}.` }],
|
|
1356
|
+
};
|
|
1357
|
+
}));
|
|
1358
|
+
}
|
|
817
1359
|
}
|
|
818
1360
|
|
|
819
1361
|
export { ComponentExplorerMcpServer, DaemonConnection };
|