@dungsil/dsh-plugin-ui-setting-skills 0.1.0 → 0.3.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/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Skills Settings · 스킬 설정
2
+
3
+ ![스킬 설정 전체 화면 비교: 좌측 라이트 모드, 우측 다크 모드](./assets/light-dark-preview.webp)
4
+
5
+ DSH Web 설정에서 로드된 사용자 스킬을 확인하고 DSH에서의 사용 여부를 설정합니다.
6
+
7
+ 위 이미지는 최신 빌드의 실제 플러그인 컴포넌트에 예시 스킬 목록·설정 응답과 공식 DSH 테마·UI primitives를 제공한 독립 브라우저 화면을 각각 캡처해 좌측 라이트·우측 다크 모드의 전체 화면을 나란히 이어 붙였습니다. 실제 DSH 세션 화면은 아니며, 스킬 이름·설명·경로와 사용 상태는 예시입니다.
8
+
9
+ ## 사용 방법
10
+
11
+ 1. DSH 플러그인 설치 화면에서 `@dungsil/dsh-plugin-ui-setting-skills`를 설치하고 활성화합니다.
12
+ 2. **설정 → 스킬**을 열고 **공용 스킬** 또는 **DSH 스킬** 탭을 선택합니다.
13
+ 3. 스킬을 펼쳐 설명과 파일 경로를 확인합니다. 오른쪽 스위치로 사용 여부를 변경하면 자동 저장합니다. 이 설정은 DSH에만 적용되며 스킬 파일은 수정하지 않습니다.
14
+
15
+ ## 동작과 호환성
16
+
17
+ - DSH `0.2.1-rc.5` 이상 `0.3.0` 미만의 Web UI를 지원합니다. 한국어·영어·중국어(간체)와 DSH 테마를 따릅니다.
18
+ - 공용 에이전트 및 DSH 사용자 스킬 경로에서 발견한 목록을 표시합니다. 프로젝트 스킬을 편집하거나 새 스킬을 만드는 화면은 아닙니다.
19
+ - 사고 과정 번역 서비스가 연결되어 있으면 스킬을 펼칠 때 설명을 해당 서비스로 자동 전송해 번역하고 캐시에 저장합니다. 원문으로 전환할 수 있습니다. 번역 서비스가 없으면 원문만 표시합니다.
20
+ - 저장 오류가 나타나면 설정 항목을 닫았다가 다시 열어 저장 상태를 확인한 뒤 다시 시도하세요.
Binary file
package/dist/client.js CHANGED
@@ -1,8 +1,68 @@
1
1
  window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-setting-skills", factory(require) { const module = { exports: {} }; const exports = module.exports;
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  let react = require("react");
4
+ let react_dom_client = require("react-dom/client");
4
5
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
6
  let react_jsx_runtime = require("react/jsx-runtime");
7
+ // ../../tools/ui/src/client.ts
8
+ /** Each effect owns its sheet, even when another plugin installs the same defaults. */
9
+ function installStyles(css) {
10
+ const style = document.createElement("style");
11
+ style.textContent = css;
12
+ document.head.append(style);
13
+ return () => style.remove();
14
+ }
15
+ // ../../tools/ui/src/settings.tsx
16
+ /** The rc.5 settings shell has no icon slot, so decorate only the matching navigation rows. */
17
+ function installSettingsIcon(id, labels, icon) {
18
+ const attribute = "data-dsh-settings-icon";
19
+ const selector = `[${attribute}="${id}"]`;
20
+ const removeStyles = installStyles(`
21
+ ${selector} > svg { display: none; }
22
+ ${typeof icon === "string" ? `${selector}::before { content: ''; display: block; flex: none; width: 16px; height: 16px; background: currentColor; mask: url("data:image/svg+xml,${encodeURIComponent(icon)}") center / contain no-repeat; }` : ""}
23
+ ${selector} > [data-dsh-settings-icon-glyph] { display: inline-flex; flex: none; }
24
+ `);
25
+ const marked = /* @__PURE__ */ new Map();
26
+ const refresh = () => {
27
+ const rows = new Set(document.querySelectorAll("[data-shortcut-modal=\"settings\"] nav button"));
28
+ for (const [row, dispose] of marked) if (!rows.has(row) || !labels.includes(row.textContent?.trim() ?? "")) {
29
+ row.removeAttribute(attribute);
30
+ dispose();
31
+ marked.delete(row);
32
+ }
33
+ for (const row of rows) if (!marked.has(row) && labels.includes(row.textContent?.trim() ?? "")) {
34
+ row.setAttribute(attribute, id);
35
+ let dispose = () => {};
36
+ if (typeof icon !== "string") {
37
+ const glyph = document.createElement("span");
38
+ glyph.setAttribute("data-dsh-settings-icon-glyph", "");
39
+ glyph.setAttribute("aria-hidden", "true");
40
+ row.prepend(glyph);
41
+ const unmount = icon(glyph);
42
+ dispose = () => {
43
+ unmount();
44
+ glyph.remove();
45
+ };
46
+ }
47
+ marked.set(row, dispose);
48
+ }
49
+ };
50
+ const observer = new MutationObserver(refresh);
51
+ observer.observe(document.body, {
52
+ childList: true,
53
+ subtree: true,
54
+ characterData: true
55
+ });
56
+ refresh();
57
+ return () => {
58
+ observer.disconnect();
59
+ for (const [row, dispose] of marked) {
60
+ row.removeAttribute(attribute);
61
+ dispose();
62
+ }
63
+ removeStyles();
64
+ };
65
+ }
6
66
  // locale/en.json
7
67
  var en_default = {
8
68
  meta: {
@@ -11,13 +71,27 @@ var en_default = {
11
71
  },
12
72
  ui: {
13
73
  "nav": "Skills",
14
- "description": "Skills loaded from your shared agent and DSH skill directories. Same-name skills are listed separately for each source.",
15
- "agents": "Shared agent skills",
74
+ "description": "This setting applies only to DSH.",
75
+ "agents": "Shared skills",
16
76
  "dsh": "DSH skills",
17
77
  "loading": "Loading skills…",
18
78
  "empty": "No loaded skills from this source.",
19
79
  "error": "Could not load skills. Close and reopen this settings section to try again.",
20
- "incomplete": "Some skill sources could not be loaded. This list may be incomplete."
80
+ "incomplete": "Some skill sources could not be loaded. This list may be incomplete.",
81
+ "use": "Use",
82
+ "disabled": "Off",
83
+ "openFolder": "Open folder",
84
+ "openingFolder": "Opening folder…",
85
+ "openFolderHint": "Open this skill’s folder in the file manager on the DSH host.",
86
+ "openFolderUnavailable": "Folder opening requires a desktop on the DSH host.",
87
+ "openFolderError": "Could not open the skill folder. Check the file manager on the DSH host and try again.",
88
+ "translate": "View translation",
89
+ "original": "View original",
90
+ "retry": "Retry",
91
+ "translating": "Translating…",
92
+ "translationNotice": "Expanding a skill automatically sends its description to the service selected in reasoning translation settings. Translations are cached using its target language.",
93
+ "translationError": "Could not translate. The original is shown. Paid services require a Session and are unavailable here.",
94
+ "saveError": "Could not save the setting. Close and reopen this section to check the saved state before trying again."
21
95
  }
22
96
  };
23
97
  // locale/ko.json
@@ -28,13 +102,27 @@ var ko_default = {
28
102
  },
29
103
  ui: {
30
104
  "nav": "스킬",
31
- "description": "공용 에이전트 및 DSH 스킬 경로에서 불러온 스킬을 표시합니다. 이름이 같은 스킬도 출처별로 각각 표시합니다.",
32
- "agents": "공용 에이전트 스킬",
105
+ "description": "이 설정은 DSH에만 적용됩니다.",
106
+ "agents": "공용 스킬",
33
107
  "dsh": "DSH 스킬",
34
108
  "loading": "스킬을 불러오는 중입니다…",
35
109
  "empty": "이 경로에서 로드된 스킬이 없습니다.",
36
110
  "error": "스킬을 불러오지 못했습니다. 이 설정 항목을 닫았다가 다시 열어 주세요.",
37
- "incomplete": "일부 스킬 소스를 불러오지 못해 목록이 완전하지 않을 수 있습니다."
111
+ "incomplete": "일부 스킬 소스를 불러오지 못해 목록이 완전하지 않을 수 있습니다.",
112
+ "use": "사용",
113
+ "disabled": "꺼짐",
114
+ "openFolder": "폴더 열기",
115
+ "openingFolder": "폴더를 여는 중…",
116
+ "openFolderHint": "DSH 실행 호스트의 파일 탐색기에서 이 스킬의 폴더를 엽니다.",
117
+ "openFolderUnavailable": "폴더를 열려면 DSH 실행 호스트에 데스크톱 환경이 필요합니다.",
118
+ "openFolderError": "스킬 폴더를 열지 못했습니다. DSH 실행 호스트의 파일 탐색기를 확인한 후 다시 시도해 주세요.",
119
+ "translate": "번역 보기",
120
+ "original": "원문 보기",
121
+ "retry": "다시 시도",
122
+ "translating": "번역 중…",
123
+ "translationNotice": "스킬을 펼치면 사고 과정 번역 설정에서 선택한 서비스로 설명을 자동 전송합니다. 번역문은 해당 설정의 대상 언어에 따라 캐시에 저장합니다.",
124
+ "translationError": "번역하지 못해 원문을 표시합니다. 유료 서비스는 세션이 필요하므로 이 화면에서 사용할 수 없습니다.",
125
+ "saveError": "설정을 저장하지 못했습니다. 이 설정 항목을 닫았다가 다시 열어 저장 상태를 확인한 후 다시 시도해 주세요."
38
126
  }
39
127
  };
40
128
  // locale/zh.json
@@ -45,18 +133,34 @@ var zh_default = {
45
133
  },
46
134
  ui: {
47
135
  "nav": "技能",
48
- "description": "显示从共享代理和 DSH 技能目录加载的技能。同名技能按来源分别列出。",
49
- "agents": "共享代理技能",
136
+ "description": "此设置仅适用于 DSH。",
137
+ "agents": "共享技能",
50
138
  "dsh": "DSH 技能",
51
139
  "loading": "正在加载技能…",
52
140
  "empty": "未从此目录加载任何技能。",
53
141
  "error": "无法加载技能。请关闭并重新打开此设置项后重试。",
54
- "incomplete": "部分技能源无法加载,列表可能不完整。"
142
+ "incomplete": "部分技能源无法加载,列表可能不完整。",
143
+ "use": "使用",
144
+ "disabled": "已关闭",
145
+ "openFolder": "打开文件夹",
146
+ "openingFolder": "正在打开文件夹…",
147
+ "openFolderHint": "在 DSH 主机的文件管理器中打开此技能的文件夹。",
148
+ "openFolderUnavailable": "打开文件夹需要 DSH 主机具备桌面环境。",
149
+ "openFolderError": "无法打开技能文件夹。请检查 DSH 主机的文件管理器后重试。",
150
+ "translate": "查看译文",
151
+ "original": "查看原文",
152
+ "retry": "重试",
153
+ "translating": "正在翻译…",
154
+ "translationNotice": "展开技能时,会自动将描述发送至思考过程翻译设置中选择的服务。译文按该设置的目标语言缓存。",
155
+ "translationError": "翻译失败,已显示原文。付费服务需要会话,无法在此页面使用。",
156
+ "saveError": "无法保存设置。请关闭并重新打开此设置项,确认已保存的状态后重试。"
55
157
  }
56
158
  };
57
159
  // src/protocol.ts
58
160
  const channel = "/api";
59
161
  const method = "dungsil-ui-setting-skills/list";
162
+ const saveMethod = "dungsil-ui-setting-skills/save";
163
+ const translateMethod = "dungsil-ui-setting-skills/translate";
60
164
  // src/client.tsx
61
165
  const name = "dungsil-ui-setting-skills";
62
166
  const inject = [
@@ -73,24 +177,87 @@ const styles = `
73
177
  .dsh-skills-settings ul { list-style: none; margin: 0; padding: 0; }
74
178
  .dsh-skills-settings code { font-size: 12px; overflow-wrap: anywhere; }
75
179
  .dsh-skills-settings [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
76
- .dsh-skills-settings details { border-bottom: .5px solid var(--dsw-alias-border-l2); }
180
+ .dsh-skills-settings li { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 16px; border-bottom: .5px solid var(--dsw-alias-border-l2); }
181
+ .dsh-skills-settings details { min-width: 0; }
77
182
  .dsh-skills-settings summary { display: flex; align-items: center; gap: 12px; padding: 16px 0; list-style: none; cursor: pointer; border-radius: var(--dsw-radius-md); }
78
183
  .dsh-skills-settings summary::-webkit-details-marker { display: none; }
79
184
  .dsh-skills-settings summary:hover { background: var(--dsw-alias-interactive-bg-hover); }
80
185
  .dsh-skills-settings summary:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary-new-colorprimary-new-color); outline-offset: 2px; }
81
186
  .dsh-skills-settings summary svg { flex: none; }
82
187
  .dsh-skills-settings details[open] summary svg { transform: rotate(90deg); }
83
- .dsh-skills-summary-text { display: flex; align-items: baseline; gap: 16px; min-width: 0; flex: 1; }
84
- .dsh-skills-summary-text strong { max-width: 50%; overflow-wrap: anywhere; }
85
- .dsh-skills-preview { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
86
- .dsh-skills-settings details[open] .dsh-skills-preview { display: none; }
87
- .dsh-skills-settings details[open] .dsh-skills-summary-text strong { max-width: 100%; }
188
+ .dsh-skills-summary-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
189
+ .dsh-skills-title { display: flex; align-items: baseline; gap: 12px; min-width: 0; }
190
+ .dsh-skills-title strong { min-width: 0; overflow-wrap: anywhere; }
191
+ .dsh-skills-title .dsh-skills-muted { flex: none; font-size: 12px; }
192
+ .dsh-skills-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
88
193
  .dsh-skills-detail { padding: 0 0 16px 28px; overflow-wrap: anywhere; }
89
- @container (max-width: 480px) {
90
- .dsh-skills-summary-text { align-items: stretch; flex-direction: column; gap: 2px; }
91
- .dsh-skills-summary-text strong { max-width: 100%; }
92
- }
194
+ .dsh-skills-actions { display: flex; align-items: center; align-self: start; gap: 12px; height: 78px; }
195
+ .dsh-skills-folder { width: 28px; padding: 0; }
196
+ .dsh-skills-open-error { grid-column: 1 / -1; padding: 0 0 16px 28px; }
197
+ .dsh-skills-list-header { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
93
198
  `;
199
+ function SkillDescription({ skill, connection, language, available, expanded }) {
200
+ const text = localizedCopy(language);
201
+ const [translated, setTranslated] = (0, react.useState)();
202
+ const [original, setOriginal] = (0, react.useState)(false);
203
+ const [pending, setPending] = (0, react.useState)(false);
204
+ const [failed, setFailed] = (0, react.useState)(false);
205
+ const [retry, setRetry] = (0, react.useState)(0);
206
+ (0, react.useEffect)(() => {
207
+ setTranslated(void 0);
208
+ setOriginal(false);
209
+ setPending(false);
210
+ setFailed(false);
211
+ if (!expanded || !available || !skill.path || !skill.description) return;
212
+ const request = new AbortController();
213
+ setPending(true);
214
+ (async () => {
215
+ try {
216
+ const result = await connection.rpc.call(channel, translateMethod, {
217
+ path: skill.path,
218
+ language
219
+ }, request.signal);
220
+ if (request.signal.aborted) return;
221
+ if (!result.ok || typeof result.value !== "string") throw new Error("Could not translate description");
222
+ setTranslated(result.value);
223
+ } catch {
224
+ if (!request.signal.aborted) setFailed(true);
225
+ } finally {
226
+ if (!request.signal.aborted) setPending(false);
227
+ }
228
+ })();
229
+ return () => request.abort();
230
+ }, [
231
+ expanded,
232
+ skill.description,
233
+ skill.path,
234
+ language,
235
+ available,
236
+ connection,
237
+ retry
238
+ ]);
239
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: original || translated === void 0 ? skill.description : translated }), available && skill.path && skill.description && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
240
+ pending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
241
+ role: "status",
242
+ children: text.translating
243
+ }),
244
+ translated !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
245
+ variant: "ghost",
246
+ size: "sm",
247
+ onClick: () => setOriginal((value) => !value),
248
+ children: original ? text.translate : text.original
249
+ }),
250
+ failed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
251
+ role: "alert",
252
+ children: text.translationError
253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
254
+ variant: "ghost",
255
+ size: "sm",
256
+ onClick: () => setRetry((value) => value + 1),
257
+ children: text.retry
258
+ })] })
259
+ ] })] });
260
+ }
94
261
  function SkillsSettings({ ctx }) {
95
262
  const locale = (0, react.useSyncExternalStore)((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getSnapshot());
96
263
  const text = localizedCopy(locale.active);
@@ -98,6 +265,14 @@ function SkillsSettings({ ctx }) {
98
265
  const connection = ctx.get("connection");
99
266
  const [view, setView] = (0, react.useState)();
100
267
  const [error, setError] = (0, react.useState)(false);
268
+ const [saveError, setSaveError] = (0, react.useState)(false);
269
+ const [busy, setBusy] = (0, react.useState)(false);
270
+ const [opening, setOpening] = (0, react.useState)();
271
+ const [openError, setOpenError] = (0, react.useState)();
272
+ const openingPending = (0, react.useRef)(false);
273
+ const pending = (0, react.useRef)(false);
274
+ const mounted = (0, react.useRef)(false);
275
+ const [expanded, setExpanded] = (0, react.useState)({});
101
276
  const [source, setSource] = (0, react.useState)("user-agents");
102
277
  const groups = ["user-agents", "user-dsh"].map((value) => ({
103
278
  value,
@@ -106,6 +281,7 @@ function SkillsSettings({ ctx }) {
106
281
  }));
107
282
  (0, react.useEffect)(() => {
108
283
  const controller = new AbortController();
284
+ mounted.current = true;
109
285
  setView(void 0);
110
286
  setError(false);
111
287
  (async () => {
@@ -118,8 +294,48 @@ function SkillsSettings({ ctx }) {
118
294
  if (!controller.signal.aborted) setError(true);
119
295
  }
120
296
  })();
121
- return () => controller.abort();
297
+ return () => {
298
+ mounted.current = false;
299
+ controller.abort();
300
+ };
122
301
  }, [connection]);
302
+ async function save(name, disabled) {
303
+ if (pending.current) return;
304
+ pending.current = true;
305
+ setBusy(true);
306
+ setSaveError(false);
307
+ try {
308
+ const result = await connection.rpc.call(channel, saveMethod, {
309
+ name,
310
+ disabled
311
+ });
312
+ if (!result.ok) throw new Error("Could not save skills");
313
+ const value = result.value;
314
+ if (mounted.current) setView((current) => current && {
315
+ ...current,
316
+ disabled: value.disabled
317
+ });
318
+ } catch {
319
+ if (mounted.current) setSaveError(true);
320
+ } finally {
321
+ pending.current = false;
322
+ if (mounted.current) setBusy(false);
323
+ }
324
+ }
325
+ async function openFolder(path) {
326
+ if (openingPending.current) return;
327
+ openingPending.current = true;
328
+ setOpening(path);
329
+ setOpenError(void 0);
330
+ try {
331
+ if (!(await connection.rpc.call("/api", "dungsil-ui-setting-skills/open-folder", { path })).ok) throw new Error("Could not open skill folder");
332
+ } catch {
333
+ if (mounted.current) setOpenError(path);
334
+ } finally {
335
+ openingPending.current = false;
336
+ if (mounted.current) setOpening(void 0);
337
+ }
338
+ }
123
339
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
124
340
  className: "dsh-skills-settings",
125
341
  "aria-label": text.nav,
@@ -129,6 +345,14 @@ function SkillsSettings({ ctx }) {
129
345
  className: "dsh-skills-muted",
130
346
  children: text.description
131
347
  }),
348
+ view && !view.folderOpenAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
349
+ className: "dsh-skills-muted",
350
+ children: text.openFolderUnavailable
351
+ }),
352
+ view?.translationAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
353
+ className: "dsh-skills-muted",
354
+ children: text.translationNotice
355
+ }),
132
356
  !view && !error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
133
357
  role: "status",
134
358
  children: text.loading
@@ -137,6 +361,10 @@ function SkillsSettings({ ctx }) {
137
361
  role: "alert",
138
362
  children: text.error
139
363
  }),
364
+ saveError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
365
+ role: "alert",
366
+ children: text.saveError
367
+ }),
140
368
  view && !view.complete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
141
369
  role: "alert",
142
370
  children: text.incomplete
@@ -160,31 +388,106 @@ function SkillsSettings({ ctx }) {
160
388
  children: group.skills.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
161
389
  className: "dsh-skills-muted",
162
390
  children: text.empty
163
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: group.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
164
- "aria-label": skill.name,
165
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {
166
- size: 16,
167
- "aria-hidden": "true"
168
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
169
- className: "dsh-skills-summary-text",
170
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: skill.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
171
- className: "dsh-skills-preview dsh-skills-muted",
172
- "aria-hidden": "true",
173
- children: skill.description
391
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
392
+ className: "dsh-skills-list-header dsh-skills-muted",
393
+ "aria-hidden": "true",
394
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
395
+ text.nav,
396
+ " ",
397
+ group.skills.length
398
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text.use })]
399
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: group.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
400
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
401
+ onToggle: (event) => {
402
+ const open = event.currentTarget.open;
403
+ setExpanded((current) => ({
404
+ ...current,
405
+ [skill.path ?? skill.name]: open
406
+ }));
407
+ },
408
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
409
+ "aria-label": skill.name,
410
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {
411
+ size: 16,
412
+ "aria-hidden": "true"
413
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
414
+ className: "dsh-skills-summary-text",
415
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
416
+ className: "dsh-skills-title",
417
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: skill.name }), view.disabled.includes(skill.name) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
418
+ className: "dsh-skills-muted",
419
+ children: text.disabled
420
+ })]
421
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
422
+ className: "dsh-skills-preview dsh-skills-muted",
423
+ "aria-hidden": "true",
424
+ children: skill.description
425
+ })]
426
+ })]
427
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
428
+ className: "dsh-skills-detail",
429
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDescription, {
430
+ skill,
431
+ connection,
432
+ language: locale.active,
433
+ available: view.translationAvailable && source === group.value,
434
+ expanded: expanded[skill.path ?? skill.name] ?? false
435
+ }), skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
436
+ className: "dsh-skills-muted",
437
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: skill.path })
438
+ })]
174
439
  })]
175
- })]
176
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
177
- className: "dsh-skills-detail",
178
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: skill.description }), skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
179
- className: "dsh-skills-muted",
180
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: skill.path })
181
- })]
182
- })] }) }, skill.path ?? skill.name)) })
440
+ }),
441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
442
+ className: "dsh-skills-actions",
443
+ children: [skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
444
+ label: opening === skill.path ? text.openingFolder : text.openFolder,
445
+ side: "top",
446
+ portal: true,
447
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
448
+ className: "dsh-skills-folder",
449
+ variant: "ghost",
450
+ size: "sm",
451
+ "aria-label": `${opening === skill.path ? text.openingFolder : text.openFolder}: ${skill.name}`,
452
+ "aria-busy": opening === skill.path,
453
+ disabled: !view.folderOpenAvailable || opening !== void 0,
454
+ "aria-description": view.folderOpenAvailable ? text.openFolderHint : text.openFolderUnavailable,
455
+ onClick: () => void openFolder(skill.path),
456
+ icon: opening === skill.path ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutlineRegular, {
457
+ size: 16,
458
+ "aria-hidden": "true"
459
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutlineRegular, {
460
+ size: 16,
461
+ "aria-hidden": "true"
462
+ })
463
+ })
464
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
465
+ label: `${text.use}: ${skill.name}`,
466
+ checked: !view.disabled.includes(skill.name),
467
+ disabled: busy || saveError,
468
+ onChange: (enabled) => void save(skill.name, !enabled)
469
+ })]
470
+ }),
471
+ skill.path && openError === skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
472
+ className: "dsh-skills-open-error",
473
+ role: "alert",
474
+ children: text.openFolderError
475
+ })
476
+ ] }, skill.path ?? skill.name)) })] })
183
477
  }, group.value))] })
184
478
  ]
185
479
  });
186
480
  }
187
481
  function apply(ctx) {
482
+ ctx.effect(() => installSettingsIcon(name, [
483
+ en_default.ui.nav,
484
+ ko_default.ui.nav,
485
+ zh_default.ui.nav
486
+ ], (container) => {
487
+ const root = (0, react_dom_client.createRoot)(container);
488
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSkillOutlineRegular, { size: 16 }));
489
+ return () => root.unmount();
490
+ }));
188
491
  ctx.effect(() => {
189
492
  const style = document.createElement("style");
190
493
  style.textContent = styles;