@bamptee/aia-code 2.0.11 → 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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Test-quick UI component
|
|
3
|
+
* Provides a user interface for managing test-quick items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { api } from '/main.js';
|
|
8
|
+
|
|
9
|
+
const STATUS_COLORS = {
|
|
10
|
+
pending: 'bg-slate-500/20 text-slate-400 border-slate-500/30',
|
|
11
|
+
active: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
|
12
|
+
completed: 'bg-green-500/20 text-green-400 border-green-500/30',
|
|
13
|
+
error: 'bg-red-500/20 text-red-400 border-red-500/30',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Status badge component
|
|
18
|
+
*/
|
|
19
|
+
function StatusBadge({ status }) {
|
|
20
|
+
return React.createElement('span', {
|
|
21
|
+
className: `inline-block px-2 py-0.5 text-xs rounded border ${STATUS_COLORS[status] || STATUS_COLORS.pending}`,
|
|
22
|
+
}, status);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Single item card component
|
|
27
|
+
*/
|
|
28
|
+
function TestQuickItemCard({ item, onStatusChange, onDelete }) {
|
|
29
|
+
const [updating, setUpdating] = React.useState(false);
|
|
30
|
+
|
|
31
|
+
async function handleStatusChange(newStatus) {
|
|
32
|
+
setUpdating(true);
|
|
33
|
+
try {
|
|
34
|
+
await api.patch(`/test-quick/${item.name}/status`, { status: newStatus });
|
|
35
|
+
onStatusChange();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Failed to update status:', err);
|
|
38
|
+
} finally {
|
|
39
|
+
setUpdating(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function handleDelete() {
|
|
44
|
+
if (!confirm(`Delete "${item.name}"?`)) return;
|
|
45
|
+
try {
|
|
46
|
+
await api.delete(`/test-quick/${item.name}`);
|
|
47
|
+
onDelete();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('Failed to delete:', err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const statusOptions = ['pending', 'active', 'completed', 'error'];
|
|
54
|
+
|
|
55
|
+
return React.createElement('div', {
|
|
56
|
+
className: 'bg-aia-card border border-aia-border rounded-lg p-4 hover:border-aia-accent/30 transition-colors',
|
|
57
|
+
},
|
|
58
|
+
// Header row
|
|
59
|
+
React.createElement('div', { className: 'flex items-center justify-between mb-2' },
|
|
60
|
+
React.createElement('h3', { className: 'text-slate-100 font-semibold' }, item.name),
|
|
61
|
+
React.createElement(StatusBadge, { status: item.status }),
|
|
62
|
+
),
|
|
63
|
+
|
|
64
|
+
// Description
|
|
65
|
+
item.description && React.createElement('p', {
|
|
66
|
+
className: 'text-sm text-slate-400 mb-3',
|
|
67
|
+
}, item.description),
|
|
68
|
+
|
|
69
|
+
// Timestamps
|
|
70
|
+
React.createElement('div', { className: 'flex gap-4 text-xs text-slate-500 mb-3' },
|
|
71
|
+
React.createElement('span', null, 'Created: ', new Date(item.createdAt).toLocaleDateString()),
|
|
72
|
+
item.updatedAt && React.createElement('span', null, 'Updated: ', new Date(item.updatedAt).toLocaleDateString()),
|
|
73
|
+
),
|
|
74
|
+
|
|
75
|
+
// Actions
|
|
76
|
+
React.createElement('div', { className: 'flex items-center gap-2 pt-2 border-t border-aia-border' },
|
|
77
|
+
React.createElement('select', {
|
|
78
|
+
value: item.status,
|
|
79
|
+
onChange: (e) => handleStatusChange(e.target.value),
|
|
80
|
+
disabled: updating,
|
|
81
|
+
className: 'bg-slate-900 border border-aia-border rounded px-2 py-1 text-xs text-slate-300 focus:border-aia-accent focus:outline-none',
|
|
82
|
+
},
|
|
83
|
+
...statusOptions.map(s =>
|
|
84
|
+
React.createElement('option', { key: s, value: s }, s)
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
React.createElement('button', {
|
|
88
|
+
onClick: handleDelete,
|
|
89
|
+
className: 'ml-auto text-red-400 hover:text-red-300 text-xs px-2 py-1 rounded hover:bg-red-500/10',
|
|
90
|
+
}, 'Delete'),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* New item form component
|
|
97
|
+
*/
|
|
98
|
+
function NewItemForm({ onCreated }) {
|
|
99
|
+
const [name, setName] = React.useState('');
|
|
100
|
+
const [description, setDescription] = React.useState('');
|
|
101
|
+
const [error, setError] = React.useState('');
|
|
102
|
+
const [loading, setLoading] = React.useState(false);
|
|
103
|
+
|
|
104
|
+
async function handleSubmit(e) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
setError('');
|
|
107
|
+
setLoading(true);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await api.post('/test-quick', { name, description });
|
|
111
|
+
setName('');
|
|
112
|
+
setDescription('');
|
|
113
|
+
onCreated();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
setError(err.message);
|
|
116
|
+
} finally {
|
|
117
|
+
setLoading(false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return React.createElement('form', {
|
|
122
|
+
onSubmit: handleSubmit,
|
|
123
|
+
className: 'bg-aia-card border border-aia-border rounded-lg p-4 space-y-3',
|
|
124
|
+
},
|
|
125
|
+
React.createElement('h3', { className: 'text-sm font-semibold text-slate-200' }, 'New Item'),
|
|
126
|
+
|
|
127
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
128
|
+
React.createElement('input', {
|
|
129
|
+
type: 'text',
|
|
130
|
+
value: name,
|
|
131
|
+
onChange: e => setName(e.target.value),
|
|
132
|
+
placeholder: 'item-name',
|
|
133
|
+
disabled: loading,
|
|
134
|
+
className: 'bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none flex-1',
|
|
135
|
+
}),
|
|
136
|
+
React.createElement('input', {
|
|
137
|
+
type: 'text',
|
|
138
|
+
value: description,
|
|
139
|
+
onChange: e => setDescription(e.target.value),
|
|
140
|
+
placeholder: 'Description (optional)',
|
|
141
|
+
disabled: loading,
|
|
142
|
+
className: 'bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none flex-1',
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
React.createElement('div', { className: 'flex items-center gap-4' },
|
|
147
|
+
React.createElement('button', {
|
|
148
|
+
type: 'submit',
|
|
149
|
+
disabled: loading || !name,
|
|
150
|
+
className: 'bg-aia-accent/20 text-aia-accent border border-aia-accent/30 rounded px-4 py-1.5 text-sm hover:bg-aia-accent/30 disabled:opacity-40',
|
|
151
|
+
}, loading ? 'Creating...' : 'Create Item'),
|
|
152
|
+
),
|
|
153
|
+
|
|
154
|
+
error && React.createElement('p', { className: 'text-red-400 text-xs' }, error),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Filter tabs component
|
|
160
|
+
*/
|
|
161
|
+
function FilterTabs({ filter, onChange, counts }) {
|
|
162
|
+
const tabs = [
|
|
163
|
+
{ key: 'all', label: 'All', count: counts.all },
|
|
164
|
+
{ key: 'pending', label: 'Pending', count: counts.pending },
|
|
165
|
+
{ key: 'active', label: 'Active', count: counts.active },
|
|
166
|
+
{ key: 'completed', label: 'Completed', count: counts.completed },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return React.createElement('div', { className: 'flex gap-1 border-b border-aia-border' },
|
|
170
|
+
...tabs.map(tab =>
|
|
171
|
+
React.createElement('button', {
|
|
172
|
+
key: tab.key,
|
|
173
|
+
onClick: () => onChange(tab.key),
|
|
174
|
+
className: `px-3 py-1.5 text-xs border-b-2 transition-colors ${
|
|
175
|
+
filter === tab.key
|
|
176
|
+
? 'border-aia-accent text-aia-accent'
|
|
177
|
+
: 'border-transparent text-slate-500 hover:text-slate-300'
|
|
178
|
+
}`,
|
|
179
|
+
},
|
|
180
|
+
tab.label,
|
|
181
|
+
React.createElement('span', { className: 'text-slate-600 ml-1' }, `(${tab.count})`),
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Main test-quick component
|
|
189
|
+
*/
|
|
190
|
+
export function TestQuickView() {
|
|
191
|
+
const [items, setItems] = React.useState([]);
|
|
192
|
+
const [loading, setLoading] = React.useState(true);
|
|
193
|
+
const [filter, setFilter] = React.useState('all');
|
|
194
|
+
const [error, setError] = React.useState('');
|
|
195
|
+
|
|
196
|
+
async function loadItems() {
|
|
197
|
+
try {
|
|
198
|
+
const result = await api.get('/test-quick');
|
|
199
|
+
setItems(result.items || []);
|
|
200
|
+
setError('');
|
|
201
|
+
} catch (err) {
|
|
202
|
+
setError(err.message);
|
|
203
|
+
} finally {
|
|
204
|
+
setLoading(false);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
loadItems();
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
async function handleClearCompleted() {
|
|
213
|
+
if (!confirm('Clear all completed items?')) return;
|
|
214
|
+
try {
|
|
215
|
+
await api.post('/test-quick/clear-completed');
|
|
216
|
+
loadItems();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
setError(err.message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Filter items
|
|
223
|
+
const filteredItems = filter === 'all'
|
|
224
|
+
? items
|
|
225
|
+
: items.filter(item => item.status === filter);
|
|
226
|
+
|
|
227
|
+
// Counts for tabs
|
|
228
|
+
const counts = {
|
|
229
|
+
all: items.length,
|
|
230
|
+
pending: items.filter(i => i.status === 'pending').length,
|
|
231
|
+
active: items.filter(i => i.status === 'active').length,
|
|
232
|
+
completed: items.filter(i => i.status === 'completed').length,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return React.createElement('div', { className: 'space-y-6' },
|
|
236
|
+
// Header
|
|
237
|
+
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
238
|
+
React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, 'Test Quick'),
|
|
239
|
+
counts.completed > 0 && React.createElement('button', {
|
|
240
|
+
onClick: handleClearCompleted,
|
|
241
|
+
className: 'text-xs text-slate-400 hover:text-slate-200 px-2 py-1 rounded hover:bg-slate-800',
|
|
242
|
+
}, 'Clear Completed'),
|
|
243
|
+
),
|
|
244
|
+
|
|
245
|
+
// New item form
|
|
246
|
+
React.createElement(NewItemForm, { onCreated: loadItems }),
|
|
247
|
+
|
|
248
|
+
// Filter tabs
|
|
249
|
+
!loading && items.length > 0 && React.createElement(FilterTabs, {
|
|
250
|
+
filter,
|
|
251
|
+
onChange: setFilter,
|
|
252
|
+
counts,
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
// Error message
|
|
256
|
+
error && React.createElement('p', { className: 'text-red-400 text-sm' }, error),
|
|
257
|
+
|
|
258
|
+
// Items list
|
|
259
|
+
loading
|
|
260
|
+
? React.createElement('p', { className: 'text-slate-500' }, 'Loading...')
|
|
261
|
+
: items.length === 0
|
|
262
|
+
? React.createElement('p', { className: 'text-slate-500' }, 'No items yet. Create one to get started.')
|
|
263
|
+
: filteredItems.length === 0
|
|
264
|
+
? React.createElement('p', { className: 'text-slate-500' }, 'No items match this filter.')
|
|
265
|
+
: React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' },
|
|
266
|
+
...filteredItems.map(item =>
|
|
267
|
+
React.createElement(TestQuickItemCard, {
|
|
268
|
+
key: item.id,
|
|
269
|
+
item,
|
|
270
|
+
onStatusChange: loadItems,
|
|
271
|
+
onDelete: loadItems,
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -9,8 +9,12 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
9
9
|
const [error, setError] = React.useState(null);
|
|
10
10
|
const [showTerminal, setShowTerminal] = React.useState(false);
|
|
11
11
|
const [terminalReady, setTerminalReady] = React.useState(false);
|
|
12
|
+
|
|
13
|
+
// Docker services state
|
|
14
|
+
const [services, setServices] = React.useState([]);
|
|
12
15
|
const [servicesLoading, setServicesLoading] = React.useState(false);
|
|
13
|
-
const [
|
|
16
|
+
const [serviceActionLoading, setServiceActionLoading] = React.useState(null); // service name being acted on
|
|
17
|
+
const [serviceTerminal, setServiceTerminal] = React.useState(null); // { service, name }
|
|
14
18
|
|
|
15
19
|
const loadStatus = async () => {
|
|
16
20
|
try {
|
|
@@ -22,10 +26,28 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
22
26
|
setLoading(false);
|
|
23
27
|
};
|
|
24
28
|
|
|
29
|
+
const loadServices = async () => {
|
|
30
|
+
setServicesLoading(true);
|
|
31
|
+
try {
|
|
32
|
+
const data = await api.get(`/features/${featureName}/wt/services`);
|
|
33
|
+
setServices(data.services || []);
|
|
34
|
+
} catch {
|
|
35
|
+
setServices([]);
|
|
36
|
+
}
|
|
37
|
+
setServicesLoading(false);
|
|
38
|
+
};
|
|
39
|
+
|
|
25
40
|
React.useEffect(() => {
|
|
26
41
|
loadStatus();
|
|
27
42
|
}, [featureName]);
|
|
28
43
|
|
|
44
|
+
// Load services when worktree is active and docker is available
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (wtStatus?.hasWorktree && wtStatus?.docker?.available && wtStatus?.docker?.hasComposeFile) {
|
|
47
|
+
loadServices();
|
|
48
|
+
}
|
|
49
|
+
}, [wtStatus?.hasWorktree, wtStatus?.docker?.available, wtStatus?.docker?.hasComposeFile]);
|
|
50
|
+
|
|
29
51
|
const handleCreateWorktree = async () => {
|
|
30
52
|
setActionLoading(true);
|
|
31
53
|
setError(null);
|
|
@@ -47,6 +69,7 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
47
69
|
try {
|
|
48
70
|
await api.delete(`/features/${featureName}/wt`);
|
|
49
71
|
setShowTerminal(false);
|
|
72
|
+
setServiceTerminal(null);
|
|
50
73
|
await loadStatus();
|
|
51
74
|
} catch (err) {
|
|
52
75
|
setError(err.message);
|
|
@@ -64,37 +87,73 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
64
87
|
return;
|
|
65
88
|
}
|
|
66
89
|
}
|
|
90
|
+
setServiceTerminal(null);
|
|
67
91
|
setShowTerminal(true);
|
|
68
92
|
};
|
|
69
93
|
|
|
70
|
-
|
|
94
|
+
// Service actions
|
|
95
|
+
const handleStartService = async (serviceName) => {
|
|
96
|
+
setServiceActionLoading(serviceName);
|
|
97
|
+
setError(null);
|
|
98
|
+
try {
|
|
99
|
+
await api.post(`/features/${featureName}/wt/services/${serviceName}/start`);
|
|
100
|
+
await loadServices();
|
|
101
|
+
} catch (err) {
|
|
102
|
+
setError(err.message);
|
|
103
|
+
}
|
|
104
|
+
setServiceActionLoading(null);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleStopService = async (serviceName) => {
|
|
108
|
+
setServiceActionLoading(serviceName);
|
|
109
|
+
setError(null);
|
|
110
|
+
try {
|
|
111
|
+
await api.post(`/features/${featureName}/wt/services/${serviceName}/stop`);
|
|
112
|
+
await loadServices();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
setError(err.message);
|
|
115
|
+
}
|
|
116
|
+
setServiceActionLoading(null);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleStartAllServices = async () => {
|
|
71
120
|
setServicesLoading(true);
|
|
72
121
|
setError(null);
|
|
73
|
-
setServicesMessage(null);
|
|
74
122
|
try {
|
|
75
123
|
await api.post(`/features/${featureName}/wt/services/start`);
|
|
76
|
-
|
|
77
|
-
setTimeout(() => setServicesMessage(null), 3000);
|
|
124
|
+
await loadServices();
|
|
78
125
|
} catch (err) {
|
|
79
126
|
setError(err.message);
|
|
80
127
|
}
|
|
81
128
|
setServicesLoading(false);
|
|
82
129
|
};
|
|
83
130
|
|
|
84
|
-
const
|
|
131
|
+
const handleStopAllServices = async () => {
|
|
85
132
|
setServicesLoading(true);
|
|
86
133
|
setError(null);
|
|
87
|
-
setServicesMessage(null);
|
|
88
134
|
try {
|
|
89
135
|
await api.post(`/features/${featureName}/wt/services/stop`);
|
|
90
|
-
|
|
91
|
-
setTimeout(() => setServicesMessage(null), 3000);
|
|
136
|
+
await loadServices();
|
|
92
137
|
} catch (err) {
|
|
93
138
|
setError(err.message);
|
|
94
139
|
}
|
|
95
140
|
setServicesLoading(false);
|
|
96
141
|
};
|
|
97
142
|
|
|
143
|
+
const handleOpenServiceShell = async (serviceName) => {
|
|
144
|
+
if (!terminalReady) {
|
|
145
|
+
try {
|
|
146
|
+
await loadXtermScripts();
|
|
147
|
+
setTerminalReady(true);
|
|
148
|
+
} catch {
|
|
149
|
+
setError('Failed to load terminal scripts');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
setShowTerminal(false);
|
|
154
|
+
setServiceTerminal({ service: serviceName });
|
|
155
|
+
};
|
|
156
|
+
|
|
98
157
|
if (loading) {
|
|
99
158
|
return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4' },
|
|
100
159
|
React.createElement('p', { className: 'text-slate-500 text-sm' }, 'Loading worktrunk status...')
|
|
@@ -140,9 +199,14 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
140
199
|
}
|
|
141
200
|
|
|
142
201
|
// Active worktree state
|
|
143
|
-
// F8: Use wss:// for HTTPS, ws:// for HTTP
|
|
144
202
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
145
203
|
const wsUrl = `${wsProtocol}//${window.location.host}/api/terminal?cwd=${encodeURIComponent(wtStatus.path)}`;
|
|
204
|
+
const serviceWsUrl = serviceTerminal
|
|
205
|
+
? `${wsProtocol}//${window.location.host}/api/terminal/compose?service=${encodeURIComponent(serviceTerminal.service)}&cwd=${encodeURIComponent(wtStatus.path)}`
|
|
206
|
+
: null;
|
|
207
|
+
|
|
208
|
+
const dockerAvailable = wtStatus.docker?.available;
|
|
209
|
+
const hasComposeFile = wtStatus.docker?.hasComposeFile;
|
|
146
210
|
|
|
147
211
|
return React.createElement('div', { className: 'bg-slate-900 border border-orange-500/30 rounded p-4 space-y-4' },
|
|
148
212
|
// Header
|
|
@@ -166,29 +230,115 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
166
230
|
React.createElement('p', { className: 'text-sm text-slate-400 font-mono truncate', title: wtStatus.path }, wtStatus.path),
|
|
167
231
|
),
|
|
168
232
|
|
|
169
|
-
//
|
|
233
|
+
// Terminal button
|
|
170
234
|
React.createElement('div', { className: 'flex flex-wrap gap-2' },
|
|
171
235
|
React.createElement('button', {
|
|
172
236
|
onClick: showTerminal ? () => setShowTerminal(false) : handleOpenTerminal,
|
|
173
237
|
className: 'bg-slate-700 text-slate-300 border border-slate-600 rounded px-3 py-1.5 text-xs hover:bg-slate-600',
|
|
174
238
|
}, showTerminal ? 'Hide Terminal' : 'Open Terminal'),
|
|
239
|
+
),
|
|
175
240
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
className: '
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
// Docker section
|
|
242
|
+
React.createElement('div', { className: 'border-t border-slate-700 pt-4 space-y-3' },
|
|
243
|
+
// Docker status header
|
|
244
|
+
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
245
|
+
React.createElement('div', { className: 'flex items-center gap-2' },
|
|
246
|
+
React.createElement('span', { className: 'text-cyan-400 font-medium text-sm' }, 'Docker'),
|
|
247
|
+
dockerAvailable
|
|
248
|
+
? React.createElement('span', { className: 'bg-emerald-500/20 text-emerald-400 text-xs px-2 py-0.5 rounded' }, 'running')
|
|
249
|
+
: React.createElement('span', { className: 'bg-red-500/20 text-red-400 text-xs px-2 py-0.5 rounded' }, 'not running'),
|
|
250
|
+
),
|
|
251
|
+
hasComposeFile && dockerAvailable && React.createElement('button', {
|
|
252
|
+
onClick: loadServices,
|
|
185
253
|
disabled: servicesLoading,
|
|
186
|
-
className: '
|
|
187
|
-
}, servicesLoading ? '...' : '
|
|
254
|
+
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
255
|
+
}, servicesLoading ? '...' : 'Refresh'),
|
|
256
|
+
),
|
|
257
|
+
|
|
258
|
+
// No Docker available
|
|
259
|
+
!dockerAvailable && React.createElement('p', { className: 'text-slate-500 text-xs' },
|
|
260
|
+
'Docker daemon is not running. Start Docker to manage services.'
|
|
261
|
+
),
|
|
262
|
+
|
|
263
|
+
// Docker available but no compose file
|
|
264
|
+
dockerAvailable && !hasComposeFile && React.createElement('p', { className: 'text-slate-500 text-xs' },
|
|
265
|
+
'No docker-compose.wt.yml found in worktree. Create one to manage services.'
|
|
266
|
+
),
|
|
267
|
+
|
|
268
|
+
// Services list
|
|
269
|
+
dockerAvailable && hasComposeFile && services.length > 0 && React.createElement('div', { className: 'space-y-2' },
|
|
270
|
+
// Start All / Stop All buttons
|
|
271
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
272
|
+
React.createElement('button', {
|
|
273
|
+
onClick: handleStartAllServices,
|
|
274
|
+
disabled: servicesLoading,
|
|
275
|
+
className: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded px-3 py-1 text-xs hover:bg-emerald-500/30 disabled:opacity-40',
|
|
276
|
+
}, servicesLoading ? '...' : 'Start All'),
|
|
277
|
+
React.createElement('button', {
|
|
278
|
+
onClick: handleStopAllServices,
|
|
279
|
+
disabled: servicesLoading,
|
|
280
|
+
className: 'bg-red-500/20 text-red-400 border border-red-500/30 rounded px-3 py-1 text-xs hover:bg-red-500/30 disabled:opacity-40',
|
|
281
|
+
}, servicesLoading ? '...' : 'Stop All'),
|
|
282
|
+
),
|
|
283
|
+
|
|
284
|
+
// Services
|
|
285
|
+
React.createElement('div', { className: 'space-y-1' },
|
|
286
|
+
...services.map(svc => {
|
|
287
|
+
const isRunning = svc.state === 'running';
|
|
288
|
+
const isLoading = serviceActionLoading === svc.name;
|
|
289
|
+
|
|
290
|
+
return React.createElement('div', {
|
|
291
|
+
key: svc.name,
|
|
292
|
+
className: 'flex items-center justify-between bg-slate-800 rounded px-3 py-2',
|
|
293
|
+
},
|
|
294
|
+
React.createElement('div', { className: 'flex items-center gap-2 flex-wrap' },
|
|
295
|
+
React.createElement('span', {
|
|
296
|
+
className: `w-2 h-2 rounded-full ${isRunning ? 'bg-emerald-400' : 'bg-slate-500'}`,
|
|
297
|
+
title: svc.state,
|
|
298
|
+
}),
|
|
299
|
+
React.createElement('span', { className: 'text-sm text-slate-300 font-mono' }, svc.name),
|
|
300
|
+
svc.health && React.createElement('span', {
|
|
301
|
+
className: `text-xs px-1.5 py-0.5 rounded ${svc.health === 'healthy' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-yellow-500/20 text-yellow-400'}`,
|
|
302
|
+
}, svc.health),
|
|
303
|
+
// Display ports
|
|
304
|
+
svc.ports && svc.ports.length > 0 && React.createElement('span', {
|
|
305
|
+
className: 'text-xs text-cyan-500 font-mono',
|
|
306
|
+
}, svc.ports.map(p => `:${p.published}`).join(' ')),
|
|
307
|
+
),
|
|
308
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
309
|
+
isRunning && React.createElement('button', {
|
|
310
|
+
onClick: () => handleOpenServiceShell(svc.name),
|
|
311
|
+
className: 'text-cyan-400 hover:text-cyan-300 text-xs',
|
|
312
|
+
}, 'Shell'),
|
|
313
|
+
isRunning
|
|
314
|
+
? React.createElement('button', {
|
|
315
|
+
onClick: () => handleStopService(svc.name),
|
|
316
|
+
disabled: isLoading,
|
|
317
|
+
className: 'text-red-400 hover:text-red-300 text-xs disabled:opacity-40',
|
|
318
|
+
}, isLoading ? '...' : 'Stop')
|
|
319
|
+
: React.createElement('button', {
|
|
320
|
+
onClick: () => handleStartService(svc.name),
|
|
321
|
+
disabled: isLoading,
|
|
322
|
+
className: 'text-emerald-400 hover:text-emerald-300 text-xs disabled:opacity-40',
|
|
323
|
+
}, isLoading ? '...' : 'Start'),
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
}),
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
|
|
330
|
+
// Loading services
|
|
331
|
+
dockerAvailable && hasComposeFile && servicesLoading && services.length === 0 && React.createElement('p', { className: 'text-slate-500 text-xs' },
|
|
332
|
+
'Loading services...'
|
|
333
|
+
),
|
|
334
|
+
|
|
335
|
+
// No services defined
|
|
336
|
+
dockerAvailable && hasComposeFile && !servicesLoading && services.length === 0 && React.createElement('p', { className: 'text-slate-500 text-xs' },
|
|
337
|
+
'No services defined in docker-compose.wt.yml'
|
|
188
338
|
),
|
|
189
339
|
),
|
|
190
340
|
|
|
191
|
-
// Terminal
|
|
341
|
+
// Worktree Terminal
|
|
192
342
|
showTerminal && terminalReady && React.createElement('div', { className: 'mt-4' },
|
|
193
343
|
React.createElement(Terminal, {
|
|
194
344
|
wsUrl,
|
|
@@ -196,8 +346,20 @@ export function WorktrunkPanel({ featureName }) {
|
|
|
196
346
|
}),
|
|
197
347
|
),
|
|
198
348
|
|
|
199
|
-
//
|
|
200
|
-
|
|
349
|
+
// Service Terminal
|
|
350
|
+
serviceTerminal && terminalReady && React.createElement('div', { className: 'mt-4' },
|
|
351
|
+
React.createElement('div', { className: 'flex items-center justify-between mb-2' },
|
|
352
|
+
React.createElement('span', { className: 'text-xs text-cyan-400' }, `Shell: ${serviceTerminal.service}`),
|
|
353
|
+
React.createElement('button', {
|
|
354
|
+
onClick: () => setServiceTerminal(null),
|
|
355
|
+
className: 'text-slate-500 hover:text-slate-300 text-xs',
|
|
356
|
+
}, 'Close'),
|
|
357
|
+
),
|
|
358
|
+
React.createElement(Terminal, {
|
|
359
|
+
wsUrl: serviceWsUrl,
|
|
360
|
+
onClose: () => setServiceTerminal(null),
|
|
361
|
+
}),
|
|
362
|
+
),
|
|
201
363
|
|
|
202
364
|
// Error
|
|
203
365
|
error && React.createElement('p', { className: 'text-red-400 text-xs' }, error),
|
package/src/ui/public/index.html
CHANGED
|
@@ -35,6 +35,19 @@
|
|
|
35
35
|
.step-in-progress { @apply bg-amber-500/20 text-amber-400 border-amber-500/30; }
|
|
36
36
|
.step-error { @apply bg-red-500/20 text-red-400 border-red-500/30; }
|
|
37
37
|
textarea { tab-size: 2; }
|
|
38
|
+
|
|
39
|
+
/* Type badges */
|
|
40
|
+
.type-feature { @apply bg-emerald-500/20 text-emerald-400 border-emerald-500/30; }
|
|
41
|
+
.type-bug { @apply bg-red-500/20 text-red-400 border-red-500/30; }
|
|
42
|
+
|
|
43
|
+
/* App chips */
|
|
44
|
+
.app-chip { @apply bg-slate-700/50 text-slate-300 border border-slate-600 px-2 py-0.5 text-xs rounded-full; }
|
|
45
|
+
.app-chip-selected { @apply bg-aia-accent/20 text-aia-accent border border-aia-accent/30 px-2 py-0.5 text-xs rounded-full; }
|
|
46
|
+
|
|
47
|
+
/* Modal animations */
|
|
48
|
+
.modal-step-enter { opacity: 0; transform: translateX(20px); }
|
|
49
|
+
.modal-step-active { opacity: 1; transform: translateX(0); transition: all 0.2s ease-out; }
|
|
50
|
+
.modal-step-exit { opacity: 0; transform: translateX(-20px); }
|
|
38
51
|
</style>
|
|
39
52
|
</head>
|
|
40
53
|
<body class="bg-aia-bg text-slate-200 min-h-screen">
|
package/src/ui/public/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
|
|
|
3
3
|
import { Dashboard } from '/components/dashboard.js';
|
|
4
4
|
import { FeatureDetail } from '/components/feature-detail.js';
|
|
5
5
|
import { ConfigView } from '/components/config-view.js';
|
|
6
|
+
import { TestQuickView } from '/components/test-quick.js';
|
|
6
7
|
|
|
7
8
|
// --- API client ---
|
|
8
9
|
export const api = {
|
|
@@ -116,6 +117,7 @@ function parseRoute(hash) {
|
|
|
116
117
|
return { page: 'feature', name: decodeURIComponent(hash.slice('#/features/'.length)) };
|
|
117
118
|
}
|
|
118
119
|
if (hash === '#/config') return { page: 'config' };
|
|
120
|
+
if (hash === '#/test-quick') return { page: 'test-quick' };
|
|
119
121
|
return { page: 'dashboard' };
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -141,12 +143,14 @@ function App() {
|
|
|
141
143
|
className: 'bg-violet-500/20 text-violet-300 border border-violet-500/30 px-2 py-0.5 rounded text-xs font-medium',
|
|
142
144
|
}, projectName),
|
|
143
145
|
React.createElement('a', { href: '#/', className: 'text-slate-400 hover:text-slate-200 text-sm' }, 'Features'),
|
|
146
|
+
React.createElement('a', { href: '#/test-quick', className: 'text-slate-400 hover:text-slate-200 text-sm' }, 'Test Quick'),
|
|
144
147
|
React.createElement('a', { href: '#/config', className: 'text-slate-400 hover:text-slate-200 text-sm' }, 'Config'),
|
|
145
148
|
),
|
|
146
149
|
React.createElement('main', { className: 'max-w-6xl mx-auto p-6' },
|
|
147
150
|
page === 'dashboard' ? React.createElement(Dashboard) :
|
|
148
151
|
page === 'feature' ? React.createElement(FeatureDetail, { name }) :
|
|
149
|
-
page === 'config' ? React.createElement(ConfigView) :
|
|
152
|
+
page === 'config' ? React.createElement(ConfigView) :
|
|
153
|
+
page === 'test-quick' ? React.createElement(TestQuickView) : null
|
|
150
154
|
)
|
|
151
155
|
);
|
|
152
156
|
}
|