@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/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
+ });