@daweifu/capability-menu 0.1.3 → 0.1.4
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.en.md +24 -14
- package/README.md +24 -14
- package/lib/client.d.ts +29 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +497 -92
- package/lib/invoke.js +72 -2
- package/lib/invoke.js.map +1 -1
- package/lib/policy.js +30 -6
- package/lib/policy.js.map +1 -1
- package/lib/registry.js +23 -4
- package/lib/registry.js.map +1 -1
- package/lib/server/remote.js +27 -0
- package/lib/server/remote.js.map +1 -1
- package/lib/types/invoke.d.ts.map +1 -1
- package/lib/types/policy.d.ts +6 -0
- package/lib/types/policy.d.ts.map +1 -1
- package/lib/types/registry.d.ts +23 -0
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/server/remote.d.ts +18 -0
- package/lib/types/server/remote.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -5240,6 +5240,7 @@ const capabilityRow$schema = object({
|
|
|
5240
5240
|
name: string().readonly(),
|
|
5241
5241
|
server: string().optional().readonly(),
|
|
5242
5242
|
source: string().optional().readonly(),
|
|
5243
|
+
preset: string().optional().readonly(),
|
|
5243
5244
|
class: union([
|
|
5244
5245
|
literal("resident"),
|
|
5245
5246
|
literal("on-demand"),
|
|
@@ -5273,7 +5274,8 @@ const toolDetail$schema = object({
|
|
|
5273
5274
|
provider: string().readonly(),
|
|
5274
5275
|
serverName: string().optional().readonly(),
|
|
5275
5276
|
path: string().optional().readonly(),
|
|
5276
|
-
source: string().optional().readonly()
|
|
5277
|
+
source: string().optional().readonly(),
|
|
5278
|
+
preset: string().optional().readonly()
|
|
5277
5279
|
}).readonly(),
|
|
5278
5280
|
tags: array(string()).readonly(),
|
|
5279
5281
|
stats: object({
|
|
@@ -5762,6 +5764,24 @@ const TYPERT_REMOTE = {
|
|
|
5762
5764
|
line: 264,
|
|
5763
5765
|
column: 3
|
|
5764
5766
|
}
|
|
5767
|
+
},
|
|
5768
|
+
{
|
|
5769
|
+
id: "@daweifu/capability-menu#capabilityPolicy/catalogVersion",
|
|
5770
|
+
service: "capabilityPolicy",
|
|
5771
|
+
namespace: "capabilityPolicy",
|
|
5772
|
+
method: "catalogVersion",
|
|
5773
|
+
invocation: { kind: "direct" },
|
|
5774
|
+
parameters: [],
|
|
5775
|
+
result: {
|
|
5776
|
+
mode: "strict",
|
|
5777
|
+
typeSymbol: "number",
|
|
5778
|
+
schema: number()
|
|
5779
|
+
},
|
|
5780
|
+
sourceLocation: {
|
|
5781
|
+
file: "src/server/remote.ts",
|
|
5782
|
+
line: 317,
|
|
5783
|
+
column: 3
|
|
5784
|
+
}
|
|
5765
5785
|
}
|
|
5766
5786
|
]
|
|
5767
5787
|
};
|
|
@@ -6361,6 +6381,219 @@ const PROJECT_SKILL_SOURCES = /* @__PURE__ */ new Set(["project-dsh", "project-a
|
|
|
6361
6381
|
*/
|
|
6362
6382
|
const ADOPTABLE_SKILL_SOURCES = /* @__PURE__ */ new Set(["user-agents", "custom"]);
|
|
6363
6383
|
//#endregion
|
|
6384
|
+
//#region src/client/skillGroups.ts
|
|
6385
|
+
/**
|
|
6386
|
+
* What the capability list decides about *which* rows it shows, kept out of
|
|
6387
|
+
* `CapabilitySection.tsx` as pure functions.
|
|
6388
|
+
*
|
|
6389
|
+
* The split is deliberate: the grouping rules are where this plugin got its one
|
|
6390
|
+
* real mis-grouping (preset skills filed under 全局技能), and they are also the
|
|
6391
|
+
* only part of the client a test can reach — this repo has no DOM harness, so
|
|
6392
|
+
* anything that lives inside the component can only be checked by hand.
|
|
6393
|
+
*/
|
|
6394
|
+
/** Group tools by server name; skills stay flat (no server), both name-sorted. */
|
|
6395
|
+
function groupRows(rows) {
|
|
6396
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
6397
|
+
const skills = [];
|
|
6398
|
+
for (const row of rows) {
|
|
6399
|
+
if (row.kind === "skill") {
|
|
6400
|
+
skills.push(row);
|
|
6401
|
+
continue;
|
|
6402
|
+
}
|
|
6403
|
+
const server = row.server ?? "built-in";
|
|
6404
|
+
const list = byServer.get(server);
|
|
6405
|
+
if (list === void 0) byServer.set(server, [row]);
|
|
6406
|
+
else list.push(row);
|
|
6407
|
+
}
|
|
6408
|
+
return {
|
|
6409
|
+
servers: [...byServer.entries()].map(([server, tools]) => ({
|
|
6410
|
+
server,
|
|
6411
|
+
tools: [...tools].sort((a, b) => a.name.localeCompare(b.name))
|
|
6412
|
+
})).sort((a, b) => a.server.localeCompare(b.server)),
|
|
6413
|
+
skills: [...skills].sort((a, b) => a.name.localeCompare(b.name))
|
|
6414
|
+
};
|
|
6415
|
+
}
|
|
6416
|
+
/**
|
|
6417
|
+
* Compile the box. A `/…/` wrapper is optional — it is stripped when present, so
|
|
6418
|
+
* both `cordis|ptc` and `/cordis|ptc/` are read as patterns — and it keeps the
|
|
6419
|
+
* common case of pasting a slash-delimited pattern from working.
|
|
6420
|
+
*/
|
|
6421
|
+
function compileNameFilter(input) {
|
|
6422
|
+
const raw = input.trim();
|
|
6423
|
+
const needle = raw.toLowerCase();
|
|
6424
|
+
if (raw === "") return {
|
|
6425
|
+
needle,
|
|
6426
|
+
regex: false,
|
|
6427
|
+
test: () => true
|
|
6428
|
+
};
|
|
6429
|
+
const pattern = raw.length > 1 && raw.startsWith("/") && raw.endsWith("/") ? raw.slice(1, -1) : raw;
|
|
6430
|
+
try {
|
|
6431
|
+
const compiled = new RegExp(pattern, "i");
|
|
6432
|
+
return {
|
|
6433
|
+
needle,
|
|
6434
|
+
regex: true,
|
|
6435
|
+
test: (values) => values.some((v) => v !== void 0 && compiled.test(v))
|
|
6436
|
+
};
|
|
6437
|
+
} catch {
|
|
6438
|
+
return {
|
|
6439
|
+
needle,
|
|
6440
|
+
regex: false,
|
|
6441
|
+
test: (values) => values.some((v) => v !== void 0 && v.toLowerCase().includes(needle))
|
|
6442
|
+
};
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
/**
|
|
6446
|
+
* Everything a row is matched against: its own name, plus the dimensions that
|
|
6447
|
+
* name the group it sits in — the MCP server (or the label the built-in group is
|
|
6448
|
+
* drawn with), the skill's source root, and the preset that ships it.
|
|
6449
|
+
*
|
|
6450
|
+
* Group headings are labels rather than row fields, which is exactly why typing
|
|
6451
|
+
* 系统 used to find nothing: the server id is `built-in`, and 系统内置 is only how
|
|
6452
|
+
* that id is painted. Both spellings are searchable now.
|
|
6453
|
+
*/
|
|
6454
|
+
function searchableText(row, labels = {}) {
|
|
6455
|
+
const values = [row.name];
|
|
6456
|
+
if (row.server !== void 0) {
|
|
6457
|
+
values.push(row.server);
|
|
6458
|
+
if (row.server === "built-in") values.push(labels.builtIn);
|
|
6459
|
+
}
|
|
6460
|
+
if (row.source !== void 0) values.push(row.source, labels.source?.(row.source));
|
|
6461
|
+
values.push(row.preset);
|
|
6462
|
+
return values.filter((v) => v !== void 0 && v !== "");
|
|
6463
|
+
}
|
|
6464
|
+
/**
|
|
6465
|
+
* Narrow a list by the filter. Applied *before* grouping so a server header's
|
|
6466
|
+
* own counts describe the rows actually listed under it.
|
|
6467
|
+
*/
|
|
6468
|
+
function filterRows(rows, filter, labels = {}) {
|
|
6469
|
+
if (filter.needle === "") return [...rows];
|
|
6470
|
+
return rows.filter((row) => filter.test(searchableText(row, labels)));
|
|
6471
|
+
}
|
|
6472
|
+
/**
|
|
6473
|
+
* Split skill rows across the three sub-tabs.
|
|
6474
|
+
*
|
|
6475
|
+
* Preset skills are split off by their `preset` field and never by `source`: a
|
|
6476
|
+
* preset's own `skill-filesystem` labels its skills `custom`, exactly like a
|
|
6477
|
+
* user-configured `customSkillDirs` entry, so a source-based rule drags both
|
|
6478
|
+
* into 全局技能 — filing a preset asset as global is precisely the mis-grouping
|
|
6479
|
+
* this split exists to fix. Rows keep the order they arrive in, so callers that
|
|
6480
|
+
* pass name-sorted rows get name-sorted groups.
|
|
6481
|
+
*/
|
|
6482
|
+
function splitSkillGroups(skills) {
|
|
6483
|
+
const presetSkills = [];
|
|
6484
|
+
const projectSkills = [];
|
|
6485
|
+
const globalSkills = [];
|
|
6486
|
+
const presetGroups = [];
|
|
6487
|
+
for (const skill of skills) {
|
|
6488
|
+
const preset = skill.preset;
|
|
6489
|
+
if (preset !== void 0) {
|
|
6490
|
+
presetSkills.push(skill);
|
|
6491
|
+
const group = presetGroups.find(([id]) => id === preset);
|
|
6492
|
+
if (group === void 0) presetGroups.push([preset, [skill]]);
|
|
6493
|
+
else group[1].push(skill);
|
|
6494
|
+
continue;
|
|
6495
|
+
}
|
|
6496
|
+
if (PROJECT_SKILL_SOURCES.has(skill.source ?? "")) projectSkills.push(skill);
|
|
6497
|
+
else globalSkills.push(skill);
|
|
6498
|
+
}
|
|
6499
|
+
presetGroups.sort(([a], [b]) => a.localeCompare(b));
|
|
6500
|
+
return {
|
|
6501
|
+
presetSkills,
|
|
6502
|
+
presetGroups,
|
|
6503
|
+
projectSkills,
|
|
6504
|
+
globalSkills
|
|
6505
|
+
};
|
|
6506
|
+
}
|
|
6507
|
+
/**
|
|
6508
|
+
* The sub-tab the Skills panel actually shows: the requested one, unless it is
|
|
6509
|
+
* 预设技能 while no preset ships skills. That tab does not exist then, and a
|
|
6510
|
+
* selection left on it must not survive — deployed presets are re-scanned on
|
|
6511
|
+
* every refresh, so the tab can disappear under the operator.
|
|
6512
|
+
*/
|
|
6513
|
+
function resolveSkillTab(selected, groups) {
|
|
6514
|
+
return selected === "preset" && groups.presetSkills.length === 0 ? "global" : selected;
|
|
6515
|
+
}
|
|
6516
|
+
/** Rows in one tier. Counts the *displayed* rows, not the whole catalog. */
|
|
6517
|
+
function countByClass(rows, cls) {
|
|
6518
|
+
return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
|
|
6519
|
+
}
|
|
6520
|
+
//#endregion
|
|
6521
|
+
//#region src/client/revalidate.ts
|
|
6522
|
+
/**
|
|
6523
|
+
* When to re-read the catalog, and how often to look — the decisions behind the
|
|
6524
|
+
* management page keeping itself fresh, kept as pure functions.
|
|
6525
|
+
*
|
|
6526
|
+
* Wiring lives in `CapabilitySection.tsx` (timers, the `visibilitychange`
|
|
6527
|
+
* listener, the two subscriptions); everything that could be got wrong lives
|
|
6528
|
+
* here, where a test can reach it. This repo has no DOM harness.
|
|
6529
|
+
*/
|
|
6530
|
+
/**
|
|
6531
|
+
* How often a visible, idle page asks the host for its catalog version.
|
|
6532
|
+
*
|
|
6533
|
+
* This is the resolution of the one case nothing on the wire announces — a file
|
|
6534
|
+
* dropped into an already-registered skill directory — so it doubles as "how
|
|
6535
|
+
* long will I stare at a stale list after an agent changes something next to
|
|
6536
|
+
* me". 5s keeps that from being annoying; the cost is one tiny request per tick,
|
|
6537
|
+
* and only while this settings page is on screen.
|
|
6538
|
+
*/
|
|
6539
|
+
const POLL_INTERVAL_MS = 5e3;
|
|
6540
|
+
/** Ceiling for the failure backoff, so a long outage does not spin. */
|
|
6541
|
+
const POLL_BACKOFF_MAX_MS = 6e4;
|
|
6542
|
+
/**
|
|
6543
|
+
* How long one version sample may take before it is treated as a failure.
|
|
6544
|
+
*
|
|
6545
|
+
* Without this, a request that neither resolves nor rejects would end the poll
|
|
6546
|
+
* for good: the next tick is only scheduled after the current one settles, and
|
|
6547
|
+
* nothing else restarts the effect while the page stays visible and idle.
|
|
6548
|
+
*/
|
|
6549
|
+
const POLL_SAMPLE_TIMEOUT_MS = 15e3;
|
|
6550
|
+
/**
|
|
6551
|
+
* Delay before the next version poll.
|
|
6552
|
+
*
|
|
6553
|
+
* `failures` is the run of consecutive failures ending at this decision: 0
|
|
6554
|
+
* means the last sample succeeded and the plain interval applies, otherwise the
|
|
6555
|
+
* delay doubles until it hits the ceiling. This is what turns "the carrier
|
|
6556
|
+
* died" from a hot loop into a slow retry that heals itself when it comes back.
|
|
6557
|
+
*/
|
|
6558
|
+
function pollDelayMs(failures) {
|
|
6559
|
+
if (!Number.isFinite(failures) || failures <= 0) return POLL_INTERVAL_MS;
|
|
6560
|
+
return Math.min(POLL_INTERVAL_MS * 2 ** failures, POLL_BACKOFF_MAX_MS);
|
|
6561
|
+
}
|
|
6562
|
+
/**
|
|
6563
|
+
* Whether a version sample means the catalog moved under the page.
|
|
6564
|
+
*
|
|
6565
|
+
* Deliberately `!==` rather than `>`: the counter is in-process, so it restarts
|
|
6566
|
+
* from zero when the plugin reloads, and a plain "greater than" test would sit
|
|
6567
|
+
* there believing a fresh host was the same host. `undefined` (nothing sampled
|
|
6568
|
+
* yet) only records — the page has just read the snapshot, and re-reading it
|
|
6569
|
+
* because it has no baseline would be busywork.
|
|
6570
|
+
*/
|
|
6571
|
+
function versionChanged(seen, current) {
|
|
6572
|
+
return seen !== void 0 && seen !== current;
|
|
6573
|
+
}
|
|
6574
|
+
/**
|
|
6575
|
+
* The value of `work`, or a rejection if it has not settled within `ms`.
|
|
6576
|
+
*
|
|
6577
|
+
* Used so a carrier that accepts a request and then goes quiet degrades into
|
|
6578
|
+
* the same retry path as a carrier that refuses one.
|
|
6579
|
+
*/
|
|
6580
|
+
function withDeadline(work, ms) {
|
|
6581
|
+
return new Promise((resolve, reject) => {
|
|
6582
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`timed out after ${ms}ms`)), ms);
|
|
6583
|
+
work.then((value) => {
|
|
6584
|
+
clearTimeout(timer);
|
|
6585
|
+
resolve(value);
|
|
6586
|
+
}, (error) => {
|
|
6587
|
+
clearTimeout(timer);
|
|
6588
|
+
reject(error);
|
|
6589
|
+
});
|
|
6590
|
+
});
|
|
6591
|
+
}
|
|
6592
|
+
/** Whether a signal may act now, or has to be parked until the page is idle. */
|
|
6593
|
+
function canRevalidate(gate) {
|
|
6594
|
+
return gate.visible && gate.ready && !gate.busy && !gate.rebuilding;
|
|
6595
|
+
}
|
|
6596
|
+
//#endregion
|
|
6364
6597
|
//#region src/client/CapabilitySection.tsx
|
|
6365
6598
|
/**
|
|
6366
6599
|
* ⚠️ VERIFIED AGAINST REAL rc.8 CLIENT API.
|
|
@@ -6412,6 +6645,8 @@ const CSS = `
|
|
|
6412
6645
|
.mc-summary{display:flex;gap:12px;flex-wrap:wrap;justify-content:flex-end;align-items:center;padding-bottom:8px}
|
|
6413
6646
|
.mc-catalog-btn{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:20px;padding:0 10px;cursor:pointer;white-space:nowrap}
|
|
6414
6647
|
.mc-catalog-btn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
|
|
6648
|
+
/* 忙碌态靠变淡表示,而不是换文案:换文案会改按钮宽度,把这行挤到第二行。 */
|
|
6649
|
+
.mc-catalog-btn:disabled{cursor:default;opacity:.6}
|
|
6415
6650
|
.mc-catalog-tabs{padding:8px 16px 0}
|
|
6416
6651
|
.mc-catalog-path{padding:8px 16px 0;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);word-break:break-all}
|
|
6417
6652
|
.mc-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:20px;white-space:nowrap}
|
|
@@ -6435,6 +6670,15 @@ body[data-ds-dark-theme] .mc-chip--resident{color:#96b6d1}
|
|
|
6435
6670
|
body[data-ds-dark-theme] .mc-chip--on-demand{color:#d4b26b}
|
|
6436
6671
|
body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
|
|
6437
6672
|
.mc-tabs{border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;justify-content:space-between;gap:22px}
|
|
6673
|
+
/* 名字过滤框:两个 tab 共用一条,宽度占满,与下方列表左对齐。
|
|
6674
|
+
它属于上面的主 tab 栏,不属于下面的列表,所以两侧都把 .mc-section 的 12px
|
|
6675
|
+
栏距收掉一部分:下划线—6px—输入框—8px—全局/项目 子页签文字(后者里的 4px
|
|
6676
|
+
是子页签自己的 padding-top)。改之前是 12+12=24px 和 12+4=16px,读起来像
|
|
6677
|
+
两个不相干的块。 */
|
|
6678
|
+
.mc-filter{display:flex;padding-top:0;margin-top:-6px;margin-bottom:-8px}
|
|
6679
|
+
.mc-filter input{box-sizing:border-box;width:100%;min-width:0;padding:6px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit;font-size:13px;line-height:18px}
|
|
6680
|
+
.mc-filter input:hover{border-color:var(--dsw-alias-border-l3)}
|
|
6681
|
+
.mc-filter input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-color:transparent}
|
|
6438
6682
|
/* Skills sub-tab bar (全局技能/项目技能): same underline chrome, no right-side summary. */
|
|
6439
6683
|
.mc-subtabs{border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px}
|
|
6440
6684
|
.mc-tab-group{display:flex;align-items:flex-end;gap:22px}
|
|
@@ -6442,13 +6686,17 @@ body[data-ds-dark-theme] .mc-chip--disabled{color:#b8abad}
|
|
|
6442
6686
|
.mc-tab:hover,.mc-tab[data-active=true]{color:var(--dsw-alias-label-primary)}
|
|
6443
6687
|
.mc-tab[data-active=true]:after,.mc-tab:focus-visible:after{background:var(--dsw-alias-label-primary);content:"";border-radius:2px 2px 0 0;height:2px;position:absolute;bottom:-1px;left:0;right:0}
|
|
6444
6688
|
.mc-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary);border-radius:2px}
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6689
|
+
/* 面板的第一行必须和另一个 tab 的第一行从过滤框下沿出发走同样远,否则切 tab 时
|
|
6690
|
+
首行会跳一下。这里留 4px,加上 .mc-section 的 4px 栏距(过滤框那侧被 -8px 收
|
|
6691
|
+
掉后剩 4px)= 8px,正好是 Skills 面板子页签文字的位置(4px 栏距 + 子页签自己
|
|
6692
|
+
的 4px padding-top)。原来这里是 12px,Tools 的首个分组因此落在过滤框下 16px
|
|
6693
|
+
处,比「全局技能」那行低一半。 */
|
|
6694
|
+
.mc-panel{min-width:0;padding-top:4px}
|
|
6695
|
+
/* Skills panel: its first row is the 全局技能/项目技能 sub-tab bar, and that bar
|
|
6696
|
+
belongs to the header stack above it (rule → name filter → sub-tabs) rather
|
|
6697
|
+
than to the list below. So the panel adds no padding of its own, and the
|
|
6698
|
+
sub-tab labels keep a 4px top padding instead of the 7px they share with the
|
|
6699
|
+
primary tabs. */
|
|
6452
6700
|
.mc-panel--tight{padding-top:0}
|
|
6453
6701
|
.mc-panel--tight .mc-subtabs .mc-tab{padding-top:4px}
|
|
6454
6702
|
.mc-panel-inner{display:flex;flex-direction:column;gap:14px}
|
|
@@ -6508,6 +6756,9 @@ body[data-ds-dark-theme] .mc-count--disabled{color:#b8abad}
|
|
|
6508
6756
|
.mc-confirm-path{font-family:var(--dsw-font-markdown-code-block-font-family);word-break:break-all}
|
|
6509
6757
|
.mc-confirm-actions{display:flex;justify-content:flex-end;gap:8px}
|
|
6510
6758
|
.mc-skill-body{border-top:1px solid var(--dsw-alias-border-l1);padding:8px 12px 12px}
|
|
6759
|
+
/* 预设技能组:预设 id 是页签之下的分组小标题,不是新一级页签。 */
|
|
6760
|
+
.mc-skill-group{display:flex;flex-direction:column;gap:8px;min-width:0}
|
|
6761
|
+
.mc-skill-group-title{font-size:12px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-secondary);padding:0 2px}
|
|
6511
6762
|
.mc-tree{display:flex;flex-direction:column;gap:2px;font-size:13px;line-height:20px}
|
|
6512
6763
|
.mc-tree-row{display:flex;align-items:center;gap:8px;padding:3px 4px;border-radius:6px;cursor:pointer;min-width:0}
|
|
6513
6764
|
.mc-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -6555,32 +6806,22 @@ function sourceLabel(source, t) {
|
|
|
6555
6806
|
const key = SOURCE_LABEL_KEYS[source];
|
|
6556
6807
|
return key === void 0 ? source : t(key);
|
|
6557
6808
|
}
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
return {
|
|
6572
|
-
servers: [...byServer.entries()].map(([server, tools]) => ({
|
|
6573
|
-
server,
|
|
6574
|
-
tools: [...tools].sort((a, b) => a.name.localeCompare(b.name))
|
|
6575
|
-
})).sort((a, b) => a.server.localeCompare(b.server)),
|
|
6576
|
-
skills: [...skills].sort((a, b) => a.name.localeCompare(b.name))
|
|
6577
|
-
};
|
|
6578
|
-
}
|
|
6579
|
-
function countByClass(rows, cls) {
|
|
6580
|
-
return rows.reduce((n, row) => row.class === cls ? n + 1 : n, 0);
|
|
6809
|
+
/**
|
|
6810
|
+
* Whether the document is visible. A hidden settings tab does no polling and
|
|
6811
|
+
* takes no re-reads; coming back is itself a revalidation trigger.
|
|
6812
|
+
*/
|
|
6813
|
+
function useDocumentVisible() {
|
|
6814
|
+
const [visible, setVisible] = (0, react.useState)(true);
|
|
6815
|
+
(0, react.useEffect)(() => {
|
|
6816
|
+
const update = () => setVisible(document.visibilityState !== "hidden");
|
|
6817
|
+
update();
|
|
6818
|
+
document.addEventListener("visibilitychange", update);
|
|
6819
|
+
return () => document.removeEventListener("visibilitychange", update);
|
|
6820
|
+
}, []);
|
|
6821
|
+
return visible;
|
|
6581
6822
|
}
|
|
6582
6823
|
function CapabilitySection(props) {
|
|
6583
|
-
const { remote, t, mountError } = props;
|
|
6824
|
+
const { remote, t, mountError, subscribeSignals } = props;
|
|
6584
6825
|
const [state, setState] = (0, react.useState)(() => {
|
|
6585
6826
|
const cached = cachedSnapshot();
|
|
6586
6827
|
return cached === void 0 ? { status: "loading" } : {
|
|
@@ -6649,6 +6890,84 @@ function CapabilitySection(props) {
|
|
|
6649
6890
|
return next;
|
|
6650
6891
|
});
|
|
6651
6892
|
}, []);
|
|
6893
|
+
const visible = useDocumentVisible();
|
|
6894
|
+
const [pendingSignal, setPendingSignal] = (0, react.useState)(false);
|
|
6895
|
+
const seenVersion = (0, react.useRef)(void 0);
|
|
6896
|
+
const pollFailures = (0, react.useRef)(0);
|
|
6897
|
+
const gate = (0, react.useRef)({
|
|
6898
|
+
visible: true,
|
|
6899
|
+
ready: false,
|
|
6900
|
+
busy: false,
|
|
6901
|
+
rebuilding: false
|
|
6902
|
+
});
|
|
6903
|
+
(0, react.useEffect)(() => {
|
|
6904
|
+
gate.current = {
|
|
6905
|
+
visible,
|
|
6906
|
+
ready: state.status === "ready",
|
|
6907
|
+
busy,
|
|
6908
|
+
rebuilding: refreshing
|
|
6909
|
+
};
|
|
6910
|
+
});
|
|
6911
|
+
/**
|
|
6912
|
+
* A signal that the host catalog may have moved. Runs now when the page is
|
|
6913
|
+
* idle; otherwise parks until it is, so a signal that lands mid-click is
|
|
6914
|
+
* never dropped (and never repaints the row under the cursor).
|
|
6915
|
+
*/
|
|
6916
|
+
const onExternalSignal = (0, react.useCallback)(() => {
|
|
6917
|
+
if (canRevalidate(gate.current)) refreshCatalog();
|
|
6918
|
+
else setPendingSignal(true);
|
|
6919
|
+
}, [refreshCatalog]);
|
|
6920
|
+
(0, react.useEffect)(() => {
|
|
6921
|
+
return subscribeSignals?.(onExternalSignal);
|
|
6922
|
+
}, [subscribeSignals, onExternalSignal]);
|
|
6923
|
+
(0, react.useEffect)(() => {
|
|
6924
|
+
if (!pendingSignal) return;
|
|
6925
|
+
if (!canRevalidate({
|
|
6926
|
+
visible,
|
|
6927
|
+
ready: state.status === "ready",
|
|
6928
|
+
busy,
|
|
6929
|
+
rebuilding: refreshing
|
|
6930
|
+
})) return;
|
|
6931
|
+
setPendingSignal(false);
|
|
6932
|
+
refreshCatalog();
|
|
6933
|
+
}, [
|
|
6934
|
+
pendingSignal,
|
|
6935
|
+
visible,
|
|
6936
|
+
state.status,
|
|
6937
|
+
busy,
|
|
6938
|
+
refreshing,
|
|
6939
|
+
refreshCatalog
|
|
6940
|
+
]);
|
|
6941
|
+
(0, react.useEffect)(() => {
|
|
6942
|
+
if (!visible || state.status !== "ready" || busy || refreshing) return;
|
|
6943
|
+
let stopped = false;
|
|
6944
|
+
let timer;
|
|
6945
|
+
const tick = async () => {
|
|
6946
|
+
try {
|
|
6947
|
+
const version = unwrap(await withDeadline(remote.catalogVersion(), POLL_SAMPLE_TIMEOUT_MS), "capabilityPolicy.catalogVersion");
|
|
6948
|
+
pollFailures.current = 0;
|
|
6949
|
+
if (versionChanged(seenVersion.current, version)) {
|
|
6950
|
+
seenVersion.current = version;
|
|
6951
|
+
await reload();
|
|
6952
|
+
}
|
|
6953
|
+
} catch {
|
|
6954
|
+
pollFailures.current += 1;
|
|
6955
|
+
}
|
|
6956
|
+
if (!stopped) timer = setTimeout(() => void tick(), pollDelayMs(pollFailures.current));
|
|
6957
|
+
};
|
|
6958
|
+
timer = setTimeout(() => void tick(), 0);
|
|
6959
|
+
return () => {
|
|
6960
|
+
stopped = true;
|
|
6961
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
6962
|
+
};
|
|
6963
|
+
}, [
|
|
6964
|
+
visible,
|
|
6965
|
+
state.status,
|
|
6966
|
+
busy,
|
|
6967
|
+
refreshing,
|
|
6968
|
+
remote,
|
|
6969
|
+
reload
|
|
6970
|
+
]);
|
|
6652
6971
|
/** Move one or more capability ids to the next class in the click cycle. */
|
|
6653
6972
|
const cycleClass = (0, react.useCallback)(async (ids, kind) => {
|
|
6654
6973
|
if (ids.length === 0 || busy) return;
|
|
@@ -6744,19 +7063,31 @@ function CapabilitySection(props) {
|
|
|
6744
7063
|
onToggleServer: toggleServer,
|
|
6745
7064
|
onCycle: cycleClass,
|
|
6746
7065
|
onRefresh: () => void refreshCatalog(),
|
|
6747
|
-
refreshing,
|
|
6748
7066
|
onNotice: setNotice
|
|
6749
7067
|
})
|
|
6750
7068
|
]
|
|
6751
7069
|
});
|
|
6752
7070
|
}
|
|
6753
7071
|
function ReadyBody(props) {
|
|
6754
|
-
const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle, onRefresh,
|
|
6755
|
-
|
|
6756
|
-
const
|
|
6757
|
-
const
|
|
7072
|
+
const { remote, snapshot, openServers, busy, activeTab, t, onTabChange, onToggleServer, onCycle, onRefresh, onNotice } = props;
|
|
7073
|
+
/** Name filter, shared by both tabs: it narrows whichever list is shown. */
|
|
7074
|
+
const [filter, setFilter] = (0, react.useState)("");
|
|
7075
|
+
const nameFilter = compileNameFilter(filter);
|
|
7076
|
+
const needle = nameFilter.needle;
|
|
7077
|
+
const shownRows = filterRows(snapshot.rows, nameFilter, {
|
|
7078
|
+
builtIn: t("builtInGroup"),
|
|
7079
|
+
source: (source) => sourceLabel(source, t)
|
|
7080
|
+
});
|
|
7081
|
+
const { servers, skills } = groupRows(shownRows);
|
|
7082
|
+
const { presetSkills, presetGroups, projectSkills, globalSkills } = splitSkillGroups(skills);
|
|
6758
7083
|
const [skillTab, setSkillTab] = (0, react.useState)("global");
|
|
6759
|
-
const
|
|
7084
|
+
const activeSkillTab = resolveSkillTab(skillTab, {
|
|
7085
|
+
presetSkills,
|
|
7086
|
+
presetGroups,
|
|
7087
|
+
projectSkills,
|
|
7088
|
+
globalSkills
|
|
7089
|
+
});
|
|
7090
|
+
const statRows = activeTab === "tools" ? shownRows.filter((r) => r.kind === "tool") : activeSkillTab === "project" ? projectSkills : activeSkillTab === "preset" ? presetSkills : globalSkills;
|
|
6760
7091
|
const summary = CLASS_KEYS.map((cls) => ({
|
|
6761
7092
|
cls,
|
|
6762
7093
|
count: countByClass(statRows, cls)
|
|
@@ -6936,35 +7267,36 @@ function ReadyBody(props) {
|
|
|
6936
7267
|
})]
|
|
6937
7268
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6938
7269
|
className: "mc-summary",
|
|
6939
|
-
children: [
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
disabled: refreshing,
|
|
6957
|
-
children: refreshing ? t("refreshing") : t("refresh")
|
|
6958
|
-
}),
|
|
6959
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6960
|
-
type: "button",
|
|
6961
|
-
className: "mc-catalog-btn",
|
|
6962
|
-
onClick: () => setRegisterOpen(true),
|
|
6963
|
-
children: t("registerCapability")
|
|
6964
|
-
})
|
|
6965
|
-
]
|
|
7270
|
+
children: [summary.map(({ cls, count }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7271
|
+
className: `mc-chip mc-chip--${cls}`,
|
|
7272
|
+
children: [
|
|
7273
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7274
|
+
className: `mc-dot mc-dot--${cls}`,
|
|
7275
|
+
"aria-hidden": "true"
|
|
7276
|
+
}),
|
|
7277
|
+
t(CLASS_SHORT_KEYS[cls]),
|
|
7278
|
+
" · ",
|
|
7279
|
+
count
|
|
7280
|
+
]
|
|
7281
|
+
}, cls)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7282
|
+
type: "button",
|
|
7283
|
+
className: "mc-catalog-btn",
|
|
7284
|
+
onClick: () => setRegisterOpen(true),
|
|
7285
|
+
children: t("registerCapability")
|
|
7286
|
+
})]
|
|
6966
7287
|
})]
|
|
6967
7288
|
}),
|
|
7289
|
+
snapshot.rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7290
|
+
className: "mc-filter",
|
|
7291
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7292
|
+
type: "search",
|
|
7293
|
+
value: filter,
|
|
7294
|
+
placeholder: t("filterByName"),
|
|
7295
|
+
"aria-label": t("filterByName"),
|
|
7296
|
+
title: t("filterHint"),
|
|
7297
|
+
onChange: (e) => setFilter(e.target.value)
|
|
7298
|
+
})
|
|
7299
|
+
}),
|
|
6968
7300
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6969
7301
|
role: "tabpanel",
|
|
6970
7302
|
hidden: activeTab !== "tools",
|
|
@@ -6973,7 +7305,7 @@ function ReadyBody(props) {
|
|
|
6973
7305
|
className: "mc-panel-inner",
|
|
6974
7306
|
children: servers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6975
7307
|
className: "mc-empty",
|
|
6976
|
-
children: t("emptyTools")
|
|
7308
|
+
children: needle === "" ? t("emptyTools") : t("filterNoMatch")
|
|
6977
7309
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: servers.map(({ server, tools }) => {
|
|
6978
7310
|
const open = openServers.has(server);
|
|
6979
7311
|
const counts = CLASS_KEYS.map((cls) => ({
|
|
@@ -7095,32 +7427,44 @@ function ReadyBody(props) {
|
|
|
7095
7427
|
className: "mc-panel-inner",
|
|
7096
7428
|
children: skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7097
7429
|
className: "mc-empty",
|
|
7098
|
-
children: t("emptySkills")
|
|
7430
|
+
children: needle === "" ? t("emptySkills") : t("filterNoMatch")
|
|
7099
7431
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7100
7432
|
className: "mc-subtabs",
|
|
7101
7433
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7102
7434
|
className: "mc-tab-group",
|
|
7103
7435
|
role: "tablist",
|
|
7104
7436
|
"aria-label": t("skillsGroup"),
|
|
7105
|
-
children: [
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7437
|
+
children: [
|
|
7438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7439
|
+
type: "button",
|
|
7440
|
+
role: "tab",
|
|
7441
|
+
className: "mc-tab",
|
|
7442
|
+
"aria-selected": activeSkillTab === "global",
|
|
7443
|
+
"data-active": activeSkillTab === "global" ? "true" : void 0,
|
|
7444
|
+
onClick: () => setSkillTab("global"),
|
|
7445
|
+
children: t("globalSkills")
|
|
7446
|
+
}),
|
|
7447
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7448
|
+
type: "button",
|
|
7449
|
+
role: "tab",
|
|
7450
|
+
className: "mc-tab",
|
|
7451
|
+
"aria-selected": activeSkillTab === "project",
|
|
7452
|
+
"data-active": activeSkillTab === "project" ? "true" : void 0,
|
|
7453
|
+
onClick: () => setSkillTab("project"),
|
|
7454
|
+
children: t("projectSkills")
|
|
7455
|
+
}),
|
|
7456
|
+
presetSkills.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7457
|
+
type: "button",
|
|
7458
|
+
role: "tab",
|
|
7459
|
+
className: "mc-tab",
|
|
7460
|
+
"aria-selected": activeSkillTab === "preset",
|
|
7461
|
+
"data-active": activeSkillTab === "preset" ? "true" : void 0,
|
|
7462
|
+
onClick: () => setSkillTab("preset"),
|
|
7463
|
+
children: t("presetSkills")
|
|
7464
|
+
})
|
|
7465
|
+
]
|
|
7122
7466
|
})
|
|
7123
|
-
}),
|
|
7467
|
+
}), activeSkillTab === "global" ? globalSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
|
|
7124
7468
|
skills: globalSkills,
|
|
7125
7469
|
remote,
|
|
7126
7470
|
busy,
|
|
@@ -7132,7 +7476,7 @@ function ReadyBody(props) {
|
|
|
7132
7476
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7133
7477
|
className: "mc-empty",
|
|
7134
7478
|
children: t("emptyGlobalSkills")
|
|
7135
|
-
}) : projectSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
|
|
7479
|
+
}) : activeSkillTab === "project" ? projectSkills.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
|
|
7136
7480
|
skills: projectSkills,
|
|
7137
7481
|
remote,
|
|
7138
7482
|
busy,
|
|
@@ -7144,7 +7488,22 @@ function ReadyBody(props) {
|
|
|
7144
7488
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7145
7489
|
className: "mc-empty",
|
|
7146
7490
|
children: t("emptyProjectSkills")
|
|
7147
|
-
})]
|
|
7491
|
+
}) : presetGroups.map(([preset, group]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7492
|
+
className: "mc-skill-group",
|
|
7493
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7494
|
+
className: "mc-skill-group-title",
|
|
7495
|
+
children: preset
|
|
7496
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillList, {
|
|
7497
|
+
skills: group,
|
|
7498
|
+
remote,
|
|
7499
|
+
busy,
|
|
7500
|
+
t,
|
|
7501
|
+
onCycle,
|
|
7502
|
+
editableSkills,
|
|
7503
|
+
onEditSkill: openSkillEdit,
|
|
7504
|
+
onAdoptSkill: adoptSkill
|
|
7505
|
+
})]
|
|
7506
|
+
}, preset))] })
|
|
7148
7507
|
})
|
|
7149
7508
|
}),
|
|
7150
7509
|
toolDetail !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -7498,7 +7857,11 @@ function SkillList(props) {
|
|
|
7498
7857
|
"aria-hidden": "true"
|
|
7499
7858
|
}), t(CLASS_SHORT_KEYS[skill.class])]
|
|
7500
7859
|
}),
|
|
7501
|
-
!editableSkills.has(skill.id) && (
|
|
7860
|
+
!editableSkills.has(skill.id) && (skill.preset !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7861
|
+
className: "mc-source",
|
|
7862
|
+
title: t("skillFromPresetHint"),
|
|
7863
|
+
children: t("skillFromPreset", { preset: skill.preset })
|
|
7864
|
+
}) : ADOPTABLE_SKILL_SOURCES.has(skill.source ?? "") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7502
7865
|
type: "button",
|
|
7503
7866
|
className: "mc-count mc-count--action",
|
|
7504
7867
|
disabled: busy,
|
|
@@ -7638,8 +8001,12 @@ async function apply(ctx) {
|
|
|
7638
8001
|
builtInGroup: "系统内置",
|
|
7639
8002
|
globalSkills: "全局技能",
|
|
7640
8003
|
projectSkills: "项目技能",
|
|
8004
|
+
presetSkills: "预设技能",
|
|
7641
8005
|
emptyGlobalSkills: "暂无全局技能",
|
|
7642
8006
|
emptyProjectSkills: "暂无项目技能",
|
|
8007
|
+
filterByName: "按名字、分组或正则过滤…",
|
|
8008
|
+
filterHint: "忽略大小写。匹配工具/技能名、MCP server、技能来源标签与预设 id;也可以直接写正则,例如 cordis|ptc 或 /^mcp__dbx/。",
|
|
8009
|
+
filterNoMatch: "没有匹配的能力(清空过滤框可恢复完整列表)",
|
|
7643
8010
|
emptyTools: "暂无工具",
|
|
7644
8011
|
emptySkills: "暂无 Skill",
|
|
7645
8012
|
toolCount: "{count} 个工具",
|
|
@@ -7651,8 +8018,6 @@ async function apply(ctx) {
|
|
|
7651
8018
|
previewClose: "关闭",
|
|
7652
8019
|
detailNotFound: "未找到该工具的详情",
|
|
7653
8020
|
cycleOverridden: "分类未生效:{count} 个能力被更高优先级规则覆盖(如通配规则),可移除对应通配规则后重试",
|
|
7654
|
-
refresh: "刷新",
|
|
7655
|
-
refreshing: "刷新中…",
|
|
7656
8021
|
refreshFailed: "刷新失败,请查看日志",
|
|
7657
8022
|
retry: "重试",
|
|
7658
8023
|
carrierFailureHint: "请求未到达服务端:浏览器与 dsh 之间的连接已断开(Failed to fetch 属传输层失败,服务端方法未执行)。若刚重启过 dsh web,请先硬刷新页面(Ctrl/Cmd+Shift+R)再重试。",
|
|
@@ -7687,6 +8052,8 @@ async function apply(ctx) {
|
|
|
7687
8052
|
skillAdopted: "已纳入管理:{path}",
|
|
7688
8053
|
skillSource: "来源:{source}",
|
|
7689
8054
|
skillSourceHint: "该技能由 dsh 从其它根发现,不在本插件管理的技能目录里,因此没有「编辑」。",
|
|
8055
|
+
skillFromPreset: "来自预设 {preset}",
|
|
8056
|
+
skillFromPresetHint: "该技能随 agent preset 分发,只对挂载了该预设的会话可见;不能纳入管理(那会把它软链进全局技能根,对所有会话生效)。",
|
|
7690
8057
|
skillUnmanaged: "未纳入管理",
|
|
7691
8058
|
sourceCustom: "自定义技能目录",
|
|
7692
8059
|
sourceBundled: "随 dsh 预置",
|
|
@@ -7732,8 +8099,12 @@ async function apply(ctx) {
|
|
|
7732
8099
|
builtInGroup: "System built-in",
|
|
7733
8100
|
globalSkills: "Global skills",
|
|
7734
8101
|
projectSkills: "Project skills",
|
|
8102
|
+
presetSkills: "Preset skills",
|
|
7735
8103
|
emptyGlobalSkills: "No global skills",
|
|
7736
8104
|
emptyProjectSkills: "No project skills",
|
|
8105
|
+
filterByName: "Filter by name, group, or regex…",
|
|
8106
|
+
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/.",
|
|
8107
|
+
filterNoMatch: "No capability matches (clear the filter to see the full list)",
|
|
7737
8108
|
emptyTools: "No tools",
|
|
7738
8109
|
emptySkills: "No skills",
|
|
7739
8110
|
toolCount: "{count} tools",
|
|
@@ -7745,8 +8116,6 @@ async function apply(ctx) {
|
|
|
7745
8116
|
previewClose: "Close",
|
|
7746
8117
|
detailNotFound: "Tool detail not found",
|
|
7747
8118
|
cycleOverridden: "Classification not applied: {count} capability(ies) overridden by a higher-priority rule (e.g. a wildcard). Remove the matching wildcard rule and retry.",
|
|
7748
|
-
refresh: "Refresh",
|
|
7749
|
-
refreshing: "Refreshing…",
|
|
7750
8119
|
refreshFailed: "Refresh failed; see the log",
|
|
7751
8120
|
retry: "Retry",
|
|
7752
8121
|
carrierFailureHint: "The request never reached the server: the browser connection to dsh is broken (\"Failed to fetch\" is a transport failure, so no server method ran). If dsh web was just restarted, hard-refresh the page (Ctrl/Cmd+Shift+R) and retry.",
|
|
@@ -7781,6 +8150,8 @@ async function apply(ctx) {
|
|
|
7781
8150
|
skillAdopted: "Adopted: {path}",
|
|
7782
8151
|
skillSource: "Source: {source}",
|
|
7783
8152
|
skillSourceHint: "dsh found this skill in another root, not in a skill directory this plugin manages, so there is no Edit button.",
|
|
8153
|
+
skillFromPreset: "From preset {preset}",
|
|
8154
|
+
skillFromPresetHint: "This skill ships with an agent preset and is visible only to sessions that mount it. It cannot be adopted (adopting would link it into the global skill root, making it apply to every session).",
|
|
7784
8155
|
skillUnmanaged: "Not managed here",
|
|
7785
8156
|
sourceCustom: "custom skill dirs",
|
|
7786
8157
|
sourceBundled: "bundled with dsh",
|
|
@@ -7832,10 +8203,44 @@ async function apply(ctx) {
|
|
|
7832
8203
|
return;
|
|
7833
8204
|
}
|
|
7834
8205
|
};
|
|
8206
|
+
/**
|
|
8207
|
+
* The two signals that say "the host catalog may have moved, and this page did
|
|
8208
|
+
* not ask": a settings document edit, and a carrier reconnect.
|
|
8209
|
+
*
|
|
8210
|
+
* `settings/document-updated` is one of the events dsh forwards to browsers
|
|
8211
|
+
* (so it arrives over the same validated channel as the remote calls) and it
|
|
8212
|
+
* fires for external edits too — composition files are edited outside the
|
|
8213
|
+
* browser, and nothing else on the wire announces that. `connection/reset` is
|
|
8214
|
+
* the client's own reconnect signal rather than a forwarded one, which is why
|
|
8215
|
+
* it is a plain local `ctx.on`; dsh's own settings sections listen to exactly
|
|
8216
|
+
* this pair, including the first connection, which closes the window where a
|
|
8217
|
+
* change lands between the initial read and the subscription.
|
|
8218
|
+
*
|
|
8219
|
+
* Neither subscription is fatal: the section falls back to its version poll.
|
|
8220
|
+
*/
|
|
8221
|
+
const subscribeSignals = (listener) => {
|
|
8222
|
+
const disposers = [];
|
|
8223
|
+
const forwarded = ctx.remote;
|
|
8224
|
+
try {
|
|
8225
|
+
if (typeof forwarded.$on !== "function") throw new Error("ctx.remote.$on is unavailable");
|
|
8226
|
+
disposers.push(forwarded.$on("settings/document-updated", () => listener()));
|
|
8227
|
+
} catch (error) {
|
|
8228
|
+
console.error("[capability-menu] settings/document-updated subscription failed:", error);
|
|
8229
|
+
}
|
|
8230
|
+
try {
|
|
8231
|
+
disposers.push(ctx.on("connection/reset", () => listener()));
|
|
8232
|
+
} catch (error) {
|
|
8233
|
+
console.error("[capability-menu] connection/reset subscription failed:", error);
|
|
8234
|
+
}
|
|
8235
|
+
return () => {
|
|
8236
|
+
for (const dispose of disposers) dispose();
|
|
8237
|
+
};
|
|
8238
|
+
};
|
|
7835
8239
|
const injected = () => {
|
|
7836
8240
|
return {
|
|
7837
8241
|
remote: remote(),
|
|
7838
8242
|
t,
|
|
8243
|
+
subscribeSignals,
|
|
7839
8244
|
...mountError !== void 0 ? { mountError } : {}
|
|
7840
8245
|
};
|
|
7841
8246
|
};
|