dsh-comfyui 0.3.0-beta.5 → 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 +65 -168
- package/README.md +63 -166
- package/client/client.js +831 -33
- package/client/client.js.map +1 -1
- package/lib/config.d.ts +14 -0
- package/lib/config.js +6 -0
- package/lib/index.js +30 -10
- package/lib/routes.js +259 -0
- package/lib/skill.js +50 -3
- package/lib/skillpack.d.ts +262 -0
- package/lib/skillpack.js +774 -0
- package/lib/store.d.ts +13 -0
- package/lib/store.js +36 -0
- package/lib/tools.d.ts +3 -0
- package/lib/tools.js +289 -3
- package/package.json +4 -9
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: "
|
|
16
|
+
apiKeyEnvHint: "密钥名:环境变量或凭据存储(默认 COMFYUI_API_KEY)。",
|
|
17
17
|
hasApiKey: "已配置 API Key",
|
|
18
18
|
noApiKey: "未配置 API Key(本地服务通常不需要)",
|
|
19
19
|
save: "保存",
|
|
@@ -25,14 +25,16 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
testFail: "连接失败:{message}",
|
|
26
26
|
configNotWritable: "设置服务不可用,请通过 cordis.yml 修改配置。",
|
|
27
27
|
mediaHost: "媒体访问地址",
|
|
28
|
-
mediaHostHint: "
|
|
28
|
+
mediaHostHint: "媒体在聊天中显示的完整地址(如 http://192.168.1.5:3080)。留空自动检测;图片不显示时填你访问本页的地址。",
|
|
29
29
|
comfyuiDirs: "ComfyUI 目录",
|
|
30
|
-
comfyuiDirsHint: "本机 ComfyUI
|
|
30
|
+
comfyuiDirsHint: "本机 ComfyUI 安装目录(可多个,支持映射/多实例/便携版)。Agent 据此定位 models、工作流、音色库等文件。",
|
|
31
31
|
comfyuiDirAdd: "+ 添加目录",
|
|
32
32
|
comfyuiDirRemove: "移除该目录",
|
|
33
33
|
comfyuiDirPlaceholder: "如 D:\\ComfyUI",
|
|
34
|
+
skillsDir: "技能包目录",
|
|
35
|
+
skillsDirHint: "工作流技能包(SKILL.md 与参考文档)目录。留空 = 数据目录下 skills/;填绝对路径可放到别的盘或同步盘。改后需手动搬移已有目录。当前实际使用:{current}",
|
|
34
36
|
language: "界面语言",
|
|
35
|
-
languageHint: "
|
|
37
|
+
languageHint: "界面语言,切换后页面自动刷新。",
|
|
36
38
|
cardGenerating: "ComfyUI 生成中…",
|
|
37
39
|
cardBackground: "后台任务",
|
|
38
40
|
cardBackgroundDone: "已完成",
|
|
@@ -49,6 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
49
51
|
cardMode: "模式",
|
|
50
52
|
cardListed: "已列出 {runs} 个运行工作流,{graphs} 个图工作流",
|
|
51
53
|
panelTitle: "ComfyUI 面板",
|
|
54
|
+
panelReset: "复位位置与大小",
|
|
52
55
|
close: "关闭",
|
|
53
56
|
lbPrev: "上一张",
|
|
54
57
|
lbNext: "下一张",
|
|
@@ -167,6 +170,54 @@ window.__ModuleLoader__.load({
|
|
|
167
170
|
assetDeleteFailed: "删除失败:{message}",
|
|
168
171
|
confirmDelete: "确定删除",
|
|
169
172
|
cancel: "取消",
|
|
173
|
+
skillButton: "技能包",
|
|
174
|
+
skillBadge: "技能包",
|
|
175
|
+
skillBadgeRequired: "技能包·必读",
|
|
176
|
+
skillBadgeHint: "该工作流挂了技能包:Agent 运行前会读它。",
|
|
177
|
+
skillTitle: "技能包",
|
|
178
|
+
skillIntro: "点击「启用技能包」后,可为这个工作流添加给 Agent 看的额外 skills 说明:什么时候用、参数怎么填、有哪些坑。",
|
|
179
|
+
skillEnable: "启用技能包",
|
|
180
|
+
skillDetach: "停用(保留文件)",
|
|
181
|
+
skillDestroy: "销毁技能包",
|
|
182
|
+
skillDestroyConfirm: "⚠ 永久删除整个技能包({count} 个文件),不可恢复",
|
|
183
|
+
skillDestroyConfirmButton: "确认销毁",
|
|
184
|
+
skillDropHint: "可把文件拖到这里导入",
|
|
185
|
+
skillRequired: "运行前必读",
|
|
186
|
+
skillDir: "目录",
|
|
187
|
+
skillSummary: "摘要",
|
|
188
|
+
skillSummaryPlaceholder: "一句话说明这个工作流适合做什么",
|
|
189
|
+
skillSummaryHint: "Agent 靠它决定要不要读正文,等同 skills 里的 description 字段。",
|
|
190
|
+
skillSave: "保存",
|
|
191
|
+
skillSaved: "已保存",
|
|
192
|
+
skillDirty: "未保存",
|
|
193
|
+
skillDirtyPrompt: "当前文件有未保存的修改。",
|
|
194
|
+
skillSaveAndSwitch: "保存并切换",
|
|
195
|
+
skillDiscard: "丢弃修改",
|
|
196
|
+
skillImport: "↑ 导入文件",
|
|
197
|
+
skillImportHint: "导入本地文件到技能包:md/txt/json/yaml 进 references,脚本进 scripts,图片/csv 进 assets(也可直接拖进左栏)。",
|
|
198
|
+
skillImported: "已导入",
|
|
199
|
+
skillImageHint: "图片只能预览,不能在此编辑;Agent 会在正文点名它时才去读取。",
|
|
200
|
+
skillNewFile: "+ 新建文件",
|
|
201
|
+
skillNewDir: "+ 新建目录",
|
|
202
|
+
skillReveal: "📂 打开目录",
|
|
203
|
+
skillRevealHint: "在文件管理器中打开这个技能包的文件夹,方便直接拷贝文件进去。打开的是运行 DSH 那台机器上的目录;若你正从另一台机器访问本页面,请按下方显示的路径自行前往。",
|
|
204
|
+
skillRevealed: "已打开",
|
|
205
|
+
skillRefresh: "刷新",
|
|
206
|
+
skillRefreshHint: "重新读取技能包目录,显示 Agent 写入或文件管理器里新增/替换的文件。",
|
|
207
|
+
skillRefreshed: "已刷新",
|
|
208
|
+
skillNewDirPlaceholder: "目录名,如 templates",
|
|
209
|
+
skillNewDirHint: "技能包可以有任意一层子目录。常用:{presets};也可以自己起名(字母/数字/汉字/下划线/连字符)。",
|
|
210
|
+
skillDirCreated: "已创建目录",
|
|
211
|
+
skillNewPlaceholder: "文件名(默认 .md)",
|
|
212
|
+
skillCreate: "创建",
|
|
213
|
+
skillRename: "重命名",
|
|
214
|
+
skillDeleteFile: "删除文件",
|
|
215
|
+
skillDeleteConfirm: "删除文件",
|
|
216
|
+
skillReadFailed: "读取失败",
|
|
217
|
+
skillFailed: "操作失败",
|
|
218
|
+
skillDeleteWithWorkflow: "该工作流有技能包,要一起删除吗?",
|
|
219
|
+
skillDeleteBoth: "一起删除",
|
|
220
|
+
skillKeepPack: "只删工作流",
|
|
170
221
|
queueEmpty: "当前没有任务",
|
|
171
222
|
queueError: "无法读取队列(ComfyUI 未运行?)",
|
|
172
223
|
jobFilterLabel: "历史",
|
|
@@ -194,7 +245,7 @@ window.__ModuleLoader__.load({
|
|
|
194
245
|
baseUrl: "Server URL",
|
|
195
246
|
baseUrlHint: "ComfyUI HTTP address (default http://127.0.0.1:8188).",
|
|
196
247
|
apiKeyEnv: "API key env var",
|
|
197
|
-
apiKeyEnvHint: "
|
|
248
|
+
apiKeyEnvHint: "Name of the key: env var or credential store (default COMFYUI_API_KEY).",
|
|
198
249
|
hasApiKey: "API key configured",
|
|
199
250
|
noApiKey: "No API key configured (usually not needed for local servers)",
|
|
200
251
|
save: "Save",
|
|
@@ -206,14 +257,16 @@ window.__ModuleLoader__.load({
|
|
|
206
257
|
testFail: "Connection failed: {message}",
|
|
207
258
|
configNotWritable: "Settings service unavailable — edit cordis.yml instead.",
|
|
208
259
|
mediaHost: "Media base URL",
|
|
209
|
-
mediaHostHint: "
|
|
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.",
|
|
210
261
|
comfyuiDirs: "ComfyUI directories",
|
|
211
|
-
comfyuiDirsHint: "ComfyUI install
|
|
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.",
|
|
212
263
|
comfyuiDirAdd: "+ Add directory",
|
|
213
264
|
comfyuiDirRemove: "Remove this directory",
|
|
214
265
|
comfyuiDirPlaceholder: "e.g. D:\\ComfyUI",
|
|
266
|
+
skillsDir: "Skill pack directory",
|
|
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}",
|
|
215
268
|
language: "Language",
|
|
216
|
-
languageHint: "
|
|
269
|
+
languageHint: "UI language; the page reloads after switching.",
|
|
217
270
|
cardGenerating: "ComfyUI generating…",
|
|
218
271
|
cardBackground: "Background job",
|
|
219
272
|
cardBackgroundDone: "Completed",
|
|
@@ -230,6 +283,7 @@ window.__ModuleLoader__.load({
|
|
|
230
283
|
cardMode: "Mode",
|
|
231
284
|
cardListed: "Listed {runs} runnable workflows, {graphs} graph workflows",
|
|
232
285
|
panelTitle: "ComfyUI Panel",
|
|
286
|
+
panelReset: "Reset position & size",
|
|
233
287
|
close: "Close",
|
|
234
288
|
lbPrev: "Previous image",
|
|
235
289
|
lbNext: "Next image",
|
|
@@ -348,6 +402,54 @@ window.__ModuleLoader__.load({
|
|
|
348
402
|
assetDeleteFailed: "Delete failed: {message}",
|
|
349
403
|
confirmDelete: "Delete",
|
|
350
404
|
cancel: "Cancel",
|
|
405
|
+
skillButton: "Skill pack",
|
|
406
|
+
skillBadge: "Skill pack",
|
|
407
|
+
skillBadgeRequired: "Skill pack (required)",
|
|
408
|
+
skillBadgeHint: "This workflow has a skill pack: the agent reads it before running.",
|
|
409
|
+
skillTitle: "Skill pack",
|
|
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.",
|
|
411
|
+
skillEnable: "Create skill pack",
|
|
412
|
+
skillDetach: "Detach (keep files)",
|
|
413
|
+
skillDestroy: "Destroy pack",
|
|
414
|
+
skillDestroyConfirm: "⚠ Permanently delete the whole pack ({count} files) — cannot be undone",
|
|
415
|
+
skillDestroyConfirmButton: "Destroy",
|
|
416
|
+
skillDropHint: "Drop files here to import",
|
|
417
|
+
skillRequired: "Required before running",
|
|
418
|
+
skillDir: "Directory",
|
|
419
|
+
skillSummary: "Summary",
|
|
420
|
+
skillSummaryPlaceholder: "One line on what this workflow is for",
|
|
421
|
+
skillSummaryHint: "The agent uses it to decide whether to read the body — like the description field of a skill.",
|
|
422
|
+
skillSave: "Save",
|
|
423
|
+
skillSaved: "Saved",
|
|
424
|
+
skillDirty: "Unsaved",
|
|
425
|
+
skillDirtyPrompt: "The current file has unsaved changes.",
|
|
426
|
+
skillSaveAndSwitch: "Save and switch",
|
|
427
|
+
skillDiscard: "Discard changes",
|
|
428
|
+
skillImport: "Import files",
|
|
429
|
+
skillImportHint: "Import local files into the pack: md/txt/json/yaml go to references, scripts to scripts, images/csv to assets (drag onto the list works too).",
|
|
430
|
+
skillImported: "Imported",
|
|
431
|
+
skillImageHint: "Images are preview-only here; the agent reads one only when the body points at it.",
|
|
432
|
+
skillNewFile: "+ New file",
|
|
433
|
+
skillNewDir: "+ New folder",
|
|
434
|
+
skillReveal: "📂 Open folder",
|
|
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.",
|
|
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",
|
|
440
|
+
skillNewDirPlaceholder: "Folder name, e.g. templates",
|
|
441
|
+
skillNewDirHint: "A pack may hold any one-level sub-directory. Common ones: {presets}; custom names work too (letters, digits, CJK, underscore, dash).",
|
|
442
|
+
skillDirCreated: "Folder created",
|
|
443
|
+
skillNewPlaceholder: "File name (.md by default)",
|
|
444
|
+
skillCreate: "Create",
|
|
445
|
+
skillRename: "Rename",
|
|
446
|
+
skillDeleteFile: "Delete file",
|
|
447
|
+
skillDeleteConfirm: "Delete file",
|
|
448
|
+
skillReadFailed: "Read failed",
|
|
449
|
+
skillFailed: "Operation failed",
|
|
450
|
+
skillDeleteWithWorkflow: "This workflow has a skill pack. Delete it too?",
|
|
451
|
+
skillDeleteBoth: "Delete both",
|
|
452
|
+
skillKeepPack: "Workflow only",
|
|
351
453
|
queueEmpty: "No tasks right now",
|
|
352
454
|
queueError: "Cannot read the queue (ComfyUI not running?)",
|
|
353
455
|
jobFilterLabel: "History",
|
|
@@ -691,6 +793,19 @@ window.__ModuleLoader__.load({
|
|
|
691
793
|
}
|
|
692
794
|
return response.json();
|
|
693
795
|
}
|
|
796
|
+
/** POST raw bytes (an imported file) and read the JSON reply. */
|
|
797
|
+
async function postRaw(url, bytes) {
|
|
798
|
+
const response = await fetch(url, {
|
|
799
|
+
method: "POST",
|
|
800
|
+
headers: { "content-type": "application/octet-stream" },
|
|
801
|
+
body: bytes
|
|
802
|
+
});
|
|
803
|
+
if (!response.ok) {
|
|
804
|
+
const payload = await response.json().catch(() => ({}));
|
|
805
|
+
throw new Error(payload.error ?? `HTTP ${response.status}`);
|
|
806
|
+
}
|
|
807
|
+
return response.json();
|
|
808
|
+
}
|
|
694
809
|
//#endregion
|
|
695
810
|
//#region src/client/settings.tsx
|
|
696
811
|
/**
|
|
@@ -705,6 +820,7 @@ window.__ModuleLoader__.load({
|
|
|
705
820
|
const [apiKeyEnv, setApiKeyEnv] = (0, react.useState)("");
|
|
706
821
|
const [mediaHost, setMediaHost] = (0, react.useState)("");
|
|
707
822
|
const [comfyuiDirs, setComfyuiDirs] = (0, react.useState)([]);
|
|
823
|
+
const [skillsDir, setSkillsDir] = (0, react.useState)("");
|
|
708
824
|
const [loadError, setLoadError] = (0, react.useState)(null);
|
|
709
825
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
710
826
|
const [saveState, setSaveState] = (0, react.useState)("idle");
|
|
@@ -720,6 +836,7 @@ window.__ModuleLoader__.load({
|
|
|
720
836
|
setApiKeyEnv(view.apiKeyEnv);
|
|
721
837
|
setMediaHost(view.mediaHost ?? "");
|
|
722
838
|
setComfyuiDirs(Array.isArray(view.comfyuiDirs) ? view.comfyuiDirs : []);
|
|
839
|
+
setSkillsDir(view.skillsDir ?? "");
|
|
723
840
|
}).catch((error) => {
|
|
724
841
|
if (cancelled) return;
|
|
725
842
|
setLoadError(error instanceof Error ? error.message : String(error));
|
|
@@ -737,7 +854,8 @@ window.__ModuleLoader__.load({
|
|
|
737
854
|
baseUrl,
|
|
738
855
|
apiKeyEnv,
|
|
739
856
|
mediaHost,
|
|
740
|
-
comfyuiDirs: dirs
|
|
857
|
+
comfyuiDirs: dirs,
|
|
858
|
+
skillsDir: skillsDir.trim()
|
|
741
859
|
} });
|
|
742
860
|
if (payload.config !== void 0) {
|
|
743
861
|
setConfig(payload.config);
|
|
@@ -745,6 +863,7 @@ window.__ModuleLoader__.load({
|
|
|
745
863
|
setApiKeyEnv(payload.config.apiKeyEnv);
|
|
746
864
|
setMediaHost(payload.config.mediaHost ?? "");
|
|
747
865
|
setComfyuiDirs(payload.config.comfyuiDirs ?? []);
|
|
866
|
+
setSkillsDir(payload.config.skillsDir ?? "");
|
|
748
867
|
}
|
|
749
868
|
setSaveState("saved");
|
|
750
869
|
} catch (error) {
|
|
@@ -850,7 +969,15 @@ window.__ModuleLoader__.load({
|
|
|
850
969
|
setComfyuiDirs([...comfyuiDirs, ""]);
|
|
851
970
|
setSaveState("idle");
|
|
852
971
|
}
|
|
853
|
-
}, t("comfyuiDirAdd")), (0, react.createElement)("div", { className: "dsc-hint" }, t("comfyuiDirsHint"))), (0, react.createElement)("div", { className: "dsc-
|
|
972
|
+
}, t("comfyuiDirAdd")), (0, react.createElement)("div", { className: "dsc-hint" }, t("comfyuiDirsHint"))), (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("skillsDir")), (0, react.createElement)("input", {
|
|
973
|
+
className: "dsc-input",
|
|
974
|
+
value: skillsDir,
|
|
975
|
+
placeholder: config.skillsRoot,
|
|
976
|
+
onChange: (event) => {
|
|
977
|
+
setSkillsDir(event.target.value);
|
|
978
|
+
setSaveState("idle");
|
|
979
|
+
}
|
|
980
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, t("skillsDirHint", { current: config.skillsRoot }))), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
854
981
|
className: "dsc-btn",
|
|
855
982
|
disabled: saving || !config.writable,
|
|
856
983
|
onClick: () => void save()
|
|
@@ -989,6 +1116,62 @@ window.__ModuleLoader__.load({
|
|
|
989
1116
|
localStorage.setItem(GEOM_KEY, JSON.stringify(geom));
|
|
990
1117
|
} catch {}
|
|
991
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
|
+
}
|
|
1154
|
+
/** Fallback suggestions when the config route has not answered yet; the
|
|
1155
|
+
* authoritative list comes from the host (`presetDirs`). */
|
|
1156
|
+
const SKILL_FALLBACK_DIRS = [
|
|
1157
|
+
"references",
|
|
1158
|
+
"scripts",
|
|
1159
|
+
"assets"
|
|
1160
|
+
];
|
|
1161
|
+
/** Imported binaries land in assets/; the editor previews these instead of
|
|
1162
|
+
* trying to read them as text. */
|
|
1163
|
+
const SKILL_IMAGE_EXTENSIONS = [
|
|
1164
|
+
".png",
|
|
1165
|
+
".jpg",
|
|
1166
|
+
".jpeg",
|
|
1167
|
+
".webp",
|
|
1168
|
+
".gif",
|
|
1169
|
+
".svg"
|
|
1170
|
+
];
|
|
1171
|
+
function isSkillImage(path) {
|
|
1172
|
+
const dot = path.lastIndexOf(".");
|
|
1173
|
+
return dot !== -1 && SKILL_IMAGE_EXTENSIONS.includes(path.slice(dot).toLowerCase());
|
|
1174
|
+
}
|
|
992
1175
|
/** Preset workflow classification tags (users can add custom ones). Stored as
|
|
993
1176
|
* plain text, so the tag identity is the label itself. */
|
|
994
1177
|
const PRESET_TAGS = [
|
|
@@ -1755,7 +1938,7 @@ window.__ModuleLoader__.load({
|
|
|
1755
1938
|
function ComfyUIPanel({ t }) {
|
|
1756
1939
|
const open = usePanelOpen();
|
|
1757
1940
|
const tab = usePanelTab();
|
|
1758
|
-
const [geom, setGeom] = (0, react.useState)(loadGeom);
|
|
1941
|
+
const [geom, setGeom] = (0, react.useState)(() => healGeom(loadGeom(), window.innerWidth, window.innerHeight));
|
|
1759
1942
|
const [dragging, setDragging] = (0, react.useState)(null);
|
|
1760
1943
|
const [lightbox, setLightbox] = (0, react.useState)(null);
|
|
1761
1944
|
const panelRef = (0, react.useRef)(null);
|
|
@@ -1766,9 +1949,18 @@ window.__ModuleLoader__.load({
|
|
|
1766
1949
|
index
|
|
1767
1950
|
});
|
|
1768
1951
|
(0, react.useEffect)(() => {
|
|
1769
|
-
if (open)
|
|
1952
|
+
if (open) {
|
|
1953
|
+
setGeom((prev) => healGeom(prev, window.innerWidth, window.innerHeight));
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1770
1956
|
saveGeom(geom);
|
|
1771
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]);
|
|
1772
1964
|
const startDrag = (event, mode) => {
|
|
1773
1965
|
if (event.button !== 0) return;
|
|
1774
1966
|
if (mode === "move") {
|
|
@@ -1792,12 +1984,14 @@ window.__ModuleLoader__.load({
|
|
|
1792
1984
|
const moveDrag = (event) => {
|
|
1793
1985
|
const d = dragRef.current;
|
|
1794
1986
|
if (d === null) return;
|
|
1795
|
-
if (dragging === "move")
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
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") {
|
|
1801
1995
|
const width = Math.max(300, Math.min(720, d.width + event.clientX - d.startX));
|
|
1802
1996
|
const height = Math.max(200, Math.min(Math.max(300, window.innerHeight - 40), d.height + event.clientY - d.startY));
|
|
1803
1997
|
setGeom((prev) => ({
|
|
@@ -1836,11 +2030,22 @@ window.__ModuleLoader__.load({
|
|
|
1836
2030
|
onPointerMove: (event) => moveDrag(event),
|
|
1837
2031
|
onPointerUp: () => endDrag(),
|
|
1838
2032
|
onPointerCancel: () => endDrag()
|
|
1839
|
-
}, (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", {
|
|
1840
2045
|
className: "dsc-panel-close",
|
|
1841
2046
|
"aria-label": t("close"),
|
|
1842
2047
|
onClick: () => panelStore.close()
|
|
1843
|
-
}, "✕")), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
|
|
2048
|
+
}, "✕"))), (0, react.createElement)("div", { className: "dsc-tabs" }, (0, react.createElement)(TabButton, {
|
|
1844
2049
|
t,
|
|
1845
2050
|
active: tab === "workflows",
|
|
1846
2051
|
label: t("tabWorkflows"),
|
|
@@ -2259,6 +2464,503 @@ window.__ModuleLoader__.load({
|
|
|
2259
2464
|
}
|
|
2260
2465
|
}));
|
|
2261
2466
|
}
|
|
2467
|
+
/**
|
|
2468
|
+
* The skill-pack editor for one workflow: attach/detach the pack, toggle the
|
|
2469
|
+
* run-time gate, and manage its files (SKILL.md plus references/ and scripts/).
|
|
2470
|
+
*
|
|
2471
|
+
* The pack is what `comfyui_workflow action: skill` hands the agent, so this
|
|
2472
|
+
* view is the only place its content is authored. Each file saves explicitly —
|
|
2473
|
+
* no autosave — because the model reads whatever is on disk the moment it asks.
|
|
2474
|
+
* SKILL.md is edited as a summary field plus a body: the `---` frontmatter that
|
|
2475
|
+
* carries the summary is composed on the host, never typed here.
|
|
2476
|
+
*/
|
|
2477
|
+
function SkillPackEditor(props) {
|
|
2478
|
+
const { t } = props;
|
|
2479
|
+
const [pack, setPack] = (0, react.useState)(null);
|
|
2480
|
+
const [enabled, setEnabled] = (0, react.useState)(props.workflow.skillDir !== void 0);
|
|
2481
|
+
const [active, setActive] = (0, react.useState)("SKILL.md");
|
|
2482
|
+
const [summary, setSummary] = (0, react.useState)("");
|
|
2483
|
+
const [text, setText] = (0, react.useState)("");
|
|
2484
|
+
const [dirty, setDirty] = (0, react.useState)(false);
|
|
2485
|
+
const [pendingOpen, setPendingOpen] = (0, react.useState)(null);
|
|
2486
|
+
const [creating, setCreating] = (0, react.useState)(null);
|
|
2487
|
+
const [creatingDir, setCreatingDir] = (0, react.useState)(null);
|
|
2488
|
+
const [presetDirs, setPresetDirs] = (0, react.useState)(SKILL_FALLBACK_DIRS);
|
|
2489
|
+
const [renaming, setRenaming] = (0, react.useState)(null);
|
|
2490
|
+
const [confirming, setConfirming] = (0, react.useState)(null);
|
|
2491
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2492
|
+
const [error, setError] = (0, react.useState)(null);
|
|
2493
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
2494
|
+
/** Load one file into the editor; SKILL.md arrives pre-split into summary + body. */
|
|
2495
|
+
const openFile = (0, react.useCallback)(async (path) => {
|
|
2496
|
+
if (isSkillImage(path)) {
|
|
2497
|
+
setActive(path);
|
|
2498
|
+
setSummary("");
|
|
2499
|
+
setText("");
|
|
2500
|
+
setDirty(false);
|
|
2501
|
+
setError(null);
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
try {
|
|
2505
|
+
const data = await getJson(`/comfyui/workflows/skill?id=${encodeURIComponent(props.workflow.id)}&path=${encodeURIComponent(path)}`);
|
|
2506
|
+
if (data.ok !== true) {
|
|
2507
|
+
setError(data.error ?? t("skillReadFailed"));
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
setActive(path);
|
|
2511
|
+
if (path === "SKILL.md") {
|
|
2512
|
+
setSummary(data.summary ?? "");
|
|
2513
|
+
setText(data.body ?? "");
|
|
2514
|
+
} else {
|
|
2515
|
+
setSummary("");
|
|
2516
|
+
setText(data.content ?? "");
|
|
2517
|
+
}
|
|
2518
|
+
setDirty(false);
|
|
2519
|
+
setError(null);
|
|
2520
|
+
} catch (cause) {
|
|
2521
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2522
|
+
}
|
|
2523
|
+
}, [props.workflow.id, t]);
|
|
2524
|
+
/** Re-read the pack listing, then the requested file. */
|
|
2525
|
+
const refresh = (0, react.useCallback)(async (path) => {
|
|
2526
|
+
try {
|
|
2527
|
+
const data = await getJson(`/comfyui/workflows/skill?id=${encodeURIComponent(props.workflow.id)}`);
|
|
2528
|
+
setEnabled(data.enabled === true);
|
|
2529
|
+
setPack(data.pack ?? null);
|
|
2530
|
+
if (Array.isArray(data.presetDirs) && data.presetDirs.length > 0) setPresetDirs(data.presetDirs);
|
|
2531
|
+
if (data.enabled === true) await openFile(path ?? "SKILL.md");
|
|
2532
|
+
} catch (cause) {
|
|
2533
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2534
|
+
}
|
|
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
|
+
};
|
|
2558
|
+
(0, react.useEffect)(() => {
|
|
2559
|
+
refresh();
|
|
2560
|
+
}, [refresh]);
|
|
2561
|
+
/** Every mutation goes through here so the listing and the flags stay in sync. */
|
|
2562
|
+
const mutate = async (body, after) => {
|
|
2563
|
+
setBusy(true);
|
|
2564
|
+
setNotice(null);
|
|
2565
|
+
try {
|
|
2566
|
+
const data = await postJson("/comfyui/workflows/skill", {
|
|
2567
|
+
id: props.workflow.id,
|
|
2568
|
+
...body
|
|
2569
|
+
});
|
|
2570
|
+
if (data.ok !== true) {
|
|
2571
|
+
setError(data.error ?? t("skillFailed"));
|
|
2572
|
+
return false;
|
|
2573
|
+
}
|
|
2574
|
+
setEnabled(data.enabled === true);
|
|
2575
|
+
setPack(data.pack ?? null);
|
|
2576
|
+
setError(null);
|
|
2577
|
+
props.onChanged();
|
|
2578
|
+
if (after !== void 0 && data.enabled === true) await openFile(after);
|
|
2579
|
+
return true;
|
|
2580
|
+
} catch (cause) {
|
|
2581
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2582
|
+
return false;
|
|
2583
|
+
} finally {
|
|
2584
|
+
setBusy(false);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
const saveFile = async () => {
|
|
2588
|
+
const body = {
|
|
2589
|
+
action: "write",
|
|
2590
|
+
path: active,
|
|
2591
|
+
content: text
|
|
2592
|
+
};
|
|
2593
|
+
if (active === "SKILL.md") body.summary = summary;
|
|
2594
|
+
const ok = await mutate(body);
|
|
2595
|
+
if (ok) {
|
|
2596
|
+
setDirty(false);
|
|
2597
|
+
setNotice(`${t("skillSaved")} ${active}`);
|
|
2598
|
+
}
|
|
2599
|
+
return ok;
|
|
2600
|
+
};
|
|
2601
|
+
/** Switching files with unsaved edits parks the request until the user decides. */
|
|
2602
|
+
const selectFile = (path) => {
|
|
2603
|
+
if (path === active) return;
|
|
2604
|
+
if (dirty) {
|
|
2605
|
+
setPendingOpen(path);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
openFile(path);
|
|
2609
|
+
};
|
|
2610
|
+
const createFile = async () => {
|
|
2611
|
+
if (creating === null) return;
|
|
2612
|
+
const name = creating.name.trim();
|
|
2613
|
+
if (name === "") return;
|
|
2614
|
+
const path = `${creating.bucket}/${name.includes(".") ? name : `${name}.md`}`;
|
|
2615
|
+
if (await mutate({
|
|
2616
|
+
action: "write",
|
|
2617
|
+
path,
|
|
2618
|
+
content: ""
|
|
2619
|
+
}, path)) setCreating(null);
|
|
2620
|
+
};
|
|
2621
|
+
/** Open the pack directory in the desktop file manager (on the machine
|
|
2622
|
+
* running DSH — the hint says so, and the path stays visible below). */
|
|
2623
|
+
const revealDir = async () => {
|
|
2624
|
+
setNotice(null);
|
|
2625
|
+
try {
|
|
2626
|
+
const data = await postJson("/comfyui/workflows/skill/reveal", { id: props.workflow.id });
|
|
2627
|
+
if (data.ok !== true) {
|
|
2628
|
+
setError(data.error ?? t("skillFailed"));
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
setError(null);
|
|
2632
|
+
setNotice(`${t("skillRevealed")} ${data.dir ?? ""}`);
|
|
2633
|
+
} catch (cause) {
|
|
2634
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
const createDir = async () => {
|
|
2638
|
+
if (creatingDir === null) return;
|
|
2639
|
+
const name = creatingDir.trim();
|
|
2640
|
+
if (name === "") return;
|
|
2641
|
+
if (await mutate({
|
|
2642
|
+
action: "mkdir",
|
|
2643
|
+
name
|
|
2644
|
+
})) {
|
|
2645
|
+
setCreatingDir(null);
|
|
2646
|
+
setNotice(`${t("skillDirCreated")} ${name}/`);
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
const applyRename = async () => {
|
|
2650
|
+
if (renaming === null) return;
|
|
2651
|
+
const to = renaming.to.trim();
|
|
2652
|
+
if (to === "") return;
|
|
2653
|
+
const bucket = renaming.from.includes("/") ? `${renaming.from.split("/")[0] ?? ""}/` : "";
|
|
2654
|
+
const target = to.includes("/") ? to : `${bucket}${to}`;
|
|
2655
|
+
if (await mutate({
|
|
2656
|
+
action: "rename",
|
|
2657
|
+
path: renaming.from,
|
|
2658
|
+
to: target
|
|
2659
|
+
}, target)) setRenaming(null);
|
|
2660
|
+
};
|
|
2661
|
+
/** Import local files into the pack. The destination bucket comes from the
|
|
2662
|
+
* host (one extension rule, shared with the agent-facing listing), so the
|
|
2663
|
+
* panel only sends the name and the bytes. */
|
|
2664
|
+
const importFiles = async (files) => {
|
|
2665
|
+
if (files.length === 0) return;
|
|
2666
|
+
setBusy(true);
|
|
2667
|
+
setNotice(null);
|
|
2668
|
+
let imported = null;
|
|
2669
|
+
let failed = 0;
|
|
2670
|
+
for (const file of files) try {
|
|
2671
|
+
const bytes = await file.arrayBuffer();
|
|
2672
|
+
const data = await postRaw(`/comfyui/workflows/skill/import?id=${encodeURIComponent(props.workflow.id)}&name=${encodeURIComponent(file.name)}`, bytes);
|
|
2673
|
+
if (data.ok !== true) {
|
|
2674
|
+
failed += 1;
|
|
2675
|
+
setError(data.error ?? t("skillFailed"));
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
setPack(data.pack ?? null);
|
|
2679
|
+
imported = data.path ?? null;
|
|
2680
|
+
} catch (cause) {
|
|
2681
|
+
failed += 1;
|
|
2682
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
2683
|
+
}
|
|
2684
|
+
setBusy(false);
|
|
2685
|
+
props.onChanged();
|
|
2686
|
+
if (imported !== null) {
|
|
2687
|
+
if (failed === 0) {
|
|
2688
|
+
setError(null);
|
|
2689
|
+
setNotice(`${t("skillImported")} ${imported}`);
|
|
2690
|
+
}
|
|
2691
|
+
await openFile(imported);
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
const head = (0, react.createElement)("div", { className: "dsc-view-head" }, (0, react.createElement)("button", {
|
|
2695
|
+
className: "dsc-btn",
|
|
2696
|
+
onClick: props.onBack
|
|
2697
|
+
}, t("wfViewBack")), (0, react.createElement)("div", {
|
|
2698
|
+
className: "dsc-wf-name",
|
|
2699
|
+
title: props.workflow.name
|
|
2700
|
+
}, `${t("skillTitle")} · ${props.workflow.name}`));
|
|
2701
|
+
if (!enabled) return (0, react.createElement)("div", null, head, (0, react.createElement)("div", { className: "dsc-hint" }, t("skillIntro")), error !== null ? (0, react.createElement)(ErrorNote, {
|
|
2702
|
+
t,
|
|
2703
|
+
message: error
|
|
2704
|
+
}) : null, (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2705
|
+
className: "dsc-btn",
|
|
2706
|
+
disabled: busy,
|
|
2707
|
+
onClick: () => void mutate({ action: "enable" }, "SKILL.md")
|
|
2708
|
+
}, t("skillEnable"))));
|
|
2709
|
+
const files = pack?.files ?? [];
|
|
2710
|
+
const impliedDirs = files.filter((file) => file.path.includes("/")).map((file) => file.path.split("/")[0] ?? "").filter((dir) => dir !== "");
|
|
2711
|
+
const packDirs = [.../* @__PURE__ */ new Set([...pack?.dirs ?? [], ...impliedDirs])].sort((a, b) => a.localeCompare(b));
|
|
2712
|
+
const buckets = [{
|
|
2713
|
+
label: "",
|
|
2714
|
+
items: files.filter((file) => !file.path.includes("/"))
|
|
2715
|
+
}, ...packDirs.map((bucket) => ({
|
|
2716
|
+
label: bucket,
|
|
2717
|
+
items: files.filter((file) => file.path.startsWith(`${bucket}/`))
|
|
2718
|
+
}))];
|
|
2719
|
+
/** Where a new file may go: the pack's own directories first, then the
|
|
2720
|
+
* presets it has not used yet. */
|
|
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)));
|
|
2728
|
+
const fileList = (0, react.createElement)("div", {
|
|
2729
|
+
className: "dsc-skill-files",
|
|
2730
|
+
onDragOver: (event) => event.preventDefault(),
|
|
2731
|
+
onDrop: (event) => {
|
|
2732
|
+
event.preventDefault();
|
|
2733
|
+
const dropped = Array.from(event.dataTransfer?.files ?? []);
|
|
2734
|
+
if (dropped.length > 0) importFiles(dropped);
|
|
2735
|
+
}
|
|
2736
|
+
}, (0, react.createElement)("div", { className: "dsc-skill-actions" }, (0, react.createElement)("label", {
|
|
2737
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2738
|
+
title: t("skillImportHint")
|
|
2739
|
+
}, t("skillImport"), (0, react.createElement)("input", {
|
|
2740
|
+
type: "file",
|
|
2741
|
+
multiple: true,
|
|
2742
|
+
style: { display: "none" },
|
|
2743
|
+
disabled: busy,
|
|
2744
|
+
onChange: (event) => {
|
|
2745
|
+
const picked = Array.from(event.target.files ?? []);
|
|
2746
|
+
event.target.value = "";
|
|
2747
|
+
if (picked.length > 0) importFiles(picked);
|
|
2748
|
+
}
|
|
2749
|
+
})), creating === null ? (0, react.createElement)("button", {
|
|
2750
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2751
|
+
disabled: busy,
|
|
2752
|
+
onClick: () => {
|
|
2753
|
+
setCreatingDir(null);
|
|
2754
|
+
setCreating({
|
|
2755
|
+
bucket: targetDirs[0] ?? "references",
|
|
2756
|
+
name: ""
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
}, t("skillNewFile")) : null, creatingDir === null ? (0, react.createElement)("button", {
|
|
2760
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2761
|
+
disabled: busy,
|
|
2762
|
+
onClick: () => {
|
|
2763
|
+
setCreating(null);
|
|
2764
|
+
setCreatingDir("");
|
|
2765
|
+
}
|
|
2766
|
+
}, t("skillNewDir")) : null, (0, react.createElement)("button", {
|
|
2767
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2768
|
+
disabled: busy,
|
|
2769
|
+
title: t("skillRevealHint"),
|
|
2770
|
+
onClick: () => void revealDir()
|
|
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", {
|
|
2777
|
+
className: "dsc-input dsc-input--sm",
|
|
2778
|
+
value: creatingDir,
|
|
2779
|
+
placeholder: t("skillNewDirPlaceholder"),
|
|
2780
|
+
onChange: (event) => setCreatingDir(event.target.value),
|
|
2781
|
+
onKeyDown: (event) => {
|
|
2782
|
+
if (event.key === "Enter") {
|
|
2783
|
+
event.preventDefault();
|
|
2784
|
+
createDir();
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, t("skillNewDirHint", { presets: presetDirs.join(" / ") })), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2788
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2789
|
+
disabled: busy,
|
|
2790
|
+
onClick: () => void createDir()
|
|
2791
|
+
}, t("skillCreate")), (0, react.createElement)("button", {
|
|
2792
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2793
|
+
onClick: () => setCreatingDir(null)
|
|
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: ".",
|
|
2796
|
+
className: "dsc-skill-bucket"
|
|
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", {
|
|
2802
|
+
className: "dsc-input dsc-input--sm",
|
|
2803
|
+
value: creating.bucket,
|
|
2804
|
+
onChange: (event) => setCreating({
|
|
2805
|
+
...creating,
|
|
2806
|
+
bucket: event.target.value
|
|
2807
|
+
})
|
|
2808
|
+
}, targetDirs.map((bucket) => (0, react.createElement)("option", {
|
|
2809
|
+
key: bucket,
|
|
2810
|
+
value: bucket
|
|
2811
|
+
}, `${bucket}/`))), (0, react.createElement)("input", {
|
|
2812
|
+
className: "dsc-input dsc-input--sm",
|
|
2813
|
+
value: creating.name,
|
|
2814
|
+
placeholder: t("skillNewPlaceholder"),
|
|
2815
|
+
onChange: (event) => setCreating({
|
|
2816
|
+
...creating,
|
|
2817
|
+
name: event.target.value
|
|
2818
|
+
}),
|
|
2819
|
+
onKeyDown: (event) => {
|
|
2820
|
+
if (event.key === "Enter") {
|
|
2821
|
+
event.preventDefault();
|
|
2822
|
+
createFile();
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2826
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2827
|
+
disabled: busy,
|
|
2828
|
+
onClick: () => void createFile()
|
|
2829
|
+
}, t("skillCreate")), (0, react.createElement)("button", {
|
|
2830
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2831
|
+
onClick: () => setCreating(null)
|
|
2832
|
+
}, t("cancel")))));
|
|
2833
|
+
const editor = (0, react.createElement)("div", { className: "dsc-skill-main" }, (0, react.createElement)("div", { className: "dsc-skill-editor-head" }, (0, react.createElement)("span", { className: "dsc-skill-path" }, active), dirty ? (0, react.createElement)("span", { className: "dsc-badge dsc-badge--warn" }, t("skillDirty")) : null), active === "SKILL.md" ? (0, react.createElement)("div", { className: "dsc-field" }, (0, react.createElement)("label", null, t("skillSummary")), (0, react.createElement)("input", {
|
|
2834
|
+
className: "dsc-input",
|
|
2835
|
+
value: summary,
|
|
2836
|
+
placeholder: t("skillSummaryPlaceholder"),
|
|
2837
|
+
onChange: (event) => {
|
|
2838
|
+
setSummary(event.target.value);
|
|
2839
|
+
setDirty(true);
|
|
2840
|
+
}
|
|
2841
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, t("skillSummaryHint"))) : null, isSkillImage(active) ? (0, react.createElement)("div", { className: "dsc-skill-preview" }, (0, react.createElement)("img", {
|
|
2842
|
+
src: `/comfyui/workflows/skill/raw?id=${encodeURIComponent(props.workflow.id)}&path=${encodeURIComponent(active)}`,
|
|
2843
|
+
alt: active
|
|
2844
|
+
}), (0, react.createElement)("div", { className: "dsc-hint" }, t("skillImageHint"))) : (0, react.createElement)("textarea", {
|
|
2845
|
+
className: "dsc-textarea dsc-textarea--skill",
|
|
2846
|
+
value: text,
|
|
2847
|
+
spellCheck: false,
|
|
2848
|
+
onChange: (event) => {
|
|
2849
|
+
setText(event.target.value);
|
|
2850
|
+
setDirty(true);
|
|
2851
|
+
},
|
|
2852
|
+
onKeyDown: (event) => {
|
|
2853
|
+
if (event.key !== "Tab") return;
|
|
2854
|
+
event.preventDefault();
|
|
2855
|
+
const area = event.target;
|
|
2856
|
+
const start = area.selectionStart;
|
|
2857
|
+
const end = area.selectionEnd;
|
|
2858
|
+
setText(`${text.slice(0, start)} ${text.slice(end)}`);
|
|
2859
|
+
setDirty(true);
|
|
2860
|
+
window.requestAnimationFrame(() => {
|
|
2861
|
+
area.selectionStart = start + 2;
|
|
2862
|
+
area.selectionEnd = start + 2;
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}), (0, react.createElement)("div", { className: "dsc-row" }, isSkillImage(active) ? null : (0, react.createElement)("button", {
|
|
2866
|
+
className: "dsc-btn",
|
|
2867
|
+
disabled: busy || !dirty,
|
|
2868
|
+
onClick: () => void saveFile()
|
|
2869
|
+
}, t("skillSave")), active !== "SKILL.md" ? (0, react.createElement)("button", {
|
|
2870
|
+
className: "dsc-btn",
|
|
2871
|
+
disabled: busy,
|
|
2872
|
+
onClick: () => setRenaming({
|
|
2873
|
+
from: active,
|
|
2874
|
+
to: active.includes("/") ? active.split("/")[1] ?? "" : active
|
|
2875
|
+
})
|
|
2876
|
+
}, t("skillRename")) : null, active !== "SKILL.md" ? (0, react.createElement)("button", {
|
|
2877
|
+
className: "dsc-btn",
|
|
2878
|
+
disabled: busy,
|
|
2879
|
+
onClick: () => setConfirming(active)
|
|
2880
|
+
}, t("skillDeleteFile")) : null), confirming !== null && confirming !== "*pack*" ? (0, react.createElement)("div", { className: "dsc-dialog dsc-dialog--danger" }, (0, react.createElement)("div", { className: "dsc-dialog-head dsc-danger-text" }, `${t("skillDeleteConfirm")} ${confirming}`), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2881
|
+
className: "dsc-btn dsc-btn--danger",
|
|
2882
|
+
disabled: busy,
|
|
2883
|
+
onClick: () => {
|
|
2884
|
+
const target = confirming;
|
|
2885
|
+
setConfirming(null);
|
|
2886
|
+
mutate({
|
|
2887
|
+
action: "delete",
|
|
2888
|
+
path: target
|
|
2889
|
+
}, "SKILL.md");
|
|
2890
|
+
}
|
|
2891
|
+
}, t("confirmDelete")), (0, react.createElement)("button", {
|
|
2892
|
+
className: "dsc-btn",
|
|
2893
|
+
onClick: () => setConfirming(null)
|
|
2894
|
+
}, t("cancel")))) : null, renaming !== null ? (0, react.createElement)("div", { className: "dsc-skill-create" }, (0, react.createElement)("input", {
|
|
2895
|
+
className: "dsc-input dsc-input--sm",
|
|
2896
|
+
value: renaming.to,
|
|
2897
|
+
onChange: (event) => setRenaming({
|
|
2898
|
+
...renaming,
|
|
2899
|
+
to: event.target.value
|
|
2900
|
+
})
|
|
2901
|
+
}), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2902
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2903
|
+
disabled: busy,
|
|
2904
|
+
onClick: () => void applyRename()
|
|
2905
|
+
}, t("skillRename")), (0, react.createElement)("button", {
|
|
2906
|
+
className: "dsc-btn dsc-btn--sm",
|
|
2907
|
+
onClick: () => setRenaming(null)
|
|
2908
|
+
}, t("cancel")))) : null);
|
|
2909
|
+
return (0, react.createElement)("div", null, head, (0, react.createElement)("div", { className: "dsc-row dsc-skill-flags" }, (0, react.createElement)("label", { className: "dsc-check" }, (0, react.createElement)("input", {
|
|
2910
|
+
type: "checkbox",
|
|
2911
|
+
checked: pack?.required === true,
|
|
2912
|
+
disabled: busy,
|
|
2913
|
+
onChange: (event) => void mutate({
|
|
2914
|
+
action: "require",
|
|
2915
|
+
required: event.target.checked
|
|
2916
|
+
})
|
|
2917
|
+
}), t("skillRequired"))), pack !== null ? (0, react.createElement)("div", {
|
|
2918
|
+
className: "dsc-skill-dir",
|
|
2919
|
+
title: pack.dir
|
|
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", {
|
|
2921
|
+
className: "dsc-btn",
|
|
2922
|
+
disabled: busy,
|
|
2923
|
+
onClick: () => {
|
|
2924
|
+
const target = pendingOpen;
|
|
2925
|
+
setPendingOpen(null);
|
|
2926
|
+
saveFile().then((ok) => {
|
|
2927
|
+
if (ok) openFile(target);
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
}, t("skillSaveAndSwitch")), (0, react.createElement)("button", {
|
|
2931
|
+
className: "dsc-btn",
|
|
2932
|
+
onClick: () => {
|
|
2933
|
+
const target = pendingOpen;
|
|
2934
|
+
setPendingOpen(null);
|
|
2935
|
+
setDirty(false);
|
|
2936
|
+
openFile(target);
|
|
2937
|
+
}
|
|
2938
|
+
}, t("skillDiscard")), (0, react.createElement)("button", {
|
|
2939
|
+
className: "dsc-btn",
|
|
2940
|
+
onClick: () => setPendingOpen(null)
|
|
2941
|
+
}, t("cancel")))) : null, notice !== null ? (0, react.createElement)("div", { className: "dsc-ok" }, notice) : null, error !== null ? (0, react.createElement)(ErrorNote, {
|
|
2942
|
+
t,
|
|
2943
|
+
message: error
|
|
2944
|
+
}) : null, (0, react.createElement)("div", { className: "dsc-skill" }, fileList, editor), (0, react.createElement)("div", { className: "dsc-skill-footer" }, (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2945
|
+
className: "dsc-btn",
|
|
2946
|
+
disabled: busy,
|
|
2947
|
+
onClick: () => void mutate({ action: "disable" }).then(() => props.onBack())
|
|
2948
|
+
}, t("skillDetach")), (0, react.createElement)("button", {
|
|
2949
|
+
className: "dsc-btn dsc-btn--danger",
|
|
2950
|
+
disabled: busy,
|
|
2951
|
+
onClick: () => setConfirming("*pack*")
|
|
2952
|
+
}, t("skillDestroy"))), confirming === "*pack*" ? (0, react.createElement)("div", { className: "dsc-dialog dsc-dialog--danger" }, (0, react.createElement)("div", { className: "dsc-dialog-head dsc-danger-text" }, t("skillDestroyConfirm", { count: pack?.files.length ?? 0 })), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
2953
|
+
className: "dsc-btn dsc-btn--danger",
|
|
2954
|
+
disabled: busy,
|
|
2955
|
+
onClick: () => {
|
|
2956
|
+
setConfirming(null);
|
|
2957
|
+
mutate({ action: "destroy" }).then(() => props.onBack());
|
|
2958
|
+
}
|
|
2959
|
+
}, t("skillDestroyConfirmButton")), (0, react.createElement)("button", {
|
|
2960
|
+
className: "dsc-btn",
|
|
2961
|
+
onClick: () => setConfirming(null)
|
|
2962
|
+
}, t("cancel")))) : null));
|
|
2963
|
+
}
|
|
2262
2964
|
function WorkflowsTab({ t }) {
|
|
2263
2965
|
const [list, setList] = (0, react.useState)(null);
|
|
2264
2966
|
const [comfyui, setComfyui] = (0, react.useState)(null);
|
|
@@ -2270,6 +2972,9 @@ window.__ModuleLoader__.load({
|
|
|
2270
2972
|
const [viewing, setViewing] = (0, react.useState)(null);
|
|
2271
2973
|
const [extract, setExtract] = (0, react.useState)(null);
|
|
2272
2974
|
const [tagFilter, setTagFilter] = (0, react.useState)(null);
|
|
2975
|
+
const [skillFor, setSkillFor] = (0, react.useState)(null);
|
|
2976
|
+
/** A delete waiting on the user's answer about the workflow's skill pack. */
|
|
2977
|
+
const [deleting, setDeleting] = (0, react.useState)(null);
|
|
2273
2978
|
const load = async () => {
|
|
2274
2979
|
try {
|
|
2275
2980
|
const data = await getJson("/comfyui/workflows");
|
|
@@ -2360,11 +3065,16 @@ window.__ModuleLoader__.load({
|
|
|
2360
3065
|
setBusy(false);
|
|
2361
3066
|
}
|
|
2362
3067
|
};
|
|
2363
|
-
|
|
3068
|
+
/** Delete a workflow. A workflow carrying a skill pack asks first: the pack
|
|
3069
|
+
* holds hand-written notes that nothing else can restore. */
|
|
3070
|
+
const remove = async (id, deleteSkill = false) => {
|
|
2364
3071
|
setBusy(true);
|
|
2365
3072
|
setError(null);
|
|
2366
3073
|
try {
|
|
2367
|
-
await postJson("/comfyui/workflows/delete", {
|
|
3074
|
+
await postJson("/comfyui/workflows/delete", {
|
|
3075
|
+
id,
|
|
3076
|
+
deleteSkill
|
|
3077
|
+
});
|
|
2368
3078
|
await load();
|
|
2369
3079
|
} catch (cause) {
|
|
2370
3080
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -2372,6 +3082,13 @@ window.__ModuleLoader__.load({
|
|
|
2372
3082
|
setBusy(false);
|
|
2373
3083
|
}
|
|
2374
3084
|
};
|
|
3085
|
+
const requestRemove = (workflow) => {
|
|
3086
|
+
if (workflow.skillDir !== void 0) {
|
|
3087
|
+
setDeleting(workflow);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
remove(workflow.id);
|
|
3091
|
+
};
|
|
2375
3092
|
const openExtract = (file) => {
|
|
2376
3093
|
setError(null);
|
|
2377
3094
|
setExtract({
|
|
@@ -2433,6 +3150,17 @@ window.__ModuleLoader__.load({
|
|
|
2433
3150
|
setBusy(false);
|
|
2434
3151
|
}
|
|
2435
3152
|
};
|
|
3153
|
+
if (skillFor !== null) return (0, react.createElement)(SkillPackEditor, {
|
|
3154
|
+
t,
|
|
3155
|
+
workflow: skillFor,
|
|
3156
|
+
onBack: () => {
|
|
3157
|
+
setSkillFor(null);
|
|
3158
|
+
load();
|
|
3159
|
+
},
|
|
3160
|
+
onChanged: () => {
|
|
3161
|
+
load();
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
2436
3164
|
if (viewing !== null) return (0, react.createElement)(WorkflowView, {
|
|
2437
3165
|
t,
|
|
2438
3166
|
file: viewing.file,
|
|
@@ -2551,14 +3279,15 @@ window.__ModuleLoader__.load({
|
|
|
2551
3279
|
const apiFold = (0, react.createElement)("details", {
|
|
2552
3280
|
className: "dsc-fold",
|
|
2553
3281
|
open: true
|
|
2554
|
-
}, (0, react.createElement)("summary", null, `${t("wfLibrarySection")} (${visibleList.length})`), (0, react.createElement)("div", { className: "dsc-fold-body" }, visibleTags.length > 0 || tagFilter !== null ? (0, react.createElement)("div", { className: "dsc-tag-filter" }, (0, react.createElement)("
|
|
2555
|
-
className:
|
|
2556
|
-
|
|
2557
|
-
|
|
3282
|
+
}, (0, react.createElement)("summary", null, `${t("wfLibrarySection")} (${visibleList.length})`), (0, react.createElement)("div", { className: "dsc-fold-body" }, visibleTags.length > 0 || tagFilter !== null ? (0, react.createElement)("div", { className: "dsc-tag-filter" }, (0, react.createElement)("select", {
|
|
3283
|
+
className: "dsc-input dsc-input--inline",
|
|
3284
|
+
title: t("wfTags"),
|
|
3285
|
+
value: tagFilter ?? "",
|
|
3286
|
+
onChange: (event) => setTagFilter(event.target.value === "" ? null : event.target.value)
|
|
3287
|
+
}, (0, react.createElement)("option", { value: "" }, `${t("wfTagAll")} (${list.length})`), visibleTags.map((tag) => (0, react.createElement)("option", {
|
|
2558
3288
|
key: tag,
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
}, `${tag} (${tagCounts.get(tag) ?? 0})`))) : null, visibleList.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("wfEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, visibleList.map((workflow) => (0, react.createElement)("div", {
|
|
3289
|
+
value: tag
|
|
3290
|
+
}, `${tag} (${tagCounts.get(tag) ?? 0})`)))) : null, visibleList.length === 0 ? (0, react.createElement)("div", { className: "dsc-meta" }, t("wfEmpty")) : (0, react.createElement)("div", { className: "dsc-list" }, visibleList.map((workflow) => (0, react.createElement)("div", {
|
|
2562
3291
|
key: workflow.id,
|
|
2563
3292
|
className: "dsc-wf",
|
|
2564
3293
|
title: t("wfNameCopyHint"),
|
|
@@ -2573,7 +3302,10 @@ window.__ModuleLoader__.load({
|
|
|
2573
3302
|
}, workflow.name), (workflow.tags ?? []).length > 0 ? (0, react.createElement)("div", { className: "dsc-wf-tags" }, (workflow.tags ?? []).map((tag) => (0, react.createElement)("span", {
|
|
2574
3303
|
key: tag,
|
|
2575
3304
|
className: "dsc-tag-chip dsc-tag-chip--mini"
|
|
2576
|
-
}, tag))) : null
|
|
3305
|
+
}, tag))) : null, workflow.skillDir !== void 0 ? (0, react.createElement)("span", {
|
|
3306
|
+
className: "dsc-badge dsc-badge--ok",
|
|
3307
|
+
title: t("skillBadgeHint")
|
|
3308
|
+
}, workflow.requireSkill === true ? t("skillBadgeRequired") : t("skillBadge")) : null), workflow.description !== "" ? (0, react.createElement)("div", { className: "dsc-wf-desc" }, workflow.description) : null, (0, react.createElement)("div", { className: "dsc-wf-updated" }, `${t("wfUpdated")} ${formatTs(workflow.updatedAt)}`), (0, react.createElement)("div", { className: "dsc-wf-actions" }, (0, react.createElement)("button", {
|
|
2577
3309
|
className: "dsc-btn",
|
|
2578
3310
|
disabled: busy,
|
|
2579
3311
|
onClick: (event) => {
|
|
@@ -2598,9 +3330,37 @@ window.__ModuleLoader__.load({
|
|
|
2598
3330
|
disabled: busy,
|
|
2599
3331
|
onClick: (event) => {
|
|
2600
3332
|
event.stopPropagation();
|
|
2601
|
-
|
|
3333
|
+
setSkillFor(workflow);
|
|
3334
|
+
setError(null);
|
|
3335
|
+
}
|
|
3336
|
+
}, t("skillButton")), (0, react.createElement)("button", {
|
|
3337
|
+
className: "dsc-btn",
|
|
3338
|
+
disabled: busy,
|
|
3339
|
+
onClick: (event) => {
|
|
3340
|
+
event.stopPropagation();
|
|
3341
|
+
requestRemove(workflow);
|
|
3342
|
+
}
|
|
3343
|
+
}, t("wfDelete"))), deleting?.id === workflow.id ? (0, react.createElement)("div", {
|
|
3344
|
+
className: "dsc-dialog dsc-dialog--danger",
|
|
3345
|
+
onClick: (event) => event.stopPropagation()
|
|
3346
|
+
}, (0, react.createElement)("div", { className: "dsc-dialog-head dsc-danger-text" }, t("skillDeleteWithWorkflow")), (0, react.createElement)("div", { className: "dsc-row" }, (0, react.createElement)("button", {
|
|
3347
|
+
className: "dsc-btn dsc-btn--danger",
|
|
3348
|
+
disabled: busy,
|
|
3349
|
+
onClick: () => {
|
|
3350
|
+
setDeleting(null);
|
|
3351
|
+
remove(workflow.id, true);
|
|
2602
3352
|
}
|
|
2603
|
-
}, t("
|
|
3353
|
+
}, t("skillDeleteBoth")), (0, react.createElement)("button", {
|
|
3354
|
+
className: "dsc-btn",
|
|
3355
|
+
disabled: busy,
|
|
3356
|
+
onClick: () => {
|
|
3357
|
+
setDeleting(null);
|
|
3358
|
+
remove(workflow.id, false);
|
|
3359
|
+
}
|
|
3360
|
+
}, t("skillKeepPack")), (0, react.createElement)("button", {
|
|
3361
|
+
className: "dsc-btn",
|
|
3362
|
+
onClick: () => setDeleting(null)
|
|
3363
|
+
}, t("cancel")))) : null)))));
|
|
2604
3364
|
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "dsc-toolbar" }, (0, react.createElement)("button", {
|
|
2605
3365
|
className: "dsc-btn",
|
|
2606
3366
|
disabled: busy,
|
|
@@ -3262,6 +4022,9 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
3262
4022
|
}
|
|
3263
4023
|
.dsc-panel-resize:hover { opacity: 1; }
|
|
3264
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); }
|
|
3265
4028
|
.dsc-panel-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 14px; padding: 4px 8px; }
|
|
3266
4029
|
.dsc-panel-close:hover { color: var(--dsw-alias-label-primary); }
|
|
3267
4030
|
.dsc-tabs { display: flex; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
@@ -3307,6 +4070,40 @@ select.dsc-input option { background: var(--dsw-alias-bg-layer-1); color: var(--
|
|
|
3307
4070
|
.dsc-derived-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }
|
|
3308
4071
|
.dsc-derived-name { color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3309
4072
|
.dsc-dialog { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 10px; }
|
|
4073
|
+
/* --- skill pack editor: file list beside a plain-text editor --- */
|
|
4074
|
+
.dsc-skill { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; margin-top: 8px; }
|
|
4075
|
+
.dsc-skill-files { flex: 0 0 190px; min-width: 160px; display: flex; flex-direction: column; gap: 6px; }
|
|
4076
|
+
.dsc-skill-main { flex: 1 1 300px; min-width: 260px; display: flex; flex-direction: column; gap: 6px; }
|
|
4077
|
+
.dsc-skill-bucket { display: flex; flex-direction: column; gap: 2px; }
|
|
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; }
|
|
4084
|
+
.dsc-skill-file {
|
|
4085
|
+
display: flex; align-items: center; justify-content: space-between; gap: 6px;
|
|
4086
|
+
border: 1px solid transparent; border-radius: 6px; background: transparent;
|
|
4087
|
+
color: inherit; font-size: 12px; padding: 4px 6px; cursor: pointer; text-align: left;
|
|
4088
|
+
}
|
|
4089
|
+
.dsc-skill-file:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
4090
|
+
.dsc-skill-file--active { border-color: var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); }
|
|
4091
|
+
.dsc-skill-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
4092
|
+
.dsc-skill-file-size { color: var(--dsw-alias-label-secondary); font-size: 11px; flex: none; }
|
|
4093
|
+
.dsc-skill-create { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
|
|
4094
|
+
.dsc-skill-editor-head { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; }
|
|
4095
|
+
.dsc-skill-path { color: var(--dsw-alias-label-primary); }
|
|
4096
|
+
.dsc-textarea--skill { min-height: 260px; font-size: 12px; line-height: 1.5; }
|
|
4097
|
+
.dsc-skill-preview { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 10px; display: flex; flex-direction: column; gap: 6px; align-items: center; }
|
|
4098
|
+
.dsc-skill-preview img { max-width: 100%; max-height: 320px; object-fit: contain; border-radius: 6px; }
|
|
4099
|
+
.dsc-skill-dir { font-size: 11px; color: var(--dsw-alias-label-secondary); margin: 4px 0; word-break: break-all; }
|
|
4100
|
+
.dsc-skill-flags { margin-top: 6px; }
|
|
4101
|
+
.dsc-skill-footer { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
4102
|
+
.dsc-skill-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
4103
|
+
.dsc-skill-drop-hint { font-size: 11px; color: var(--dsw-alias-label-secondary); border: 1px dashed var(--dsw-alias-border-l1); border-radius: 6px; padding: 4px 6px; text-align: center; }
|
|
4104
|
+
.dsc-dialog--danger { border-color: var(--dsw-alias-state-error-primary); }
|
|
4105
|
+
.dsc-danger-text { color: var(--dsw-alias-state-error-primary); font-weight: 600; font-size: 12px; line-height: 1.5; }
|
|
4106
|
+
.dsc-check { display: flex; align-items: center; gap: 6px; font-size: 12px; }
|
|
3310
4107
|
.dsc-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
3311
4108
|
.dsc-extract-modes { display: flex; flex-direction: column; gap: 8px; margin: 10px 0; }
|
|
3312
4109
|
.dsc-mode { display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; cursor: pointer; }
|
|
@@ -3435,7 +4232,7 @@ video.dsc-picker-player { max-height: 180px; }
|
|
|
3435
4232
|
.dsc-picker-name { font-size: 13px; line-height: 1.5; padding: 2px 0; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3436
4233
|
.dsc-picker-card:hover .dsc-picker-name { color: var(--dsw-alias-brand-primary); }
|
|
3437
4234
|
/* --- workflow tags: filter bar, chips, editor --- */
|
|
3438
|
-
.dsc-tag-filter { display: flex;
|
|
4235
|
+
.dsc-tag-filter { display: flex; justify-content: flex-end; padding: 6px 0 2px; }
|
|
3439
4236
|
.dsc-tag-chip { border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; padding: 2px 10px; font-size: 12px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-primary); }
|
|
3440
4237
|
.dsc-tag-chip--active { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-layer-1); }
|
|
3441
4238
|
.dsc-tag-chip--mini { font-size: 11px; padding: 1px 8px; cursor: default; border: 1px solid var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); }
|
|
@@ -3452,6 +4249,7 @@ video.dsc-picker-player { max-height: 180px; }
|
|
|
3452
4249
|
.dsc-queue-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
3453
4250
|
.dsc-btn { border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 2px 8px; font-size: 12px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-primary); }
|
|
3454
4251
|
.dsc-btn--sm { padding: 1px 6px; font-size: 11px; }
|
|
4252
|
+
.dsc-input--sm { padding: 3px 6px; font-size: 12px; }
|
|
3455
4253
|
.dsc-btn:disabled { opacity: 0.5; cursor: default; }
|
|
3456
4254
|
.dsc-job-item { display: flex; align-items: center; gap: 10px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 7px 10px; background: var(--dsw-alias-bg-layer-1); transition: background 0.12s, border-color 0.12s; }
|
|
3457
4255
|
.dsc-job-item:hover { background: var(--dsw-alias-bg-layer-2); border-color: var(--dsw-alias-border-l2); }
|