dsh-capability-panel 1.0.0 → 1.1.0

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,
@@ -451,36 +452,6 @@ function registerPresetEnforcement(ctx, capabilities, presetTools, sessionOverri
451
452
  });
452
453
  }
453
454
 
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
- };
482
- }
483
-
484
455
  //#endregion
485
456
  //#region src/load-state.ts
486
457
  /** Pull the skill name out of a `skill` tool call's stringified arguments. */
@@ -631,7 +602,7 @@ function decideStates(available, loads, shadowedSeqs, disabledSkills = /* @__PUR
631
602
  if (bucket === void 0) byName.set(record.skillName, [record]);
632
603
  else bucket.push(record);
633
604
  }
634
- return available.map(({ name, description, masked }) => {
605
+ return available.map(({ name, description, masked, source, provider, path, group }) => {
635
606
  const records = byName.get(name) ?? [];
636
607
  let state = "unloaded";
637
608
  if (records.length > 0) {
@@ -644,7 +615,11 @@ function decideStates(available, loads, shadowedSeqs, disabledSkills = /* @__PUR
644
615
  ...description === void 0 ? {} : { description },
645
616
  state,
646
617
  enabled: !disabledSkills.has(name) && masked !== true,
647
- loadCount: records.length
618
+ loadCount: records.length,
619
+ source,
620
+ provider,
621
+ ...path === void 0 ? {} : { path },
622
+ ...group === void 0 ? {} : { group }
648
623
  };
649
624
  });
650
625
  }
@@ -669,6 +644,299 @@ function groupMcpTools(toolNames) {
669
644
  })).sort((a, b) => a.server.localeCompare(b.server));
670
645
  }
671
646
 
647
+ //#endregion
648
+ //#region src/host/catalog.ts
649
+ /** Coerce a skill summary field to a string, falling back when the runtime shape is wrong. */
650
+ function coerceString(value, fallback) {
651
+ return typeof value === "string" ? value : fallback;
652
+ }
653
+ /**
654
+ * The source ROOT a skill was discovered under. The skills service reports
655
+ * each skill's own directory as resourceBase (`<root>/<name>`), so the group
656
+ * folder is its parent.
657
+ */
658
+ function parentDir(path) {
659
+ const trimmed = path.replace(/[\\/]+$/, "");
660
+ if (trimmed === "") return "/";
661
+ const slash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
662
+ return slash > 0 ? trimmed.slice(0, slash) : trimmed;
663
+ }
664
+ /**
665
+ * Abbreviate a host path for display: the session cwd becomes a relative
666
+ * path, the user's home becomes `~`. Everything else stays absolute.
667
+ */
668
+ function displayPath(path, cwd) {
669
+ if (cwd !== void 0 && cwd !== "" && path.startsWith(`${cwd}/`)) return path.slice(cwd.length + 1);
670
+ const home = process.env["HOME"];
671
+ if (home !== void 0 && home !== "" && path.startsWith(home)) return `~${path.slice(home.length)}`;
672
+ return path;
673
+ }
674
+ /** The DSH home, matching the runtime's own resolution order. */
675
+ function dshHome() {
676
+ return process.env["DSH_HOME"] ?? (process.env["HOME"] !== void 0 ? `${process.env["HOME"]}/.dsh` : void 0);
677
+ }
678
+ const EMPTY_STATE = {
679
+ skills: /* @__PURE__ */ new Map(),
680
+ mcpServers: /* @__PURE__ */ new Map(),
681
+ mcpTools: /* @__PURE__ */ new Map(),
682
+ systemTools: /* @__PURE__ */ new Map(),
683
+ userToggled: /* @__PURE__ */ new Set()
684
+ };
685
+ async function readAvailable(services, sessionId, degraded, presetDirs = []) {
686
+ const skills = services.get("skills");
687
+ if (skills === void 0) {
688
+ degraded.push("skills service unavailable");
689
+ return [];
690
+ }
691
+ try {
692
+ const agents = services.get("agents");
693
+ if (agents === void 0) {
694
+ degraded.push("agents service unavailable: session skill view cannot be determined");
695
+ return [];
696
+ }
697
+ const agent = agents.get(sessionId);
698
+ if (agent === void 0) {
699
+ degraded.push(`session agent "${sessionId}" unavailable: session skill view cannot be determined`);
700
+ return [];
701
+ }
702
+ const cwd = agent.session?.header?.cwd;
703
+ const list = await skills.list({
704
+ ...cwd === void 0 ? {} : { cwd },
705
+ scope: agent
706
+ });
707
+ const out = [];
708
+ const seen = /* @__PURE__ */ new Map();
709
+ const groupFor = (source, rawRoot) => {
710
+ if (source !== "custom" || rawRoot === "") return void 0;
711
+ const owner = presetDirs.find((p) => rawRoot === p.path || rawRoot.startsWith(`${p.path}/`));
712
+ return owner === void 0 ? void 0 : `preset:${owner.key}`;
713
+ };
714
+ for (const item of list) {
715
+ if (typeof item.name !== "string" || item.name === "") continue;
716
+ const masked = item.invocation?.modelInvocable === false;
717
+ const source = coerceString(item.source, "unknown");
718
+ const provider = coerceString(item.provider, "unknown");
719
+ const base = item.resourceBase;
720
+ const rawPath = base !== null && typeof base === "object" && base.kind === "directory" ? coerceString(base.path, "") : "";
721
+ const rawRoot = rawPath === "" ? "" : parentDir(rawPath);
722
+ const path = rawRoot === "" ? "" : displayPath(rawRoot, cwd);
723
+ const group = groupFor(source, rawRoot);
724
+ const existing = seen.get(item.name);
725
+ if (existing !== void 0) {
726
+ if (masked) existing.masked = true;
727
+ if (existing.provider === "capability-panel" && provider !== "capability-panel") {
728
+ existing.source = source;
729
+ existing.provider = provider;
730
+ if (path !== "") existing.path = path;
731
+ if (group !== void 0) existing.group = group;
732
+ }
733
+ continue;
734
+ }
735
+ const description = typeof item.description === "string" ? item.description : void 0;
736
+ const row = {
737
+ name: item.name,
738
+ ...description === void 0 ? {} : { description },
739
+ ...masked ? { masked: true } : {},
740
+ source,
741
+ provider,
742
+ ...path === "" ? {} : { path },
743
+ ...group === void 0 ? {} : { group }
744
+ };
745
+ seen.set(item.name, row);
746
+ out.push(row);
747
+ }
748
+ return out;
749
+ } catch (error) {
750
+ degraded.push(`skills read failed: ${error instanceof Error ? error.message : String(error)}`);
751
+ return [];
752
+ }
753
+ }
754
+ /**
755
+ * Read load facts off the LIVE session's in-memory log — the same object the
756
+ * agent loop itself reads to assemble the next request.
757
+ *
758
+ * This used to go through `sessionQuery.readSession` + `listEvents`, which are
759
+ * built for cross-session/cold reads: each call structuredClones the ENTIRE
760
+ * event log, `readSession` additionally replay-validates it via Session.create,
761
+ * and a write landing between the two parallel reads forced a whole second
762
+ * round. On a long session (60k events / tens of MB) that meant four full-log
763
+ * clones plus two full replays of synchronous CPU work on every panel open —
764
+ * blocking the Node event loop and freezing the GUI served by the same process.
765
+ *
766
+ * The live session needs none of that: `snapshotEvents()` hands back borrowed
767
+ * references (zero-copy), and `surface.nodes` is maintained incrementally as
768
+ * events land, so "what the model sees right now" is an O(1) membership test.
769
+ * Scanning references for the few `skill` tool calls costs microseconds even
770
+ * on the longest log. Both reads happen in one synchronous tick, so the view
771
+ * is always self-consistent — no cross-read race, no retry loop.
772
+ *
773
+ * A session without a live in-memory view (e.g. restored but not yet attached)
774
+ * degrades honestly instead of paying for a cold-log read the panel never
775
+ * asked for.
776
+ */
777
+ function readLogFacts(services, sessionId, degraded) {
778
+ const empty = {
779
+ loads: [],
780
+ shadowed: /* @__PURE__ */ new Set(),
781
+ pruned: /* @__PURE__ */ new Set()
782
+ };
783
+ try {
784
+ const session = services.get("agents")?.get(sessionId)?.session;
785
+ const nodes = session?.surface?.nodes;
786
+ if (session === void 0 || typeof session.snapshotEvents !== "function" || !Array.isArray(nodes)) {
787
+ degraded.push("live session view unavailable: load states cannot be determined");
788
+ return empty;
789
+ }
790
+ const events = session.snapshotEvents();
791
+ if (!Array.isArray(events)) {
792
+ degraded.push("snapshotEvents() returned an unexpected shape; cannot read skill loads");
793
+ return empty;
794
+ }
795
+ const surfaceSeqs = /* @__PURE__ */ new Set();
796
+ for (const seq of nodes) if (typeof seq === "number") surfaceSeqs.add(seq);
797
+ const loads = collectLoadRecords(events);
798
+ const resultSeqs = indexToolResultSeqs(events);
799
+ return {
800
+ loads,
801
+ shadowed: shadowedLoadSeqs(loads, resultSeqs, surfaceSeqs),
802
+ pruned: prunedLoadSeqs(loads, resultSeqs, surfaceSeqs, events)
803
+ };
804
+ } catch (error) {
805
+ degraded.push(`event read failed: ${error instanceof Error ? error.message : String(error)}`);
806
+ return empty;
807
+ }
808
+ }
809
+ function readMcp(services, degraded, disabledServers, disabledTools, agent, presetName, presetPath) {
810
+ const tools = services.get("tools");
811
+ if (tools === void 0) {
812
+ degraded.push("tools service unavailable");
813
+ return [];
814
+ }
815
+ try {
816
+ const names = [];
817
+ const descriptions = /* @__PURE__ */ new Map();
818
+ const collect = (scope) => {
819
+ for (const schema of tools.schemas(scope)) {
820
+ if (typeof schema.name !== "string" || !schema.name.startsWith("mcp__")) continue;
821
+ if (!names.includes(schema.name)) {
822
+ names.push(schema.name);
823
+ if (typeof schema.description === "string" && schema.description !== "") descriptions.set(schema.name, schema.description);
824
+ }
825
+ }
826
+ };
827
+ collect(void 0);
828
+ if (agent !== void 0) collect(agent);
829
+ const globalNames = /* @__PURE__ */ new Set();
830
+ for (const schema of tools.schemas()) if (typeof schema.name === "string" && schema.name.startsWith("mcp__")) globalNames.add(schema.name);
831
+ return groupMcpTools(names).map((group) => {
832
+ const enabled = !disabledServers.has(group.server);
833
+ const entries = group.tools.map((tool) => {
834
+ const name = `mcp__${group.server}__${tool}`;
835
+ const description = descriptions.get(name);
836
+ return {
837
+ name,
838
+ label: tool,
839
+ ...description === void 0 ? {} : { description },
840
+ enabled: enabled && !disabledTools.has(name)
841
+ };
842
+ });
843
+ const allGlobal = group.tools.every((tool) => globalNames.has(`mcp__${group.server}__${tool}`));
844
+ const source = allGlobal ? "host" : presetName ?? "preset";
845
+ const rawPath = allGlobal ? dshHome() : presetPath;
846
+ return {
847
+ server: group.server,
848
+ tools: entries,
849
+ enabled,
850
+ source,
851
+ ...rawPath === void 0 ? {} : { path: displayPath(rawPath) }
852
+ };
853
+ });
854
+ } catch (error) {
855
+ degraded.push(`tool read failed: ${error instanceof Error ? error.message : String(error)}`);
856
+ return [];
857
+ }
858
+ }
859
+ function readSystemTools(services, degraded, disabledTools, agent) {
860
+ const tools = services.get("tools");
861
+ if (tools === void 0) {
862
+ if (!degraded.includes("tools service unavailable")) degraded.push("tools service unavailable");
863
+ return [];
864
+ }
865
+ try {
866
+ let reachable;
867
+ if (agent !== void 0) {
868
+ reachable = /* @__PURE__ */ new Set();
869
+ for (const schema of tools.schemas(agent)) if (typeof schema.name === "string") reachable.add(schema.name);
870
+ }
871
+ const byName = /* @__PURE__ */ new Map();
872
+ const collect = (scope) => {
873
+ for (const schema of tools.schemas(scope)) {
874
+ if (typeof schema.name !== "string" || schema.name.startsWith("mcp__") || byName.has(schema.name)) continue;
875
+ const description = typeof schema.description === "string" && schema.description !== "" ? schema.description : void 0;
876
+ byName.set(schema.name, {
877
+ name: schema.name,
878
+ label: schema.name,
879
+ ...description === void 0 ? {} : { description },
880
+ enabled: !disabledTools.has(schema.name) && (reachable === void 0 || reachable.has(schema.name)),
881
+ ...schema.name === RESERVED_TOOL ? { reserved: true } : {}
882
+ });
883
+ }
884
+ };
885
+ collect(void 0);
886
+ if (agent !== void 0) collect(agent);
887
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
888
+ } catch (error) {
889
+ degraded.push(`tool read failed: ${error instanceof Error ? error.message : String(error)}`);
890
+ return [];
891
+ }
892
+ }
893
+ async function buildPayload(services, sessionId, capabilityState = EMPTY_STATE, blocked = {}) {
894
+ const degraded = [];
895
+ const disabledSkills = new Set(capabilityState.skills.keys());
896
+ const disabledServers = new Set(capabilityState.mcpServers.keys());
897
+ const disabledTools = new Set(capabilityState.mcpTools.keys());
898
+ const disabledSystem = new Set(capabilityState.systemTools.keys());
899
+ if (sessionId === null) return {
900
+ sessionId: null,
901
+ skills: [],
902
+ mcp: readMcp(services, degraded, disabledServers, disabledTools),
903
+ systemTools: readSystemTools(services, degraded, disabledSystem),
904
+ blocked,
905
+ ...degraded.length > 0 ? { degraded } : {}
906
+ };
907
+ const agent = services.get("agents")?.get(sessionId);
908
+ let presetName;
909
+ let presetPath;
910
+ let presetDirs = [];
911
+ if (agent !== void 0) {
912
+ const presetId = services.get("agentPresets")?.composedPreset(agent.ctx);
913
+ try {
914
+ const presets = await services.get("agentPresets")?.list();
915
+ presetDirs = (presets ?? []).filter((p) => typeof p.path === "string" && p.path !== "").map((p) => ({
916
+ key: p.name ?? p.id,
917
+ path: p.path
918
+ }));
919
+ if (presetId !== void 0) {
920
+ const preset = presets?.find((p) => p.id === presetId);
921
+ presetName = preset?.name ?? presetId;
922
+ presetPath = preset?.path;
923
+ }
924
+ } catch {
925
+ presetName = presetId;
926
+ }
927
+ }
928
+ const available = await readAvailable(services, sessionId, degraded, presetDirs);
929
+ const logFacts = readLogFacts(services, sessionId, degraded);
930
+ return {
931
+ sessionId,
932
+ skills: decideStates(available, logFacts.loads, logFacts.shadowed, disabledSkills, logFacts.pruned),
933
+ mcp: readMcp(services, degraded, disabledServers, disabledTools, agent, presetName, presetPath),
934
+ systemTools: readSystemTools(services, degraded, disabledSystem, agent),
935
+ blocked,
936
+ ...degraded.length > 0 ? { degraded } : {}
937
+ };
938
+ }
939
+
672
940
  //#endregion
673
941
  //#region src/host/preset-tools.ts
674
942
  function requireService(service, message) {
@@ -694,18 +962,32 @@ function toolSummaries(tools, scope) {
694
962
  * workspace would contribute. A project skill is marked rather than dropped:
695
963
  * hiding it would silently shorten the list, while marking it says plainly
696
964
  * that a session opened elsewhere will not see the row.
965
+ *
966
+ * Each row also carries the same provenance the session payload reports:
967
+ * the raw runtime source, the discovery root (parent of the skill's own
968
+ * directory, abbreviated for display), and a `preset:<name>` group when the
969
+ * root sits inside a preset's directory.
697
970
  */
698
- async function presetSkillRows(skills, scope, disabled, cwd) {
971
+ async function presetSkillRows(skills, scope, disabled, cwd, presetDirs = []) {
699
972
  const seen = /* @__PURE__ */ new Map();
700
973
  const add = (summaries, project) => {
701
974
  for (const summary of summaries) {
702
975
  if (typeof summary.name !== "string" || summary.name === "" || seen.has(summary.name)) continue;
703
976
  const description = typeof summary.description === "string" ? summary.description : void 0;
977
+ const source = typeof summary.source === "string" ? summary.source : void 0;
978
+ const base = summary.resourceBase;
979
+ const rawPath = base !== null && typeof base === "object" && base.kind === "directory" ? base.path : void 0;
980
+ const rawRoot = typeof rawPath === "string" && rawPath !== "" ? parentDir(rawPath) : "";
981
+ const path = rawRoot === "" ? void 0 : displayPath(rawRoot, cwd);
982
+ const owner = source === "custom" && rawRoot !== "" ? presetDirs.find((p) => rawRoot === p.path || rawRoot.startsWith(`${p.path}/`)) : void 0;
704
983
  seen.set(summary.name, {
705
984
  name: summary.name,
706
985
  ...description === void 0 ? {} : { description },
707
986
  enabled: !disabled.has(summary.name),
708
- ...project ? { project: true } : {}
987
+ ...project ? { project: true } : {},
988
+ ...source === void 0 ? {} : { source },
989
+ ...path === void 0 ? {} : { path },
990
+ ...owner === void 0 ? {} : { group: `preset:${owner.key}` }
709
991
  });
710
992
  }
711
993
  };
@@ -743,6 +1025,12 @@ function createPresetToolController(ctx, access) {
743
1025
  const skills = ctx.get("skills");
744
1026
  const cwd = process.cwd();
745
1027
  const presets = await agentPresets.list();
1028
+ const presetDirs = presets.filter((p) => typeof p.path === "string" && p.path !== "").map((p) => ({
1029
+ key: p.name ?? p.id,
1030
+ path: p.path
1031
+ }));
1032
+ const globalNames = /* @__PURE__ */ new Set();
1033
+ for (const schema of tools.schemas()) if (typeof schema.name === "string" && schema.name.startsWith("mcp__")) globalNames.add(schema.name);
746
1034
  return {
747
1035
  presets: await Promise.all(presets.map(async (preset) => {
748
1036
  let entries = [];
@@ -758,7 +1046,7 @@ function createPresetToolController(ctx, access) {
758
1046
  if (skills !== void 0) {
759
1047
  const disabledSkills = new Set(stored.presetSkills[preset.id] ?? []);
760
1048
  try {
761
- skillRows = await presetSkillRows(skills, scope, disabledSkills, cwd);
1049
+ skillRows = await presetSkillRows(skills, scope, disabledSkills, cwd, presetDirs);
762
1050
  } catch (error) {
763
1051
  throw new HttpError(503, `preset "${preset.id}" skills are unavailable: ${errorMessage(error)}`);
764
1052
  }
@@ -776,12 +1064,17 @@ function createPresetToolController(ctx, access) {
776
1064
  ...name === RESERVED_TOOL ? { reserved: true } : {}
777
1065
  };
778
1066
  };
1067
+ const presetName = preset.name ?? preset.id;
779
1068
  const mcp = groupMcpTools(entries.map((entry) => entry.name)).map((group) => {
780
1069
  const tools$1 = group.tools.map((tool) => row(`mcp__${group.server}__${tool}`, tool));
1070
+ const allGlobal = group.tools.every((tool) => globalNames.has(`mcp__${group.server}__${tool}`));
1071
+ const rawPath = allGlobal ? dshHome() : preset.path;
781
1072
  return {
782
1073
  server: group.server,
783
1074
  tools: tools$1,
784
- enabled: tools$1.some((tool) => tool.enabled)
1075
+ enabled: tools$1.some((tool) => tool.enabled),
1076
+ source: allGlobal ? "host" : presetName,
1077
+ ...rawPath === void 0 ? {} : { path: displayPath(rawPath) }
785
1078
  };
786
1079
  });
787
1080
  const systemTools = entries.filter((entry) => !entry.name.startsWith("mcp__")).map((entry) => row(entry.name, entry.name));
@@ -904,212 +1197,21 @@ function isLoopback(req) {
904
1197
  }
905
1198
 
906
1199
  //#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
- }
1200
+ //#region src/host/open-folder.ts
962
1201
  /**
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.
1202
+ * Open a folder in the system file manager.
965
1203
  *
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.
980
- *
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.
1204
+ * Covers macOS (`open`), Windows (`start`), and every freedesktop Linux
1205
+ * desktop (GNOME, KDE, XFCE, …) via `xdg-open`. Server-only Linux distros
1206
+ * that lack a desktop cannot open folders, but the error is graceful.
984
1207
  */
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
- };
1208
+ function openFolder(path) {
1209
+ return new Promise((resolve, reject) => {
1210
+ exec(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} "${path}"`, (error) => {
1211
+ if (error) reject(/* @__PURE__ */ new Error(`failed to open folder: ${error.message}`));
1212
+ else resolve();
1048
1213
  });
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
- };
1214
+ });
1113
1215
  }
1114
1216
 
1115
1217
  //#endregion
@@ -1253,6 +1355,74 @@ function createRouteHandler(services, capabilities, stats, blockedCounts, preset
1253
1355
  }, true);
1254
1356
  return;
1255
1357
  }
1358
+ if (url.pathname === `${ROUTE}/open-folder`) {
1359
+ if (req.method !== "POST") {
1360
+ res.writeHead(405, {
1361
+ allow: "POST",
1362
+ "content-type": "text/plain; charset=utf-8"
1363
+ });
1364
+ res.end("method not allowed");
1365
+ return;
1366
+ }
1367
+ if (!validatePresetContentType(req, res)) return;
1368
+ const body = await readRequestBody(req);
1369
+ if (body === null || typeof body !== "object") {
1370
+ json(res, 400, { error: "invalid request body" });
1371
+ return;
1372
+ }
1373
+ const record = body;
1374
+ if (typeof record.source !== "string" || record.source === "") {
1375
+ json(res, 400, { error: "source is required" });
1376
+ return;
1377
+ }
1378
+ const source = record.source;
1379
+ const sessionId$1 = typeof record.sessionId === "string" ? record.sessionId : null;
1380
+ let folderPath;
1381
+ if (sessionId$1 !== null) try {
1382
+ const skills = services.get("skills");
1383
+ const agent = services.get("agents")?.get(sessionId$1);
1384
+ if (skills !== void 0 && agent !== void 0) {
1385
+ const cwd = agent.session?.header?.cwd;
1386
+ const list = await skills.list({
1387
+ ...cwd === void 0 ? {} : { cwd },
1388
+ scope: agent
1389
+ });
1390
+ for (const item of list) {
1391
+ if (item.source !== source) continue;
1392
+ const base = item.resourceBase;
1393
+ if (base !== null && typeof base === "object" && base.kind === "directory") {
1394
+ const path = base.path;
1395
+ if (typeof path === "string" && path !== "") {
1396
+ folderPath = parentDir(path);
1397
+ break;
1398
+ }
1399
+ }
1400
+ }
1401
+ }
1402
+ } catch {}
1403
+ if (folderPath === void 0 && (source === "user-dsh" || source === "user-agents")) {
1404
+ const dshHome$1 = process.env["DSH_HOME"] ?? (process.env["HOME"] !== void 0 ? `${process.env["HOME"]}/.dsh` : void 0);
1405
+ if (dshHome$1 !== void 0) folderPath = source === "user-dsh" ? `${dshHome$1}/skills` : `${process.env["DSH_AGENTS_HOME"] ?? `${process.env["HOME"]}/.agents`}/skills`;
1406
+ } else if (folderPath === void 0 && (source === "project-dsh" || source === "project-agents")) {
1407
+ const cwd = (sessionId$1 === null ? void 0 : services.get("agents")?.get(sessionId$1))?.session?.header?.cwd ?? (sessionId$1 === null ? process.cwd() : void 0);
1408
+ if (cwd !== void 0) folderPath = source === "project-dsh" ? `${cwd}/.dsh/skills` : `${cwd}/.agents/skills`;
1409
+ } else if (folderPath === void 0 && source === "host") folderPath = process.env["DSH_HOME"] ?? (process.env["HOME"] !== void 0 ? `${process.env["HOME"]}/.dsh` : void 0);
1410
+ else if (folderPath === void 0 && source !== "bundled" && source !== "runtime") try {
1411
+ const preset = (await services.get("agentPresets")?.list())?.find((p) => p.id === source || p.name === source);
1412
+ if (preset !== void 0) folderPath = preset.path;
1413
+ } catch {}
1414
+ if (folderPath === void 0) {
1415
+ json(res, 404, { error: `cannot open folder for source "${source}"` });
1416
+ return;
1417
+ }
1418
+ try {
1419
+ await openFolder(folderPath);
1420
+ json(res, 200, { ok: true });
1421
+ } catch (error) {
1422
+ json(res, 500, { error: `failed to open folder: ${errorMessage(error)}` });
1423
+ }
1424
+ return;
1425
+ }
1256
1426
  if (url.pathname !== ROUTE && url.pathname !== "/") {
1257
1427
  res.writeHead(404, {
1258
1428
  "content-type": "text/plain; charset=utf-8",
@@ -1367,6 +1537,36 @@ function createSessionOverrideStore(access) {
1367
1537
  };
1368
1538
  }
1369
1539
 
1540
+ //#endregion
1541
+ //#region src/host/settings-scope.ts
1542
+ const TOOLKIT_SETTINGS_NAMESPACE = "capability-panel";
1543
+ const SessionOverrideSchema = z.object({
1544
+ skills: z.dict(z.boolean()).default({}),
1545
+ mcpServers: z.dict(z.boolean()).default({}),
1546
+ mcpTools: z.dict(z.boolean()).default({}),
1547
+ systemTools: z.dict(z.boolean()).default({})
1548
+ });
1549
+ const ToolkitSettingsSchema = z.object({
1550
+ presets: z.dict(z.array(z.string())).default({}),
1551
+ presetSkills: z.dict(z.array(z.string())).default({}),
1552
+ sessions: z.dict(SessionOverrideSchema).default({})
1553
+ });
1554
+ function createToolkitSettingsAccess(ctx) {
1555
+ let scope;
1556
+ let writeQueue = Promise.resolve();
1557
+ return {
1558
+ scope() {
1559
+ if (scope === void 0) scope = ctx.get("settings")?.register(TOOLKIT_SETTINGS_NAMESPACE, ToolkitSettingsSchema, { applies: "live" });
1560
+ return scope;
1561
+ },
1562
+ serialize(work) {
1563
+ const next = writeQueue.then(work, work);
1564
+ writeQueue = next.then(() => void 0, () => void 0);
1565
+ return next;
1566
+ }
1567
+ };
1568
+ }
1569
+
1370
1570
  //#endregion
1371
1571
  //#region src/host/stats-store.ts
1372
1572
  function statsFilePath(environment = process.env, home = homedir()) {