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