@comecaramelos/dsh-chrome-mcp 0.0.1
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.md +21 -0
- package/README.md +115 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +585 -0
- package/lib/index.js +647 -0
- package/package.json +40 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@comecaramelos/dsh-chrome-mcp",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
|
|
8
|
+
var react = require("react");
|
|
9
|
+
var reactJsx = require("react/jsx-runtime");
|
|
10
|
+
var clientStore = require("@deepseek-ai/dsh-client-store");
|
|
11
|
+
// NOTE: no UI primitives required — the extra flags editor is a plain
|
|
12
|
+
// textarea writing through the controller (props.saveFlags), not the
|
|
13
|
+
// shell Menu primitive the reference used for its profile picker.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Chrome DevTools MCP settings + status card, browser half.
|
|
17
|
+
*
|
|
18
|
+
* Registers one card into the shared `settings.plugin.item` slot
|
|
19
|
+
* (Settings → Plugins → Plugin configuration), keyed by the
|
|
20
|
+
* `chrome-mcp` settings namespace the host half serves. The card
|
|
21
|
+
* offers, per the product spec:
|
|
22
|
+
* - error reporting: chrome executable / connection errors surfaced
|
|
23
|
+
* from the host's `lastError`, reported by a single UI element: the
|
|
24
|
+
* red status dot in the header, whose `title` carries the full
|
|
25
|
+
* message. A served `chromeMissing` flag keeps that dot red even
|
|
26
|
+
* when the captured error was cleared (its title then falls back to
|
|
27
|
+
* the not-found label). No body paragraph, no extra badge.
|
|
28
|
+
* - an extra flags editor (host field `extraFlags`, one flag per
|
|
29
|
+
* line, default `--no-usage-statistics`, `--no-performance-crux`), writing the persisted
|
|
30
|
+
* field via scope.set (host fiber.update relaunches the bridge),
|
|
31
|
+
* - the Chrome executable version discovered by the last check, and
|
|
32
|
+
* - the full configuration-guide message with a link to
|
|
33
|
+
* https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/configuration.md
|
|
34
|
+
* The card is collapsible like the stock plugin cards: closed by
|
|
35
|
+
* default, with the header as its disclosure button.
|
|
36
|
+
*/
|
|
37
|
+
/** Settings namespace served by the host half; also the slot key. */
|
|
38
|
+
var NS = "chrome-mcp";
|
|
39
|
+
/** Locale dictionary namespace. */
|
|
40
|
+
var LOCALE_NS = "chromeMcp";
|
|
41
|
+
/** Required services (cordis fiber inject). */
|
|
42
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
43
|
+
|
|
44
|
+
/** Configuration docs link (must match host CONFIGURATION_DOCS_URL). */
|
|
45
|
+
var CONFIGURATION_URL =
|
|
46
|
+
"https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/docs/configuration.md";
|
|
47
|
+
|
|
48
|
+
// ── card styles ────────────────────────────────────────────────────
|
|
49
|
+
var CSS = {
|
|
50
|
+
card: "dshcmc_card",
|
|
51
|
+
cardOpen: "dshcmc_cardOpen",
|
|
52
|
+
header: "dshcmc_header",
|
|
53
|
+
headText: "dshcmc_headText",
|
|
54
|
+
nameRow: "dshcmc_nameRow",
|
|
55
|
+
name: "dshcmc_name",
|
|
56
|
+
desc: "dshcmc_desc",
|
|
57
|
+
chevron: "dshcmc_chevron",
|
|
58
|
+
chevronOpen: "dshcmc_chevronOpen",
|
|
59
|
+
body: "dshcmc_body",
|
|
60
|
+
field: "dshcmc_field",
|
|
61
|
+
rowText: "dshcmc_rowText",
|
|
62
|
+
fieldTitle: "dshcmc_fieldTitle",
|
|
63
|
+
fieldDesc: "dshcmc_fieldDesc",
|
|
64
|
+
row: "dshcmc_row",
|
|
65
|
+
button: "dshcmc_button",
|
|
66
|
+
readOnly: "dshcmc_readOnly",
|
|
67
|
+
dot: "dshcmc_dot",
|
|
68
|
+
dotError: "dshcmc_dotError",
|
|
69
|
+
link: "dshcmc_link",
|
|
70
|
+
value: "dshcmc_value",
|
|
71
|
+
docs: "dshcmc_docs",
|
|
72
|
+
editor: "dshcmc_editor",
|
|
73
|
+
text: "dshcmc_text"
|
|
74
|
+
};
|
|
75
|
+
var css =
|
|
76
|
+
"." + CSS.card + "{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}" +
|
|
77
|
+
"." + CSS.card + ":hover{border-color:var(--dsw-alias-label-dimmed)}" +
|
|
78
|
+
"." + CSS.cardOpen + "{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}" +
|
|
79
|
+
"." + CSS.header + "{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}" +
|
|
80
|
+
"." + CSS.header + ":focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}" +
|
|
81
|
+
"." + CSS.headText + "{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}" +
|
|
82
|
+
"." + CSS.nameRow + "{align-items:center;gap:8px;min-width:0;display:flex}" +
|
|
83
|
+
"." + CSS.name + "{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}" +
|
|
84
|
+
"." + CSS.desc + "{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}" +
|
|
85
|
+
"." + CSS.chevron + "{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}" +
|
|
86
|
+
"." + CSS.chevronOpen + "{transform:rotate(180deg)}" +
|
|
87
|
+
"." + CSS.body + "{border-top:.5px solid var(--dsw-alias-border-l2);display:flex;flex-direction:column;gap:10px;margin:0 16px;padding:12px 0}" +
|
|
88
|
+
"." + CSS.field + "{align-items:flex-start;gap:8px;display:flex}" +
|
|
89
|
+
"." + CSS.rowText + "{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:48px;display:flex}" +
|
|
90
|
+
"." + CSS.fieldTitle + "{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}" +
|
|
91
|
+
"." + CSS.fieldDesc + "{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}" +
|
|
92
|
+
"." + CSS.row + "{display:flex;gap:10px;align-items:center}" +
|
|
93
|
+
"." + CSS.button +
|
|
94
|
+
"{display:inline-flex;align-items:center;justify-content:center;gap:4px;border:.5px solid var(--dsw-alias-border-l3);" +
|
|
95
|
+
"border-radius:14px;cursor:pointer;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);" +
|
|
96
|
+
"background:transparent;padding:0 10px;height:28px}" +
|
|
97
|
+
"." + CSS.button + ":hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}" +
|
|
98
|
+
"." + CSS.button + ":disabled{cursor:not-allowed;opacity:.4}" +
|
|
99
|
+
"." + CSS.button + ":focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}" +
|
|
100
|
+
"." + CSS.readOnly + "{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}" +
|
|
101
|
+
"." + CSS.dot + "{box-sizing:border-box;corner-shape:round;border-radius:50%;flex:none;width:8px;height:8px;display:inline-block}" +
|
|
102
|
+
"." + CSS.dotError + "{background:var(--dsw-alias-state-error-primary)}" +
|
|
103
|
+
"." + CSS.link + "{color:var(--dsw-alias-brand-primary);text-decoration:none;font-size:12px;line-height:1.5}" +
|
|
104
|
+
"." + CSS.link + ":hover{text-decoration:underline}" +
|
|
105
|
+
"." + CSS.value + "{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.5;white-space:pre-wrap}" +
|
|
106
|
+
"." + CSS.docs + "{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;overflow-wrap:anywhere}" +
|
|
107
|
+
"." + CSS.editor + "{flex-direction:column;gap:8px;display:flex}" +
|
|
108
|
+
"." + CSS.text +
|
|
109
|
+
"{box-sizing:border-box;width:100%;border:.5px solid var(--dsw-alias-border-l3);border-radius:10px;background:var(--dsw-alias-bg-layer-1);" +
|
|
110
|
+
"color:var(--dsw-alias-label-primary);font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.5;" +
|
|
111
|
+
"padding:6px 10px;min-height:64px;resize:vertical}" +
|
|
112
|
+
"." + CSS.text + ":focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-1px}";
|
|
113
|
+
var TAG_ID = "@comecaramelos/dsh-chrome-mcp/ChromeMcpCard.module.css";
|
|
114
|
+
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="' + TAG_ID + '"]') === null) {
|
|
115
|
+
var tag = document.createElement("style");
|
|
116
|
+
tag.dataset.plugin = "@comecaramelos/dsh-chrome-mcp";
|
|
117
|
+
tag.dataset.pluginCss = TAG_ID;
|
|
118
|
+
tag.textContent = css;
|
|
119
|
+
document.head.appendChild(tag);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── locale dictionaries ────────────────────────────────────────────
|
|
123
|
+
var en = {
|
|
124
|
+
title: "Chrome DevTools MCP",
|
|
125
|
+
description: "Chrome DevTools MCP server (chrome-devtools-mcp) settings and status",
|
|
126
|
+
flagsTitle: "Extra flags",
|
|
127
|
+
flagsHint: "One flag per line, appended to `npx -y chrome-devtools-mcp@latest`. Defaults to --no-usage-statistics, --no-performance-crux.",
|
|
128
|
+
flagsPlaceholder: "No extra flags",
|
|
129
|
+
flagsSave: "Save flags",
|
|
130
|
+
versionTitle: "Detected Chrome version",
|
|
131
|
+
check: "Re-check Chrome",
|
|
132
|
+
checking: "Checking…",
|
|
133
|
+
docsMessage: "Find the complete list of server parameters (e.g., --headless, --isolated, --slim) and how to configure WebSocket connections in the Configuration Guide:",
|
|
134
|
+
statusError: "Error",
|
|
135
|
+
statusOk: "OK",
|
|
136
|
+
chromeMissing: "Chrome executable not found",
|
|
137
|
+
readOnly: "Settings are read-only in this deployment.",
|
|
138
|
+
expand: "Expand",
|
|
139
|
+
collapse: "Collapse"
|
|
140
|
+
};
|
|
141
|
+
// ── card controller ────────────────────────────────────────────────
|
|
142
|
+
/** Re-describe cadence (ms) while waiting for the host's check run. */
|
|
143
|
+
var CHECK_POLL_TICK_MS = 700;
|
|
144
|
+
/** Give up waiting for the check revision to advance after this (ms). The host check subprocess times out at 15s. */
|
|
145
|
+
var CHECK_POLL_TIMEOUT_MS = 20000;
|
|
146
|
+
/**
|
|
147
|
+
* Projects the bound `chrome-mcp` settings scope onto a snapshot store
|
|
148
|
+
* the card reads through its `useChromeMcpCard` hook, and owns the two
|
|
149
|
+
* write actions (save flags, re-check). `mirror` is the shared
|
|
150
|
+
* settings describe mirror (`settingsScope.describe()`) the re-check
|
|
151
|
+
* action re-reads while the host's in-memory check status is still en
|
|
152
|
+
* route — module plugins have no client→host push channel beyond
|
|
153
|
+
* persisted writes.
|
|
154
|
+
*/
|
|
155
|
+
var ChromeMcpCardController = class {
|
|
156
|
+
constructor(scope, mirror) {
|
|
157
|
+
this.scope = scope;
|
|
158
|
+
this.mirror = mirror;
|
|
159
|
+
this.disposed = false;
|
|
160
|
+
this.pollTimer = null;
|
|
161
|
+
this.pollGeneration = 0;
|
|
162
|
+
this.checkWaitStarted = false;
|
|
163
|
+
this.store = clientStore.createSnapshotStore({
|
|
164
|
+
available: false,
|
|
165
|
+
writable: false,
|
|
166
|
+
extraFlags: [],
|
|
167
|
+
lastError: "",
|
|
168
|
+
chromeMissing: false,
|
|
169
|
+
checkRevision: 0,
|
|
170
|
+
chromeVersion: "",
|
|
171
|
+
actionError: ""
|
|
172
|
+
});
|
|
173
|
+
this.unsubscribe = scope.subscribe(() => this.publish());
|
|
174
|
+
this.publish();
|
|
175
|
+
}
|
|
176
|
+
/** Republish the latest scope snapshot into the card store. */
|
|
177
|
+
publish() {
|
|
178
|
+
if (this.disposed) return;
|
|
179
|
+
var previous = this.store.getSnapshot();
|
|
180
|
+
var snapshot = this.scope.getSnapshot();
|
|
181
|
+
if (snapshot.status !== "ready" || snapshot.value === void 0) {
|
|
182
|
+
this.store.set({
|
|
183
|
+
available: false,
|
|
184
|
+
writable: false,
|
|
185
|
+
extraFlags: [],
|
|
186
|
+
lastError: "",
|
|
187
|
+
chromeMissing: false,
|
|
188
|
+
checkRevision: 0,
|
|
189
|
+
chromeVersion: "",
|
|
190
|
+
actionError: previous.actionError
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
var value = snapshot.value;
|
|
195
|
+
// The status fields (lastError / chromeMissing / checkRevision /
|
|
196
|
+
// chromeVersion) ride the composition **base** layer: describe()
|
|
197
|
+
// clones that entry object fresh on every read, while the frozen
|
|
198
|
+
// resolved `value` folds the base in only at commits. Base-only
|
|
199
|
+
// host changes — the registration-time check, bridge-log and
|
|
200
|
+
// tool-call captures — are therefore invisible to `value`, so
|
|
201
|
+
// status is read from `base`, falling back to `value` only when
|
|
202
|
+
// the read carries no base layer. `extraFlags` keeps reading
|
|
203
|
+
// `value`: the persisted user layer wins there, not the
|
|
204
|
+
// host-seeded entry field.
|
|
205
|
+
var base = snapshot.base ?? {};
|
|
206
|
+
var servedRevision =
|
|
207
|
+
typeof base.checkRevision === "number"
|
|
208
|
+
? base.checkRevision
|
|
209
|
+
: typeof value.checkRevision === "number"
|
|
210
|
+
? value.checkRevision
|
|
211
|
+
: 0;
|
|
212
|
+
this.store.set({
|
|
213
|
+
available: true,
|
|
214
|
+
writable: snapshot.writable === true,
|
|
215
|
+
extraFlags: Array.isArray(value.extraFlags) ? value.extraFlags : [],
|
|
216
|
+
lastError: typeof base.lastError === "string" ? base.lastError : (value.lastError ?? ""),
|
|
217
|
+
chromeMissing: base.chromeMissing === undefined ? value.chromeMissing === true : base.chromeMissing === true,
|
|
218
|
+
checkRevision: servedRevision,
|
|
219
|
+
chromeVersion: typeof base.chromeVersion === "string" ? base.chromeVersion : (typeof value.chromeVersion === "string" ? value.chromeVersion : ""),
|
|
220
|
+
actionError: ""
|
|
221
|
+
});
|
|
222
|
+
// Startup catch-up: the registration-time executable check lands
|
|
223
|
+
// in the base layer *after* this first describe read, and a
|
|
224
|
+
// module plugin has no push channel — poll the shared describe
|
|
225
|
+
// mirror until the served revision advances (the same budget the
|
|
226
|
+
// re-check waits with). A card attaching after the check already
|
|
227
|
+
// ran sees a nonzero revision and does not poll.
|
|
228
|
+
if (!this.checkWaitStarted) {
|
|
229
|
+
this.checkWaitStarted = true;
|
|
230
|
+
if (servedRevision === 0) this.waitForCheck(0, Date.now() + CHECK_POLL_TIMEOUT_MS);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Immediate write of the user's extra flags (persisted; the host's
|
|
235
|
+
* validate hook rejects invalid lists here, and the next bridge
|
|
236
|
+
* launch starts with them).
|
|
237
|
+
*/
|
|
238
|
+
saveFlags(flags) {
|
|
239
|
+
return this.scope.set("extraFlags", flags).catch((error) => this.noteError(error));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Ask the host to re-run the Chrome executable/connection check,
|
|
243
|
+
* then wait for the result: write `recheckNonce`, then poll the
|
|
244
|
+
* shared describe mirror until the served `checkRevision` advances
|
|
245
|
+
* past the click-time value — or the check timeout budget elapses.
|
|
246
|
+
*/
|
|
247
|
+
recheck() {
|
|
248
|
+
if (this.disposed) return Promise.resolve();
|
|
249
|
+
var baseline = this.store.getSnapshot().checkRevision;
|
|
250
|
+
var wait = this.waitForCheck(baseline, Date.now() + CHECK_POLL_TIMEOUT_MS);
|
|
251
|
+
return this.scope
|
|
252
|
+
.set("recheckNonce", Date.now())
|
|
253
|
+
.catch((error) => this.noteError(error))
|
|
254
|
+
.then(() => wait);
|
|
255
|
+
}
|
|
256
|
+
/** Re-describe the settings mirror every tick until the check revision advances or the deadline passes. */
|
|
257
|
+
waitForCheck(baseline, deadline) {
|
|
258
|
+
var controller = this;
|
|
259
|
+
// A new wait supersedes any pending one (the startup catch-up
|
|
260
|
+
// poll yielding to a manual re-check), so exactly one poll loop
|
|
261
|
+
// runs and the superseded wait settles instead of dangling.
|
|
262
|
+
var generation = ++controller.pollGeneration;
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
var timer;
|
|
265
|
+
var settled = false;
|
|
266
|
+
var arm = (delay) => {
|
|
267
|
+
timer = setTimeout(tick, delay);
|
|
268
|
+
controller.pollTimer = timer;
|
|
269
|
+
};
|
|
270
|
+
var finish = () => {
|
|
271
|
+
if (settled) return;
|
|
272
|
+
settled = true;
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
if (controller.pollTimer === timer) controller.pollTimer = null;
|
|
275
|
+
resolve();
|
|
276
|
+
};
|
|
277
|
+
var satisfied = () =>
|
|
278
|
+
controller.store.getSnapshot().checkRevision > baseline || Date.now() >= deadline;
|
|
279
|
+
var stale = () => controller.disposed || generation !== controller.pollGeneration;
|
|
280
|
+
var tick = () => {
|
|
281
|
+
if (stale() || satisfied()) {
|
|
282
|
+
finish();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
var loaded;
|
|
286
|
+
try {
|
|
287
|
+
loaded = controller.mirror.load();
|
|
288
|
+
} catch (error) {
|
|
289
|
+
loaded = void 0; // mirror read failure: the next tick retries
|
|
290
|
+
}
|
|
291
|
+
Promise.resolve(loaded)
|
|
292
|
+
.catch(() => {})
|
|
293
|
+
.then(() => {
|
|
294
|
+
if (stale() || satisfied()) {
|
|
295
|
+
finish();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
arm(CHECK_POLL_TICK_MS);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
arm(CHECK_POLL_TICK_MS);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
noteError(error) {
|
|
305
|
+
if (this.disposed) return;
|
|
306
|
+
var previous = this.store.getSnapshot();
|
|
307
|
+
this.store.set({
|
|
308
|
+
...previous,
|
|
309
|
+
actionError: error != null && typeof error.message === "string" ? error.message : String(error)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** The face the card's slot registration injects. */
|
|
313
|
+
inject() {
|
|
314
|
+
return {
|
|
315
|
+
hooks: { chromeMcpCard: this.store },
|
|
316
|
+
saveFlags: (flags) => this.saveFlags(flags),
|
|
317
|
+
recheck: () => this.recheck(),
|
|
318
|
+
/** One mirror re-read (used when the card opens). */
|
|
319
|
+
refresh: () => this.refresh()
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Re-read the shared describe mirror once so base-layer status
|
|
324
|
+
* captured asynchronously by the host (bridge connection errors)
|
|
325
|
+
* reaches an opened card without waiting for a write or a re-check.
|
|
326
|
+
*/
|
|
327
|
+
refresh() {
|
|
328
|
+
if (this.disposed) return Promise.resolve();
|
|
329
|
+
var load;
|
|
330
|
+
try {
|
|
331
|
+
load = this.mirror.load();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
load = void 0;
|
|
334
|
+
}
|
|
335
|
+
return Promise.resolve(load).catch(() => {});
|
|
336
|
+
}
|
|
337
|
+
dispose() {
|
|
338
|
+
this.disposed = true;
|
|
339
|
+
if (this.pollTimer !== null) {
|
|
340
|
+
clearTimeout(this.pollTimer);
|
|
341
|
+
this.pollTimer = null;
|
|
342
|
+
}
|
|
343
|
+
this.unsubscribe();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// ── card component ─────────────────────────────────────────────────
|
|
348
|
+
/** Chevron matching the shell's IconChevronDownOutline14 primitive. */
|
|
349
|
+
var CHEVRON_PATH =
|
|
350
|
+
"M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z";
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Render the Chrome DevTools MCP settings/status card. Collapsible
|
|
354
|
+
* like the stock plugin cards: the header is a disclosure button
|
|
355
|
+
* (closed by default) and the controls live in the body.
|
|
356
|
+
* @param props - locale copy, the card snapshot hook, and its actions.
|
|
357
|
+
* @returns the card, or nothing until the namespace is served.
|
|
358
|
+
*/
|
|
359
|
+
function ChromeMcpCard(props) {
|
|
360
|
+
var t = props.t;
|
|
361
|
+
var state = props.useChromeMcpCard((snapshot) => snapshot);
|
|
362
|
+
var openState = react.useState(false);
|
|
363
|
+
var open = openState[0];
|
|
364
|
+
var setOpen = openState[1];
|
|
365
|
+
var checkingState = react.useState(false);
|
|
366
|
+
var isChecking = checkingState[0];
|
|
367
|
+
var setChecking = checkingState[1];
|
|
368
|
+
var bodyId = react.useId();
|
|
369
|
+
var flagsRef = react.useRef(null);
|
|
370
|
+
if (!state.available) return null;
|
|
371
|
+
// Header status dot: the single error surface. Red while a
|
|
372
|
+
// check/connection error, a rejected action, or the known
|
|
373
|
+
// executable-missing flag (which survives bridge-update-success
|
|
374
|
+
// clears of lastError) is present; hidden while healthy.
|
|
375
|
+
var error = state.lastError || state.actionError || "";
|
|
376
|
+
var tip = error !== "" ? error : state.chromeMissing === true ? t("chromeMissing") : "";
|
|
377
|
+
var dotAria = tip !== "" ? t("statusError") : t("statusOk");
|
|
378
|
+
return (0, reactJsx.jsx)("li", {
|
|
379
|
+
className: CSS.card + (open ? " " + CSS.cardOpen : ""),
|
|
380
|
+
children: [
|
|
381
|
+
(0, reactJsx.jsxs)("button", {
|
|
382
|
+
type: "button",
|
|
383
|
+
className: CSS.header,
|
|
384
|
+
"aria-expanded": open,
|
|
385
|
+
"aria-label": t(open ? "collapse" : "expand") + ": " + t("title"),
|
|
386
|
+
"aria-controls": bodyId,
|
|
387
|
+
onClick: () => {
|
|
388
|
+
setOpen(!open);
|
|
389
|
+
if (!open && typeof props.refresh === "function") props.refresh();
|
|
390
|
+
},
|
|
391
|
+
children: [
|
|
392
|
+
(0, reactJsx.jsxs)("span", {
|
|
393
|
+
className: CSS.headText,
|
|
394
|
+
children: [
|
|
395
|
+
(0, reactJsx.jsxs)("div", {
|
|
396
|
+
className: CSS.nameRow,
|
|
397
|
+
children: [
|
|
398
|
+
(0, reactJsx.jsx)("span", {
|
|
399
|
+
className: CSS.name,
|
|
400
|
+
children: t("title")
|
|
401
|
+
}),
|
|
402
|
+
tip !== ""
|
|
403
|
+
? (0, reactJsx.jsx)("span", {
|
|
404
|
+
className: CSS.dot + " " + CSS.dotError,
|
|
405
|
+
role: "img",
|
|
406
|
+
"aria-label": dotAria,
|
|
407
|
+
title: tip
|
|
408
|
+
})
|
|
409
|
+
: null
|
|
410
|
+
]
|
|
411
|
+
}),
|
|
412
|
+
(0, reactJsx.jsx)("span", { className: CSS.desc, children: t("description") })
|
|
413
|
+
]
|
|
414
|
+
}),
|
|
415
|
+
(0, reactJsx.jsx)("svg", {
|
|
416
|
+
width: 14,
|
|
417
|
+
height: 14,
|
|
418
|
+
className: CSS.chevron + (open ? " " + CSS.chevronOpen : ""),
|
|
419
|
+
viewBox: "0 0 14 14",
|
|
420
|
+
fill: "none",
|
|
421
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
422
|
+
children: (0, reactJsx.jsx)("path", { d: CHEVRON_PATH, fill: "currentColor" })
|
|
423
|
+
})
|
|
424
|
+
]
|
|
425
|
+
}),
|
|
426
|
+
open
|
|
427
|
+
? (0, reactJsx.jsxs)("div", {
|
|
428
|
+
id: bodyId,
|
|
429
|
+
className: CSS.body,
|
|
430
|
+
children: [
|
|
431
|
+
!state.writable
|
|
432
|
+
? (0, reactJsx.jsx)("p", { className: CSS.readOnly, role: "status", children: t("readOnly") })
|
|
433
|
+
: null,
|
|
434
|
+
(0, reactJsx.jsxs)("div", {
|
|
435
|
+
className: CSS.field,
|
|
436
|
+
children: [
|
|
437
|
+
(0, reactJsx.jsxs)("div", {
|
|
438
|
+
className: CSS.rowText,
|
|
439
|
+
children: [
|
|
440
|
+
(0, reactJsx.jsx)("div", { className: CSS.fieldTitle, children: t("flagsTitle") }),
|
|
441
|
+
(0, reactJsx.jsx)("div", { className: CSS.fieldDesc, children: t("flagsHint") })
|
|
442
|
+
]
|
|
443
|
+
})
|
|
444
|
+
]
|
|
445
|
+
}),
|
|
446
|
+
// Extra flags editor: one flag per line; the write goes
|
|
447
|
+
// through props.saveFlags (host validate + fiber.update).
|
|
448
|
+
// `key` re-mounts the textarea when the store value
|
|
449
|
+
// changes underneath the editor (e.g. a save from another
|
|
450
|
+
// window), so the shown value never drifts.
|
|
451
|
+
state.writable
|
|
452
|
+
? (0, reactJsx.jsxs)("div", {
|
|
453
|
+
className: CSS.editor,
|
|
454
|
+
children: [
|
|
455
|
+
(0, reactJsx.jsx)("textarea", {
|
|
456
|
+
className: CSS.text,
|
|
457
|
+
rows: Math.max(3, state.extraFlags.length),
|
|
458
|
+
placeholder: t("flagsPlaceholder"),
|
|
459
|
+
defaultValue: state.extraFlags.join("\n"),
|
|
460
|
+
key: state.extraFlags.join("\n"),
|
|
461
|
+
ref: flagsRef
|
|
462
|
+
}),
|
|
463
|
+
(0, reactJsx.jsx)("div", {
|
|
464
|
+
className: CSS.row,
|
|
465
|
+
children: (0, reactJsx.jsx)("button", {
|
|
466
|
+
type: "button",
|
|
467
|
+
className: CSS.button,
|
|
468
|
+
disabled: !state.writable,
|
|
469
|
+
onClick: () => {
|
|
470
|
+
var raw = flagsRef.current && typeof flagsRef.current.value === "string" ? flagsRef.current.value : "";
|
|
471
|
+
props.saveFlags(
|
|
472
|
+
raw
|
|
473
|
+
.split(/\r?\n/u)
|
|
474
|
+
.map((line) => line.trim())
|
|
475
|
+
.filter((line) => line !== "")
|
|
476
|
+
);
|
|
477
|
+
},
|
|
478
|
+
children: t("flagsSave")
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
]
|
|
482
|
+
})
|
|
483
|
+
: (0, reactJsx.jsx)("p", {
|
|
484
|
+
className: CSS.value,
|
|
485
|
+
children:
|
|
486
|
+
state.extraFlags.length > 0 ? state.extraFlags.join("\n") : t("flagsPlaceholder")
|
|
487
|
+
}),
|
|
488
|
+
(0, reactJsx.jsxs)("div", {
|
|
489
|
+
className: CSS.row,
|
|
490
|
+
children: [
|
|
491
|
+
(0, reactJsx.jsx)("button", {
|
|
492
|
+
type: "button",
|
|
493
|
+
className: CSS.button,
|
|
494
|
+
disabled: !state.writable || isChecking,
|
|
495
|
+
onClick: () => {
|
|
496
|
+
setChecking(true);
|
|
497
|
+
var checkPromise;
|
|
498
|
+
try {
|
|
499
|
+
checkPromise = props.recheck();
|
|
500
|
+
} catch (error) {
|
|
501
|
+
checkPromise = Promise.reject(error);
|
|
502
|
+
}
|
|
503
|
+
if (checkPromise == null || typeof checkPromise.finally !== "function") {
|
|
504
|
+
checkPromise = Promise.resolve(checkPromise);
|
|
505
|
+
}
|
|
506
|
+
checkPromise.finally(() => setChecking(false));
|
|
507
|
+
},
|
|
508
|
+
children: isChecking ? t("checking") : t("check")
|
|
509
|
+
})
|
|
510
|
+
]
|
|
511
|
+
}),
|
|
512
|
+
// Executable version discovered by the last check.
|
|
513
|
+
state.chromeVersion !== ""
|
|
514
|
+
? (0, reactJsx.jsxs)("div", {
|
|
515
|
+
className: CSS.field,
|
|
516
|
+
children: [
|
|
517
|
+
(0, reactJsx.jsxs)("div", {
|
|
518
|
+
className: CSS.rowText,
|
|
519
|
+
children: [
|
|
520
|
+
(0, reactJsx.jsx)("div", { className: CSS.fieldTitle, children: t("versionTitle") }),
|
|
521
|
+
(0, reactJsx.jsx)("p", {
|
|
522
|
+
className: CSS.value,
|
|
523
|
+
children: state.chromeVersion
|
|
524
|
+
})
|
|
525
|
+
]
|
|
526
|
+
})
|
|
527
|
+
]
|
|
528
|
+
})
|
|
529
|
+
: null,
|
|
530
|
+
// Product spec: the full configuration-guide message, with
|
|
531
|
+
// the upstream URL rendered as the external link.
|
|
532
|
+
(0, reactJsx.jsxs)("p", {
|
|
533
|
+
className: CSS.docs,
|
|
534
|
+
children: [
|
|
535
|
+
t("docsMessage") + " ",
|
|
536
|
+
(0, reactJsx.jsx)("a", {
|
|
537
|
+
className: CSS.link,
|
|
538
|
+
href: CONFIGURATION_URL,
|
|
539
|
+
target: "_blank",
|
|
540
|
+
rel: "noopener noreferrer",
|
|
541
|
+
children: CONFIGURATION_URL
|
|
542
|
+
})
|
|
543
|
+
]
|
|
544
|
+
})
|
|
545
|
+
]
|
|
546
|
+
})
|
|
547
|
+
: null
|
|
548
|
+
]
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── plugin body ────────────────────────────────────────────────────
|
|
553
|
+
/**
|
|
554
|
+
* Mount the settings/status card.
|
|
555
|
+
* @param ctx - the browser plugin context.
|
|
556
|
+
*/
|
|
557
|
+
function apply(ctx) {
|
|
558
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, { en }), "chrome-mcp: dictionaries");
|
|
559
|
+
var binder = ctx.settingsScope;
|
|
560
|
+
var scope = binder.bind({ namespace: NS });
|
|
561
|
+
// The shared describe mirror: the recheck action re-reads it while
|
|
562
|
+
// the host's in-memory check status is en route.
|
|
563
|
+
var controller = new ChromeMcpCardController(scope, binder.describe());
|
|
564
|
+
ctx.effect(
|
|
565
|
+
() => () => controller.dispose(),
|
|
566
|
+
"chrome-mcp: card controller"
|
|
567
|
+
);
|
|
568
|
+
ctx.slots.inject("settings.plugin.item", () =>
|
|
569
|
+
ctx.slots.register(
|
|
570
|
+
{
|
|
571
|
+
name: "settings.plugin.item",
|
|
572
|
+
key: NS,
|
|
573
|
+
locale: LOCALE_NS,
|
|
574
|
+
inject: () => controller.inject()
|
|
575
|
+
},
|
|
576
|
+
ChromeMcpCard
|
|
577
|
+
)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
exports.apply = apply;
|
|
582
|
+
exports.inject = inject;
|
|
583
|
+
return module.exports;
|
|
584
|
+
}
|
|
585
|
+
});
|