plugin-agent-orchestrator 1.0.20 → 1.0.22

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 (158) hide show
  1. package/dist/client/index.js +1 -1
  2. package/dist/externalVersion.js +6 -6
  3. package/dist/server/collections/agent-execution-spans.js +24 -0
  4. package/dist/server/collections/agent-loop-runs.js +36 -0
  5. package/dist/server/collections/orchestrator-config.js +14 -0
  6. package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
  7. package/dist/server/plugin.js +56 -0
  8. package/dist/server/resources/agent-loop.js +33 -25
  9. package/dist/server/resources/tracing.js +5 -8
  10. package/dist/server/services/AgentHarness.js +56 -90
  11. package/dist/server/services/AgentLoopController.js +164 -125
  12. package/dist/server/services/AgentLoopRepository.js +16 -34
  13. package/dist/server/services/AgentLoopService.js +7 -1
  14. package/dist/server/services/AgentPlannerService.js +5 -25
  15. package/dist/server/services/AgentRegistryService.js +34 -24
  16. package/dist/server/services/CircuitBreaker.js +120 -0
  17. package/dist/server/services/ContextAggregator.js +201 -0
  18. package/dist/server/services/ExecutionSpanService.js +2 -5
  19. package/dist/server/services/RunEventBus.js +73 -0
  20. package/dist/server/services/TokenTracker.js +173 -0
  21. package/dist/server/tools/agent-loop.js +30 -63
  22. package/dist/server/tools/delegate-task.js +14 -72
  23. package/dist/server/tools/orchestrator-plan.js +10 -47
  24. package/dist/server/types.js +24 -0
  25. package/dist/server/utils/ctx-utils.js +152 -0
  26. package/dist/server/utils/logging.js +86 -0
  27. package/package.json +44 -44
  28. package/src/client/AgentRunsTab.tsx +764 -764
  29. package/src/client/HarnessProfilesTab.tsx +247 -247
  30. package/src/client/OrchestratorSettings.tsx +106 -106
  31. package/src/client/RulesTab.tsx +716 -716
  32. package/src/client/hooks/useRunEventStream.ts +76 -0
  33. package/src/client/index.tsx +2 -1
  34. package/src/client/plugin.tsx +27 -27
  35. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  36. package/src/client/skill-hub/index.tsx +51 -51
  37. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
  38. package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
  39. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  40. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
  41. package/src/client/tools/PlanApprovalCard.tsx +175 -175
  42. package/src/client/tools/registerOrchestratorCards.ts +7 -7
  43. package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
  44. package/src/server/__tests__/circuit-breaker.test.ts +169 -0
  45. package/src/server/__tests__/context-aggregator.test.ts +222 -0
  46. package/src/server/__tests__/parallel-execution.test.ts +318 -0
  47. package/src/server/__tests__/smoke.test.ts +120 -0
  48. package/src/server/collections/agent-execution-spans.ts +24 -0
  49. package/src/server/collections/agent-harness-profiles.ts +59 -59
  50. package/src/server/collections/agent-loop-events.ts +71 -71
  51. package/src/server/collections/agent-loop-runs.ts +38 -1
  52. package/src/server/collections/agent-loop-steps.ts +144 -144
  53. package/src/server/collections/orchestrator-config.ts +14 -0
  54. package/src/server/collections/skill-executions.ts +106 -106
  55. package/src/server/collections/skill-loop-configs.ts +65 -65
  56. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  57. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
  58. package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
  59. package/src/server/plugin.ts +68 -0
  60. package/src/server/resources/agent-loop.ts +21 -12
  61. package/src/server/resources/tracing.ts +3 -7
  62. package/src/server/services/AgentHarness.ts +78 -116
  63. package/src/server/services/AgentLoopController.ts +197 -122
  64. package/src/server/services/AgentLoopRepository.ts +9 -25
  65. package/src/server/services/AgentLoopService.ts +13 -1
  66. package/src/server/services/AgentPlanValidator.ts +73 -73
  67. package/src/server/services/AgentPlannerService.ts +2 -25
  68. package/src/server/services/AgentRegistryService.ts +40 -31
  69. package/src/server/services/CircuitBreaker.ts +116 -0
  70. package/src/server/services/ContextAggregator.ts +239 -0
  71. package/src/server/services/ExecutionSpanService.ts +2 -4
  72. package/src/server/services/RunEventBus.ts +45 -0
  73. package/src/server/services/TokenTracker.ts +209 -0
  74. package/src/server/skill-hub/plugin.ts +898 -898
  75. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -460
  76. package/src/server/tools/agent-loop.ts +18 -57
  77. package/src/server/tools/delegate-task.ts +11 -93
  78. package/src/server/tools/orchestrator-plan.ts +26 -50
  79. package/src/server/tools/skill-execute.ts +160 -160
  80. package/src/server/types.ts +55 -0
  81. package/src/server/utils/ctx-utils.ts +118 -0
  82. package/src/server/utils/logging.ts +63 -0
  83. package/dist/client/AIEmployeeSelect.d.ts +0 -11
  84. package/dist/client/AIEmployeesContext.d.ts +0 -30
  85. package/dist/client/AgentRunsTab.d.ts +0 -2
  86. package/dist/client/HarnessProfilesTab.d.ts +0 -2
  87. package/dist/client/OrchestratorSettings.d.ts +0 -3
  88. package/dist/client/RulesTab.d.ts +0 -2
  89. package/dist/client/TracingTab.d.ts +0 -2
  90. package/dist/client/index.d.ts +0 -1
  91. package/dist/client/plugin.d.ts +0 -6
  92. package/dist/client/skill-hub/components/ExecutionHistory.d.ts +0 -2
  93. package/dist/client/skill-hub/components/ExecutionProgress.d.ts +0 -20
  94. package/dist/client/skill-hub/components/GitSkillImport.d.ts +0 -7
  95. package/dist/client/skill-hub/components/LoopSettings.d.ts +0 -2
  96. package/dist/client/skill-hub/components/SkillEditor.d.ts +0 -7
  97. package/dist/client/skill-hub/components/SkillManager.d.ts +0 -2
  98. package/dist/client/skill-hub/components/SkillMetrics.d.ts +0 -2
  99. package/dist/client/skill-hub/components/SkillTestPanel.d.ts +0 -7
  100. package/dist/client/skill-hub/index.d.ts +0 -11
  101. package/dist/client/skill-hub/locale.d.ts +0 -3
  102. package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +0 -6
  103. package/dist/client/skill-hub/tools/SkillHubCard.d.ts +0 -3
  104. package/dist/client/skill-hub/tools/loopTemplates.d.ts +0 -22
  105. package/dist/client/skill-hub/tools/registerSkillLoopCards.d.ts +0 -1
  106. package/dist/client/skill-hub/utils/jsonFields.d.ts +0 -3
  107. package/dist/client/tools/PlanApprovalCard.d.ts +0 -3
  108. package/dist/client/tools/registerOrchestratorCards.d.ts +0 -1
  109. package/dist/index.d.ts +0 -2
  110. package/dist/server/collections/agent-execution-spans.d.ts +0 -9
  111. package/dist/server/collections/agent-harness-profiles.d.ts +0 -2
  112. package/dist/server/collections/agent-loop-events.d.ts +0 -2
  113. package/dist/server/collections/agent-loop-runs.d.ts +0 -2
  114. package/dist/server/collections/agent-loop-steps.d.ts +0 -2
  115. package/dist/server/collections/orchestrator-config.d.ts +0 -2
  116. package/dist/server/collections/orchestrator-logs.d.ts +0 -8
  117. package/dist/server/collections/skill-definitions.d.ts +0 -3
  118. package/dist/server/collections/skill-executions.d.ts +0 -3
  119. package/dist/server/collections/skill-loop-configs.d.ts +0 -3
  120. package/dist/server/collections/skill-worker-configs.d.ts +0 -3
  121. package/dist/server/index.d.ts +0 -1
  122. package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +0 -4
  123. package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +0 -4
  124. package/dist/server/migrations/20260427000000-add-tracing-detail-fields.d.ts +0 -7
  125. package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +0 -4
  126. package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +0 -4
  127. package/dist/server/migrations/20260429000000-add-llm-fields.d.ts +0 -7
  128. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +0 -16
  129. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +0 -7
  130. package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.d.ts +0 -7
  131. package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.d.ts +0 -12
  132. package/dist/server/plugin.d.ts +0 -16
  133. package/dist/server/resources/agent-loop.d.ts +0 -3
  134. package/dist/server/resources/tracing.d.ts +0 -7
  135. package/dist/server/services/AgentHarness.d.ts +0 -42
  136. package/dist/server/services/AgentLoopController.d.ts +0 -205
  137. package/dist/server/services/AgentLoopRepository.d.ts +0 -20
  138. package/dist/server/services/AgentLoopService.d.ts +0 -149
  139. package/dist/server/services/AgentPlanValidator.d.ts +0 -4
  140. package/dist/server/services/AgentPlannerService.d.ts +0 -8
  141. package/dist/server/services/AgentRegistryService.d.ts +0 -13
  142. package/dist/server/services/CodeValidator.d.ts +0 -32
  143. package/dist/server/services/ExecutionSpanService.d.ts +0 -46
  144. package/dist/server/services/FileManager.d.ts +0 -28
  145. package/dist/server/services/SandboxRunner.d.ts +0 -41
  146. package/dist/server/services/SkillManager.d.ts +0 -6
  147. package/dist/server/services/SkillRepositoryService.d.ts +0 -22
  148. package/dist/server/services/WorkerEnvManager.d.ts +0 -26
  149. package/dist/server/skill-hub/actions/git-import.d.ts +0 -21
  150. package/dist/server/skill-hub/mcp/McpController.d.ts +0 -15
  151. package/dist/server/skill-hub/plugin.d.ts +0 -61
  152. package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +0 -16
  153. package/dist/server/skill-hub/utils/json-fields.d.ts +0 -7
  154. package/dist/server/tools/agent-loop.d.ts +0 -235
  155. package/dist/server/tools/delegate-task.d.ts +0 -19
  156. package/dist/server/tools/external-rag-search.d.ts +0 -42
  157. package/dist/server/tools/orchestrator-plan.d.ts +0 -205
  158. package/dist/server/tools/skill-execute.d.ts +0 -36
@@ -1,247 +1,247 @@
1
- import React from 'react';
2
- import { Button, Card, Drawer, Form, Input, Popconfirm, Space, Switch, Table, Tag, Typography, message } from 'antd';
3
- import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
4
- import { useAPIClient, useRequest } from '@nocobase/client';
5
-
6
- const { Text } = Typography;
7
-
8
- const parseSettings = (value: string) => {
9
- const text = String(value || '').trim();
10
- if (!text) return {};
11
- return JSON.parse(text);
12
- };
13
-
14
- export const HarnessProfilesTab: React.FC = () => {
15
- const api = useAPIClient();
16
- const [open, setOpen] = React.useState(false);
17
- const [editingRecord, setEditingRecord] = React.useState<any>(null);
18
- const [form] = Form.useForm();
19
-
20
- const { data, loading, refresh } = useRequest({
21
- url: 'agentHarnessProfiles:list',
22
- params: {
23
- sort: ['tag'],
24
- pageSize: 100,
25
- },
26
- });
27
-
28
- const rows = React.useMemo(() => {
29
- const raw = (data as any)?.data;
30
- return Array.isArray(raw) ? raw : [];
31
- }, [data]);
32
-
33
- const openDrawer = (record?: any) => {
34
- setEditingRecord(record || null);
35
- form.resetFields();
36
- form.setFieldsValue(
37
- record
38
- ? {
39
- ...record,
40
- settingsText: JSON.stringify(record.settings || {}, null, 2),
41
- }
42
- : {
43
- tag: '',
44
- title: '',
45
- description: '',
46
- enabled: true,
47
- settingsText: JSON.stringify(
48
- {
49
- requirePlanApproval: true,
50
- allowSubAgents: true,
51
- allowToolCalls: true,
52
- maxParallelSubAgents: 3,
53
- maxControllerSteps: 100,
54
- },
55
- null,
56
- 2,
57
- ),
58
- },
59
- );
60
- setOpen(true);
61
- };
62
-
63
- const closeDrawer = () => {
64
- setOpen(false);
65
- setEditingRecord(null);
66
- };
67
-
68
- const saveProfile = async (values: any) => {
69
- let settings: any;
70
- try {
71
- settings = parseSettings(values.settingsText);
72
- } catch (error: any) {
73
- message.error(`Settings JSON is invalid: ${error?.message || error}`);
74
- return;
75
- }
76
-
77
- const payload = {
78
- tag: String(values.tag || '').trim(),
79
- title: values.title,
80
- description: values.description,
81
- enabled: values.enabled !== false,
82
- settings,
83
- };
84
-
85
- try {
86
- if (editingRecord) {
87
- await api.request({
88
- url: 'agentHarnessProfiles:update',
89
- method: 'put',
90
- params: { filterByTk: editingRecord.id },
91
- data: payload,
92
- });
93
- message.success('Harness profile updated');
94
- } else {
95
- await api.request({
96
- url: 'agentHarnessProfiles:create',
97
- method: 'post',
98
- data: payload,
99
- });
100
- message.success('Harness profile created');
101
- }
102
- closeDrawer();
103
- refresh();
104
- } catch (error: any) {
105
- const msg = error?.response?.data?.errors?.[0]?.message || error?.message || 'unknown error';
106
- message.error(`Save failed: ${msg}`);
107
- }
108
- };
109
-
110
- const deleteProfile = async (id: string | number) => {
111
- try {
112
- await api.request({
113
- url: 'agentHarnessProfiles:destroy',
114
- method: 'delete',
115
- params: { filterByTk: id },
116
- });
117
- message.success('Harness profile deleted');
118
- refresh();
119
- } catch (error: any) {
120
- message.error(`Delete failed: ${error?.message || 'unknown error'}`);
121
- }
122
- };
123
-
124
- const columns = [
125
- {
126
- title: 'Tag',
127
- dataIndex: 'tag',
128
- key: 'tag',
129
- width: 140,
130
- render: (tag: string) => <Tag color="blue">{tag}</Tag>,
131
- },
132
- {
133
- title: 'Title',
134
- dataIndex: 'title',
135
- key: 'title',
136
- render: (title: string, record: any) => title || record.tag,
137
- },
138
- {
139
- title: 'Enabled',
140
- dataIndex: 'enabled',
141
- key: 'enabled',
142
- width: 90,
143
- render: (enabled: boolean, record: any) => (
144
- <Switch
145
- size="small"
146
- checked={enabled !== false}
147
- onChange={async (checked) => {
148
- await api.request({
149
- url: 'agentHarnessProfiles:update',
150
- method: 'put',
151
- params: { filterByTk: record.id },
152
- data: { enabled: checked },
153
- });
154
- refresh();
155
- }}
156
- />
157
- ),
158
- },
159
- {
160
- title: 'Settings',
161
- key: 'settings',
162
- render: (_: any, record: any) => (
163
- <Space size={4} wrap>
164
- {Object.entries(record.settings || {})
165
- .slice(0, 5)
166
- .map(([key, value]) => (
167
- <Tag key={key}>
168
- {key}: {String(value)}
169
- </Tag>
170
- ))}
171
- </Space>
172
- ),
173
- },
174
- {
175
- title: 'Actions',
176
- key: 'actions',
177
- width: 150,
178
- render: (_: any, record: any) => (
179
- <Space>
180
- <Button type="link" size="small" icon={<EditOutlined />} onClick={() => openDrawer(record)}>
181
- Edit
182
- </Button>
183
- <Popconfirm title="Delete this profile?" onConfirm={() => deleteProfile(record.id)}>
184
- <Button type="link" size="small" danger icon={<DeleteOutlined />}>
185
- Delete
186
- </Button>
187
- </Popconfirm>
188
- </Space>
189
- ),
190
- },
191
- ];
192
-
193
- return (
194
- <div>
195
- <Card bordered={false}>
196
- <Space direction="vertical" size={16} style={{ width: '100%' }}>
197
- <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
198
- <Text type="secondary">
199
- Harness profiles are selected by orchestration rules through the harnessTag field.
200
- </Text>
201
- <Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
202
- New Profile
203
- </Button>
204
- </div>
205
- <Table rowKey="id" loading={loading} dataSource={rows} columns={columns} pagination={false} />
206
- </Space>
207
- </Card>
208
-
209
- <Drawer
210
- title={editingRecord ? 'Edit Harness Profile' : 'New Harness Profile'}
211
- width={560}
212
- open={open}
213
- onClose={closeDrawer}
214
- extra={
215
- <Space>
216
- <Button onClick={closeDrawer}>Cancel</Button>
217
- <Button type="primary" onClick={() => form.submit()}>
218
- Save
219
- </Button>
220
- </Space>
221
- }
222
- >
223
- <Form form={form} layout="vertical" onFinish={saveProfile}>
224
- <Form.Item name="tag" label="Tag" rules={[{ required: true, message: 'Tag is required' }]}>
225
- <Input placeholder="default" disabled={editingRecord?.tag === 'default'} />
226
- </Form.Item>
227
- <Form.Item name="title" label="Title">
228
- <Input />
229
- </Form.Item>
230
- <Form.Item name="description" label="Description">
231
- <Input.TextArea rows={3} />
232
- </Form.Item>
233
- <Form.Item
234
- name="settingsText"
235
- label="Settings JSON"
236
- rules={[{ required: true, message: 'Settings JSON is required' }]}
237
- >
238
- <Input.TextArea rows={12} spellCheck={false} />
239
- </Form.Item>
240
- <Form.Item name="enabled" label="Enabled" valuePropName="checked">
241
- <Switch />
242
- </Form.Item>
243
- </Form>
244
- </Drawer>
245
- </div>
246
- );
247
- };
1
+ import React from 'react';
2
+ import { Button, Card, Drawer, Form, Input, Popconfirm, Space, Switch, Table, Tag, Typography, message } from 'antd';
3
+ import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
4
+ import { useAPIClient, useRequest } from '@nocobase/client';
5
+
6
+ const { Text } = Typography;
7
+
8
+ const parseSettings = (value: string) => {
9
+ const text = String(value || '').trim();
10
+ if (!text) return {};
11
+ return JSON.parse(text);
12
+ };
13
+
14
+ export const HarnessProfilesTab: React.FC = () => {
15
+ const api = useAPIClient();
16
+ const [open, setOpen] = React.useState(false);
17
+ const [editingRecord, setEditingRecord] = React.useState<any>(null);
18
+ const [form] = Form.useForm();
19
+
20
+ const { data, loading, refresh } = useRequest({
21
+ url: 'agentHarnessProfiles:list',
22
+ params: {
23
+ sort: ['tag'],
24
+ pageSize: 100,
25
+ },
26
+ });
27
+
28
+ const rows = React.useMemo(() => {
29
+ const raw = (data as any)?.data;
30
+ return Array.isArray(raw) ? raw : [];
31
+ }, [data]);
32
+
33
+ const openDrawer = (record?: any) => {
34
+ setEditingRecord(record || null);
35
+ form.resetFields();
36
+ form.setFieldsValue(
37
+ record
38
+ ? {
39
+ ...record,
40
+ settingsText: JSON.stringify(record.settings || {}, null, 2),
41
+ }
42
+ : {
43
+ tag: '',
44
+ title: '',
45
+ description: '',
46
+ enabled: true,
47
+ settingsText: JSON.stringify(
48
+ {
49
+ requirePlanApproval: true,
50
+ allowSubAgents: true,
51
+ allowToolCalls: true,
52
+ maxParallelSubAgents: 3,
53
+ maxControllerSteps: 100,
54
+ },
55
+ null,
56
+ 2,
57
+ ),
58
+ },
59
+ );
60
+ setOpen(true);
61
+ };
62
+
63
+ const closeDrawer = () => {
64
+ setOpen(false);
65
+ setEditingRecord(null);
66
+ };
67
+
68
+ const saveProfile = async (values: any) => {
69
+ let settings: any;
70
+ try {
71
+ settings = parseSettings(values.settingsText);
72
+ } catch (error: any) {
73
+ message.error(`Settings JSON is invalid: ${error?.message || error}`);
74
+ return;
75
+ }
76
+
77
+ const payload = {
78
+ tag: String(values.tag || '').trim(),
79
+ title: values.title,
80
+ description: values.description,
81
+ enabled: values.enabled !== false,
82
+ settings,
83
+ };
84
+
85
+ try {
86
+ if (editingRecord) {
87
+ await api.request({
88
+ url: 'agentHarnessProfiles:update',
89
+ method: 'put',
90
+ params: { filterByTk: editingRecord.id },
91
+ data: payload,
92
+ });
93
+ message.success('Harness profile updated');
94
+ } else {
95
+ await api.request({
96
+ url: 'agentHarnessProfiles:create',
97
+ method: 'post',
98
+ data: payload,
99
+ });
100
+ message.success('Harness profile created');
101
+ }
102
+ closeDrawer();
103
+ refresh();
104
+ } catch (error: any) {
105
+ const msg = error?.response?.data?.errors?.[0]?.message || error?.message || 'unknown error';
106
+ message.error(`Save failed: ${msg}`);
107
+ }
108
+ };
109
+
110
+ const deleteProfile = async (id: string | number) => {
111
+ try {
112
+ await api.request({
113
+ url: 'agentHarnessProfiles:destroy',
114
+ method: 'delete',
115
+ params: { filterByTk: id },
116
+ });
117
+ message.success('Harness profile deleted');
118
+ refresh();
119
+ } catch (error: any) {
120
+ message.error(`Delete failed: ${error?.message || 'unknown error'}`);
121
+ }
122
+ };
123
+
124
+ const columns = [
125
+ {
126
+ title: 'Tag',
127
+ dataIndex: 'tag',
128
+ key: 'tag',
129
+ width: 140,
130
+ render: (tag: string) => <Tag color="blue">{tag}</Tag>,
131
+ },
132
+ {
133
+ title: 'Title',
134
+ dataIndex: 'title',
135
+ key: 'title',
136
+ render: (title: string, record: any) => title || record.tag,
137
+ },
138
+ {
139
+ title: 'Enabled',
140
+ dataIndex: 'enabled',
141
+ key: 'enabled',
142
+ width: 90,
143
+ render: (enabled: boolean, record: any) => (
144
+ <Switch
145
+ size="small"
146
+ checked={enabled !== false}
147
+ onChange={async (checked) => {
148
+ await api.request({
149
+ url: 'agentHarnessProfiles:update',
150
+ method: 'put',
151
+ params: { filterByTk: record.id },
152
+ data: { enabled: checked },
153
+ });
154
+ refresh();
155
+ }}
156
+ />
157
+ ),
158
+ },
159
+ {
160
+ title: 'Settings',
161
+ key: 'settings',
162
+ render: (_: any, record: any) => (
163
+ <Space size={4} wrap>
164
+ {Object.entries(record.settings || {})
165
+ .slice(0, 5)
166
+ .map(([key, value]) => (
167
+ <Tag key={key}>
168
+ {key}: {String(value)}
169
+ </Tag>
170
+ ))}
171
+ </Space>
172
+ ),
173
+ },
174
+ {
175
+ title: 'Actions',
176
+ key: 'actions',
177
+ width: 150,
178
+ render: (_: any, record: any) => (
179
+ <Space>
180
+ <Button type="link" size="small" icon={<EditOutlined />} onClick={() => openDrawer(record)}>
181
+ Edit
182
+ </Button>
183
+ <Popconfirm title="Delete this profile?" onConfirm={() => deleteProfile(record.id)}>
184
+ <Button type="link" size="small" danger icon={<DeleteOutlined />}>
185
+ Delete
186
+ </Button>
187
+ </Popconfirm>
188
+ </Space>
189
+ ),
190
+ },
191
+ ];
192
+
193
+ return (
194
+ <div>
195
+ <Card bordered={false}>
196
+ <Space direction="vertical" size={16} style={{ width: '100%' }}>
197
+ <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
198
+ <Text type="secondary">
199
+ Harness profiles are selected by orchestration rules through the harnessTag field.
200
+ </Text>
201
+ <Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
202
+ New Profile
203
+ </Button>
204
+ </div>
205
+ <Table rowKey="id" loading={loading} dataSource={rows} columns={columns} pagination={false} />
206
+ </Space>
207
+ </Card>
208
+
209
+ <Drawer
210
+ title={editingRecord ? 'Edit Harness Profile' : 'New Harness Profile'}
211
+ width={560}
212
+ open={open}
213
+ onClose={closeDrawer}
214
+ extra={
215
+ <Space>
216
+ <Button onClick={closeDrawer}>Cancel</Button>
217
+ <Button type="primary" onClick={() => form.submit()}>
218
+ Save
219
+ </Button>
220
+ </Space>
221
+ }
222
+ >
223
+ <Form form={form} layout="vertical" onFinish={saveProfile}>
224
+ <Form.Item name="tag" label="Tag" rules={[{ required: true, message: 'Tag is required' }]}>
225
+ <Input placeholder="default" disabled={editingRecord?.tag === 'default'} />
226
+ </Form.Item>
227
+ <Form.Item name="title" label="Title">
228
+ <Input />
229
+ </Form.Item>
230
+ <Form.Item name="description" label="Description">
231
+ <Input.TextArea rows={3} />
232
+ </Form.Item>
233
+ <Form.Item
234
+ name="settingsText"
235
+ label="Settings JSON"
236
+ rules={[{ required: true, message: 'Settings JSON is required' }]}
237
+ >
238
+ <Input.TextArea rows={12} spellCheck={false} />
239
+ </Form.Item>
240
+ <Form.Item name="enabled" label="Enabled" valuePropName="checked">
241
+ <Switch />
242
+ </Form.Item>
243
+ </Form>
244
+ </Drawer>
245
+ </div>
246
+ );
247
+ };