@loom-framework/core 0.1.0-alpha.146 → 0.1.0-alpha.148

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.
Files changed (36) hide show
  1. package/builtin-skills/loom/SKILL.md +42 -24
  2. package/builtin-skills/loom/references/README.md +102 -28
  3. package/dist/cli/commands/dev.d.ts +0 -1
  4. package/dist/cli/commands/dev.d.ts.map +1 -1
  5. package/dist/cli/commands/dev.js +1 -19
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/generate-page.d.ts +1 -1
  8. package/dist/cli/commands/generate-page.d.ts.map +1 -1
  9. package/dist/cli/commands/generate-page.js +4 -13
  10. package/dist/cli/commands/generate-page.js.map +1 -1
  11. package/dist/cli/commands/generate-system-settings.d.ts +9 -0
  12. package/dist/cli/commands/generate-system-settings.d.ts.map +1 -0
  13. package/dist/cli/commands/generate-system-settings.js +88 -0
  14. package/dist/cli/commands/generate-system-settings.js.map +1 -0
  15. package/dist/cli/commands/generate.d.ts.map +1 -1
  16. package/dist/cli/commands/generate.js +2 -0
  17. package/dist/cli/commands/generate.js.map +1 -1
  18. package/dist/cli/commands/init.d.ts.map +1 -1
  19. package/dist/cli/commands/init.js +21 -0
  20. package/dist/cli/commands/init.js.map +1 -1
  21. package/dist/cli/helpers/app-tsx-wiring.d.ts +8 -4
  22. package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -1
  23. package/dist/cli/helpers/app-tsx-wiring.js +232 -136
  24. package/dist/cli/helpers/app-tsx-wiring.js.map +1 -1
  25. package/dist/cli/templates/frontend-entry.d.ts.map +1 -1
  26. package/dist/cli/templates/frontend-entry.js +0 -3
  27. package/dist/cli/templates/frontend-entry.js.map +1 -1
  28. package/dist/cli/templates/index.d.ts +1 -0
  29. package/dist/cli/templates/index.d.ts.map +1 -1
  30. package/dist/cli/templates/index.js +1 -0
  31. package/dist/cli/templates/index.js.map +1 -1
  32. package/dist/cli/templates/system-settings-page.d.ts +10 -0
  33. package/dist/cli/templates/system-settings-page.d.ts.map +1 -0
  34. package/dist/cli/templates/system-settings-page.js +768 -0
  35. package/dist/cli/templates/system-settings-page.js.map +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,768 @@
1
+ /**
2
+ * System settings page templates
3
+ *
4
+ * Generates ModelManagement and SkillManagement pages as local source files.
5
+ * Pages import sub-components from @loom-framework/frontend-antd and
6
+ * register their own i18n keys via registerMessages().
7
+ */
8
+ // ── Model Management ──
9
+ export function modelManagementPageTemplate() {
10
+ return `import React, { useState, useEffect, useCallback } from 'react';
11
+ import { Card, Table, Button, Modal, Form, Input, Switch, Tag, Space, Typography, message, theme, Breadcrumb, Flex } from 'antd';
12
+ import { PlusOutlined, EditOutlined, DeleteOutlined, RobotOutlined, HomeOutlined, AppstoreAddOutlined } from '@ant-design/icons';
13
+ import { useLocale, useAppShell, registerMessages } from '@loom-framework/frontend-antd';
14
+
15
+ registerMessages('zh-CN', {
16
+ 'model.title': '模型管理',
17
+ 'model.addModel': '添加模型',
18
+ 'model.addEngine': '添加AI引擎',
19
+ 'model.editModel': '编辑模型',
20
+ 'model.modelName': '模型名',
21
+ 'model.authToken': '认证令牌',
22
+ 'model.baseUrl': 'API 地址',
23
+ 'model.setDefault': '设为默认',
24
+ 'model.default': '默认',
25
+ 'model.engine': 'AI 引擎',
26
+ 'model.configured': '已配置',
27
+ 'model.notSet': '未设置',
28
+ 'model.deleteConfirm': '确认删除模型 "{name}"?',
29
+ 'model.deleteSuccess': '模型已删除',
30
+ 'model.addSuccess': '模型已添加',
31
+ 'model.updateSuccess': '模型已更新',
32
+ 'model.saveFailed': '保存模型失败',
33
+ 'model.loadFailed': '加载模型失败',
34
+ 'model.nameRequired': '请输入模型名称',
35
+ 'model.baseUrlRequired': '请输入 API 地址',
36
+ 'model.tokenMasked': '留空保持当前值不变',
37
+ 'model.enterToken': '输入认证令牌',
38
+ });
39
+
40
+ registerMessages('en-US', {
41
+ 'model.title': 'Model Management',
42
+ 'model.addModel': 'Add Model',
43
+ 'model.addEngine': 'Add AI Engine',
44
+ 'model.editModel': 'Edit Model',
45
+ 'model.modelName': 'Model Name',
46
+ 'model.authToken': 'Auth Token',
47
+ 'model.baseUrl': 'API Base URL',
48
+ 'model.setDefault': 'Set as Default',
49
+ 'model.default': 'Default',
50
+ 'model.engine': 'AI Engine',
51
+ 'model.configured': 'Configured',
52
+ 'model.notSet': 'Not Set',
53
+ 'model.deleteConfirm': 'Delete model "{name}"?',
54
+ 'model.deleteSuccess': 'Model deleted',
55
+ 'model.addSuccess': 'Model added',
56
+ 'model.updateSuccess': 'Model updated',
57
+ 'model.saveFailed': 'Failed to save model',
58
+ 'model.loadFailed': 'Failed to load models',
59
+ 'model.nameRequired': 'Model name is required',
60
+ 'model.baseUrlRequired': 'Base URL is required',
61
+ 'model.tokenMasked': 'Leave empty to keep current',
62
+ 'model.enterToken': 'Enter auth token',
63
+ });
64
+
65
+ interface ModelItem {
66
+ name: string;
67
+ authToken?: string;
68
+ hasAuthToken?: boolean;
69
+ baseUrl?: string;
70
+ isDefault: boolean;
71
+ }
72
+
73
+ const API = '/api/v1/ai/models';
74
+
75
+ export default function ModelManagementPage(): React.ReactElement {
76
+ const { token } = theme.useToken();
77
+ const { t } = useLocale();
78
+ const { breadcrumbs, onNavClick } = useAppShell();
79
+ const [models, setModels] = useState<ModelItem[]>([]);
80
+ const [loading, setLoading] = useState(false);
81
+ const [modalOpen, setModalOpen] = useState(false);
82
+ const [editingModel, setEditingModel] = useState<ModelItem | null>(null);
83
+ const [form] = Form.useForm();
84
+
85
+ const fetchModels = useCallback(async () => {
86
+ setLoading(true);
87
+ try {
88
+ const res = await fetch(API);
89
+ const data = await res.json();
90
+ setModels(Array.isArray(data) ? data : []);
91
+ } catch {
92
+ message.error(t('model.loadFailed') || 'Failed to load models');
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ }, []);
97
+
98
+ useEffect(() => { fetchModels(); }, [fetchModels]);
99
+
100
+ const handleSave = async () => {
101
+ try {
102
+ const values = await form.validateFields();
103
+ let updatedModels = models.map(m => ({ ...m, isDefault: false }));
104
+
105
+ if (editingModel) {
106
+ updatedModels = updatedModels.map(m =>
107
+ m.name === editingModel.name ? { ...m, ...values, hasAuthToken: values.authToken ? true : m.hasAuthToken } : m
108
+ );
109
+ } else {
110
+ updatedModels.push({ ...values, hasAuthToken: !!values.authToken });
111
+ }
112
+
113
+ if (values.isDefault) {
114
+ updatedModels = updatedModels.map(m => ({ ...m, isDefault: m.name === values.name }));
115
+ } else if (!updatedModels.some(m => m.isDefault)) {
116
+ updatedModels[0].isDefault = true;
117
+ }
118
+
119
+ const res = await fetch(API, {
120
+ method: 'PUT',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({ models: updatedModels }),
123
+ });
124
+
125
+ if (res.ok) {
126
+ const data = await res.json();
127
+ setModels(Array.isArray(data) ? data : []);
128
+ message.success(editingModel ? (t('model.updateSuccess') || 'Model updated') : (t('model.addSuccess') || 'Model added'));
129
+ } else {
130
+ message.error(t('model.saveFailed') || 'Failed to save model');
131
+ }
132
+
133
+ setModalOpen(false);
134
+ setEditingModel(null);
135
+ form.resetFields();
136
+ } catch { /* validation error */ }
137
+ };
138
+
139
+ const handleDelete = async (name: string) => {
140
+ Modal.confirm({
141
+ title: t('model.deleteConfirm')?.replace('{name}', name) || \`Delete model "\${name}"?\`,
142
+ onOk: async () => {
143
+ const updatedModels = models.filter(m => m.name !== name);
144
+ if (updatedModels.length > 0 && !updatedModels.some(m => m.isDefault)) {
145
+ updatedModels[0].isDefault = true;
146
+ }
147
+
148
+ const res = await fetch(API, {
149
+ method: 'PUT',
150
+ headers: { 'Content-Type': 'application/json' },
151
+ body: JSON.stringify({ models: updatedModels }),
152
+ });
153
+
154
+ if (res.ok) {
155
+ const data = await res.json();
156
+ setModels(Array.isArray(data) ? data : []);
157
+ message.success(t('model.deleteSuccess') || 'Model deleted');
158
+ }
159
+ },
160
+ });
161
+ };
162
+
163
+ const openEditModal = (model: ModelItem) => {
164
+ setEditingModel(model);
165
+ form.setFieldsValue({
166
+ name: model.name,
167
+ authToken: model.authToken,
168
+ baseUrl: model.baseUrl,
169
+ isDefault: model.isDefault,
170
+ });
171
+ setModalOpen(true);
172
+ };
173
+
174
+ const openAddModal = () => {
175
+ setEditingModel(null);
176
+ form.resetFields();
177
+ form.setFieldsValue({ isDefault: models.length === 0 });
178
+ setModalOpen(true);
179
+ };
180
+
181
+ const columns = [
182
+ {
183
+ title: t('model.modelName') || 'Model Name',
184
+ dataIndex: 'name',
185
+ key: 'name',
186
+ render: (name: string, record: ModelItem) => (
187
+ <Space>
188
+ <span style={{ fontWeight: token.fontWeightStrong }}>{name}</span>
189
+ {record.isDefault && <Tag color="blue">{t('model.default') || 'Default'}</Tag>}
190
+ </Space>
191
+ ),
192
+ },
193
+ {
194
+ title: t('model.baseUrl') || 'API Base URL',
195
+ dataIndex: 'baseUrl',
196
+ key: 'baseUrl',
197
+ render: (url: string) => url ? <Typography.Text copyable={{ text: url }} style={{ fontSize: token.fontSizeSM }}>{url}</Typography.Text> : <Typography.Text type="secondary">—</Typography.Text>,
198
+ },
199
+ {
200
+ title: t('model.authToken') || 'Auth Token',
201
+ dataIndex: 'hasAuthToken',
202
+ key: 'hasAuthToken',
203
+ width: 120,
204
+ render: (has: boolean) => has ? <Tag color="green">{t('model.configured') || 'Configured'}</Tag> : <Tag>{t('model.notSet') || 'Not Set'}</Tag>,
205
+ },
206
+ {
207
+ title: t('common.action') || 'Action',
208
+ key: 'action',
209
+ width: 120,
210
+ render: (_: unknown, record: ModelItem) => (
211
+ <Space size={0}>
212
+ <Button type="text" size="small" icon={<EditOutlined />} onClick={() => openEditModal(record)} />
213
+ {!record.isDefault && (
214
+ <Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.name)} />
215
+ )}
216
+ </Space>
217
+ ),
218
+ },
219
+ ];
220
+
221
+ return (
222
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
223
+ <Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
224
+ <Breadcrumb items={[{ title: <HomeOutlined onClick={() => onNavClick?.('')} style={{ cursor: 'pointer' }} /> }, ...(breadcrumbs || []).map(b => ({ title: b.path ? <a onClick={() => onNavClick?.(b.path!)}>{b.title}</a> : b.title }))]} />
225
+ <Button type="primary" icon={<AppstoreAddOutlined />} disabled>
226
+ {t('model.addEngine') || 'Add AI Engine'}
227
+ </Button>
228
+ </Flex>
229
+ <Card
230
+ title={
231
+ <Space>
232
+ <RobotOutlined />
233
+ <span>Claude Code</span>
234
+ <Typography.Text type="secondary" style={{ fontWeight: 'normal' }}>
235
+ {t('model.engine') || 'AI Engine'}
236
+ </Typography.Text>
237
+ </Space>
238
+ }
239
+ extra={
240
+ <Button type="primary" size="small" icon={<PlusOutlined />} onClick={openAddModal}>
241
+ {t('model.addModel') || 'Add Model'}
242
+ </Button>
243
+ }
244
+ >
245
+ <Table
246
+ dataSource={models}
247
+ columns={columns}
248
+ rowKey="name"
249
+ loading={loading}
250
+ pagination={false}
251
+ size="small"
252
+ />
253
+ </Card>
254
+
255
+ <Modal
256
+ title={editingModel ? (t('model.editModel') || 'Edit Model') : (t('model.addModel') || 'Add Model')}
257
+ open={modalOpen}
258
+ onOk={handleSave}
259
+ onCancel={() => { setModalOpen(false); setEditingModel(null); form.resetFields(); }}
260
+ okText={t('common.save') || 'Save'}
261
+ cancelText={t('common.cancel') || 'Cancel'}
262
+ destroyOnClose
263
+ >
264
+ <Form form={form} layout="vertical" style={{ marginTop: token.marginMD }}>
265
+ <Form.Item
266
+ name="name"
267
+ label={t('model.modelName') || 'Model Name'}
268
+ rules={[{ required: true, message: t('model.nameRequired') || 'Model name is required' }]}
269
+ >
270
+ <Input disabled={!!editingModel} placeholder="e.g. GLM-5.1, Kimi-K2.5" />
271
+ </Form.Item>
272
+ <Form.Item
273
+ name="baseUrl"
274
+ label={t('model.baseUrl') || 'API Base URL'}
275
+ rules={[{ required: true, message: t('model.baseUrlRequired') || 'Base URL is required' }]}
276
+ >
277
+ <Input placeholder="https://api.example.com" />
278
+ </Form.Item>
279
+ <Form.Item
280
+ name="authToken"
281
+ label={t('model.authToken') || 'Auth Token'}
282
+ >
283
+ <Input.Password placeholder={editingModel?.hasAuthToken ? t('model.tokenMasked') || 'Leave empty to keep current' : t('model.enterToken') || 'Enter auth token'} />
284
+ </Form.Item>
285
+ <Form.Item
286
+ name="isDefault"
287
+ label={t('model.setDefault') || 'Set as Default'}
288
+ valuePropName="checked"
289
+ >
290
+ <Switch />
291
+ </Form.Item>
292
+ </Form>
293
+ </Modal>
294
+ </div>
295
+ );
296
+ }
297
+ `;
298
+ }
299
+ // ── Skill Management ──
300
+ export function skillManagementPageTemplate() {
301
+ return `import React, { useState, useEffect, useCallback, useMemo } from 'react';
302
+ import {
303
+ Card,
304
+ Tree,
305
+ Button,
306
+ Modal,
307
+ Spin,
308
+ Dropdown,
309
+ Segmented,
310
+ Flex,
311
+ Breadcrumb,
312
+ Space,
313
+ theme,
314
+ } from 'antd';
315
+ import {
316
+ FolderOutlined,
317
+ ReloadOutlined,
318
+ UploadOutlined,
319
+ DownloadOutlined,
320
+ DeleteOutlined,
321
+ MoreOutlined,
322
+ EyeOutlined,
323
+ CodeOutlined,
324
+ HomeOutlined,
325
+ } from '@ant-design/icons';
326
+ import {
327
+ useLocale, useLoomTheme, useAppShell, registerMessages,
328
+ SkillFileViewer, SkillUploadModal,
329
+ fetchSkillsList, fetchSkillDetail, fetchSkillFile, deleteSkill, downloadSkill,
330
+ getFileIcon,
331
+ } from '@loom-framework/frontend-antd';
332
+ import type { SkillInfo, SkillDetail, FileContent, SkillTreeNode } from '@loom-framework/frontend-antd';
333
+
334
+ registerMessages('zh-CN', {
335
+ 'skill.title': '技能管理',
336
+ 'skill.upload': '上传技能',
337
+ 'skill.delete': '删除',
338
+ 'skill.detail': '详情',
339
+ 'skill.files': '文件',
340
+ 'skill.content': '内容',
341
+ 'skill.noSelection': '请选择一个 Skill 查看详情',
342
+ 'skill.uploadArchive': '上传压缩包',
343
+ 'skill.uploadFile': '上传文件',
344
+ 'skill.refresh': '刷新',
345
+ 'skill.download': '下载',
346
+ 'skill.more': '更多',
347
+ 'skill.confirmDelete': '确认删除',
348
+ 'skill.confirmDeleteContent': '确定要删除 Skill "{name}" 吗?',
349
+ 'skill.deleteSuccess': 'Skill "{name}" 已删除',
350
+ 'skill.deleteFailed': '删除失败',
351
+ 'skill.loadFailed': '加载 Skill 列表失败',
352
+ 'skill.loadFileFailed': '加载文件内容失败',
353
+ 'skill.createSuccess': 'Skill 创建成功',
354
+ 'skill.parseSuccess': '压缩包解析成功',
355
+ 'skill.parseFailed': '解析压缩包失败',
356
+ 'skill.uploadFirst': '请先上传压缩包',
357
+ 'skill.downloadFailed': '下载失败',
358
+ 'skill.skillDetail': 'Skill 详情',
359
+ 'skill.contentPreview': '内容预览',
360
+ 'skill.selectToView': '选择左侧 Skill 或文件以查看详情',
361
+ 'skill.preview': '预览',
362
+ 'skill.source': '源码',
363
+ 'skill.uploadNew': '上传新 Skill',
364
+ 'skill.manualCreate': '手动创建',
365
+ 'skill.archiveUpload': '上传压缩包',
366
+ 'skill.skillName': 'Skill 名称',
367
+ 'skill.skillNameRequired': '请输入 Skill 名称',
368
+ 'skill.skillNamePattern': '只能包含字母、数字、中划线和下划线',
369
+ 'skill.skillContent': 'SKILL.md 内容',
370
+ 'skill.referencesFiles': 'References 文件',
371
+ 'skill.dragOrClick': '拖拽文件到此处,或点击选择文件',
372
+ 'skill.referencesHint': '文件将存入 references/ 目录,支持 .md, .json, .yaml 等文本文件',
373
+ 'skill.scriptsFiles': 'Scripts 文件',
374
+ 'skill.scriptsHint': '文件将存入 scripts/ 目录,支持 .sh, .py, .js 等脚本文件',
375
+ 'skill.archiveNameAuto': '从压缩包自动识别,可修改',
376
+ 'skill.uploadZip': '上传 ZIP 压缩包',
377
+ 'skill.dragZip': '拖拽 .zip 文件到此处,或点击选择',
378
+ 'skill.archiveHint': '压缩包必须包含 SKILL.md,支持任意子目录(references/、scripts/ 等)',
379
+ 'skill.parsing': '正在解析压缩包...',
380
+ 'skill.parseSuccessTitle': '解析成功',
381
+ 'skill.detectedName': '检测到 Skill 名称',
382
+ 'skill.identified': '已识别',
383
+ 'skill.fileCount': '{count} 个文件',
384
+ 'skill.fileCountLabel': '文件数量',
385
+ 'skill.fileStructure': '文件结构:',
386
+ });
387
+
388
+ registerMessages('en-US', {
389
+ 'skill.title': 'Skill Management',
390
+ 'skill.upload': 'Upload Skill',
391
+ 'skill.delete': 'Delete',
392
+ 'skill.detail': 'Detail',
393
+ 'skill.files': 'Files',
394
+ 'skill.content': 'Content',
395
+ 'skill.noSelection': 'Select a skill to view details',
396
+ 'skill.uploadArchive': 'Upload archive',
397
+ 'skill.uploadFile': 'Upload file',
398
+ 'skill.refresh': 'Refresh',
399
+ 'skill.download': 'Download',
400
+ 'skill.more': 'More',
401
+ 'skill.confirmDelete': 'Confirm Delete',
402
+ 'skill.confirmDeleteContent': 'Are you sure you want to delete Skill "{name}"?',
403
+ 'skill.deleteSuccess': 'Skill "{name}" deleted',
404
+ 'skill.deleteFailed': 'Delete failed',
405
+ 'skill.loadFailed': 'Failed to load skill list',
406
+ 'skill.loadFileFailed': 'Failed to load file content',
407
+ 'skill.createSuccess': 'Skill created successfully',
408
+ 'skill.parseSuccess': 'Archive parsed successfully',
409
+ 'skill.parseFailed': 'Failed to parse archive',
410
+ 'skill.uploadFirst': 'Please upload an archive first',
411
+ 'skill.downloadFailed': 'Download failed',
412
+ 'skill.skillDetail': 'Skill Details',
413
+ 'skill.contentPreview': 'Content Preview',
414
+ 'skill.selectToView': 'Select a skill or file to view details',
415
+ 'skill.preview': 'Preview',
416
+ 'skill.source': 'Source',
417
+ 'skill.uploadNew': 'Upload New Skill',
418
+ 'skill.manualCreate': 'Manual',
419
+ 'skill.archiveUpload': 'Archive',
420
+ 'skill.skillName': 'Skill Name',
421
+ 'skill.skillNameRequired': 'Please enter skill name',
422
+ 'skill.skillNamePattern': 'Only letters, numbers, hyphens and underscores',
423
+ 'skill.skillContent': 'SKILL.md Content',
424
+ 'skill.referencesFiles': 'References Files',
425
+ 'skill.dragOrClick': 'Drag files here or click to select',
426
+ 'skill.referencesHint': 'Files will be stored in references/ directory',
427
+ 'skill.scriptsFiles': 'Scripts Files',
428
+ 'skill.scriptsHint': 'Files will be stored in scripts/ directory',
429
+ 'skill.archiveNameAuto': 'Auto-detected from archive, editable',
430
+ 'skill.uploadZip': 'Upload ZIP Archive',
431
+ 'skill.dragZip': 'Drag .zip file here or click to select',
432
+ 'skill.archiveHint': 'Archive must contain SKILL.md, supports subdirectories',
433
+ 'skill.parsing': 'Parsing archive...',
434
+ 'skill.parseSuccessTitle': 'Parse Successful',
435
+ 'skill.detectedName': 'Detected Skill Name',
436
+ 'skill.identified': 'Identified',
437
+ 'skill.fileCount': '{count} files',
438
+ 'skill.fileCountLabel': 'File Count',
439
+ 'skill.fileStructure': 'File structure:',
440
+ });
441
+
442
+ export default function SkillManagementPage(): React.ReactElement {
443
+ const { mode } = useLoomTheme();
444
+ const { t } = useLocale();
445
+ const { breadcrumbs, onNavClick } = useAppShell();
446
+ const { token } = theme.useToken();
447
+
448
+ const [skills, setSkills] = useState<SkillInfo[]>([]);
449
+ const [treeData, setTreeData] = useState<SkillTreeNode[]>([]);
450
+ const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
451
+ const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
452
+ const [skillDetail, setSkillDetail] = useState<SkillDetail | null>(null);
453
+ const [fileContent, setFileContent] = useState<FileContent | null>(null);
454
+ const [loading, setLoading] = useState(false);
455
+ const [viewMode, setViewMode] = useState<'preview' | 'source'>('preview');
456
+ const [uploadModalOpen, setUploadModalOpen] = useState(false);
457
+
458
+ const buildTreeData = useCallback((skillList: SkillInfo[]): SkillTreeNode[] => {
459
+ return skillList.map((skill) => {
460
+ const children: SkillTreeNode[] = [
461
+ {
462
+ key: \`\${skill.name}/SKILL.md\`,
463
+ title: 'SKILL.md',
464
+ icon: <FolderOutlined style={{ color: token.colorPrimary }} />,
465
+ isLeaf: true,
466
+ isFile: true,
467
+ filePath: 'SKILL.md',
468
+ skillName: skill.name,
469
+ },
470
+ ];
471
+
472
+ for (const dir of skill.subDirs) {
473
+ children.push({
474
+ key: \`\${skill.name}/\${dir}\`,
475
+ title: \`\${dir}/\`,
476
+ icon: <FolderOutlined style={{ color: token.colorWarning }} />,
477
+ isLeaf: false,
478
+ isFile: false,
479
+ isDir: true,
480
+ filePath: dir,
481
+ children: [],
482
+ skillName: skill.name,
483
+ });
484
+ }
485
+
486
+ return {
487
+ key: skill.name,
488
+ title: skill.name,
489
+ icon: <FolderOutlined />,
490
+ isLeaf: false,
491
+ isFile: false,
492
+ isSkill: true,
493
+ children,
494
+ skillName: skill.name,
495
+ };
496
+ });
497
+ }, [token]);
498
+
499
+ const loadDirChildren = useCallback(async (skillName: string, dirName: string): Promise<SkillTreeNode[]> => {
500
+ const detail = await fetchSkillDetail(skillName);
501
+ const files = detail.files.filter((f) => f.path.startsWith(\`\${dirName}/\`));
502
+
503
+ const dirChildren: SkillTreeNode[] = [];
504
+ for (const f of files) {
505
+ const relativePath = f.path.slice(dirName.length + 1);
506
+ const parts = relativePath.split('/');
507
+
508
+ let currentLevel = dirChildren;
509
+ for (let i = 0; i < parts.length; i++) {
510
+ const part = parts[i]!;
511
+ const isLast = i === parts.length - 1;
512
+ const partialPath = \`\${dirName}/\${parts.slice(0, i + 1).join('/')}\`;
513
+
514
+ if (isLast) {
515
+ currentLevel.push({
516
+ key: \`\${skillName}/\${f.path}\`,
517
+ title: part,
518
+ icon: getFileIcon(part, { primary: token.colorPrimary, warning: token.colorWarning, success: token.colorSuccess }),
519
+ isLeaf: true,
520
+ isFile: true,
521
+ filePath: f.path,
522
+ skillName,
523
+ });
524
+ } else {
525
+ let dirNode = currentLevel.find((n) => n.isDir && n.filePath === partialPath);
526
+ if (!dirNode) {
527
+ dirNode = {
528
+ key: \`\${skillName}/\${partialPath}\`,
529
+ title: \`\${part}/\`,
530
+ icon: <FolderOutlined style={{ color: token.colorWarning }} />,
531
+ isLeaf: false,
532
+ isFile: false,
533
+ isDir: true,
534
+ filePath: partialPath,
535
+ children: [],
536
+ skillName,
537
+ };
538
+ currentLevel.push(dirNode);
539
+ }
540
+ currentLevel = dirNode.children!;
541
+ }
542
+ }
543
+ }
544
+
545
+ return dirChildren;
546
+ }, [token]);
547
+
548
+ const refreshSkills = useCallback(async () => {
549
+ setLoading(true);
550
+ try {
551
+ const list = await fetchSkillsList();
552
+ setSkills(list);
553
+ setTreeData(buildTreeData(list));
554
+ setExpandedKeys(list.map((s) => s.name));
555
+ } catch {
556
+ // message handled by api
557
+ } finally {
558
+ setLoading(false);
559
+ }
560
+ }, [buildTreeData]);
561
+
562
+ useEffect(() => {
563
+ refreshSkills();
564
+ }, [refreshSkills]);
565
+
566
+ const handleTreeExpand = useCallback(
567
+ async (keys: React.Key[], info: { node: SkillTreeNode; expanded: boolean }) => {
568
+ setExpandedKeys(keys);
569
+ if (!info.expanded) return;
570
+ const node = info.node;
571
+ if (node.skillName && node.isDir) {
572
+ if (node.children && node.children.length > 0) return;
573
+ const dirName = node.filePath || node.title.replace(/\\/$/, '');
574
+ const dirChildren = await loadDirChildren(node.skillName, dirName);
575
+
576
+ const updateNode = (nodes: SkillTreeNode[]): SkillTreeNode[] =>
577
+ nodes.map((n) => {
578
+ if (n.key === node.key) {
579
+ return { ...n, children: dirChildren };
580
+ }
581
+ if (n.children) {
582
+ return { ...n, children: updateNode(n.children) };
583
+ }
584
+ return n;
585
+ });
586
+
587
+ setTreeData((prev) => updateNode(prev));
588
+ }
589
+ },
590
+ [loadDirChildren],
591
+ );
592
+
593
+ const handleTreeSelect = useCallback(
594
+ async (keys: React.Key[], info: { node: SkillTreeNode }) => {
595
+ setSelectedKeys(keys);
596
+ if (keys.length === 0) {
597
+ setSkillDetail(null);
598
+ setFileContent(null);
599
+ return;
600
+ }
601
+ const node = info.node;
602
+ if (!node.isFile) {
603
+ setSkillDetail(null);
604
+ setFileContent(null);
605
+ return;
606
+ }
607
+ setLoading(true);
608
+ try {
609
+ if (node.filePath === 'SKILL.md') {
610
+ const detail = await fetchSkillDetail(node.skillName!);
611
+ setSkillDetail(detail);
612
+ setFileContent(null);
613
+ } else {
614
+ const content = await fetchSkillFile(node.skillName!, node.filePath!);
615
+ setFileContent(content);
616
+ setSkillDetail(null);
617
+ }
618
+ } catch {
619
+ // error handled silently
620
+ } finally {
621
+ setLoading(false);
622
+ }
623
+ },
624
+ [],
625
+ );
626
+
627
+ const handleDelete = useCallback(
628
+ async (skillName: string) => {
629
+ try {
630
+ await deleteSkill(skillName);
631
+ setSkillDetail(null);
632
+ setFileContent(null);
633
+ setSelectedKeys([]);
634
+ refreshSkills();
635
+ } catch {
636
+ // error handled silently
637
+ }
638
+ },
639
+ [refreshSkills],
640
+ );
641
+
642
+ const rightTitle = useMemo(() => {
643
+ if (skillDetail) return \`SKILL.md — \${skillDetail.name}\`;
644
+ if (fileContent) return fileContent.path;
645
+ return t('skill.skillDetail');
646
+ }, [skillDetail, fileContent, t]);
647
+
648
+ return (
649
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
650
+ <Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
651
+ <Breadcrumb items={[{ title: <HomeOutlined onClick={() => onNavClick?.('')} style={{ cursor: 'pointer' }} /> }, ...(breadcrumbs || []).map(b => ({ title: b.path ? <a onClick={() => onNavClick?.(b.path!)}>{b.title}</a> : b.title }))]} />
652
+ <Space>
653
+ <Button icon={<ReloadOutlined />} onClick={refreshSkills} loading={loading}>
654
+ {t('skill.refresh')}
655
+ </Button>
656
+ <Button type="primary" icon={<UploadOutlined />} onClick={() => setUploadModalOpen(true)}>
657
+ {t('skill.uploadNew')}
658
+ </Button>
659
+ <Dropdown
660
+ menu={{
661
+ items: selectedKeys.length > 0
662
+ ? [
663
+ {
664
+ key: 'download',
665
+ label: t('skill.download'),
666
+ icon: <DownloadOutlined />,
667
+ onClick: () => {
668
+ const key = String(selectedKeys[0]);
669
+ const skillName = key.includes('/') ? key.split('/')[0] : key;
670
+ downloadSkill(skillName).catch(() => {});
671
+ },
672
+ },
673
+ {
674
+ key: 'delete',
675
+ label: t('skill.delete'),
676
+ icon: <DeleteOutlined />,
677
+ danger: true,
678
+ onClick: () => {
679
+ const key = String(selectedKeys[0]);
680
+ const skillName = key.includes('/') ? key.split('/')[0] : key;
681
+ Modal.confirm({
682
+ title: t('skill.confirmDelete'),
683
+ content: t('skill.confirmDeleteContent', { name: skillName }),
684
+ okType: 'danger',
685
+ onOk: () => handleDelete(skillName),
686
+ });
687
+ },
688
+ },
689
+ ]
690
+ : [],
691
+ }}
692
+ >
693
+ <Button icon={<MoreOutlined />} disabled={selectedKeys.length === 0}>
694
+ {t('skill.more')}
695
+ </Button>
696
+ </Dropdown>
697
+ </Space>
698
+ </Flex>
699
+ <Spin spinning={loading && !skillDetail && !fileContent}>
700
+ <div style={{ display: 'flex', height: '100%', padding: 16, gap: 16, overflow: 'hidden' }}>
701
+ <div style={{ width: 300, minWidth: 240, maxWidth: 500, overflow: 'auto', flexShrink: 0 }}>
702
+ <Tree<SkillTreeNode>
703
+ treeData={treeData as unknown as any}
704
+ selectedKeys={selectedKeys}
705
+ expandedKeys={expandedKeys}
706
+ showLine={{ showLeafIcon: false }}
707
+ blockNode
708
+ onSelect={handleTreeSelect}
709
+ onExpand={handleTreeExpand}
710
+ style={{ fontSize: 14 }}
711
+ titleRender={(node: any) => (
712
+ <Flex align="center" gap={4}>
713
+ {node.icon}
714
+ <span>{node.title}</span>
715
+ </Flex>
716
+ )}
717
+ />
718
+ </div>
719
+
720
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
721
+ <Card
722
+ size="small"
723
+ style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
724
+ styles={{ body: { padding: 16, flex: 1, overflow: 'auto', minHeight: 0 } }}
725
+ title={
726
+ <Flex align="center" gap="small">
727
+ <span>{rightTitle}</span>
728
+ {(skillDetail || fileContent) && (
729
+ <Segmented
730
+ size="small"
731
+ options={[
732
+ { label: t('skill.preview'), value: 'preview', icon: <EyeOutlined /> },
733
+ { label: t('skill.source'), value: 'source', icon: <CodeOutlined /> },
734
+ ]}
735
+ value={viewMode}
736
+ onChange={(v) => setViewMode(v as 'preview' | 'source')}
737
+ />
738
+ )}
739
+ </Flex>
740
+ }
741
+ >
742
+ <SkillFileViewer
743
+ skillDetail={skillDetail}
744
+ fileContent={fileContent}
745
+ viewMode={viewMode}
746
+ onViewModeChange={setViewMode}
747
+ mode={mode}
748
+ token={token}
749
+ t={t}
750
+ />
751
+ </Card>
752
+ </div>
753
+ </div>
754
+ </Spin>
755
+
756
+ <SkillUploadModal
757
+ open={uploadModalOpen}
758
+ onClose={() => setUploadModalOpen(false)}
759
+ onSuccess={refreshSkills}
760
+ token={token}
761
+ t={t}
762
+ />
763
+ </div>
764
+ );
765
+ }
766
+ `;
767
+ }
768
+ //# sourceMappingURL=system-settings-page.js.map