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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { exec } from "node:child_process";
1
2
  import z from "@deepseek-ai/schemastery";
2
3
  import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
3
4
  import { homedir } from "node:os";
@@ -161,14 +162,14 @@ function createCapabilityController(ctx, appendStats, blockedCounts) {
161
162
  ctx.effect(() => guardDispose, "capability-panel: tool guard");
162
163
  };
163
164
  ensureGuard();
164
- ctx.on("tools/result", (exec, result) => {
165
- const agent = exec.agent;
165
+ ctx.on("tools/result", (exec$1, result) => {
166
+ const agent = exec$1.agent;
166
167
  if (agent === void 0 || typeof agent.id !== "string") return;
167
168
  const state = states.get(agent.id);
168
169
  if (state === void 0) return;
169
170
  const hit = classifyBlockedCall({
170
- name: exec.name,
171
- arguments: exec.arguments,
171
+ name: exec$1.name,
172
+ arguments: exec$1.arguments,
172
173
  agent,
173
174
  ...result.isError && result.error !== void 0 ? { error: result.error } : {}
174
175
  }, new Set(state.skills.keys()), disabledToolNames(state));
@@ -225,8 +226,8 @@ function createCapabilityController(ctx, appendStats, blockedCounts) {
225
226
  name: original.name,
226
227
  description: original.description,
227
228
  content: original.content,
228
- source: "custom",
229
- provider: "capability-panel",
229
+ source: typeof original.source === "string" ? original.source : "custom",
230
+ provider: typeof original.provider === "string" ? original.provider : "capability-panel",
230
231
  ...original.resourceBase === void 0 ? {} : { resourceBase: original.resourceBase },
231
232
  invocation: {
232
233
  modelInvocable: false,
@@ -336,8 +337,8 @@ function createCapabilityController(ctx, appendStats, blockedCounts) {
336
337
  name: original.name,
337
338
  description: original.description,
338
339
  content: original.content,
339
- source: "custom",
340
- provider: "capability-panel",
340
+ source: typeof original.source === "string" ? original.source : "custom",
341
+ provider: typeof original.provider === "string" ? original.provider : "capability-panel",
341
342
  ...original.resourceBase === void 0 ? {} : { resourceBase: original.resourceBase },
342
343
  invocation: {
343
344
  modelInvocable: false,
@@ -359,33 +360,60 @@ function createCapabilityController(ctx, appendStats, blockedCounts) {
359
360
  }
360
361
  if (maskedAny) ensurePromptNote(agent, ensureState());
361
362
  };
363
+ const mutationQueues = /* @__PURE__ */ new Map();
364
+ const enqueue = (sessionId, op) => {
365
+ const next = (mutationQueues.get(sessionId) ?? Promise.resolve()).then(op, op);
366
+ mutationQueues.set(sessionId, next.then(() => void 0, () => void 0));
367
+ return next;
368
+ };
369
+ const restoreImpl = async (sessionId, overrides) => {
370
+ const groups = [
371
+ ["skill", overrides.skills],
372
+ ["mcp-server", overrides.mcpServers],
373
+ ["mcp-tool", overrides.mcpTools],
374
+ ["system-tool", overrides.systemTools]
375
+ ];
376
+ for (const [kind, positions] of groups) for (const [name, enabled] of Object.entries(positions)) {
377
+ if (states.get(sessionId)?.userToggled.has(`${kind}:${name}`) === true) continue;
378
+ try {
379
+ if (kind === "skill") await setSkill(sessionId, name, enabled);
380
+ else if (kind === "mcp-server") setServer(sessionId, name, enabled);
381
+ else setTool(sessionId, name, enabled, kind === "system-tool");
382
+ } catch {}
383
+ }
384
+ const st = states.get(sessionId);
385
+ if (st !== void 0 && st.userToggled.size === 0 && st.skills.size === 0 && st.mcpServers.size === 0 && st.mcpTools.size === 0 && st.systemTools.size === 0) {
386
+ st.noteDispose?.();
387
+ delete st.noteDispose;
388
+ states.delete(sessionId);
389
+ }
390
+ };
362
391
  return {
363
392
  states,
364
393
  state: (sessionId) => states.get(sessionId),
365
- seed,
366
- async restore(sessionId, overrides) {
367
- const groups = [
368
- ["skill", overrides.skills],
369
- ["mcp-server", overrides.mcpServers],
370
- ["mcp-tool", overrides.mcpTools],
371
- ["system-tool", overrides.systemTools]
372
- ];
373
- for (const [kind, positions] of groups) for (const [name, enabled] of Object.entries(positions)) {
374
- if (states.get(sessionId)?.userToggled.has(`${kind}:${name}`) === true) continue;
375
- try {
376
- if (kind === "skill") await setSkill(sessionId, name, enabled);
377
- else if (kind === "mcp-server") setServer(sessionId, name, enabled);
378
- else setTool(sessionId, name, enabled, kind === "system-tool");
379
- } catch {}
380
- }
394
+ seed: (sessionId, defaults) => enqueue(sessionId, () => seed(sessionId, defaults)),
395
+ restore: (sessionId, overrides) => enqueue(sessionId, () => restoreImpl(sessionId, overrides)),
396
+ reseed: (sessionId, defaults, overrides) => enqueue(sessionId, async () => {
381
397
  const st = states.get(sessionId);
382
- if (st !== void 0 && st.userToggled.size === 0 && st.skills.size === 0 && st.mcpServers.size === 0 && st.mcpTools.size === 0 && st.systemTools.size === 0) {
398
+ if (st !== void 0) {
399
+ for (const map of [
400
+ st.systemTools,
401
+ st.mcpServers,
402
+ st.mcpTools,
403
+ st.skills
404
+ ]) {
405
+ for (const dispose of map.values()) dispose();
406
+ map.clear();
407
+ }
383
408
  st.noteDispose?.();
384
409
  delete st.noteDispose;
410
+ st.userToggled.clear();
385
411
  states.delete(sessionId);
386
412
  }
387
- },
388
- async set(sessionId, kind, name, enabled) {
413
+ if (defaults.tools.length > 0 || defaults.skills.length > 0) await seed(sessionId, defaults);
414
+ if (overrides !== void 0) await restoreImpl(sessionId, overrides);
415
+ }),
416
+ set: (sessionId, kind, name, enabled) => enqueue(sessionId, async () => {
389
417
  stateFor(sessionId).userToggled.add(`${kind}:${name}`);
390
418
  if (kind === "skill") await setSkill(sessionId, name, enabled);
391
419
  else if (kind === "mcp-server") setServer(sessionId, name, enabled);
@@ -396,7 +424,7 @@ function createCapabilityController(ctx, appendStats, blockedCounts) {
396
424
  kind: enabled ? "enable" : "disable",
397
425
  name: `${kind}:${name}`
398
426
  });
399
- }
427
+ })
400
428
  };
401
429
  }
402
430
 
@@ -449,36 +477,27 @@ function registerPresetEnforcement(ctx, capabilities, presetTools, sessionOverri
449
477
  return;
450
478
  }
451
479
  });
452
- }
453
-
454
- //#endregion
455
- //#region src/host/settings-scope.ts
456
- const TOOLKIT_SETTINGS_NAMESPACE = "capability-panel";
457
- const SessionOverrideSchema = z.object({
458
- skills: z.dict(z.boolean()).default({}),
459
- mcpServers: z.dict(z.boolean()).default({}),
460
- mcpTools: z.dict(z.boolean()).default({}),
461
- systemTools: z.dict(z.boolean()).default({})
462
- });
463
- const ToolkitSettingsSchema = z.object({
464
- presets: z.dict(z.array(z.string())).default({}),
465
- presetSkills: z.dict(z.array(z.string())).default({}),
466
- sessions: z.dict(SessionOverrideSchema).default({})
467
- });
468
- function createToolkitSettingsAccess(ctx) {
469
- let scope;
470
- let writeQueue = Promise.resolve();
471
- return {
472
- scope() {
473
- if (scope === void 0) scope = ctx.get("settings")?.register(TOOLKIT_SETTINGS_NAMESPACE, ToolkitSettingsSchema, { applies: "live" });
474
- return scope;
475
- },
476
- serialize(work) {
477
- const next = writeQueue.then(work, work);
478
- writeQueue = next.then(() => void 0, () => void 0);
479
- return next;
480
+ /**
481
+ * A preset switch lands AFTER creation (the session header records the
482
+ * composing preset at creation, and the picker's select() recomposes the
483
+ * blank session later): the first agent/created seeded the ORIGINAL
484
+ * preset's defaults, so the new preset's defaults must re-seed on top of a
485
+ * clean slate. Session overrides replay last — the user's own switches in
486
+ * this session outrank either preset's defaults.
487
+ */
488
+ ctx.on("agent-preset/selected", (sessionId, presetId) => {
489
+ try {
490
+ if (typeof sessionId !== "string" || typeof presetId !== "string") return void 0;
491
+ const defaults = presetTools.defaultsFor(presetId) ?? {
492
+ tools: [],
493
+ skills: []
494
+ };
495
+ const overrides = sessionOverrides.overridesFor(sessionId);
496
+ return capabilities.reseed(sessionId, defaults, overrides);
497
+ } catch {
498
+ return;
480
499
  }
481
- };
500
+ });
482
501
  }
483
502
 
484
503
  //#endregion
@@ -631,7 +650,7 @@ function decideStates(available, loads, shadowedSeqs, disabledSkills = /* @__PUR
631
650
  if (bucket === void 0) byName.set(record.skillName, [record]);
632
651
  else bucket.push(record);
633
652
  }
634
- return available.map(({ name, description, masked }) => {
653
+ return available.map(({ name, description, masked, source, provider, path, group }) => {
635
654
  const records = byName.get(name) ?? [];
636
655
  let state = "unloaded";
637
656
  if (records.length > 0) {
@@ -644,7 +663,11 @@ function decideStates(available, loads, shadowedSeqs, disabledSkills = /* @__PUR
644
663
  ...description === void 0 ? {} : { description },
645
664
  state,
646
665
  enabled: !disabledSkills.has(name) && masked !== true,
647
- loadCount: records.length
666
+ loadCount: records.length,
667
+ source,
668
+ provider,
669
+ ...path === void 0 ? {} : { path },
670
+ ...group === void 0 ? {} : { group }
648
671
  };
649
672
  });
650
673
  }
@@ -669,6 +692,299 @@ function groupMcpTools(toolNames) {
669
692
  })).sort((a, b) => a.server.localeCompare(b.server));
670
693
  }
671
694
 
695
+ //#endregion
696
+ //#region src/host/catalog.ts
697
+ /** Coerce a skill summary field to a string, falling back when the runtime shape is wrong. */
698
+ function coerceString(value, fallback) {
699
+ return typeof value === "string" ? value : fallback;
700
+ }
701
+ /**
702
+ * The source ROOT a skill was discovered under. The skills service reports
703
+ * each skill's own directory as resourceBase (`<root>/<name>`), so the group
704
+ * folder is its parent.
705
+ */
706
+ function parentDir(path) {
707
+ const trimmed = path.replace(/[\\/]+$/, "");
708
+ if (trimmed === "") return "/";
709
+ const slash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
710
+ return slash > 0 ? trimmed.slice(0, slash) : trimmed;
711
+ }
712
+ /**
713
+ * Abbreviate a host path for display: the session cwd becomes a relative
714
+ * path, the user's home becomes `~`. Everything else stays absolute.
715
+ */
716
+ function displayPath(path, cwd) {
717
+ if (cwd !== void 0 && cwd !== "" && path.startsWith(`${cwd}/`)) return path.slice(cwd.length + 1);
718
+ const home = process.env["HOME"];
719
+ if (home !== void 0 && home !== "" && path.startsWith(home)) return `~${path.slice(home.length)}`;
720
+ return path;
721
+ }
722
+ /** The DSH home, matching the runtime's own resolution order. */
723
+ function dshHome() {
724
+ return process.env["DSH_HOME"] ?? (process.env["HOME"] !== void 0 ? `${process.env["HOME"]}/.dsh` : void 0);
725
+ }
726
+ const EMPTY_STATE = {
727
+ skills: /* @__PURE__ */ new Map(),
728
+ mcpServers: /* @__PURE__ */ new Map(),
729
+ mcpTools: /* @__PURE__ */ new Map(),
730
+ systemTools: /* @__PURE__ */ new Map(),
731
+ userToggled: /* @__PURE__ */ new Set()
732
+ };
733
+ async function readAvailable(services, sessionId, degraded, presetDirs = []) {
734
+ const skills = services.get("skills");
735
+ if (skills === void 0) {
736
+ degraded.push("skills service unavailable");
737
+ return [];
738
+ }
739
+ try {
740
+ const agents = services.get("agents");
741
+ if (agents === void 0) {
742
+ degraded.push("agents service unavailable: session skill view cannot be determined");
743
+ return [];
744
+ }
745
+ const agent = agents.get(sessionId);
746
+ if (agent === void 0) {
747
+ degraded.push(`session agent "${sessionId}" unavailable: session skill view cannot be determined`);
748
+ return [];
749
+ }
750
+ const cwd = agent.session?.header?.cwd;
751
+ const list = await skills.list({
752
+ ...cwd === void 0 ? {} : { cwd },
753
+ scope: agent
754
+ });
755
+ const out = [];
756
+ const seen = /* @__PURE__ */ new Map();
757
+ const groupFor = (source, rawRoot) => {
758
+ if (source !== "custom" || rawRoot === "") return void 0;
759
+ const owner = presetDirs.find((p) => rawRoot === p.path || rawRoot.startsWith(`${p.path}/`));
760
+ return owner === void 0 ? void 0 : `preset:${owner.key}`;
761
+ };
762
+ for (const item of list) {
763
+ if (typeof item.name !== "string" || item.name === "") continue;
764
+ const masked = item.invocation?.modelInvocable === false;
765
+ const source = coerceString(item.source, "unknown");
766
+ const provider = coerceString(item.provider, "unknown");
767
+ const base = item.resourceBase;
768
+ const rawPath = base !== null && typeof base === "object" && base.kind === "directory" ? coerceString(base.path, "") : "";
769
+ const rawRoot = rawPath === "" ? "" : parentDir(rawPath);
770
+ const path = rawRoot === "" ? "" : displayPath(rawRoot, cwd);
771
+ const group = groupFor(source, rawRoot);
772
+ const existing = seen.get(item.name);
773
+ if (existing !== void 0) {
774
+ if (masked) existing.masked = true;
775
+ if (existing.provider === "capability-panel" && provider !== "capability-panel") {
776
+ existing.source = source;
777
+ existing.provider = provider;
778
+ if (path !== "") existing.path = path;
779
+ if (group !== void 0) existing.group = group;
780
+ }
781
+ continue;
782
+ }
783
+ const description = typeof item.description === "string" ? item.description : void 0;
784
+ const row = {
785
+ name: item.name,
786
+ ...description === void 0 ? {} : { description },
787
+ ...masked ? { masked: true } : {},
788
+ source,
789
+ provider,
790
+ ...path === "" ? {} : { path },
791
+ ...group === void 0 ? {} : { group }
792
+ };
793
+ seen.set(item.name, row);
794
+ out.push(row);
795
+ }
796
+ return out;
797
+ } catch (error) {
798
+ degraded.push(`skills read failed: ${error instanceof Error ? error.message : String(error)}`);
799
+ return [];
800
+ }
801
+ }
802
+ /**
803
+ * Read load facts off the LIVE session's in-memory log — the same object the
804
+ * agent loop itself reads to assemble the next request.
805
+ *
806
+ * This used to go through `sessionQuery.readSession` + `listEvents`, which are
807
+ * built for cross-session/cold reads: each call structuredClones the ENTIRE
808
+ * event log, `readSession` additionally replay-validates it via Session.create,
809
+ * and a write landing between the two parallel reads forced a whole second
810
+ * round. On a long session (60k events / tens of MB) that meant four full-log
811
+ * clones plus two full replays of synchronous CPU work on every panel open —
812
+ * blocking the Node event loop and freezing the GUI served by the same process.
813
+ *
814
+ * The live session needs none of that: `snapshotEvents()` hands back borrowed
815
+ * references (zero-copy), and `surface.nodes` is maintained incrementally as
816
+ * events land, so "what the model sees right now" is an O(1) membership test.
817
+ * Scanning references for the few `skill` tool calls costs microseconds even
818
+ * on the longest log. Both reads happen in one synchronous tick, so the view
819
+ * is always self-consistent — no cross-read race, no retry loop.
820
+ *
821
+ * A session without a live in-memory view (e.g. restored but not yet attached)
822
+ * degrades honestly instead of paying for a cold-log read the panel never
823
+ * asked for.
824
+ */
825
+ function readLogFacts(services, sessionId, degraded) {
826
+ const empty = {
827
+ loads: [],
828
+ shadowed: /* @__PURE__ */ new Set(),
829
+ pruned: /* @__PURE__ */ new Set()
830
+ };
831
+ try {
832
+ const session = services.get("agents")?.get(sessionId)?.session;
833
+ const nodes = session?.surface?.nodes;
834
+ if (session === void 0 || typeof session.snapshotEvents !== "function" || !Array.isArray(nodes)) {
835
+ degraded.push("live session view unavailable: load states cannot be determined");
836
+ return empty;
837
+ }
838
+ const events = session.snapshotEvents();
839
+ if (!Array.isArray(events)) {
840
+ degraded.push("snapshotEvents() returned an unexpected shape; cannot read skill loads");
841
+ return empty;
842
+ }
843
+ const surfaceSeqs = /* @__PURE__ */ new Set();
844
+ for (const seq of nodes) if (typeof seq === "number") surfaceSeqs.add(seq);
845
+ const loads = collectLoadRecords(events);
846
+ const resultSeqs = indexToolResultSeqs(events);
847
+ return {
848
+ loads,
849
+ shadowed: shadowedLoadSeqs(loads, resultSeqs, surfaceSeqs),
850
+ pruned: prunedLoadSeqs(loads, resultSeqs, surfaceSeqs, events)
851
+ };
852
+ } catch (error) {
853
+ degraded.push(`event read failed: ${error instanceof Error ? error.message : String(error)}`);
854
+ return empty;
855
+ }
856
+ }
857
+ function readMcp(services, degraded, disabledServers, disabledTools, agent, presetName, presetPath) {
858
+ const tools = services.get("tools");
859
+ if (tools === void 0) {
860
+ degraded.push("tools service unavailable");
861
+ return [];
862
+ }
863
+ try {
864
+ const names = [];
865
+ const descriptions = /* @__PURE__ */ new Map();
866
+ const collect = (scope) => {
867
+ for (const schema of tools.schemas(scope)) {
868
+ if (typeof schema.name !== "string" || !schema.name.startsWith("mcp__")) continue;
869
+ if (!names.includes(schema.name)) {
870
+ names.push(schema.name);
871
+ if (typeof schema.description === "string" && schema.description !== "") descriptions.set(schema.name, schema.description);
872
+ }
873
+ }
874
+ };
875
+ collect(void 0);
876
+ if (agent !== void 0) collect(agent);
877
+ const globalNames = /* @__PURE__ */ new Set();
878
+ for (const schema of tools.schemas()) if (typeof schema.name === "string" && schema.name.startsWith("mcp__")) globalNames.add(schema.name);
879
+ return groupMcpTools(names).map((group) => {
880
+ const enabled = !disabledServers.has(group.server);
881
+ const entries = group.tools.map((tool) => {
882
+ const name = `mcp__${group.server}__${tool}`;
883
+ const description = descriptions.get(name);
884
+ return {
885
+ name,
886
+ label: tool,
887
+ ...description === void 0 ? {} : { description },
888
+ enabled: enabled && !disabledTools.has(name)
889
+ };
890
+ });
891
+ const allGlobal = group.tools.every((tool) => globalNames.has(`mcp__${group.server}__${tool}`));
892
+ const source = allGlobal ? "host" : presetName ?? "preset";
893
+ const rawPath = allGlobal ? dshHome() : presetPath;
894
+ return {
895
+ server: group.server,
896
+ tools: entries,
897
+ enabled,
898
+ source,
899
+ ...rawPath === void 0 ? {} : { path: displayPath(rawPath) }
900
+ };
901
+ });
902
+ } catch (error) {
903
+ degraded.push(`tool read failed: ${error instanceof Error ? error.message : String(error)}`);
904
+ return [];
905
+ }
906
+ }
907
+ function readSystemTools(services, degraded, disabledTools, agent) {
908
+ const tools = services.get("tools");
909
+ if (tools === void 0) {
910
+ if (!degraded.includes("tools service unavailable")) degraded.push("tools service unavailable");
911
+ return [];
912
+ }
913
+ try {
914
+ let reachable;
915
+ if (agent !== void 0) {
916
+ reachable = /* @__PURE__ */ new Set();
917
+ for (const schema of tools.schemas(agent)) if (typeof schema.name === "string") reachable.add(schema.name);
918
+ }
919
+ const byName = /* @__PURE__ */ new Map();
920
+ const collect = (scope) => {
921
+ for (const schema of tools.schemas(scope)) {
922
+ if (typeof schema.name !== "string" || schema.name.startsWith("mcp__") || byName.has(schema.name)) continue;
923
+ const description = typeof schema.description === "string" && schema.description !== "" ? schema.description : void 0;
924
+ byName.set(schema.name, {
925
+ name: schema.name,
926
+ label: schema.name,
927
+ ...description === void 0 ? {} : { description },
928
+ enabled: !disabledTools.has(schema.name) && (reachable === void 0 || reachable.has(schema.name)),
929
+ ...schema.name === RESERVED_TOOL ? { reserved: true } : {}
930
+ });
931
+ }
932
+ };
933
+ collect(void 0);
934
+ if (agent !== void 0) collect(agent);
935
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
936
+ } catch (error) {
937
+ degraded.push(`tool read failed: ${error instanceof Error ? error.message : String(error)}`);
938
+ return [];
939
+ }
940
+ }
941
+ async function buildPayload(services, sessionId, capabilityState = EMPTY_STATE, blocked = {}) {
942
+ const degraded = [];
943
+ const disabledSkills = new Set(capabilityState.skills.keys());
944
+ const disabledServers = new Set(capabilityState.mcpServers.keys());
945
+ const disabledTools = new Set(capabilityState.mcpTools.keys());
946
+ const disabledSystem = new Set(capabilityState.systemTools.keys());
947
+ if (sessionId === null) return {
948
+ sessionId: null,
949
+ skills: [],
950
+ mcp: readMcp(services, degraded, disabledServers, disabledTools),
951
+ systemTools: readSystemTools(services, degraded, disabledSystem),
952
+ blocked,
953
+ ...degraded.length > 0 ? { degraded } : {}
954
+ };
955
+ const agent = services.get("agents")?.get(sessionId);
956
+ let presetName;
957
+ let presetPath;
958
+ let presetDirs = [];
959
+ if (agent !== void 0) {
960
+ const presetId = services.get("agentPresets")?.composedPreset(agent.ctx);
961
+ try {
962
+ const presets = await services.get("agentPresets")?.list();
963
+ presetDirs = (presets ?? []).filter((p) => typeof p.path === "string" && p.path !== "").map((p) => ({
964
+ key: p.name ?? p.id,
965
+ path: p.path
966
+ }));
967
+ if (presetId !== void 0) {
968
+ const preset = presets?.find((p) => p.id === presetId);
969
+ presetName = preset?.name ?? presetId;
970
+ presetPath = preset?.path;
971
+ }
972
+ } catch {
973
+ presetName = presetId;
974
+ }
975
+ }
976
+ const available = await readAvailable(services, sessionId, degraded, presetDirs);
977
+ const logFacts = readLogFacts(services, sessionId, degraded);
978
+ return {
979
+ sessionId,
980
+ skills: decideStates(available, logFacts.loads, logFacts.shadowed, disabledSkills, logFacts.pruned),
981
+ mcp: readMcp(services, degraded, disabledServers, disabledTools, agent, presetName, presetPath),
982
+ systemTools: readSystemTools(services, degraded, disabledSystem, agent),
983
+ blocked,
984
+ ...degraded.length > 0 ? { degraded } : {}
985
+ };
986
+ }
987
+
672
988
  //#endregion
673
989
  //#region src/host/preset-tools.ts
674
990
  function requireService(service, message) {
@@ -694,18 +1010,32 @@ function toolSummaries(tools, scope) {
694
1010
  * workspace would contribute. A project skill is marked rather than dropped:
695
1011
  * hiding it would silently shorten the list, while marking it says plainly
696
1012
  * that a session opened elsewhere will not see the row.
1013
+ *
1014
+ * Each row also carries the same provenance the session payload reports:
1015
+ * the raw runtime source, the discovery root (parent of the skill's own
1016
+ * directory, abbreviated for display), and a `preset:<name>` group when the
1017
+ * root sits inside a preset's directory.
697
1018
  */
698
- async function presetSkillRows(skills, scope, disabled, cwd) {
1019
+ async function presetSkillRows(skills, scope, disabled, cwd, presetDirs = []) {
699
1020
  const seen = /* @__PURE__ */ new Map();
700
1021
  const add = (summaries, project) => {
701
1022
  for (const summary of summaries) {
702
1023
  if (typeof summary.name !== "string" || summary.name === "" || seen.has(summary.name)) continue;
703
1024
  const description = typeof summary.description === "string" ? summary.description : void 0;
1025
+ const source = typeof summary.source === "string" ? summary.source : void 0;
1026
+ const base = summary.resourceBase;
1027
+ const rawPath = base !== null && typeof base === "object" && base.kind === "directory" ? base.path : void 0;
1028
+ const rawRoot = typeof rawPath === "string" && rawPath !== "" ? parentDir(rawPath) : "";
1029
+ const path = rawRoot === "" ? void 0 : displayPath(rawRoot, cwd);
1030
+ const owner = source === "custom" && rawRoot !== "" ? presetDirs.find((p) => rawRoot === p.path || rawRoot.startsWith(`${p.path}/`)) : void 0;
704
1031
  seen.set(summary.name, {
705
1032
  name: summary.name,
706
1033
  ...description === void 0 ? {} : { description },
707
1034
  enabled: !disabled.has(summary.name),
708
- ...project ? { project: true } : {}
1035
+ ...project ? { project: true } : {},
1036
+ ...source === void 0 ? {} : { source },
1037
+ ...path === void 0 ? {} : { path },
1038
+ ...owner === void 0 ? {} : { group: `preset:${owner.key}` }
709
1039
  });
710
1040
  }
711
1041
  };
@@ -743,25 +1073,28 @@ function createPresetToolController(ctx, access) {
743
1073
  const skills = ctx.get("skills");
744
1074
  const cwd = process.cwd();
745
1075
  const presets = await agentPresets.list();
1076
+ const presetDirs = presets.filter((p) => typeof p.path === "string" && p.path !== "").map((p) => ({
1077
+ key: p.name ?? p.id,
1078
+ path: p.path
1079
+ }));
1080
+ const globalNames = /* @__PURE__ */ new Set();
1081
+ for (const schema of tools.schemas()) if (typeof schema.name === "string" && schema.name.startsWith("mcp__")) globalNames.add(schema.name);
746
1082
  return {
747
1083
  presets: await Promise.all(presets.map(async (preset) => {
748
1084
  let entries = [];
749
1085
  let skillRows = [];
1086
+ let mountError;
750
1087
  if (preset.broken === void 0) {
751
1088
  let scope;
752
1089
  try {
753
1090
  scope = await agentPresets.standingKeyFor(preset.id);
754
1091
  entries = toolSummaries(tools, scope);
755
- } catch (error) {
756
- throw new HttpError(503, `preset "${preset.id}" tools are unavailable: ${errorMessage(error)}`);
757
- }
758
- if (skills !== void 0) {
759
- const disabledSkills = new Set(stored.presetSkills[preset.id] ?? []);
760
- try {
761
- skillRows = await presetSkillRows(skills, scope, disabledSkills, cwd);
762
- } catch (error) {
763
- throw new HttpError(503, `preset "${preset.id}" skills are unavailable: ${errorMessage(error)}`);
1092
+ if (skills !== void 0) {
1093
+ const disabledSkills = new Set(stored.presetSkills[preset.id] ?? []);
1094
+ skillRows = await presetSkillRows(skills, scope, disabledSkills, cwd, presetDirs);
764
1095
  }
1096
+ } catch (error) {
1097
+ mountError = errorMessage(error);
765
1098
  }
766
1099
  }
767
1100
  const disabled = new Set(configured[preset.id] ?? []);
@@ -776,12 +1109,17 @@ function createPresetToolController(ctx, access) {
776
1109
  ...name === RESERVED_TOOL ? { reserved: true } : {}
777
1110
  };
778
1111
  };
1112
+ const presetName = preset.name ?? preset.id;
779
1113
  const mcp = groupMcpTools(entries.map((entry) => entry.name)).map((group) => {
780
1114
  const tools$1 = group.tools.map((tool) => row(`mcp__${group.server}__${tool}`, tool));
1115
+ const allGlobal = group.tools.every((tool) => globalNames.has(`mcp__${group.server}__${tool}`));
1116
+ const rawPath = allGlobal ? dshHome() : preset.path;
781
1117
  return {
782
1118
  server: group.server,
783
1119
  tools: tools$1,
784
- enabled: tools$1.some((tool) => tool.enabled)
1120
+ enabled: tools$1.some((tool) => tool.enabled),
1121
+ source: allGlobal ? "host" : presetName,
1122
+ ...rawPath === void 0 ? {} : { path: displayPath(rawPath) }
785
1123
  };
786
1124
  });
787
1125
  const systemTools = entries.filter((entry) => !entry.name.startsWith("mcp__")).map((entry) => row(entry.name, entry.name));
@@ -790,7 +1128,7 @@ function createPresetToolController(ctx, access) {
790
1128
  name: preset.name ?? preset.id,
791
1129
  trust: preset.trust,
792
1130
  ...preset.description === void 0 ? {} : { description: preset.description },
793
- ...preset.broken === void 0 ? {} : { broken: preset.broken },
1131
+ ...preset.broken !== void 0 ? { broken: preset.broken } : mountError === void 0 ? {} : { broken: `failed to mount: ${mountError}` },
794
1132
  skills: skillRows,
795
1133
  mcp,
796
1134
  systemTools
@@ -904,212 +1242,21 @@ function isLoopback(req) {
904
1242
  }
905
1243
 
906
1244
  //#endregion
907
- //#region src/host/catalog.ts
908
- const EMPTY_STATE = {
909
- skills: /* @__PURE__ */ new Map(),
910
- mcpServers: /* @__PURE__ */ new Map(),
911
- mcpTools: /* @__PURE__ */ new Map(),
912
- systemTools: /* @__PURE__ */ new Map(),
913
- userToggled: /* @__PURE__ */ new Set()
914
- };
915
- async function readAvailable(services, sessionId, degraded) {
916
- const skills = services.get("skills");
917
- if (skills === void 0) {
918
- degraded.push("skills service unavailable");
919
- return [];
920
- }
921
- try {
922
- const agents = services.get("agents");
923
- if (agents === void 0) {
924
- degraded.push("agents service unavailable: session skill view cannot be determined");
925
- return [];
926
- }
927
- const agent = agents.get(sessionId);
928
- if (agent === void 0) {
929
- degraded.push(`session agent "${sessionId}" unavailable: session skill view cannot be determined`);
930
- return [];
931
- }
932
- const cwd = agent.session?.header?.cwd;
933
- const list = await skills.list({
934
- ...cwd === void 0 ? {} : { cwd },
935
- scope: agent
936
- });
937
- const out = [];
938
- const seen = /* @__PURE__ */ new Map();
939
- for (const item of list) {
940
- if (typeof item.name !== "string" || item.name === "") continue;
941
- const masked = item.invocation?.modelInvocable === false;
942
- const existing = seen.get(item.name);
943
- if (existing !== void 0) {
944
- if (masked) existing.masked = true;
945
- continue;
946
- }
947
- const description = typeof item.description === "string" ? item.description : void 0;
948
- const row = {
949
- name: item.name,
950
- ...description === void 0 ? {} : { description },
951
- ...masked ? { masked: true } : {}
952
- };
953
- seen.set(item.name, row);
954
- out.push(row);
955
- }
956
- return out;
957
- } catch (error) {
958
- degraded.push(`skills read failed: ${error instanceof Error ? error.message : String(error)}`);
959
- return [];
960
- }
961
- }
1245
+ //#region src/host/open-folder.ts
962
1246
  /**
963
- * Read load facts off the LIVE session's in-memory log — the same object the
964
- * agent loop itself reads to assemble the next request.
965
- *
966
- * This used to go through `sessionQuery.readSession` + `listEvents`, which are
967
- * built for cross-session/cold reads: each call structuredClones the ENTIRE
968
- * event log, `readSession` additionally replay-validates it via Session.create,
969
- * and a write landing between the two parallel reads forced a whole second
970
- * round. On a long session (60k events / tens of MB) that meant four full-log
971
- * clones plus two full replays of synchronous CPU work on every panel open —
972
- * blocking the Node event loop and freezing the GUI served by the same process.
973
- *
974
- * The live session needs none of that: `snapshotEvents()` hands back borrowed
975
- * references (zero-copy), and `surface.nodes` is maintained incrementally as
976
- * events land, so "what the model sees right now" is an O(1) membership test.
977
- * Scanning references for the few `skill` tool calls costs microseconds even
978
- * on the longest log. Both reads happen in one synchronous tick, so the view
979
- * is always self-consistent — no cross-read race, no retry loop.
1247
+ * Open a folder in the system file manager.
980
1248
  *
981
- * A session without a live in-memory view (e.g. restored but not yet attached)
982
- * degrades honestly instead of paying for a cold-log read the panel never
983
- * asked for.
1249
+ * Covers macOS (`open`), Windows (`start`), and every freedesktop Linux
1250
+ * desktop (GNOME, KDE, XFCE, …) via `xdg-open`. Server-only Linux distros
1251
+ * that lack a desktop cannot open folders, but the error is graceful.
984
1252
  */
985
- function readLogFacts(services, sessionId, degraded) {
986
- const empty = {
987
- loads: [],
988
- shadowed: /* @__PURE__ */ new Set(),
989
- pruned: /* @__PURE__ */ new Set()
990
- };
991
- try {
992
- const session = services.get("agents")?.get(sessionId)?.session;
993
- const nodes = session?.surface?.nodes;
994
- if (session === void 0 || typeof session.snapshotEvents !== "function" || !Array.isArray(nodes)) {
995
- degraded.push("live session view unavailable: load states cannot be determined");
996
- return empty;
997
- }
998
- const events = session.snapshotEvents();
999
- if (!Array.isArray(events)) {
1000
- degraded.push("snapshotEvents() returned an unexpected shape; cannot read skill loads");
1001
- return empty;
1002
- }
1003
- const surfaceSeqs = /* @__PURE__ */ new Set();
1004
- for (const seq of nodes) if (typeof seq === "number") surfaceSeqs.add(seq);
1005
- const loads = collectLoadRecords(events);
1006
- const resultSeqs = indexToolResultSeqs(events);
1007
- return {
1008
- loads,
1009
- shadowed: shadowedLoadSeqs(loads, resultSeqs, surfaceSeqs),
1010
- pruned: prunedLoadSeqs(loads, resultSeqs, surfaceSeqs, events)
1011
- };
1012
- } catch (error) {
1013
- degraded.push(`event read failed: ${error instanceof Error ? error.message : String(error)}`);
1014
- return empty;
1015
- }
1016
- }
1017
- function readMcp(services, degraded, disabledServers, disabledTools) {
1018
- const tools = services.get("tools");
1019
- if (tools === void 0) {
1020
- degraded.push("tools service unavailable");
1021
- return [];
1022
- }
1023
- try {
1024
- const names = [];
1025
- const descriptions = /* @__PURE__ */ new Map();
1026
- for (const schema of tools.schemas()) {
1027
- if (typeof schema.name !== "string") continue;
1028
- names.push(schema.name);
1029
- if (typeof schema.description === "string" && schema.description !== "") descriptions.set(schema.name, schema.description);
1030
- }
1031
- return groupMcpTools(names).map((group) => {
1032
- const enabled = !disabledServers.has(group.server);
1033
- const entries = group.tools.map((tool) => {
1034
- const name = `mcp__${group.server}__${tool}`;
1035
- const description = descriptions.get(name);
1036
- return {
1037
- name,
1038
- label: tool,
1039
- ...description === void 0 ? {} : { description },
1040
- enabled: enabled && !disabledTools.has(name)
1041
- };
1042
- });
1043
- return {
1044
- server: group.server,
1045
- tools: entries,
1046
- enabled
1047
- };
1253
+ function openFolder(path) {
1254
+ return new Promise((resolve, reject) => {
1255
+ exec(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} "${path}"`, (error) => {
1256
+ if (error) reject(/* @__PURE__ */ new Error(`failed to open folder: ${error.message}`));
1257
+ else resolve();
1048
1258
  });
1049
- } catch (error) {
1050
- degraded.push(`tool read failed: ${error instanceof Error ? error.message : String(error)}`);
1051
- return [];
1052
- }
1053
- }
1054
- function readSystemTools(services, degraded, disabledTools, agent) {
1055
- const tools = services.get("tools");
1056
- if (tools === void 0) {
1057
- if (!degraded.includes("tools service unavailable")) degraded.push("tools service unavailable");
1058
- return [];
1059
- }
1060
- try {
1061
- let reachable;
1062
- if (agent !== void 0) {
1063
- reachable = /* @__PURE__ */ new Set();
1064
- for (const schema of tools.schemas(agent)) if (typeof schema.name === "string") reachable.add(schema.name);
1065
- }
1066
- const byName = /* @__PURE__ */ new Map();
1067
- const collect = (scope) => {
1068
- for (const schema of tools.schemas(scope)) {
1069
- if (typeof schema.name !== "string" || schema.name.startsWith("mcp__") || byName.has(schema.name)) continue;
1070
- const description = typeof schema.description === "string" && schema.description !== "" ? schema.description : void 0;
1071
- byName.set(schema.name, {
1072
- name: schema.name,
1073
- label: schema.name,
1074
- ...description === void 0 ? {} : { description },
1075
- enabled: !disabledTools.has(schema.name) && (reachable === void 0 || reachable.has(schema.name)),
1076
- ...schema.name === RESERVED_TOOL ? { reserved: true } : {}
1077
- });
1078
- }
1079
- };
1080
- collect(void 0);
1081
- if (agent !== void 0) collect(agent);
1082
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1083
- } catch (error) {
1084
- degraded.push(`tool read failed: ${error instanceof Error ? error.message : String(error)}`);
1085
- return [];
1086
- }
1087
- }
1088
- async function buildPayload(services, sessionId, capabilityState = EMPTY_STATE, blocked = {}) {
1089
- const degraded = [];
1090
- const disabledSkills = new Set(capabilityState.skills.keys());
1091
- const disabledServers = new Set(capabilityState.mcpServers.keys());
1092
- const disabledTools = new Set(capabilityState.mcpTools.keys());
1093
- const disabledSystem = new Set(capabilityState.systemTools.keys());
1094
- if (sessionId === null) return {
1095
- sessionId: null,
1096
- skills: [],
1097
- mcp: readMcp(services, degraded, disabledServers, disabledTools),
1098
- systemTools: readSystemTools(services, degraded, disabledSystem),
1099
- blocked,
1100
- ...degraded.length > 0 ? { degraded } : {}
1101
- };
1102
- const agent = services.get("agents")?.get(sessionId);
1103
- const available = await readAvailable(services, sessionId, degraded);
1104
- const logFacts = readLogFacts(services, sessionId, degraded);
1105
- return {
1106
- sessionId,
1107
- skills: decideStates(available, logFacts.loads, logFacts.shadowed, disabledSkills, logFacts.pruned),
1108
- mcp: readMcp(services, degraded, disabledServers, disabledTools),
1109
- systemTools: readSystemTools(services, degraded, disabledSystem, agent),
1110
- blocked,
1111
- ...degraded.length > 0 ? { degraded } : {}
1112
- };
1259
+ });
1113
1260
  }
1114
1261
 
1115
1262
  //#endregion
@@ -1253,6 +1400,74 @@ function createRouteHandler(services, capabilities, stats, blockedCounts, preset
1253
1400
  }, true);
1254
1401
  return;
1255
1402
  }
1403
+ if (url.pathname === `${ROUTE}/open-folder`) {
1404
+ if (req.method !== "POST") {
1405
+ res.writeHead(405, {
1406
+ allow: "POST",
1407
+ "content-type": "text/plain; charset=utf-8"
1408
+ });
1409
+ res.end("method not allowed");
1410
+ return;
1411
+ }
1412
+ if (!validatePresetContentType(req, res)) return;
1413
+ const body = await readRequestBody(req);
1414
+ if (body === null || typeof body !== "object") {
1415
+ json(res, 400, { error: "invalid request body" });
1416
+ return;
1417
+ }
1418
+ const record = body;
1419
+ if (typeof record.source !== "string" || record.source === "") {
1420
+ json(res, 400, { error: "source is required" });
1421
+ return;
1422
+ }
1423
+ const source = record.source;
1424
+ const sessionId$1 = typeof record.sessionId === "string" ? record.sessionId : null;
1425
+ let folderPath;
1426
+ if (sessionId$1 !== null) try {
1427
+ const skills = services.get("skills");
1428
+ const agent = services.get("agents")?.get(sessionId$1);
1429
+ if (skills !== void 0 && agent !== void 0) {
1430
+ const cwd = agent.session?.header?.cwd;
1431
+ const list = await skills.list({
1432
+ ...cwd === void 0 ? {} : { cwd },
1433
+ scope: agent
1434
+ });
1435
+ for (const item of list) {
1436
+ if (item.source !== source) continue;
1437
+ const base = item.resourceBase;
1438
+ if (base !== null && typeof base === "object" && base.kind === "directory") {
1439
+ const path = base.path;
1440
+ if (typeof path === "string" && path !== "") {
1441
+ folderPath = parentDir(path);
1442
+ break;
1443
+ }
1444
+ }
1445
+ }
1446
+ }
1447
+ } catch {}
1448
+ if (folderPath === void 0 && (source === "user-dsh" || source === "user-agents")) {
1449
+ const dshHome$1 = process.env["DSH_HOME"] ?? (process.env["HOME"] !== void 0 ? `${process.env["HOME"]}/.dsh` : void 0);
1450
+ if (dshHome$1 !== void 0) folderPath = source === "user-dsh" ? `${dshHome$1}/skills` : `${process.env["DSH_AGENTS_HOME"] ?? `${process.env["HOME"]}/.agents`}/skills`;
1451
+ } else if (folderPath === void 0 && (source === "project-dsh" || source === "project-agents")) {
1452
+ const cwd = (sessionId$1 === null ? void 0 : services.get("agents")?.get(sessionId$1))?.session?.header?.cwd ?? (sessionId$1 === null ? process.cwd() : void 0);
1453
+ if (cwd !== void 0) folderPath = source === "project-dsh" ? `${cwd}/.dsh/skills` : `${cwd}/.agents/skills`;
1454
+ } else if (folderPath === void 0 && source === "host") folderPath = process.env["DSH_HOME"] ?? (process.env["HOME"] !== void 0 ? `${process.env["HOME"]}/.dsh` : void 0);
1455
+ else if (folderPath === void 0 && source !== "bundled" && source !== "runtime") try {
1456
+ const preset = (await services.get("agentPresets")?.list())?.find((p) => p.id === source || p.name === source);
1457
+ if (preset !== void 0) folderPath = preset.path;
1458
+ } catch {}
1459
+ if (folderPath === void 0) {
1460
+ json(res, 404, { error: `cannot open folder for source "${source}"` });
1461
+ return;
1462
+ }
1463
+ try {
1464
+ await openFolder(folderPath);
1465
+ json(res, 200, { ok: true });
1466
+ } catch (error) {
1467
+ json(res, 500, { error: `failed to open folder: ${errorMessage(error)}` });
1468
+ }
1469
+ return;
1470
+ }
1256
1471
  if (url.pathname !== ROUTE && url.pathname !== "/") {
1257
1472
  res.writeHead(404, {
1258
1473
  "content-type": "text/plain; charset=utf-8",
@@ -1367,6 +1582,36 @@ function createSessionOverrideStore(access) {
1367
1582
  };
1368
1583
  }
1369
1584
 
1585
+ //#endregion
1586
+ //#region src/host/settings-scope.ts
1587
+ const TOOLKIT_SETTINGS_NAMESPACE = "capability-panel";
1588
+ const SessionOverrideSchema = z.object({
1589
+ skills: z.dict(z.boolean()).default({}),
1590
+ mcpServers: z.dict(z.boolean()).default({}),
1591
+ mcpTools: z.dict(z.boolean()).default({}),
1592
+ systemTools: z.dict(z.boolean()).default({})
1593
+ });
1594
+ const ToolkitSettingsSchema = z.object({
1595
+ presets: z.dict(z.array(z.string())).default({}),
1596
+ presetSkills: z.dict(z.array(z.string())).default({}),
1597
+ sessions: z.dict(SessionOverrideSchema).default({})
1598
+ });
1599
+ function createToolkitSettingsAccess(ctx) {
1600
+ let scope;
1601
+ let writeQueue = Promise.resolve();
1602
+ return {
1603
+ scope() {
1604
+ if (scope === void 0) scope = ctx.get("settings")?.register(TOOLKIT_SETTINGS_NAMESPACE, ToolkitSettingsSchema, { applies: "live" });
1605
+ return scope;
1606
+ },
1607
+ serialize(work) {
1608
+ const next = writeQueue.then(work, work);
1609
+ writeQueue = next.then(() => void 0, () => void 0);
1610
+ return next;
1611
+ }
1612
+ };
1613
+ }
1614
+
1370
1615
  //#endregion
1371
1616
  //#region src/host/stats-store.ts
1372
1617
  function statsFilePath(environment = process.env, home = homedir()) {