dsh-capability-panel 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -45,13 +45,20 @@ function parseSkillEntry(value) {
45
45
  const state = value["state"];
46
46
  if (state !== "loaded" && state !== "pruned" && state !== "evicted" && state !== "unloaded") return null;
47
47
  if (typeof value["enabled"] !== "boolean" || typeof value["loadCount"] !== "number") return null;
48
+ if (typeof value["source"] !== "string" || typeof value["provider"] !== "string") return null;
48
49
  const description = optString(value["description"]);
50
+ const path = optString(value["path"]);
51
+ const group = optString(value["group"]);
49
52
  return {
50
53
  name: value["name"],
51
54
  state,
52
55
  enabled: value["enabled"],
53
56
  loadCount: value["loadCount"],
54
- ...description === void 0 ? {} : { description }
57
+ source: value["source"],
58
+ provider: value["provider"],
59
+ ...description === void 0 ? {} : { description },
60
+ ...path === void 0 ? {} : { path },
61
+ ...group === void 0 ? {} : { group }
55
62
  };
56
63
  }
57
64
  function parseToolEntry(value) {
@@ -78,10 +85,14 @@ function parseMcpServerEntry(value) {
78
85
  if (parsed === null) return null;
79
86
  tools.push(parsed);
80
87
  }
88
+ const source = optString(value["source"]);
89
+ const path = optString(value["path"]);
81
90
  return {
82
91
  server: value["server"],
83
92
  enabled: value["enabled"],
84
- tools
93
+ tools,
94
+ ...source === void 0 ? {} : { source },
95
+ ...path === void 0 ? {} : { path }
85
96
  };
86
97
  }
87
98
  function parseBlocked(value) {
@@ -161,7 +172,7 @@ function close() {
161
172
  open: false
162
173
  });
163
174
  }
164
- const ROUTE = "/api/capability-panel";
175
+ const ROUTE$1 = "/api/capability-panel";
165
176
  /**
166
177
  * Reads and writes have different ordering domains. A newer refresh supersedes
167
178
  * an older refresh, but it cannot invalidate a user's mutation response.
@@ -196,7 +207,7 @@ function finishRequest(requestEpoch, patch) {
196
207
  }
197
208
  async function requestPayload$1(sessionId, init) {
198
209
  const query = sessionId === null ? "" : `?session=${encodeURIComponent(sessionId)}`;
199
- const response = await fetch(`${ROUTE}${query}`, {
210
+ const response = await fetch(`${ROUTE$1}${query}`, {
200
211
  credentials: "same-origin",
201
212
  ...init
202
213
  });
@@ -282,10 +293,11 @@ function filterCapabilities(source, rawQuery) {
282
293
  const skills = source.skills.filter((skill) => hit(query, [
283
294
  skill.name,
284
295
  skill.description,
285
- skill.stateLabel
296
+ skill.stateLabel,
297
+ skill.sourceLabel
286
298
  ]));
287
299
  const mcp = source.mcp.flatMap((server) => {
288
- if (hit(query, [server.server])) return [server];
300
+ if (hit(query, [server.server, server.source])) return [server];
289
301
  const tools = server.tools.filter((tool) => hit(query, [
290
302
  tool.label,
291
303
  tool.name,
@@ -311,11 +323,12 @@ function filterCapabilities(source, rawQuery) {
311
323
 
312
324
  //#endregion
313
325
  //#region src/client/filter.ts
314
- function filterPayload(payload, rawQuery, stateLabel = (skill) => skill.state) {
326
+ function filterPayload(payload, rawQuery, stateLabel = (skill) => skill.state, sourceLabel = (skill) => skill.source) {
315
327
  if (rawQuery.trim() === "") return filterCapabilities(payload, rawQuery);
316
328
  const skills = payload.skills.map((skill) => ({
317
329
  ...skill,
318
- stateLabel: stateLabel(skill)
330
+ stateLabel: stateLabel(skill),
331
+ sourceLabel: sourceLabel(skill)
319
332
  }));
320
333
  return filterCapabilities({
321
334
  ...payload,
@@ -409,7 +422,17 @@ const zh = {
409
422
  "preset.noTools": "这个 preset 没有可用能力。",
410
423
  "preset.reserved": "{name} 是保留的传输通道,不能关闭。",
411
424
  "preset.broken": "这个 preset 无法组装会话:{reason}。修好它之后才能列出工具。",
412
- "degraded.item": "⚠ 部分读取失败:{note}"
425
+ "degraded.item": "⚠ 部分读取失败:{note}",
426
+ "source.project-dsh": "项目 .dsh",
427
+ "source.project-agents": "项目 agent",
428
+ "source.runtime": "运行时",
429
+ "source.user-dsh": "用户 .dsh",
430
+ "source.user-agents": "用户 agent",
431
+ "source.custom": "自定义",
432
+ "source.bundled": "内置",
433
+ "source.host": "全局",
434
+ "source.preset": "预设",
435
+ "source.openFolder": "在文件管理器中打开 {source}"
413
436
  };
414
437
  const en = {
415
438
  "state.loaded": "loaded",
@@ -459,7 +482,17 @@ const en = {
459
482
  "preset.noTools": "This preset exposes no capabilities.",
460
483
  "preset.reserved": "{name} is a reserved transport and cannot be disabled.",
461
484
  "preset.broken": "This preset cannot compose a session: {reason}. Fix it before its tools can be listed.",
462
- "degraded.item": "⚠ Partial read failed: {note}"
485
+ "degraded.item": "⚠ Partial read failed: {note}",
486
+ "source.project-dsh": "project .dsh",
487
+ "source.project-agents": "project agent",
488
+ "source.runtime": "runtime",
489
+ "source.user-dsh": "user .dsh",
490
+ "source.user-agents": "user agent",
491
+ "source.custom": "custom",
492
+ "source.bundled": "bundled",
493
+ "source.host": "global",
494
+ "source.preset": "preset",
495
+ "source.openFolder": "Open {source} in file manager"
463
496
  };
464
497
  /**
465
498
  * Register both dictionaries as one effect: the single-locale form is the
@@ -482,6 +515,7 @@ const TOK = {
482
515
  textSecondary: "var(--dsw-alias-label-secondary, #61666b)",
483
516
  textTertiary: "var(--dsw-alias-label-tertiary, #81858c)",
484
517
  border: "var(--dsw-alias-border-l1, rgba(0,0,0,.04))",
518
+ borderStrong: "var(--dsw-alias-border-l2, rgba(0,0,0,.1))",
485
519
  switchOn: "var(--dsw-alias-state-business-primary, #4176e6)",
486
520
  switchOff: "var(--dsw-alias-border-l2, rgba(0,0,0,.1))",
487
521
  switchThumb: "var(--dsw-alias-bg-layer-1, #ffffff)",
@@ -533,31 +567,30 @@ const PANEL_CSS = [
533
567
  ".ci-preset-filter{flex:1 1 220px;min-width:180px;height:34px;box-sizing:border-box;padding:0 10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px;background-color:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-weight:400}",
534
568
  ".ci-preset-part{margin:18px 0 0}",
535
569
  ".ci-preset-part:first-of-type{margin-top:10px}",
536
- ".ci-preset-part-title{margin:0;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,#81858c)}",
570
+ ".ci-preset-part-title{margin:0;font-size:14px;font-weight:600;line-height:22px;color:var(--dsw-alias-label-primary,#0f1115)}",
537
571
  ".ci-preset-badge{display:inline-block;margin-left:6px;padding:1px 6px;line-height:1.5;border-radius:999px;font-size:11px;font-weight:500;vertical-align:middle;background-color:var(--dsw-alias-bg-fill-2,rgba(0,0,0,.05));color:var(--dsw-alias-label-tertiary,#81858c)}",
538
572
  ".ci-preset-group{padding:0}",
539
573
  ".ci-preset-server-trigger{display:flex;align-items:center;gap:8px;flex:1;min-width:0;background:none;border:none;padding:0;text-align:left;font:inherit;color:inherit;cursor:pointer}",
540
574
  ".ci-preset-server-trigger:disabled{cursor:default}",
541
- ".ci-preset-group .ci-preset-tool-list{margin:0;padding-left:26px;border-top:none}",
542
- ".ci-preset-group .ci-preset-tool-row:last-child{border-bottom:none}",
575
+ ".ci-preset-group .ci-preset-tool-list{margin:0;padding-left:26px}",
543
576
  ".ci-preset-picker-label{display:grid;gap:6px;color:var(--dsw-alias-label-primary,#0f1115);font-weight:600}",
544
577
  ".ci-preset-picker{height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px;background-color:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115);font:inherit;font-weight:400}",
545
578
  ".ci-preset-picker:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:1px}",
546
579
  ".ci-settings-subtitle{margin:0 0 6px;color:var(--dsw-alias-label-primary,#0f1115);font-size:14px;line-height:22px}",
547
- ".ci-preset-tool-list{list-style:none;margin:0;padding:0;border-top:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.04))}",
548
- ".ci-preset-tool-row{display:flex;align-items:center;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.04))}",
580
+ ".ci-preset-tool-list{list-style:none;margin:0;padding:0}",
581
+ ".ci-preset-tool-row{display:flex;align-items:center;gap:16px}",
549
582
  ".ci-preset-tool-copy{display:grid;min-width:0;flex:1}",
550
583
  ".ci-preset-tool-name{color:var(--dsw-alias-label-primary,#0f1115);font-weight:600;overflow-wrap:anywhere}",
551
584
  ".ci-preset-tool-description{color:var(--dsw-alias-label-tertiary,#81858c);line-height:18px;overflow-wrap:anywhere}",
552
- ".ci-preset-item{list-style:none;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.04))}",
553
- ".ci-preset-item:last-child{border-bottom:none}",
554
- ".ci-preset-item .ci-preset-tool-row{border-bottom:none}",
585
+ ".ci-preset-item{list-style:none}",
555
586
  ".ci-preset-disclosure{display:block}",
556
587
  ".ci-preset-spacer{width:18px;flex:none}",
557
588
  ".ci-preset-detail{padding:0 0 10px 24px;color:var(--dsw-alias-label-tertiary,#81858c);line-height:18px;overflow-wrap:anywhere}",
558
589
  ".ci-preset-kinds{margin:0 0 4px}",
559
590
  ".ci-preset-kinds .ci-tabs{display:inline-flex}",
560
591
  ".ci-preset-kinds .ci-tab{flex:0 0 auto;padding:0 12px}",
592
+ ".ci-source-header:hover .ci-folder-icon{opacity:1 !important}",
593
+ ".ci-source-divider{list-style:none}",
561
594
  "@media (prefers-reduced-motion: reduce){.ci-thumb,.ci-panel,.ci-collapse,.ci-chevron svg{transition:none !important}}"
562
595
  ].join("\n");
563
596
 
@@ -4560,15 +4593,21 @@ function parseSkills(raw) {
4560
4593
  const skills = [];
4561
4594
  for (const rawSkill of raw) {
4562
4595
  if (!isRecord(rawSkill)) return null;
4563
- const { name, description, enabled, project } = rawSkill;
4596
+ const { name, description, enabled, project, source, path, group } = rawSkill;
4564
4597
  if (typeof name !== "string" || typeof enabled !== "boolean") return null;
4565
4598
  if (description !== void 0 && typeof description !== "string") return null;
4566
4599
  if (project !== void 0 && typeof project !== "boolean") return null;
4600
+ if (source !== void 0 && typeof source !== "string") return null;
4601
+ if (path !== void 0 && typeof path !== "string") return null;
4602
+ if (group !== void 0 && typeof group !== "string") return null;
4567
4603
  skills.push({
4568
4604
  name,
4569
4605
  ...description === void 0 ? {} : { description },
4570
4606
  enabled,
4571
- ...project === void 0 ? {} : { project }
4607
+ ...project === void 0 ? {} : { project },
4608
+ ...source === void 0 ? {} : { source },
4609
+ ...path === void 0 ? {} : { path },
4610
+ ...group === void 0 ? {} : { group }
4572
4611
  });
4573
4612
  }
4574
4613
  return skills;
@@ -4593,14 +4632,18 @@ function parsePresetToolPayload(value) {
4593
4632
  const mcp = [];
4594
4633
  for (const rawServer of rawMcp) {
4595
4634
  if (!isRecord(rawServer)) return null;
4596
- const { server, tools: rawTools, enabled } = rawServer;
4635
+ const { server, tools: rawTools, enabled, source, path } = rawServer;
4597
4636
  if (typeof server !== "string" || typeof enabled !== "boolean" || !Array.isArray(rawTools)) return null;
4637
+ if (source !== void 0 && typeof source !== "string") return null;
4638
+ if (path !== void 0 && typeof path !== "string") return null;
4598
4639
  const tools = parseTools(rawTools);
4599
4640
  if (tools === null) return null;
4600
4641
  mcp.push({
4601
4642
  server,
4602
4643
  tools,
4603
- enabled
4644
+ enabled,
4645
+ ...source === void 0 ? {} : { source },
4646
+ ...path === void 0 ? {} : { path }
4604
4647
  });
4605
4648
  }
4606
4649
  presets.push({
@@ -4748,6 +4791,28 @@ function resetPresetTools() {
4748
4791
 
4749
4792
  //#endregion
4750
4793
  //#region src/client/preset-section.ts
4794
+ /** Middle ellipsis for long path labels; the full path stays in the tooltip. */
4795
+ const ellipsizeMiddle = (text, max$1 = 56) => {
4796
+ if (text.length <= max$1) return text;
4797
+ const head = Math.ceil((max$1 - 1) / 2);
4798
+ const tail = Math.floor((max$1 - 1) / 2);
4799
+ return `${text.slice(0, head)}…${text.slice(text.length - tail)}`;
4800
+ };
4801
+ /** Group items by a key, preserving first-seen order. */
4802
+ function groupBy(items, keyOf) {
4803
+ const groups = /* @__PURE__ */ new Map();
4804
+ const order = [];
4805
+ for (const item of items) {
4806
+ const key = keyOf(item);
4807
+ const bucket = groups.get(key);
4808
+ if (bucket !== void 0) bucket.push(item);
4809
+ else {
4810
+ groups.set(key, [item]);
4811
+ order.push(key);
4812
+ }
4813
+ }
4814
+ return order.map((key) => [key, groups.get(key)]);
4815
+ }
4751
4816
  /**
4752
4817
  * Settings-side twin of the composer panel. It reuses that panel's vocabulary
4753
4818
  * and interaction — an always-visible filter, MCP tools collapsed under their
@@ -4778,6 +4843,101 @@ function PresetToolSection(props) {
4778
4843
  label,
4779
4844
  onCheckedChange: onChange
4780
4845
  });
4846
+ /**
4847
+ * Open a source folder from the settings page. The route resolves
4848
+ * user/project/host/preset sources without a session (project sources
4849
+ * against the dsh process's own cwd, matching how this page lists them).
4850
+ */
4851
+ const openSourceFolder = (source) => {
4852
+ fetch("/api/capability-panel/open-folder", {
4853
+ method: "POST",
4854
+ credentials: "same-origin",
4855
+ headers: { "content-type": "application/json" },
4856
+ body: JSON.stringify({ source })
4857
+ }).then((response) => {
4858
+ if (!response.ok) console.warn(`[capability-panel] cannot open source folder (${response.status})`);
4859
+ }).catch((error) => {
4860
+ console.warn("[capability-panel] open source folder request failed", error);
4861
+ });
4862
+ };
4863
+ /** Sources the open-folder route can resolve without a session. */
4864
+ const SESSIONLESS_OPENABLE = new Set([
4865
+ "user-dsh",
4866
+ "user-agents",
4867
+ "project-dsh",
4868
+ "project-agents",
4869
+ "host"
4870
+ ]);
4871
+ /**
4872
+ * A ruled source divider inside a settings list — the same visual language
4873
+ * as the session panel's group headers (`── label (count) ────`). Preset
4874
+ * groups name their preset; other groups show the directory (ellipsized);
4875
+ * openable groups offer the hover folder icon and a click-through.
4876
+ */
4877
+ const sourceDivider = (groupKey, items, first) => {
4878
+ const presetName = groupKey.startsWith("preset:") ? groupKey.slice(7) : void 0;
4879
+ const path = items.find((item) => item.path !== void 0)?.path;
4880
+ const translated = t(`source.${groupKey}`);
4881
+ const fallbackLabel = translated === `source.${groupKey}` ? groupKey : translated;
4882
+ const label = presetName ?? (path !== void 0 ? ellipsizeMiddle(path) : groupKey === "host" ? t("source.host") : fallbackLabel);
4883
+ const openSource = presetName ?? groupKey;
4884
+ const openable = presetName !== void 0 || SESSIONLESS_OPENABLE.has(groupKey);
4885
+ const rule = (grow) => react.createElement("span", { style: grow ? {
4886
+ flex: "1",
4887
+ height: "1px",
4888
+ background: TOK.borderStrong
4889
+ } : {
4890
+ flex: "none",
4891
+ width: "16px",
4892
+ height: "1px",
4893
+ background: TOK.borderStrong
4894
+ } });
4895
+ return react.createElement("li", {
4896
+ key: `source:${groupKey}`,
4897
+ className: "ci-source-divider"
4898
+ }, react.createElement("div", {
4899
+ className: "ci-source-header",
4900
+ style: {
4901
+ display: "flex",
4902
+ alignItems: "center",
4903
+ gap: "8px",
4904
+ margin: first ? "6px 0 2px" : "18px 0 2px"
4905
+ }
4906
+ }, rule(false), react.createElement("span", {
4907
+ style: {
4908
+ flex: "none",
4909
+ display: "inline-flex",
4910
+ alignItems: "center",
4911
+ gap: "4px",
4912
+ fontSize: "12px",
4913
+ fontWeight: 500,
4914
+ color: TOK.textTertiary,
4915
+ fontVariantNumeric: "tabular-nums",
4916
+ cursor: openable ? "pointer" : "default"
4917
+ },
4918
+ title: openable ? t("source.openFolder", { source: path ?? label }) : void 0,
4919
+ onClick: openable ? () => {
4920
+ openSourceFolder(openSource);
4921
+ } : void 0
4922
+ }, openable ? react.createElement("span", {
4923
+ className: "ci-folder-icon",
4924
+ style: {
4925
+ display: "inline-grid",
4926
+ placeItems: "center",
4927
+ opacity: 0,
4928
+ transition: "opacity 0.15s"
4929
+ }
4930
+ }, react.createElement("svg", {
4931
+ width: "12",
4932
+ height: "12",
4933
+ viewBox: "0 0 24 24",
4934
+ fill: "none",
4935
+ stroke: "currentColor",
4936
+ "stroke-width": "2",
4937
+ "stroke-linecap": "round",
4938
+ "stroke-linejoin": "round"
4939
+ }, react.createElement("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" }))) : null, `${label} (${items.length})`), rule(true)));
4940
+ };
4781
4941
  const toggle$1 = (tool, presetId) => switchFor(tool.enabled, tool.reserved === true, tool.reserved === true ? t("preset.reserved", { name: tool.label }) : t(tool.enabled ? "action.disable" : "action.enable", { name: tool.label }), (checked) => {
4782
4942
  setPresetTool(presetId, tool.name, checked);
4783
4943
  });
@@ -4869,8 +5029,8 @@ function PresetToolSection(props) {
4869
5029
  group("skills", t("group.skills", {
4870
5030
  shown: view.skills.length,
4871
5031
  total: totals.skills
4872
- }), view.skills.map((skill) => skillRow(skill, selected.id)));
4873
- const serverRows = view.mcp.map((server) => {
5032
+ }), groupBy(view.skills, (skill) => skill.group ?? skill.source ?? "unknown").flatMap(([groupKey, items], i) => [sourceDivider(groupKey, items, i === 0), ...items.map((skill) => skillRow(skill, selected.id))]));
5033
+ const serverRow = (server) => {
4874
5034
  const key = `mcp:${server.server}`;
4875
5035
  const disclosure = resolveDisclosure(expanded[key] === true, filtering);
4876
5036
  return react.createElement("li", {
@@ -4893,11 +5053,11 @@ function PresetToolSection(props) {
4893
5053
  }, chevronIcon), react.createElement("span", { className: "ci-preset-tool-copy" }, react.createElement("span", { className: "ci-preset-tool-name" }, server.server), react.createElement("span", { className: "ci-preset-tool-description" }, server.tools.length === 1 ? t("server.tool.one") : t("server.tools", { count: server.tools.length })))), switchFor(server.enabled, false, t(server.enabled ? "action.disable" : "action.enable", { name: server.server }), (checked) => {
4894
5054
  setPresetServer(selected.id, server.server, checked);
4895
5055
  })), react.createElement(CollapsiblePanel, { className: "ci-collapse" }, react.createElement("ul", { className: "ci-preset-tool-list" }, ...server.tools.map((tool) => toolRow(tool, selected.id, true))))));
4896
- });
5056
+ };
4897
5057
  group("mcp", t("group.mcp", {
4898
5058
  shown: view.mcp.length,
4899
5059
  total: totals.mcp
4900
- }), serverRows);
5060
+ }), groupBy(view.mcp, (server) => server.source === void 0 || server.source === "host" ? "host" : `preset:${server.source}`).flatMap(([groupKey, items], i) => [sourceDivider(groupKey, items, i === 0), ...items.map((server) => serverRow(server))]));
4901
5061
  group("system", t("group.system", {
4902
5062
  shown: view.systemTools.length,
4903
5063
  total: totals.systemTools
@@ -10631,6 +10791,7 @@ const PopoverPopup = /* @__PURE__ */ react.forwardRef(function PopoverPopup$1(co
10631
10791
 
10632
10792
  //#endregion
10633
10793
  //#region src/client/index.ts
10794
+ const ROUTE = "/api/capability-panel";
10634
10795
  /**
10635
10796
  * Order by how much the reader needs to act on it: what fell out of context
10636
10797
  * first, then what is in it, then the rest.
@@ -10704,7 +10865,121 @@ function apply(ctx) {
10704
10865
  const normalizedQuery = query.trim();
10705
10866
  const filtering = normalizedQuery !== "";
10706
10867
  const payload = snap.payload;
10707
- const view = payload === null ? null : filterPayload(payload, normalizedQuery, (skill) => t(`state.${skill.state}`));
10868
+ const skillSourceLabel = (skill) => {
10869
+ const key = `source.${skill.source}`;
10870
+ const translated = t(key);
10871
+ return translated === key ? skill.source : translated;
10872
+ };
10873
+ const openSourceFolder = (source) => {
10874
+ if (sessionId === null) return;
10875
+ fetch(`${ROUTE}/open-folder`, {
10876
+ method: "POST",
10877
+ credentials: "same-origin",
10878
+ headers: { "content-type": "application/json" },
10879
+ body: JSON.stringify({
10880
+ sessionId,
10881
+ source
10882
+ })
10883
+ }).then(async (response) => {
10884
+ if (!response.ok) {
10885
+ const detail = await response.text();
10886
+ console.warn(`[capability-panel] cannot open source folder (${response.status}): ${detail}`);
10887
+ }
10888
+ }).catch((error) => {
10889
+ console.warn("[capability-panel] open source folder request failed", error);
10890
+ });
10891
+ };
10892
+ /**
10893
+ * Middle ellipsis for long path labels: both ends carry the meaning
10894
+ * (`~/…` context, the tail directory), the middle is the expendable
10895
+ * part. The full path stays in the hover tooltip.
10896
+ */
10897
+ const ellipsizeMiddle$1 = (text, max$1 = 42) => {
10898
+ if (text.length <= max$1) return text;
10899
+ const head = Math.ceil((max$1 - 1) / 2);
10900
+ const tail = Math.floor((max$1 - 1) / 2);
10901
+ return `${text.slice(0, head)}…${text.slice(text.length - tail)}`;
10902
+ };
10903
+ /**
10904
+ * Section header for a source group, rendered as a labeled rule:
10905
+ * `── 📁 ~/.agents/skills (4) ────────────`. Label rule: preset-bundled
10906
+ * entries name their preset; every other group shows its real directory
10907
+ * (host-abbreviated `~`/cwd-relative, middle-ellipsized when long), and
10908
+ * only groups without a filesystem location fall back to the translated
10909
+ * source name. `groupKey` is the grouping identity; `rawSource` is what
10910
+ * the open-folder route resolves by.
10911
+ */
10912
+ const sourceSectionHeader = (groupKey, rawSource, count, first, path, openable = false) => {
10913
+ const label = groupKey.startsWith("preset:") ? groupKey.slice(7) : path !== void 0 ? ellipsizeMiddle$1(path) : groupKey === "host" ? t("source.host") : skillSourceLabel({ source: groupKey });
10914
+ const rule = (grow) => h("span", { style: grow ? {
10915
+ flex: "1",
10916
+ height: "1px",
10917
+ background: TOK.borderStrong
10918
+ } : {
10919
+ flex: "none",
10920
+ width: "16px",
10921
+ height: "1px",
10922
+ background: TOK.borderStrong
10923
+ } });
10924
+ return h("div", {
10925
+ key: `source:${groupKey}`,
10926
+ className: "ci-source-header",
10927
+ style: {
10928
+ display: "flex",
10929
+ alignItems: "center",
10930
+ gap: "8px",
10931
+ margin: first ? "0 0 2px" : "10px 0 2px"
10932
+ }
10933
+ }, rule(false), h("span", {
10934
+ style: {
10935
+ flex: "none",
10936
+ display: "inline-flex",
10937
+ alignItems: "center",
10938
+ gap: "4px",
10939
+ fontWeight: 500,
10940
+ color: TOK.textTertiary,
10941
+ fontVariantNumeric: "tabular-nums",
10942
+ cursor: openable ? "pointer" : "default"
10943
+ },
10944
+ title: openable ? t("source.openFolder", { source: path ?? label }) : void 0,
10945
+ onClick: openable ? () => {
10946
+ openSourceFolder(rawSource);
10947
+ } : void 0
10948
+ }, openable ? h("span", {
10949
+ className: "ci-folder-icon",
10950
+ style: {
10951
+ display: "inline-grid",
10952
+ placeItems: "center",
10953
+ opacity: 0,
10954
+ transition: "opacity 0.15s"
10955
+ }
10956
+ }, h("svg", {
10957
+ width: "12",
10958
+ height: "12",
10959
+ viewBox: "0 0 24 24",
10960
+ fill: "none",
10961
+ stroke: "currentColor",
10962
+ "stroke-width": "2",
10963
+ "stroke-linecap": "round",
10964
+ "stroke-linejoin": "round"
10965
+ }, h("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" }))) : null, `${label} (${count})`), rule(true));
10966
+ };
10967
+ /** Group items by source, preserving order within each group. */
10968
+ const groupBySource = (items, getSource) => {
10969
+ const groups = /* @__PURE__ */ new Map();
10970
+ const order = [];
10971
+ for (const item of items) {
10972
+ const source = getSource(item);
10973
+ const bucket = groups.get(source);
10974
+ if (bucket) bucket.push(item);
10975
+ else {
10976
+ groups.set(source, [item]);
10977
+ order.push(source);
10978
+ }
10979
+ }
10980
+ return order.map((source) => [source, groups.get(source)]);
10981
+ };
10982
+ const view = payload === null ? null : filterPayload(payload, normalizedQuery, (skill) => t(`state.${skill.state}`), skillSourceLabel);
10708
10983
  const skills = view === null ? [] : sortSkills(view.skills);
10709
10984
  const mcp = view?.mcp ?? [];
10710
10985
  const systemTools = view?.systemTools ?? [];
@@ -10769,7 +11044,7 @@ function apply(ctx) {
10769
11044
  margin: first ? "0 0 2px" : "12px 0 2px"
10770
11045
  }
10771
11046
  }, text);
10772
- const nameText = (text) => h("span", {
11047
+ const nameText = (text, sourceLabel) => h("span", {
10773
11048
  className: "ci-name",
10774
11049
  style: {
10775
11050
  flex: "1 1 auto",
@@ -10777,7 +11052,12 @@ function apply(ctx) {
10777
11052
  color: TOK.textPrimary,
10778
11053
  fontWeight: 500
10779
11054
  }
10780
- }, text);
11055
+ }, text, sourceLabel !== void 0 ? h("span", { style: {
11056
+ color: TOK.textTertiary,
11057
+ fontWeight: 400,
11058
+ fontSize: "11px",
11059
+ marginLeft: "2px"
11060
+ } }, `· ${sourceLabel}`) : null);
10781
11061
  /** The trigger's accessible name, localized with its subject. */
10782
11062
  const disclosureAria = (subject, detailKey, disclosure) => {
10783
11063
  const detail = t(detailKey);
@@ -10798,7 +11078,7 @@ function apply(ctx) {
10798
11078
  * trigger owns only chevron + label; trailing actions remain independent
10799
11079
  * buttons and never toggle the description.
10800
11080
  */
10801
- const disclosureRow$1 = (key, enabled, label, description, actions, className = ROW_ROOT_CLASS) => {
11081
+ const disclosureRow$1 = (key, enabled, label, description, actions, className = ROW_ROOT_CLASS, sourceLabel) => {
10802
11082
  const hasDescription = description !== void 0 && description !== "";
10803
11083
  const disclosure = resolveDisclosure(expanded[key] === true, filtering);
10804
11084
  const ariaLabel = disclosureAria(label, "detail.description", disclosure);
@@ -10809,7 +11089,7 @@ function apply(ctx) {
10809
11089
  }, h("div", { className: ROW_HEADER_CLASS }, h("span", { style: {
10810
11090
  width: "18px",
10811
11091
  flex: "none"
10812
- } }), nameText(label), ...actions));
11092
+ } }), nameText(label, sourceLabel), ...actions));
10813
11093
  return h(CollapsibleRoot, {
10814
11094
  key,
10815
11095
  open: disclosure.open,
@@ -10825,18 +11105,23 @@ function apply(ctx) {
10825
11105
  }, h("span", {
10826
11106
  className: "ci-chevron",
10827
11107
  "aria-hidden": true
10828
- }, chevronIcon$1), nameText(label)), ...actions), h(CollapsiblePanel, { className: "ci-collapse" }, h("div", { className: "ci-description" }, description)));
11108
+ }, chevronIcon$1), nameText(label, sourceLabel)), ...actions), h(CollapsiblePanel, { className: "ci-collapse" }, h("div", { className: "ci-description" }, description)));
10829
11109
  };
10830
11110
  /**
10831
11111
  * Put a skill's slash command into the composer — never auto-submit:
10832
11112
  * whether to send is the user's call (Enter). A non-empty draft is
10833
11113
  * appended to, never replaced. Works for disabled skills too: the
10834
11114
  * disable shadow keeps userInvocable: true by design.
11115
+ *
11116
+ * The draft comes from the live input-state selector: the old
11117
+ * `props.input` snapshot is gone in newer hosts, and reading a stale
11118
+ * snapshot would REPLACE the user's draft instead of appending.
10835
11119
  */
11120
+ const composerDraft = props.useInput === void 0 ? props.input?.draft ?? "" : props.useInput((state) => state.draft);
10836
11121
  const insertCommand = (name) => {
10837
11122
  const actions = props.inputActions;
10838
11123
  if (actions === void 0) return;
10839
- const draft = props.input?.draft ?? "";
11124
+ const draft = composerDraft;
10840
11125
  actions.setDraft(draft.trim() === "" ? `/${name} ` : `${draft} /${name} `);
10841
11126
  close();
10842
11127
  };
@@ -10868,7 +11153,7 @@ function apply(ctx) {
10868
11153
  insertButton(skill.name),
10869
11154
  blockedChip(blocked[skill.name] ?? 0),
10870
11155
  switchControl("skill", skill.name, skill.enabled)
10871
- ]);
11156
+ ], ROW_ROOT_CLASS);
10872
11157
  const mcpToolRow = (tool, serverEnabled) => disclosureRow$1(`mcp-tool:${tool.name}`, tool.enabled, tool.label, tool.description, [blockedChip(blocked[tool.name] ?? 0), serverEnabled ? switchControl("mcp-tool", tool.name, tool.enabled) : null], MCP_TOOL_ROOT_CLASS);
10873
11158
  const serverRow = (server) => {
10874
11159
  const serverBlocked = server.tools.reduce((sum, tool) => sum + (blocked[tool.name] ?? 0), 0);
@@ -10981,7 +11266,7 @@ function apply(ctx) {
10981
11266
  value: "system",
10982
11267
  className: "ci-tab",
10983
11268
  "aria-label": t("tab.system.aria", { count: totals.systemTools })
10984
- }, `${t("tab.system")} ${totals.systemTools}`)), h(TabsPanel, { value: "skills" }, skills.length === 0 && payload !== null && !snap.loading ? emptyNote(t("empty.skills")) : h("div", {}, ...skills.map(skillRow))), h(TabsPanel, { value: "mcp" }, mcp.length === 0 && payload !== null && !snap.loading ? emptyNote(t("empty.mcp")) : h("div", {}, ...mcp.map(serverRow))), h(TabsPanel, { value: "system" }, systemTools.length === 0 && payload !== null && !snap.loading ? emptyNote(t("empty.system")) : h("div", {}, ...systemTools.map(systemRow))))];
11269
+ }, `${t("tab.system")} ${totals.systemTools}`)), h(TabsPanel, { value: "skills" }, skills.length === 0 && payload !== null && !snap.loading ? emptyNote(t("empty.skills")) : h("div", {}, ...groupBySource(skills, (s) => s.group ?? s.source).flatMap(([groupKey, items], i) => [sourceSectionHeader(groupKey, items[0].source, items.length, i === 0, items.find((item) => item.path !== void 0)?.path, items.some((item) => item.path !== void 0)), ...items.map(skillRow)]))), h(TabsPanel, { value: "mcp" }, mcp.length === 0 && payload !== null && !snap.loading ? emptyNote(t("empty.mcp")) : h("div", {}, ...groupBySource(mcp, (s) => s.source === void 0 || s.source === "host" ? "host" : `preset:${s.source}`).flatMap(([groupKey, items], i) => [sourceSectionHeader(groupKey, items[0].source ?? "host", items.length, i === 0, items.find((item) => item.path !== void 0)?.path, true), ...items.map(serverRow)]))), h(TabsPanel, { value: "system" }, systemTools.length === 0 && payload !== null && !snap.loading ? emptyNote(t("empty.system")) : h("div", {}, ...systemTools.map(systemRow))))];
10985
11270
  return h(PopoverRoot, {
10986
11271
  open: snap.open,
10987
11272
  onOpenChange: syncOpen
@@ -11018,7 +11303,7 @@ function apply(ctx) {
11018
11303
  "aria-label": t("panel.aria"),
11019
11304
  style: {
11020
11305
  boxSizing: "border-box",
11021
- width: "360px",
11306
+ width: "480px",
11022
11307
  maxHeight: "min(60vh, var(--available-height, 60vh))",
11023
11308
  overflowY: "auto",
11024
11309
  padding: "10px 12px",