dsh-mcp-panel 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -75,6 +75,10 @@ window.__ModuleLoader__.load({
75
75
  badge: "killed",
76
76
  tone: "muted"
77
77
  };
78
+ default: return {
79
+ badge: "unknown",
80
+ tone: "muted"
81
+ };
78
82
  }
79
83
  }
80
84
  /**
@@ -112,6 +116,39 @@ window.__ModuleLoader__.load({
112
116
  refreshIntervalMs: snapshot.refreshIntervalMs
113
117
  };
114
118
  }
119
+ /**
120
+ * Count the summary facts for the tab header line. Counting is derived from
121
+ * the same badge codes the cards show, so the line can never disagree with
122
+ * the rows beneath it.
123
+ *
124
+ * @param servers - the presented server rows (typically the filtered list).
125
+ * @returns the summary counts.
126
+ */
127
+ function summarizePanel(servers) {
128
+ let connected = 0;
129
+ let errored = 0;
130
+ for (const row of servers) if (row.badge === "connected") connected += 1;
131
+ else if (row.badge === "failed" || row.badge === "exhausted") errored += 1;
132
+ return {
133
+ total: servers.length,
134
+ connected,
135
+ errored
136
+ };
137
+ }
138
+ /**
139
+ * Filter server rows by a case-insensitive substring match against the
140
+ * server name or its display target. An empty query returns the rows
141
+ * unchanged (stable identity, no copies).
142
+ *
143
+ * @param servers - the presented server rows.
144
+ * @param query - the raw filter text.
145
+ * @returns the matching rows.
146
+ */
147
+ function filterServers(servers, query) {
148
+ const needle = query.trim().toLocaleLowerCase();
149
+ if (needle === "") return servers;
150
+ return servers.filter((row) => row.view.serverName.toLocaleLowerCase().includes(needle) || row.view.target.toLocaleLowerCase().includes(needle));
151
+ }
115
152
  //#endregion
116
153
  //#region src/client/McpPanelTab.tsx
117
154
  /** The read-only MCP management tab: server rows, badges, tools, probes. */
@@ -136,6 +173,7 @@ window.__ModuleLoader__.load({
136
173
  case "failed": return t("probeFailed");
137
174
  case "killed": return t("probeKilled");
138
175
  case "stopping": return t("probeStopping");
176
+ default: return t("probeUnknown");
139
177
  }
140
178
  }
141
179
  /** Local wall-clock range for one probe row; component-layer formatting only. */
@@ -148,10 +186,11 @@ window.__ModuleLoader__.load({
148
186
  function McpPanelTab({ status, probe, t }) {
149
187
  const listId = (0, react.useId)();
150
188
  const [request, setRequest] = (0, react.useState)(0);
151
- const [expanded, setExpanded] = (0, react.useState)(null);
189
+ const [expanded, setExpanded] = (0, react.useState)({});
152
190
  const [probeError, setProbeError] = (0, react.useState)(null);
153
191
  const [state, setState] = (0, react.useState)({ status: "loading" });
154
192
  const [toolQueries, setToolQueries] = (0, react.useState)({});
193
+ const [serverQuery, setServerQuery] = (0, react.useState)("");
155
194
  const reload = () => {
156
195
  setRequest((value) => value + 1);
157
196
  };
@@ -162,8 +201,11 @@ window.__ModuleLoader__.load({
162
201
  status: "ready",
163
202
  snapshot
164
203
  });
165
- }, () => {
166
- if (current) setState({ status: "error" });
204
+ }, (error) => {
205
+ if (current) setState({
206
+ status: "error",
207
+ message: error instanceof Error ? error.message : String(error)
208
+ });
167
209
  });
168
210
  return () => {
169
211
  current = false;
@@ -185,11 +227,24 @@ window.__ModuleLoader__.load({
185
227
  document.removeEventListener("visibilitychange", onVisible);
186
228
  };
187
229
  }, [intervalMs]);
230
+ const model = state.status === "ready" ? presentMcpPanel(state.snapshot) : void 0;
231
+ const visibleServers = model !== void 0 ? filterServers(model.servers, serverQuery) : [];
232
+ const summary = model !== void 0 ? summarizePanel(visibleServers) : null;
233
+ const summaryText = summary === null ? "" : t("summary").replace("{total}", String(summary.total)).replace("{connected}", String(summary.connected)).replace("{errored}", String(summary.errored));
234
+ const probeRunning = model !== void 0 && model.probes.some((probe) => probe.view.status === "running");
235
+ (0, react.useEffect)(() => {
236
+ if (!probeRunning) return void 0;
237
+ const timer = setInterval(() => {
238
+ if (!document.hidden) reload();
239
+ }, 1500);
240
+ return () => {
241
+ clearInterval(timer);
242
+ };
243
+ }, [probeRunning]);
188
244
  const retry = () => {
189
245
  setState({ status: "loading" });
190
246
  reload();
191
247
  };
192
- const model = state.status === "ready" ? presentMcpPanel(state.snapshot) : void 0;
193
248
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
194
249
  className: "dmcp-section",
195
250
  "data-dsh-mcp-panel": "",
@@ -201,14 +256,21 @@ window.__ModuleLoader__.load({
201
256
  }) : null,
202
257
  state.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
203
258
  className: "dmcp-failure",
204
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
205
- role: "alert",
206
- children: t("error")
207
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
208
- type: "button",
209
- onClick: retry,
210
- children: t("retry")
211
- })]
259
+ children: [
260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
261
+ role: "alert",
262
+ children: t("error")
263
+ }),
264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
265
+ className: "dmcp-failure-detail",
266
+ children: state.message
267
+ }),
268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
269
+ type: "button",
270
+ onClick: retry,
271
+ children: t("retry")
272
+ })
273
+ ]
212
274
  }) : null,
213
275
  model !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
214
276
  className: "dmcp-panel",
@@ -216,130 +278,177 @@ window.__ModuleLoader__.load({
216
278
  model.empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
217
279
  className: "dmcp-status",
218
280
  children: t("empty")
219
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
220
- className: "dmcp-heading",
221
- children: t("servers")
222
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
223
- className: "dmcp-cards",
224
- children: model.servers.map((row) => {
225
- const open = expanded === row.view.serverName;
226
- const detailId = `${listId}-${encodeURIComponent(row.view.serverName)}`;
227
- const toolQuery = toolQueries[row.view.serverName] ?? "";
228
- const probeRunning = model.probes.some((probe) => probe.view.status === "running" && probe.view.serverName === row.view.serverName);
229
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
230
- className: "dmcp-card",
231
- "data-mcp-server": row.view.serverName,
232
- "data-open": open ? "true" : void 0,
233
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
281
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
283
+ className: "dmcp-heading",
284
+ children: t("servers")
285
+ }),
286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
287
+ className: "dmcp-summary",
288
+ children: summaryText
289
+ }),
290
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
291
+ className: "dmcp-toolbar",
292
+ children: [
293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
294
+ type: "search",
295
+ className: "dmcp-tool-filter dmcp-server-filter",
296
+ value: serverQuery,
297
+ placeholder: t("filterServers"),
298
+ "aria-label": t("filterServers"),
299
+ onChange: (event) => {
300
+ setServerQuery(event.currentTarget.value);
301
+ }
302
+ }),
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
234
304
  type: "button",
235
- className: "dmcp-card-content",
236
- "aria-expanded": open,
237
- "aria-controls": detailId,
305
+ className: "dmcp-action",
238
306
  onClick: () => {
239
- setExpanded((current) => current === row.view.serverName ? null : row.view.serverName);
307
+ setExpanded(Object.fromEntries(visibleServers.map((row) => [row.view.serverName, true])));
240
308
  },
241
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
242
- className: "dmcp-card-title",
243
- children: row.view.serverName
244
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
245
- className: "dmcp-card-trailing",
309
+ children: t("expandAll")
310
+ }),
311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
312
+ type: "button",
313
+ className: "dmcp-action",
314
+ onClick: () => {
315
+ setExpanded({});
316
+ },
317
+ children: t("collapseAll")
318
+ })
319
+ ]
320
+ }),
321
+ visibleServers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
322
+ className: "dmcp-status",
323
+ children: t("noMatch")
324
+ }) : null,
325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
326
+ className: "dmcp-cards",
327
+ children: visibleServers.map((row) => {
328
+ const open = expanded[row.view.serverName] === true;
329
+ const detailId = `${listId}-${encodeURIComponent(row.view.serverName)}`;
330
+ const toolQuery = toolQueries[row.view.serverName] ?? "";
331
+ const rowProbeRunning = model.probes.some((probe) => probe.view.status === "running" && probe.view.serverName === row.view.serverName);
332
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
333
+ className: "dmcp-card",
334
+ "data-mcp-server": row.view.serverName,
335
+ "data-open": open ? "true" : void 0,
336
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
337
+ type: "button",
338
+ className: "dmcp-card-content",
339
+ "aria-expanded": open,
340
+ "aria-controls": detailId,
341
+ onClick: () => {
342
+ setExpanded((current) => {
343
+ const next = { ...current };
344
+ if (next[row.view.serverName] === true) delete next[row.view.serverName];
345
+ else next[row.view.serverName] = true;
346
+ return next;
347
+ });
348
+ },
349
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
350
+ className: "dmcp-card-title",
351
+ children: row.view.serverName
352
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
353
+ className: "dmcp-card-trailing",
354
+ children: [
355
+ row.view.probeState !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
356
+ tone: row.view.probeState === "reachable" ? "ok" : "error",
357
+ label: row.view.probeState === "reachable" ? t("probeReachable") : t("probeUnreachable")
358
+ }) : null,
359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
360
+ tone: row.tone,
361
+ label: badgeLabel(row.badge, t)
362
+ }),
363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
364
+ className: "dmcp-tool-count",
365
+ children: [
366
+ row.view.toolCount,
367
+ " ",
368
+ t("tools")
369
+ ]
370
+ })
371
+ ]
372
+ })]
373
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
374
+ className: "dmcp-card-details",
375
+ id: detailId,
246
376
  children: [
247
- row.view.probeState !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
248
- tone: row.view.probeState === "reachable" ? "ok" : "error",
249
- label: row.view.probeState === "reachable" ? t("probeReachable") : t("probeUnreachable")
250
- }) : null,
251
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
252
- tone: row.tone,
253
- label: badgeLabel(row.badge, t)
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
378
+ className: "dmcp-details",
379
+ children: [
380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: badgeLabel(row.badge, t) })] }),
381
+ row.hasAttemptBudget ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("attempt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
382
+ row.view.attempt < 0 ? t("none") : row.view.attempt,
383
+ "/",
384
+ row.view.maxAttempts < 0 ? t("none") : row.view.maxAttempts
385
+ ] })] }) : null,
386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("reconnects") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: row.reconnects ?? t("none") })] }),
387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("lastError") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
388
+ className: row.hasError ? "dmcp-error-text" : void 0,
389
+ children: row.view.lastError ?? t("none")
390
+ })] }),
391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("fiber") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: row.view.fiberPhase ?? t("none") })] }),
392
+ row.view.configuredNote !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("configured") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: row.view.configuredNote })] }) : null,
393
+ row.ageSeconds !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("lastEvent") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [row.ageSeconds, "s"] })] }) : null,
394
+ row.view.delayMs !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("retryIn") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
395
+ row.view.delayMs,
396
+ " ",
397
+ t("ms")
398
+ ] })] }) : null
399
+ ]
254
400
  }),
255
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
256
- className: "dmcp-tool-count",
401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
402
+ className: "dmcp-target",
403
+ title: row.view.target,
257
404
  children: [
258
- row.view.toolCount,
405
+ row.view.transport,
259
406
  " ",
260
- t("tools")
407
+ row.view.target
261
408
  ]
262
- })
409
+ }),
410
+ row.view.transport === "streamable-http" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
411
+ type: "button",
412
+ className: "dmcp-probe-now",
413
+ disabled: rowProbeRunning,
414
+ onClick: () => {
415
+ setProbeError(null);
416
+ Promise.resolve().then(() => probe(row.view.serverName)).then(() => {
417
+ reload();
418
+ }, (error) => {
419
+ setProbeError(error instanceof Error ? error.message : String(error));
420
+ });
421
+ },
422
+ children: rowProbeRunning ? t("probeRunning") : t("probeNow")
423
+ }) : null,
424
+ row.view.tools.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
425
+ className: "dmcp-status",
426
+ children: t("noTools")
427
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
428
+ type: "search",
429
+ className: "dmcp-tool-filter",
430
+ value: toolQuery,
431
+ placeholder: t("filterTools"),
432
+ "aria-label": t("filterTools"),
433
+ onChange: (event) => {
434
+ setToolQueries((current) => ({
435
+ ...current,
436
+ [row.view.serverName]: event.currentTarget.value
437
+ }));
438
+ }
439
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
440
+ className: "dmcp-tools",
441
+ children: row.view.tools.filter((tool) => toolQuery.trim() === "" || tool.name.toLocaleLowerCase().includes(toolQuery.trim().toLocaleLowerCase()) || tool.description.toLocaleLowerCase().includes(toolQuery.trim().toLocaleLowerCase())).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: tool.name }), tool.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
442
+ className: "dmcp-tool-description",
443
+ children: tool.description
444
+ }) : null] }, tool.name))
445
+ })] })
263
446
  ]
264
- })]
265
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
266
- className: "dmcp-card-details",
267
- id: detailId,
268
- children: [
269
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
270
- className: "dmcp-details",
271
- children: [
272
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("status") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: badgeLabel(row.badge, t) })] }),
273
- row.hasAttemptBudget ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("attempt") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
274
- row.view.attempt < 0 ? t("none") : row.view.attempt,
275
- "/",
276
- row.view.maxAttempts < 0 ? t("none") : row.view.maxAttempts
277
- ] })] }) : null,
278
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("reconnects") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: row.reconnects ?? t("none") })] }),
279
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("lastError") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", {
280
- className: row.hasError ? "dmcp-error-text" : void 0,
281
- children: row.view.lastError ?? t("none")
282
- })] }),
283
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("fiber") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: row.view.fiberPhase ?? t("none") })] }),
284
- row.view.configuredNote !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("configured") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: row.view.configuredNote })] }) : null,
285
- row.ageSeconds !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("lastEvent") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [row.ageSeconds, "s"] })] }) : null,
286
- row.view.delayMs !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("retryIn") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
287
- row.view.delayMs,
288
- " ",
289
- t("ms")
290
- ] })] }) : null
291
- ]
292
- }),
293
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
294
- className: "dmcp-target",
295
- title: row.view.target,
296
- children: [
297
- row.view.transport,
298
- " ",
299
- row.view.target
300
- ]
301
- }),
302
- row.view.transport === "streamable-http" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
303
- type: "button",
304
- className: "dmcp-probe-now",
305
- disabled: probeRunning,
306
- onClick: () => {
307
- setProbeError(null);
308
- Promise.resolve().then(() => probe(row.view.serverName)).then(() => {
309
- reload();
310
- }, (error) => {
311
- setProbeError(error instanceof Error ? error.message : String(error));
312
- });
313
- },
314
- children: probeRunning ? t("probeRunning") : t("probeNow")
315
- }) : null,
316
- row.view.tools.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
317
- className: "dmcp-status",
318
- children: t("noTools")
319
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
320
- type: "search",
321
- className: "dmcp-tool-filter",
322
- value: toolQuery,
323
- placeholder: t("filterTools"),
324
- "aria-label": t("filterTools"),
325
- onChange: (event) => {
326
- setToolQueries((current) => ({
327
- ...current,
328
- [row.view.serverName]: event.currentTarget.value
329
- }));
330
- }
331
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
332
- className: "dmcp-tools",
333
- children: row.view.tools.filter((tool) => toolQuery.trim() === "" || tool.name.toLocaleLowerCase().includes(toolQuery.trim().toLocaleLowerCase()) || tool.description.toLocaleLowerCase().includes(toolQuery.trim().toLocaleLowerCase())).map((tool) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: tool.name }), tool.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
334
- className: "dmcp-tool-description",
335
- children: tool.description
336
- }) : null] }, tool.name))
337
- })] })
338
- ]
339
- }) : null]
340
- }, row.view.serverName);
447
+ }) : null]
448
+ }, row.view.serverName);
449
+ })
341
450
  })
342
- })] }),
451
+ ] }),
343
452
  !model.observed && !model.empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
344
453
  className: "dmcp-derived-note",
345
454
  children: t("derivedNote")
@@ -398,13 +507,12 @@ window.__ModuleLoader__.load({
398
507
  ]
399
508
  });
400
509
  }
401
- /** One tone-colored badge chip. */
510
+ /** One tone-colored badge chip. The label is visible text, so no `role="img"`
511
+ * or `aria-label`: screen readers announce it exactly once. */
402
512
  function Badge({ tone, label }) {
403
513
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
404
514
  className: "dmcp-badge",
405
515
  "data-tone": tone,
406
- role: "img",
407
- "aria-label": label,
408
516
  title: label,
409
517
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
410
518
  className: "dmcp-dot",
@@ -423,6 +531,11 @@ window.__ModuleLoader__.load({
423
531
  retry: "重试",
424
532
  empty: "此 profile 未配置官方 MCP 服务器(@deepseek-ai/dsh-mcp-client 行)。",
425
533
  servers: "服务器",
534
+ summary: "服务器 {total} 台:已连接 {connected},错误 {errored}",
535
+ filterServers: "过滤服务器",
536
+ noMatch: "没有匹配的服务器。",
537
+ expandAll: "全部展开",
538
+ collapseAll: "全部折叠",
426
539
  tools: "工具",
427
540
  noTools: "未注册工具",
428
541
  status: "状态",
@@ -452,6 +565,7 @@ window.__ModuleLoader__.load({
452
565
  probeFailed: "失败",
453
566
  probeKilled: "已取消",
454
567
  probeStopping: "取消中",
568
+ probeUnknown: "未知",
455
569
  probeNow: "探测",
456
570
  probeReachable: "可达",
457
571
  probeUnreachable: "不可达",
@@ -466,6 +580,11 @@ window.__ModuleLoader__.load({
466
580
  retry: "Retry",
467
581
  empty: "This profile configures no official MCP servers (@deepseek-ai/dsh-mcp-client rows).",
468
582
  servers: "Servers",
583
+ summary: "{total} servers: {connected} connected, {errored} with errors",
584
+ filterServers: "Filter servers",
585
+ noMatch: "No servers match the filter.",
586
+ expandAll: "Expand all",
587
+ collapseAll: "Collapse all",
469
588
  tools: "Tools",
470
589
  noTools: "No tools registered",
471
590
  status: "Status",
@@ -495,6 +614,7 @@ window.__ModuleLoader__.load({
495
614
  probeFailed: "Failed",
496
615
  probeKilled: "Killed",
497
616
  probeStopping: "Stopping",
617
+ probeUnknown: "Unknown",
498
618
  probeNow: "Probe",
499
619
  probeReachable: "Reachable",
500
620
  probeUnreachable: "Unreachable",
@@ -4663,7 +4783,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4663
4783
  literal("stopping"),
4664
4784
  literal("completed"),
4665
4785
  literal("killed"),
4666
- literal("failed")
4786
+ literal("failed"),
4787
+ literal("unknown")
4667
4788
  ]),
4668
4789
  startedAt: number().int(),
4669
4790
  finishedAt: number().int().nullable(),
@@ -4788,6 +4909,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4788
4909
  font: inherit;
4789
4910
  cursor: pointer;
4790
4911
  }
4912
+ [data-dsh-mcp-panel] .dmcp-failure-detail {
4913
+ margin: 0;
4914
+ color: var(--dsw-alias-label-secondary);
4915
+ overflow-wrap: anywhere;
4916
+ }
4791
4917
  [data-dsh-mcp-panel] .dmcp-panel {
4792
4918
  display: flex;
4793
4919
  flex-direction: column;
@@ -4799,6 +4925,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4799
4925
  font-size: 1em;
4800
4926
  color: var(--dsw-alias-label-primary);
4801
4927
  }
4928
+ [data-dsh-mcp-panel] .dmcp-summary {
4929
+ margin: 0;
4930
+ color: var(--dsw-alias-label-secondary);
4931
+ }
4932
+ [data-dsh-mcp-panel] .dmcp-toolbar {
4933
+ display: flex;
4934
+ align-items: center;
4935
+ gap: 8px;
4936
+ flex-wrap: wrap;
4937
+ }
4938
+ [data-dsh-mcp-panel] .dmcp-server-filter {
4939
+ flex: 1 1 180px;
4940
+ min-width: 0;
4941
+ }
4942
+ [data-dsh-mcp-panel] .dmcp-action {
4943
+ font: inherit;
4944
+ cursor: pointer;
4945
+ color: var(--dsw-alias-label-primary);
4946
+ background: var(--dsw-alias-bg-layer-1);
4947
+ border: 1px solid var(--dsw-alias-border-l3);
4948
+ border-radius: 4px;
4949
+ padding: 4px 10px;
4950
+ }
4951
+ [data-dsh-mcp-panel] .dmcp-action:focus-visible {
4952
+ outline: 2px solid var(--dsw-alias-brand-primary);
4953
+ outline-offset: -2px;
4954
+ }
4802
4955
  [data-dsh-mcp-panel] .dmcp-cards {
4803
4956
  list-style: none;
4804
4957
  margin: 0;