dsh-comfyui 0.3.0-beta.6 → 0.4.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.en.md CHANGED
@@ -16,6 +16,15 @@
16
16
  <img src="https://img.shields.io/npm/l/dsh-comfyui" alt="license" />
17
17
  </p>
18
18
 
19
+ > **Version pairing (pick the right dsh before installing, otherwise dshmarket shows a risk banner)**:
20
+ >
21
+ > | dsh-comfyui | Paired DeepSeek Harness | Notes |
22
+ > | --- | --- | --- |
23
+ > | **0.4.0 (latest, `latest` tag)** | **>= 0.1.2** | From 0.4.0 this plugin uses the new settings service API |
24
+ > | **0.3.x (beta line, `beta` tag)** | **0.1.1** | On older dsh versions stay on the 0.3.x line |
25
+ >
26
+ > Install: `dsh plugin --profile web add dsh-comfyui` (latest 0.4.0) / `dsh plugin --profile web add dsh-comfyui@beta` (0.3.x for older hosts).
27
+
19
28
  ## Features
20
29
 
21
30
  ### Agent tools
package/README.md CHANGED
@@ -16,6 +16,15 @@
16
16
  <img src="https://img.shields.io/npm/l/dsh-comfyui" alt="license" />
17
17
  </p>
18
18
 
19
+ > **版本配对(选对 dsh 版本再装,否则 dshmarket 会报风险提示)**:
20
+ >
21
+ > | dsh-comfyui | 配对的 DeepSeek Harness | 说明 |
22
+ > | --- | --- | --- |
23
+ > | **0.4.0(最新,`latest` tag)** | **≥ 0.1.2** | 0.4.0 起使用新版 settings 服务 API |
24
+ > | **0.3.x(beta 线,`beta` tag)** | **0.1.1** | 老版本 dsh 请留在 0.3.x 线 |
25
+ >
26
+ > 安装:`dsh plugin --profile web add dsh-comfyui`(装最新 0.4.0)/ `dsh plugin --profile web add dsh-comfyui@beta`(老宿主装 0.3.x)。
27
+
19
28
  ## 功能
20
29
 
21
30
  ### Agent 工具
package/client/client.js CHANGED
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
13
13
  baseUrl: "服务器地址",
14
14
  baseUrlHint: "ComfyUI 的 HTTP 地址(默认 http://127.0.0.1:8188)。",
15
15
  apiKeyEnv: "API Key 环境变量名",
16
- apiKeyEnvHint: "凭据存储或环境变量中的密钥名(默认 COMFYUI_API_KEY),远程服务通常需要。",
16
+ apiKeyEnvHint: "密钥名:环境变量或凭据存储(默认 COMFYUI_API_KEY)。",
17
17
  hasApiKey: "已配置 API Key",
18
18
  noApiKey: "未配置 API Key(本地服务通常不需要)",
19
19
  save: "保存",
@@ -25,16 +25,16 @@ window.__ModuleLoader__.load({
25
25
  testFail: "连接失败:{message}",
26
26
  configNotWritable: "设置服务不可用,请通过 cordis.yml 修改配置。",
27
27
  mediaHost: "媒体访问地址",
28
- mediaHostHint: "生成图片/媒体在聊天中使用的完整地址(如 http://192.168.1.5:3080)。留空时自动使用浏览器访问本服务器的地址;内网或反向代理下若图片不显示,请在此填写你访问本页面所用的地址。",
28
+ mediaHostHint: "媒体在聊天中显示的完整地址(如 http://192.168.1.5:3080)。留空自动检测;图片不显示时填你访问本页的地址。",
29
29
  comfyuiDirs: "ComfyUI 目录",
30
- comfyuiDirsHint: "本机 ComfyUI 安装目录,可添加多个(目录映射 / 多实例 / 便携版)。Agent 会据此直接定位你的 ComfyUI 文件(models、工作流、TTS-Audio-Suite 音色库等),无需再向你询问。",
30
+ comfyuiDirsHint: "本机 ComfyUI 安装目录(可多个,支持映射/多实例/便携版)。Agent 据此定位 models、工作流、音色库等文件。",
31
31
  comfyuiDirAdd: "+ 添加目录",
32
32
  comfyuiDirRemove: "移除该目录",
33
33
  comfyuiDirPlaceholder: "如 D:\\ComfyUI",
34
34
  skillsDir: "技能包目录",
35
- skillsDirHint: "技能包(每个工作流的 SKILL.md 与参考文档)的存放目录。留空表示放在插件数据目录下的 skills/。填绝对路径可以把这些文档放到别的盘、同步盘或版本库里;相对路径会被忽略。改这里不会自动搬走已有的技能包,需要你手动把旧目录里的文件夹移过去。当前实际使用:{current}",
35
+ skillsDirHint: "工作流技能包(SKILL.md 与参考文档)目录。留空 = 数据目录下 skills/;填绝对路径可放到别的盘或同步盘。改后需手动搬移已有目录。当前实际使用:{current}",
36
36
  language: "界面语言",
37
- languageHint: "切换插件界面语言(中文 / English)。切换后页面会自动刷新。",
37
+ languageHint: "界面语言,切换后页面自动刷新。",
38
38
  cardGenerating: "ComfyUI 生成中…",
39
39
  cardBackground: "后台任务",
40
40
  cardBackgroundDone: "已完成",
@@ -51,6 +51,7 @@ window.__ModuleLoader__.load({
51
51
  cardMode: "模式",
52
52
  cardListed: "已列出 {runs} 个运行工作流,{graphs} 个图工作流",
53
53
  panelTitle: "ComfyUI 面板",
54
+ panelReset: "复位位置与大小",
54
55
  close: "关闭",
55
56
  lbPrev: "上一张",
56
57
  lbNext: "下一张",
@@ -174,7 +175,7 @@ window.__ModuleLoader__.load({
174
175
  skillBadgeRequired: "技能包·必读",
175
176
  skillBadgeHint: "该工作流挂了技能包:Agent 运行前会读它。",
176
177
  skillTitle: "技能包",
177
- skillIntro: "技能包是写给 Agent 看的一份说明书:什么时候用这个工作流、关键参数怎么填、哪些环节容易翻车。它不会常驻在对话里——Agent 只有在 comfyui_workflow list 里看到摘要、并决定要用这个工作流时,才会读取正文;正文里点名的参考文档,它才会再去打开。适合参数多、坑多、或带风格合集的复杂工作流。",
178
+ skillIntro: "点击「启用技能包」后,可为这个工作流添加给 Agent 看的额外 skills 说明:什么时候用、参数怎么填、有哪些坑。",
178
179
  skillEnable: "启用技能包",
179
180
  skillDetach: "停用(保留文件)",
180
181
  skillDestroy: "销毁技能包",
@@ -182,11 +183,10 @@ window.__ModuleLoader__.load({
182
183
  skillDestroyConfirmButton: "确认销毁",
183
184
  skillDropHint: "可把文件拖到这里导入",
184
185
  skillRequired: "运行前必读",
185
- skillRequiredHint: "勾选后,Agent 未读技能包就调用本工作流会被拒绝并提示先读。",
186
186
  skillDir: "目录",
187
187
  skillSummary: "摘要",
188
188
  skillSummaryPlaceholder: "一句话说明这个工作流适合做什么",
189
- skillSummaryHint: "这句话会出现在 comfyui_workflow list 里,Agent 靠它决定要不要读正文。建议 40 字以内。",
189
+ skillSummaryHint: "Agent 靠它决定要不要读正文,等同 skills 里的 description 字段。",
190
190
  skillSave: "保存",
191
191
  skillSaved: "已保存",
192
192
  skillDirty: "未保存",
@@ -202,6 +202,9 @@ window.__ModuleLoader__.load({
202
202
  skillReveal: "📂 打开目录",
203
203
  skillRevealHint: "在文件管理器中打开这个技能包的文件夹,方便直接拷贝文件进去。打开的是运行 DSH 那台机器上的目录;若你正从另一台机器访问本页面,请按下方显示的路径自行前往。",
204
204
  skillRevealed: "已打开",
205
+ skillRefresh: "刷新",
206
+ skillRefreshHint: "重新读取技能包目录,显示 Agent 写入或文件管理器里新增/替换的文件。",
207
+ skillRefreshed: "已刷新",
205
208
  skillNewDirPlaceholder: "目录名,如 templates",
206
209
  skillNewDirHint: "技能包可以有任意一层子目录。常用:{presets};也可以自己起名(字母/数字/汉字/下划线/连字符)。",
207
210
  skillDirCreated: "已创建目录",
@@ -242,7 +245,7 @@ window.__ModuleLoader__.load({
242
245
  baseUrl: "Server URL",
243
246
  baseUrlHint: "ComfyUI HTTP address (default http://127.0.0.1:8188).",
244
247
  apiKeyEnv: "API key env var",
245
- apiKeyEnvHint: "Credential-store or environment variable name (default COMFYUI_API_KEY); usually needed for remote servers.",
248
+ apiKeyEnvHint: "Name of the key: env var or credential store (default COMFYUI_API_KEY).",
246
249
  hasApiKey: "API key configured",
247
250
  noApiKey: "No API key configured (usually not needed for local servers)",
248
251
  save: "Save",
@@ -254,16 +257,16 @@ window.__ModuleLoader__.load({
254
257
  testFail: "Connection failed: {message}",
255
258
  configNotWritable: "Settings service unavailable — edit cordis.yml instead.",
256
259
  mediaHost: "Media base URL",
257
- mediaHostHint: "Absolute base URL used for generated media in chat (e.g. http://192.168.1.5:3080). Leave empty to auto-detect the address your browser uses to reach this server; fill it in when images do not load over LAN or a reverse proxy.",
260
+ mediaHostHint: "Base URL for media shown in chat (e.g. http://192.168.1.5:3080). Auto-detected when empty; set it if images do not load over LAN or a reverse proxy.",
258
261
  comfyuiDirs: "ComfyUI directories",
259
- comfyuiDirsHint: "ComfyUI install root(s) on this machine; add multiple for mapped/multiple installs. The agent reads these to locate your ComfyUI files (models, workflows, the TTS-Audio-Suite voice library) directly instead of asking you.",
262
+ comfyuiDirsHint: "ComfyUI install roots on this machine (multiple allowed: mapped/multiple/portable installs). The agent uses them to locate models, workflows and voice libraries.",
260
263
  comfyuiDirAdd: "+ Add directory",
261
264
  comfyuiDirRemove: "Remove this directory",
262
265
  comfyuiDirPlaceholder: "e.g. D:\\ComfyUI",
263
266
  skillsDir: "Skill pack directory",
264
- skillsDirHint: "Where skill packs live (each workflow SKILL.md and its reference files). Empty means skills/ under the plugin data directory. An absolute path keeps these documents on another drive, in a synced folder, or under version control; a relative path is ignored. Changing this does not move existing packs — move the folders yourself. Currently in use: {current}",
267
+ skillsDirHint: "Directory for workflow skill packs (SKILL.md + reference files). Empty = skills/ under the plugin data dir; an absolute path may target another drive or a synced folder. Existing packs are not moved automatically. Currently in use: {current}",
265
268
  language: "Language",
266
- languageHint: "Switch the plugin UI language (中文 / English). The page reloads after switching.",
269
+ languageHint: "UI language; the page reloads after switching.",
267
270
  cardGenerating: "ComfyUI generating…",
268
271
  cardBackground: "Background job",
269
272
  cardBackgroundDone: "Completed",
@@ -280,6 +283,7 @@ window.__ModuleLoader__.load({
280
283
  cardMode: "Mode",
281
284
  cardListed: "Listed {runs} runnable workflows, {graphs} graph workflows",
282
285
  panelTitle: "ComfyUI Panel",
286
+ panelReset: "Reset position & size",
283
287
  close: "Close",
284
288
  lbPrev: "Previous image",
285
289
  lbNext: "Next image",
@@ -403,7 +407,7 @@ window.__ModuleLoader__.load({
403
407
  skillBadgeRequired: "Skill pack (required)",
404
408
  skillBadgeHint: "This workflow has a skill pack: the agent reads it before running.",
405
409
  skillTitle: "Skill pack",
406
- skillIntro: "A skill pack is a manual written for the agent: when to use this workflow, how to fill its key parameters, which stages go wrong. It never sits in the conversation — the agent sees only a one-line summary in comfyui_workflow list, loads the body when it picks this workflow, and opens a reference file only when the body points at one. Worth it for workflows with many parameters, real pitfalls, or a collection of style presets.",
410
+ skillIntro: "Click \"Create skill pack\" to add extra skill instructions for this workflow: when to use it, how to fill its parameters, and which pitfalls to avoid.",
407
411
  skillEnable: "Create skill pack",
408
412
  skillDetach: "Detach (keep files)",
409
413
  skillDestroy: "Destroy pack",
@@ -411,11 +415,10 @@ window.__ModuleLoader__.load({
411
415
  skillDestroyConfirmButton: "Destroy",
412
416
  skillDropHint: "Drop files here to import",
413
417
  skillRequired: "Required before running",
414
- skillRequiredHint: "When checked, running this workflow is refused until the agent has loaded the pack.",
415
418
  skillDir: "Directory",
416
419
  skillSummary: "Summary",
417
420
  skillSummaryPlaceholder: "One line on what this workflow is for",
418
- skillSummaryHint: "This line appears in comfyui_workflow list; the agent uses it to decide whether to load the body. Keep it short.",
421
+ skillSummaryHint: "The agent uses it to decide whether to read the body — like the description field of a skill.",
419
422
  skillSave: "Save",
420
423
  skillSaved: "Saved",
421
424
  skillDirty: "Unsaved",
@@ -431,6 +434,9 @@ window.__ModuleLoader__.load({
431
434
  skillReveal: "📂 Open folder",
432
435
  skillRevealHint: "Open this pack folder in the file manager so you can copy files straight in. It opens on the machine running DSH; if you are viewing this page from another machine, use the path shown below instead.",
433
436
  skillRevealed: "Opened",
437
+ skillRefresh: "Refresh",
438
+ skillRefreshHint: "Re-read the pack directory to show files added or replaced by the agent or the file manager.",
439
+ skillRefreshed: "Refreshed",
434
440
  skillNewDirPlaceholder: "Folder name, e.g. templates",
435
441
  skillNewDirHint: "A pack may hold any one-level sub-directory. Common ones: {presets}; custom names work too (letters, digits, CJK, underscore, dash).",
436
442
  skillDirCreated: "Folder created",
@@ -1110,6 +1116,41 @@ window.__ModuleLoader__.load({
1110
1116
  localStorage.setItem(GEOM_KEY, JSON.stringify(geom));
1111
1117
  } catch {}
1112
1118
  }
1119
+ /** Default panel width from styles.ts (`.dsc-panel` width var), used only to
1120
+ * estimate the panel extent while clamping a saved position. */
1121
+ const PANEL_WIDTH_FALLBACK = 400;
1122
+ /** Approx. height of the drag-handle band (`.dsc-panel-head`). */
1123
+ const HEAD_BAND = 48;
1124
+ /** Minimum margin kept between the panel and the viewport edge. */
1125
+ const EDGE = 8;
1126
+ /**
1127
+ * Clamp a top-left corner so the drag-handle band always stays inside the
1128
+ * viewport. The failure this prevents: once the header is pushed past the top
1129
+ * edge (or a saved position from a larger monitor no longer fits), there is no
1130
+ * grab surface left and the panel cannot be moved back at all.
1131
+ */
1132
+ function clampPos(x, y, width, vw, vh) {
1133
+ const maxX = Math.max(EDGE, vw - width - EDGE);
1134
+ const maxY = Math.max(EDGE, vh - HEAD_BAND - EDGE);
1135
+ return {
1136
+ x: Math.min(Math.max(x, EDGE), maxX),
1137
+ y: Math.min(Math.max(y, EDGE), maxY)
1138
+ };
1139
+ }
1140
+ /** Snap persisted geometry back within reach of the cursor. Geometry that
1141
+ * never carried an explicit position (the CSS-anchored default) is returned
1142
+ * unchanged; geometry already inside the safe area keeps its object identity
1143
+ * so callers can skip pointless re-renders. */
1144
+ function healGeom(geom, vw, vh) {
1145
+ if (geom.x === void 0 || geom.y === void 0) return geom;
1146
+ const pos = clampPos(geom.x, geom.y, geom.width ?? PANEL_WIDTH_FALLBACK, vw, vh);
1147
+ if (pos.x === geom.x && pos.y === geom.y) return geom;
1148
+ return {
1149
+ ...geom,
1150
+ x: pos.x,
1151
+ y: pos.y
1152
+ };
1153
+ }
1113
1154
  /** Fallback suggestions when the config route has not answered yet; the
1114
1155
  * authoritative list comes from the host (`presetDirs`). */
1115
1156
  const SKILL_FALLBACK_DIRS = [
@@ -1897,7 +1938,7 @@ window.__ModuleLoader__.load({
1897
1938
  function ComfyUIPanel({ t }) {
1898
1939
  const open = usePanelOpen();
1899
1940
  const tab = usePanelTab();
1900
- const [geom, setGeom] = (0, react.useState)(loadGeom);
1941
+ const [geom, setGeom] = (0, react.useState)(() => healGeom(loadGeom(), window.innerWidth, window.innerHeight));
1901
1942
  const [dragging, setDragging] = (0, react.useState)(null);
1902
1943
  const [lightbox, setLightbox] = (0, react.useState)(null);
1903
1944
  const panelRef = (0, react.useRef)(null);
@@ -1908,9 +1949,18 @@ window.__ModuleLoader__.load({
1908
1949
  index
1909
1950
  });
1910
1951
  (0, react.useEffect)(() => {
1911
- if (open) return;
1952
+ if (open) {
1953
+ setGeom((prev) => healGeom(prev, window.innerWidth, window.innerHeight));
1954
+ return;
1955
+ }
1912
1956
  saveGeom(geom);
1913
1957
  }, [open]);
1958
+ (0, react.useEffect)(() => {
1959
+ if (!open) return;
1960
+ const onResize = () => setGeom((prev) => healGeom(prev, window.innerWidth, window.innerHeight));
1961
+ window.addEventListener("resize", onResize);
1962
+ return () => window.removeEventListener("resize", onResize);
1963
+ }, [open]);
1914
1964
  const startDrag = (event, mode) => {
1915
1965
  if (event.button !== 0) return;
1916
1966
  if (mode === "move") {
@@ -1934,12 +1984,14 @@ window.__ModuleLoader__.load({
1934
1984
  const moveDrag = (event) => {
1935
1985
  const d = dragRef.current;
1936
1986
  if (d === null) return;
1937
- if (dragging === "move") setGeom((prev) => ({
1938
- ...prev,
1939
- x: Math.round(d.origX + event.clientX - d.startX),
1940
- y: Math.round(d.origY + event.clientY - d.startY)
1941
- }));
1942
- else if (dragging === "resize") {
1987
+ if (dragging === "move") {
1988
+ const pos = clampPos(Math.round(d.origX + event.clientX - d.startX), Math.round(d.origY + event.clientY - d.startY), d.width, window.innerWidth, window.innerHeight);
1989
+ setGeom((prev) => ({
1990
+ ...prev,
1991
+ x: pos.x,
1992
+ y: pos.y
1993
+ }));
1994
+ } else if (dragging === "resize") {
1943
1995
  const width = Math.max(300, Math.min(720, d.width + event.clientX - d.startX));
1944
1996
  const height = Math.max(200, Math.min(Math.max(300, window.innerHeight - 40), d.height + event.clientY - d.startY));
1945
1997
  setGeom((prev) => ({
@@ -1978,11 +2030,22 @@ window.__ModuleLoader__.load({
1978
2030
  onPointerMove: (event) => moveDrag(event),
1979
2031
  onPointerUp: () => endDrag(),
1980
2032
  onPointerCancel: () => endDrag()
1981
- }, (0, react.createElement)("span", { className: "dsc-panel-title" }, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle"))), (0, react.createElement)("button", {
2033
+ }, (0, react.createElement)("span", { className: "dsc-panel-title" }, (0, react.createElement)("span", { className: "dsc-trigger-glyph" }, (0, react.createElement)(ComfyUIIcon)), (0, react.createElement)("span", null, t("panelTitle"))), (0, react.createElement)("span", { className: "dsc-panel-head-actions" }, (0, react.createElement)("button", {
2034
+ className: "dsc-panel-reset",
2035
+ type: "button",
2036
+ title: t("panelReset"),
2037
+ "aria-label": t("panelReset"),
2038
+ onClick: () => {
2039
+ setGeom({});
2040
+ try {
2041
+ localStorage.removeItem(GEOM_KEY);
2042
+ } catch {}
2043
+ }
2044
+ }, "↺"), (0, react.createElement)("button", {
1982
2045
  className: "dsc-panel-close",
1983
2046
  "aria-label": t("close"),
1984
2047
  onClick: () => panelStore.close()
1985
- }, "✕")), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
2048
+ }, "✕"))), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
1986
2049
  t,
1987
2050
  active: tab === "workflows",
1988
2051
  label: t("tabWorkflows"),
@@ -2470,6 +2533,28 @@ window.__ModuleLoader__.load({
2470
2533
  setError(cause instanceof Error ? cause.message : String(cause));
2471
2534
  }
2472
2535
  }, [props.workflow.id, openFile]);
2536
+ /** Re-read the pack listing and — only when nothing is unsaved — the file
2537
+ * currently open, so files added or replaced outside this panel (the agent
2538
+ * writing via comfyui_skill, or the file manager after `打开目录`) show up
2539
+ * without leaving and re-entering the view. */
2540
+ const refreshFiles = async () => {
2541
+ setBusy(true);
2542
+ setNotice(null);
2543
+ try {
2544
+ const data = await getJson(`/comfyui/workflows/skill?id=${encodeURIComponent(props.workflow.id)}`);
2545
+ setEnabled(data.enabled === true);
2546
+ setPack(data.pack ?? null);
2547
+ if (Array.isArray(data.presetDirs) && data.presetDirs.length > 0) setPresetDirs(data.presetDirs);
2548
+ setError(null);
2549
+ props.onChanged();
2550
+ if (data.enabled === true && !dirty) await openFile(active);
2551
+ setNotice(t("skillRefreshed"));
2552
+ } catch (cause) {
2553
+ setError(cause instanceof Error ? cause.message : String(cause));
2554
+ } finally {
2555
+ setBusy(false);
2556
+ }
2557
+ };
2473
2558
  (0, react.useEffect)(() => {
2474
2559
  refresh();
2475
2560
  }, [refresh]);
@@ -2634,6 +2719,12 @@ window.__ModuleLoader__.load({
2634
2719
  /** Where a new file may go: the pack's own directories first, then the
2635
2720
  * presets it has not used yet. */
2636
2721
  const targetDirs = [...packDirs, ...presetDirs.filter((dir) => !packDirs.includes(dir))];
2722
+ /** One row in the pack file list; clicking opens it for editing. */
2723
+ const fileButton = (file) => (0, react.createElement)("button", {
2724
+ key: file.path,
2725
+ className: `dsc-skill-file${file.path === active ? " dsc-skill-file--active" : ""}`,
2726
+ onClick: () => selectFile(file.path)
2727
+ }, (0, react.createElement)("span", { className: "dsc-skill-file-name" }, file.path.includes("/") ? file.path.split("/")[1] : file.path), (0, react.createElement)("span", { className: "dsc-skill-file-size" }, formatBytes(file.size)));
2637
2728
  const fileList = (0, react.createElement)("div", {
2638
2729
  className: "dsc-skill-files",
2639
2730
  onDragOver: (event) => event.preventDefault(),
@@ -2677,7 +2768,12 @@ window.__ModuleLoader__.load({
2677
2768
  disabled: busy,
2678
2769
  title: t("skillRevealHint"),
2679
2770
  onClick: () => void revealDir()
2680
- }, t("skillReveal"))), creatingDir === null ? null : (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("input", {
2771
+ }, t("skillReveal")), (0, react.createElement)("button", {
2772
+ className: "dsc-btn dsc-btn--sm",
2773
+ disabled: busy,
2774
+ title: t("skillRefreshHint"),
2775
+ onClick: () => void refreshFiles()
2776
+ }, t("skillRefresh"))), creatingDir === null ? null : (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("input", {
2681
2777
  className: "dsc-input dsc-input--sm",
2682
2778
  value: creatingDir,
2683
2779
  placeholder: t("skillNewDirPlaceholder"),
@@ -2695,14 +2791,14 @@ window.__ModuleLoader__.load({
2695
2791
  }, t("skillCreate")), (0, react.createElement)("button", {
2696
2792
  className: "dsc-btn dsc-btn--sm",
2697
2793
  onClick: () => setCreatingDir(null)
2698
- }, t("cancel")))), (0, react.createElement)("div", { className: "dsc-skill-drop-hint" }, t("skillDropHint")), buckets.map((bucket) => bucket.items.length === 0 && bucket.label !== "" ? null : (0, react.createElement)("div", {
2699
- key: bucket.label === "" ? "." : bucket.label,
2794
+ }, t("cancel")))), (0, react.createElement)("div", { className: "dsc-skill-drop-hint" }, t("skillDropHint")), buckets.map((bucket) => bucket.items.length === 0 && bucket.label !== "" ? null : bucket.label === "" ? (0, react.createElement)("div", {
2795
+ key: ".",
2700
2796
  className: "dsc-skill-bucket"
2701
- }, bucket.label !== "" ? (0, react.createElement)("div", { className: "dsc-skill-bucket-name" }, `${bucket.label}/`) : null, bucket.items.map((file) => (0, react.createElement)("button", {
2702
- key: file.path,
2703
- className: `dsc-skill-file${file.path === active ? " dsc-skill-file--active" : ""}`,
2704
- onClick: () => selectFile(file.path)
2705
- }, (0, react.createElement)("span", { className: "dsc-skill-file-name" }, file.path.includes("/") ? file.path.split("/")[1] : file.path), (0, react.createElement)("span", { className: "dsc-skill-file-size" }, formatBytes(file.size)))))), creating === null ? null : (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("select", {
2797
+ }, bucket.items.map(fileButton)) : (0, react.createElement)("details", {
2798
+ key: bucket.label,
2799
+ className: "dsc-fold dsc-skill-fold",
2800
+ open: true
2801
+ }, (0, react.createElement)("summary", null, `${bucket.label}/`), (0, react.createElement)("div", { className: "dsc-skill-bucket" }, bucket.items.map(fileButton)))), creating === null ? null : (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("select", {
2706
2802
  className: "dsc-input dsc-input--sm",
2707
2803
  value: creating.bucket,
2708
2804
  onChange: (event) => setCreating({
@@ -2818,7 +2914,7 @@ window.__ModuleLoader__.load({
2818
2914
  action: "require",
2819
2915
  required: event.target.checked
2820
2916
  })
2821
- }), t("skillRequired")), (0, react.createElement)("span", { className: "dsc-hint" }, t("skillRequiredHint"))), pack !== null ? (0, react.createElement)("div", {
2917
+ }), t("skillRequired"))), pack !== null ? (0, react.createElement)("div", {
2822
2918
  className: "dsc-skill-dir",
2823
2919
  title: pack.dir
2824
2920
  }, `${t("skillDir")}: ${pack.dir}`) : null, pendingOpen !== null ? (0, react.createElement)("div", { className: "dsc-dialog" }, (0, react.createElement)("div", { className: "dsc-dialog-head" }, t("skillDirtyPrompt")), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
@@ -3926,6 +4022,9 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
3926
4022
  }
3927
4023
  .dsc-panel-resize:hover { opacity: 1; }
3928
4024
  .dsc-panel-title { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; font-size: 14px; }
4025
+ .dsc-panel-head-actions { display: inline-flex; align-items: center; gap: 2px; }
4026
+ .dsc-panel-reset { border: none; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; font-size: 13px; padding: 4px 6px; line-height: 1; }
4027
+ .dsc-panel-reset:hover { color: var(--dsw-alias-label-primary); }
3929
4028
  .dsc-panel-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 14px; padding: 4px 8px; }
3930
4029
  .dsc-panel-close:hover { color: var(--dsw-alias-label-primary); }
3931
4030
  .dsc-tabs { display: flex; border-bottom: 1px solid var(--dsw-alias-border-l1); }
@@ -3976,7 +4075,12 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
3976
4075
  .dsc-skill-files { flex: 0 0 190px; min-width: 160px; display: flex; flex-direction: column; gap: 6px; }
3977
4076
  .dsc-skill-main { flex: 1 1 300px; min-width: 260px; display: flex; flex-direction: column; gap: 6px; }
3978
4077
  .dsc-skill-bucket { display: flex; flex-direction: column; gap: 2px; }
3979
- .dsc-skill-bucket-name { font-size: 11px; color: var(--dsw-alias-label-secondary); margin-top: 4px; }
4078
+ /* Foldable pack directories: each folder renders as a <details> whose header
4079
+ stays put while its files collapse, keeping crowded packs scannable. */
4080
+ .dsc-skill-fold { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; margin: 2px 0; }
4081
+ .dsc-skill-fold summary { padding: 5px 10px; font-size: 12px; color: var(--dsw-alias-label-secondary); border-radius: 8px; }
4082
+ .dsc-skill-fold[open] summary { color: var(--dsw-alias-label-primary); border-bottom: 1px solid var(--dsw-alias-border-l1); border-radius: 8px 8px 0 0; }
4083
+ .dsc-skill-fold .dsc-skill-bucket { padding: 4px 6px 6px; }
3980
4084
  .dsc-skill-file {
3981
4085
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
3982
4086
  border: 1px solid transparent; border-radius: 6px; background: transparent;