@vscode/component-explorer-cli 0.1.1-8 → 0.2.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 +5 -0
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +28 -2
- 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 +13 -5
- 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 +15 -4
- package/dist/commands/screenshotCommand.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 +36 -11
- 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 +10 -63
- 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 +97 -58
- package/dist/componentExplorer.d.ts +13 -17
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +49 -31
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonContext.d.ts +4 -0
- package/dist/daemon/DaemonContext.d.ts.map +1 -0
- package/dist/daemon/DaemonService.d.ts +92 -23
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +473 -118
- 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 +8 -3
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +28 -24
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts +6 -1
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +97 -5
- 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 +62 -3
- 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/formatValue.d.ts +2 -0
- package/dist/formatValue.d.ts.map +1 -0
- package/dist/formatValue.js +96 -0
- package/dist/formatValue.js.map +1 -0
- package/dist/formatValue.test.d.ts +2 -0
- package/dist/formatValue.test.d.ts.map +1 -0
- 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 +6 -1
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +17 -3
- package/dist/httpServer.js.map +1 -1
- 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 +19 -5
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +447 -97
- 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/watchConfig.d.ts +19 -12
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +43 -48
- 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,6 +9,7 @@ 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
|
|
@@ -70,9 +71,22 @@ function noDaemonError(hint) {
|
|
|
70
71
|
// ---------------------------------------------------------------------------
|
|
71
72
|
class DaemonConnection {
|
|
72
73
|
client;
|
|
74
|
+
_stale = false;
|
|
73
75
|
constructor(client) {
|
|
74
76
|
this.client = client;
|
|
75
77
|
}
|
|
78
|
+
get isStale() { return this._stale; }
|
|
79
|
+
markStale() { this._stale = true; }
|
|
80
|
+
}
|
|
81
|
+
function isPipeConnectionError(e) {
|
|
82
|
+
if (!(e instanceof Error)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const code = e.code;
|
|
86
|
+
if (code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EPIPE') {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return /connect ENOENT|ECONNREFUSED|ECONNRESET|EPIPE/.test(e.message);
|
|
76
90
|
}
|
|
77
91
|
// ---------------------------------------------------------------------------
|
|
78
92
|
// ComponentExplorerMcpServer
|
|
@@ -87,6 +101,8 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
87
101
|
}
|
|
88
102
|
_mcp;
|
|
89
103
|
_watchList = new WatchList();
|
|
104
|
+
_taskManager = new TaskManager();
|
|
105
|
+
_taskLastReportedIndex = new Map();
|
|
90
106
|
_pollFn;
|
|
91
107
|
_noAutostartHint;
|
|
92
108
|
_multiSessionTools = [];
|
|
@@ -97,6 +113,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
97
113
|
this._daemonConnection = _daemonConnection;
|
|
98
114
|
this._pollFn = options.pollFn;
|
|
99
115
|
this._noAutostartHint = options.noAutostartHint;
|
|
116
|
+
this._callTimeoutMs = options.callTimeoutMs ?? ComponentExplorerMcpServer._DEFAULT_CALL_TIMEOUT_MS;
|
|
100
117
|
this._mcp = new McpServer({
|
|
101
118
|
name: 'component-explorer',
|
|
102
119
|
version: '0.1.0',
|
|
@@ -108,7 +125,6 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
108
125
|
}));
|
|
109
126
|
}
|
|
110
127
|
async _onDaemonChanged(daemon) {
|
|
111
|
-
// Cancel any existing event stream
|
|
112
128
|
this._eventStreamAbortController?.abort();
|
|
113
129
|
this._eventStreamAbortController = undefined;
|
|
114
130
|
if (!daemon) {
|
|
@@ -116,10 +132,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
116
132
|
this._log('info', { type: 'daemon-disconnected' });
|
|
117
133
|
return;
|
|
118
134
|
}
|
|
119
|
-
// Fetch sessions and start event listener
|
|
120
135
|
try {
|
|
121
136
|
this._sessions = await daemon.methods.sessions();
|
|
122
|
-
this._log('
|
|
137
|
+
this._log('debug', { type: 'daemon-connected', sessions: this._sessions.length });
|
|
123
138
|
this._updateMultiSessionToolVisibility();
|
|
124
139
|
this._startEventListener(daemon);
|
|
125
140
|
}
|
|
@@ -128,37 +143,77 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
128
143
|
this._sessions = [];
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
_getConnection() {
|
|
147
|
+
const conn = this._daemonConnection.get();
|
|
148
|
+
if (conn?.isStale) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return conn;
|
|
133
152
|
}
|
|
134
|
-
/**
|
|
135
|
-
* If no daemon is connected and we have a pollFn, poll immediately and wait
|
|
136
|
-
* up to 3 seconds for the daemon to become available.
|
|
137
|
-
*/
|
|
138
153
|
async _waitForDaemon() {
|
|
139
|
-
let
|
|
140
|
-
if (
|
|
141
|
-
return
|
|
154
|
+
let conn = this._getConnection();
|
|
155
|
+
if (conn) {
|
|
156
|
+
return conn.client;
|
|
142
157
|
}
|
|
143
|
-
|
|
158
|
+
if (!this._pollFn) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
this._log('debug', { type: 'waiting-for-daemon' });
|
|
144
162
|
const startTime = Date.now();
|
|
145
163
|
const timeout = 3000;
|
|
146
164
|
while (Date.now() - startTime < timeout) {
|
|
147
165
|
await this._pollFn();
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
return
|
|
166
|
+
conn = this._getConnection();
|
|
167
|
+
if (conn) {
|
|
168
|
+
return conn.client;
|
|
151
169
|
}
|
|
152
|
-
// Wait 200ms before next poll
|
|
153
170
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
154
171
|
}
|
|
155
172
|
return undefined;
|
|
156
173
|
}
|
|
174
|
+
_handleDisconnect() {
|
|
175
|
+
const conn = this._daemonConnection.get();
|
|
176
|
+
if (conn && !conn.isStale) {
|
|
177
|
+
conn.markStale();
|
|
178
|
+
this._sessions = [];
|
|
179
|
+
this._log('debug', { type: 'daemon-connection-lost' });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
157
182
|
_noDaemonError() {
|
|
158
183
|
return noDaemonError(this._noAutostartHint);
|
|
159
184
|
}
|
|
185
|
+
static _DEFAULT_CALL_TIMEOUT_MS = 15_000;
|
|
186
|
+
_callTimeoutMs;
|
|
187
|
+
async _withDaemon(fn, options) {
|
|
188
|
+
const daemon = await this._waitForDaemon();
|
|
189
|
+
if (!daemon) {
|
|
190
|
+
return this._noDaemonError();
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
if (options?.noTimeout) {
|
|
194
|
+
return await fn(daemon);
|
|
195
|
+
}
|
|
196
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('__mcp_timeout__')), this._callTimeoutMs));
|
|
197
|
+
return await Promise.race([fn(daemon), timeout]);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (e instanceof Error && e.message === '__mcp_timeout__') {
|
|
201
|
+
return {
|
|
202
|
+
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.` }],
|
|
203
|
+
isError: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (isPipeConnectionError(e)) {
|
|
207
|
+
this._log('debug', { type: 'daemon-call-failed', error: String(e) });
|
|
208
|
+
this._handleDisconnect();
|
|
209
|
+
return this._noDaemonError();
|
|
210
|
+
}
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
160
214
|
_log(level, data) {
|
|
161
|
-
|
|
215
|
+
const mcpLevel = level === 'trace' ? 'debug' : level;
|
|
216
|
+
this._mcp.sendLoggingMessage({ level: mcpLevel, logger: 'daemon', data }).catch(() => { });
|
|
162
217
|
}
|
|
163
218
|
_startEventListener(daemon) {
|
|
164
219
|
const controller = new AbortController();
|
|
@@ -173,7 +228,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
173
228
|
if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
|
|
174
229
|
this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
|
|
175
230
|
}
|
|
176
|
-
if (event.type === 'ref-change') {
|
|
231
|
+
if (event.type === 'ref-change' || event.type === 'session-change') {
|
|
177
232
|
await this._refreshSessions();
|
|
178
233
|
}
|
|
179
234
|
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
@@ -209,12 +264,22 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
209
264
|
}
|
|
210
265
|
}
|
|
211
266
|
async _refreshSessions() {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this.
|
|
267
|
+
const conn = this._getConnection();
|
|
268
|
+
if (conn) {
|
|
269
|
+
try {
|
|
270
|
+
const prevCount = this._sessions.length;
|
|
271
|
+
this._sessions = await conn.client.methods.sessions();
|
|
272
|
+
if (this._sessions.length !== prevCount) {
|
|
273
|
+
this._updateMultiSessionToolVisibility();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (isPipeConnectionError(e)) {
|
|
278
|
+
this._handleDisconnect();
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
throw e;
|
|
282
|
+
}
|
|
218
283
|
}
|
|
219
284
|
}
|
|
220
285
|
}
|
|
@@ -243,6 +308,30 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
243
308
|
}
|
|
244
309
|
}
|
|
245
310
|
// -- Tool registration ---------------------------------------------------
|
|
311
|
+
_filterFixtures(allFixtures, fixtureIdPattern, labelPattern) {
|
|
312
|
+
let fixtureIdRegex;
|
|
313
|
+
if (fixtureIdPattern) {
|
|
314
|
+
try {
|
|
315
|
+
fixtureIdRegex = new RegExp(fixtureIdPattern);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return { error: `Error: Invalid fixtureIdPattern: ${fixtureIdPattern}` };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
let labelRegex;
|
|
322
|
+
if (labelPattern) {
|
|
323
|
+
try {
|
|
324
|
+
labelRegex = new RegExp(labelPattern);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return { error: `Error: Invalid labelPattern: ${labelPattern}` };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
fixtures: allFixtures.filter(f => (!fixtureIdRegex || fixtureIdRegex.test(f.fixtureId)) &&
|
|
332
|
+
(!labelRegex || f.labels.some(l => labelRegex.test(l)))),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
246
335
|
_registerTools() {
|
|
247
336
|
this._registerListFixtures();
|
|
248
337
|
this._registerScreenshot();
|
|
@@ -256,30 +345,40 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
256
345
|
this._registerWatchCompare();
|
|
257
346
|
this._registerWaitForUpdate();
|
|
258
347
|
this._registerSessions();
|
|
348
|
+
this._registerRestartSession();
|
|
349
|
+
this._registerOpenSession();
|
|
350
|
+
this._registerCloseSession();
|
|
351
|
+
this._registerUpdateSessionRef();
|
|
259
352
|
this._registerGetUrl();
|
|
353
|
+
this._registerCheckStability();
|
|
354
|
+
this._registerCheckTask();
|
|
355
|
+
this._registerCancelTask();
|
|
260
356
|
}
|
|
261
357
|
_registerListFixtures() {
|
|
262
358
|
this._mcp.registerTool('list_fixtures', {
|
|
263
359
|
description: 'List all fixtures from a session',
|
|
264
360
|
inputSchema: {
|
|
361
|
+
fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
|
|
362
|
+
labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
|
|
265
363
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
266
364
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
267
365
|
},
|
|
268
366
|
annotations: { readOnlyHint: true },
|
|
269
|
-
}, async (args) => {
|
|
270
|
-
const daemon = await this._waitForDaemon();
|
|
271
|
-
if (!daemon) {
|
|
272
|
-
return this._noDaemonError();
|
|
273
|
-
}
|
|
367
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
274
368
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
369
|
+
this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
|
|
275
370
|
return this._withSourceTreeRetry(async () => {
|
|
276
371
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
277
|
-
const
|
|
372
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
373
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
374
|
+
if ('error' in filtered) {
|
|
375
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
376
|
+
}
|
|
278
377
|
return {
|
|
279
|
-
content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
|
|
378
|
+
content: [{ type: 'text', text: JSON.stringify(filtered.fixtures, null, 2) }],
|
|
280
379
|
};
|
|
281
380
|
});
|
|
282
|
-
});
|
|
381
|
+
}));
|
|
283
382
|
}
|
|
284
383
|
_registerScreenshot() {
|
|
285
384
|
this._mcp.registerTool('screenshot', {
|
|
@@ -292,12 +391,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
292
391
|
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..'),
|
|
293
392
|
},
|
|
294
393
|
annotations: { readOnlyHint: true },
|
|
295
|
-
}, async (args) => {
|
|
296
|
-
const daemon = await this._waitForDaemon();
|
|
297
|
-
if (!daemon) {
|
|
298
|
-
return this._noDaemonError();
|
|
299
|
-
}
|
|
394
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
300
395
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
396
|
+
this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
|
|
397
|
+
this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
|
|
301
398
|
return this._withSourceTreeRetry(async () => {
|
|
302
399
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
303
400
|
const result = await daemon.methods.screenshots.take({
|
|
@@ -313,8 +410,17 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
313
410
|
hash: r.hash,
|
|
314
411
|
sourceTreeId: r.sourceTreeId,
|
|
315
412
|
};
|
|
316
|
-
if (r.
|
|
317
|
-
info.
|
|
413
|
+
if (r.hasError) {
|
|
414
|
+
info.hasError = true;
|
|
415
|
+
}
|
|
416
|
+
if (r.error) {
|
|
417
|
+
info.error = r.error;
|
|
418
|
+
}
|
|
419
|
+
if (r.events && r.events.length > 0) {
|
|
420
|
+
info.events = r.events;
|
|
421
|
+
}
|
|
422
|
+
if (r.resultData !== undefined) {
|
|
423
|
+
info.resultData = r.resultData;
|
|
318
424
|
}
|
|
319
425
|
if (r.isStable !== undefined) {
|
|
320
426
|
info.isStable = r.isStable;
|
|
@@ -340,7 +446,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
340
446
|
content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
|
|
341
447
|
return { content };
|
|
342
448
|
});
|
|
343
|
-
});
|
|
449
|
+
}));
|
|
344
450
|
}
|
|
345
451
|
_registerCompareScreenshot() {
|
|
346
452
|
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
@@ -353,13 +459,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
353
459
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
354
460
|
},
|
|
355
461
|
annotations: { readOnlyHint: true },
|
|
356
|
-
}, async (args) => {
|
|
357
|
-
const daemon = await this._waitForDaemon();
|
|
358
|
-
if (!daemon) {
|
|
359
|
-
return this._noDaemonError();
|
|
360
|
-
}
|
|
462
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
361
463
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
362
464
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
465
|
+
this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
|
|
363
466
|
return this._withSourceTreeRetry(async () => {
|
|
364
467
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
365
468
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
@@ -376,12 +479,32 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
376
479
|
match: r.match,
|
|
377
480
|
baselineHash: r.baselineHash,
|
|
378
481
|
currentHash: r.currentHash,
|
|
482
|
+
baselineSourceTreeId,
|
|
483
|
+
currentSourceTreeId,
|
|
379
484
|
};
|
|
380
|
-
if (r.
|
|
381
|
-
info.
|
|
485
|
+
if (r.baselineHasError) {
|
|
486
|
+
info.baselineHasError = true;
|
|
487
|
+
}
|
|
488
|
+
if (r.baselineError) {
|
|
489
|
+
info.baselineError = r.baselineError;
|
|
490
|
+
}
|
|
491
|
+
if (r.baselineEvents && r.baselineEvents.length > 0) {
|
|
492
|
+
info.baselineEvents = r.baselineEvents;
|
|
493
|
+
}
|
|
494
|
+
if (r.baselineResultData !== undefined) {
|
|
495
|
+
info.baselineResultData = r.baselineResultData;
|
|
382
496
|
}
|
|
383
|
-
if (r.
|
|
384
|
-
info.
|
|
497
|
+
if (r.currentHasError) {
|
|
498
|
+
info.currentHasError = true;
|
|
499
|
+
}
|
|
500
|
+
if (r.currentError) {
|
|
501
|
+
info.currentError = r.currentError;
|
|
502
|
+
}
|
|
503
|
+
if (r.currentEvents && r.currentEvents.length > 0) {
|
|
504
|
+
info.currentEvents = r.currentEvents;
|
|
505
|
+
}
|
|
506
|
+
if (r.currentResultData !== undefined) {
|
|
507
|
+
info.currentResultData = r.currentResultData;
|
|
385
508
|
}
|
|
386
509
|
if (r.approval) {
|
|
387
510
|
info.approval = r.approval;
|
|
@@ -397,7 +520,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
397
520
|
}
|
|
398
521
|
return { content };
|
|
399
522
|
});
|
|
400
|
-
});
|
|
523
|
+
}));
|
|
401
524
|
tool.disable();
|
|
402
525
|
this._multiSessionTools.push(tool);
|
|
403
526
|
}
|
|
@@ -410,11 +533,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
410
533
|
modifiedHash: z.string(),
|
|
411
534
|
comment: z.string().describe('Reason for approving this diff'),
|
|
412
535
|
},
|
|
413
|
-
}, async (args) => {
|
|
414
|
-
const daemon = await this._waitForDaemon();
|
|
415
|
-
if (!daemon) {
|
|
416
|
-
return this._noDaemonError();
|
|
417
|
-
}
|
|
536
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
418
537
|
await daemon.methods.approvals.approve(args);
|
|
419
538
|
return {
|
|
420
539
|
content: [{
|
|
@@ -422,7 +541,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
422
541
|
text: `Approved diff for ${args.fixtureId}: ${args.originalHash} → ${args.modifiedHash}`,
|
|
423
542
|
}],
|
|
424
543
|
};
|
|
425
|
-
});
|
|
544
|
+
}));
|
|
426
545
|
tool.disable();
|
|
427
546
|
this._multiSessionTools.push(tool);
|
|
428
547
|
}
|
|
@@ -438,12 +557,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
438
557
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
439
558
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
440
559
|
},
|
|
441
|
-
}, async (args) => {
|
|
442
|
-
const daemon = await this._waitForDaemon();
|
|
443
|
-
if (!daemon) {
|
|
444
|
-
return this._noDaemonError();
|
|
445
|
-
}
|
|
560
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
446
561
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
562
|
+
this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
|
|
563
|
+
this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
|
|
447
564
|
return this._withSourceTreeRetry(async () => {
|
|
448
565
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
449
566
|
const result = await daemon.methods.evaluate({
|
|
@@ -464,7 +581,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
464
581
|
content: [{ type: 'text', text }],
|
|
465
582
|
};
|
|
466
583
|
});
|
|
467
|
-
});
|
|
584
|
+
}));
|
|
468
585
|
}
|
|
469
586
|
_registerDebugReloadPage() {
|
|
470
587
|
this._mcp.registerTool('debug_reload_page', {
|
|
@@ -475,11 +592,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
475
592
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
476
593
|
},
|
|
477
594
|
annotations: { destructiveHint: true },
|
|
478
|
-
}, async (args) => {
|
|
479
|
-
const daemon = await this._waitForDaemon();
|
|
480
|
-
if (!daemon) {
|
|
481
|
-
return this._noDaemonError();
|
|
482
|
-
}
|
|
595
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
483
596
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
484
597
|
const sourceTreeId = this._sourceTreeId(sessionName);
|
|
485
598
|
await daemon.methods.evaluate({
|
|
@@ -490,7 +603,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
490
603
|
return {
|
|
491
604
|
content: [{ type: 'text', text: `Reloaded page for session '${sessionName}'.` }],
|
|
492
605
|
};
|
|
493
|
-
});
|
|
606
|
+
}));
|
|
494
607
|
}
|
|
495
608
|
_registerWatchAdd() {
|
|
496
609
|
this._mcp.registerTool('watch_add', {
|
|
@@ -550,11 +663,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
550
663
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
551
664
|
},
|
|
552
665
|
annotations: { readOnlyHint: true },
|
|
553
|
-
}, async (args) => {
|
|
554
|
-
const daemon = await this._waitForDaemon();
|
|
555
|
-
if (!daemon) {
|
|
556
|
-
return this._noDaemonError();
|
|
557
|
-
}
|
|
666
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
558
667
|
const ids = [...this._watchList.fixtureIds];
|
|
559
668
|
if (ids.length === 0) {
|
|
560
669
|
return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
|
|
@@ -607,7 +716,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
607
716
|
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
608
717
|
};
|
|
609
718
|
});
|
|
610
|
-
});
|
|
719
|
+
}));
|
|
611
720
|
tool.disable();
|
|
612
721
|
this._multiSessionTools.push(tool);
|
|
613
722
|
}
|
|
@@ -622,14 +731,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
622
731
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
623
732
|
},
|
|
624
733
|
annotations: { readOnlyHint: true },
|
|
625
|
-
}, async (args) => {
|
|
626
|
-
const daemon = await this._waitForDaemon();
|
|
627
|
-
if (!daemon) {
|
|
628
|
-
return this._noDaemonError();
|
|
629
|
-
}
|
|
734
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
630
735
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
631
736
|
const knownSourceTreeId = args.sourceTreeId;
|
|
632
|
-
// Check if already changed
|
|
633
737
|
const currentSourceTreeId = this._sourceTreeId(sessionName);
|
|
634
738
|
if (currentSourceTreeId && currentSourceTreeId !== knownSourceTreeId) {
|
|
635
739
|
return this._waitForUpdateResult(daemon, sessionName, currentSourceTreeId);
|
|
@@ -638,8 +742,13 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
638
742
|
const events = await daemon.methods.events();
|
|
639
743
|
const iterator = events[Symbol.asyncIterator]();
|
|
640
744
|
try {
|
|
641
|
-
const
|
|
745
|
+
const deadline = Date.now() + 5000;
|
|
642
746
|
while (true) {
|
|
747
|
+
const remaining = deadline - Date.now();
|
|
748
|
+
if (remaining <= 0) {
|
|
749
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
750
|
+
}
|
|
751
|
+
const timeout = new Promise(resolve => setTimeout(() => resolve('timeout'), remaining));
|
|
643
752
|
const next = iterator.next();
|
|
644
753
|
const result = await Promise.race([next, timeout]);
|
|
645
754
|
if (result === 'timeout') {
|
|
@@ -654,7 +763,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
654
763
|
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
655
764
|
}
|
|
656
765
|
if (ev.type === 'ref-change') {
|
|
657
|
-
await this._refreshSessions();
|
|
766
|
+
const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
|
|
767
|
+
if (refreshResult === 'timeout') {
|
|
768
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
769
|
+
}
|
|
658
770
|
}
|
|
659
771
|
const newSourceTreeId = this._sourceTreeId(sessionName);
|
|
660
772
|
if (newSourceTreeId && newSourceTreeId !== knownSourceTreeId) {
|
|
@@ -665,7 +777,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
665
777
|
finally {
|
|
666
778
|
await iterator.return?.();
|
|
667
779
|
}
|
|
668
|
-
});
|
|
780
|
+
}));
|
|
669
781
|
}
|
|
670
782
|
async _waitForUpdateResult(daemon, sessionName, sourceTreeId) {
|
|
671
783
|
const watchedIds = [...this._watchList.fixtureIds];
|
|
@@ -708,16 +820,96 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
708
820
|
this._mcp.registerTool('sessions', {
|
|
709
821
|
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
710
822
|
annotations: { readOnlyHint: true },
|
|
711
|
-
}, async () => {
|
|
712
|
-
const daemon = await this._waitForDaemon();
|
|
713
|
-
if (!daemon) {
|
|
714
|
-
return this._noDaemonError();
|
|
715
|
-
}
|
|
823
|
+
}, async () => this._withDaemon(async (_daemon) => {
|
|
716
824
|
await this._refreshSessions();
|
|
717
825
|
return {
|
|
718
826
|
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
719
827
|
};
|
|
720
|
-
});
|
|
828
|
+
}));
|
|
829
|
+
}
|
|
830
|
+
_registerRestartSession() {
|
|
831
|
+
this._mcp.registerTool('restart_session', {
|
|
832
|
+
description: 'Restart a session by disposing its browser page and dev server, then recreating them. ' +
|
|
833
|
+
'Use this when a session appears stuck (e.g. after a timeout).',
|
|
834
|
+
inputSchema: {
|
|
835
|
+
sessionName: z.string().optional().describe('Session name to restart (defaults to first session)'),
|
|
836
|
+
},
|
|
837
|
+
annotations: { destructiveHint: true },
|
|
838
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
839
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
840
|
+
this._log('info', { type: 'tool-call', tool: 'restart_session', sessionName });
|
|
841
|
+
const sessions = await daemon.methods.restartSession({ sessionName });
|
|
842
|
+
this._sessions = sessions;
|
|
843
|
+
return {
|
|
844
|
+
content: [{ type: 'text', text: `Session '${sessionName}' restarted.\n` + JSON.stringify(sessions, null, 2) }],
|
|
845
|
+
};
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
_registerOpenSession() {
|
|
849
|
+
this._mcp.registerTool('open_session', {
|
|
850
|
+
description: 'Open a new worktree-backed session at a given git ref. ' +
|
|
851
|
+
'The ref can be a branch name, tag, commit SHA, or the special value "INDEX" to snapshot staged changes. ' +
|
|
852
|
+
'The daemon allocates a reusable worktree slot from a fixed pool (max configured in component-explorer.json). ' +
|
|
853
|
+
'Returns the updated session list on success.',
|
|
854
|
+
inputSchema: {
|
|
855
|
+
name: z.string().describe('Unique session name (e.g. "baseline", "bisect")'),
|
|
856
|
+
ref: z.string().describe('Git ref: branch, tag, commit SHA, or "INDEX" for staged changes'),
|
|
857
|
+
},
|
|
858
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
859
|
+
this._log('info', { type: 'tool-call', tool: 'open_session', name: args.name, ref: args.ref });
|
|
860
|
+
const result = await daemon.methods.openSession({ name: args.name, ref: args.ref });
|
|
861
|
+
if ('error' in result) {
|
|
862
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
863
|
+
}
|
|
864
|
+
this._sessions = result.sessions;
|
|
865
|
+
this._updateMultiSessionToolVisibility();
|
|
866
|
+
return {
|
|
867
|
+
content: [{ type: 'text', text: JSON.stringify(result.sessions, null, 2) }],
|
|
868
|
+
};
|
|
869
|
+
}, { noTimeout: true }));
|
|
870
|
+
}
|
|
871
|
+
_registerCloseSession() {
|
|
872
|
+
this._mcp.registerTool('close_session', {
|
|
873
|
+
description: 'Close a dynamic worktree session and release its worktree slot back to the pool. ' +
|
|
874
|
+
'Cannot close static sessions configured in component-explorer.json.',
|
|
875
|
+
inputSchema: {
|
|
876
|
+
name: z.string().describe('Session name to close'),
|
|
877
|
+
},
|
|
878
|
+
annotations: { destructiveHint: true },
|
|
879
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
880
|
+
this._log('info', { type: 'tool-call', tool: 'close_session', name: args.name });
|
|
881
|
+
const result = await daemon.methods.closeSession({ name: args.name });
|
|
882
|
+
if ('error' in result) {
|
|
883
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
884
|
+
}
|
|
885
|
+
this._sessions = result.sessions;
|
|
886
|
+
this._updateMultiSessionToolVisibility();
|
|
887
|
+
return {
|
|
888
|
+
content: [{ type: 'text', text: `Session '${args.name}' closed.\n` + JSON.stringify(result.sessions, null, 2) }],
|
|
889
|
+
};
|
|
890
|
+
}));
|
|
891
|
+
}
|
|
892
|
+
_registerUpdateSessionRef() {
|
|
893
|
+
this._mcp.registerTool('update_session_ref', {
|
|
894
|
+
description: 'Change the git ref of an existing dynamic session. ' +
|
|
895
|
+
'The worktree is checked out to the new ref and Vite\'s HMR handles the incremental update (no server restart). ' +
|
|
896
|
+
'Fails if the worktree has uncommitted changes — the error will list the dirty files. ' +
|
|
897
|
+
'The ref can be a branch, tag, commit SHA, or "INDEX" for staged changes.',
|
|
898
|
+
inputSchema: {
|
|
899
|
+
name: z.string().describe('Session name to update'),
|
|
900
|
+
ref: z.string().describe('New git ref: branch, tag, commit SHA, or "INDEX"'),
|
|
901
|
+
},
|
|
902
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
903
|
+
this._log('info', { type: 'tool-call', tool: 'update_session_ref', name: args.name, ref: args.ref });
|
|
904
|
+
const result = await daemon.methods.updateSessionRef({ name: args.name, ref: args.ref });
|
|
905
|
+
if ('error' in result) {
|
|
906
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
907
|
+
}
|
|
908
|
+
this._sessions = result.sessions;
|
|
909
|
+
return {
|
|
910
|
+
content: [{ type: 'text', text: JSON.stringify(result.sessions, null, 2) }],
|
|
911
|
+
};
|
|
912
|
+
}, { noTimeout: true }));
|
|
721
913
|
}
|
|
722
914
|
_registerGetUrl() {
|
|
723
915
|
this._mcp.registerTool('get_url', {
|
|
@@ -732,15 +924,24 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
732
924
|
annotations: { readOnlyHint: true },
|
|
733
925
|
}, async (args) => {
|
|
734
926
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
735
|
-
|
|
927
|
+
let session = this._sessions.find(s => s.name === sessionName);
|
|
736
928
|
if (!session) {
|
|
737
|
-
// Try to refresh sessions if we don't have the requested session
|
|
738
929
|
const daemon = await this._waitForDaemon();
|
|
739
930
|
if (daemon) {
|
|
740
|
-
|
|
931
|
+
try {
|
|
932
|
+
await this._refreshSessions();
|
|
933
|
+
}
|
|
934
|
+
catch (e) {
|
|
935
|
+
if (isPipeConnectionError(e)) {
|
|
936
|
+
this._handleDisconnect();
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
throw e;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
741
942
|
}
|
|
742
|
-
|
|
743
|
-
if (!
|
|
943
|
+
session = this._sessions.find(s => s.name === sessionName);
|
|
944
|
+
if (!session) {
|
|
744
945
|
return {
|
|
745
946
|
content: [{
|
|
746
947
|
type: 'text',
|
|
@@ -750,8 +951,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
750
951
|
};
|
|
751
952
|
}
|
|
752
953
|
}
|
|
753
|
-
const
|
|
754
|
-
const baseUrl = resolved && !resolved.isLoading ? resolved.serverUrl : undefined;
|
|
954
|
+
const baseUrl = session && !session.isLoading ? session.serverUrl : undefined;
|
|
755
955
|
if (!baseUrl) {
|
|
756
956
|
return {
|
|
757
957
|
content: [{
|
|
@@ -788,6 +988,156 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
788
988
|
};
|
|
789
989
|
});
|
|
790
990
|
}
|
|
991
|
+
_registerCheckStability() {
|
|
992
|
+
this._mcp.registerTool('check_stability', {
|
|
993
|
+
description: 'Check rendering stability of fixtures. Each fixture is unmounted, re-mounted, and screenshotted 3 times (~3s per fixture). ' +
|
|
994
|
+
'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task. ' +
|
|
995
|
+
'When returning a taskId, includes partial results collected so far.',
|
|
996
|
+
inputSchema: {
|
|
997
|
+
fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
|
|
998
|
+
labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
|
|
999
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
1000
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
1001
|
+
},
|
|
1002
|
+
annotations: { readOnlyHint: true },
|
|
1003
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1004
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
1005
|
+
this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
|
|
1006
|
+
return this._withSourceTreeRetry(async () => {
|
|
1007
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
1008
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
1009
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
1010
|
+
if ('error' in filtered) {
|
|
1011
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
1012
|
+
}
|
|
1013
|
+
const fixtures = filtered.fixtures;
|
|
1014
|
+
this._log('info', { type: 'check-stability-start', total: fixtures.length, filtered: allFixtures.length - fixtures.length });
|
|
1015
|
+
const task = this._taskManager.startTask(async (report, signal) => {
|
|
1016
|
+
const results = [];
|
|
1017
|
+
report({ completed: 0, total: fixtures.length, partialResult: results });
|
|
1018
|
+
for (let i = 0; i < fixtures.length; i++) {
|
|
1019
|
+
if (signal.aborted) {
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
const fixture = fixtures[i];
|
|
1023
|
+
this._log('info', { type: 'check-stability-progress', fixtureId: fixture.fixtureId, index: i + 1, total: fixtures.length });
|
|
1024
|
+
const result = await daemon.methods.screenshots.take({
|
|
1025
|
+
fixtureId: fixture.fixtureId,
|
|
1026
|
+
sessionName,
|
|
1027
|
+
sourceTreeId,
|
|
1028
|
+
includeImage: false,
|
|
1029
|
+
stabilityCheck: true,
|
|
1030
|
+
});
|
|
1031
|
+
const r = result;
|
|
1032
|
+
results.push({
|
|
1033
|
+
fixtureId: fixture.fixtureId,
|
|
1034
|
+
isStable: r.isStable ?? true,
|
|
1035
|
+
screenshots: r.stabilityScreenshots?.map(s => ({ hash: s.hash, delayMs: s.delayMs })) ?? [],
|
|
1036
|
+
});
|
|
1037
|
+
report({ completed: i + 1, total: fixtures.length, partialResult: results });
|
|
1038
|
+
}
|
|
1039
|
+
const stable = results.filter(r => r.isStable).length;
|
|
1040
|
+
return {
|
|
1041
|
+
fixtures: results,
|
|
1042
|
+
summary: { total: results.length, stable, unstable: results.length - stable },
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
const waited = await this._taskManager.waitForTask(task.id, 10_000);
|
|
1046
|
+
if (!waited) {
|
|
1047
|
+
return { content: [{ type: 'text', text: 'Error: task disappeared' }], isError: true };
|
|
1048
|
+
}
|
|
1049
|
+
if (waited.done) {
|
|
1050
|
+
return {
|
|
1051
|
+
content: [{ type: 'text', text: JSON.stringify(waited.result, null, 2) }],
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const partial = waited.progress.partialResult;
|
|
1055
|
+
this._taskLastReportedIndex.set(task.id, partial.length);
|
|
1056
|
+
return {
|
|
1057
|
+
content: [{
|
|
1058
|
+
type: 'text',
|
|
1059
|
+
text: JSON.stringify({
|
|
1060
|
+
taskId: task.id,
|
|
1061
|
+
status: 'running',
|
|
1062
|
+
progress: { completed: waited.progress.completed, total: waited.progress.total },
|
|
1063
|
+
elapsedMs: waited.elapsedMs,
|
|
1064
|
+
results: partial,
|
|
1065
|
+
}, null, 2),
|
|
1066
|
+
}],
|
|
1067
|
+
};
|
|
1068
|
+
});
|
|
1069
|
+
}));
|
|
1070
|
+
}
|
|
1071
|
+
_registerCheckTask() {
|
|
1072
|
+
this._mcp.registerTool('check_task', {
|
|
1073
|
+
description: 'Check on a running task. Waits up to ~2s for completion; if still running, returns progress and new results since last check.',
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
taskId: z.string().describe('The task ID returned by a previous tool call'),
|
|
1076
|
+
},
|
|
1077
|
+
annotations: { readOnlyHint: true },
|
|
1078
|
+
}, async (args) => {
|
|
1079
|
+
const waited = await this._taskManager.waitForTask(args.taskId, 2_000);
|
|
1080
|
+
if (!waited) {
|
|
1081
|
+
this._taskLastReportedIndex.delete(args.taskId);
|
|
1082
|
+
return {
|
|
1083
|
+
content: [{ type: 'text', text: `Error: No task found with id '${args.taskId}'` }],
|
|
1084
|
+
isError: true,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
if (waited.done) {
|
|
1088
|
+
const lastIndex = this._taskLastReportedIndex.get(args.taskId) ?? 0;
|
|
1089
|
+
this._taskLastReportedIndex.delete(args.taskId);
|
|
1090
|
+
const fullResult = waited.result;
|
|
1091
|
+
const newResults = fullResult.fixtures.slice(lastIndex);
|
|
1092
|
+
return {
|
|
1093
|
+
content: [{
|
|
1094
|
+
type: 'text',
|
|
1095
|
+
text: JSON.stringify({
|
|
1096
|
+
status: 'done',
|
|
1097
|
+
newResults,
|
|
1098
|
+
summary: fullResult.summary,
|
|
1099
|
+
}, null, 2),
|
|
1100
|
+
}],
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const partial = waited.progress.partialResult;
|
|
1104
|
+
const lastIndex = this._taskLastReportedIndex.get(args.taskId) ?? 0;
|
|
1105
|
+
const newResults = partial.slice(lastIndex);
|
|
1106
|
+
this._taskLastReportedIndex.set(args.taskId, partial.length);
|
|
1107
|
+
return {
|
|
1108
|
+
content: [{
|
|
1109
|
+
type: 'text',
|
|
1110
|
+
text: JSON.stringify({
|
|
1111
|
+
taskId: args.taskId,
|
|
1112
|
+
status: 'running',
|
|
1113
|
+
progress: { completed: waited.progress.completed, total: waited.progress.total },
|
|
1114
|
+
elapsedMs: waited.elapsedMs,
|
|
1115
|
+
newResults,
|
|
1116
|
+
}, null, 2),
|
|
1117
|
+
}],
|
|
1118
|
+
};
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
_registerCancelTask() {
|
|
1122
|
+
this._mcp.registerTool('cancel_task', {
|
|
1123
|
+
description: 'Cancel a running task',
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
taskId: z.string().describe('The task ID to cancel'),
|
|
1126
|
+
},
|
|
1127
|
+
}, async (args) => {
|
|
1128
|
+
const task = this._taskManager.getTask(args.taskId);
|
|
1129
|
+
if (!task) {
|
|
1130
|
+
return {
|
|
1131
|
+
content: [{ type: 'text', text: `Error: No task found with id '${args.taskId}'` }],
|
|
1132
|
+
isError: true,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
this._taskManager.removeTask(args.taskId);
|
|
1136
|
+
return {
|
|
1137
|
+
content: [{ type: 'text', text: `Task '${args.taskId}' cancelled.` }],
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
791
1141
|
}
|
|
792
1142
|
|
|
793
1143
|
export { ComponentExplorerMcpServer, DaemonConnection };
|