@michengai/dsh-agency-agents 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ## Features
20
20
 
21
- - Browse, enable, and disable bundled experts across 17 divisions in **Settings → Experts**.
21
+ - Filter by category or search, then enable or disable bundled experts in **Settings → Experts**.
22
22
  - Summon enabled experts by name or slug from the composer's **Experts** picker.
23
23
  - Use `list_experts` to discover experts and `summon_expert` to start a one-shot specialist subagent.
24
24
  - Use 271 bundled personas immediately, or connect a separately synchronized expert directory.
package/README.zh-CN.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ## 功能概览
20
20
 
21
- - 在「设置 → 专家」中按 17 个分区浏览、启用或停用内置专家。
21
+ - 在「设置 → 专家」中按分类筛选或搜索,再启用或停用内置专家。
22
22
  - 在输入框的「专家」中按名称或 slug 召唤已启用的专家处理完整任务。
23
23
  - 提供 `list_experts` 与 `summon_expert` 工具,分别用于发现专家和启动一次性子代理。
24
24
  - 内置 271 份 persona,无需额外下载;也可接入自行同步的专家目录。
package/lib/client.js CHANGED
@@ -2529,6 +2529,16 @@ window.__ModuleLoader__.load({
2529
2529
  "settings.loading": "正在加载专家…",
2530
2530
  "settings.enabled": "已启用",
2531
2531
  "settings.disabled": "已停用",
2532
+ "settings.filter.category": "分类",
2533
+ "settings.filter.all": "全部",
2534
+ "settings.filter.option": "{name}({count})",
2535
+ "settings.filter.showing.one": "显示 {count} 位",
2536
+ "settings.filter.showing.other": "显示 {count} 位",
2537
+ "settings.search": "搜索",
2538
+ "settings.search.placeholder": "名称、slug 或简介",
2539
+ "settings.search.clear": "清除搜索",
2540
+ "settings.empty": "没有匹配的专家。试试其他关键词,或切换到「{all}」。",
2541
+ "settings.empty.reset": "清除筛选",
2532
2542
  "error.conflict": "配置已被其他窗口修改。",
2533
2543
  "error.conflict.refreshed": "配置已被其他窗口修改,已为您刷新。",
2534
2544
  "error.conflict.refreshFailed": "配置已被其他窗口修改,但刷新失败,请手动刷新。",
@@ -2569,6 +2579,16 @@ window.__ModuleLoader__.load({
2569
2579
  "settings.loading": "Loading experts…",
2570
2580
  "settings.enabled": "Enabled",
2571
2581
  "settings.disabled": "Disabled",
2582
+ "settings.filter.category": "Category",
2583
+ "settings.filter.all": "All",
2584
+ "settings.filter.option": "{name} ({count})",
2585
+ "settings.filter.showing.one": "Showing {count} expert",
2586
+ "settings.filter.showing.other": "Showing {count} experts",
2587
+ "settings.search": "Search",
2588
+ "settings.search.placeholder": "Name, slug, or description",
2589
+ "settings.search.clear": "Clear search",
2590
+ "settings.empty": "No matching experts. Try another keyword, or switch to {all}.",
2591
+ "settings.empty.reset": "Clear filters",
2572
2592
  "error.conflict": "Settings were changed in another window.",
2573
2593
  "error.conflict.refreshed": "Settings were changed in another window and have been refreshed.",
2574
2594
  "error.conflict.refreshFailed": "Settings were changed in another window, but refresh failed. Please refresh manually.",
@@ -6645,6 +6665,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6645
6665
  emoji: e.emoji,
6646
6666
  division: e.division,
6647
6667
  divisionZh: ZH_DIVISION[e.division] ?? e.division,
6668
+ divisionEn: EN_DIVISION[e.division] ?? e.division,
6648
6669
  description: e.description,
6649
6670
  descriptionEn: e.descriptionEn
6650
6671
  })).sort((a, b) => a.division.localeCompare(b.division) || a.slug.localeCompare(b.slug));
@@ -6667,6 +6688,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6667
6688
  experts: (groups.get(d) ?? []).slice().sort((a, b) => compareExpertName(a, b, active))
6668
6689
  }));
6669
6690
  }
6691
+ /** 规范化检索词:去首尾空白并转小写,便于中英和 slug 统一匹配。 */
6692
+ function normalizeExpertQuery(query) {
6693
+ return query.trim().toLowerCase();
6694
+ }
6695
+ /** 按名称、slug、分区或简介做包含匹配;空检索视为全部命中。 */
6696
+ function matchExpertQuery(expert, query) {
6697
+ const q = normalizeExpertQuery(query);
6698
+ if (q === "") return true;
6699
+ return [
6700
+ expert.slug,
6701
+ expert.name,
6702
+ expert.nameEn,
6703
+ expert.division,
6704
+ expert.divisionZh,
6705
+ expert.divisionEn,
6706
+ expert.description,
6707
+ expert.descriptionEn
6708
+ ].some((field) => field.toLowerCase().includes(q));
6709
+ }
6710
+ /** 先按分区收窄,再按检索词过滤。division 为空表示全部分类。 */
6711
+ function filterExperts(list, options) {
6712
+ const division = options.division ?? "";
6713
+ return list.filter((expert) => (division === "" || expert.division === division) && matchExpertQuery(expert, options.query ?? ""));
6714
+ }
6715
+ const DIVISION_COUNTS = Object.fromEntries(DIVISION_ORDER.map((division) => [division, EXPERTS.filter((expert) => expert.division === division).length]));
6670
6716
  /** 按当前 locale 取专家显示名:en 用花名册英文名,其余用中文名。 */
6671
6717
  function displayName(e, active) {
6672
6718
  return active === "en" ? e.nameEn : e.name;
@@ -6675,7 +6721,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6675
6721
  function displayDescription(e, active) {
6676
6722
  return active === "en" && e.descriptionEn !== "" ? e.descriptionEn : e.description;
6677
6723
  }
6678
- const CSS = ".aag-btn-wrap{position:relative;order:1;margin-right:-8px}.aag-btn{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 4px 0 8px;border:none;border-radius:24px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;font-weight:500;cursor:pointer}.aag-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.aag-menu{position:absolute;bottom:calc(100% + 4px);left:0;box-sizing:border-box;padding:4px;display:flex;flex-direction:column;gap:0;width:300px;max-width:360px;max-height:calc(100vh - 24px);overflow-y:auto;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);z-index:10000}.aag-menu-title{padding:8px 10px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.aag-menu-item{display:flex;align-items:center;gap:8px;width:100%;min-height:40px;padding:8px 10px;border:none;border-radius:10px;background:transparent;cursor:pointer;text-align:left;font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary);box-sizing:border-box}.aag-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover)}.aag-emoji{flex:0 0 auto;font-size:16px}.aag-menu-empty{padding:8px 10px;color:var(--dsw-alias-label-secondary);font-size:13px}[data-composer-card] :has(> button[aria-haspopup=\"listbox\"]) > :nth-child(2){order:2}\n.aag-section{box-sizing:border-box;display:flex;min-width:0;max-width:760px;width:100%;margin:0 auto;flex-direction:column;gap:16px;padding:0 0 32px;color:var(--dsw-alias-label-primary)}\n.aag-toolbar{display:flex;align-items:flex-start;gap:16px;padding-bottom:12px}\n.aag-title{margin:0;font-size:20px;line-height:28px;font-weight:650;letter-spacing:-.2px}\n.aag-desc{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}\n.aag-actions{display:flex;align-items:center;gap:8px;margin-left:auto}\n.aag-action{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 12px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:550;cursor:pointer;transition:opacity 180ms ease,background 180ms ease,border-color 180ms ease}\n.aag-action:hover:not(:disabled){opacity:.9}.aag-action:disabled{opacity:.5;cursor:default}\n.aag-action-secondary{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}\n.aag-action:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}\n.aag-summary{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}\n.aag-summary-count{font-size:18px;font-weight:650}.aag-summary-label{color:var(--dsw-alias-label-secondary);font-size:12px}.aag-summary-separator{width:1px;height:24px;background:var(--dsw-alias-border-l2)}\n.aag-group{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}\n.aag-group-head{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}\n.aag-group-title{margin:0;font-size:14px;font-weight:600}.aag-count{color:var(--dsw-alias-label-tertiary);font-size:12px}\n.aag-row{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}\n.aag-row:last-child{border-bottom:0}\n.aag-row-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}\n.aag-row-id{display:flex;align-items:center;gap:7px;min-width:0}\n.aag-row-name{overflow:hidden;font-size:13px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}\n.aag-tag{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}\n.aag-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}\n.aag-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}\n.aag-note{overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}\n.aag-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}\n@media (max-width:560px){.aag-toolbar{flex-wrap:wrap}.aag-actions{margin-left:0}.aag-row{align-items:flex-start;flex-wrap:wrap}.aag-row>.aag-action{margin-left:auto}}\n@media (prefers-reduced-motion:reduce){.aag-action{transition:none}}\n" + DIVISION_ORDER.map((d) => `[role="listbox"] div[data-source="division.${d}"] ~ button span:last-child`).join(",") + "{flex:1 1 auto;max-width:none;min-width:0}";
6724
+ const CSS = ".aag-btn-wrap{position:relative;order:1;margin-right:-8px}.aag-btn{display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 4px 0 8px;border:none;border-radius:24px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;font-weight:500;cursor:pointer}.aag-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.aag-menu{position:absolute;bottom:calc(100% + 4px);left:0;box-sizing:border-box;padding:4px;display:flex;flex-direction:column;gap:0;width:300px;max-width:360px;max-height:calc(100vh - 24px);overflow-y:auto;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);z-index:10000}.aag-menu-title{padding:8px 10px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.aag-menu-item{display:flex;align-items:center;gap:8px;width:100%;min-height:40px;padding:8px 10px;border:none;border-radius:10px;background:transparent;cursor:pointer;text-align:left;font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary);box-sizing:border-box}.aag-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover)}.aag-emoji{flex:0 0 auto;font-size:16px}.aag-menu-empty{padding:8px 10px;color:var(--dsw-alias-label-secondary);font-size:13px}[data-composer-card] :has(> button[aria-haspopup=\"listbox\"]) > :nth-child(2){order:2}\n.aag-section{box-sizing:border-box;display:flex;min-width:0;max-width:760px;width:100%;margin:0 auto;flex-direction:column;gap:16px;padding:0 0 32px;color:var(--dsw-alias-label-primary)}\n.aag-toolbar{display:flex;align-items:flex-start;gap:16px;padding-bottom:12px}\n.aag-title{margin:0;font-size:20px;line-height:28px;font-weight:650;letter-spacing:-.2px}\n.aag-desc{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}\n.aag-actions{display:flex;align-items:center;gap:8px;margin-left:auto}\n.aag-action{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 12px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:550;cursor:pointer;transition:opacity 180ms ease,background 180ms ease,border-color 180ms ease}\n.aag-action:hover:not(:disabled){opacity:.9}.aag-action:disabled{opacity:.5;cursor:default}\n.aag-action-secondary{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}\n.aag-action:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}\n.aag-summary{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}\n.aag-summary-count{font-size:18px;font-weight:650}.aag-summary-label{color:var(--dsw-alias-label-secondary);font-size:12px}.aag-summary-separator{width:1px;height:24px;background:var(--dsw-alias-border-l2)}\n.aag-group{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}\n.aag-group-head{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}\n.aag-group-title{margin:0;font-size:14px;font-weight:600}.aag-count{color:var(--dsw-alias-label-tertiary);font-size:12px}\n.aag-row{display:flex;align-items:center;gap:12px;padding:13px 16px;border-bottom:1px solid var(--dsw-alias-border-l1)}\n.aag-row:last-child{border-bottom:0}\n.aag-row-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}\n.aag-row-id{display:flex;align-items:center;gap:7px;min-width:0}\n.aag-row-name{overflow:hidden;font-size:13px;font-weight:500;line-height:20px;text-overflow:ellipsis;white-space:nowrap}\n.aag-tag{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}\n.aag-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}\n.aag-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}\n.aag-note{overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}\n.aag-error{color:var(--dsw-alias-state-error-primary);font-size:13px;line-height:20px}\n.aag-filters{display:flex;align-items:flex-end;gap:10px}\n.aag-field{display:flex;min-width:0;flex:1;flex-direction:column;gap:6px}\n.aag-field-category{flex:0 1 220px}\n.aag-field-search{flex:1 1 240px}\n.aag-label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px}\n.aag-control{box-sizing:border-box;width:100%;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}\n.aag-control:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}\n.aag-select{position:relative}\n.aag-select-trigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}\n.aag-select-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}\n.aag-select-trigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}\n.aag-select-trigger[aria-expanded=\"true\"]{border-color:var(--dsw-alias-state-success-primary)}\n.aag-select-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.aag-select-caret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}\n.aag-select-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}\n.aag-select-option{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}\n.aag-select-option:hover,.aag-select-option[data-active=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}\n.aag-select-option[aria-selected=\"true\"]{color:var(--dsw-alias-state-success-primary)}\n.aag-search-wrap{position:relative;display:flex;align-items:center}\n.aag-search{padding-right:32px}.aag-search::-webkit-search-cancel-button,.aag-search::-webkit-search-decoration{-webkit-appearance:none;appearance:none}\n.aag-search-clear{position:absolute;right:4px;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:16px;line-height:1;cursor:pointer}\n.aag-search-clear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}\n.aag-search-clear:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}\n.aag-empty{display:flex;flex-direction:column;align-items:center;gap:12px;padding:28px 16px;border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;text-align:center}\n.aag-filter-meta{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-size:12px}\n@media (max-width:560px){.aag-toolbar{flex-wrap:wrap}.aag-actions{margin-left:0}.aag-filters{flex-direction:column;align-items:stretch}.aag-field-category,.aag-field-search{flex:none}.aag-row{align-items:flex-start;flex-wrap:wrap}.aag-row>.aag-action{margin-left:auto}.aag-filter-meta{margin-left:0}}\n@media (prefers-reduced-motion:reduce){.aag-action{transition:none}}\n" + DIVISION_ORDER.map((d) => `[role="listbox"] div[data-source="division.${d}"] ~ button span:last-child`).join(",") + "{flex:1 1 auto;max-width:none;min-width:0}";
6679
6725
  /** 将写失败映射到 agency 词条;非冲突错误返回 null,由调用方展示原始消息。 */
6680
6726
  function writeErrorKey(error, options) {
6681
6727
  if (error instanceof Error && error.message.includes("changed since it was read")) {
@@ -6806,9 +6852,138 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6806
6852
  onClick
6807
6853
  }, expertIcon(), react.default.createElement("span", null, props.t("settings.nav"))), menu);
6808
6854
  }
6855
+ function CategorySelect(props) {
6856
+ const [open, setOpen] = react.default.useState(false);
6857
+ const selectedIndex = Math.max(0, props.options.findIndex((option) => option.value === props.value));
6858
+ const [active, setActive] = react.default.useState(selectedIndex);
6859
+ const rootRef = react.default.useRef(null);
6860
+ const triggerRef = react.default.useRef(null);
6861
+ const listRef = react.default.useRef(null);
6862
+ const wasOpen = react.default.useRef(false);
6863
+ const selected = props.options[selectedIndex];
6864
+ react.default.useEffect(() => {
6865
+ if (!open) return;
6866
+ setActive(selectedIndex);
6867
+ const onPointerDown = (ev) => {
6868
+ const target = ev.target;
6869
+ if (target instanceof Node && rootRef.current?.contains(target) === true) return;
6870
+ setOpen(false);
6871
+ };
6872
+ document.addEventListener("pointerdown", onPointerDown);
6873
+ return () => document.removeEventListener("pointerdown", onPointerDown);
6874
+ }, [open, selectedIndex]);
6875
+ react.default.useEffect(() => {
6876
+ if (open) {
6877
+ listRef.current?.focus();
6878
+ wasOpen.current = true;
6879
+ return;
6880
+ }
6881
+ if (wasOpen.current) {
6882
+ triggerRef.current?.focus();
6883
+ wasOpen.current = false;
6884
+ }
6885
+ }, [open]);
6886
+ react.default.useEffect(() => {
6887
+ if (!open) return;
6888
+ document.getElementById(props.id + "-opt-" + String(active))?.scrollIntoView({ block: "nearest" });
6889
+ }, [
6890
+ active,
6891
+ open,
6892
+ props.id
6893
+ ]);
6894
+ const choose = (value) => {
6895
+ props.onChange(value);
6896
+ setOpen(false);
6897
+ };
6898
+ const move = (next) => {
6899
+ if (props.options.length === 0) return;
6900
+ setActive(Math.min(props.options.length - 1, Math.max(0, next)));
6901
+ };
6902
+ const onTriggerKeyDown = (ev) => {
6903
+ if (ev.key === "ArrowDown" || ev.key === "ArrowUp" || ev.key === "Enter" || ev.key === " ") {
6904
+ ev.preventDefault();
6905
+ setOpen(true);
6906
+ }
6907
+ };
6908
+ const onListKeyDown = (ev) => {
6909
+ if (ev.key === "ArrowDown") {
6910
+ ev.preventDefault();
6911
+ move(active + 1);
6912
+ return;
6913
+ }
6914
+ if (ev.key === "ArrowUp") {
6915
+ ev.preventDefault();
6916
+ move(active - 1);
6917
+ return;
6918
+ }
6919
+ if (ev.key === "Home") {
6920
+ ev.preventDefault();
6921
+ move(0);
6922
+ return;
6923
+ }
6924
+ if (ev.key === "End") {
6925
+ ev.preventDefault();
6926
+ move(props.options.length - 1);
6927
+ return;
6928
+ }
6929
+ if (ev.key === "Enter" || ev.key === " ") {
6930
+ ev.preventDefault();
6931
+ const option = props.options[active];
6932
+ if (option !== void 0) choose(option.value);
6933
+ return;
6934
+ }
6935
+ if (ev.key === "Escape" || ev.key === "Tab") setOpen(false);
6936
+ };
6937
+ return react.default.createElement("div", {
6938
+ className: "aag-select",
6939
+ ref: rootRef
6940
+ }, react.default.createElement("button", {
6941
+ id: props.id,
6942
+ ref: triggerRef,
6943
+ type: "button",
6944
+ className: "aag-select-trigger",
6945
+ "aria-haspopup": "listbox",
6946
+ "aria-expanded": open,
6947
+ "aria-controls": props.id + "-list",
6948
+ onClick: () => setOpen((current) => !current),
6949
+ onKeyDown: onTriggerKeyDown
6950
+ }, react.default.createElement("span", { className: "aag-select-value" }, selected === void 0 ? "" : selected.label), react.default.createElement("svg", {
6951
+ className: "aag-select-caret",
6952
+ viewBox: "0 0 12 12",
6953
+ "aria-hidden": true,
6954
+ focusable: false
6955
+ }, react.default.createElement("path", {
6956
+ d: "M2.5 4.5L6 8l3.5-3.5",
6957
+ fill: "none",
6958
+ stroke: "currentColor",
6959
+ strokeWidth: "1.5",
6960
+ strokeLinecap: "round",
6961
+ strokeLinejoin: "round"
6962
+ }))), open ? react.default.createElement("div", {
6963
+ id: props.id + "-list",
6964
+ ref: listRef,
6965
+ className: "aag-select-menu",
6966
+ role: "listbox",
6967
+ tabIndex: 0,
6968
+ "aria-activedescendant": props.id + "-opt-" + String(active),
6969
+ onKeyDown: onListKeyDown
6970
+ }, props.options.map((option, index) => react.default.createElement("button", {
6971
+ key: option.value === "" ? "all" : option.value,
6972
+ id: props.id + "-opt-" + String(index),
6973
+ type: "button",
6974
+ role: "option",
6975
+ className: "aag-select-option",
6976
+ "aria-selected": option.value === props.value,
6977
+ "data-active": index === active,
6978
+ onMouseEnter: () => setActive(index),
6979
+ onClick: () => choose(option.value)
6980
+ }, option.label))) : null);
6981
+ }
6809
6982
  function AgentsSettings(props) {
6810
6983
  const [state, setState] = react.default.useState(null);
6811
6984
  const [error, setError] = react.default.useState(null);
6985
+ const [query, setQuery] = react.default.useState("");
6986
+ const [division, setDivision] = react.default.useState("");
6812
6987
  const saving = react.default.useRef(false);
6813
6988
  const [isSaving, setIsSaving] = react.default.useState(false);
6814
6989
  const load = react.default.useCallback(() => {
@@ -6876,9 +7051,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6876
7051
  className: "aag-note"
6877
7052
  }, props.t("settings.loading")));
6878
7053
  else {
6879
- const groups = groupByDivision(EXPERTS, props.getActive());
7054
+ const filtered = filterExperts(EXPERTS, {
7055
+ query,
7056
+ division
7057
+ });
7058
+ const groups = groupByDivision(filtered, props.getActive());
6880
7059
  const enabledCount = [...state.enabled].filter((slug) => EXPERTS.some((e) => e.slug === slug)).length;
6881
7060
  const total = EXPERTS.length;
7061
+ const filteredCount = filtered.length;
7062
+ const hasFilter = normalizeExpertQuery(query) !== "" || division !== "";
7063
+ const resetFilters = () => {
7064
+ setQuery("");
7065
+ setDivision("");
7066
+ };
6882
7067
  nodes.push(react.default.createElement("div", {
6883
7068
  key: "toolbar",
6884
7069
  className: "aag-toolbar"
@@ -6891,7 +7076,59 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6891
7076
  nodes.push(react.default.createElement("div", {
6892
7077
  key: "summary",
6893
7078
  className: "aag-summary"
6894
- }, react.default.createElement("span", { className: "aag-summary-count" }, total), react.default.createElement("span", { className: "aag-summary-label" }, props.t(total === 1 ? "summary.total.one" : "summary.total.other", { count: total })), react.default.createElement("span", { className: "aag-summary-separator" }), react.default.createElement("span", { className: "aag-summary-count" }, enabledCount), react.default.createElement("span", { className: "aag-summary-label" }, props.t(enabledCount === 1 ? "summary.enabled.one" : "summary.enabled.other", { count: enabledCount }))));
7079
+ }, react.default.createElement("span", { className: "aag-summary-count" }, total), react.default.createElement("span", { className: "aag-summary-label" }, props.t(total === 1 ? "summary.total.one" : "summary.total.other", { count: total })), react.default.createElement("span", { className: "aag-summary-separator" }), react.default.createElement("span", { className: "aag-summary-count" }, enabledCount), react.default.createElement("span", { className: "aag-summary-label" }, props.t(enabledCount === 1 ? "summary.enabled.one" : "summary.enabled.other", { count: enabledCount })), hasFilter ? react.default.createElement("span", {
7080
+ className: "aag-filter-meta",
7081
+ "aria-live": "polite"
7082
+ }, props.t(filteredCount === 1 ? "settings.filter.showing.one" : "settings.filter.showing.other", { count: filteredCount })) : null));
7083
+ nodes.push(react.default.createElement("div", {
7084
+ key: "filters",
7085
+ className: "aag-filters"
7086
+ }, react.default.createElement("div", { className: "aag-field aag-field-category" }, react.default.createElement("label", {
7087
+ className: "aag-label",
7088
+ htmlFor: "aag-filter-category"
7089
+ }, props.t("settings.filter.category")), react.default.createElement(CategorySelect, {
7090
+ id: "aag-filter-category",
7091
+ value: division,
7092
+ onChange: setDivision,
7093
+ options: [{
7094
+ value: "",
7095
+ label: props.t("settings.filter.option", {
7096
+ name: props.t("settings.filter.all"),
7097
+ count: total
7098
+ })
7099
+ }, ...DIVISION_ORDER.map((key) => ({
7100
+ value: key,
7101
+ label: props.t("settings.filter.option", {
7102
+ name: props.t(`division.${key}`),
7103
+ count: DIVISION_COUNTS[key] ?? 0
7104
+ })
7105
+ }))]
7106
+ })), react.default.createElement("div", { className: "aag-field aag-field-search" }, react.default.createElement("label", {
7107
+ className: "aag-label",
7108
+ htmlFor: "aag-filter-search"
7109
+ }, props.t("settings.search")), react.default.createElement("div", { className: "aag-search-wrap" }, react.default.createElement("input", {
7110
+ id: "aag-filter-search",
7111
+ className: "aag-control aag-search",
7112
+ type: "search",
7113
+ value: query,
7114
+ autoComplete: "off",
7115
+ spellCheck: false,
7116
+ placeholder: props.t("settings.search.placeholder"),
7117
+ onChange: (ev) => setQuery(ev.currentTarget.value)
7118
+ }), query !== "" ? react.default.createElement("button", {
7119
+ type: "button",
7120
+ className: "aag-search-clear",
7121
+ "aria-label": props.t("settings.search.clear"),
7122
+ onClick: () => setQuery("")
7123
+ }, "×") : null))));
7124
+ if (groups.length === 0) nodes.push(react.default.createElement("div", {
7125
+ key: "empty",
7126
+ className: "aag-empty"
7127
+ }, react.default.createElement("div", null, props.t("settings.empty", { all: props.t("settings.filter.all") })), react.default.createElement("button", {
7128
+ type: "button",
7129
+ className: "aag-action aag-action-secondary",
7130
+ onClick: resetFilters
7131
+ }, props.t("settings.empty.reset"))));
6895
7132
  for (const g of groups) nodes.push(react.default.createElement("div", {
6896
7133
  key: g.division,
6897
7134
  className: "aag-group"
@@ -6990,7 +7227,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6990
7227
  exports.applyExpertSummon = applyExpertSummon;
6991
7228
  exports.buildSummonInstruction = buildSummonInstruction;
6992
7229
  exports.compareExpertName = compareExpertName;
7230
+ exports.filterExperts = filterExperts;
6993
7231
  exports.inject = inject;
7232
+ exports.matchExpertQuery = matchExpertQuery;
7233
+ exports.normalizeExpertQuery = normalizeExpertQuery;
6994
7234
  exports.writeErrorKey = writeErrorKey;
6995
7235
  exports.writeErrorMessage = writeErrorMessage;
6996
7236
  return module.exports;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@michengai/dsh-agency-agents",
3
3
  "description": "Agency Experts — a summonable domain-expert roster for DSH (expert mode)",
4
- "version": "0.1.14",
4
+ "version": "0.1.16",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",