dsh-thinking-guard 0.1.21 → 0.1.22

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
@@ -3919,6 +3919,30 @@ button.v3-tag{border:0;cursor:pointer;font-family:inherit}
3919
3919
  button.v3-tag:disabled{opacity:.55;cursor:default}
3920
3920
  .v3-group>summary:focus-visible{outline:2px solid var(--v3-info);outline-offset:-2px}
3921
3921
  .v3-tools{display:flex;flex-direction:column}
3922
+ /* ---- v3 catalog rework: 一行工具栏 / 能力诊断卡片 / 卡片向总览看齐 ---- */
3923
+ .mc-cat-root{display:flex;flex-direction:column;gap:12px;min-width:0}
3924
+ .mc-cat-root .v3-catstat{margin-bottom:0}
3925
+ .mc-cat-bar{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;min-width:0}
3926
+ .mc-cat-bar>.mc-cat-actions{flex:none}
3927
+ .mc-cat-bar .mc-cat-actions button{font-size:11px;white-space:nowrap}
3928
+ .mc-cat-tabs{display:flex;align-items:center;flex:none;min-width:0}
3929
+ .mc-cat-tabs .mc-main-tabs{border-bottom:0}
3930
+ .mc-cat-search{flex:1 1 auto;width:auto;min-width:96px;max-width:220px;display:flex}
3931
+ .mc-cat-search input{box-sizing:border-box;width:100%;height:28px;padding:0 9px;border:1px solid var(--v3-line-2);border-radius:var(--v3-r-sm);background:var(--v3-panel);color:var(--v3-t1);font:inherit;font-size:12.5px}
3932
+ .mc-cat-search input:hover{border-color:var(--v3-line-2)}
3933
+ .mc-cat-search input:focus-visible{outline:2px solid var(--v3-info);outline-offset:1px;border-color:transparent}
3934
+ .mc-cat-actions{display:flex;align-items:center;gap:6px;flex:none;margin-left:auto;flex-wrap:nowrap}
3935
+ .mc-cat-diag{background:var(--v3-panel);border:1px solid var(--v3-line);border-radius:var(--v3-r-lg);overflow:hidden}
3936
+ .mc-cat-diag>summary{display:flex;align-items:center;gap:8px;padding:11px 15px;min-height:38px;cursor:pointer;list-style:none;font-size:12.5px;font-weight:600;color:var(--v3-t2)}
3937
+ .mc-cat-diag>summary::-webkit-details-marker{display:none}
3938
+ .mc-cat-diag>summary:hover{background:var(--v3-panel-2)}
3939
+ .mc-cat-diag[open]>summary{border-bottom:1px solid var(--v3-line)}
3940
+ .mc-cat-diag-body{display:flex;flex-direction:column;gap:4px;padding:11px 15px;font-size:11.5px;line-height:18px;color:var(--v3-t3)}
3941
+ .v3-root .v3-catgrid{gap:14px}
3942
+ .v3-root .v3-group{border-radius:var(--v3-r-lg);margin-bottom:0}
3943
+ .v3-root .v3-group>summary{padding:11px 15px;min-height:38px}
3944
+ .v3-root .v3-tag{font-size:11px;height:19px;line-height:19px;padding:0 7px}
3945
+ .v3-root .v3-trow{min-height:34px;font-size:12.5px}
3922
3946
  `;
3923
3947
  if (typeof document !== "undefined" && document.querySelector(`style[data-css-id="${CSS_ID}"]`) === null) {
3924
3948
  const tag = document.createElement("style");
@@ -3991,14 +4015,6 @@ function MarkdownFilePreview(props) {
3991
4015
  })] });
3992
4016
  }
3993
4017
  /**
3994
- * Which build of the plugin is on screen. Several rounds of "is my profile
3995
- * running the version I just installed?" had to be answered by grepping
3996
- * `node_modules`, because the panel said nothing about itself. The value is
3997
- * inlined by `tsdown` (see `src/client/env.d.ts`).
3998
- */
3999
- const PLUGIN_VERSION = "0.1.21";
4000
- const PLUGIN_TITLE = `capability-menu v${PLUGIN_VERSION}`;
4001
- /**
4002
4018
  * dsh's skill source labels, shown on rows this plugin cannot edit — so "why is
4003
4019
  * there no 编辑 button" has a visible answer. Labels that name one directory are
4004
4020
  * shown as that path; the rest name a category and go through the dictionary, so
@@ -4882,128 +4898,116 @@ function ReadyBody(props) {
4882
4898
  }
4883
4899
  ];
4884
4900
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4885
- className: "v3-root",
4901
+ className: "v3-root mc-cat-root",
4886
4902
  children: [
4887
4903
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(V3Styles, {}),
4888
4904
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4889
- className: "mc-heading-row",
4890
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4891
- className: "mc-heading-meta",
4892
- children: [
4893
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4894
- className: "mc-heading",
4895
- children: t("title")
4896
- }),
4897
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4898
- className: "mc-version",
4899
- title: PLUGIN_TITLE,
4900
- children: ["v", PLUGIN_VERSION]
4901
- }),
4902
- updateStatus?.updateAvailable && (updateStatus.updatable || updateStatus.reason === "local") && updatePhase !== "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4903
- type: "button",
4904
- variant: "primary",
4905
- size: "sm",
4906
- className: "mc-update-check",
4907
- title: t("updateAvailable", { version: updateStatus.latest }),
4908
- disabled: updatePhase === "installing",
4909
- onClick: () => setUpdateDialogOpen(true),
4910
- children: t("newVersion")
4905
+ className: "mc-cat-bar",
4906
+ "data-v3-catalog-bar": "true",
4907
+ children: [
4908
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4909
+ className: "mc-cat-tabs",
4910
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
4911
+ className: "mc-main-tabs",
4912
+ items: [{
4913
+ value: "tools",
4914
+ id: "mc-tab-tools",
4915
+ panelId: "mc-panel-tools",
4916
+ label: t("toolsGroup")
4917
+ }, {
4918
+ value: "skills",
4919
+ id: "mc-tab-skills",
4920
+ panelId: "mc-panel-skills",
4921
+ label: t("skillsGroup")
4922
+ }],
4923
+ value: activeTab,
4924
+ onChange: onTabChange,
4925
+ label: t("title")
4911
4926
  })
4912
- ]
4913
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4914
- className: "mc-heading-actions",
4915
- children: [
4916
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4917
- size: "sm",
4918
- variant: "outline",
4919
- onClick: () => setRegisterOpen(true),
4920
- children: t("registerCapability")
4921
- }),
4922
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4923
- size: "sm",
4924
- variant: "outline",
4925
- onClick: () => void openCatalogDocs(),
4926
- children: t("viewCatalog")
4927
- }),
4928
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
4929
- className: "mc-feedback-link",
4930
- href: "https://github.com/PKUfudawei/dsh-capability-menu",
4931
- target: "_blank",
4932
- rel: "noopener noreferrer",
4933
- "aria-label": t("feedback"),
4934
- children: t("feedback")
4927
+ }),
4928
+ snapshot.rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4929
+ className: "mc-cat-search",
4930
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
4931
+ type: "search",
4932
+ value: filter,
4933
+ placeholder: t("filterByName"),
4934
+ "aria-label": t("filterByName"),
4935
+ title: t("filterHint"),
4936
+ onChange: (e) => setFilter(e.target.value)
4935
4937
  })
4936
- ]
4937
- })]
4938
+ }),
4939
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4940
+ className: "mc-cat-actions",
4941
+ children: [
4942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4943
+ size: "sm",
4944
+ variant: "outline",
4945
+ onClick: () => setRegisterOpen(true),
4946
+ children: t("registerCapability")
4947
+ }),
4948
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4949
+ size: "sm",
4950
+ variant: "outline",
4951
+ onClick: () => void openCatalogDocs(),
4952
+ children: t("viewCatalog")
4953
+ }),
4954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4955
+ type: "button",
4956
+ size: "sm",
4957
+ variant: "outline",
4958
+ onClick: () => window.open("https://github.com/PKUfudawei/dsh-capability-menu", "_blank", "noopener,noreferrer"),
4959
+ children: t("feedback")
4960
+ }),
4961
+ updateStatus?.updateAvailable && (updateStatus.updatable || updateStatus.reason === "local") && updatePhase !== "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4962
+ type: "button",
4963
+ variant: "primary",
4964
+ size: "sm",
4965
+ className: "mc-update-check",
4966
+ title: t("updateAvailable", { version: updateStatus.latest }),
4967
+ disabled: updatePhase === "installing",
4968
+ onClick: () => setUpdateDialogOpen(true),
4969
+ children: t("newVersion")
4970
+ })
4971
+ ]
4972
+ })
4973
+ ]
4938
4974
  }),
4939
4975
  (updatePhase === "done" || updateStatus?.updateAvailable && !updateStatus.updatable && updateStatus.reason !== "local") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4940
4976
  className: "mc-update-status",
4941
4977
  role: "status",
4942
4978
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: updatePhase === "done" ? t(updateStatus?.reason === "local" ? "pluginSwitched" : "pluginUpdated") : t("updateAvailable", { version: updateStatus?.latest ?? "" }) }), updatePhase !== "done" && updateStatus?.updateAvailable && !updateStatus.updatable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(updateStatus.reason === "non-registry" ? "pluginNonRegistrySource" : "pluginProfileUnavailable") })]
4943
4979
  }),
4944
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4945
- className: "mc-desc",
4946
- children: t("desc")
4947
- }),
4948
- diagnostics !== null && skillDiagnosticCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
4949
- className: "mc-diagnostics",
4950
- children: [
4951
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("diagnosticTitle", { count: skillDiagnosticCount }) }),
4952
- diagnostics.unavailableSkills.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticUnavailable", {
4953
- name: item.name,
4954
- source: item.source,
4955
- preset: item.preset ?? ""
4956
- }) }, `unavailable-${item.name}-${item.preset ?? ""}-${index}`)),
4957
- diagnostics.shadowedSkills.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticShadowed", {
4958
- name: item.name,
4959
- winner: item.selectedFrom,
4960
- hidden: item.shadowedFrom
4961
- }) }, `shadowed-${item.name}-${item.shadowedFrom}-${index}`)),
4962
- diagnostics.brokenPresets.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticBroken", {
4963
- id: item.id,
4964
- reason: item.reason
4965
- }) }, `broken-${item.id}`)),
4966
- diagnostics.unbrowseableSkills.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticUnbrowseable", {
4967
- name: item.name,
4968
- reason: item.reason
4969
- }) }, `browse-${item.name}`)),
4970
- diagnostics.undocumentedSkills.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticUndocumented", {
4971
- name: item.name,
4972
- summary: item.summary
4973
- }) }, `undocumented-${item.name}`))
4974
- ]
4975
- }),
4976
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4977
- className: "mc-tabs",
4978
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
4979
- className: "mc-main-tabs",
4980
- items: [{
4981
- value: "tools",
4982
- id: "mc-tab-tools",
4983
- panelId: "mc-panel-tools",
4984
- label: t("toolsGroup")
4985
- }, {
4986
- value: "skills",
4987
- id: "mc-tab-skills",
4988
- panelId: "mc-panel-skills",
4989
- label: t("skillsGroup")
4990
- }],
4991
- value: activeTab,
4992
- onChange: onTabChange,
4993
- label: t("title")
4994
- })
4995
- }),
4996
4980
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogStatStrip, { cells: statCells }),
4997
- snapshot.rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4998
- className: "mc-filter",
4999
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
5000
- type: "search",
5001
- value: filter,
5002
- placeholder: t("filterByName"),
5003
- "aria-label": t("filterByName"),
5004
- title: t("filterHint"),
5005
- onChange: (e) => setFilter(e.target.value)
5006
- })
4981
+ diagnostics !== null && skillDiagnosticCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
4982
+ className: "mc-cat-diag",
4983
+ "data-v3-catalog-diagnostics": "true",
4984
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("diagnosticTitle", { count: skillDiagnosticCount }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4985
+ className: "mc-cat-diag-body",
4986
+ children: [
4987
+ diagnostics.unavailableSkills.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticUnavailable", {
4988
+ name: item.name,
4989
+ source: item.source,
4990
+ preset: item.preset ?? ""
4991
+ }) }, `unavailable-${item.name}-${item.preset ?? ""}-${index}`)),
4992
+ diagnostics.shadowedSkills.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticShadowed", {
4993
+ name: item.name,
4994
+ winner: item.selectedFrom,
4995
+ hidden: item.shadowedFrom
4996
+ }) }, `shadowed-${item.name}-${item.shadowedFrom}-${index}`)),
4997
+ diagnostics.brokenPresets.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticBroken", {
4998
+ id: item.id,
4999
+ reason: item.reason
5000
+ }) }, `broken-${item.id}`)),
5001
+ diagnostics.unbrowseableSkills.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticUnbrowseable", {
5002
+ name: item.name,
5003
+ reason: item.reason
5004
+ }) }, `browse-${item.name}`)),
5005
+ diagnostics.undocumentedSkills.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("diagnosticUndocumented", {
5006
+ name: item.name,
5007
+ summary: item.summary
5008
+ }) }, `undocumented-${item.name}`))
5009
+ ]
5010
+ })]
5007
5011
  }),
5008
5012
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5009
5013
  role: "tabpanel",
@@ -6513,11 +6517,7 @@ const COVERAGE_LABELS = Object.freeze({
6513
6517
  partial: "部分",
6514
6518
  unknown: "未知"
6515
6519
  });
6516
- /**
6517
- * Zero-count placeholders. Required for `plugin-tool` / `skill`; the other
6518
- * kinds get a neutral line so no zero is ever presented as "未使用".
6519
- */
6520
- const CALL_INDEX_ZERO_SOURCE_NOTES = Object.freeze({
6520
+ Object.freeze({
6521
6521
  mcp: "本次索引未解析到 MCP 来源的调用;覆盖不完整时不能据此判定未使用。",
6522
6522
  native: "本次索引未解析到原生工具调用;覆盖不完整时不能据此判定未使用。",
6523
6523
  "plugin-tool": "本次索引未解析出插件工具归属(插件 owner 不可证明),0 在这里不等于未使用。",
@@ -6556,10 +6556,6 @@ function formatCallIndexCount(value, totalExact) {
6556
6556
  const text = Math.trunc(value).toLocaleString("en-US");
6557
6557
  return totalExact === true ? text : `≥${text}`;
6558
6558
  }
6559
- /** Human label for the time filter. */
6560
- function callIndexRangeLabel(range) {
6561
- return range === "all" ? "全部" : `${range} 天`;
6562
- }
6563
6559
  /**
6564
6560
  * Local-day window for `range`, expressed as the wire's half-open
6565
6561
  * `[fromMs, toMs)`. `'all'` carries no lower bound.
@@ -6804,24 +6800,6 @@ function callIndexOutcomeMix(summary) {
6804
6800
  total: summary.total
6805
6801
  };
6806
6802
  }
6807
- /**
6808
- * `bySource` for display. Always returns all five kinds in a fixed order, so
6809
- * the distribution never reshuffles between reads; a zero carries its
6810
- * placeholder instead of disappearing.
6811
- */
6812
- function callIndexSourceData(bySource) {
6813
- const total = bySource === void 0 ? 0 : SOURCE_DISPLAY_ORDER.reduce((sum, kind) => sum + bySource[kind], 0);
6814
- return SOURCE_DISPLAY_ORDER.map((kind) => {
6815
- const count = bySource?.[kind] ?? 0;
6816
- return {
6817
- kind,
6818
- label: CALL_INDEX_SOURCE_LABELS[kind],
6819
- count,
6820
- share: total > 0 ? count / total : 0,
6821
- ...count === 0 ? { zeroNote: CALL_INDEX_ZERO_SOURCE_NOTES[kind] } : {}
6822
- };
6823
- });
6824
- }
6825
6803
  /** Fixed chart box; the SVG scales with CSS but the geometry is deterministic. */
6826
6804
  const CALL_INDEX_TREND_GEOMETRY = Object.freeze({
6827
6805
  width: 640,
@@ -7002,18 +6980,13 @@ function callIndexOverviewMetrics(overview, status) {
7002
6980
  ...status?.task === void 0 ? {} : { tasks: status.task }
7003
6981
  };
7004
6982
  }
7005
- /** True when `plugin-tool` and `skill` both resolved to zero. */
7006
- function callIndexHasUnexplainedSources(aggregates) {
7007
- if (aggregates === void 0) return false;
7008
- return aggregates.bySource["plugin-tool"] === 0 && aggregates.bySource.skill === 0;
7009
- }
7010
6983
  /** Short Session id for a dense table cell; the full id stays in `title`. */
7011
6984
  function shortCallIndexSessionId(sessionId) {
7012
6985
  return sessionId.length <= 12 ? sessionId : sessionId.slice(0, 12);
7013
6986
  }
7014
6987
  /** Inline stylesheet; a `<style>` element keeps this file dependency-free. */
7015
6988
  function CallIndexStyles() {
7016
- return (0, react.createElement)("style", { "data-call-index-style": "true" }, ".mc-ci-filter{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0}.mc-ci-filter label{display:flex;gap:4px;align-items:center;font-size:12px}.mc-ci-table-scroll{width:100%;overflow:auto;max-height:min(60vh,560px);overscroll-behavior:contain}.mc-ci-table{width:100%;border-collapse:collapse;font-size:12px}.mc-ci-table thead th{position:sticky;top:0;z-index:1;background:var(--mc-surface,Canvas);box-shadow:inset 0 -1px 0 color-mix(in srgb,currentColor 12%,transparent)}.mc-ci-progress{display:flex;flex-direction:column;gap:4px;margin:8px 0}.mc-ci-progress-track{height:6px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,currentColor 12%,transparent)}.mc-ci-progress-fill{height:100%;border-radius:999px;background:color-mix(in srgb,currentColor 55%,transparent);transition:width .3s ease}.mc-ci-progress-fill.is-indeterminate{animation:mc-ci-indeterminate 1.1s ease-in-out infinite}@keyframes mc-ci-indeterminate{0%{transform:translateX(-60%)}100%{transform:translateX(340%)}}@media (prefers-reduced-motion:reduce){.mc-ci-progress-fill{transition:none}.mc-ci-progress-fill.is-indeterminate{animation:none;width:100%!important;opacity:.4}}.mc-ci-session-picker{display:inline-flex;align-items:center;gap:6px;font-size:12px}.mc-ci-session-picker-label{opacity:.7}.mc-ci-session-select{max-width:280px;padding:3px 6px;border-radius:6px;font-size:12px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);background:transparent;color:inherit}.mc-ci-session-select:disabled{opacity:.5}.mc-ci-progress-meta{display:flex;gap:8px;align-items:baseline;font-size:12px}.mc-ci-progress-value{font-variant-numeric:tabular-nums;font-weight:600}.mc-ci-progress-detail{opacity:.72}.mc-ci-table th,.mc-ci-table td{padding:4px 6px;text-align:left;border-bottom:1px solid color-mix(in srgb,currentColor 12%,transparent);white-space:nowrap}.mc-ci-table tbody tr{cursor:pointer}.mc-ci-table tbody tr[aria-selected=\"true\"]{background:color-mix(in srgb,currentColor 8%,transparent)}.mc-ci-table td:nth-child(2),.mc-ci-table td:nth-child(4){max-width:220px;overflow:hidden;text-overflow:ellipsis}.mc-ci-table th:nth-child(2),.mc-ci-table th:nth-child(4){max-width:220px}.mc-ci-num{font-variant-numeric:tabular-nums}.mc-ci-bar{display:flex;height:8px;gap:1px;margin:4px 0}.mc-ci-bar > span{display:block;height:100%;min-width:2px}.mc-ci-aside{border-left:1px solid color-mix(in srgb,currentColor 15%,transparent);padding-left:12px;min-width:240px;font-size:12px}.mc-ci-metric{display:inline-flex;flex-direction:column;margin-right:18px;min-width:110px;vertical-align:top}.mc-ci-metric-label{font-size:11px;opacity:.7}.mc-ci-metric-value{font-size:18px;font-variant-numeric:tabular-nums}.mc-ci-metric-hint{font-size:11px;opacity:.75}.mc-ci-trend svg{width:100%;max-width:640px;height:auto}.v3-status-pills{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:12px}.v3-statusline.v3-status-dash{border-style:dashed;color:var(--v3-t4)}.v3-filtercard{padding:11px 14px 13px}.v3-filter-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:9px}.v3-search{display:flex;align-items:center;gap:6px;height:26px;padding:0 8px;border:1px solid var(--v3-line);border-radius:var(--v3-r-sm);background:var(--v3-panel-2);min-width:150px;flex:1 1 150px}.v3-search input{border:0;background:transparent;color:var(--v3-t1);font:inherit;font-size:12px;width:100%;outline:0;min-width:0}.v3-search input::placeholder{color:var(--v3-t4)}.v3-kv{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}.v3-kv-l{font-size:11px;color:var(--v3-t4)}.v3-kv-v{font-size:12px;color:var(--v3-t2);margin-top:2px;overflow-wrap:anywhere}.v3-ts.v3-ts--h330{max-height:330px}.v3-src i[data-zero=\"true\"]{background:var(--v3-panel-2)}");
6989
+ return (0, react.createElement)("style", { "data-call-index-style": "true" }, ".mc-ci-filter{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0}.mc-ci-filter label{display:flex;gap:4px;align-items:center;font-size:12px}.mc-ci-table-scroll{width:100%;overflow:auto;max-height:min(60vh,560px);overscroll-behavior:contain}.mc-ci-table{width:100%;border-collapse:collapse;font-size:12px}.mc-ci-table thead th{position:sticky;top:0;z-index:1;background:var(--mc-surface,Canvas);box-shadow:inset 0 -1px 0 color-mix(in srgb,currentColor 12%,transparent)}.mc-ci-progress{display:flex;flex-direction:column;gap:4px;margin:8px 0}.mc-ci-progress-track{height:6px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,currentColor 12%,transparent)}.mc-ci-progress-fill{height:100%;border-radius:999px;background:color-mix(in srgb,currentColor 55%,transparent);transition:width .3s ease}.mc-ci-progress-fill.is-indeterminate{animation:mc-ci-indeterminate 1.1s ease-in-out infinite}@keyframes mc-ci-indeterminate{0%{transform:translateX(-60%)}100%{transform:translateX(340%)}}@media (prefers-reduced-motion:reduce){.mc-ci-progress-fill{transition:none}.mc-ci-progress-fill.is-indeterminate{animation:none;width:100%!important;opacity:.4}}.mc-ci-session-picker{display:inline-flex;align-items:center;gap:6px;font-size:12px}.mc-ci-session-picker-label{opacity:.7}.mc-ci-session-select{max-width:280px;padding:3px 6px;border-radius:6px;font-size:12px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);background:transparent;color:inherit}.mc-ci-session-select:disabled{opacity:.5}.mc-ci-progress-meta{display:flex;gap:8px;align-items:baseline;font-size:12px}.mc-ci-progress-value{font-variant-numeric:tabular-nums;font-weight:600}.mc-ci-progress-detail{opacity:.72}.mc-ci-table th,.mc-ci-table td{padding:4px 6px;text-align:left;border-bottom:1px solid color-mix(in srgb,currentColor 12%,transparent);white-space:nowrap}.mc-ci-table tbody tr{cursor:pointer}.mc-ci-table tbody tr[aria-selected=\"true\"]{background:color-mix(in srgb,currentColor 8%,transparent)}.mc-ci-table td:nth-child(2),.mc-ci-table td:nth-child(4){max-width:220px;overflow:hidden;text-overflow:ellipsis}.mc-ci-table th:nth-child(2),.mc-ci-table th:nth-child(4){max-width:220px}.mc-ci-num{font-variant-numeric:tabular-nums}.mc-ci-bar{display:flex;height:8px;gap:1px;margin:4px 0}.mc-ci-bar > span{display:block;height:100%;min-width:2px}.mc-ci-aside{border-left:1px solid color-mix(in srgb,currentColor 15%,transparent);padding-left:12px;min-width:240px;font-size:12px}.mc-ci-metric{display:inline-flex;flex-direction:column;margin-right:18px;min-width:110px;vertical-align:top}.mc-ci-metric-label{font-size:11px;opacity:.7}.mc-ci-metric-value{font-size:18px;font-variant-numeric:tabular-nums}.mc-ci-metric-hint{font-size:11px;opacity:.75}.mc-ci-trend svg{width:100%;max-width:640px;height:auto}.v3-status-pills{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:12px}.v3-statusline.v3-status-dash{border-style:dashed;color:var(--v3-t4)}.v3-filtercard{padding:11px 14px 13px}.v3-filter-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:9px}.v3-search{display:flex;align-items:center;gap:6px;height:26px;padding:0 8px;border:1px solid var(--v3-line);border-radius:var(--v3-r-sm);background:var(--v3-panel-2);min-width:150px;flex:1 1 150px}.v3-search input{border:0;background:transparent;color:var(--v3-t1);font:inherit;font-size:12px;width:100%;outline:0;min-width:0}.v3-search input::placeholder{color:var(--v3-t4)}.v3-kv{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}.v3-kv-l{font-size:11px;color:var(--v3-t4)}.v3-kv-v{font-size:12px;color:var(--v3-t2);margin-top:2px;overflow-wrap:anywhere}.v3-ts.v3-ts--h330{max-height:330px}.v3-chartview{overflow-x:auto;cursor:grab;overscroll-behavior-x:contain;scrollbar-width:thin;user-select:none;-webkit-user-select:none}.v3-chartview:active{cursor:grabbing}.v3-chartview svg{display:block}.v3-src i[data-zero=\"true\"]{background:var(--v3-panel-2)}");
7017
6990
  }
7018
6991
  /** One v3 `.v3-stat` cell: label, big value, optional hint line, optional sparkline. */
7019
6992
  function V3Stat(props) {
@@ -7064,21 +7037,32 @@ function toolDisplayLabel(toolName) {
7064
7037
  }
7065
7038
  return toolName;
7066
7039
  }
7040
+ /** Pixel width of the trend for `days` buckets: days x min column, floored. */
7041
+ function callIndexTrendWidth(days) {
7042
+ return Math.max(320, (Number.isFinite(days) && days > 0 ? Math.floor(days) : 1) * 34);
7043
+ }
7067
7044
  /**
7068
- * The v3 overview trend: a 720×212 line + area (calls), failure底柱 (error), a
7069
- * dashed "recent week" band and a peak label. Drawn from `callIndexTrend`'s bars
7070
- * so its numbers are the same ones the caption quotes — no second data path.
7045
+ * The v3 overview trend: a line + area (calls), failure底柱 (error), a dashed
7046
+ * "recent week" band and a peak label, drawn inside a horizontally scrollable
7047
+ * strip so a long series can be panned instead of squeezed.
7048
+ *
7049
+ * Geometry comes from `callIndexTrend`'s bars (one data path). The SVG is
7050
+ * days x min-column wide (never narrower than the card), so day labels stay
7051
+ * legible; the wrapper scrolls and supports pointer drag-to-pan. The last day
7052
+ * label is right-anchored at the padding edge so it cannot overlap the one
7053
+ * before it.
7071
7054
  */
7072
7055
  function OverviewTrendSvg(props) {
7073
- const { chart } = props, W = 720, H = 212, padL = 34, padT = 12;
7074
- const plotW = 676;
7075
- const plotH = 174;
7056
+ const { chart } = props;
7076
7057
  const bars = chart.bars;
7077
7058
  const n = bars.length;
7059
+ const W = callIndexTrendWidth(n), H = 212, padL = 34, padR = 22, padT = 12;
7060
+ const plotW = W - padL - padR;
7061
+ const plotH = 174;
7078
7062
  const maxV = chart.maxCalls > 0 ? Math.ceil(chart.maxCalls / 10) * 10 : 1;
7079
7063
  const xAt = (i) => padL + (n <= 1 ? plotW / 2 : i * plotW / (n - 1));
7080
7064
  const yAt = (v) => 186 - v / maxV * plotH;
7081
- const points = bars.map((bar, i) => `${xAt(i).toFixed(1)},${yAt(bar.calls).toFixed(1)}`);
7065
+ const points = bars.map((bar, i) => xAt(i).toFixed(1) + "," + yAt(bar.calls).toFixed(1));
7082
7066
  let peak = 0;
7083
7067
  for (let i = 1; i < n; i++) if (bars[i].calls > bars[peak].calls) peak = i;
7084
7068
  const children = [];
@@ -7086,7 +7070,7 @@ function OverviewTrendSvg(props) {
7086
7070
  const start = Math.max(0, n - 7);
7087
7071
  const slot = n > 1 ? plotW / (n - 1) : plotW;
7088
7072
  const left = Math.max(padL, xAt(start) - slot / 2);
7089
- const right = Math.min(710, xAt(n - 1) + slot / 2);
7073
+ const right = Math.min(W - padR, xAt(n - 1) + slot / 2);
7090
7074
  children.push((0, react.createElement)("rect", {
7091
7075
  key: "hl",
7092
7076
  className: "v3-hlbox",
@@ -7101,15 +7085,15 @@ function OverviewTrendSvg(props) {
7101
7085
  const value = maxV * g / 4;
7102
7086
  const y = yAt(value);
7103
7087
  children.push((0, react.createElement)("line", {
7104
- key: `grid-${g}`,
7088
+ key: "grid-" + g,
7105
7089
  className: "v3-grid",
7106
7090
  x1: padL,
7107
7091
  y1: y.toFixed(1),
7108
- x2: 710,
7092
+ x2: W - padR,
7109
7093
  y2: y.toFixed(1)
7110
7094
  }));
7111
7095
  children.push((0, react.createElement)("text", {
7112
- key: `tick-${g}`,
7096
+ key: "tick-" + g,
7113
7097
  className: "v3-axis",
7114
7098
  x: 27,
7115
7099
  y: (y + 3.5).toFixed(1),
@@ -7121,19 +7105,19 @@ function OverviewTrendSvg(props) {
7121
7105
  if (bar.error <= 0) return;
7122
7106
  const errH = Math.max(2.5, bar.error / maxV * plotH);
7123
7107
  children.push((0, react.createElement)("rect", {
7124
- key: `err-${bar.day}`,
7108
+ key: "err-" + bar.day,
7125
7109
  className: "v3-bar-er",
7126
7110
  x: (xAt(i) - barWidth / 2).toFixed(1),
7127
7111
  y: (186 - errH).toFixed(1),
7128
7112
  width: barWidth.toFixed(1),
7129
7113
  height: errH.toFixed(1),
7130
7114
  rx: 1.5
7131
- }, (0, react.createElement)("title", null, `${bar.day}:失败 ${bar.error} 次`)));
7115
+ }, (0, react.createElement)("title", null, bar.day + ":失败 " + bar.error + " 次")));
7132
7116
  });
7133
7117
  children.push((0, react.createElement)("polygon", {
7134
7118
  key: "area",
7135
7119
  className: "v3-area",
7136
- points: `${padL},186 ${points.join(" ")} 710,186`
7120
+ points: "34,186 " + points.join(" ") + " " + (W - padR) + ",186"
7137
7121
  }));
7138
7122
  children.push((0, react.createElement)("polyline", {
7139
7123
  key: "line",
@@ -7143,12 +7127,12 @@ function OverviewTrendSvg(props) {
7143
7127
  bars.forEach((bar, i) => {
7144
7128
  if (i % 4 !== 0 && i !== n - 1 && i !== peak) return;
7145
7129
  children.push((0, react.createElement)("circle", {
7146
- key: `pt-${bar.day}`,
7130
+ key: "pt-" + bar.day,
7147
7131
  className: "v3-pt",
7148
7132
  cx: xAt(i).toFixed(1),
7149
7133
  cy: yAt(bar.calls).toFixed(1),
7150
7134
  r: 2.6
7151
- }, (0, react.createElement)("title", null, `${bar.day}:${bar.calls} 次调用,失败 ${bar.error} 次`)));
7135
+ }, (0, react.createElement)("title", null, bar.day + ":" + bar.calls + " 次调用,失败 " + bar.error + " 次")));
7152
7136
  });
7153
7137
  if (n > 0) {
7154
7138
  children.push((0, react.createElement)("text", {
@@ -7157,9 +7141,10 @@ function OverviewTrendSvg(props) {
7157
7141
  x: (xAt(peak) - 8).toFixed(1),
7158
7142
  y: (yAt(bars[peak].calls) - 9).toFixed(1),
7159
7143
  "text-anchor": "end"
7160
- }, `峰值 ${bars[peak].calls}`));
7161
- for (let m = 0; m < n; m += 5) children.push((0, react.createElement)("text", {
7162
- key: `xl-${m}`,
7144
+ }, "峰值 " + bars[peak].calls));
7145
+ const step = Math.max(1, Math.ceil(n / 6));
7146
+ for (let m = 0; m < n - 1; m += step) children.push((0, react.createElement)("text", {
7147
+ key: "xl-" + m,
7163
7148
  className: "v3-axis",
7164
7149
  x: xAt(m).toFixed(1),
7165
7150
  y: 204,
@@ -7168,35 +7153,106 @@ function OverviewTrendSvg(props) {
7168
7153
  children.push((0, react.createElement)("text", {
7169
7154
  key: "xl-last",
7170
7155
  className: "v3-axis",
7171
- x: 710,
7156
+ x: (W - padR).toFixed(1),
7172
7157
  y: 204,
7173
- "text-anchor": "end"
7158
+ "text-anchor": "end",
7159
+ "data-call-index-lasttick": "true"
7174
7160
  }, bars[n - 1].day.slice(5)));
7175
7161
  }
7176
- return (0, react.createElement)("svg", {
7162
+ const svg = (0, react.createElement)("svg", {
7177
7163
  className: "v3-chart",
7178
- viewBox: `0 0 ${W} ${H}`,
7164
+ viewBox: "0 0 " + W + " 212",
7179
7165
  preserveAspectRatio: "none",
7180
7166
  role: "img",
7181
- "aria-label": `每日调用与失败数:${n} 天,峰值 ${chart.maxCalls} 次`,
7182
- "data-call-index-svg": "trend"
7167
+ width: W,
7168
+ height: H,
7169
+ style: {
7170
+ width: W + "px",
7171
+ minWidth: "100%",
7172
+ height: "212px"
7173
+ },
7174
+ "aria-label": "每日调用与失败数:" + n + " 天,峰值 " + chart.maxCalls + " 次",
7175
+ "data-call-index-svg": "trend",
7176
+ "data-call-index-width": String(W)
7183
7177
  }, ...children);
7178
+ let dragging = false;
7179
+ let startX = 0;
7180
+ let startScroll = 0;
7181
+ return (0, react.createElement)("div", {
7182
+ className: "v3-chartview",
7183
+ "data-call-index-chartview": "true",
7184
+ onPointerDown: (event) => {
7185
+ dragging = true;
7186
+ startX = event.clientX;
7187
+ startScroll = event.currentTarget.scrollLeft;
7188
+ event.currentTarget.style.cursor = "grabbing";
7189
+ if (event.pointerId !== void 0 && typeof event.currentTarget.setPointerCapture === "function") try {
7190
+ event.currentTarget.setPointerCapture(event.pointerId);
7191
+ } catch {}
7192
+ },
7193
+ onPointerMove: (event) => {
7194
+ if (!dragging) return;
7195
+ event.currentTarget.scrollLeft = startScroll - (event.clientX - startX);
7196
+ },
7197
+ onPointerUp: (event) => {
7198
+ dragging = false;
7199
+ event.currentTarget.style.cursor = "grab";
7200
+ },
7201
+ onPointerLeave: (event) => {
7202
+ if (!dragging) return;
7203
+ dragging = false;
7204
+ event.currentTarget.style.cursor = "grab";
7205
+ }
7206
+ }, svg);
7184
7207
  }
7185
- /** 来源构成: a one-segment donut (未解析) + five-source list + an honesty note. */
7208
+ /**
7209
+ * 来源构成,按工具名前缀启发式分类。
7210
+ *
7211
+ * NOT Host attribution: the Host cannot prove a plugin owner, so this card does
7212
+ * not pretend to. It splits the already-read per-tool rows by a plain rule —
7213
+ * a `mcp__` prefix is MCP, everything else is native — and says so at the foot.
7214
+ * When the per-tool read has not happened there is nothing to classify, so the
7215
+ * card shows 「来源未解析」 and explains that this is not a zero count.
7216
+ */
7186
7217
  function OverviewSourcePanel(props) {
7187
- if (props.bySource === void 0) return (0, react.createElement)("div", { className: "v3-panel" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "来源构成")), (0, react.createElement)("p", {
7218
+ const note = (0, react.createElement)("p", { className: "v3-src-note" }, (0, react.createElement)("span", null, "按工具名前缀推断,不是 Host 归属。"), (0, react.createElement)("b", { style: { color: "var(--v3-t3)" } }, " 0 在这里不等于未使用"));
7219
+ if (props.tools === void 0) return (0, react.createElement)("div", { className: "v3-panel" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "来源构成")), (0, react.createElement)("p", {
7188
7220
  className: "v3-hint",
7189
7221
  "data-call-index-missing": "by-source"
7190
- }, "Host 未返回来源分布(bySource);这不是「全部为 0」。"));
7191
- const data = callIndexSourceData(props.bySource);
7192
- const total = data.reduce((sum, item) => sum + item.count, 0);
7193
- const unknownCount = data.find((item) => item.kind === "unknown")?.count ?? 0;
7194
- const share = total > 0 ? unknownCount / total : 0;
7195
- const percent = Math.round(share * 100);
7222
+ }, "尚未读取逐工具明细,来源未解析;这不是「全部为 0」。"), note);
7223
+ const mcpCalls = props.tools.filter((row) => row.toolName.startsWith("mcp__")).reduce((sum, row) => sum + row.calls, 0);
7224
+ const nativeCalls = props.tools.reduce((sum, row) => sum + row.calls, 0) - mcpCalls;
7225
+ const total = mcpCalls + nativeCalls;
7226
+ const data = [{
7227
+ kind: "mcp",
7228
+ label: CALL_INDEX_SOURCE_LABELS.mcp,
7229
+ count: mcpCalls,
7230
+ color: "var(--v3-info)"
7231
+ }, {
7232
+ kind: "native",
7233
+ label: CALL_INDEX_SOURCE_LABELS.native,
7234
+ count: nativeCalls,
7235
+ color: "var(--v3-ok)"
7236
+ }];
7196
7237
  const radius = 43;
7197
7238
  const circumference = 2 * Math.PI * radius;
7198
- const segment = share * circumference;
7199
- const allUnresolved = total > 0 && unknownCount === total;
7239
+ let offset = 0;
7240
+ const segments = data.filter((item) => item.count > 0).map((item) => {
7241
+ const length = total > 0 ? item.count / total * circumference : 0;
7242
+ const circle = (0, react.createElement)("circle", {
7243
+ key: item.kind,
7244
+ className: "v3-seg",
7245
+ cx: 52,
7246
+ cy: 52,
7247
+ r: radius,
7248
+ "data-seg": item.kind,
7249
+ strokeDasharray: `${length.toFixed(1)} ${(circumference - length).toFixed(1)}`,
7250
+ strokeDashoffset: (-offset).toFixed(1),
7251
+ style: { stroke: item.color }
7252
+ });
7253
+ offset += length;
7254
+ return circle;
7255
+ });
7200
7256
  return (0, react.createElement)("div", { className: "v3-panel" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "来源构成"), (0, react.createElement)("span", { className: "v3-pn" }, `${formatCallIndexCount(total, props.totalExact)} 次`)), (0, react.createElement)("div", { className: "v3-donut-wrap" }, (0, react.createElement)("div", { className: "v3-donut" }, (0, react.createElement)("svg", {
7201
7257
  viewBox: "0 0 104 104",
7202
7258
  "aria-hidden": "true"
@@ -7205,19 +7261,12 @@ function OverviewSourcePanel(props) {
7205
7261
  cx: 52,
7206
7262
  cy: 52,
7207
7263
  r: radius
7208
- }), (0, react.createElement)("circle", {
7209
- className: "v3-seg",
7210
- cx: 52,
7211
- cy: 52,
7212
- r: radius,
7213
- "data-seg": "unknown",
7214
- strokeDasharray: `${segment.toFixed(1)} ${(circumference - segment).toFixed(1)}`,
7215
- strokeDashoffset: 0
7216
- })), (0, react.createElement)("div", { className: "v3-mid" }, (0, react.createElement)("b", { className: "v3-num" }, `${percent}%`), (0, react.createElement)("span", null, "未解析"))), (0, react.createElement)("div", { className: "v3-src-list" }, ...data.map((item) => (0, react.createElement)("div", {
7264
+ }), ...segments), (0, react.createElement)("div", { className: "v3-mid" }, (0, react.createElement)("b", { className: "v3-num" }, formatCallIndexCount(total, props.totalExact)), (0, react.createElement)("span", null, "次调用"))), (0, react.createElement)("div", { className: "v3-src-list" }, ...data.map((item) => (0, react.createElement)("div", {
7217
7265
  key: item.kind,
7218
7266
  className: "v3-src",
7219
- "data-source-row": item.kind
7220
- }, (0, react.createElement)("i", item.count === 0 ? { "data-zero": "true" } : {}), (0, react.createElement)("span", { className: "v3-nm" }, item.label), (0, react.createElement)("b", null, formatCallIndexCount(item.count, props.totalExact)), (0, react.createElement)("em", null, total > 0 ? `${Math.round(item.share * 100)}%` : "0%"))))), (0, react.createElement)("p", { className: "v3-src-note" }, allUnresolved ? `Host 无法证明插件 owner,非 mcp__ 前缀的调用也不带归属证据,所以 ${formatCallIndexCount(total, props.totalExact)} 次调用全部落在「来源未解析」——这是当前唯一真实的分布形态,不是抽样。` : "Host 无法证明插件 owner,非 mcp__ 前缀的调用也不带归属证据;未解析出归属的调用单列为「来源未解析」,其余按 Host 能证明的 kind 归类。", (0, react.createElement)("b", { style: { color: "var(--v3-t3)" } }, " 0 在这里不等于未使用")));
7267
+ "data-source-row": item.kind,
7268
+ "data-source-count": String(item.count)
7269
+ }, (0, react.createElement)("i", item.count === 0 ? { "data-zero": "true" } : {}), (0, react.createElement)("span", { className: "v3-nm" }, item.label), (0, react.createElement)("b", null, formatCallIndexCount(item.count, props.totalExact)), (0, react.createElement)("em", null, total > 0 ? `${Math.round(item.count / total * 100)}%` : "0%"))))), note);
7221
7270
  }
7222
7271
  function callIndexDiagnostics(input) {
7223
7272
  const items = [];
@@ -7552,9 +7601,8 @@ function CallIndexDetailPanelView(props) {
7552
7601
  ...failure === void 0 ? {} : { failure }
7553
7602
  });
7554
7603
  const totalText = page === void 0 ? void 0 : formatCallIndexCount(page.total ?? page.summary.total, page.totalExact);
7555
- const coverageLabel = page === void 0 ? void 0 : COVERAGE_LABELS[page.coverage];
7556
7604
  const scanning = status !== void 0 && (status.phase === "running" || (status.sessions?.pending ?? 0) > 0 || (status.sessions?.pendingLive ?? 0) > 0);
7557
- const statusText = page === void 0 ? loading === true ? "正在读取调用明细…" : "尚未读取调用明细" : `快照代 ${page.generation} · 覆盖 ${coverageLabel ?? "未知"} · 按工具聚合`;
7605
+ const statusText = page === void 0 ? loading === true ? "正在读取调用明细…" : "尚未读取调用明细" : scanning ? "索引仍在扫描,数字会继续增加" : "索引已完成";
7558
7606
  const rates = callIndexRates(page?.summary);
7559
7607
  const successStats = rates.success;
7560
7608
  const failureStats = rates.failure;
@@ -7621,7 +7669,7 @@ function CallIndexDetailPanelView(props) {
7621
7669
  className: "v3-dot",
7622
7670
  "data-tone": state.kind === "failure" || state.kind === "disconnected" ? "bad" : scanning ? "run" : "ok",
7623
7671
  "aria-hidden": "true"
7624
- }), statusText), (0, react.createElement)("span", { className: "v3-statusline v3-status-dash" }, `${rows.length} 个工具 · ${formatCallIndexCount(readCalls, page?.totalExact)} 次调用${readIsPartial ? `(全部 ${formatCallIndexCount(totalCalls, page?.totalExact)} 次)` : ""}`)), (0, react.createElement)("div", { className: "v3-panel v3-filtercard" }, (0, react.createElement)("div", { className: "v3-filter-row" }, CallIndexFilters({
7672
+ }), (0, react.createElement)("span", null, statusText))), (0, react.createElement)("div", { className: "v3-panel v3-filtercard" }, (0, react.createElement)("div", { className: "v3-filter-row" }, CallIndexFilters({
7625
7673
  query,
7626
7674
  ...onQueryChange === void 0 ? {} : { onChange: onQueryChange },
7627
7675
  ...onResolveScope === void 0 ? {} : { onResolveScope },
@@ -7710,7 +7758,7 @@ function CallIndexDetailPanelView(props) {
7710
7758
  }, `条长 = 失败次数(不是失败率)。共 ${failingRows.length} 个工具出现过失败。`))), (0, react.createElement)("div", {
7711
7759
  className: "v3-panel",
7712
7760
  style: { marginTop: 12 }
7713
- }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "完整工具表"), (0, react.createElement)("span", { className: "v3-pn" }, `${rows.length} 行 · 表内滚动`)), (0, react.createElement)(CallIndexToolTable, { rows }), (0, react.createElement)("div", { className: "v3-tfoot" }, (0, react.createElement)("span", null, `${rows.length} 个工具 · 表内滚动`), page === void 0 ? null : (0, react.createElement)("span", { style: { marginLeft: "auto" } }, `快照代 ${page.generation} · 覆盖 ${coverageLabel ?? "未知"}`))), page?.nextCursor !== void 0 && loadingMore === true ? (0, react.createElement)("p", {
7761
+ }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "完整工具表"), (0, react.createElement)("span", { className: "v3-pn" }, `${rows.length} 行 · 表内滚动`)), (0, react.createElement)(CallIndexToolTable, { rows }), (0, react.createElement)("div", { className: "v3-tfoot" }, (0, react.createElement)("span", null, `${rows.length} 个工具 · 表内滚动`))), page?.nextCursor !== void 0 && loadingMore === true ? (0, react.createElement)("p", {
7714
7762
  className: "v3-hint",
7715
7763
  "data-call-index-note": "loading-more",
7716
7764
  style: { marginTop: 8 }
@@ -7725,14 +7773,15 @@ function CallIndexDetailPanelView(props) {
7725
7773
  function CallIndexOverviewPanelView(props) {
7726
7774
  const { status, overview, loading, failure, onRefresh, tools, toolsLoading, toolsFailure } = props;
7727
7775
  const metrics = callIndexOverviewMetrics(overview, status);
7728
- const rangeLabel = callIndexRangeLabel("all");
7729
7776
  const exact = metrics.callsTotalExact;
7730
7777
  const mix = callIndexOutcomeMix(overview?.summary);
7731
7778
  const aggregates = metrics.aggregates;
7732
7779
  const notice = failure === void 0 ? void 0 : failure.disconnected ? `与 Host 断连:${callIndexFailureText(failure)}${failure.hostCode === void 0 ? "" : `(${failure.hostCode})`}` : callIndexFailureText(failure);
7733
7780
  const coverageLabel = status === void 0 ? "未知" : COVERAGE_LABELS[status.coverage];
7734
7781
  const scanning = status !== void 0 && (status.phase === "running" || (status.sessions?.pending ?? 0) > 0 || (status.sessions?.pendingLive ?? 0) > 0);
7735
- const sessionsText = metrics.sessionsListed === void 0 ? "Session 未读取" : `${metrics.sessionsListed} 列出 / ${metrics.sessionsVisible ?? "—"} 可见`;
7782
+ const pendingSessions = status?.sessions?.pending ?? 0;
7783
+ const scanPhrase = status !== void 0 && status.phase === "running" ? "仍在扫描" : pendingSessions > 0 ? `还有 ${pendingSessions} 个会话待补扫` : "扫描已完成";
7784
+ const statusSentence = `已索引 ${metrics.sessionsListed ?? "—"} 个会话 · 覆盖${coverageLabel} · ${scanPhrase}`;
7736
7785
  const coverageHint = exact === true ? (0, react.createElement)("span", { style: {
7737
7786
  display: "inline-flex",
7738
7787
  alignItems: "center",
@@ -7773,11 +7822,6 @@ function CallIndexOverviewPanelView(props) {
7773
7822
  const failingTop = failingRows === void 0 ? [] : failingRows.slice(0, 3);
7774
7823
  const failingRest = failingRows === void 0 ? [] : failingRows.slice(3);
7775
7824
  const failingRestErrors = failingRest.reduce((sum, row) => sum + row.error, 0);
7776
- const failingReadErrors = failingRows === void 0 ? 0 : failingRows.reduce((sum, row) => sum + row.error, 0);
7777
- const failDenominator = metrics.failed !== void 0 && metrics.failed > 0 ? metrics.failed : failingReadErrors;
7778
- const readCalls = tools === void 0 ? 0 : totalToolCalls;
7779
- const readIsPartial = metrics.calls !== void 0 && readCalls < metrics.calls;
7780
- const worstRateTool = failingRows === void 0 || failingRows.length === 0 ? void 0 : failingRows.slice().sort((a, b) => b.error / b.calls - a.error / a.calls)[0];
7781
7825
  const toolsNotice = tools === void 0 ? (0, react.createElement)("p", {
7782
7826
  className: "v3-hint",
7783
7827
  "data-call-index-note": "tools"
@@ -7795,24 +7839,23 @@ function CallIndexOverviewPanelView(props) {
7795
7839
  className: "v3-root mc-section",
7796
7840
  "aria-label": "调用统计总览",
7797
7841
  "aria-busy": loading === true
7798
- }, CallIndexStyles(), V3Styles(), (0, react.createElement)("header", { className: "mc-heading-row" }, (0, react.createElement)("div", { className: "mc-heading-meta" }, (0, react.createElement)("h2", { className: "mc-heading" }, "总览"), (0, react.createElement)("span", { className: "mc-version" }, loading === true ? "正在读取…" : rangeLabel === "全部" ? "范围:全部已索引会话" : `范围:最近 ${rangeLabel}`)), (0, react.createElement)("div", { className: "mc-heading-actions" }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
7842
+ }, CallIndexStyles(), V3Styles(), (0, react.createElement)("div", { style: {
7843
+ display: "flex",
7844
+ justifyContent: "flex-end"
7845
+ } }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
7799
7846
  size: "sm",
7800
7847
  variant: "outline",
7801
7848
  disabled: loading === true || onRefresh === void 0,
7802
7849
  onClick: () => onRefresh?.()
7803
- }, "刷新"))), (0, react.createElement)("p", { className: "mc-desc" }, "调用索引的总览:调用次数、Session 覆盖、来源构成与按日趋势。所有统计口径以覆盖状态为准,未知一律如实标注。"), notice === void 0 ? null : (0, react.createElement)("div", {
7850
+ }, "刷新")), notice === void 0 ? null : (0, react.createElement)("div", {
7804
7851
  className: "mc-notice",
7805
7852
  role: failure?.disconnected === true ? "alert" : "status",
7806
7853
  "data-call-index-state": failure?.disconnected === true ? "disconnected" : "failure"
7807
7854
  }, notice), (0, react.createElement)("div", { className: "v3-status-pills" }, (0, react.createElement)("span", { className: "v3-statusline" }, (0, react.createElement)("span", {
7808
- className: "v3-dot",
7809
- "data-tone": coverageLabel === "完整" ? "ok" : "warn",
7810
- "aria-hidden": "true"
7811
- }), (0, react.createElement)("span", null, "覆盖 ", (0, react.createElement)("b", null, coverageLabel), ` · ${sessionsText}${exact === true ? "" : " · 所有总量为下界"}`)), (0, react.createElement)("span", { className: "v3-statusline" }, (0, react.createElement)("span", {
7812
7855
  className: "v3-dot",
7813
7856
  "data-tone": scanning ? "run" : "ok",
7814
7857
  "aria-hidden": "true"
7815
- }), scanning ? "正在扫描" : "扫描已结束", ` · ${formatCallIndexCount(sessionInfo?.pending, true)} 个待处理`), (0, react.createElement)("span", { className: "v3-statusline v3-status-dash" }, `口径:${overview?.corpus === "persisted-plus-observed-live" ? "已持久化 + 挂载后实时观测" : "Host 未声明 corpus"}`)), (0, react.createElement)("div", {
7858
+ }), (0, react.createElement)("span", null, statusSentence))), (0, react.createElement)("div", {
7816
7859
  className: "v3-stats",
7817
7860
  "data-call-index-metrics": "overview"
7818
7861
  }, V3Stat({
@@ -7843,11 +7886,7 @@ function CallIndexOverviewPanelView(props) {
7843
7886
  label: "不同工具",
7844
7887
  value: formatCallIndexCount(metrics.distinctTools, exact),
7845
7888
  hint: "按 toolName 去重"
7846
- })), callIndexHasUnexplainedSources(aggregates) ? (0, react.createElement)("p", {
7847
- className: "v3-hint",
7848
- "data-call-index-note": "unexplained-sources",
7849
- style: { marginTop: 10 }
7850
- }, "插件工具与技能执行入口的归属目前都解析为 0:Host 无法证明插件 owner,技能执行归属也未验证,所以这两个 0 表示「未解析出」,不表示「未使用」。") : null, (0, react.createElement)("div", {
7889
+ })), (0, react.createElement)("div", {
7851
7890
  className: "v3-board",
7852
7891
  style: { marginTop: 12 }
7853
7892
  }, (0, react.createElement)("div", { className: "v3-panel" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "按日调用趋势"), (0, react.createElement)("span", { className: "v3-pn" }, aggregates === void 0 ? "趋势不可用" : `合计 ${trend.totalCalls} 次 · 峰值 ${trend.maxCalls} 次`)), aggregates === void 0 ? (0, react.createElement)("p", {
@@ -7865,7 +7904,7 @@ function CallIndexOverviewPanelView(props) {
7865
7904
  height: 8,
7866
7905
  borderRadius: 2
7867
7906
  } }), "最近一周"))), OverviewSourcePanel({
7868
- ...aggregates?.bySource === void 0 ? {} : { bySource: aggregates.bySource },
7907
+ ...tools === void 0 ? {} : { tools },
7869
7908
  ...exact === void 0 ? {} : { totalExact: exact }
7870
7909
  })), (0, react.createElement)("div", {
7871
7910
  className: "v3-board",
@@ -7884,7 +7923,10 @@ function CallIndexOverviewPanelView(props) {
7884
7923
  ...bad ? { "data-bad": "true" } : {},
7885
7924
  ...row.error === 0 ? { "data-zero": "true" } : {}
7886
7925
  }, `失败 ${row.error}`));
7887
- }))), (0, react.createElement)("div", { className: "v3-panel" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "异常集中度"), (0, react.createElement)("span", { className: "v3-pn" }, tools === void 0 ? "按失败数排序" : `${formatCallIndexCount(metrics.failed, exact)} 次失败`)), toolsNotice !== null ? toolsNotice : failingRows === void 0 || failingRows.length === 0 ? (0, react.createElement)("p", { className: "v3-hint" }, "当前范围没有失败调用。") : (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "v3-rank" }, ...failingTop.map((row) => (0, react.createElement)("div", {
7926
+ }))), (0, react.createElement)("div", { className: "v3-panel" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "工具调用失败"), (0, react.createElement)("span", { className: "v3-pn" }, tools === void 0 ? "按失败数排序" : `${formatCallIndexCount(metrics.failed, exact)} 次失败`)), toolsNotice !== null ? toolsNotice : failingRows === void 0 || failingRows.length === 0 ? (0, react.createElement)("p", { className: "v3-hint" }, "当前范围没有失败调用。") : (0, react.createElement)("div", {
7927
+ className: "v3-rank",
7928
+ "data-call-index-failures": "true"
7929
+ }, ...failingTop.map((row) => (0, react.createElement)("div", {
7888
7930
  key: "fc-" + row.toolName + "::" + row.sourceKind,
7889
7931
  className: "v3-rank-row"
7890
7932
  }, (0, react.createElement)("span", {
@@ -7899,7 +7941,7 @@ function CallIndexOverviewPanelView(props) {
7899
7941
  color: "var(--v3-bad)",
7900
7942
  fontWeight: 600
7901
7943
  }
7902
- }, String(row.error)))), ...failingRest.length === 0 ? [] : [(0, react.createElement)("div", {
7944
+ }, `${row.error} 次`))), ...failingRest.length === 0 ? [] : [(0, react.createElement)("div", {
7903
7945
  key: "fc-rest",
7904
7946
  className: "v3-rank-row"
7905
7947
  }, (0, react.createElement)("span", {
@@ -7911,42 +7953,7 @@ function CallIndexOverviewPanelView(props) {
7911
7953
  }, `其余 ${failingRest.length} 个工具合计`), (0, react.createElement)("span", {
7912
7954
  className: "v3-n",
7913
7955
  style: { color: "var(--v3-t2)" }
7914
- }, String(failingRestErrors)))]), failDenominator > 0 ? (0, react.createElement)("p", {
7915
- className: "v3-hint",
7916
- style: { marginTop: 9 }
7917
- }, `这些失败合计占全部失败的 ${Math.round(failingTop.reduce((sum, row) => sum + row.error, 0) / failDenominator * 100)}%(占全部失败,不是占单个工具)${readIsPartial ? ";分母是全部失败数,分子只含已读取的逐工具行" : ""}。`) : null, worstRateTool === void 0 ? null : (0, react.createElement)("div", { style: {
7918
- marginTop: 11,
7919
- paddingTop: 11,
7920
- borderTop: "1px solid var(--v3-line)"
7921
- } }, (0, react.createElement)("div", {
7922
- className: "v3-src",
7923
- style: { fontSize: 11.5 }
7924
- }, (0, react.createElement)("span", {
7925
- className: "v3-nm",
7926
- style: {
7927
- color: "var(--v3-t4)",
7928
- flex: "1 1 auto"
7929
- }
7930
- }, "失败率最高的工具"), (0, react.createElement)("b", { style: { color: "var(--v3-bad)" } }, `${Math.round(worstRateTool.error / worstRateTool.calls * 100)}%`)), (0, react.createElement)("p", {
7931
- className: "v3-hint",
7932
- style: { marginTop: 4 }
7933
- }, `${worstRateTool.toolName} ${worstRateTool.error} / ${worstRateTool.calls}(占该工具失败率,不是占全部失败)。`)), (0, react.createElement)("div", { style: {
7934
- marginTop: 11,
7935
- paddingTop: 11,
7936
- borderTop: "1px solid var(--v3-line)"
7937
- } }, (0, react.createElement)("div", {
7938
- className: "v3-src",
7939
- style: { fontSize: 11.5 }
7940
- }, (0, react.createElement)("span", {
7941
- className: "v3-nm",
7942
- style: {
7943
- color: "var(--v3-t4)",
7944
- flex: "1 1 auto"
7945
- }
7946
- }, "未配对结果"), (0, react.createElement)("b", { style: { color: "var(--v3-warn)" } }, formatCallIndexCount(mix?.unresolved, exact))), (0, react.createElement)("p", {
7947
- className: "v3-hint",
7948
- style: { marginTop: 4 }
7949
- }, "没有捕获到结果,不计入成功率分母,也不等于失败。"))))), (0, react.createElement)("div", {
7956
+ }, `${failingRestErrors} 次`))]))), (0, react.createElement)("div", {
7950
7957
  className: "v3-panel",
7951
7958
  style: { marginTop: 12 }
7952
7959
  }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("span", { className: "v3-pt" }, "索引状态"), (0, react.createElement)("span", { className: "v3-pn" }, metrics.tasks === void 0 ? "最近扫描未记录" : `最近扫描 ${metrics.tasks.mode} · ${metrics.tasks.phase}`)), (0, react.createElement)("div", { className: "v3-kv" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "v3-kv-l" }, "数据范围"), (0, react.createElement)("div", { className: "v3-kv-v" }, overview?.corpus === "persisted-plus-observed-live" ? "已持久化 + 挂载后实时观测" : "Host 未声明")), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "v3-kv-l" }, "Session 列出 / 可见"), (0, react.createElement)("div", { className: "v3-kv-v v3-num" }, sessionInfo === void 0 ? "—" : `${sessionInfo.listed} / ${sessionInfo.visible}`)), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "v3-kv-l" }, "待处理 / 失败"), (0, react.createElement)("div", { className: "v3-kv-v v3-num" }, sessionInfo === void 0 ? "—" : `${sessionInfo.pending} / ${sessionInfo.failed}`)), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "v3-kv-l" }, "覆盖(事件 / 枚举 / 语义)"), (0, react.createElement)("div", { className: "v3-kv-v" }, status === void 0 ? "—" : `${COVERAGE_LABELS[status.eventCoverage]} / ${COVERAGE_LABELS[status.enumerationCoverage]} / ${COVERAGE_LABELS[status.coverage]}`))), taskNote));
@@ -8007,6 +8014,14 @@ function detailAfterRefresh(previous, page) {
8007
8014
  detailFailure: void 0
8008
8015
  };
8009
8016
  }
8017
+ /** Pure policy decision, so the container's refresh strategy is testable. */
8018
+ function callIndexRefreshPolicy(status) {
8019
+ const scanning = status !== void 0 && (status.phase === "running" || status.sessions.pending > 0 || status.sessions.pendingLive > 0);
8020
+ return {
8021
+ scanning,
8022
+ mode: scanning ? "status-only" : "full"
8023
+ };
8024
+ }
8010
8025
  /**
8011
8026
  * Container for 调用明细: owns the query, the paging cursor and the detail read.
8012
8027
  * Keeps the previous page visible while the next one loads, so a range click
@@ -8082,23 +8097,35 @@ function CallIndexDetailPanel({ api, initialQuery, resolveScopeTarget, resolveSe
8082
8097
  refreshToken
8083
8098
  ]);
8084
8099
  /**
8085
- * Re-read while a scan is running.
8100
+ * Refresh status/progress while a scan is running — WITHOUT re-reading calls.
8086
8101
  *
8087
- * The 刷新 button is gone (D-031): indexing starts by itself, so the page must
8088
- * also refresh by itself or it would freeze on the first, empty snapshot. This
8089
- * polls ONLY while the Host reports an unfinished scan, then stops — it is not
8090
- * a permanent timer, and it never triggers indexing, only reads.
8102
+ * The old loop bumped `refreshToken`, which re-read status + calls + sessions
8103
+ * every 2s and replaced the data page under the operator mid-scan. Now it polls
8104
+ * `getStatus` only, keeps the current rows untouched, and triggers ONE full read
8105
+ * when the scan finishes (or on the explicit 刷新 button).
8091
8106
  */
8092
- const scanningNow = state.status !== void 0 && (state.status.phase === "running" || state.status.sessions.pending > 0);
8107
+ const scanningNow = callIndexRefreshPolicy(state.status).scanning;
8093
8108
  (0, react.useEffect)(() => {
8094
8109
  if (!scanningNow) return;
8095
- const timer = setTimeout(() => setRefreshToken((value) => value + 1), 2e3);
8096
- return () => clearTimeout(timer);
8097
- }, [
8098
- scanningNow,
8099
- refreshToken,
8100
- state.page?.generation
8101
- ]);
8110
+ let active = true;
8111
+ const id = setInterval(() => {
8112
+ (async () => {
8113
+ const result = await api.getStatus();
8114
+ if (!active || !result.ok) return;
8115
+ const next = result.value;
8116
+ const stillScanning = callIndexRefreshPolicy(next).scanning;
8117
+ setState((previous) => ({
8118
+ ...previous,
8119
+ status: next
8120
+ }));
8121
+ if (!stillScanning) setRefreshToken((value) => value + 1);
8122
+ })();
8123
+ }, 2e3);
8124
+ return () => {
8125
+ active = false;
8126
+ clearInterval(id);
8127
+ };
8128
+ }, [scanningNow, api]);
8102
8129
  /**
8103
8130
  * Append the next page.
8104
8131
  *
@@ -8330,6 +8357,28 @@ function CallIndexOverviewPanel({ api }) {
8330
8357
  active = false;
8331
8358
  };
8332
8359
  }, [api, refreshToken]);
8360
+ const scanning = callIndexRefreshPolicy(state.status).scanning;
8361
+ (0, react.useEffect)(() => {
8362
+ if (!scanning) return;
8363
+ let active = true;
8364
+ const id = setInterval(() => {
8365
+ (async () => {
8366
+ const result = await api.getStatus();
8367
+ if (!active || !result.ok) return;
8368
+ const next = result.value;
8369
+ const stillScanning = callIndexRefreshPolicy(next).scanning;
8370
+ setState((previous) => ({
8371
+ ...previous,
8372
+ status: next
8373
+ }));
8374
+ if (!stillScanning) setRefreshToken((value) => value + 1);
8375
+ })();
8376
+ }, 2e3);
8377
+ return () => {
8378
+ active = false;
8379
+ clearInterval(id);
8380
+ };
8381
+ }, [scanning, api]);
8333
8382
  return (0, react.createElement)(CallIndexOverviewPanelView, {
8334
8383
  ...state.status === void 0 ? {} : { status: state.status },
8335
8384
  ...state.overview === void 0 ? {} : { overview: state.overview },
@@ -8468,27 +8517,24 @@ function OverviewContainer(props) {
8468
8517
  /**
8469
8518
  * 思考限制 — the Guard configuration surface (v3 · 数据看板, D-044).
8470
8519
  *
8471
- * Three sections, in the order the product specified: repeated identical calls,
8472
- * alternating cycles, no-progress protection. Each carries a model-side
8473
- * identifier ('duplicate_call' / 'alternating_cycle' / 'no_progress') shown as
8474
- * plain text, because the reminder text injected into the conversation is
8475
- * English while the label a user reads here is Chinese. The identifier is what
8476
- * ties the two together, so it is displayed rather than hidden.
8520
+ * Three sections, in product order: repeated identical calls, alternating
8521
+ * cycles, no-progress protection. Each carries a model-side identifier
8522
+ * ('duplicate_call' / 'alternating_cycle' / 'no_progress') shown as plain text,
8523
+ * because the reminder injected into the conversation is English while the label
8524
+ * a user reads here is Chinese. The identifier ties the two together.
8477
8525
  *
8478
- * v3 SHAPE: the switches are no longer a single-column stack of .mc-guard-card.
8479
- * They become a two-column grid of .v3-gitem cards (>900px two columns, auto
8480
- * single column via the container query already in v3-ui.ts). Each card has a
8481
- * StateDot + title + model id + switch head, a compact body, and a thin
8482
- * supplementary foot. The fourth grid cell is the 「生效范围」 explanation, not
8483
- * a switch, so the grid fills evenly.
8526
+ * v3 SHAPE: a two-column grid of .v3-gitem cards (>900px two columns, auto single
8527
+ * column via the container query in v3-ui.ts). The Guard counters sit at the very
8528
+ * top as a four-cell .v3-stats strip, followed by one short boundary hint. No page
8529
+ * title, no "activity" sub-title, no "counting since" line — the tab already says
8530
+ * what this page is.
8484
8531
  *
8485
8532
  * DOM CONTRACT kept for the shared test suite: nine <li class="mc-guard-point">
8486
8533
  * whose text still comes from t(); the three data-guard-section values in order;
8487
- * exactly one threshold <input> (value '3, 5, 8', readOnly tracks onChange); the
8488
- * four data-guard-stat cells and data-guard-stats / data-guard-stats-since; and
8534
+ * the threshold editor wrapped in data-guard-thresholds (now three number inputs,
8535
+ * values from the Host); the four data-guard-stat cells and data-guard-stats; and
8489
8536
  * three role="switch" buttons whose aria-checked follows the props. No Chinese
8490
- * colon (':') is introduced by this file — spacing and full-width spaces do
8491
- * the separating instead.
8537
+ * colon (':') is introduced by this file — spacing does the separating instead.
8492
8538
  *
8493
8539
  * Kept hook-free and prop-driven so the settings test suite (no DOM harness) can
8494
8540
  * walk the returned tree.
@@ -8496,13 +8542,12 @@ function OverviewContainer(props) {
8496
8542
  /**
8497
8543
  * Section id → localized title plus the lines that explain what the switch does.
8498
8544
  *
8499
- * The descriptions are part of the contract, not decoration: a bare switch does
8545
+ * the descriptions are part of the contract, not decoration: a bare switch does
8500
8546
  * not tell a reader whether turning it off stops reminders only or also stops
8501
8547
  * local denials. Each bullet states the real behaviour the detector implements.
8502
8548
  *
8503
8549
  * v3 keeps all three bullets (nine <li class="mc-guard-point"> in total, as the
8504
- * shared suite asserts) but lays them out as one compact wrapped row per card,
8505
- * so the paragraph reads like a sentence rather than a three-line list.
8550
+ * shared suite asserts) but lays them out as one compact wrapped row per card.
8506
8551
  */
8507
8552
  const SECTIONS = [
8508
8553
  {
@@ -8535,9 +8580,10 @@ const SECTIONS = [
8535
8580
  ];
8536
8581
  /**
8537
8582
  * Page-specific rules for the v3 Guard grid. Everything else comes from
8538
- * v3-ui.ts; the only local vocabulary here is the compact bullet row and the
8539
- * threshold editor chrome. Colours are always v3 semantic variables — nothing
8540
- * is hard-coded, so light and dark themes both track the host.
8583
+ * v3-ui.ts; the only local vocabulary here is the compact bullet row, the
8584
+ * threshold editor chrome and the per-card "fired" foot. Colours are always v3
8585
+ * semantic variables — nothing is hard-coded, so light and dark themes both
8586
+ * track the host.
8541
8587
  */
8542
8588
  const GUARD_CSS = [
8543
8589
  ".mc-section.v3-root{display:flex;flex-direction:column;gap:12px}",
@@ -8547,13 +8593,14 @@ const GUARD_CSS = [
8547
8593
  ".v3-gitem--info{background:var(--v3-panel-2)}",
8548
8594
  ".v3-gitem .v3-gh .v3-sw{margin-left:auto}",
8549
8595
  ".v3-gfoot-edit{margin-top:9px}",
8550
- ".v3-guard-stats{margin:12px 0}",
8596
+ ".v3-guard-stats{margin:0}",
8597
+ ".v3-gfoot-note{color:var(--v3-t4)}",
8551
8598
  ".mc-guard-threshold-edit{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
8552
8599
  ".mc-guard-threshold-edit-label{font-size:11.5px;color:var(--v3-t3)}",
8553
- ".mc-guard-threshold-edit-input{width:150px;padding:3px 7px;border-radius:var(--v3-r-sm);font-size:12px;font-family:var(--v3-font-code);color:var(--v3-t1);background:var(--v3-panel-2);border:1px solid var(--v3-line)}",
8554
- ".mc-guard-threshold-edit-input:focus-visible{outline:2px solid var(--v3-info);outline-offset:1px}",
8555
- ".mc-guard-threshold-edit-input:disabled{opacity:.6}",
8556
- ".mc-guard-threshold-edit-hint{font-size:10.5px;color:var(--v3-t4)}"
8600
+ ".mc-guard-threshold-num{width:56px;padding:3px 6px;border-radius:var(--v3-r-sm);font-size:12px;text-align:center;font-family:var(--v3-font-code);color:var(--v3-t1);background:var(--v3-panel-2);border:1px solid var(--v3-line)}",
8601
+ ".mc-guard-threshold-num:focus-visible{outline:2px solid var(--v3-info);outline-offset:1px}",
8602
+ ".mc-guard-threshold-num:disabled{opacity:.6}",
8603
+ ".mc-guard-threshold-hint{margin:6px 0 0}"
8557
8604
  ].join("");
8558
8605
  /** '—' for a counter the Host did not report; never a fabricated 0. */
8559
8606
  function statText(value) {
@@ -8563,10 +8610,11 @@ function statText(value) {
8563
8610
  * The Guard's real activity board, restyled as a v3 stat strip.
8564
8611
  *
8565
8612
  * Answers the two questions the page previously could not: 「它是否在工作」
8566
- * (observations / since) and 「调用成功还是失败」 (outcome split). Every number
8567
- * comes from the Host; a missing counter renders '—', so an older Host reads as
8613
+ * (observations) and 「调用成功还是失败」 (outcome split). Every number comes
8614
+ * from the Host; a missing counter renders '—', so an older Host reads as
8568
8615
  * "unknown" rather than "zero". The four data-guard-stat cells and the
8569
- * data-guard-stats / data-guard-stats-since hooks are the published contract.
8616
+ * data-guard-stats hook are the published contract. No sub-title and no
8617
+ * "counting since" line: the strip is the first thing on the page.
8570
8618
  */
8571
8619
  function GuardStatsBoard(props) {
8572
8620
  const { t, stats } = props;
@@ -8576,35 +8624,21 @@ function GuardStatsBoard(props) {
8576
8624
  className: "v3-stat",
8577
8625
  "data-guard-stat": label
8578
8626
  }, (0, react.createElement)("div", { className: "v3-stat-l" }, label), (0, react.createElement)("div", { className: "v3-stat-v" }, value), hint === void 0 ? null : (0, react.createElement)("div", { className: "v3-stat-d" }, hint));
8579
- return (0, react.createElement)("div", { className: "v3-guard-stats" }, (0, react.createElement)("div", { className: "v3-ph" }, (0, react.createElement)("h3", { className: "v3-pt" }, t("guardStatsTitle"))), (0, react.createElement)("div", {
8627
+ return (0, react.createElement)("div", { className: "v3-guard-stats" }, (0, react.createElement)("div", {
8580
8628
  className: "v3-stats",
8581
8629
  "data-guard-stats": "true",
8582
8630
  role: "group",
8583
8631
  "aria-label": t("guardStatsTitle")
8584
- }, cell(t("guardStatObservations"), statText(stats?.observations), t("guardStatObservationsHint")), cell(t("guardStatOutcomes"), statText(outcomes?.success) + " / " + statText(outcomes?.error), t("guardStatOutcomesHint")), cell(t("guardStatReminders"), statText(reminders?.repeat) + " / " + statText(reminders?.cycle), t("guardStatRemindersHint")), cell(t("guardStatDenials"), statText(stats?.denials), t("guardStatDenialsHint"))), stats?.since === void 0 ? null : (0, react.createElement)("p", {
8585
- className: "v3-src-note",
8586
- "data-guard-stats-since": "true"
8587
- }, t("guardStatsSince") + " " + formatGuardTime(stats.since)));
8588
- }
8589
- /** 'YYYY-MM-DD HH:mm' in the browser zone; '—' when unknown. */
8590
- function formatGuardTime(time) {
8591
- if (time === void 0 || !Number.isFinite(time)) return "—";
8592
- const date = new Date(time);
8593
- if (Number.isNaN(date.getTime())) return "—";
8594
- const pad = (value) => String(value).padStart(2, "0");
8595
- return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
8632
+ }, cell(t("guardStatObservations"), statText(stats?.observations), t("guardStatObservationsHint")), cell(t("guardStatOutcomes"), statText(outcomes?.success) + " / " + statText(outcomes?.error), t("guardStatOutcomesHint")), cell(t("guardStatReminders"), statText(reminders?.repeat) + " / " + statText(reminders?.cycle), t("guardStatRemindersHint")), cell(t("guardStatDenials"), statText(stats?.denials), t("guardStatDenialsHint"))));
8596
8633
  }
8597
8634
  /**
8598
8635
  * Editable reminder thresholds.
8599
8636
  *
8600
- * The thresholds were a hard-coded 3/5/8 the operator could read but not change,
8601
- * which made the section look decorative. They are now one comma-separated
8602
- * field: the list is short, order matters, and a number-per-input row would be
8603
- * more chrome than content. Invalid input is refused by the Host and reported
8604
- * back, so this input never claims a value the detector rejected.
8605
- *
8606
- * Kept as its own component so the shared suite's expanding walk can reach the
8607
- * single <input> (data-guard-thresholds marks the wrapper).
8637
+ * Three plain number inputs instead of one comma-separated text field: the
8638
+ * operator sees one box per firing point and never has to learn a syntax. Values
8639
+ * always come from the Host; a keystroke is assembled into a fresh array and
8640
+ * handed to onThresholdsChange, and an older Host that does not accept a write
8641
+ * gets disabled boxes. data-guard-thresholds marks the wrapper.
8608
8642
  */
8609
8643
  function GuardThresholdEditor(props) {
8610
8644
  const { t, thresholds, onChange } = props;
@@ -8613,52 +8647,32 @@ function GuardThresholdEditor(props) {
8613
8647
  return (0, react.createElement)("div", {
8614
8648
  className: "mc-guard-threshold-edit",
8615
8649
  "data-guard-thresholds": "true"
8616
- }, (0, react.createElement)("label", {
8617
- className: "mc-guard-threshold-edit-label",
8618
- htmlFor: "mc-guard-thresholds-input"
8619
- }, t("guardThresholds")), (0, react.createElement)("input", {
8620
- id: "mc-guard-thresholds-input",
8621
- className: "mc-guard-threshold-edit-input",
8622
- type: "text",
8623
- inputMode: "numeric",
8624
- value: thresholds.join(", "),
8650
+ }, (0, react.createElement)("label", { className: "mc-guard-threshold-edit-label" }, t("guardThresholds")), ...thresholds.map((value, index) => (0, react.createElement)("input", {
8651
+ key: index,
8652
+ type: "number",
8653
+ min: 2,
8654
+ max: 50,
8655
+ step: 1,
8656
+ className: "mc-guard-threshold-num",
8657
+ value: String(value),
8625
8658
  readOnly: !editable,
8626
8659
  disabled: !editable,
8627
- "aria-label": t("guardThresholds"),
8660
+ "aria-label": t("guardThresholds") + " " + String(index + 1),
8628
8661
  onChange: (event) => {
8629
8662
  if (!editable) return;
8630
- const parsed = parseGuardThresholds(event.target.value);
8631
- if (parsed === void 0) return;
8632
- onChange(parsed);
8663
+ const raw = Number(event.target.value);
8664
+ if (!Number.isSafeInteger(raw) || raw < 2 || raw > 50) return;
8665
+ const next = thresholds.slice();
8666
+ next[index] = raw;
8667
+ onChange(next);
8633
8668
  }
8634
- }), (0, react.createElement)("span", { className: "mc-guard-threshold-edit-hint" }, editable ? t("guardThresholdsEditableHint") : t("guardThresholdsFixedHint")));
8635
- }
8636
- /**
8637
- * Parse '3, 5, 8' into a threshold list.
8638
- *
8639
- * Returns 'undefined' for anything the Host would reject (empty, non-integer,
8640
- * out of range, not strictly increasing). Parsing here as well as on the Host is
8641
- * deliberate: it keeps an invalid keystroke from round-tripping through the
8642
- * remote and flickering the field.
8643
- */
8644
- function parseGuardThresholds(text) {
8645
- const parts = text.split(",").map((part) => part.trim()).filter((part) => part !== "");
8646
- if (parts.length === 0 || parts.length > 5) return void 0;
8647
- const parsed = [];
8648
- for (const part of parts) {
8649
- if (!/^\d+$/.test(part)) return void 0;
8650
- const value = Number(part);
8651
- if (!Number.isSafeInteger(value) || value < 2 || value > 50) return void 0;
8652
- if (parsed.length > 0 && value <= parsed[parsed.length - 1]) return void 0;
8653
- parsed.push(value);
8654
- }
8655
- return parsed;
8669
+ })));
8656
8670
  }
8657
- /** The supplementary foot line for a section, per the v3 design copy. */
8658
- function sectionFoot(id, t) {
8659
- if (id === "alternating_cycle") return (0, react.createElement)("div", { className: "v3-gfoot" }, (0, react.createElement)("span", null, t("guardFootCycle")));
8660
- if (id === "no_progress") return (0, react.createElement)("div", { className: "v3-gfoot" }, (0, react.createElement)("span", null, t("guardFootNoProgress")));
8661
- return (0, react.createElement)("div", { className: "v3-gfoot" });
8671
+ /** The counter a card reports as "fired", per the product mapping. */
8672
+ function firedValue(id, stats) {
8673
+ if (id === "duplicate_call") return stats?.reminders?.repeat;
8674
+ if (id === "alternating_cycle") return stats?.reminders?.cycle;
8675
+ return stats?.denials;
8662
8676
  }
8663
8677
  function GuardPanel(props) {
8664
8678
  const { t, enabled, duplicateThresholds, onToggle, onThresholdsChange, stats, onReset } = props;
@@ -8673,6 +8687,12 @@ function GuardPanel(props) {
8673
8687
  className: "v3-dot",
8674
8688
  "aria-hidden": "true"
8675
8689
  });
8690
+ const belowBullets = entry.id === "duplicate_call" ? (0, react.createElement)("div", { className: "v3-gfoot-edit" }, (0, react.createElement)(GuardThresholdEditor, {
8691
+ t,
8692
+ thresholds: duplicateThresholds,
8693
+ ...onThresholdsChange === void 0 ? {} : { onChange: onThresholdsChange }
8694
+ }), (0, react.createElement)("p", { className: "v3-hint mc-guard-threshold-hint" }, t("guardThresholdHint"))) : (0, react.createElement)("p", { className: "v3-hint" }, t(entry.id === "alternating_cycle" ? "guardPurposeB" : "guardPurposeC"));
8695
+ const footNote = entry.id === "alternating_cycle" ? (0, react.createElement)("span", { className: "v3-gfoot-note" }, t("guardFootCycle")) : entry.id === "no_progress" ? (0, react.createElement)("span", { className: "v3-gfoot-note" }, t("guardFootNoProgress")) : null;
8676
8696
  return (0, react.createElement)("div", {
8677
8697
  key: entry.id,
8678
8698
  className: "v3-gitem",
@@ -8688,27 +8708,23 @@ function GuardPanel(props) {
8688
8708
  })), (0, react.createElement)("ul", { className: "v3-gpoints" }, ...entry.bodyKeys.map((key) => (0, react.createElement)("li", {
8689
8709
  key,
8690
8710
  className: "mc-guard-point"
8691
- }, t(key)))), entry.id === "duplicate_call" ? (0, react.createElement)("div", { className: "v3-gfoot" }, (0, react.createElement)("span", null, t("guardThresholds")), ...(duplicateThresholds ?? []).map((value, index) => (0, react.createElement)("span", {
8692
- key: index,
8693
- className: "v3-kbd"
8694
- }, String(value)))) : sectionFoot(entry.id, t), entry.id === "duplicate_call" && duplicateThresholds !== void 0 ? (0, react.createElement)("div", { className: "v3-gfoot-edit" }, (0, react.createElement)(GuardThresholdEditor, {
8695
- t,
8696
- thresholds: duplicateThresholds,
8697
- ...onThresholdsChange === void 0 ? {} : { onChange: onThresholdsChange }
8698
- })) : null);
8711
+ }, t(key)))), belowBullets, (0, react.createElement)("div", { className: "v3-gfoot" }, (0, react.createElement)("span", null, t("guardFired")), (0, react.createElement)("span", { className: "v3-kbd" }, statText(firedValue(entry.id, stats))), footNote));
8699
8712
  };
8700
8713
  const scopeCell = (0, react.createElement)("div", {
8701
8714
  key: "scope",
8702
8715
  className: "v3-gitem v3-gitem--info"
8703
8716
  }, (0, react.createElement)("div", { className: "v3-gh" }, (0, react.createElement)("span", { className: "v3-gnm" }, t("guardScopeTitle"))), (0, react.createElement)("p", null, t("guardScopeBody")), (0, react.createElement)("div", { className: "v3-gfoot" }, (0, react.createElement)("span", null, t("guardScopeFoot"))));
8704
- return (0, react.createElement)("section", { className: "mc-section v3-root" }, V3Styles(), (0, react.createElement)("style", { "data-guard-style": "true" }, GUARD_CSS), (0, react.createElement)("header", { className: "mc-heading-row" }, (0, react.createElement)("h2", { className: "mc-heading" }, t("guardTitle")), (0, react.createElement)("div", { className: "mc-heading-actions" }, onReset !== void 0 && (0, react.createElement)("button", {
8717
+ return (0, react.createElement)("section", { className: "mc-section v3-root" }, V3Styles(), (0, react.createElement)("style", { "data-guard-style": "true" }, GUARD_CSS), onReset === void 0 ? null : (0, react.createElement)("div", { style: {
8718
+ display: "flex",
8719
+ justifyContent: "flex-end"
8720
+ } }, (0, react.createElement)("button", {
8705
8721
  type: "button",
8706
8722
  className: "mc-count mc-count--action",
8707
8723
  onClick: onReset
8708
- }, t("guardReset")))), (0, react.createElement)("p", { className: "v3-notebox" }, (0, react.createElement)("b", null, t("guardBoundaryLabel")), " ", t("guardBoundary")), (0, react.createElement)(GuardStatsBoard, {
8724
+ }, t("guardReset"))), (0, react.createElement)(GuardStatsBoard, {
8709
8725
  t,
8710
8726
  ...stats === void 0 ? {} : { stats }
8711
- }), (0, react.createElement)("div", { className: "v3-gpanel" }, ...SECTIONS.map(section), scopeCell));
8727
+ }), (0, react.createElement)("p", { className: "v3-hint" }, t("guardBoundaryShort")), (0, react.createElement)("div", { className: "v3-gpanel" }, ...SECTIONS.map(section), scopeCell));
8712
8728
  }
8713
8729
  //#endregion
8714
8730
  //#region src/client/GuardContainer.tsx
@@ -9033,6 +9049,19 @@ function PluginSection(props) {
9033
9049
  live = false;
9034
9050
  };
9035
9051
  }, [callIndexApi]);
9052
+ /**
9053
+ * Reset the settings content scroller when the category changes.
9054
+ *
9055
+ * The shell owns the scrolling column and swaps panels with `hidden`, so the
9056
+ * previous tab's scrollTop survives the switch and the next page opens halfway
9057
+ * down (observed: 执行保护 opened below its counter board). Reset the nearest
9058
+ * scrollable ancestor of the active panel.
9059
+ */
9060
+ (0, react.useEffect)(() => {
9061
+ let node = document.getElementById("mc-category-panel-" + active);
9062
+ while (node !== null && !(node.scrollHeight > node.clientHeight + 4)) node = node.parentElement;
9063
+ if (node !== null) node.scrollTop = 0;
9064
+ }, [active]);
9036
9065
  return (0, react.createElement)(PluginSectionView, {
9037
9066
  capability: props.capability,
9038
9067
  usageApi: props.usageApi,
@@ -9173,6 +9202,11 @@ async function apply(ctx) {
9173
9202
  guardScopeBody: "三个开关按 Agent / Turn 隔离,不跨 Session 继承;关闭只停止对应的提醒与局部拒绝,不改动任何配置或历史。",
9174
9203
  guardScopeFoot: "恢复默认只写回开关值,不重写其他配置",
9175
9204
  guardBoundaryLabel: "边界",
9205
+ guardBoundaryShort: "这里的数字只用于展示,不构成硬停止;三档阈值只参与校验与计数,不产生提醒也不停止任务。",
9206
+ guardThresholdHint: "默认 3 / 5 / 8 次;改的是第几次重复时提醒,只提醒、不拦截。",
9207
+ guardFired: "实际触发",
9208
+ guardPurposeB: "作用:识别 A→B→A→B 这类交替循环并提醒,避免来回空转。",
9209
+ guardPurposeC: "作用:证据明确时拦下已证实的重复操作,其余情况只提醒。",
9176
9210
  guardFootCycle: "命中后只给出说明,不阻断调用",
9177
9211
  guardFootNoProgress: "降级条件 日志缺失 · 结果不可比较 · 识别不确定",
9178
9212
  guardSectionA: "A · 相同调用重复提醒",
@@ -9223,7 +9257,7 @@ async function apply(ctx) {
9223
9257
  emptyGlobalSkills: "暂无全局技能",
9224
9258
  emptyProjectSkills: "暂无项目技能",
9225
9259
  emptyPresetSkills: "暂无预设技能",
9226
- filterByName: "按名称、分组或正则表达式筛选…",
9260
+ filterByName: "筛选名称或分组",
9227
9261
  filterHint: "忽略大小写。匹配工具/技能名称、MCP 服务器、技能来源标签与预设 ID;也可以输入正则表达式,例如 cordis|ptc 或 /^mcp__dbx/。",
9228
9262
  filterNoMatch: "没有匹配的能力(清空过滤框可恢复完整列表)",
9229
9263
  emptyTools: "暂无工具",
@@ -9326,7 +9360,7 @@ async function apply(ctx) {
9326
9360
  summaryLimitInvalid: "请输入不小于 20 的整数",
9327
9361
  summaryLimitDisabled: "检索信息存储未启用",
9328
9362
  summaryLimitSaved: "检索描述上限已保存",
9329
- diagnosticTitle: "能力诊断:{count} 项",
9363
+ diagnosticTitle: "能力诊断 · {count} 项",
9330
9364
  diagnosticUnavailable: "不可调用:{name}(来源 {source}{preset})",
9331
9365
  diagnosticShadowed: "同名覆盖:{name} 保留 {winner},隐藏 {hidden}",
9332
9366
  diagnosticBroken: "损坏的预设:{id}({reason};受影响技能数量由 DSH 未提供)",
@@ -9379,6 +9413,11 @@ async function apply(ctx) {
9379
9413
  guardScopeBody: "The three switches are isolated per Agent / turn and never inherited across Sessions; turning one off stops only its reminders and local denials, and changes no configuration or history.",
9380
9414
  guardScopeFoot: "Restoring defaults writes only the switch values; no other configuration is rewritten.",
9381
9415
  guardBoundaryLabel: "Boundary",
9416
+ guardBoundaryShort: "These numbers are informational only and never stop a task; the three tier thresholds participate in validation and counting only.",
9417
+ guardThresholdHint: "Defaults are 3 / 5 / 8; editing changes when the reminder fires. Reminders never block a call.",
9418
+ guardFired: "Fired",
9419
+ guardPurposeB: "Purpose: detect alternating A→B→A→B cycles and remind, so the agent stops going in circles.",
9420
+ guardPurposeC: "Purpose: block a confirmed repeat when the evidence is clear; otherwise only remind.",
9382
9421
  guardFootCycle: "Fires an explanation only; it never blocks the call.",
9383
9422
  guardFootNoProgress: "Degrades when: logs missing · results incomparable · match uncertain",
9384
9423
  guardSectionA: "A · Repeated identical calls",
@@ -9429,7 +9468,7 @@ async function apply(ctx) {
9429
9468
  emptyGlobalSkills: "No global skills",
9430
9469
  emptyProjectSkills: "No project skills",
9431
9470
  emptyPresetSkills: "No preset skills",
9432
- filterByName: "Filter by name, group, or regex…",
9471
+ filterByName: "Filter name or group",
9433
9472
  filterHint: "Case-insensitive. Matches tool/skill names, MCP servers, skill source labels and preset ids; a regular expression works too, e.g. cordis|ptc or /^mcp__dbx/.",
9434
9473
  filterNoMatch: "No capability matches (clear the filter to see the full list)",
9435
9474
  emptyTools: "No tools",