@bamptee/aia-code 2.0.12 → 2.0.14

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.
@@ -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 [servicesMessage, setServicesMessage] = React.useState(null);
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
- const handleStartServices = async () => {
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
- setServicesMessage('Services started');
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 handleStopServices = async () => {
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
- setServicesMessage('Services stopped');
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
- // Actions
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
- // Docker services buttons (only if services exist)
177
- wtStatus.hasServices && React.createElement(React.Fragment, null,
178
- React.createElement('button', {
179
- onClick: handleStartServices,
180
- disabled: servicesLoading,
181
- className: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded px-3 py-1.5 text-xs hover:bg-emerald-500/30 disabled:opacity-40',
182
- }, servicesLoading ? '...' : 'Start Services'),
183
- React.createElement('button', {
184
- onClick: handleStopServices,
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: 'bg-red-500/20 text-red-400 border border-red-500/30 rounded px-3 py-1.5 text-xs hover:bg-red-500/30 disabled:opacity-40',
187
- }, servicesLoading ? '...' : 'Stop Services'),
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
- // F9: Services feedback message
200
- servicesMessage && React.createElement('p', { className: 'text-emerald-400 text-xs' }, servicesMessage),
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),
@@ -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">
@@ -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) : null
152
+ page === 'config' ? React.createElement(ConfigView) :
153
+ page === 'test-quick' ? React.createElement(TestQuickView) : null
150
154
  )
151
155
  );
152
156
  }