plugin-agent-orchestrator 1.0.22 → 1.0.25

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 (103) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/214.723affb37c13bf7a.js +10 -0
  5. package/dist/client-v2/264.0533912e6c5ea2d7.js +10 -0
  6. package/dist/client-v2/41.1805b2edfaa4afe2.js +10 -0
  7. package/dist/client-v2/418.5ae055abf141820e.js +10 -0
  8. package/dist/client-v2/619.d99d3c9e61c99064.js +10 -0
  9. package/dist/client-v2/70.a15d7fcec7c41768.js +10 -0
  10. package/dist/client-v2/892.72db4161511c8a16.js +10 -0
  11. package/dist/client-v2/926.87f660b670d85bcc.js +10 -0
  12. package/dist/client-v2/index.js +10 -0
  13. package/dist/externalVersion.js +8 -6
  14. package/dist/locale/en-US.json +7 -0
  15. package/dist/locale/vi-VN.json +7 -0
  16. package/dist/locale/zh-CN.json +27 -0
  17. package/dist/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.js +63 -0
  18. package/dist/server/plugin.js +32 -1
  19. package/dist/server/services/AgentHarness.js +52 -27
  20. package/dist/server/services/AgentLoopController.js +8 -2
  21. package/dist/server/services/AgentLoopService.js +1 -1
  22. package/dist/server/services/AgentRegistryService.js +53 -42
  23. package/dist/server/services/CircuitBreaker.js +7 -2
  24. package/dist/server/services/CodeValidator.js +48 -14
  25. package/dist/server/services/SandboxRunner.js +18 -14
  26. package/dist/server/skill-hub/plugin.js +44 -17
  27. package/dist/server/tools/delegate-task.js +7 -2
  28. package/dist/server/tools/skill-execute.js +33 -2
  29. package/dist/server/utils/ai-manager.js +51 -0
  30. package/dist/server/utils/ctx-utils.js +11 -0
  31. package/dist/server/utils/skill-settings.js +122 -0
  32. package/package.json +49 -45
  33. package/src/client/AIEmployeesContext.tsx +60 -19
  34. package/src/client/AgentRunsTab.tsx +769 -764
  35. package/src/client/HarnessProfilesTab.tsx +257 -247
  36. package/src/client/RulesTab.tsx +787 -716
  37. package/src/client/TracingTab.tsx +9 -6
  38. package/src/client/plugin.tsx +34 -27
  39. package/src/client/skill-hub/components/ExecutionHistory.tsx +9 -8
  40. package/src/client/skill-hub/components/GitSkillImport.tsx +12 -5
  41. package/src/client/skill-hub/components/LoopSettings.tsx +2 -2
  42. package/src/client/skill-hub/components/SkillEditor.tsx +2 -2
  43. package/src/client/skill-hub/components/SkillManager.tsx +2 -2
  44. package/src/client/skill-hub/components/SkillMetrics.tsx +157 -124
  45. package/src/client/skill-hub/components/SkillTestPanel.tsx +14 -13
  46. package/src/client/skill-hub/index.tsx +58 -51
  47. package/src/client/skill-hub/locale.ts +1 -1
  48. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +132 -99
  49. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +71 -58
  50. package/src/client/tools/PlanApprovalCard.tsx +3 -2
  51. package/src/client/tools/registerOrchestratorCards.ts +17 -7
  52. package/src/client-v2/components/AIEmployeeSelect.tsx +47 -0
  53. package/src/client-v2/components/AIEmployeesContext.tsx +110 -0
  54. package/src/client-v2/components/AgentRunsTab.tsx +767 -0
  55. package/src/client-v2/components/HarnessProfilesTab.tsx +254 -0
  56. package/src/client-v2/components/RulesTab.tsx +782 -0
  57. package/src/client-v2/components/TracingTab.tsx +432 -0
  58. package/src/client-v2/hooks/useApiRequest.ts +114 -0
  59. package/src/client-v2/index.tsx +1 -0
  60. package/src/client-v2/pages/AgentRunsPage.tsx +13 -0
  61. package/src/client-v2/pages/ExecutionHistoryPage.tsx +10 -0
  62. package/src/client-v2/pages/HarnessProfilesPage.tsx +10 -0
  63. package/src/client-v2/pages/LoopSettingsPage.tsx +10 -0
  64. package/src/client-v2/pages/RulesPage.tsx +13 -0
  65. package/src/client-v2/pages/SkillDefinitionsPage.tsx +10 -0
  66. package/src/client-v2/pages/SkillMetricsPage.tsx +10 -0
  67. package/src/client-v2/pages/TracingPage.tsx +13 -0
  68. package/src/client-v2/plugin.tsx +70 -0
  69. package/src/client-v2/skill-hub/components/ExecutionHistory.tsx +196 -0
  70. package/src/client-v2/skill-hub/components/FileLinkList.tsx +37 -0
  71. package/src/client-v2/skill-hub/components/GitSkillImport.tsx +539 -0
  72. package/src/client-v2/skill-hub/components/LoopSettings.tsx +331 -0
  73. package/src/client-v2/skill-hub/components/SkillEditor.tsx +453 -0
  74. package/src/client-v2/skill-hub/components/SkillManager.tsx +174 -0
  75. package/src/client-v2/skill-hub/components/SkillMetrics.tsx +157 -0
  76. package/src/client-v2/skill-hub/components/SkillTestPanel.tsx +135 -0
  77. package/src/client-v2/skill-hub/locale.ts +13 -0
  78. package/src/client-v2/skill-hub/tools/loopTemplates.ts +52 -0
  79. package/src/client-v2/skill-hub/utils/jsonFields.ts +41 -0
  80. package/src/client-v2/utils/jsonFields.ts +41 -0
  81. package/src/locale/en-US.json +7 -0
  82. package/src/locale/vi-VN.json +7 -0
  83. package/src/locale/zh-CN.json +27 -0
  84. package/src/server/__tests__/agent-registry-service.test.ts +147 -0
  85. package/src/server/__tests__/code-validator.test.ts +63 -0
  86. package/src/server/__tests__/skill-execute.test.ts +33 -0
  87. package/src/server/__tests__/skill-settings.test.ts +63 -0
  88. package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +39 -0
  89. package/src/server/plugin.ts +62 -21
  90. package/src/server/services/AgentHarness.ts +49 -22
  91. package/src/server/services/AgentLoopController.ts +17 -6
  92. package/src/server/services/AgentLoopService.ts +1 -1
  93. package/src/server/services/AgentPlannerService.ts +10 -0
  94. package/src/server/services/AgentRegistryService.ts +89 -47
  95. package/src/server/services/CircuitBreaker.ts +10 -0
  96. package/src/server/services/CodeValidator.ts +237 -159
  97. package/src/server/services/SandboxRunner.ts +203 -189
  98. package/src/server/skill-hub/plugin.ts +933 -898
  99. package/src/server/tools/delegate-task.ts +12 -9
  100. package/src/server/tools/skill-execute.ts +194 -160
  101. package/src/server/utils/ai-manager.ts +24 -0
  102. package/src/server/utils/ctx-utils.ts +14 -0
  103. package/src/server/utils/skill-settings.ts +116 -0
@@ -0,0 +1,453 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Modal, Form, Input, Select, InputNumber, Switch, message, Upload, Radio, Button, Space, theme } from 'antd';
3
+ import { InboxOutlined, CloseOutlined, SaveOutlined } from '@ant-design/icons';
4
+ import { useApiClient as useAPIClient } from '../../hooks/useApiRequest';
5
+ import { useT } from '../locale';
6
+ import { formatJsonText, stringifyJsonText } from '../utils/jsonFields';
7
+
8
+ const { TextArea } = Input;
9
+ const { useToken } = theme;
10
+
11
+ interface SkillEditorProps {
12
+ skill: any | null;
13
+ onClose: (saved?: boolean) => void;
14
+ }
15
+
16
+ export const SkillEditor: React.FC<SkillEditorProps> = ({ skill, onClose }) => {
17
+ const api = useAPIClient();
18
+ const t = useT();
19
+ const { token } = useToken();
20
+ const [form] = Form.useForm();
21
+ const isEditing = !!skill;
22
+ const [templates, setTemplates] = useState<any[]>([]);
23
+
24
+ useEffect(() => {
25
+ if (!isEditing) {
26
+ api
27
+ .request({ url: 'skillHub:listTemplates' })
28
+ .then(({ data }) => {
29
+ const responseData = data?.data?.data || data?.data || data;
30
+ let list = responseData;
31
+ if (list && list.data && Array.isArray(list.data)) list = list.data;
32
+ setTemplates(Array.isArray(list) ? list : []);
33
+ })
34
+ .catch(() => {
35
+ setTemplates([]);
36
+ });
37
+ }
38
+ }, [api, isEditing]);
39
+
40
+ useEffect(() => {
41
+ if (skill) {
42
+ form.setFieldsValue({
43
+ ...skill,
44
+ inputSchema: formatJsonText(skill.inputSchema, null),
45
+ interactionSchema: formatJsonText(skill.interactionSchema, null),
46
+ packages: formatJsonText(skill.packages, []),
47
+ });
48
+ } else {
49
+ form.resetFields();
50
+ form.setFieldsValue({
51
+ storageType: 'database',
52
+ language: 'python',
53
+ timeoutSeconds: 60,
54
+ maxOutputSizeMb: 50,
55
+ enabled: true,
56
+ toolScope: 'CUSTOM',
57
+ autoCall: false,
58
+ packages: '[]',
59
+ });
60
+ }
61
+ }, [skill, form]);
62
+
63
+ const storageType = Form.useWatch('storageType', form) || 'database';
64
+
65
+ useEffect(() => {
66
+ if (storageType === 'plugin') {
67
+ const pSource = form.getFieldValue('pluginSource');
68
+ if (pSource) {
69
+ const tmpl = templates.find((item) => item.name === pSource);
70
+ if (tmpl) {
71
+ form.setFieldsValue({
72
+ name: tmpl.name,
73
+ title: tmpl.title,
74
+ description: tmpl.description,
75
+ language: tmpl.language,
76
+ instructions: tmpl.instructions || '',
77
+ inputSchema: formatJsonText(tmpl.inputSchema, null),
78
+ interactionSchema: formatJsonText(tmpl.interactionSchema, null),
79
+ packages: formatJsonText(tmpl.packages, []),
80
+ timeoutSeconds: tmpl.timeoutSeconds || 60,
81
+ maxOutputSizeMb: tmpl.maxOutputSizeMb || 50,
82
+ toolScope: tmpl.toolScope || 'CUSTOM',
83
+ storageUrl: tmpl.storageUrl,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ }, [storageType, templates, form]);
89
+
90
+ const handleTemplateSelect = (templateName: string) => {
91
+ const tmpl = templates.find((item) => item.name === templateName);
92
+ if (!tmpl) return;
93
+
94
+ if (storageType === 'plugin') {
95
+ form.setFieldsValue({
96
+ name: tmpl.name,
97
+ title: tmpl.title,
98
+ description: tmpl.description || '',
99
+ language: tmpl.language || 'python',
100
+ instructions: tmpl.instructions || '',
101
+ inputSchema: formatJsonText(tmpl.inputSchema, null),
102
+ interactionSchema: formatJsonText(tmpl.interactionSchema, null),
103
+ packages: formatJsonText(tmpl.packages, []),
104
+ timeoutSeconds: tmpl.timeoutSeconds || 60,
105
+ maxOutputSizeMb: tmpl.maxOutputSizeMb || 50,
106
+ toolScope: tmpl.toolScope || 'CUSTOM',
107
+ storageUrl: tmpl.storageUrl,
108
+ });
109
+ } else {
110
+ form.setFieldsValue({
111
+ name: tmpl.name,
112
+ title: tmpl.title,
113
+ description: tmpl.description || '',
114
+ instructions: tmpl.instructions || '',
115
+ language: tmpl.language || 'python',
116
+ codeTemplate: tmpl.codeTemplate || '',
117
+ inputSchema: formatJsonText(tmpl.inputSchema, null),
118
+ interactionSchema: formatJsonText(tmpl.interactionSchema, null),
119
+ packages: formatJsonText(tmpl.packages, []),
120
+ timeoutSeconds: tmpl.timeoutSeconds || 60,
121
+ maxOutputSizeMb: tmpl.maxOutputSizeMb || 50,
122
+ toolScope: tmpl.toolScope || 'CUSTOM',
123
+ enabled: tmpl.enabled ?? true,
124
+ });
125
+ }
126
+ message.success(t('Template applied'));
127
+ };
128
+
129
+ const handleSave = async () => {
130
+ try {
131
+ const values = await form.validateFields();
132
+
133
+ try {
134
+ if (values.inputSchema) JSON.parse(values.inputSchema);
135
+ } catch {
136
+ message.error(t('Invalid JSON in Input Schema'));
137
+ return;
138
+ }
139
+
140
+ try {
141
+ if (values.interactionSchema) JSON.parse(values.interactionSchema);
142
+ } catch {
143
+ message.error(t('Invalid JSON in Interaction Schema'));
144
+ return;
145
+ }
146
+
147
+ try {
148
+ if (values.packages) JSON.parse(values.packages);
149
+ } catch {
150
+ message.error(t('Invalid JSON in Packages'));
151
+ return;
152
+ }
153
+
154
+ const data = {
155
+ ...values,
156
+ inputSchema: values.inputSchema ? stringifyJsonText(values.inputSchema) : null,
157
+ interactionSchema: values.interactionSchema ? stringifyJsonText(values.interactionSchema) : null,
158
+ packages: stringifyJsonText(values.packages || [], []),
159
+ };
160
+
161
+ if (isEditing) {
162
+ await api.request({
163
+ url: 'skillDefinitions:update',
164
+ method: 'POST',
165
+ params: { filterByTk: skill.id },
166
+ data,
167
+ });
168
+ } else {
169
+ await api.request({
170
+ url: 'skillDefinitions:create',
171
+ method: 'POST',
172
+ data,
173
+ });
174
+ }
175
+
176
+ message.success(t(isEditing ? 'Skill updated' : 'Skill created'));
177
+ onClose(true);
178
+ } catch (err: any) {
179
+ if (err?.errorFields) return;
180
+ message.error(t('Failed to save skill'));
181
+ }
182
+ };
183
+
184
+ return (
185
+ <Modal
186
+ open
187
+ title={isEditing ? t('Edit Skill') : t('New Skill')}
188
+ onCancel={() => onClose()}
189
+ width={720}
190
+ destroyOnClose
191
+ footer={null}
192
+ styles={{
193
+ body: { padding: 0, display: 'flex', flexDirection: 'column', maxHeight: 'calc(100vh - 120px)' },
194
+ }}
195
+ style={{ top: 40 }}
196
+ >
197
+ <Form
198
+ form={form}
199
+ layout="vertical"
200
+ size="middle"
201
+ component="div"
202
+ style={{ display: 'flex', flexDirection: 'column', maxHeight: 'calc(100vh - 120px)' }}
203
+ >
204
+ <Form.Item name="storageUrl" hidden>
205
+ <Input />
206
+ </Form.Item>
207
+
208
+ <div
209
+ style={{
210
+ flexShrink: 0,
211
+ background: token.colorBgContainer,
212
+ borderBottom: `1px solid ${token.colorBorderSecondary}`,
213
+ padding: '12px 24px',
214
+ display: 'flex',
215
+ alignItems: 'center',
216
+ justifyContent: 'space-between',
217
+ flexWrap: 'wrap',
218
+ gap: 12,
219
+ }}
220
+ >
221
+ <Form.Item name="storageType" noStyle>
222
+ <Radio.Group optionType="button" buttonStyle="solid" size="small">
223
+ <Radio value="database">{t('Database Editor')}</Radio>
224
+ <Radio value="local">{t('ZIP Package')}</Radio>
225
+ <Radio value="plugin" disabled={!Array.isArray(templates) || templates.length === 0}>
226
+ {t('Bind to Plugin')}
227
+ </Radio>
228
+ </Radio.Group>
229
+ </Form.Item>
230
+
231
+ <Space size={16}>
232
+ <Space size={4}>
233
+ <span style={{ fontSize: 12, color: token.colorTextSecondary }}>{t('Enabled')}</span>
234
+ <Form.Item name="enabled" valuePropName="checked" noStyle>
235
+ <Switch size="small" />
236
+ </Form.Item>
237
+ </Space>
238
+ <Space size={4}>
239
+ <span style={{ fontSize: 12, color: token.colorTextSecondary }}>{t('Auto Call')}</span>
240
+ <Form.Item name="autoCall" valuePropName="checked" noStyle>
241
+ <Switch size="small" />
242
+ </Form.Item>
243
+ </Space>
244
+ </Space>
245
+
246
+ <Space>
247
+ <Button size="small" icon={<CloseOutlined />} onClick={() => onClose()}>
248
+ {t('Cancel')}
249
+ </Button>
250
+ <Button type="primary" size="small" icon={<SaveOutlined />} onClick={handleSave}>
251
+ {isEditing ? t('Save') : t('Create')}
252
+ </Button>
253
+ </Space>
254
+ </div>
255
+
256
+ <div
257
+ style={{
258
+ flex: 1,
259
+ overflowY: 'auto',
260
+ padding: '16px 24px 24px',
261
+ minHeight: 0,
262
+ }}
263
+ >
264
+ {storageType === 'database' && !isEditing && Array.isArray(templates) && templates.length > 0 && (
265
+ <Form.Item label={t('Import Template to Pre-fill code (Optional)')} style={{ marginBottom: 24 }}>
266
+ <Select placeholder={t('Select a template to pre-fill')} onChange={handleTemplateSelect} allowClear>
267
+ {templates.map((tmpl) => (
268
+ <Select.Option key={tmpl.name} value={tmpl.name}>
269
+ {tmpl.title} ({tmpl.pluginSource || tmpl.name})
270
+ </Select.Option>
271
+ ))}
272
+ </Select>
273
+ </Form.Item>
274
+ )}
275
+
276
+ {storageType === 'plugin' && (
277
+ <>
278
+ <Form.Item name="inputSchema" hidden>
279
+ <TextArea />
280
+ </Form.Item>
281
+ <Form.Item name="packages" hidden>
282
+ <Input />
283
+ </Form.Item>
284
+ <Form.Item
285
+ name="pluginSource"
286
+ label={t('Select Plugin Skill')}
287
+ rules={[{ required: true }]}
288
+ style={{
289
+ marginBottom: 24,
290
+ padding: 12,
291
+ background: '#e6f4ff',
292
+ borderRadius: 8,
293
+ border: '1px solid #91caff',
294
+ }}
295
+ extra={
296
+ <div style={{ fontSize: 12, color: '#1677ff', marginTop: 8 }}>
297
+ {t(
298
+ 'Binding dynamically delegates execution to the plugin logic. Code, Language, and Schemas are managed externally.',
299
+ )}
300
+ </div>
301
+ }
302
+ >
303
+ <Select
304
+ placeholder={t('Choose an enabled plugin skill to attach')}
305
+ onChange={handleTemplateSelect}
306
+ allowClear
307
+ >
308
+ {Array.isArray(templates) &&
309
+ templates.map((tmpl) => (
310
+ <Select.Option key={tmpl.name} value={tmpl.name}>
311
+ {tmpl.title} ({tmpl.pluginSource || tmpl.name})
312
+ </Select.Option>
313
+ ))}
314
+ </Select>
315
+ </Form.Item>
316
+ </>
317
+ )}
318
+
319
+ <Form.Item name="name" label={t('Name (Internal Identifier)')} rules={[{ required: true }]}>
320
+ <Input placeholder="generate-word-report" disabled={isEditing || storageType === 'plugin'} />
321
+ </Form.Item>
322
+
323
+ <Form.Item name="title" label={t('Title')} rules={[{ required: true }]}>
324
+ <Input placeholder="Generate Word Report" disabled={storageType === 'plugin'} />
325
+ </Form.Item>
326
+
327
+ <Form.Item name="description" label={t('Description')}>
328
+ <TextArea
329
+ rows={2}
330
+ placeholder="Description for AI employee to understand this skill"
331
+ disabled={storageType === 'plugin'}
332
+ />
333
+ </Form.Item>
334
+
335
+ <Form.Item name="instructions" label={t('Instructions / Documentation')}>
336
+ <TextArea
337
+ rows={6}
338
+ placeholder="Detailed instructions or markdown documentation for this skill"
339
+ disabled={storageType === 'plugin'}
340
+ />
341
+ </Form.Item>
342
+
343
+ {storageType === 'local' && (
344
+ <Form.Item
345
+ label={t('Skill Package ZIP')}
346
+ name="fileId"
347
+ valuePropName="fileId"
348
+ getValueFromEvent={(e: any) =>
349
+ e?.file?.response?.data?.id || e?.fileList?.[0]?.response?.data?.id || undefined
350
+ }
351
+ rules={[{ required: true }]}
352
+ >
353
+ <Upload.Dragger
354
+ name="file"
355
+ action="/api/attachments:create"
356
+ headers={{ Authorization: `Bearer ${api.auth.getToken()}` }}
357
+ maxCount={1}
358
+ accept=".zip"
359
+ >
360
+ <p className="ant-upload-drag-icon">
361
+ <InboxOutlined />
362
+ </p>
363
+ <p className="ant-upload-text">{t('Click or drag ZIP file to this area to upload')}</p>
364
+ <p className="ant-upload-hint">
365
+ {t(
366
+ 'Upload a skill package zip containing SKILL.md and index.py/.js. Metadata will be extracted automatically.',
367
+ )}
368
+ </p>
369
+ </Upload.Dragger>
370
+ </Form.Item>
371
+ )}
372
+
373
+ {storageType !== 'plugin' && (
374
+ <Form.Item name="language" label={t('Language')}>
375
+ <Select>
376
+ <Select.Option value="python">Python</Select.Option>
377
+ <Select.Option value="node">Node.js</Select.Option>
378
+ </Select>
379
+ </Form.Item>
380
+ )}
381
+
382
+ {storageType === 'database' && (
383
+ <Form.Item
384
+ name="codeTemplate"
385
+ label={t('Code Template')}
386
+ extra={t('Use {{placeholder}} for input parameters. Use OUTPUT_DIR env var for output directory.')}
387
+ >
388
+ <TextArea
389
+ rows={12}
390
+ style={{ fontFamily: 'monospace', fontSize: 13 }}
391
+ placeholder={'import os\n\noutput_dir = os.environ.get("OUTPUT_DIR", "/output")\n# Your code here'}
392
+ />
393
+ </Form.Item>
394
+ )}
395
+
396
+ {storageType !== 'plugin' && (
397
+ <Form.Item
398
+ name="inputSchema"
399
+ label={t('Input Schema (JSON)')}
400
+ extra={t('JSON Schema defining input parameters for this skill')}
401
+ >
402
+ <TextArea
403
+ rows={6}
404
+ style={{ fontFamily: 'monospace', fontSize: 13 }}
405
+ placeholder={'{\n "type": "object",\n "properties": {},\n "required": []\n}'}
406
+ />
407
+ </Form.Item>
408
+ )}
409
+
410
+ <Form.Item
411
+ name="interactionSchema"
412
+ label={t('Interaction Schema (optional)')}
413
+ extra={t(
414
+ 'If set, user is prompted to fill / confirm input before execution. Type: form | select | confirm.',
415
+ )}
416
+ >
417
+ <TextArea
418
+ rows={6}
419
+ style={{ fontFamily: 'monospace', fontSize: 13 }}
420
+ placeholder={
421
+ '{\n "type": "form",\n "prompt": "Confirm parameters",\n "fields": {\n "fileName": { "type": "string", "title": "File name", "required": true }\n }\n}'
422
+ }
423
+ />
424
+ </Form.Item>
425
+
426
+ {storageType !== 'plugin' && (
427
+ <Form.Item name="packages" label={t('Packages (JSON array)')}>
428
+ <Input placeholder='["python-docx", "openpyxl"]' style={{ fontFamily: 'monospace' }} />
429
+ </Form.Item>
430
+ )}
431
+
432
+ <div style={{ display: 'flex', gap: 16 }}>
433
+ <Form.Item name="timeoutSeconds" label={t('Timeout (seconds)')} style={{ flex: 1 }}>
434
+ <InputNumber min={5} max={300} style={{ width: '100%' }} />
435
+ </Form.Item>
436
+
437
+ <Form.Item name="maxOutputSizeMb" label={t('Max Output (MB)')} style={{ flex: 1 }}>
438
+ <InputNumber min={1} max={200} style={{ width: '100%' }} />
439
+ </Form.Item>
440
+
441
+ <Form.Item name="toolScope" label={t('Tool Scope')} style={{ flex: 1 }}>
442
+ <Select>
443
+ <Select.Option value="CUSTOM">CUSTOM</Select.Option>
444
+ <Select.Option value="GENERAL">GENERAL</Select.Option>
445
+ <Select.Option value="SPECIFIED">SPECIFIED</Select.Option>
446
+ </Select>
447
+ </Form.Item>
448
+ </div>
449
+ </div>
450
+ </Form>
451
+ </Modal>
452
+ );
453
+ };
@@ -0,0 +1,174 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Card, Button, Space, message, Popconfirm, Tag, List, Typography, Tooltip, Switch } from 'antd';
3
+ import { PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, BranchesOutlined } from '@ant-design/icons';
4
+ import { useApiClient as useAPIClient } from '../../hooks/useApiRequest';
5
+ import { useT } from '../locale';
6
+ import { SkillEditor } from './SkillEditor';
7
+ import { SkillTestPanel } from './SkillTestPanel';
8
+ import { GitSkillImport } from './GitSkillImport';
9
+
10
+ export const SkillManager: React.FC = () => {
11
+ const api = useAPIClient();
12
+ const t = useT();
13
+ const [skills, setSkills] = useState<any[]>([]);
14
+ const [loading, setLoading] = useState(false);
15
+ const [editorVisible, setEditorVisible] = useState(false);
16
+ const [testVisible, setTestVisible] = useState(false);
17
+ const [editingSkill, setEditingSkill] = useState<any>(null);
18
+ const [testingSkill, setTestingSkill] = useState<any>(null);
19
+ const [gitImportVisible, setGitImportVisible] = useState(false);
20
+
21
+ const fetchSkills = useCallback(async () => {
22
+ setLoading(true);
23
+ try {
24
+ const { data } = await api.request({ url: 'skillDefinitions:list', params: { pageSize: 100 } });
25
+ const rawData = data?.data?.data ?? data?.data ?? [];
26
+ setSkills(Array.isArray(rawData) ? rawData : []);
27
+ } catch {
28
+ message.error(t('Failed to load skills'));
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ }, [api, t]);
33
+
34
+ useEffect(() => {
35
+ fetchSkills();
36
+ }, [fetchSkills]);
37
+
38
+ const handleCreate = () => {
39
+ setEditingSkill(null);
40
+ setEditorVisible(true);
41
+ };
42
+
43
+ const handleEdit = (record: any) => {
44
+ setEditingSkill(record);
45
+ setEditorVisible(true);
46
+ };
47
+
48
+ const handleTest = (record: any) => {
49
+ setTestingSkill(record);
50
+ setTestVisible(true);
51
+ };
52
+
53
+ const handleDelete = async (id: number) => {
54
+ try {
55
+ await api.request({ url: 'skillDefinitions:destroy', method: 'POST', params: { filterByTk: id } });
56
+ message.success(t('Deleted'));
57
+ fetchSkills();
58
+ } catch {
59
+ message.error(t('Failed to delete'));
60
+ }
61
+ };
62
+
63
+ const handleToggleEnabled = async (record: any) => {
64
+ try {
65
+ await api.request({
66
+ url: 'skillDefinitions:update',
67
+ method: 'POST',
68
+ params: { filterByTk: record.id },
69
+ data: { enabled: !record.enabled },
70
+ });
71
+ fetchSkills();
72
+ } catch {
73
+ message.error(t('Failed to update'));
74
+ }
75
+ };
76
+
77
+ const handleEditorClose = (saved?: boolean) => {
78
+ setEditorVisible(false);
79
+ setEditingSkill(null);
80
+ if (saved) fetchSkills();
81
+ };
82
+
83
+ return (
84
+ <Card
85
+ title={t('Skill Definitions')}
86
+ extra={
87
+ <Space>
88
+ <Button icon={<BranchesOutlined />} onClick={() => setGitImportVisible(true)}>
89
+ {t('Import from Git')}
90
+ </Button>
91
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
92
+ {t('New Skill')}
93
+ </Button>
94
+ </Space>
95
+ }
96
+ >
97
+ <List
98
+ grid={{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }}
99
+ dataSource={skills}
100
+ loading={loading}
101
+ renderItem={(skill) => (
102
+ <List.Item>
103
+ <Card
104
+ size="small"
105
+ title={
106
+ <Typography.Text ellipsis title={skill.title}>
107
+ {skill.title}
108
+ </Typography.Text>
109
+ }
110
+ extra={<Tag color={skill.language === 'python' ? 'blue' : 'green'}>{skill.language}</Tag>}
111
+ actions={[
112
+ <Tooltip key="test" title={t('Test')}>
113
+ <PlayCircleOutlined onClick={() => handleTest(skill)} />
114
+ </Tooltip>,
115
+ <Tooltip key="edit" title={t('Edit')}>
116
+ <EditOutlined onClick={() => handleEdit(skill)} />
117
+ </Tooltip>,
118
+ <Popconfirm key="delete" title={t('Delete?')} onConfirm={() => handleDelete(skill.id)}>
119
+ <Tooltip title={t('Delete')}>
120
+ <DeleteOutlined style={{ color: 'red' }} />
121
+ </Tooltip>
122
+ </Popconfirm>,
123
+ ]}
124
+ style={{ boxShadow: '0 2px 8px rgba(0,0,0,0.05)', borderRadius: 8 }}
125
+ >
126
+ <Card.Meta
127
+ title={
128
+ <Typography.Text type="secondary" style={{ fontSize: 13 }}>
129
+ {skill.name}
130
+ </Typography.Text>
131
+ }
132
+ description={
133
+ <div
134
+ style={{
135
+ height: 60,
136
+ overflow: 'hidden',
137
+ display: '-webkit-box',
138
+ WebkitLineClamp: 3,
139
+ WebkitBoxOrient: 'vertical',
140
+ fontSize: 13,
141
+ }}
142
+ >
143
+ {skill.description || t('No description')}
144
+ </div>
145
+ }
146
+ />
147
+ <div style={{ marginTop: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
148
+ <Space size={4}>
149
+ <Switch checked={skill.enabled} onChange={() => handleToggleEnabled(skill)} size="small" />
150
+ <span style={{ fontSize: 12 }}>{skill.enabled ? t('Enabled') : t('Disabled')}</span>
151
+ </Space>
152
+ <Tag color="purple" style={{ margin: 0, fontSize: 11 }}>
153
+ {skill.storageType ? skill.storageType.toUpperCase() : 'DB'}
154
+ </Tag>
155
+ </div>
156
+ </Card>
157
+ </List.Item>
158
+ )}
159
+ />
160
+
161
+ {editorVisible && <SkillEditor skill={editingSkill} onClose={handleEditorClose} />}
162
+
163
+ {testVisible && testingSkill && <SkillTestPanel skill={testingSkill} onClose={() => setTestVisible(false)} />}
164
+
165
+ <GitSkillImport
166
+ open={gitImportVisible}
167
+ onClose={(synced) => {
168
+ setGitImportVisible(false);
169
+ if (synced) fetchSkills();
170
+ }}
171
+ />
172
+ </Card>
173
+ );
174
+ };