plugin-agent-orchestrator 1.0.22 → 1.0.23

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 (96) 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 +7 -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 +51 -14
  34. package/src/client/AgentRunsTab.tsx +767 -764
  35. package/src/client/HarnessProfilesTab.tsx +254 -247
  36. package/src/client/RulesTab.tsx +780 -716
  37. package/src/client/TracingTab.tsx +1 -0
  38. package/src/client/plugin.tsx +34 -27
  39. package/src/client/skill-hub/components/GitSkillImport.tsx +10 -3
  40. package/src/client/skill-hub/components/SkillMetrics.tsx +157 -124
  41. package/src/client/skill-hub/index.tsx +58 -51
  42. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +132 -99
  43. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +71 -58
  44. package/src/client/tools/registerOrchestratorCards.ts +17 -7
  45. package/src/client-v2/components/AIEmployeeSelect.tsx +47 -0
  46. package/src/client-v2/components/AIEmployeesContext.tsx +110 -0
  47. package/src/client-v2/components/AgentRunsTab.tsx +767 -0
  48. package/src/client-v2/components/HarnessProfilesTab.tsx +254 -0
  49. package/src/client-v2/components/RulesTab.tsx +782 -0
  50. package/src/client-v2/components/TracingTab.tsx +432 -0
  51. package/src/client-v2/hooks/useApiRequest.ts +114 -0
  52. package/src/client-v2/index.tsx +1 -0
  53. package/src/client-v2/pages/AgentRunsPage.tsx +13 -0
  54. package/src/client-v2/pages/ExecutionHistoryPage.tsx +10 -0
  55. package/src/client-v2/pages/HarnessProfilesPage.tsx +10 -0
  56. package/src/client-v2/pages/LoopSettingsPage.tsx +10 -0
  57. package/src/client-v2/pages/RulesPage.tsx +13 -0
  58. package/src/client-v2/pages/SkillDefinitionsPage.tsx +10 -0
  59. package/src/client-v2/pages/SkillMetricsPage.tsx +10 -0
  60. package/src/client-v2/pages/TracingPage.tsx +13 -0
  61. package/src/client-v2/plugin.tsx +70 -0
  62. package/src/client-v2/skill-hub/components/ExecutionHistory.tsx +196 -0
  63. package/src/client-v2/skill-hub/components/FileLinkList.tsx +37 -0
  64. package/src/client-v2/skill-hub/components/GitSkillImport.tsx +539 -0
  65. package/src/client-v2/skill-hub/components/LoopSettings.tsx +331 -0
  66. package/src/client-v2/skill-hub/components/SkillEditor.tsx +453 -0
  67. package/src/client-v2/skill-hub/components/SkillManager.tsx +174 -0
  68. package/src/client-v2/skill-hub/components/SkillMetrics.tsx +157 -0
  69. package/src/client-v2/skill-hub/components/SkillTestPanel.tsx +135 -0
  70. package/src/client-v2/skill-hub/locale.ts +13 -0
  71. package/src/client-v2/skill-hub/tools/loopTemplates.ts +52 -0
  72. package/src/client-v2/skill-hub/utils/jsonFields.ts +41 -0
  73. package/src/client-v2/utils/jsonFields.ts +41 -0
  74. package/src/locale/en-US.json +7 -0
  75. package/src/locale/vi-VN.json +7 -0
  76. package/src/locale/zh-CN.json +27 -0
  77. package/src/server/__tests__/agent-registry-service.test.ts +147 -0
  78. package/src/server/__tests__/code-validator.test.ts +63 -0
  79. package/src/server/__tests__/skill-execute.test.ts +33 -0
  80. package/src/server/__tests__/skill-settings.test.ts +63 -0
  81. package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +39 -0
  82. package/src/server/plugin.ts +62 -21
  83. package/src/server/services/AgentHarness.ts +49 -22
  84. package/src/server/services/AgentLoopController.ts +17 -6
  85. package/src/server/services/AgentLoopService.ts +1 -1
  86. package/src/server/services/AgentPlannerService.ts +10 -0
  87. package/src/server/services/AgentRegistryService.ts +89 -47
  88. package/src/server/services/CircuitBreaker.ts +10 -0
  89. package/src/server/services/CodeValidator.ts +237 -159
  90. package/src/server/services/SandboxRunner.ts +203 -189
  91. package/src/server/skill-hub/plugin.ts +933 -898
  92. package/src/server/tools/delegate-task.ts +12 -9
  93. package/src/server/tools/skill-execute.ts +194 -160
  94. package/src/server/utils/ai-manager.ts +24 -0
  95. package/src/server/utils/ctx-utils.ts +14 -0
  96. package/src/server/utils/skill-settings.ts +116 -0
@@ -0,0 +1,539 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import {
3
+ Modal,
4
+ Steps,
5
+ Select,
6
+ Button,
7
+ Table,
8
+ Tag,
9
+ Space,
10
+ Typography,
11
+ Empty,
12
+ Spin,
13
+ Input,
14
+ Switch,
15
+ Alert,
16
+ message,
17
+ Result,
18
+ theme,
19
+ } from 'antd';
20
+ import { BranchesOutlined, FolderOutlined, SyncOutlined, DatabaseOutlined, SearchOutlined } from '@ant-design/icons';
21
+ import { useApiClient as useAPIClient } from '../../hooks/useApiRequest';
22
+ import { useT } from '../locale';
23
+
24
+ const { Text } = Typography;
25
+ const { useToken } = theme;
26
+
27
+ interface GitSkillImportProps {
28
+ open: boolean;
29
+ onClose: (synced?: boolean) => void;
30
+ }
31
+
32
+ export const GitSkillImport: React.FC<GitSkillImportProps> = ({ open, onClose }) => {
33
+ const t = useT();
34
+ const { token } = useToken();
35
+ const api = useAPIClient();
36
+
37
+ const [step, setStep] = useState(0);
38
+
39
+ // Step 0: Select repository
40
+ const [repos, setRepos] = useState<any[]>([]);
41
+ const [reposLoading, setReposLoading] = useState(false);
42
+ const [selectedRepoId, setSelectedRepoId] = useState<number | null>(null);
43
+
44
+ // Step 1: Browse & select root folder
45
+ const [branches, setBranches] = useState<string[]>([]);
46
+ const [, setCurrentBranch] = useState('');
47
+ const [selectedRef, setSelectedRef] = useState('HEAD');
48
+ const [folders, setFolders] = useState<any[]>([]);
49
+ const [foldersLoading, setFoldersLoading] = useState(false);
50
+ const [currentTreePath, setCurrentTreePath] = useState('');
51
+ const [selectedRootFolder, setSelectedRootFolder] = useState<string | null>(null);
52
+
53
+ // Step 2: Select skills to sync
54
+ const [skills, setSkills] = useState<any[]>([]);
55
+ const [skillsLoading, setSkillsLoading] = useState(false);
56
+ const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
57
+ const [overwrite, setOverwrite] = useState(false);
58
+ const [importPrefix, setImportPrefix] = useState('');
59
+ const [skillsConfigInfo, setSkillsConfigInfo] = useState<any>(null);
60
+ const [searchText, setSearchText] = useState('');
61
+
62
+ // Step 3: Sync results
63
+ const [syncResults, setSyncResults] = useState<any[] | null>(null);
64
+ const [syncing, setSyncing] = useState(false);
65
+
66
+ const loadRepos = useCallback(async () => {
67
+ setReposLoading(true);
68
+ try {
69
+ const { data } = await api.request({ url: 'gitRepositories:list', params: { pageSize: 100 } });
70
+ const responseData = data?.data?.data || data?.data || [];
71
+ setRepos(responseData.filter((r: any) => r.status === 'connected'));
72
+ } finally {
73
+ setReposLoading(false);
74
+ }
75
+ }, [api]);
76
+
77
+ useEffect(() => {
78
+ if (open) {
79
+ loadRepos();
80
+ setStep(0);
81
+ setSelectedRepoId(null);
82
+ setSelectedRootFolder(null);
83
+ setSkills([]);
84
+ setSelectedSkills([]);
85
+ setSkillsConfigInfo(null);
86
+ setSyncResults(null);
87
+ setOverwrite(false);
88
+ setCurrentTreePath('');
89
+ setImportPrefix('');
90
+ setSearchText('');
91
+ }
92
+ }, [open, loadRepos]);
93
+
94
+ useEffect(() => {
95
+ if (!selectedRepoId) return;
96
+ api
97
+ .request({ url: 'gitManager:branches', params: { repositoryId: selectedRepoId } })
98
+ .then(({ data }) => {
99
+ const responseData = data?.data?.data || data?.data;
100
+ setBranches(responseData?.all || []);
101
+ const cur = responseData?.current || 'main';
102
+ setCurrentBranch(cur);
103
+ setSelectedRef(cur);
104
+ })
105
+ .catch(() => setBranches([]));
106
+ }, [selectedRepoId, api]);
107
+
108
+ const loadFolders = useCallback(async () => {
109
+ if (!selectedRepoId) return;
110
+ setFoldersLoading(true);
111
+ try {
112
+ const { data } = await api.request({
113
+ url: 'gitManager:fileTree',
114
+ params: { repositoryId: selectedRepoId, ref: selectedRef, treePath: currentTreePath },
115
+ });
116
+ const responseData = data?.data?.data || data?.data || [];
117
+ setFolders(responseData.filter((f: any) => f.type === 'tree'));
118
+ } finally {
119
+ setFoldersLoading(false);
120
+ }
121
+ }, [api, selectedRepoId, selectedRef, currentTreePath]);
122
+
123
+ useEffect(() => {
124
+ if (step === 1 && selectedRepoId) {
125
+ loadFolders();
126
+ }
127
+ }, [step, selectedRepoId, selectedRef, currentTreePath, loadFolders]);
128
+
129
+ const loadSkills = useCallback(async () => {
130
+ if (!selectedRepoId || selectedRootFolder === null) return;
131
+ setSkillsLoading(true);
132
+ try {
133
+ const { data } = await api.request({
134
+ url: 'skillHub:gitListSkills',
135
+ params: {
136
+ repositoryId: selectedRepoId,
137
+ ref: selectedRef,
138
+ rootFolder: selectedRootFolder,
139
+ },
140
+ });
141
+ const list = data?.data?.data || data?.data || [];
142
+ setSkillsConfigInfo(data?.data?.config || data?.config || null);
143
+ setSkills(list);
144
+ setSelectedSkills(list.filter((s: any) => !s.existsInDb).map((s: any) => s.folder));
145
+ } catch (err: any) {
146
+ message.error(err?.response?.data?.errors?.[0]?.message || t('Failed to read skills from git'));
147
+ setSkillsConfigInfo(null);
148
+ setSkills([]);
149
+ } finally {
150
+ setSkillsLoading(false);
151
+ }
152
+ }, [api, selectedRepoId, selectedRef, selectedRootFolder, t]);
153
+
154
+ useEffect(() => {
155
+ if (step === 2 && selectedRootFolder !== null) {
156
+ loadSkills();
157
+ }
158
+ }, [step, selectedRootFolder, loadSkills]);
159
+
160
+ const filteredSkills = useMemo(() => {
161
+ if (!searchText.trim()) return skills;
162
+ const lower = searchText.toLowerCase();
163
+ return skills.filter(
164
+ (s) =>
165
+ (s.title || s.name || '').toLowerCase().includes(lower) || (s.description || '').toLowerCase().includes(lower),
166
+ );
167
+ }, [skills, searchText]);
168
+
169
+ const handleSync = async () => {
170
+ if (selectedSkills.length === 0) {
171
+ message.warning(t('Please select at least one skill'));
172
+ return;
173
+ }
174
+ setSyncing(true);
175
+ try {
176
+ const { data } = await api.request({
177
+ url: 'skillHub:gitSyncSkills',
178
+ method: 'POST',
179
+ params: {
180
+ repositoryId: selectedRepoId,
181
+ ref: selectedRef,
182
+ rootFolder: selectedRootFolder,
183
+ prefix: importPrefix,
184
+ },
185
+ data: { skills: selectedSkills, overwrite },
186
+ });
187
+ setSyncResults(data?.data?.data || data?.data || []);
188
+ setStep(3);
189
+ } catch (err: any) {
190
+ message.error(err?.response?.data?.errors?.[0]?.message || t('Sync failed'));
191
+ } finally {
192
+ setSyncing(false);
193
+ }
194
+ };
195
+
196
+ const canNext = () => {
197
+ if (step === 0) return !!selectedRepoId;
198
+ if (step === 1) return selectedRootFolder !== null;
199
+ if (step === 2) return selectedSkills.length > 0;
200
+ return false;
201
+ };
202
+
203
+ const skillColumns = [
204
+ {
205
+ title: t('Skill'),
206
+ key: 'name',
207
+ render: (_: any, record: any) => (
208
+ <div>
209
+ <Text strong>{record.title || record.name}</Text>
210
+ <div>
211
+ <Text type="secondary" style={{ fontSize: 12 }}>
212
+ {record.name}
213
+ </Text>
214
+ </div>
215
+ </div>
216
+ ),
217
+ },
218
+ {
219
+ title: t('Language'),
220
+ dataIndex: 'language',
221
+ key: 'language',
222
+ width: 100,
223
+ render: (lang: string) => <Tag color={lang === 'python' ? 'blue' : 'green'}>{lang}</Tag>,
224
+ },
225
+ {
226
+ title: t('Source'),
227
+ dataIndex: 'storageType',
228
+ key: 'storageType',
229
+ width: 100,
230
+ render: (storageType: string) => (
231
+ <Tag color={storageType === 'plugin' ? 'purple' : 'cyan'}>{(storageType || 'git').toUpperCase()}</Tag>
232
+ ),
233
+ },
234
+ {
235
+ title: t('Description'),
236
+ dataIndex: 'description',
237
+ key: 'description',
238
+ ellipsis: true,
239
+ },
240
+ {
241
+ title: t('Status'),
242
+ key: 'status',
243
+ width: 120,
244
+ render: (_: any, record: any) =>
245
+ record.existsInDb ? <Tag color="orange">{t('Exists')}</Tag> : <Tag color="green">{t('New')}</Tag>,
246
+ },
247
+ ];
248
+
249
+ const resultColumns = [
250
+ {
251
+ title: t('Skill'),
252
+ dataIndex: 'name',
253
+ key: 'name',
254
+ render: (name: string) => {
255
+ const prefix = importPrefix ? importPrefix.trim() : '';
256
+ const displayName = prefix ? `${prefix}${name}` : name;
257
+ return (
258
+ <div>
259
+ <Text strong>{displayName}</Text>
260
+ {prefix && (
261
+ <div>
262
+ <Text type="secondary" style={{ fontSize: 11 }}>
263
+ {t('Original')}: {name}
264
+ </Text>
265
+ </div>
266
+ )}
267
+ </div>
268
+ );
269
+ },
270
+ },
271
+ {
272
+ title: t('Result'),
273
+ key: 'status',
274
+ render: (_: any, record: any) => {
275
+ const colorMap: Record<string, string> = { created: 'green', updated: 'blue', skipped: 'orange', error: 'red' };
276
+ return (
277
+ <Space>
278
+ <Tag color={colorMap[record.status] || 'default'}>{record.status}</Tag>
279
+ {record.reason && (
280
+ <Text type="secondary" style={{ fontSize: 12 }}>
281
+ {record.reason}
282
+ </Text>
283
+ )}
284
+ </Space>
285
+ );
286
+ },
287
+ },
288
+ ];
289
+
290
+ return (
291
+ <Modal
292
+ open={open}
293
+ title={
294
+ <Space>
295
+ <BranchesOutlined />
296
+ {t('Import Skills from Git')}
297
+ </Space>
298
+ }
299
+ onCancel={() => onClose()}
300
+ width={800}
301
+ footer={
302
+ step === 3 ? (
303
+ <Button type="primary" onClick={() => onClose(true)}>
304
+ {t('Done')}
305
+ </Button>
306
+ ) : (
307
+ <Space>
308
+ {step > 0 && step < 3 && <Button onClick={() => setStep(step - 1)}>{t('Back')}</Button>}
309
+ {step < 2 && (
310
+ <Button type="primary" disabled={!canNext()} onClick={() => setStep(step + 1)}>
311
+ {t('Next')}
312
+ </Button>
313
+ )}
314
+ {step === 2 && (
315
+ <Button type="primary" loading={syncing} disabled={selectedSkills.length === 0} onClick={handleSync}>
316
+ <SyncOutlined /> {t('Sync Selected')} ({selectedSkills.length})
317
+ </Button>
318
+ )}
319
+ </Space>
320
+ )
321
+ }
322
+ destroyOnClose
323
+ >
324
+ <Steps
325
+ current={step}
326
+ size="small"
327
+ style={{ marginBottom: 24 }}
328
+ items={[
329
+ { title: t('Repository') },
330
+ { title: t('Root Folder') },
331
+ { title: t('Select Skills') },
332
+ { title: t('Result') },
333
+ ]}
334
+ />
335
+
336
+ {step === 0 && (
337
+ <div>
338
+ {reposLoading ? (
339
+ <Spin />
340
+ ) : repos.length === 0 ? (
341
+ <Empty description={t('No connected repositories. Please configure one in Git Manager first.')} />
342
+ ) : (
343
+ <div>
344
+ <Text strong style={{ display: 'block', marginBottom: 8 }}>
345
+ <DatabaseOutlined /> {t('Select a connected git repository')}
346
+ </Text>
347
+ <Select
348
+ style={{ width: '100%' }}
349
+ placeholder={t('Select repository')}
350
+ value={selectedRepoId}
351
+ onChange={setSelectedRepoId}
352
+ options={repos.map((r) => ({
353
+ label: (
354
+ <Space>
355
+ {r.name}
356
+ <Text type="secondary" style={{ fontSize: 12 }}>
357
+ {r.repoUrl}
358
+ </Text>
359
+ </Space>
360
+ ),
361
+ value: r.id,
362
+ }))}
363
+ />
364
+ </div>
365
+ )}
366
+ </div>
367
+ )}
368
+
369
+ {step === 1 && (
370
+ <div>
371
+ <div style={{ marginBottom: 12, display: 'flex', gap: 12, alignItems: 'center' }}>
372
+ <BranchesOutlined />
373
+ <Select
374
+ size="small"
375
+ style={{ width: 200 }}
376
+ value={selectedRef}
377
+ onChange={(val) => {
378
+ setSelectedRef(val);
379
+ setSelectedRootFolder(null);
380
+ }}
381
+ options={branches.map((b) => ({ label: b, value: b }))}
382
+ />
383
+ </div>
384
+
385
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
386
+ <Text strong>
387
+ <FolderOutlined /> {t('Current Path')}: {currentTreePath || '/ (Root)'}
388
+ </Text>
389
+ <Button type="primary" onClick={() => setSelectedRootFolder(currentTreePath)}>
390
+ {t('Use Current Folder')}
391
+ </Button>
392
+ </div>
393
+
394
+ {foldersLoading ? (
395
+ <Spin />
396
+ ) : (
397
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
398
+ {currentTreePath && (
399
+ <Button
400
+ icon={<FolderOutlined />}
401
+ onClick={() => {
402
+ const parts = currentTreePath.split('/');
403
+ parts.pop();
404
+ setCurrentTreePath(parts.join('/'));
405
+ }}
406
+ >
407
+ ..
408
+ </Button>
409
+ )}
410
+ {folders.map((f) => (
411
+ <Button
412
+ key={f.path}
413
+ icon={<FolderOutlined />}
414
+ onClick={() => setCurrentTreePath(f.path)}
415
+ style={{ height: 'auto', padding: '8px 16px' }}
416
+ >
417
+ {f.name}
418
+ </Button>
419
+ ))}
420
+ {folders.length === 0 && !currentTreePath && <Empty description={t('No folders found')} />}
421
+ </div>
422
+ )}
423
+
424
+ {selectedRootFolder !== null && (
425
+ <Alert
426
+ style={{ marginTop: 12 }}
427
+ type="success"
428
+ message={`${t('Selected Root Folder')}: ${selectedRootFolder || '/'}`}
429
+ showIcon
430
+ />
431
+ )}
432
+ </div>
433
+ )}
434
+
435
+ {step === 2 && (
436
+ <div>
437
+ {skillsConfigInfo?.initializedSkillsJson && (
438
+ <Alert
439
+ style={{ marginBottom: 12 }}
440
+ type="info"
441
+ showIcon
442
+ message={`${t('Initialized skills.json')}: ${skillsConfigInfo.path || 'skills.json'}`}
443
+ />
444
+ )}
445
+ {skillsLoading ? (
446
+ <Spin />
447
+ ) : skills.length === 0 ? (
448
+ <Empty description={t('No skills found')} />
449
+ ) : (
450
+ <div>
451
+ <div
452
+ style={{
453
+ marginBottom: 12,
454
+ display: 'flex',
455
+ justifyContent: 'space-between',
456
+ alignItems: 'center',
457
+ gap: 12,
458
+ flexWrap: 'wrap',
459
+ }}
460
+ >
461
+ <Input
462
+ placeholder={t('Search by title or description...')}
463
+ prefix={<SearchOutlined style={{ color: token.colorTextQuaternary }} />}
464
+ value={searchText}
465
+ onChange={(e) => setSearchText(e.target.value)}
466
+ allowClear
467
+ style={{ width: 260 }}
468
+ />
469
+ <Space>
470
+ <Input
471
+ size="small"
472
+ placeholder={t('Skill Name Prefix')}
473
+ value={importPrefix}
474
+ onChange={(e) => setImportPrefix(e.target.value)}
475
+ style={{ width: 150 }}
476
+ />
477
+ <Text style={{ fontSize: 12, marginLeft: 8 }}>{t('Overwrite')}</Text>
478
+ <Switch size="small" checked={overwrite} onChange={setOverwrite} />
479
+ </Space>
480
+ </div>
481
+
482
+ <div style={{ marginBottom: 8 }}>
483
+ <Text type="secondary" style={{ fontSize: 12 }}>
484
+ {filteredSkills.length} / {skills.length} {t('skills found')}
485
+ {importPrefix && (
486
+ <span style={{ marginLeft: 8 }}>
487
+ • {t('Prefix')}: <Tag style={{ margin: 0 }}>{importPrefix}</Tag>{' '}
488
+ <Text type="secondary" style={{ fontSize: 11 }}>
489
+ ({t('applied on sync')})
490
+ </Text>
491
+ </span>
492
+ )}
493
+ </Text>
494
+ </div>
495
+
496
+ <Table
497
+ dataSource={filteredSkills}
498
+ columns={skillColumns}
499
+ rowKey="folder"
500
+ size="small"
501
+ pagination={{
502
+ pageSize: 10,
503
+ size: 'small',
504
+ showSizeChanger: false,
505
+ showTotal: (total, range) => `${range[0]}-${range[1]} / ${total}`,
506
+ }}
507
+ rowSelection={{
508
+ selectedRowKeys: selectedSkills,
509
+ onChange: (keys) => setSelectedSkills(keys as string[]),
510
+ }}
511
+ scroll={{ x: 'max-content' }}
512
+ />
513
+ </div>
514
+ )}
515
+ </div>
516
+ )}
517
+
518
+ {step === 3 && syncResults && (
519
+ <div>
520
+ <Result
521
+ status="success"
522
+ title={t('Sync Complete')}
523
+ subTitle={`${syncResults.filter((r) => r.status === 'created').length} ${t('created')}, ${
524
+ syncResults.filter((r) => r.status === 'updated').length
525
+ } ${t('updated')}, ${syncResults.filter((r) => r.status === 'skipped').length} ${t('skipped')}`}
526
+ />
527
+ <Table
528
+ dataSource={syncResults}
529
+ columns={resultColumns}
530
+ rowKey="folder"
531
+ size="small"
532
+ pagination={false}
533
+ scroll={{ x: 'max-content' }}
534
+ />
535
+ </div>
536
+ )}
537
+ </Modal>
538
+ );
539
+ };