@michengai/dsh-skills-manager 0.1.29 → 0.1.31

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/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@
4
4
 
5
5
  The five most recent published versions are listed below.
6
6
 
7
+ ## Unreleased
8
+
9
+ ## 0.1.31 — 2026-08-30
10
+
11
+ - Added project Skill discovery for active Session workspaces, covering project-root `.dsh/skills` and `.agents/skills` with stable workspace-specific identities and official rank order.
12
+ - Kept project files under DSH's scoped filesystem provider instead of registering a duplicate provider or reusing user-global toggle state.
13
+ - Labeled project entries as discovered rather than loaded, exposed workspace-specific rank/path evidence and a manual Refresh action, and warned when an active workspace is not readable from the host.
14
+ - Added regression coverage for nearest-git-root resolution, duplicate Session workspaces, same-name priority within one project, same-name isolation across projects, live modification/removal, stale source identities, and linked bundles outside the project skill root.
15
+ - Added Settings creation and recoverable Trash deletion for active project `.dsh/skills`, reusing the global whole-bundle rollback/publish flow while keeping project `.agents/skills` read-only.
16
+ - Trash now records the original user or project source, displays it in Settings, and restores project entries only after resolving the same opaque root from a currently active Session workspace.
17
+ - Added enable/disable controls for project `.dsh/skills`; they update the Skill's invocation-policy fields while leaving Session-scoped catalog ownership with DSH's official provider.
18
+ - Fixed project Trash and restore failing with `EXDEV` when the project and `$DSH_HOME` are on different volumes by using a guarded copy-then-hide fallback.
19
+ - Hid empty project roots from the main source list while preserving them as Create Skill destinations, and removed the unsupported project source-wide toggle without affecting per-Skill enable/disable.
20
+ - Fixed writable project discovery falling back to a non-Git Session cwd and potentially aliasing user-level Skill roots; project sources now require a real `.git` ancestor and are hidden and rejected when they overlap any user Skill root.
21
+ - Stopped list and detail scans from following linked Skill roots, including project `.dsh`/`.agents` containers and Windows reparse points, while retaining the existing linked-bundle containment guard.
22
+
23
+ Published package: [`@michengai/dsh-skills-manager@0.1.31`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.31).
24
+
25
+ ## 0.1.30 — 2026-08-28
26
+
27
+ - Fixed reverse-proxy domains and LAN IPs already trusted by DSH Web through `--trusted-host` still being rejected by Skills Manager's own loopback-only Host fence.
28
+ - Reused `webRuntime.trustedHosts` with DSH-compatible authority semantics: port-less entries match any port, explicit ports match exactly, and unknown Hosts, non-canonical authorities, foreign Origins, and explicit cross-site requests remain blocked.
29
+ - Added isolated real-DSH startup verification and regression coverage for domains, LAN IPs, ports, Origins, cross-site requests, and mutation markers.
30
+
31
+ Published package: [`@michengai/dsh-skills-manager@0.1.30`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.30).
32
+
7
33
  ## 0.1.29 — 2026-08-28
8
34
 
9
35
  - Restored the standard DSH README header navigation: the Changelog link now sits between the language switch and the Apache-2.0 license link.
@@ -23,20 +49,3 @@ Published package: [`@michengai/dsh-skills-manager@0.1.28`](https://www.npmjs.co
23
49
  - Verified the reporter-provided cross-platform fnOS `trim-cli` Skill archive through the complete isolated import path.
24
50
 
25
51
  Published package: [`@michengai/dsh-skills-manager@0.1.27`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.27).
26
-
27
- ## 0.1.26 — 2026-08-27
28
-
29
- - Fixed browser folder imports that exceeded the former 16 MiB Base64 JSON request ceiling while still fitting within the documented 25 MiB decoded-content limit.
30
- - Added client-side checks for the existing 10 MiB ZIP, 5 MiB per-file, 25 MiB total, and 500-entry upload limits so oversized selections fail with a specific message before being read.
31
- - Replaced the large-string Base64 validation regex with stack-safe validation to prevent multi-MiB files from exhausting the JavaScript call stack.
32
-
33
- Published package: [`@michengai/dsh-skills-manager@0.1.26`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.26).
34
-
35
- ## 0.1.25 — 2026-08-27
36
-
37
- - Added provider-backed loading for public Agents, Codex, Claude, Gemini, and OpenCode while keeping external source files read-only and persisting toggles in manager state.
38
- - Added Markdown details, diagnostics, conversational creation, recoverable Trash, and second-stage permanent deletion.
39
- - Replaced chained importing with one upload dialog for `.zip`, skill folders, and a single `SKILL.md`, with path, size, count, and archive safety limits.
40
- - Fixed corrupted-state overwrite, trash rollback data loss, Windows `EPERM` publication failures, and the missing Repair-and-enable payload field.
41
-
42
- Published package: [`@michengai/dsh-skills-manager@0.1.25`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.25).
@@ -4,6 +4,32 @@
4
4
 
5
5
  以下记录最近发布的五个版本。
6
6
 
7
+ ## 未发布
8
+
9
+ ## 0.1.31 — 2026-08-30
10
+
11
+ - 新增活动 Session 工作区的项目 Skill 发现,覆盖项目根 `.dsh/skills` 与 `.agents/skills`,使用稳定的 workspace 专属身份并遵循官方优先级。
12
+ - 项目文件继续由 DSH 作用域 filesystem provider 管理,不重复注册 provider,也不复用用户级全局启停状态。
13
+ - 项目条目标记为“已发现”而不是“已加载”,展示 workspace 专属优先级/路径证据和手动刷新入口,并在宿主无法读取活动工作区时给出警告。
14
+ - 补充最近 Git 根、Session 工作区去重、单项目同名优先级、跨项目同名隔离、实时修改/删除、过期来源身份以及项目 Skill 根外链接目录等回归测试。
15
+ - 新增活动项目 `.dsh/skills` 的设置页创建与可恢复回收站删除,复用全局 Skill 的完整目录回滚/发布流程,同时保持项目 `.agents/skills` 只读。
16
+ - 回收站现在记录并展示原用户级或项目级来源;项目条目仅在当前活动 Session 可重新解析出同一不透明根身份时恢复。
17
+ - 新增项目 `.dsh/skills` 启停控件:只改写 Skill 自身调用策略字段,Session 作用域目录加载仍由 DSH 官方 provider 负责。
18
+ - 修复项目与 `$DSH_HOME` 位于不同磁盘时,移入回收站和恢复因 `EXDEV` 失败的问题,新增受保护的“复制后原子隐藏”降级流程。
19
+ - 主来源列表不再显示空项目根,但“创建技能”仍可选择它们;同时移除不受支持的项目来源总开关,不影响逐 Skill 启停。
20
+ - 修复可写项目发现会回落到无 Git 的 Session cwd、进而可能与用户级技能根重合的问题;项目来源现在必须找到真实 `.git` 祖先,与任一用户技能根重叠时直接隐藏并拒绝写入。
21
+ - 列表和详情不再跟随技能根本身的链接,包括项目 `.dsh`/`.agents` 容器及 Windows reparse point,同时保留既有的 bundle 子链接边界检查。
22
+
23
+ 发布包:[`@michengai/dsh-skills-manager@0.1.31`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.31)。
24
+
25
+ ## 0.1.30 — 2026-08-28
26
+
27
+ - 修复 DSH Web 已通过 `--trusted-host` 信任的反向代理域名或局域网 IP,仍被 Skills Manager 自身 loopback-only Host 校验拒绝的问题。
28
+ - 复用 `webRuntime.trustedHosts`,按 DSH 的 authority 语义支持无端口任意端口匹配和带端口精确匹配,同时继续拒绝未知 Host、非规范 authority、异源 Origin 与显式 cross-site 请求。
29
+ - 补充真实 DSH 隔离启动验证以及域名、LAN IP、端口、Origin、跨站请求和写接口标记的回归测试。
30
+
31
+ 发布包:[`@michengai/dsh-skills-manager@0.1.30`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.30)。
32
+
7
33
  ## 0.1.29 — 2026-08-28
8
34
 
9
35
  - 恢复 DSH 系列 README 的标准头部导航:更新日志入口位于语言切换与 Apache-2.0 许可证链接之间。
@@ -23,20 +49,3 @@
23
49
  - 使用反馈者提供的跨平台 fnOS `trim-cli` Skill 压缩包完成隔离环境全链路导入验证。
24
50
 
25
51
  发布包:[`@michengai/dsh-skills-manager@0.1.27`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.27)。
26
-
27
- ## 0.1.26 — 2026-08-27
28
-
29
- - 修复浏览器上传文件夹时,原始内容仍在 25 MiB 总量限制内,却因 Base64 JSON 超过旧 16 MiB 请求体上限而失败的问题。
30
- - 在浏览器读取内容前检查既有的 ZIP 10 MiB、单文件 5 MiB、总量 25 MiB 和 500 个条目限制,超限时直接显示具体原因。
31
- - 将大字符串 Base64 正则校验改为栈安全校验,避免数 MiB 文件耗尽 JavaScript 调用栈。
32
-
33
- 发布包:[`@michengai/dsh-skills-manager@0.1.26`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.26)。
34
-
35
- ## 0.1.25 — 2026-08-27
36
-
37
- - 新增公共 Agent、Codex、Claude、Gemini 与 OpenCode 技能源加载;启停策略仅写入管理器状态,不修改外部源文件。
38
- - 新增正文详情、格式诊断、对话创建、可恢复回收站及二次永久删除。
39
- - 导入改为单弹窗流程,统一支持 `.zip`、技能文件夹和单个 `SKILL.md`,并补充路径、大小、数量与归档安全边界。
40
- - 修复损坏状态文件覆盖、回收站回滚丢失、Windows `EPERM` 发布失败和「修复并启用」字段缺失等问题。
41
-
42
- 发布包:[`@michengai/dsh-skills-manager@0.1.25`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.25)。
package/README.md CHANGED
@@ -23,10 +23,11 @@
23
23
  ## Features
24
24
 
25
25
  - Discover and load user-level skills from `.agents`, Codex, Claude, Gemini, and OpenCode into DSH.
26
+ - Discover project-level `.dsh/skills` and `.agents/skills` from active Session workspaces, grouped by project; project DSH skills can be enabled, disabled, created, moved to Trash, and restored while project Agent skills stay read-only.
26
27
  - Persist external source and skill toggles under `$DSH_HOME/skills-manager/state.json` without rewriting shared source files.
27
28
  - Inspect Markdown bodies, frontmatter, load state, duplicate shadowing, and format diagnostics in a source-first UI.
28
- - Create a local DSH skill from Settings or conversation; conversational writes require user approval.
29
- - Move DSH-local skills to recoverable Trash, then restore or permanently delete them.
29
+ - Create a user- or project-level DSH skill from Settings; conversational creation remains user-level and requires approval.
30
+ - Move user- and project-level DSH skills to recoverable Trash, then restore them to the original source or permanently delete them.
30
31
  - Import `.zip` archives, skill folders, or a single `SKILL.md` safely into `$DSH_HOME/skills`.
31
32
 
32
33
  ## Screenshots
@@ -51,7 +52,6 @@ This product can be installed independently or used through the desktop app or W
51
52
  | --- | --- |
52
53
  | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | The host runtime that provides models, sessions, tools, and the plugin system |
53
54
  | [DSH Codex Desktop](https://github.com/MichengAI/dsh-codex-desktop) | A ready-to-install desktop product with this product and the other five feature products built in |
54
- | [DSH Codex Suite](https://github.com/MichengAI/dsh-codex-ui/tree/main/packages/dsh-codex-suite) | A one-click suite for existing DSH Web environments that installs this product and the other five feature products |
55
55
  | Six feature products | [Codex UI](https://github.com/MichengAI/dsh-codex-ui) · [IM Connect](https://github.com/MichengAI/dsh-im-connect) · [Automation](https://github.com/MichengAI/dsh-automation) · [Skills Manager](https://github.com/MichengAI/dsh-skills-manager) · [Archive Manager](https://github.com/MichengAI/dsh-archive-manager) · [Agency Agents](https://github.com/MichengAI/dsh-agency-agents) |
56
56
 
57
57
  ## Prerequisites
@@ -137,12 +137,12 @@ Open **Settings → Skills**, then use the panel as follows:
137
137
  | --- | --- | --- |
138
138
  | Search or filter | Narrow by source, name, or description. | All sources |
139
139
  | Inspect details | Review body, frontmatter, path, format diagnostics, and duplicate shadowing. | All sources |
140
- | Enable or disable | DSH skills update their own invocation policy; external skills update manager-local state only. | All sources |
141
- | Create or import | Create in Settings, or import `.zip`, a folder containing `SKILL.md`, or one `SKILL.md`. | `$DSH_HOME/skills` |
140
+ | Enable or disable | User and project DSH skills update their own invocation policy; external user skills update manager-local state only. | User-level sources and active project DSH skills |
141
+ | Create or import | Choose a user or active project DSH destination when creating in Settings; imports remain user-level. | `$DSH_HOME/skills`, active `<project>/.dsh/skills` for creation |
142
142
  | Create from conversation | Let an Agent call `create_skill`; DSH asks for approval before writing. | `$DSH_HOME/skills` |
143
- | Delete and recover | Move to Trash, restore, or permanently delete in a second step. | DSH-local skills |
143
+ | Delete and recover | Move to Trash, restore to the original source, or permanently delete in a second step. | User and active-project DSH skills |
144
144
 
145
- > Toggling a shared source never changes its files; only DSH-local skills can move to Trash.
145
+ > Toggling a shared source never changes its files; only user or project DSH skills can move to Trash.
146
146
 
147
147
  Escape closes only the frontmost upload or confirmation dialog and leaves Settings open.
148
148
 
@@ -153,12 +153,21 @@ Escape closes only the frontmost upload or confirmation dialog and leaves Settin
153
153
  | `$DSH_HOME\skills` | Yes | Updates local invocation policy | Yes | Moves to Trash |
154
154
  | `$DSH_AGENTS_HOME\skills` | Yes | Manager state only | No | No |
155
155
  | `~/.codex/skills`, `~/.claude/skills`, `~/.gemini/skills`, `~/.config/opencode/skills` | Yes | Manager state only | No | No |
156
+ | `<project>/.dsh/skills` | Yes, for active Session workspaces | Updates the Skill's invocation policy; DSH's scoped provider remains responsible for loading | Create in Settings | Moves to Trash and restores to the original project |
157
+ | `<project>/.agents/skills` | Yes, for active Session workspaces | No; managed by DSH's scoped filesystem provider | No | No |
156
158
 
157
159
  - Enable, disable, and delete accept only one ordinary skill-name path segment.
160
+ - Project roots are derived only from active Session `cwd` values; client requests carry an opaque source key and cannot nominate an arbitrary workspace path.
161
+ - Project sources follow DSH's nearest-`.git` root convention and rank order (`project-dsh` 100 before `project-agents` 200). The manager re-scans for each state/detail request and does not register a duplicate project provider; writes are limited to invocation-policy toggles and create/Trash/restore under the active project's `.dsh/skills` root.
162
+ - Trash falls back to copy-then-hide when a project and `$DSH_HOME` are on different volumes; restore uses the same guarded cross-volume path in reverse.
163
+ - Project Trash entries retain their original opaque source identity. Restore is allowed only while that original project is still represented by an active Session workspace; the client cannot nominate a replacement path.
164
+ - Project writes reject linked `.dsh` or `.dsh/skills` directories so a repository cannot redirect creation, deletion, or restore outside its own project root.
165
+ - Project rows say **Discovered**, not **Loaded**: only DSH's Session-scoped catalog can establish model visibility. Use Refresh after IDE, Git, or shell changes; the official provider remains responsible for model-catalog watching and invalidation.
166
+ - Empty project roots stay out of the main source list to reduce noise, but remain selectable in Create Skill so the first project Skill can still be created. Project DSH supports per-Skill toggles only, not a source-wide switch.
158
167
  - Replacements copy to a temporary sibling path first and keep the original until that succeeds.
159
- - All endpoints, including GET `/state`, accept only loopback `Host` values (`localhost`, `127.0.0.1`, `[::1]`).
160
- - Write endpoints also require JSON and the DSH client request marker, so cross-site browser requests cannot trigger local file operations.
161
- - Import accepts the local path the user selected. The HTTP API trusts loopback callers only; do not expose the host web server beyond this machine.
168
+ - Every endpoint, including GET `/state`, accepts only a loopback `Host` or a canonical `host[:port]` that the DSH Web runtime already trusts through its LAN bind and `--trusted-host`; unknown hosts still receive 403.
169
+ - Browser requests must also carry a same-origin `Origin` when present and must not be marked cross-site; write endpoints continue to require JSON and the DSH client request marker.
170
+ - Import accepts the local path selected by the user. The Host trust fence prevents DNS rebinding but is not authentication; reverse-proxy and LAN deployments still need authentication, a VPN, or network access controls.
162
171
 
163
172
  ## Secondary development
164
173
 
package/README.zh-CN.md CHANGED
@@ -23,10 +23,11 @@
23
23
  ## 功能概览
24
24
 
25
25
  - 自动发现并把 `.agents`、Codex、Claude、Gemini 和 OpenCode 的用户级技能真正加载进 DSH。
26
+ - 从活动 Session 工作区发现项目级 `.dsh/skills` 与 `.agents/skills` 并按项目分组;项目 DSH Skill 可启停、创建、移入回收站和恢复,项目 Agent Skill 保持只读。
26
27
  - 外部来源启停只写入 `$DSH_HOME\skills-manager\state.json`,绝不改写共享源文件。
27
28
  - 按来源折叠、搜索和筛选,并查看技能正文、frontmatter、加载状态、重名遮蔽与格式诊断。
28
- - 在设置页或对话中创建 DSH 本地技能;对话工具在写入前请求用户确认。
29
- - DSH 本地技能删除后先进入回收站,支持恢复和二次永久删除。
29
+ - 在设置页创建用户级或项目级 DSH Skill;对话创建仍为用户级,并在写入前请求确认。
30
+ - 用户级和项目级 DSH Skill 删除后先进入回收站,可恢复到原来源或二次永久删除。
30
31
  - 支持把 `.zip`、包含 `SKILL.md` 的技能文件夹或单个 `SKILL.md` 安全导入 `$DSH_HOME\skills`。
31
32
 
32
33
  ## 界面预览
@@ -51,7 +52,6 @@ DSH 本地技能移入回收站前需要确认;永久删除前仍可恢复:
51
52
  | --- | --- |
52
53
  | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | 本产品的运行宿主,提供模型、会话、工具和插件系统 |
53
54
  | [DSH Codex Desktop](https://github.com/MichengAI/dsh-codex-desktop) | 下载安装即用的桌面产品,已内置本产品和其他 5 个功能产品 |
54
- | [DSH Codex Suite](https://github.com/MichengAI/dsh-codex-ui/tree/main/packages/dsh-codex-suite) | 面向已有 DSH Web 环境的一键套件,会安装本产品和其他 5 个功能产品 |
55
55
  | 6 个功能产品 | [Codex UI](https://github.com/MichengAI/dsh-codex-ui) · [IM Connect](https://github.com/MichengAI/dsh-im-connect) · [Automation](https://github.com/MichengAI/dsh-automation) · [Skills Manager](https://github.com/MichengAI/dsh-skills-manager) · [Archive Manager](https://github.com/MichengAI/dsh-archive-manager) · [Agency Agents](https://github.com/MichengAI/dsh-agency-agents) |
56
56
 
57
57
  ## 前置条件
@@ -137,12 +137,12 @@ dsh --profile web --dump-config
137
137
  | --- | --- | --- |
138
138
  | 搜索或筛选 | 按来源、名称或简介收窄列表。 | 全部来源 |
139
139
  | 查看详情与诊断 | 查看正文、frontmatter、源文件路径、格式问题和重名遮蔽。 | 全部来源 |
140
- | 启用或停用 | DSH 技能更新自身调用策略;外部技能只更新 manager 本地状态。 | 全部来源 |
141
- | 创建或导入 | 在设置页创建,或导入 `.zip`、包含 `SKILL.md` 的文件夹、单个 `SKILL.md`。 | `$DSH_HOME\skills` |
140
+ | 启用或停用 | 用户级与项目级 DSH Skill 更新自身调用策略;外部用户技能只更新 manager 本地状态。 | 用户级来源与活动项目 DSH Skill |
141
+ | 创建或导入 | 在设置页创建时选择用户 DSH 或活动项目 DSH;导入仍为用户级。 | `$DSH_HOME\skills`,创建可选活动 `<project>/.dsh/skills` |
142
142
  | 从对话创建 | 让 Agent 调用 `create_skill`;写入前由 DSH 审批界面确认。 | `$DSH_HOME\skills` |
143
- | 删除与恢复 | 删除先进入回收站;可恢复或永久二次删除。 | DSH 本地技能 |
143
+ | 删除与恢复 | 删除先进入回收站;可恢复到原来源或永久二次删除。 | 用户级与活动项目级 DSH Skill |
144
144
 
145
- > 共享来源的启停不会修改源文件;只有 DSH 本地技能可以移入回收站。
145
+ > 共享来源的启停不会修改源文件;只有用户级或项目级 DSH Skill 可以移入回收站。
146
146
 
147
147
  按 ESC 只关闭最上层上传框或确认框,设置页会保持打开。
148
148
 
@@ -153,12 +153,21 @@ dsh --profile web --dump-config
153
153
  | `$DSH_HOME\skills` | 支持 | 改写本地调用策略 | 支持 | 进入回收站 |
154
154
  | `$DSH_AGENTS_HOME\skills` | 支持 | 仅写 manager 状态 | 不支持 | 不支持 |
155
155
  | `~/.codex/skills`、`~/.claude/skills`、`~/.gemini/skills`、`~/.config/opencode/skills` | 支持 | 仅写 manager 状态 | 不支持 | 不支持 |
156
+ | `<project>/.dsh/skills` | 支持活动 Session 工作区 | 改写 Skill 调用策略;仍由 DSH 作用域 provider 负责加载 | 设置页支持创建 | 进入回收站并恢复到原项目 |
157
+ | `<project>/.agents/skills` | 支持活动 Session 工作区 | 不支持;由 DSH 作用域 filesystem provider 管理 | 不支持 | 不支持 |
156
158
 
157
159
  - 启用、停用和删除只接受单个普通技能名称,目录穿越名称会被拒绝。
160
+ - 项目根只从活动 Session 的 `cwd` 推导;客户端只提交不透明来源 key,不能指定任意工作区路径。
161
+ - 项目来源遵循 DSH 最近 `.git` 项目根和固定优先级(`project-dsh` 100、`project-agents` 200)。管理器每次读取状态或详情时重新扫描且不重复注册项目 provider;写入仅限活动项目 `.dsh/skills` 下的调用策略启停、创建、回收与恢复。
162
+ - 当项目与 `$DSH_HOME` 位于不同磁盘时,回收站会降级为“复制后在源盘原子隐藏”;恢复使用反向的同一安全流程。
163
+ - 项目回收站条目保存原始不透明来源身份。仅当原项目仍由活动 Session 工作区提供时才允许恢复;客户端不能指定替代路径。
164
+ - 项目写入会拒绝链接形式的 `.dsh` 或 `.dsh/skills` 目录,避免仓库把创建、删除或恢复重定向到项目根之外。
165
+ - 项目条目标记为“已发现”而不是“已加载”:只有 DSH 的 Session 作用域目录才能证明模型可见性。IDE、Git 或 shell 改动后可点击“刷新”;模型目录 watcher 与 invalidation 仍由官方 provider 负责。
166
+ - 没有 Skill 的项目根不会出现在主来源列表中,但仍保留在“创建技能”的目标选择中,确保可以创建第一个项目 Skill。项目 DSH 只提供逐 Skill 启停,不提供整个项目来源总开关。
158
167
  - 覆盖前先复制到同目录临时路径;复制成功前不会改动现有技能。
159
- - 全部接口(含 GET `/state`)只接受 loopback `Host`(`localhost`、`127.0.0.1`、`[::1]`)。
160
- - 写入接口还要求 JSON 与 DSH 客户端请求标记,跨站浏览器请求不能触发本地文件操作。
161
- - 导入接受用户选定的本机路径。HTTP 接口只信任 loopback 来源,不要把宿主 webServer 暴露到非本机地址。
168
+ - 全部接口(含 GET `/state`)只接受 loopback `Host`,或 DSH Web runtime 已通过 LAN 绑定和 `--trusted-host` 明确信任的 `host[:port]`;未知 Host 继续返回 403。
169
+ - 浏览器请求还必须满足同源 `Origin` 且不能标记为 cross-site;写入接口继续要求 JSON 与 DSH 客户端请求标记。
170
+ - 导入接受用户选定的本机路径。Host 信任栅栏用于防 DNS rebinding,不是身份认证;通过反向代理或局域网提供服务时,仍应配置认证、VPN 或网络访问控制。
162
171
 
163
172
  ## 二次开发
164
173
 
package/lib/client.js CHANGED
@@ -13,56 +13,56 @@ window.__ModuleLoader__.load({
13
13
  zh: {
14
14
  "title": "技能", "desc": "统一加载和管理本机 Agent Skills。",
15
15
  "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.repair.enable": "修复并启用",
16
- "status.enabled": "已加载", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.source.on": "已加载", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
16
+ "status.enabled": "已加载", "status.discovered": "已发现", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已加载", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
17
17
  "summary.total": "{count} 个技能", "summary.loaded": "{count} 个已在 DSH 加载", "summary.disabled": "{count} 个已停用", "summary.issues": "{count} 个诊断项", "summary.group": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "DSH 加载状态",
18
18
  "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
19
19
  "empty.search": "没有匹配的技能。", "empty.source": "该来源目录不存在或暂时没有技能。", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
20
20
  "source.external.note": "通过 Skills Manager 只读加载,启停不会改写源文件。", "source.dsh.note": "DSH 本地技能可创建、导入和移到回收站。",
21
21
  "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "Frontmatter", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
22
- "create.title": "创建技能", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "也可以在对话中直接说“创建一个 Skill”,DSH 会在写入前请求确认。",
22
+ "create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话中的 create_skill 仍创建用户级 DSH Skill;项目 Skill 可在这里选择活动项目后创建。",
23
23
  "import.title": "导入技能", "upload.drop.title": "拖拽技能到此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md。", "select.folder.invalid": "所选文件夹中没有找到 SKILL.md。", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
24
- "trash.title": "回收站", "trash.count": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}",
25
- "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "“{name}”将从 DSH 技能目录移入回收站,之后可以恢复。", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "“{name}”将从回收站永久删除,无法恢复。",
24
+ "trash.title": "回收站", "trash.count": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
25
+ "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "“{name}”将从当前技能来源移入回收站,之后可以恢复到原位置。", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "“{name}”将从回收站永久删除,无法恢复。",
26
26
  "result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "error.action": "操作失败:{error}",
27
- "warning.state.invalid": "技能管理器状态文件不可读;外部技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理: {path}({error})",
28
- "error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}",
27
+ "warning.state.invalid": "技能管理器状态文件不可读;外部技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理: {path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
28
+ "error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}",
29
29
  "error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持包含符号链接的 skill 来源: {path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
30
30
  "error.import.overlap": "导入来源不能与 DSH 技能目录相同、包含或位于其中", "error.import.emptySource": "目录下未找到任何 skill 条目: {path}", "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
31
31
  "error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
32
- "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
32
+ "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
33
33
  "error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
34
34
  "error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
35
35
  "error.proto.forbidden": "禁止的修改请求(缺少客户端标记)", "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)", "error.proto.contentType": "请求体必须是 application/json", "error.proto.method": "不支持的请求方法", "error.proto.unknownAction": "未知操作", "error.proto.bodyTooLarge": "请求体过大", "error.proto.invalidJson": "请求体不是合法 JSON", "error.proto.nonJson": "服务端返回非 JSON 响应(HTTP {status})",
36
36
  "diagnostic.frontmatter.missing": "缺少完整 YAML frontmatter", "diagnostic.name.missing": "frontmatter 缺少 name", "diagnostic.name.invalid": "技能名称不是合法 kebab-case:{name}", "diagnostic.description.missing": "frontmatter 缺少 description", "diagnostic.invocation.invalid": "调用策略字段值无效", "diagnostic.shadowed": "被更高优先级来源 {root} 覆盖",
37
- "action.enable": "启用", "action.disable": "停用", "action.delete": "删除", "action.toggle": "启用或停用",
38
- "root.dsh": "DSH 技能", "root.agents": "公共 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode"
37
+ "action.enable": "启用", "action.disable": "停用", "action.create": "创建", "action.delete": "删除", "action.restore": "恢复", "action.toggle": "启用或停用",
38
+ "root.dsh": "DSH 技能", "root.agents": "公共 Agent", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode"
39
39
  },
40
40
  en: {
41
41
  "title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.",
42
42
  "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.repair.enable": "Repair & enable",
43
- "status.enabled": "Loaded", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.source.on": "Loaded", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
43
+ "status.enabled": "Loaded", "status.discovered": "Discovered", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Loaded", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
44
44
  "summary.total": "{count} skills", "summary.loaded": "{count} loaded in DSH", "summary.disabled": "{count} disabled", "summary.issues": "{count} diagnostics", "summary.group": "{count} skills", "table.skill": "Skill name and description", "table.status": "DSH load status",
45
45
  "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
46
46
  "empty.search": "No matching skills.", "empty.source": "This source does not exist or has no skills yet.", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
47
47
  "source.external.note": "Loaded read-only through Skills Manager; toggles never rewrite source files.", "source.dsh.note": "Local DSH skills can be created, imported, and moved to trash.",
48
48
  "detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Frontmatter", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
49
- "create.title": "Create skill", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "You can also say “create a Skill” in chat; DSH asks for confirmation before writing.",
49
+ "create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "The conversational create_skill tool still creates a user-level DSH Skill; choose an active project here for a project Skill.",
50
50
  "import.title": "Import skill", "upload.drop.title": "Drop a skill here", "upload.drop.copy": "Supports .zip, a skill folder, or one SKILL.md", "upload.selected": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
51
- "trash.title": "Trash", "trash.count": "{count} skills pending", "trash.empty": "Trash is empty.", "trash.deletedAt": "Deleted {time}",
52
- "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” will move out of the DSH skills directory and can be restored later.", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” will be permanently deleted from trash and cannot be recovered.",
51
+ "trash.title": "Trash", "trash.count": "{count} skills pending", "trash.empty": "Trash is empty.", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
52
+ "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” will move out of its current skill source and can be restored to the same location later.", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” will be permanently deleted from trash and cannot be recovered.",
53
53
  "result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "error.action": "Action failed: {error}",
54
- "warning.state.invalid": "The manager state file could not be read; external skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})",
55
- "error.root.readonly": "The shared Agent skills directory does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}",
54
+ "warning.state.invalid": "The manager state file could not be read; external skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
55
+ "error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}",
56
56
  "error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Skill sources containing symbolic links are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
57
57
  "error.import.overlap": "Import source cannot be the same as, contain, or be inside the DSH skills directory", "error.import.emptySource": "No skill entries found in the directory: {path}", "error.import.invalidName": "Cannot generate a valid kebab-case name (original: {name})", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
58
58
  "error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
59
- "error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
59
+ "error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
60
60
  "error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
61
61
  "error.create.descriptionRequired": "Skill description is required", "error.create.bodyRequired": "Skill body is required", "error.create.tooLarge": "Skill content is too large", "error.create.conflict": "A skill with the same name already exists: {name}",
62
62
  "error.proto.forbidden": "Forbidden mutation request (missing client marker)", "error.proto.forbiddenHost": "Forbidden request origin (invalid host)", "error.proto.contentType": "Content type must be application/json", "error.proto.method": "Method not allowed", "error.proto.unknownAction": "Unknown action", "error.proto.bodyTooLarge": "Request body too large", "error.proto.invalidJson": "Invalid JSON request body", "error.proto.nonJson": "Server returned a non-JSON response (HTTP {status})",
63
63
  "diagnostic.frontmatter.missing": "Missing complete YAML frontmatter", "diagnostic.name.missing": "Frontmatter is missing name", "diagnostic.name.invalid": "Skill name is not valid kebab-case: {name}", "diagnostic.description.missing": "Frontmatter is missing description", "diagnostic.invocation.invalid": "Invocation policy value is invalid", "diagnostic.shadowed": "Shadowed by higher-priority source {root}",
64
- "action.enable": "enable", "action.disable": "disable", "action.delete": "delete", "action.toggle": "enabling or disabling",
65
- "root.dsh": "DSH skills", "root.agents": "Shared Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode"
64
+ "action.enable": "enable", "action.disable": "disable", "action.create": "create", "action.delete": "delete", "action.restore": "restore", "action.toggle": "enabling or disabling",
65
+ "root.dsh": "DSH skills", "root.agents": "Shared Agent", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode"
66
66
  }
67
67
  };
68
68
 
@@ -90,6 +90,7 @@ window.__ModuleLoader__.load({
90
90
  function parseApiResponse(response) { return response.json().catch(function () { var error = new Error("non-json response"); error.code = "error.proto.nonJson"; error.params = { status: response.status }; throw error; }).then(function (payload) { if (!response.ok || payload.ok === false) throw payload; return payload.data; }); }
91
91
  function callApi(path, options) { return fetch("/api/dsh-skills-manager" + path, options).then(parseApiResponse); }
92
92
  function isSkillEnabled(skill) { if (skill.loaded !== undefined) return skill.loaded === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
93
+ function rootDisplayName(t, root) { var base = translateOrFallback(t, "root." + (root.localeKey || root.kind || root.key), root.label); return root.projectName ? base + " · " + root.projectName : base; }
93
94
  function summarizeImportResult(t, data) {
94
95
  var importedItems = data && data.imported || [];
95
96
  var imported = importedItems.map(function (item) { return item.name; });
@@ -112,6 +113,7 @@ window.__ModuleLoader__.load({
112
113
  function normalizeSkillQuery(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
113
114
  function matchSkillQuery(skill, query) { var q = normalizeSkillQuery(query); if (!q) return true; return [skill.name, skill.declaredName, skill.description, skill.kind, skill.kindLabel, skill.statusLabel, skill.rootKey, skill.rootLabel].some(function (value) { return String(value == null ? "" : value).toLowerCase().includes(q); }); }
114
115
  function filterSkills(list, options) { var rootKey = options && options.rootKey != null ? options.rootKey : ""; return list.filter(function (skill) { return (!rootKey || skill.rootKey === rootKey) && matchSkillQuery(skill, options && options.query); }); }
116
+ function visibleSkillRoots(roots) { return (roots || []).filter(function (root) { return root.scope !== "project" || (root.skills || []).length > 0; }); }
115
117
  var MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20, MAX_UPLOAD_ENTRY_BYTES = 32 << 20, MAX_UPLOAD_TOTAL_BYTES = 64 << 20, MAX_UPLOAD_ENTRIES = 1000;
116
118
  function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
117
119
  function inspectUploadSelection(files) {
@@ -158,7 +160,7 @@ window.__ModuleLoader__.load({
158
160
  var es = react.useState({ codex: true }), expanded = es[0], setExpanded = es[1];
159
161
  var ms = react.useState(null), modal = ms[0], setModal = ms[1];
160
162
  var rs = react.useState(null), result = rs[0], setResult = rs[1];
161
- var fms = react.useState({ name: "", description: "", body: "" }), form = fms[0], setForm = fms[1];
163
+ var fms = react.useState({ root: "dsh", name: "", description: "", body: "" }), form = fms[0], setForm = fms[1];
162
164
  var us = react.useState(null), upload = us[0], setUpload = us[1];
163
165
  var ds = react.useState(null), detail = ds[0], setDetail = ds[1];
164
166
  var inflightRef = react.useRef(false);
@@ -170,27 +172,30 @@ window.__ModuleLoader__.load({
170
172
  function post(path, body, successKey, successParams) { if (inflightRef.current) return Promise.reject({ error: "operation already in progress" }); inflightRef.current = true; setBusy(true); setResult(null); return callApi(path, { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify(body || {}) }).then(function (data) { setResult({ ok: true, text: successKey ? t(successKey, successParams || data || {}) : t("result.updated") }); return refresh(true).then(function () { return data; }); }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); throw error; }).finally(function () { inflightRef.current = false; setBusy(false); }); }
171
173
  function openDetail(root, skill) { setBusy(true); setDetail(null); setModal("detail"); callApi("/detail", { method: "POST", headers: MUTATION_HEADERS, body: JSON.stringify({ root: root.key, name: skill.name }) }).then(setDetail).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); setModal(null); }).finally(function () { setBusy(false); }); }
172
174
  function updateForm(key, value) { setForm(Object.assign({}, form, { [key]: value })); }
173
- function submitCreate() { post("/create", form, null).then(function (data) { setModal(null); setForm({ name: "", description: "", body: "" }); setResult({ ok: true, text: t("result.created", { name: data.name }) }); }).catch(function () {}); }
175
+ function submitCreate() { post("/create", form, null).then(function (data) { setModal(null); setForm({ root: "dsh", name: "", description: "", body: "" }); setResult({ ok: true, text: t("result.created", { name: data.name }) }); }).catch(function () {}); }
174
176
  function selectUploadFiles(files) { pickerOpenRef.current = false; var selected = inspectUploadSelection(files); if (!selected) return; if (selected.error) { setUpload(null); setResult({ ok: false, text: translateError(t, selected.error) }); return; } setUpload(selected); setResult(null); }
175
177
  function openNativePicker(ref) { if (busy || pickerOpenRef.current || !ref.current) return; pickerOpenRef.current = true; ref.current.value = ""; function release() { setTimeout(function () { pickerOpenRef.current = false; }, 0); } window.addEventListener("focus", release, { once: true }); ref.current.click(); setTimeout(function () { pickerOpenRef.current = false; }, 30000); }
176
178
  function submitImport() { if (!upload) return; buildUploadPayload(upload).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
177
- var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, loaded: 0, disabled: 0, issues: 0 } }, roots = data.roots || [];
178
- var options = [{ value: "", label: t("filter.all") }].concat(roots.map(function (root) { return { value: root.key, label: t("filter.option", { name: translateOrFallback(t, "root." + root.key, root.label), count: root.count == null ? root.skills.length : root.count }) }; }));
179
+ var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, loaded: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
180
+ var createRoots = allRoots.filter(function (root) { return root.mutable === true; }), createOptions = createRoots.map(function (root) { return { value: root.key, label: rootDisplayName(t, root) }; }); if (!createOptions.length) createOptions.push({ value: "dsh", label: t("root.dsh") });
181
+ function openCreate() { var selectedRoot = createRoots.some(function (root) { return root.key === activeSource; }) ? activeSource : "dsh"; setForm(Object.assign({}, form, { root: selectedRoot })); setModal("create"); }
182
+ function trashRootLabel(item) { return item.root && item.root.scope === "project" ? t("root.projectDsh") + " · " + (item.root.projectName || item.root.projectRoot) : t("root.dsh"); }
183
+ var options = [{ value: "", label: t("filter.all") }].concat(roots.map(function (root) { return { value: root.key, label: t("filter.option", { name: rootDisplayName(t, root), count: root.count == null ? root.skills.length : root.count }) }; }));
179
184
 
180
185
  function renderSkill(root, skill) {
181
- var enabled = isSkillEnabled(skill), repairable = root.mutable && skill.hasFrontmatter && skill.invocationPolicyValid === false && !skill.shadowedBy, key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false || !skill.invocationPolicyValid ? "status.invalid" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dssm-shadowed" : enabled ? "dssm-loaded" : "dssm-disabled";
182
- return h("div", { key: skill.name, className: "dssm-row" }, h("div", { className: "dssm-main" }, h("div", { className: "dssm-name" }, skill.declaredName || skill.name), h("div", { className: "dssm-note" }, skill.description || t("note.missing"))), h("div", { className: "dssm-tags" }, h("span", { className: "dssm-tag" }, translateOrFallback(t, "root." + root.key, root.label)), !root.mutable ? h("span", { className: "dssm-tag" }, t("status.readonly")) : null), h("div", { className: "dssm-status " + cls }, t(key)), h("div", { className: "dssm-row-actions" }, repairable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/enable", { root: root.key, name: skill.name }); } }, t("btn.repair.enable")) : h(Switch, { on: enabled, disabled: busy || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }), h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
186
+ var enabled = isSkillEnabled(skill), repairable = root.mutable && skill.hasFrontmatter && skill.invocationPolicyValid === false && !skill.shadowedBy, key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false || !skill.invocationPolicyValid ? "status.invalid" : root.scope === "project" && enabled ? "status.discovered" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dssm-shadowed" : enabled ? "dssm-loaded" : "dssm-disabled";
187
+ return h("div", { key: skill.name, className: "dssm-row" }, h("div", { className: "dssm-main" }, h("div", { className: "dssm-name" }, skill.declaredName || skill.name), h("div", { className: "dssm-note" }, skill.description || t("note.missing"))), h("div", { className: "dssm-tags" }, h("span", { className: "dssm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dssm-tag" }, t("status.readonly")) : null), h("div", { className: "dssm-status " + cls }, t(key)), h("div", { className: "dssm-row-actions" }, repairable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/enable", { root: root.key, name: skill.name }); } }, t("btn.repair.enable")) : root.toggleable !== false ? h(Switch, { on: enabled, disabled: busy || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
183
188
  }
184
189
  function renderRoot(root) {
185
- if (source && source !== root.key) return null;
186
- var filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: root.label }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
187
- return h("section", { key: root.key, className: "dssm-source" }, h("div", { className: "dssm-source-head" }, h("button", { type: "button", className: "dssm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dssm-source-title" }, translateOrFallback(t, "root." + root.key, root.label)), h("span", { className: "dssm-count" }, t("summary.group", { count: root.count == null ? root.skills.length : root.count })), h("span", { className: "dssm-tag " + (root.key === "dsh" ? "" : root.enabled ? "dssm-tag-on" : "dssm-tag-off") }, root.key === "dsh" ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), h("span", { className: "dssm-path", title: root.path }, root.path)), root.key !== "dsh" ? h("span", { className: "dssm-source-actions" }, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + root.label, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dssm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dssm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dssm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
190
+ if (activeSource && activeSource !== root.key) return null;
191
+ var displayName = rootDisplayName(t, root), filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: displayName }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
192
+ return h("section", { key: root.key, className: "dssm-source" }, h("div", { className: "dssm-source-head" }, h("button", { type: "button", className: "dssm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dssm-source-title" }, displayName), h("span", { className: "dssm-count" }, t("summary.group", { count: root.count == null ? root.skills.length : root.count })), h("span", { className: "dssm-tag " + (root.scope === "project" ? "dssm-tag-on" : root.key === "dsh" ? "" : root.enabled ? "dssm-tag-on" : "dssm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dssm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dssm-path", title: root.path }, root.path)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dssm-source-actions" }, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dssm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dssm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dssm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
188
193
  }
189
194
 
190
195
  var summary = data.summary || { total: 0, loaded: 0, disabled: 0, issues: 0 };
191
- var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn", onClick: function () { setModal("create"); } }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.loaded, "summary.loaded"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(item[1], { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: source, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
196
+ var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.loaded, "summary.loaded"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(item[1], { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
192
197
 
193
- if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-form" }, [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dssm-field" }, h("span", { className: "dssm-label" }, t(field[1])), h("input", { className: "dssm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.body")), h("textarea", { className: "dssm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dssm-help" }, t("create.chat.note"))), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
198
+ if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-form" }, h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dssm-field" }, h("span", { className: "dssm-label" }, t(field[1])), h("input", { className: "dssm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.body")), h("textarea", { className: "dssm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dssm-help" }, t("create.chat.note"))), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
194
199
  if (modal === "import") content.push(h(Modal, { key: "import", className: "dssm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
195
200
  h("input", { ref: importInputRef, className: "dssm-hidden-input", type: "file", accept: ".zip,.md", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
196
201
  h("input", { ref: folderInputRef, className: "dssm-hidden-input", type: "file", multiple: true, webkitdirectory: "", directory: "", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
@@ -204,7 +209,7 @@ window.__ModuleLoader__.load({
204
209
  h("div", { className: "dssm-upload-requirements" }, h("div", { className: "dssm-label" }, t("upload.requirements")), h("ul", null, h("li", null, t("upload.requirement.skill")), h("li", null, t("upload.requirement.frontmatter")), h("li", null, t("upload.requirement.copy")))),
205
210
  h("div", { className: "dssm-modal-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } }, t("btn.cancel")), h("button", { type: "button", className: "dssm-btn", disabled: busy || !upload, onClick: submitImport }, busy ? t("upload.importing") : t("btn.import.now")))
206
211
  )); if (modal === "detail") content.push(h(Modal, { key: "detail", wide: true, title: t("detail.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, detail ? h(react.Fragment, null, h("div", { className: "dssm-detail-path" }, detail.path), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.diagnostics")), detail.diagnostics.length ? detail.diagnostics.map(function (item, index) { return h("div", { key: index, className: "dssm-diag" }, t(item.code, item.params || {})); }) : h("div", { className: "dssm-note" }, t("detail.noIssues"))), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.body")), h("pre", { className: "dssm-code" }, detail.body || "")), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.frontmatter")), h("pre", { className: "dssm-code" }, JSON.stringify(detail.frontmatter, null, 2)))) : h("div", { className: "dssm-empty" }, t("loading"))));
207
- if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-count" }, t("trash.count", { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dssm-trash-item" }, h("div", { className: "dssm-trash-main" }, h("div", { className: "dssm-name" }, item.name), h("div", { className: "dssm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }))), h("button", { className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dssm-btn dssm-btn-quiet dssm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dssm-empty" }, t("trash.empty"))));
212
+ if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-count" }, t("trash.count", { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dssm-trash-item" }, h("div", { className: "dssm-trash-main" }, h("div", { className: "dssm-name" }, item.name), h("div", { className: "dssm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), h("button", { className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dssm-btn dssm-btn-quiet dssm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dssm-empty" }, t("trash.empty"))));
208
213
  if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dssm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(function () {}); } }, t("btn.trash")))));
209
214
  if (modal && modal.type === "delete-confirm") content.push(h(Modal, { key: "delete-confirm", title: t("confirm.delete.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dssm-desc" }, t("confirm.delete.desc", { name: modal.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal("trash"); } }, t("btn.cancel")), h("button", { className: "dssm-btn dssm-btn-danger", disabled: busy, onClick: function () { post("/trash-delete", { id: modal.id }, "result.deleted", { name: modal.name }).then(function () { setModal("trash"); }).catch(function () {}); } }, t("btn.delete.forever")))));
210
215
  return h("section", { className: "dssm-section" }, content);
@@ -212,7 +217,7 @@ window.__ModuleLoader__.load({
212
217
 
213
218
  var inject = ["slots", "locale"];
214
219
  function apply(ctx) { ctx.effect(function () { return ctx.locale.register(NS, DICT); }); ctx.slots.inject("settings.section", function () { return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: function () { return ctx.locale.bind(NS)("title"); }, icon: "skill", locale: NS }, SkillManagerSection); }); }
215
- module.exports.DICT = DICT; module.exports.translateError = translateError; module.exports.parseApiResponse = parseApiResponse; module.exports.isSkillEnabled = isSkillEnabled; module.exports.summarizeImportResult = summarizeImportResult; module.exports.normalizeSkillQuery = normalizeSkillQuery; module.exports.matchSkillQuery = matchSkillQuery; module.exports.filterSkills = filterSkills; module.exports.trapModalFocus = trapModalFocus; module.exports.handleModalEscape = handleModalEscape; module.exports.inspectUploadSelection = inspectUploadSelection; module.exports.apply = apply; module.exports.inject = inject;
220
+ module.exports.DICT = DICT; module.exports.translateError = translateError; module.exports.parseApiResponse = parseApiResponse; module.exports.isSkillEnabled = isSkillEnabled; module.exports.rootDisplayName = rootDisplayName; module.exports.summarizeImportResult = summarizeImportResult; module.exports.normalizeSkillQuery = normalizeSkillQuery; module.exports.matchSkillQuery = matchSkillQuery; module.exports.filterSkills = filterSkills; module.exports.visibleSkillRoots = visibleSkillRoots; module.exports.trapModalFocus = trapModalFocus; module.exports.handleModalEscape = handleModalEscape; module.exports.inspectUploadSelection = inspectUploadSelection; module.exports.apply = apply; module.exports.inject = inject;
216
221
  return module.exports;
217
222
  }
218
223
  });