@bamptee/aia-code 2.0.12 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +408 -34
- package/package.json +11 -2
- package/src/constants.js +24 -0
- package/src/providers/anthropic.js +2 -21
- package/src/providers/cli-runner.js +5 -3
- package/src/services/agent-sessions.js +110 -0
- package/src/services/apps.js +132 -0
- package/src/services/config.js +41 -0
- package/src/services/feature.js +28 -7
- package/src/services/model-call.js +2 -2
- package/src/services/runner.js +23 -1
- package/src/services/status.js +69 -1
- package/src/services/test-quick.js +229 -0
- package/src/services/worktrunk.js +135 -21
- package/src/types/test-quick.js +88 -0
- package/src/ui/api/config.js +28 -1
- package/src/ui/api/features.js +160 -50
- package/src/ui/api/index.js +2 -0
- package/src/ui/api/test-quick.js +207 -0
- package/src/ui/api/worktrunk.js +63 -25
- package/src/ui/public/components/config-view.js +95 -0
- package/src/ui/public/components/dashboard.js +823 -163
- package/src/ui/public/components/feature-detail.js +517 -124
- package/src/ui/public/components/test-quick.js +276 -0
- package/src/ui/public/components/worktrunk-panel.js +187 -25
- package/src/ui/public/index.html +13 -0
- package/src/ui/public/main.js +5 -1
- package/src/ui/server.js +97 -67
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview API routes for test-quick feature
|
|
3
|
+
* Provides REST endpoints for managing test-quick items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { json, error } from '../router.js';
|
|
7
|
+
import {
|
|
8
|
+
loadItems,
|
|
9
|
+
createItem,
|
|
10
|
+
getItem,
|
|
11
|
+
listItems,
|
|
12
|
+
updateItem,
|
|
13
|
+
deleteItem,
|
|
14
|
+
updateStatus,
|
|
15
|
+
clearCompleted,
|
|
16
|
+
} from '../../services/test-quick.js';
|
|
17
|
+
import { TEST_QUICK_STATUS, isValidTestQuickName } from '../../types/test-quick.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registers test-quick API routes
|
|
21
|
+
* @param {Object} router - Router instance
|
|
22
|
+
*/
|
|
23
|
+
export function registerTestQuickRoutes(router) {
|
|
24
|
+
/**
|
|
25
|
+
* GET /api/test-quick/statuses
|
|
26
|
+
* Get available status values
|
|
27
|
+
* Note: Must be registered before :name route to avoid conflict
|
|
28
|
+
*/
|
|
29
|
+
router.get('/api/test-quick/statuses', async (req, res) => {
|
|
30
|
+
json(res, { statuses: Object.values(TEST_QUICK_STATUS) });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* GET /api/test-quick
|
|
35
|
+
* List all test-quick items with optional status filter
|
|
36
|
+
*/
|
|
37
|
+
router.get('/api/test-quick', async (req, res, { root, query }) => {
|
|
38
|
+
try {
|
|
39
|
+
const options = {};
|
|
40
|
+
if (query?.status) {
|
|
41
|
+
options.status = query.status;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await listItems(options, root);
|
|
45
|
+
json(res, result);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
error(res, err.message, 500);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GET /api/test-quick/:name
|
|
53
|
+
* Get a single test-quick item by name
|
|
54
|
+
*/
|
|
55
|
+
router.get('/api/test-quick/:name', async (req, res, { params, root }) => {
|
|
56
|
+
try {
|
|
57
|
+
const result = await getItem(params.name, root);
|
|
58
|
+
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
return error(res, result.error, 404);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
json(res, result);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
error(res, err.message, 500);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* POST /api/test-quick
|
|
71
|
+
* Create a new test-quick item
|
|
72
|
+
*/
|
|
73
|
+
router.post('/api/test-quick', async (req, res, { root, parseBody }) => {
|
|
74
|
+
try {
|
|
75
|
+
const body = await parseBody();
|
|
76
|
+
|
|
77
|
+
if (!body.name) {
|
|
78
|
+
return error(res, 'Name is required', 400);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isValidTestQuickName(body.name)) {
|
|
82
|
+
return error(
|
|
83
|
+
res,
|
|
84
|
+
'Invalid name. Use lowercase alphanumeric with hyphens (e.g., my-item)',
|
|
85
|
+
400
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await createItem(
|
|
90
|
+
{
|
|
91
|
+
name: body.name,
|
|
92
|
+
description: body.description,
|
|
93
|
+
},
|
|
94
|
+
root
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
return error(res, result.error, 400);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
json(res, result, 201);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
error(res, err.message, 500);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* PUT /api/test-quick/:name
|
|
109
|
+
* Update a test-quick item
|
|
110
|
+
*/
|
|
111
|
+
router.put('/api/test-quick/:name', async (req, res, { params, root, parseBody }) => {
|
|
112
|
+
try {
|
|
113
|
+
const body = await parseBody();
|
|
114
|
+
const updates = {};
|
|
115
|
+
|
|
116
|
+
if (body.description !== undefined) {
|
|
117
|
+
updates.description = body.description;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (body.status !== undefined) {
|
|
121
|
+
const validStatuses = Object.values(TEST_QUICK_STATUS);
|
|
122
|
+
if (!validStatuses.includes(body.status)) {
|
|
123
|
+
return error(res, `Invalid status. Valid values: ${validStatuses.join(', ')}`, 400);
|
|
124
|
+
}
|
|
125
|
+
updates.status = body.status;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (body.metadata !== undefined) {
|
|
129
|
+
updates.metadata = body.metadata;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Object.keys(updates).length === 0) {
|
|
133
|
+
return error(res, 'No valid updates provided', 400);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await updateItem(params.name, updates, root);
|
|
137
|
+
|
|
138
|
+
if (!result.success) {
|
|
139
|
+
return error(res, result.error, 404);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
json(res, result);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
error(res, err.message, 500);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* PATCH /api/test-quick/:name/status
|
|
150
|
+
* Update only the status of a test-quick item
|
|
151
|
+
*/
|
|
152
|
+
router.patch('/api/test-quick/:name/status', async (req, res, { params, root, parseBody }) => {
|
|
153
|
+
try {
|
|
154
|
+
const body = await parseBody();
|
|
155
|
+
|
|
156
|
+
if (!body.status) {
|
|
157
|
+
return error(res, 'Status is required', 400);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const validStatuses = Object.values(TEST_QUICK_STATUS);
|
|
161
|
+
if (!validStatuses.includes(body.status)) {
|
|
162
|
+
return error(res, `Invalid status. Valid values: ${validStatuses.join(', ')}`, 400);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await updateStatus(params.name, body.status, root);
|
|
166
|
+
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
return error(res, result.error, 404);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
json(res, result);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
error(res, err.message, 500);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* DELETE /api/test-quick/:name
|
|
179
|
+
* Delete a test-quick item
|
|
180
|
+
*/
|
|
181
|
+
router.delete('/api/test-quick/:name', async (req, res, { params, root }) => {
|
|
182
|
+
try {
|
|
183
|
+
const result = await deleteItem(params.name, root);
|
|
184
|
+
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
return error(res, result.error, 404);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
json(res, result);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
error(res, err.message, 500);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* POST /api/test-quick/clear-completed
|
|
197
|
+
* Clear all completed items
|
|
198
|
+
*/
|
|
199
|
+
router.post('/api/test-quick/clear-completed', async (req, res, { root }) => {
|
|
200
|
+
try {
|
|
201
|
+
const result = await clearCompleted(root);
|
|
202
|
+
json(res, result);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
error(res, err.message, 500);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
package/src/ui/api/worktrunk.js
CHANGED
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
hasWorktree,
|
|
7
7
|
removeWorktree,
|
|
8
8
|
getFeatureBranch,
|
|
9
|
-
startServices,
|
|
10
|
-
stopServices,
|
|
11
9
|
hasDockerServices,
|
|
10
|
+
isDockerRunning,
|
|
12
11
|
getServicesStatus,
|
|
12
|
+
startComposeService,
|
|
13
|
+
stopComposeService,
|
|
14
|
+
startAllComposeServices,
|
|
15
|
+
stopAllComposeServices,
|
|
13
16
|
} from '../../services/worktrunk.js';
|
|
14
17
|
import { json, error } from '../router.js';
|
|
15
18
|
|
|
@@ -28,21 +31,25 @@ export function registerWorktrunkRoutes(router) {
|
|
|
28
31
|
installed: false,
|
|
29
32
|
hasWorktree: false,
|
|
30
33
|
path: null,
|
|
31
|
-
|
|
34
|
+
docker: { available: false, hasComposeFile: false },
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
const branch = getFeatureBranch(params.name);
|
|
36
39
|
const wtPath = getWorktreePath(branch, root);
|
|
37
40
|
const hasWt = wtPath !== null;
|
|
38
|
-
const
|
|
41
|
+
const dockerRunning = isDockerRunning();
|
|
42
|
+
const hasComposeFile = hasWt && hasDockerServices(wtPath);
|
|
39
43
|
|
|
40
44
|
json(res, {
|
|
41
45
|
installed: true,
|
|
42
46
|
hasWorktree: hasWt,
|
|
43
47
|
path: wtPath,
|
|
44
48
|
branch,
|
|
45
|
-
|
|
49
|
+
docker: {
|
|
50
|
+
available: dockerRunning,
|
|
51
|
+
hasComposeFile,
|
|
52
|
+
},
|
|
46
53
|
});
|
|
47
54
|
});
|
|
48
55
|
|
|
@@ -94,29 +101,45 @@ export function registerWorktrunkRoutes(router) {
|
|
|
94
101
|
}
|
|
95
102
|
});
|
|
96
103
|
|
|
97
|
-
//
|
|
98
|
-
router.
|
|
104
|
+
// Get services status (from docker-compose.wt.yml)
|
|
105
|
+
router.get('/api/features/:name/wt/services', async (req, res, { params, root }) => {
|
|
99
106
|
const branch = getFeatureBranch(params.name);
|
|
100
107
|
const wtPath = getWorktreePath(branch, root);
|
|
101
108
|
|
|
102
109
|
if (!wtPath) {
|
|
103
|
-
return
|
|
110
|
+
return json(res, { services: [], hasComposeFile: false, dockerAvailable: false });
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const dockerAvailable = isDockerRunning();
|
|
114
|
+
const hasComposeFile = hasDockerServices(wtPath);
|
|
115
|
+
|
|
116
|
+
if (!dockerAvailable || !hasComposeFile) {
|
|
117
|
+
return json(res, { services: [], hasComposeFile, dockerAvailable });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const services = getServicesStatus(wtPath);
|
|
121
|
+
json(res, { services, hasComposeFile, dockerAvailable });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Start a specific service
|
|
125
|
+
router.post('/api/features/:name/wt/services/:service/start', async (req, res, { params, root }) => {
|
|
126
|
+
const branch = getFeatureBranch(params.name);
|
|
127
|
+
const wtPath = getWorktreePath(branch, root);
|
|
128
|
+
|
|
129
|
+
if (!wtPath) {
|
|
130
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
try {
|
|
111
|
-
|
|
134
|
+
startComposeService(wtPath, params.service);
|
|
112
135
|
json(res, { ok: true });
|
|
113
136
|
} catch (err) {
|
|
114
|
-
error(res, `Failed to start
|
|
137
|
+
error(res, `Failed to start service: ${err.message}`, 500);
|
|
115
138
|
}
|
|
116
139
|
});
|
|
117
140
|
|
|
118
|
-
// Stop
|
|
119
|
-
router.post('/api/features/:name/wt/services/stop', async (req, res, { params, root }) => {
|
|
141
|
+
// Stop a specific service
|
|
142
|
+
router.post('/api/features/:name/wt/services/:service/stop', async (req, res, { params, root }) => {
|
|
120
143
|
const branch = getFeatureBranch(params.name);
|
|
121
144
|
const wtPath = getWorktreePath(branch, root);
|
|
122
145
|
|
|
@@ -124,30 +147,45 @@ export function registerWorktrunkRoutes(router) {
|
|
|
124
147
|
return error(res, 'No worktree found for this feature', 404);
|
|
125
148
|
}
|
|
126
149
|
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
try {
|
|
151
|
+
stopComposeService(wtPath, params.service);
|
|
152
|
+
json(res, { ok: true });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
error(res, `Failed to stop service: ${err.message}`, 500);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Start all services
|
|
159
|
+
router.post('/api/features/:name/wt/services/start', async (req, res, { params, root }) => {
|
|
160
|
+
const branch = getFeatureBranch(params.name);
|
|
161
|
+
const wtPath = getWorktreePath(branch, root);
|
|
162
|
+
|
|
163
|
+
if (!wtPath) {
|
|
164
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
129
165
|
}
|
|
130
166
|
|
|
131
167
|
try {
|
|
132
|
-
|
|
168
|
+
startAllComposeServices(wtPath);
|
|
133
169
|
json(res, { ok: true });
|
|
134
170
|
} catch (err) {
|
|
135
|
-
error(res, `Failed to
|
|
171
|
+
error(res, `Failed to start services: ${err.message}`, 500);
|
|
136
172
|
}
|
|
137
173
|
});
|
|
138
174
|
|
|
139
|
-
//
|
|
140
|
-
router.
|
|
175
|
+
// Stop all services
|
|
176
|
+
router.post('/api/features/:name/wt/services/stop', async (req, res, { params, root }) => {
|
|
141
177
|
const branch = getFeatureBranch(params.name);
|
|
142
178
|
const wtPath = getWorktreePath(branch, root);
|
|
143
179
|
|
|
144
180
|
if (!wtPath) {
|
|
145
|
-
return
|
|
181
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
146
182
|
}
|
|
147
183
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
184
|
+
try {
|
|
185
|
+
stopAllComposeServices(wtPath);
|
|
186
|
+
json(res, { ok: true });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
error(res, `Failed to stop services: ${err.message}`, 500);
|
|
189
|
+
}
|
|
152
190
|
});
|
|
153
191
|
}
|
|
@@ -245,6 +245,98 @@ function ProjectSettings({ onSaved }) {
|
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
function ProjectScope({ onSaved }) {
|
|
249
|
+
const [apps, setApps] = React.useState([]);
|
|
250
|
+
const [loading, setLoading] = React.useState(true);
|
|
251
|
+
const [scanning, setScanning] = React.useState(false);
|
|
252
|
+
const [dirty, setDirty] = React.useState(false);
|
|
253
|
+
const [msg, setMsg] = React.useState(null);
|
|
254
|
+
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
api.get('/apps').then(setApps).catch(() => {}).finally(() => setLoading(false));
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
const handleToggle = async (appName, enabled) => {
|
|
260
|
+
setDirty(true);
|
|
261
|
+
setMsg(null);
|
|
262
|
+
try {
|
|
263
|
+
await api.patch(`/apps/${appName}`, { enabled });
|
|
264
|
+
setApps(prev => prev.map(a => a.name === appName ? { ...a, enabled } : a));
|
|
265
|
+
setDirty(false);
|
|
266
|
+
setMsg({ type: 'ok', text: 'Saved.' });
|
|
267
|
+
if (onSaved) onSaved();
|
|
268
|
+
} catch (e) {
|
|
269
|
+
setMsg({ type: 'err', text: e.message });
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const handleScan = async () => {
|
|
274
|
+
setScanning(true);
|
|
275
|
+
setMsg(null);
|
|
276
|
+
try {
|
|
277
|
+
const scanned = await api.post('/apps/scan');
|
|
278
|
+
setApps(scanned);
|
|
279
|
+
setMsg({ type: 'ok', text: `Found ${scanned.length} app(s).` });
|
|
280
|
+
if (onSaved) onSaved();
|
|
281
|
+
} catch (e) {
|
|
282
|
+
setMsg({ type: 'err', text: e.message });
|
|
283
|
+
}
|
|
284
|
+
setScanning(false);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (loading) return React.createElement('p', { className: 'text-slate-500 text-sm' }, 'Loading...');
|
|
288
|
+
|
|
289
|
+
return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4 space-y-4' },
|
|
290
|
+
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
291
|
+
React.createElement('h3', { className: 'text-sm font-semibold text-amber-400' }, 'Project Scope'),
|
|
292
|
+
React.createElement('div', { className: 'flex gap-2 items-center' },
|
|
293
|
+
msg && React.createElement('span', { className: `text-xs ${msg.type === 'ok' ? 'text-emerald-400' : 'text-red-400'}` }, msg.text),
|
|
294
|
+
React.createElement('button', {
|
|
295
|
+
onClick: handleScan,
|
|
296
|
+
disabled: scanning,
|
|
297
|
+
className: 'bg-aia-accent/20 text-aia-accent border border-aia-accent/30 rounded px-3 py-1 text-xs hover:bg-aia-accent/30 disabled:opacity-40',
|
|
298
|
+
}, scanning ? 'Scanning...' : 'Re-scan Project'),
|
|
299
|
+
),
|
|
300
|
+
),
|
|
301
|
+
|
|
302
|
+
apps.length === 0
|
|
303
|
+
? React.createElement('p', { className: 'text-slate-500 text-sm' },
|
|
304
|
+
'No apps detected. Click "Re-scan Project" to detect apps and submodules.'
|
|
305
|
+
)
|
|
306
|
+
: React.createElement('div', { className: 'space-y-2' },
|
|
307
|
+
...apps.map(app =>
|
|
308
|
+
React.createElement('div', {
|
|
309
|
+
key: app.name,
|
|
310
|
+
className: 'flex items-center justify-between py-2 border-b border-slate-700 last:border-0',
|
|
311
|
+
},
|
|
312
|
+
React.createElement('div', { className: 'flex items-center gap-2' },
|
|
313
|
+
React.createElement('span', { className: 'text-lg' }, app.icon || '\uD83D\uDCC1'),
|
|
314
|
+
React.createElement('div', null,
|
|
315
|
+
React.createElement('span', { className: 'text-sm text-slate-200' }, app.name),
|
|
316
|
+
React.createElement('p', { className: 'text-xs text-slate-500' }, app.path),
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
React.createElement('label', { className: 'relative inline-flex items-center cursor-pointer' },
|
|
320
|
+
React.createElement('input', {
|
|
321
|
+
type: 'checkbox',
|
|
322
|
+
checked: app.enabled !== false,
|
|
323
|
+
onChange: (e) => handleToggle(app.name, e.target.checked),
|
|
324
|
+
className: 'sr-only peer',
|
|
325
|
+
}),
|
|
326
|
+
React.createElement('div', {
|
|
327
|
+
className: 'w-9 h-5 bg-slate-700 rounded-full peer peer-checked:bg-aia-accent peer-checked:after:translate-x-full after:content-[\'\'] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all',
|
|
328
|
+
}),
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
),
|
|
333
|
+
|
|
334
|
+
React.createElement('p', { className: 'text-xs text-slate-500' },
|
|
335
|
+
'Enable/disable apps to control feature scope options. Disabled apps won\'t appear in feature scope selection.'
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
248
340
|
export function ConfigView() {
|
|
249
341
|
const [contextFiles, setContextFiles] = React.useState([]);
|
|
250
342
|
const [knowledgeCategories, setKnowledgeCategories] = React.useState([]);
|
|
@@ -293,6 +385,9 @@ export function ConfigView() {
|
|
|
293
385
|
React.createElement(ProjectSettings, { onSaved: handlePreferencesSaved }),
|
|
294
386
|
),
|
|
295
387
|
|
|
388
|
+
// Project Scope
|
|
389
|
+
React.createElement(ProjectScope, { onSaved: handlePreferencesSaved }),
|
|
390
|
+
|
|
296
391
|
// config.yaml (advanced)
|
|
297
392
|
React.createElement(YamlEditor, {
|
|
298
393
|
key: `config-${configVersion}`,
|