@vscode/component-explorer-cli 0.2.1-8 → 0.2.1-80
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 +6 -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 +50 -0
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +78 -0
- package/dist/browserPage.js.map +1 -1
- package/dist/commands/acceptCommand.d.ts +1 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +21 -14
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/checkStabilityCommand.d.ts +2 -0
- package/dist/commands/checkStabilityCommand.d.ts.map +1 -1
- package/dist/commands/checkStabilityCommand.js +25 -8
- package/dist/commands/checkStabilityCommand.js.map +1 -1
- package/dist/commands/compareCommand.d.ts +1 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +31 -22
- 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 +0 -2
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +143 -75
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/renderCommand.d.ts +9 -1
- package/dist/commands/renderCommand.d.ts.map +1 -1
- package/dist/commands/renderCommand.js +313 -28
- package/dist/commands/renderCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +14 -11
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/serviceDiffCommitsCommand.d.ts.map +1 -1
- package/dist/commands/serviceDiffCommitsCommand.js +10 -9
- package/dist/commands/serviceDiffCommitsCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +1 -0
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +31 -20
- 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 +17 -14
- package/dist/comparison.js.map +1 -1
- package/dist/component-explorer-config.schema.json +55 -2
- package/dist/componentExplorer.d.ts +106 -11
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +166 -23
- 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 +47 -23
- 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/inProcessClient.js +2 -2
- package/dist/daemon/lifecycle.d.ts +4 -1
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +1 -1
- 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 +26 -8
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeName.d.ts +3 -3
- package/dist/daemon/pipeName.d.ts.map +1 -1
- package/dist/daemon/pipeName.js +1 -2
- package/dist/daemon/pipeName.js.map +1 -1
- package/dist/daemon/pipeServer.js +3 -3
- package/dist/daemon/version.d.ts +1 -1
- package/dist/daemon/version.js +2 -2
- 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/git/gitCommitResolver.d.ts +2 -1
- package/dist/git/gitCommitResolver.d.ts.map +1 -1
- package/dist/git/gitCommitResolver.js +8 -8
- 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 +7 -7
- 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 -5
- package/dist/git/gitUtils.d.ts.map +1 -1
- package/dist/git/gitUtils.js +5 -4
- 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 +10 -1
- package/dist/index.js.map +1 -1
- package/dist/js-component-explorer/packages/common/dist/explorerUrl.js +21 -0
- package/dist/js-component-explorer/packages/common/dist/explorerUrl.js.map +1 -0
- package/dist/js-component-explorer/packages/common/dist/renderManifest.js +67 -0
- package/dist/js-component-explorer/packages/common/dist/renderManifest.js.map +1 -0
- package/dist/js-component-explorer/packages/semver/dist/index.js.map +1 -0
- package/dist/js-component-explorer/packages/simple-api/dist/chunk-FJ7AVNQE.js.map +1 -0
- package/dist/js-component-explorer/packages/simple-api/dist/chunk-TTRCY65Z.js.map +1 -0
- package/dist/js-component-explorer/packages/simple-api/dist/chunk-WNXMRXWV.js.map +1 -0
- package/dist/js-component-explorer/packages/simple-api/dist/express.js.map +1 -0
- 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 +4 -19
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +299 -305
- package/dist/mcp/McpServer.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 +6 -1
- package/dist/processTree.d.ts.map +1 -1
- package/dist/processTree.js +48 -5
- package/dist/processTree.js.map +1 -1
- package/dist/renderManifest.js +1 -1
- package/dist/server/httpServer.d.ts +1 -1
- package/dist/server/httpServer.d.ts.map +1 -1
- package/dist/server/httpServer.js +4 -8
- 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 +86 -5
- 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 -30
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -24
- 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/dist/vscode-observables/observables/dist/disposables.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/base.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/debugLocation.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/debugName.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/index.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +1 -0
- package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/logging/debugGetDependencyGraph.js +1 -1
- package/dist/vscode-observables/observables/dist/observableInternal/logging/debugGetDependencyGraph.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/logging/logging.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/derivedImpl.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/observables/observableValue.js.map +1 -0
- package/dist/{external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js → vscode-observables/observables/dist/observableInternal/reactions/createEffect.js} +9 -5
- package/dist/vscode-observables/observables/dist/observableInternal/reactions/createEffect.js.map +1 -0
- package/dist/{external/vscode-observables/observables/dist/observableInternal/reactions/autorunImpl.js → vscode-observables/observables/dist/observableInternal/reactions/createEffectImpl.js} +1 -1
- package/dist/vscode-observables/observables/dist/observableInternal/reactions/createEffectImpl.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/transaction.js.map +1 -0
- package/dist/vscode-observables/observables/dist/observableInternal/utils/utils.js.map +1 -0
- package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/utils/utilsCancellation.js +2 -2
- package/dist/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +1 -0
- package/package.json +9 -8
- package/dist/commands/artifactCommand.d.ts +0 -14
- package/dist/commands/artifactCommand.d.ts.map +0 -1
- package/dist/external/vscode-observables/observables/dist/disposables.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/base.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/debugLocation.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/debugName.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/index.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugGetDependencyGraph.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/logging.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derivedImpl.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/observableValue.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/reactions/autorunImpl.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/transaction.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utils.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +0 -1
- package/dist/packages/common/dist/renderManifest.js +0 -46
- package/dist/packages/common/dist/renderManifest.js.map +0 -1
- package/dist/packages/semver/dist/index.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-FJ7AVNQE.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-TTRCY65Z.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-WNXMRXWV.js.map +0 -1
- package/dist/packages/simple-api/dist/express.js.map +0 -1
- /package/dist/{packages → js-component-explorer/packages}/semver/dist/index.js +0 -0
- /package/dist/{packages → js-component-explorer/packages}/simple-api/dist/chunk-FJ7AVNQE.js +0 -0
- /package/dist/{packages → js-component-explorer/packages}/simple-api/dist/chunk-TTRCY65Z.js +0 -0
- /package/dist/{packages → js-component-explorer/packages}/simple-api/dist/chunk-WNXMRXWV.js +0 -0
- /package/dist/{packages → js-component-explorer/packages}/simple-api/dist/express.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/disposables.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/base.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/commonFacade/deps.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/debugLocation.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/debugName.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/index.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/logging/consoleObservableLogger.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/logging/logging.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/baseObservable.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/derived.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/derivedImpl.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/observableFromEvent.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/observables/observableValue.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/transaction.js +0 -0
- /package/dist/{external/vscode-observables → vscode-observables}/observables/dist/observableInternal/utils/utils.js +0 -0
package/dist/mcp/McpServer.js
CHANGED
|
@@ -1,127 +1,24 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import '../
|
|
5
|
-
import { Disposable } from '../
|
|
6
|
-
import '../
|
|
7
|
-
import { autorun } from '../
|
|
8
|
-
import '../
|
|
9
|
-
import '../
|
|
10
|
-
import '../
|
|
11
|
-
import
|
|
4
|
+
import '../vscode-observables/observables/dist/observableInternal/index.js';
|
|
5
|
+
import { Disposable } from '../vscode-observables/observables/dist/disposables.js';
|
|
6
|
+
import '../vscode-observables/observables/dist/observableInternal/debugLocation.js';
|
|
7
|
+
import { autorun } from '../vscode-observables/observables/dist/observableInternal/reactions/createEffect.js';
|
|
8
|
+
import '../vscode-observables/observables/dist/observableInternal/observables/derived.js';
|
|
9
|
+
import '../vscode-observables/observables/dist/observableInternal/utils/utils.js';
|
|
10
|
+
import '../vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
|
|
11
|
+
import '../js-component-explorer/packages/common/dist/renderManifest.js';
|
|
12
|
+
import { buildExplorerUrl } from '../js-component-explorer/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 {
|
|
@@ -392,20 +235,20 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
392
235
|
_registerListFixtures() {
|
|
393
236
|
this._mcp.registerTool('list_fixtures', {
|
|
394
237
|
description: 'List all fixtures from a session',
|
|
395
|
-
inputSchema: {
|
|
396
|
-
|
|
397
|
-
|
|
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)'),
|
|
398
241
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
399
242
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
400
|
-
},
|
|
243
|
+
}),
|
|
401
244
|
annotations: { readOnlyHint: true },
|
|
402
245
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
403
246
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
404
247
|
this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
|
|
405
|
-
return this._withSourceTreeRetry(async () => {
|
|
248
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
406
249
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
407
250
|
const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
408
|
-
const filtered = this._filterFixtures(listResult.fixtures, args.
|
|
251
|
+
const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
|
|
409
252
|
if ('error' in filtered) {
|
|
410
253
|
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
411
254
|
}
|
|
@@ -421,19 +264,28 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
421
264
|
_registerScreenshot() {
|
|
422
265
|
this._mcp.registerTool('screenshot', {
|
|
423
266
|
description: 'Take a screenshot of a single fixture. ' +
|
|
424
|
-
'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. '
|
|
425
|
-
|
|
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({
|
|
426
272
|
fixtureId: z.string().describe('The fixture ID'),
|
|
427
273
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
428
274
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
429
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..'),
|
|
430
|
-
|
|
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
|
+
}),
|
|
431
283
|
annotations: { readOnlyHint: true },
|
|
432
284
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
433
285
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
434
286
|
this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
|
|
435
287
|
this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
|
|
436
|
-
return this._withSourceTreeRetry(async () => {
|
|
288
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
437
289
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
438
290
|
const result = await daemon.methods.screenshots.take({
|
|
439
291
|
fixtureId: args.fixtureId,
|
|
@@ -441,6 +293,8 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
441
293
|
sourceTreeId,
|
|
442
294
|
includeImage: true,
|
|
443
295
|
stabilityCheck: args.stabilityCheck,
|
|
296
|
+
input: args.input,
|
|
297
|
+
disposeAfter: args.disposeAfter,
|
|
444
298
|
});
|
|
445
299
|
const r = result;
|
|
446
300
|
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
@@ -448,6 +302,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
448
302
|
if (r.hash && r.image) {
|
|
449
303
|
this._imageLru.put(r.hash, r.image);
|
|
450
304
|
}
|
|
305
|
+
const filterEvents = (events) => args.includeConsoleLog ? events : events.filter(e => e.type !== 'console.log');
|
|
451
306
|
const info = {
|
|
452
307
|
hash: r.hash,
|
|
453
308
|
sourceTreeId: r.sourceTreeId,
|
|
@@ -459,14 +314,37 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
459
314
|
info.error = r.error;
|
|
460
315
|
}
|
|
461
316
|
if (r.events && r.events.length > 0) {
|
|
462
|
-
|
|
317
|
+
const filtered = filterEvents(r.events);
|
|
318
|
+
if (filtered.length > 0) {
|
|
319
|
+
info.events = filtered;
|
|
320
|
+
}
|
|
463
321
|
}
|
|
464
|
-
if (r.
|
|
465
|
-
info.
|
|
322
|
+
if (r.output !== undefined) {
|
|
323
|
+
info.output = r.output;
|
|
466
324
|
}
|
|
467
325
|
if (r.isStable !== undefined) {
|
|
468
326
|
info.isStable = r.isStable;
|
|
469
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
|
+
}
|
|
470
348
|
// Visual review status
|
|
471
349
|
if (r.hash) {
|
|
472
350
|
try {
|
|
@@ -510,19 +388,19 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
510
388
|
_registerCompareScreenshot() {
|
|
511
389
|
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
512
390
|
description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
|
|
513
|
-
inputSchema: {
|
|
391
|
+
inputSchema: z.strictObject({
|
|
514
392
|
fixtureId: z.string().describe('The fixture ID'),
|
|
515
393
|
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
516
394
|
currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
|
|
517
395
|
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
518
396
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
519
|
-
},
|
|
397
|
+
}),
|
|
520
398
|
annotations: { readOnlyHint: true },
|
|
521
399
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
522
400
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
523
401
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
524
402
|
this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
|
|
525
|
-
return this._withSourceTreeRetry(async () => {
|
|
403
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
526
404
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
527
405
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
528
406
|
const result = await daemon.methods.screenshots.compare({
|
|
@@ -550,8 +428,8 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
550
428
|
if (r.baselineEvents && r.baselineEvents.length > 0) {
|
|
551
429
|
info.baselineEvents = r.baselineEvents;
|
|
552
430
|
}
|
|
553
|
-
if (r.
|
|
554
|
-
info.
|
|
431
|
+
if (r.baselineOutput !== undefined) {
|
|
432
|
+
info.baselineOutput = r.baselineOutput;
|
|
555
433
|
}
|
|
556
434
|
if (r.currentHasError) {
|
|
557
435
|
info.currentHasError = true;
|
|
@@ -562,8 +440,8 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
562
440
|
if (r.currentEvents && r.currentEvents.length > 0) {
|
|
563
441
|
info.currentEvents = r.currentEvents;
|
|
564
442
|
}
|
|
565
|
-
if (r.
|
|
566
|
-
info.
|
|
443
|
+
if (r.currentOutput !== undefined) {
|
|
444
|
+
info.currentOutput = r.currentOutput;
|
|
567
445
|
}
|
|
568
446
|
if (r.approval) {
|
|
569
447
|
info.approval = r.approval;
|
|
@@ -586,12 +464,12 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
586
464
|
_registerApproveDiff() {
|
|
587
465
|
const tool = this._mcp.registerTool('approve_diff', {
|
|
588
466
|
description: 'Approve a visual diff so it won\'t require re-inspection next time',
|
|
589
|
-
inputSchema: {
|
|
467
|
+
inputSchema: z.strictObject({
|
|
590
468
|
fixtureId: z.string(),
|
|
591
469
|
originalHash: z.string(),
|
|
592
470
|
modifiedHash: z.string(),
|
|
593
471
|
comment: z.string().describe('Reason for approving this diff'),
|
|
594
|
-
},
|
|
472
|
+
}),
|
|
595
473
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
596
474
|
await daemon.methods.approvals.approve(args);
|
|
597
475
|
return {
|
|
@@ -609,18 +487,18 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
609
487
|
description: 'Approve or reject a fixture\'s screenshot based on its expectedVisualDescriptions. ' +
|
|
610
488
|
'You must take a screenshot first and pass the resulting hash. ' +
|
|
611
489
|
'On approve, caches (expectedVisualDescriptions, screenshotHash) so future runs auto-approve.',
|
|
612
|
-
inputSchema: {
|
|
490
|
+
inputSchema: z.strictObject({
|
|
613
491
|
fixtureId: z.string().describe('The fixture ID'),
|
|
614
492
|
screenshotHash: z.string().describe('The screenshot hash (from a prior screenshot tool call)'),
|
|
615
493
|
verdict: z.enum(['approve', 'reject']).describe('Whether the visual matches expectations'),
|
|
616
494
|
comment: z.string().describe('Reason for the verdict'),
|
|
617
495
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
618
496
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
619
|
-
},
|
|
497
|
+
}),
|
|
620
498
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
621
499
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
622
500
|
this._log('debug', { type: 'tool-call', tool: 'review_visual', fixtureId: args.fixtureId, verdict: args.verdict });
|
|
623
|
-
return this._withSourceTreeRetry(async () => {
|
|
501
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
624
502
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
625
503
|
// Get fixture descriptions
|
|
626
504
|
const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
@@ -644,13 +522,15 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
644
522
|
}
|
|
645
523
|
else {
|
|
646
524
|
return {
|
|
647
|
-
content: [{
|
|
525
|
+
content: [{
|
|
526
|
+
type: 'text', text: JSON.stringify({
|
|
648
527
|
fixtureId: args.fixtureId,
|
|
649
528
|
verdict: 'rejected',
|
|
650
529
|
comment: args.comment,
|
|
651
530
|
screenshotHash: args.screenshotHash,
|
|
652
531
|
expectedVisualDescriptions: fixture.expectedVisualDescriptions,
|
|
653
|
-
}, null, 2)
|
|
532
|
+
}, null, 2)
|
|
533
|
+
}],
|
|
654
534
|
};
|
|
655
535
|
}
|
|
656
536
|
});
|
|
@@ -660,23 +540,23 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
660
540
|
this._mcp.registerTool('check_visuals', {
|
|
661
541
|
description: 'Batch check visual review status for fixtures that have expectedVisualDescription. ' +
|
|
662
542
|
'Returns lists of approved, needs-review, and no-expectation fixtures.',
|
|
663
|
-
inputSchema: {
|
|
664
|
-
|
|
665
|
-
|
|
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'),
|
|
666
546
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
667
547
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
668
|
-
},
|
|
548
|
+
}),
|
|
669
549
|
annotations: { readOnlyHint: true },
|
|
670
550
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
671
551
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
672
552
|
this._log('debug', { type: 'tool-call', tool: 'check_visuals', sessionName });
|
|
673
|
-
return this._withSourceTreeRetry(async () => {
|
|
553
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
674
554
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
675
555
|
const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
676
556
|
if (listResult.loadError) {
|
|
677
557
|
return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
|
|
678
558
|
}
|
|
679
|
-
const filtered = this._filterFixtures(listResult.fixtures, args.
|
|
559
|
+
const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
|
|
680
560
|
if ('error' in filtered) {
|
|
681
561
|
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
682
562
|
}
|
|
@@ -734,17 +614,17 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
734
614
|
'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
|
|
735
615
|
'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
|
|
736
616
|
'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
|
|
737
|
-
inputSchema: {
|
|
617
|
+
inputSchema: z.strictObject({
|
|
738
618
|
expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
|
|
739
619
|
fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
|
|
740
620
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
741
621
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
742
|
-
},
|
|
622
|
+
}),
|
|
743
623
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
744
624
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
745
625
|
this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
|
|
746
626
|
this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
|
|
747
|
-
return this._withSourceTreeRetry(async () => {
|
|
627
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
748
628
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
749
629
|
const result = await daemon.methods.evaluate({
|
|
750
630
|
sessionName,
|
|
@@ -771,9 +651,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
771
651
|
description: 'Force-reload the browser page used for rendering fixtures. ' +
|
|
772
652
|
'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
|
|
773
653
|
'that persist after source changes. Normal HMR updates should handle most cases automatically.',
|
|
774
|
-
inputSchema: {
|
|
654
|
+
inputSchema: z.strictObject({
|
|
775
655
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
776
|
-
},
|
|
656
|
+
}),
|
|
777
657
|
annotations: { destructiveHint: true },
|
|
778
658
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
779
659
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
@@ -791,9 +671,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
791
671
|
_registerWatchAdd() {
|
|
792
672
|
this._mcp.registerTool('watch_add', {
|
|
793
673
|
description: 'Add fixtures to the watch list. Watched fixtures are automatically re-screenshotted when source changes.',
|
|
794
|
-
inputSchema: {
|
|
674
|
+
inputSchema: z.strictObject({
|
|
795
675
|
fixtureIds: z.array(z.string()).describe('Fixture IDs to add'),
|
|
796
|
-
},
|
|
676
|
+
}),
|
|
797
677
|
}, async (args) => {
|
|
798
678
|
this._watchList.add(args.fixtureIds);
|
|
799
679
|
return {
|
|
@@ -807,9 +687,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
807
687
|
_registerWatchRemove() {
|
|
808
688
|
this._mcp.registerTool('watch_remove', {
|
|
809
689
|
description: 'Remove fixtures from the watch list',
|
|
810
|
-
inputSchema: {
|
|
690
|
+
inputSchema: z.strictObject({
|
|
811
691
|
fixtureIds: z.array(z.string()).describe('Fixture IDs to remove'),
|
|
812
|
-
},
|
|
692
|
+
}),
|
|
813
693
|
}, async (args) => {
|
|
814
694
|
this._watchList.remove(args.fixtureIds);
|
|
815
695
|
return {
|
|
@@ -823,9 +703,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
823
703
|
_registerWatchSet() {
|
|
824
704
|
this._mcp.registerTool('watch_set', {
|
|
825
705
|
description: 'Replace the watch list entirely',
|
|
826
|
-
inputSchema: {
|
|
706
|
+
inputSchema: z.strictObject({
|
|
827
707
|
fixtureIds: z.array(z.string()).describe('Fixture IDs to watch'),
|
|
828
|
-
},
|
|
708
|
+
}),
|
|
829
709
|
}, async (args) => {
|
|
830
710
|
this._watchList.set(args.fixtureIds);
|
|
831
711
|
return {
|
|
@@ -839,12 +719,12 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
839
719
|
_registerWatchCompare() {
|
|
840
720
|
const tool = this._mcp.registerTool('watch_compare', {
|
|
841
721
|
description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
|
|
842
|
-
inputSchema: {
|
|
722
|
+
inputSchema: z.strictObject({
|
|
843
723
|
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
844
724
|
currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
|
|
845
725
|
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
846
726
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
847
|
-
},
|
|
727
|
+
}),
|
|
848
728
|
annotations: { readOnlyHint: true },
|
|
849
729
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
850
730
|
const ids = [...this._watchList.fixtureIds];
|
|
@@ -853,7 +733,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
853
733
|
}
|
|
854
734
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
855
735
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
856
|
-
return this._withSourceTreeRetry(async () => {
|
|
736
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
857
737
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
858
738
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
859
739
|
const [baselineResult, currentResult] = await Promise.all([
|
|
@@ -909,10 +789,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
909
789
|
'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
|
|
910
790
|
'otherwise waits for a source-change or ref-change event. ' +
|
|
911
791
|
'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
|
|
912
|
-
inputSchema: {
|
|
792
|
+
inputSchema: z.strictObject({
|
|
913
793
|
sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
|
|
914
794
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
915
|
-
},
|
|
795
|
+
}),
|
|
916
796
|
annotations: { readOnlyHint: true },
|
|
917
797
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
918
798
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
@@ -946,7 +826,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
946
826
|
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
947
827
|
}
|
|
948
828
|
if (ev.type === 'ref-change') {
|
|
949
|
-
const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
|
|
829
|
+
const refreshResult = await Promise.race([this._refreshSessions(daemon), timeout]);
|
|
950
830
|
if (refreshResult === 'timeout') {
|
|
951
831
|
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
952
832
|
}
|
|
@@ -1003,8 +883,8 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1003
883
|
this._mcp.registerTool('sessions', {
|
|
1004
884
|
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
1005
885
|
annotations: { readOnlyHint: true },
|
|
1006
|
-
}, async () => this._withDaemon(async (
|
|
1007
|
-
await this._refreshSessions();
|
|
886
|
+
}, async () => this._withDaemon(async (daemon) => {
|
|
887
|
+
await this._refreshSessions(daemon);
|
|
1008
888
|
return {
|
|
1009
889
|
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
1010
890
|
};
|
|
@@ -1014,9 +894,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1014
894
|
this._mcp.registerTool('restart_session', {
|
|
1015
895
|
description: 'Restart a session by disposing its browser page and dev server, then recreating them. ' +
|
|
1016
896
|
'Use this when a session appears stuck (e.g. after a timeout).',
|
|
1017
|
-
inputSchema: {
|
|
897
|
+
inputSchema: z.strictObject({
|
|
1018
898
|
sessionName: z.string().optional().describe('Session name to restart (defaults to first session)'),
|
|
1019
|
-
},
|
|
899
|
+
}),
|
|
1020
900
|
annotations: { destructiveHint: true },
|
|
1021
901
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1022
902
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
@@ -1034,10 +914,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1034
914
|
'The ref can be a branch name, tag, commit SHA, or the special value "INDEX" to snapshot staged changes. ' +
|
|
1035
915
|
'The daemon allocates a reusable worktree slot from a fixed pool (max configured in component-explorer.json). ' +
|
|
1036
916
|
'Returns the updated session list on success.',
|
|
1037
|
-
inputSchema: {
|
|
917
|
+
inputSchema: z.strictObject({
|
|
1038
918
|
name: z.string().describe('Unique session name (e.g. "baseline", "bisect")'),
|
|
1039
919
|
ref: z.string().describe('Git ref: branch, tag, commit SHA, or "INDEX" for staged changes'),
|
|
1040
|
-
},
|
|
920
|
+
}),
|
|
1041
921
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1042
922
|
this._log('info', { type: 'tool-call', tool: 'open_session', name: args.name, ref: args.ref });
|
|
1043
923
|
const result = await daemon.methods.openSession({ name: args.name, ref: args.ref });
|
|
@@ -1055,9 +935,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1055
935
|
this._mcp.registerTool('close_session', {
|
|
1056
936
|
description: 'Close a dynamic worktree session and release its worktree slot back to the pool. ' +
|
|
1057
937
|
'Cannot close static sessions configured in component-explorer.json.',
|
|
1058
|
-
inputSchema: {
|
|
938
|
+
inputSchema: z.strictObject({
|
|
1059
939
|
name: z.string().describe('Session name to close'),
|
|
1060
|
-
},
|
|
940
|
+
}),
|
|
1061
941
|
annotations: { destructiveHint: true },
|
|
1062
942
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1063
943
|
this._log('info', { type: 'tool-call', tool: 'close_session', name: args.name });
|
|
@@ -1078,10 +958,10 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1078
958
|
'The worktree is checked out to the new ref and Vite\'s HMR handles the incremental update (no server restart). ' +
|
|
1079
959
|
'Fails if the worktree has uncommitted changes — the error will list the dirty files. ' +
|
|
1080
960
|
'The ref can be a branch, tag, commit SHA, or "INDEX" for staged changes.',
|
|
1081
|
-
inputSchema: {
|
|
961
|
+
inputSchema: z.strictObject({
|
|
1082
962
|
name: z.string().describe('Session name to update'),
|
|
1083
963
|
ref: z.string().describe('New git ref: branch, tag, commit SHA, or "INDEX"'),
|
|
1084
|
-
},
|
|
964
|
+
}),
|
|
1085
965
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1086
966
|
this._log('info', { type: 'tool-call', tool: 'update_session_ref', name: args.name, ref: args.ref });
|
|
1087
967
|
const result = await daemon.methods.updateSessionRef({ name: args.name, ref: args.ref });
|
|
@@ -1098,30 +978,17 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1098
978
|
this._mcp.registerTool('get_url', {
|
|
1099
979
|
description: 'Get URL for viewing fixtures. Returns the full Component Explorer UI by default. ' +
|
|
1100
980
|
'Use `embedded: true` for a minimal single-fixture view (requires fixtureId).',
|
|
1101
|
-
inputSchema: {
|
|
981
|
+
inputSchema: z.strictObject({
|
|
1102
982
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
1103
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.'),
|
|
1104
984
|
embedded: z.boolean().optional().describe('If true, returns an embedded single-fixture URL (minimal UI, requires fixtureId). Default: false (full explorer UI).'),
|
|
1105
|
-
},
|
|
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 {
|
|
@@ -1156,6 +1023,7 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1156
1023
|
const mode = args.embedded ? 'embedded' : undefined;
|
|
1157
1024
|
const url = buildExplorerUrl({
|
|
1158
1025
|
baseUrl,
|
|
1026
|
+
pathname: EXPLORER_ROUTE,
|
|
1159
1027
|
mode,
|
|
1160
1028
|
fixtureId: args.fixtureId,
|
|
1161
1029
|
});
|
|
@@ -1170,36 +1038,49 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1170
1038
|
return {
|
|
1171
1039
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1172
1040
|
};
|
|
1173
|
-
});
|
|
1041
|
+
}));
|
|
1174
1042
|
}
|
|
1175
1043
|
_registerCheckFixtureErrors() {
|
|
1176
1044
|
this._mcp.registerTool('check_fixture_errors', {
|
|
1177
|
-
description: 'Render fixtures and check for 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. ' +
|
|
1178
1051
|
'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task.',
|
|
1179
|
-
inputSchema: {
|
|
1180
|
-
|
|
1181
|
-
|
|
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)'),
|
|
1182
1055
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
1183
1056
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
1184
|
-
|
|
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
|
+
}),
|
|
1185
1062
|
annotations: { readOnlyHint: true },
|
|
1186
1063
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1187
1064
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
1188
|
-
this._log('debug', { type: 'tool-call', tool: 'check_fixture_errors', sessionName,
|
|
1189
|
-
return this._withSourceTreeRetry(async () => {
|
|
1065
|
+
this._log('debug', { type: 'tool-call', tool: 'check_fixture_errors', sessionName, fixtureIdRegex: args.fixtureIdRegex, labelRegex: args.labelRegex });
|
|
1066
|
+
return this._withSourceTreeRetry(daemon, async () => {
|
|
1190
1067
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
1191
1068
|
const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
1192
1069
|
if (listResult.loadError) {
|
|
1193
1070
|
return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
|
|
1194
1071
|
}
|
|
1195
|
-
const filtered = this._filterFixtures(listResult.fixtures, args.
|
|
1072
|
+
const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
|
|
1196
1073
|
if ('error' in filtered) {
|
|
1197
1074
|
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
1198
1075
|
}
|
|
1199
1076
|
const fixtures = filtered.fixtures;
|
|
1200
1077
|
const task = this._taskManager.startTask(async (report, signal) => {
|
|
1201
|
-
|
|
1202
|
-
|
|
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 });
|
|
1203
1084
|
for (let i = 0; i < fixtures.length; i++) {
|
|
1204
1085
|
if (signal.aborted) {
|
|
1205
1086
|
break;
|
|
@@ -1211,30 +1092,52 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1211
1092
|
sessionName,
|
|
1212
1093
|
sourceTreeId,
|
|
1213
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,
|
|
1214
1102
|
});
|
|
1215
1103
|
const r = result;
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
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);
|
|
1219
1123
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1124
|
+
else {
|
|
1125
|
+
okCount++;
|
|
1222
1126
|
}
|
|
1223
|
-
results.push(entry);
|
|
1224
1127
|
}
|
|
1225
1128
|
catch (e) {
|
|
1226
|
-
|
|
1129
|
+
erroredResults.push({
|
|
1227
1130
|
fixtureId: fixture.fixtureId,
|
|
1228
1131
|
hasError: true,
|
|
1229
1132
|
error: { message: e instanceof Error ? e.message : String(e) },
|
|
1230
1133
|
});
|
|
1231
1134
|
}
|
|
1232
|
-
|
|
1135
|
+
processed++;
|
|
1136
|
+
report({ completed: i + 1, total: fixtures.length, partialResult: erroredResults });
|
|
1233
1137
|
}
|
|
1234
|
-
const withErrors = results.filter(r => r.hasError);
|
|
1235
1138
|
return {
|
|
1236
|
-
fixtures:
|
|
1237
|
-
summary: { total:
|
|
1139
|
+
fixtures: erroredResults,
|
|
1140
|
+
summary: { total: processed, ok: okCount, errored: erroredResults.length },
|
|
1238
1141
|
};
|
|
1239
1142
|
});
|
|
1240
1143
|
const waited = await this._taskManager.waitForTask(task.id, 10_000);
|
|
@@ -1268,23 +1171,27 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1268
1171
|
description: 'Check rendering stability of fixtures. Each fixture is unmounted, re-mounted, and screenshotted 3 times (~3s per fixture). ' +
|
|
1269
1172
|
'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task. ' +
|
|
1270
1173
|
'When returning a taskId, includes partial results collected so far.',
|
|
1271
|
-
inputSchema: {
|
|
1272
|
-
|
|
1273
|
-
|
|
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)'),
|
|
1274
1177
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
1275
1178
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
1276
|
-
|
|
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
|
+
}),
|
|
1277
1184
|
annotations: { readOnlyHint: true },
|
|
1278
1185
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1279
1186
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
1280
|
-
this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName,
|
|
1281
|
-
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 () => {
|
|
1282
1189
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
1283
1190
|
const listResult = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
1284
1191
|
if (listResult.loadError) {
|
|
1285
1192
|
return { content: [{ type: 'text', text: `Error: Fixture loading failed: ${listResult.loadError}\nThe fixture list may be incomplete.` }], isError: true };
|
|
1286
1193
|
}
|
|
1287
|
-
const filtered = this._filterFixtures(listResult.fixtures, args.
|
|
1194
|
+
const filtered = this._filterFixtures(listResult.fixtures, args.fixtureIdRegex, args.labelRegex);
|
|
1288
1195
|
if ('error' in filtered) {
|
|
1289
1196
|
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
1290
1197
|
}
|
|
@@ -1305,6 +1212,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1305
1212
|
sourceTreeId,
|
|
1306
1213
|
includeImage: false,
|
|
1307
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,
|
|
1308
1218
|
});
|
|
1309
1219
|
const r = result;
|
|
1310
1220
|
results.push({
|
|
@@ -1349,9 +1259,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1349
1259
|
_registerCheckTask() {
|
|
1350
1260
|
this._mcp.registerTool('check_task', {
|
|
1351
1261
|
description: 'Check on a running task. Waits up to ~2s for completion; if still running, returns progress and new results since last check.',
|
|
1352
|
-
inputSchema: {
|
|
1262
|
+
inputSchema: z.strictObject({
|
|
1353
1263
|
taskId: z.string().describe('The task ID returned by a previous tool call'),
|
|
1354
|
-
},
|
|
1264
|
+
}),
|
|
1355
1265
|
annotations: { readOnlyHint: true },
|
|
1356
1266
|
}, async (args) => {
|
|
1357
1267
|
const waited = await this._taskManager.waitForTask(args.taskId, 2_000);
|
|
@@ -1399,9 +1309,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1399
1309
|
_registerCancelTask() {
|
|
1400
1310
|
this._mcp.registerTool('cancel_task', {
|
|
1401
1311
|
description: 'Cancel a running task',
|
|
1402
|
-
inputSchema: {
|
|
1312
|
+
inputSchema: z.strictObject({
|
|
1403
1313
|
taskId: z.string().describe('The task ID to cancel'),
|
|
1404
|
-
},
|
|
1314
|
+
}),
|
|
1405
1315
|
}, async (args) => {
|
|
1406
1316
|
const task = this._taskManager.getTask(args.taskId);
|
|
1407
1317
|
if (!task) {
|
|
@@ -1421,9 +1331,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1421
1331
|
description: 'Retrieve a recently-taken screenshot image by its hash. ' +
|
|
1422
1332
|
'Keeps the last ~10 images in an LRU cache. ' +
|
|
1423
1333
|
'Useful for debugging when screenshot hashes behave unexpectedly.',
|
|
1424
|
-
inputSchema: {
|
|
1334
|
+
inputSchema: z.strictObject({
|
|
1425
1335
|
hash: z.string().describe('The screenshot hash to look up'),
|
|
1426
|
-
},
|
|
1336
|
+
}),
|
|
1427
1337
|
annotations: { readOnlyHint: true },
|
|
1428
1338
|
}, async (args) => {
|
|
1429
1339
|
const image = this._imageLru.get(args.hash);
|
|
@@ -1447,9 +1357,9 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1447
1357
|
'Only use this tool when the user explicitly asks to show or hide the browser. ' +
|
|
1448
1358
|
'Do not call this tool automatically or as part of other workflows. ' +
|
|
1449
1359
|
'Note: changing visibility closes the current browser instance, so the next screenshot or evaluate_js call will relaunch it.',
|
|
1450
|
-
inputSchema: {
|
|
1360
|
+
inputSchema: z.strictObject({
|
|
1451
1361
|
visible: z.boolean().describe('true to show the browser window (headed mode), false to hide it (headless mode)'),
|
|
1452
|
-
},
|
|
1362
|
+
}),
|
|
1453
1363
|
annotations: { destructiveHint: true },
|
|
1454
1364
|
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1455
1365
|
await daemon.methods.setBrowserVisibility({ visible: args.visible });
|
|
@@ -1459,6 +1369,90 @@ class ComponentExplorerMcpServer extends Disposable {
|
|
|
1459
1369
|
}));
|
|
1460
1370
|
}
|
|
1461
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
|
+
}
|
|
1462
1456
|
|
|
1463
|
-
export { ComponentExplorerMcpServer
|
|
1457
|
+
export { ComponentExplorerMcpServer };
|
|
1464
1458
|
//# sourceMappingURL=McpServer.js.map
|