@dungsil/dsh-plugin-ui-setting-skills 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: {
@@ -20,6 +80,17 @@ var en_default = {
20
80
  "incomplete": "Some skill sources could not be loaded. This list may be incomplete.",
21
81
  "use": "Use",
22
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.",
23
94
  "saveError": "Could not save the setting. Close and reopen this section to check the saved state before trying again."
24
95
  }
25
96
  };
@@ -40,6 +111,17 @@ var ko_default = {
40
111
  "incomplete": "일부 스킬 소스를 불러오지 못해 목록이 완전하지 않을 수 있습니다.",
41
112
  "use": "사용",
42
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": "번역하지 못해 원문을 표시합니다. 유료 서비스는 세션이 필요하므로 이 화면에서 사용할 수 없습니다.",
43
125
  "saveError": "설정을 저장하지 못했습니다. 이 설정 항목을 닫았다가 다시 열어 저장 상태를 확인한 후 다시 시도해 주세요."
44
126
  }
45
127
  };
@@ -60,6 +142,17 @@ var zh_default = {
60
142
  "incomplete": "部分技能源无法加载,列表可能不完整。",
61
143
  "use": "使用",
62
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": "翻译失败,已显示原文。付费服务需要会话,无法在此页面使用。",
63
156
  "saveError": "无法保存设置。请关闭并重新打开此设置项,确认已保存的状态后重试。"
64
157
  }
65
158
  };
@@ -67,6 +160,7 @@ var zh_default = {
67
160
  const channel = "/api";
68
161
  const method = "dungsil-ui-setting-skills/list";
69
162
  const saveMethod = "dungsil-ui-setting-skills/save";
163
+ const translateMethod = "dungsil-ui-setting-skills/translate";
70
164
  // src/client.tsx
71
165
  const name = "dungsil-ui-setting-skills";
72
166
  const inject = [
@@ -83,8 +177,8 @@ const styles = `
83
177
  .dsh-skills-settings ul { list-style: none; margin: 0; padding: 0; }
84
178
  .dsh-skills-settings code { font-size: 12px; overflow-wrap: anywhere; }
85
179
  .dsh-skills-settings [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
86
- .dsh-skills-settings li { display: flex; align-items: flex-start; gap: 16px; border-bottom: .5px solid var(--dsw-alias-border-l2); }
87
- .dsh-skills-settings details { flex: 1; min-width: 0; }
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; }
88
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); }
89
183
  .dsh-skills-settings summary::-webkit-details-marker { display: none; }
90
184
  .dsh-skills-settings summary:hover { background: var(--dsw-alias-interactive-bg-hover); }
@@ -97,9 +191,73 @@ const styles = `
97
191
  .dsh-skills-title .dsh-skills-muted { flex: none; font-size: 12px; }
98
192
  .dsh-skills-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
99
193
  .dsh-skills-detail { padding: 0 0 16px 28px; overflow-wrap: anywhere; }
100
- .dsh-skills-toggle { flex: none; padding-top: 27px; }
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; }
101
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); }
102
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
+ }
103
261
  function SkillsSettings({ ctx }) {
104
262
  const locale = (0, react.useSyncExternalStore)((listener) => ctx.locale.subscribe(listener), () => ctx.locale.getSnapshot());
105
263
  const text = localizedCopy(locale.active);
@@ -109,8 +267,11 @@ function SkillsSettings({ ctx }) {
109
267
  const [error, setError] = (0, react.useState)(false);
110
268
  const [saveError, setSaveError] = (0, react.useState)(false);
111
269
  const [busy, setBusy] = (0, react.useState)(false);
270
+ const [folderStatus, setFolderStatus] = (0, react.useState)({});
271
+ const openingPending = (0, react.useRef)(/* @__PURE__ */ new Set());
112
272
  const pending = (0, react.useRef)(false);
113
273
  const mounted = (0, react.useRef)(false);
274
+ const [expanded, setExpanded] = (0, react.useState)({});
114
275
  const [source, setSource] = (0, react.useState)("user-agents");
115
276
  const groups = ["user-agents", "user-dsh"].map((value) => ({
116
277
  value,
@@ -160,6 +321,28 @@ function SkillsSettings({ ctx }) {
160
321
  if (mounted.current) setBusy(false);
161
322
  }
162
323
  }
324
+ async function openFolder(path) {
325
+ if (openingPending.current.has(path)) return;
326
+ openingPending.current.add(path);
327
+ setFolderStatus((current) => ({
328
+ ...current,
329
+ [path]: "opening"
330
+ }));
331
+ try {
332
+ if (!(await connection.rpc.call("/api", "dungsil-ui-setting-skills/open-folder", { path })).ok) throw new Error("Could not open skill folder");
333
+ if (mounted.current) setFolderStatus((current) => ({
334
+ ...current,
335
+ [path]: void 0
336
+ }));
337
+ } catch {
338
+ if (mounted.current) setFolderStatus((current) => ({
339
+ ...current,
340
+ [path]: "error"
341
+ }));
342
+ } finally {
343
+ openingPending.current.delete(path);
344
+ }
345
+ }
163
346
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
164
347
  className: "dsh-skills-settings",
165
348
  "aria-label": text.nav,
@@ -169,6 +352,14 @@ function SkillsSettings({ ctx }) {
169
352
  className: "dsh-skills-muted",
170
353
  children: text.description
171
354
  }),
355
+ view && !view.folderOpenAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
356
+ className: "dsh-skills-muted",
357
+ children: text.openFolderUnavailable
358
+ }),
359
+ view?.translationAvailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
360
+ className: "dsh-skills-muted",
361
+ children: text.translationNotice
362
+ }),
172
363
  !view && !error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
173
364
  role: "status",
174
365
  children: text.loading
@@ -212,45 +403,98 @@ function SkillsSettings({ ctx }) {
212
403
  " ",
213
404
  group.skills.length
214
405
  ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text.use })]
215
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: group.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
216
- "aria-label": skill.name,
217
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {
218
- size: 16,
219
- "aria-hidden": "true"
220
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
221
- className: "dsh-skills-summary-text",
222
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
223
- className: "dsh-skills-title",
224
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: skill.name }), view.disabled.includes(skill.name) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
406
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: group.skills.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
408
+ onToggle: (event) => {
409
+ const open = event.currentTarget.open;
410
+ setExpanded((current) => ({
411
+ ...current,
412
+ [skill.path ?? skill.name]: open
413
+ }));
414
+ },
415
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
416
+ "aria-label": skill.name,
417
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, {
418
+ size: 16,
419
+ "aria-hidden": "true"
420
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
421
+ className: "dsh-skills-summary-text",
422
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
423
+ className: "dsh-skills-title",
424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: skill.name }), view.disabled.includes(skill.name) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
425
+ className: "dsh-skills-muted",
426
+ children: text.disabled
427
+ })]
428
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
429
+ className: "dsh-skills-preview dsh-skills-muted",
430
+ "aria-hidden": "true",
431
+ children: skill.description
432
+ })]
433
+ })]
434
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
435
+ className: "dsh-skills-detail",
436
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillDescription, {
437
+ skill,
438
+ connection,
439
+ language: locale.active,
440
+ available: view.translationAvailable && source === group.value,
441
+ expanded: expanded[skill.path ?? skill.name] ?? false
442
+ }), skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
225
443
  className: "dsh-skills-muted",
226
- children: text.disabled
444
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: skill.path })
227
445
  })]
228
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
229
- className: "dsh-skills-preview dsh-skills-muted",
230
- "aria-hidden": "true",
231
- children: skill.description
232
446
  })]
233
- })]
234
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
235
- className: "dsh-skills-detail",
236
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: skill.description }), skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
237
- className: "dsh-skills-muted",
238
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: skill.path })
239
- })]
240
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
241
- className: "dsh-skills-toggle",
242
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
243
- label: `${text.use}: ${skill.name}`,
244
- checked: !view.disabled.includes(skill.name),
245
- disabled: busy || saveError,
246
- onChange: (enabled) => void save(skill.name, !enabled)
447
+ }),
448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
449
+ className: "dsh-skills-actions",
450
+ children: [skill.path && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
451
+ label: folderStatus[skill.path] === "opening" ? text.openingFolder : text.openFolder,
452
+ side: "top",
453
+ portal: true,
454
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
455
+ className: "dsh-skills-folder",
456
+ variant: "ghost",
457
+ size: "sm",
458
+ "aria-label": `${folderStatus[skill.path] === "opening" ? text.openingFolder : text.openFolder}: ${skill.name}`,
459
+ "aria-busy": folderStatus[skill.path] === "opening",
460
+ disabled: !view.folderOpenAvailable || folderStatus[skill.path] === "opening",
461
+ "aria-description": view.folderOpenAvailable ? text.openFolderHint : text.openFolderUnavailable,
462
+ onClick: () => void openFolder(skill.path),
463
+ icon: folderStatus[skill.path] === "opening" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutlineRegular, {
464
+ size: 16,
465
+ "aria-hidden": "true"
466
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutlineRegular, {
467
+ size: 16,
468
+ "aria-hidden": "true"
469
+ })
470
+ })
471
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
472
+ label: `${text.use}: ${skill.name}`,
473
+ checked: !view.disabled.includes(skill.name),
474
+ disabled: busy || saveError,
475
+ onChange: (enabled) => void save(skill.name, !enabled)
476
+ })]
477
+ }),
478
+ skill.path && folderStatus[skill.path] === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
479
+ className: "dsh-skills-open-error",
480
+ role: "alert",
481
+ children: text.openFolderError
247
482
  })
248
- })] }, skill.path ?? skill.name)) })] })
483
+ ] }, skill.path ?? skill.name)) })] })
249
484
  }, group.value))] })
250
485
  ]
251
486
  });
252
487
  }
253
488
  function apply(ctx) {
489
+ ctx.effect(() => installSettingsIcon(name, [
490
+ en_default.ui.nav,
491
+ ko_default.ui.nav,
492
+ zh_default.ui.nav
493
+ ], (container) => {
494
+ const root = (0, react_dom_client.createRoot)(container);
495
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSkillOutlineRegular, { size: 16 }));
496
+ return () => root.unmount();
497
+ }));
254
498
  ctx.effect(() => {
255
499
  const style = document.createElement("style");
256
500
  style.textContent = styles;
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
1
5
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
6
+ import { dshCachePath } from "@deepseek-ai/dsh-home-paths";
7
+ import { canOpenNativePath, openNativePath } from "@deepseek-ai/dsh-native-command";
2
8
  import { SkillRegistry } from "@deepseek-ai/dsh-skill";
3
9
  import { FileSystemSkillProvider } from "@deepseek-ai/dsh-skill-filesystem";
4
10
  // ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.6/node_modules/@deepseek-ai/cosmokit/lib/index.js
@@ -890,12 +896,15 @@ defineMethod("transform", [
890
896
  const channel = "/api";
891
897
  const method = "dungsil-ui-setting-skills/list";
892
898
  const saveMethod = "dungsil-ui-setting-skills/save";
899
+ const translateMethod = "dungsil-ui-setting-skills/translate";
900
+ const openFolderMethod = "dungsil-ui-setting-skills/open-folder";
893
901
  // src/index.ts
894
902
  const name = "dungsil-ui-setting-skills";
895
903
  const inject = ["connection", "configEditor"];
896
904
  const Config = Schema.object({ disabled: Schema.array(Schema.string()).default([]) });
897
905
  function apply(ctx, config = {}) {
898
906
  const editor = ctx.configEditor;
907
+ const cacheDir = dshCachePath("dungsil-ui-setting-skills", "translations-v1");
899
908
  const entry = () => editor.entries().find((entry) => entry.options.name === "@dungsil/dsh-plugin-ui-setting-skills");
900
909
  const disabled = () => entry()?.options.config?.disabled ?? config.disabled ?? [];
901
910
  ctx.effect(() => {
@@ -930,7 +939,12 @@ function apply(ctx, config = {}) {
930
939
  lifetime.abort();
931
940
  await provider.dispose();
932
941
  }, "ui-setting-skills: dispose discovery");
933
- for (const endpoint of [method, saveMethod]) ctx.effect(() => ctx.connection.fetch.register({
942
+ for (const endpoint of [
943
+ method,
944
+ saveMethod,
945
+ translateMethod,
946
+ openFolderMethod
947
+ ]) ctx.effect(() => ctx.connection.fetch.register({
934
948
  path: `${channel}/${endpoint}`,
935
949
  methods: ["POST"],
936
950
  requestBody: "buffered",
@@ -942,6 +956,103 @@ function apply(ctx, config = {}) {
942
956
  return new Response("Invalid request", { status: 400 });
943
957
  }
944
958
  if (!body.success || body.data.method !== endpoint) return new Response("Invalid request", { status: 400 });
959
+ if (endpoint === "dungsil-ui-setting-skills/open-folder") {
960
+ const payload = body.data.payload;
961
+ if (!payload || typeof payload.path !== "string") return new Response("Invalid request", { status: 400 });
962
+ let result;
963
+ try {
964
+ const signal = AbortSignal.any([
965
+ request.signal,
966
+ lifetime.signal,
967
+ AbortSignal.timeout(15e3)
968
+ ]);
969
+ if (!canOpenNativePath()) throw new Error("Native folder opening unavailable");
970
+ const observation = await provider.list({ signal });
971
+ const skill = ("candidates" in observation ? observation.candidates : observation).find((skill) => skill.path === payload.path && ["user-agents", "user-dsh"].includes(skill.source));
972
+ if (!skill?.path) throw new Error("Unknown user skill");
973
+ await openNativePath(dirname(skill.path), signal);
974
+ result = {
975
+ ok: true,
976
+ value: {}
977
+ };
978
+ } catch {
979
+ result = {
980
+ ok: false,
981
+ error: {
982
+ code: "open_folder_failed",
983
+ message: "Could not open skill folder.",
984
+ details: {}
985
+ }
986
+ };
987
+ }
988
+ return Response.json({
989
+ type: "server-response",
990
+ rpcId: body.data.rpcId,
991
+ result
992
+ }, { headers: { "Cache-Control": "no-store" } });
993
+ }
994
+ if (endpoint === "dungsil-ui-setting-skills/translate") {
995
+ const payload = body.data.payload;
996
+ if (!payload || typeof payload.path !== "string" || typeof payload.language !== "string") return new Response("Invalid request", { status: 400 });
997
+ let result;
998
+ try {
999
+ const signal = AbortSignal.any([request.signal, lifetime.signal]);
1000
+ const translator = ctx.get("cotTranslation");
1001
+ if (!translator) throw new Error("Translation unavailable");
1002
+ const observation = await provider.list({ signal });
1003
+ const skill = ("candidates" in observation ? observation.candidates : observation).find((skill) => skill.path === payload.path && ["user-agents", "user-dsh"].includes(skill.source));
1004
+ if (!skill) throw new Error("Unknown user skill");
1005
+ const { preferences } = await translator.limits(signal);
1006
+ const targetLanguage = preferences.targetLanguage === "auto" ? payload.language : preferences.targetLanguage;
1007
+ const key = createHash("sha256").update(JSON.stringify([
1008
+ preferences.provider,
1009
+ targetLanguage,
1010
+ skill.description
1011
+ ])).digest("hex");
1012
+ const cachePath = join(cacheDir, `${key}.json`);
1013
+ let value;
1014
+ try {
1015
+ const cached = JSON.parse(await readFile(cachePath, "utf8"));
1016
+ if (typeof cached === "string") value = cached;
1017
+ } catch {}
1018
+ signal.throwIfAborted();
1019
+ if (value === void 0) {
1020
+ value = await translator.translate({
1021
+ text: skill.description,
1022
+ provider: preferences.provider,
1023
+ targetLanguage
1024
+ }, signal);
1025
+ signal.throwIfAborted();
1026
+ try {
1027
+ await writeFileAtomic(cachePath, JSON.stringify(value), {
1028
+ mode: 384,
1029
+ dirMode: 448
1030
+ });
1031
+ } catch {
1032
+ ctx.logger.warn("Could not cache skill description translation");
1033
+ }
1034
+ }
1035
+ signal.throwIfAborted();
1036
+ result = {
1037
+ ok: true,
1038
+ value
1039
+ };
1040
+ } catch {
1041
+ result = {
1042
+ ok: false,
1043
+ error: {
1044
+ code: "translation_failed",
1045
+ message: "Could not translate skill description.",
1046
+ details: {}
1047
+ }
1048
+ };
1049
+ }
1050
+ return Response.json({
1051
+ type: "server-response",
1052
+ rpcId: body.data.rpcId,
1053
+ result
1054
+ }, { headers: { "Cache-Control": "no-store" } });
1055
+ }
945
1056
  if (endpoint === "dungsil-ui-setting-skills/save") {
946
1057
  const payload = body.data.payload;
947
1058
  if (!payload || typeof payload.name !== "string" || typeof payload.disabled !== "boolean") return new Response("Invalid request", { status: 400 });
@@ -992,6 +1103,8 @@ function apply(ctx, config = {}) {
992
1103
  result = {
993
1104
  ok: true,
994
1105
  value: {
1106
+ translationAvailable: !!ctx.get("cotTranslation"),
1107
+ folderOpenAvailable: canOpenNativePath(),
995
1108
  disabled: disabled(),
996
1109
  complete: "candidates" in observation ? observation.complete : true,
997
1110
  skills: skills.flatMap((skill) => skill.source === "user-agents" || skill.source === "user-dsh" ? [{
package/locale/en.json CHANGED
@@ -14,6 +14,17 @@
14
14
  "incomplete": "Some skill sources could not be loaded. This list may be incomplete.",
15
15
  "use": "Use",
16
16
  "disabled": "Off",
17
+ "openFolder": "Open folder",
18
+ "openingFolder": "Opening folder…",
19
+ "openFolderHint": "Open this skill’s folder in the file manager on the DSH host.",
20
+ "openFolderUnavailable": "Folder opening requires a desktop on the DSH host.",
21
+ "openFolderError": "Could not open the skill folder. Check the file manager on the DSH host and try again.",
22
+ "translate": "View translation",
23
+ "original": "View original",
24
+ "retry": "Retry",
25
+ "translating": "Translating…",
26
+ "translationNotice": "Expanding a skill automatically sends its description to the service selected in reasoning translation settings. Translations are cached using its target language.",
27
+ "translationError": "Could not translate. The original is shown. Paid services require a Session and are unavailable here.",
17
28
  "saveError": "Could not save the setting. Close and reopen this section to check the saved state before trying again."
18
29
  }
19
30
  }
package/locale/ko.json CHANGED
@@ -14,6 +14,17 @@
14
14
  "incomplete": "일부 스킬 소스를 불러오지 못해 목록이 완전하지 않을 수 있습니다.",
15
15
  "use": "사용",
16
16
  "disabled": "꺼짐",
17
+ "openFolder": "폴더 열기",
18
+ "openingFolder": "폴더를 여는 중…",
19
+ "openFolderHint": "DSH 실행 호스트의 파일 탐색기에서 이 스킬의 폴더를 엽니다.",
20
+ "openFolderUnavailable": "폴더를 열려면 DSH 실행 호스트에 데스크톱 환경이 필요합니다.",
21
+ "openFolderError": "스킬 폴더를 열지 못했습니다. DSH 실행 호스트의 파일 탐색기를 확인한 후 다시 시도해 주세요.",
22
+ "translate": "번역 보기",
23
+ "original": "원문 보기",
24
+ "retry": "다시 시도",
25
+ "translating": "번역 중…",
26
+ "translationNotice": "스킬을 펼치면 사고 과정 번역 설정에서 선택한 서비스로 설명을 자동 전송합니다. 번역문은 해당 설정의 대상 언어에 따라 캐시에 저장합니다.",
27
+ "translationError": "번역하지 못해 원문을 표시합니다. 유료 서비스는 세션이 필요하므로 이 화면에서 사용할 수 없습니다.",
17
28
  "saveError": "설정을 저장하지 못했습니다. 이 설정 항목을 닫았다가 다시 열어 저장 상태를 확인한 후 다시 시도해 주세요."
18
29
  }
19
30
  }
package/locale/zh.json CHANGED
@@ -14,6 +14,17 @@
14
14
  "incomplete": "部分技能源无法加载,列表可能不完整。",
15
15
  "use": "使用",
16
16
  "disabled": "已关闭",
17
+ "openFolder": "打开文件夹",
18
+ "openingFolder": "正在打开文件夹…",
19
+ "openFolderHint": "在 DSH 主机的文件管理器中打开此技能的文件夹。",
20
+ "openFolderUnavailable": "打开文件夹需要 DSH 主机具备桌面环境。",
21
+ "openFolderError": "无法打开技能文件夹。请检查 DSH 主机的文件管理器后重试。",
22
+ "translate": "查看译文",
23
+ "original": "查看原文",
24
+ "retry": "重试",
25
+ "translating": "正在翻译…",
26
+ "translationNotice": "展开技能时,会自动将描述发送至思考过程翻译设置中选择的服务。译文按该设置的目标语言缓存。",
27
+ "translationError": "翻译失败,已显示原文。付费服务需要会话,无法在此页面使用。",
17
28
  "saveError": "无法保存设置。请关闭并重新打开此设置项,确认已保存的状态后重试。"
18
29
  }
19
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-setting-skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "View loaded user skills in DeepSeek Harness settings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "locale",
14
+ "assets",
14
15
  "cordis.patch.yml"
15
16
  ],
16
17
  "type": "module",
@@ -23,6 +24,11 @@
23
24
  "publishConfig": {
24
25
  "access": "public"
25
26
  },
27
+ "dependencies": {
28
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.5",
29
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.5",
30
+ "@deepseek-ai/dsh-native-command": "0.2.1-rc.5"
31
+ },
26
32
  "devDependencies": {
27
33
  "@deepseek-ai/cordis": "4.0.5",
28
34
  "@deepseek-ai/dsh-app-boot": "0.2.1-rc.5",
@@ -32,6 +38,7 @@
32
38
  "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.5",
33
39
  "@deepseek-ai/dsh-config-editor": "0.2.1-rc.5",
34
40
  "@deepseek-ai/dsh-credentials-local": "0.2.1-rc.5",
41
+ "@deepseek-ai/dsh-experimental-client-ui-cot-translation": "0.2.1-rc.5",
35
42
  "@deepseek-ai/dsh-skill": "0.2.1-rc.5",
36
43
  "@deepseek-ai/dsh-skill-filesystem": "0.2.1-rc.5",
37
44
  "@deepseek-ai/schemastery": "3.18.5",
@@ -39,7 +46,10 @@
39
46
  "@dungsil/dsh-tsdown": "0.0.0",
40
47
  "@dungsil/dsh-ui": "0.0.0",
41
48
  "@types/react": "18.3.31",
49
+ "@types/react-dom": "18.3.7",
50
+ "happy-dom": "21.0.4",
42
51
  "react": "18.3.1",
52
+ "react-dom": "18.3.1",
43
53
  "tsdown": "0.23.4"
44
54
  },
45
55
  "peerDependencies": {