@bamptee/aia-code 2.0.0 → 2.0.2
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 +55 -4
- package/package.json +7 -1
- package/src/commands/init.js +11 -1
- package/src/models.js +19 -2
- package/src/prompt-builder.js +33 -1
- package/src/providers/cli-runner.js +2 -1
- package/src/services/config.js +53 -2
- package/src/services/flow-analyzer.js +32 -0
- package/src/services/runner.js +2 -2
- package/src/services/status.js +14 -0
- package/src/services/suggestions.js +166 -0
- package/src/services/worktrunk.js +197 -0
- package/src/ui/api/config.js +55 -2
- package/src/ui/api/constants.js +9 -3
- package/src/ui/api/features.js +301 -6
- package/src/ui/api/index.js +2 -0
- package/src/ui/api/worktrunk.js +153 -0
- package/src/ui/public/components/config-view.js +196 -2
- package/src/ui/public/components/dashboard.js +64 -4
- package/src/ui/public/components/feature-detail.js +589 -104
- package/src/ui/public/components/terminal.js +197 -0
- package/src/ui/public/components/worktrunk-panel.js +205 -0
- package/src/ui/public/main.js +23 -1
- package/src/ui/router.js +1 -1
- package/src/ui/server.js +85 -0
- package/src/utils/prompt.js +38 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isWtInstalled,
|
|
3
|
+
listWorktrees,
|
|
4
|
+
createWorktree,
|
|
5
|
+
getWorktreePath,
|
|
6
|
+
hasWorktree,
|
|
7
|
+
removeWorktree,
|
|
8
|
+
getFeatureBranch,
|
|
9
|
+
startServices,
|
|
10
|
+
stopServices,
|
|
11
|
+
hasDockerServices,
|
|
12
|
+
getServicesStatus,
|
|
13
|
+
} from '../../services/worktrunk.js';
|
|
14
|
+
import { json, error } from '../router.js';
|
|
15
|
+
|
|
16
|
+
export function registerWorktrunkRoutes(router) {
|
|
17
|
+
// Check if wt CLI is installed
|
|
18
|
+
router.get('/api/wt/status', async (req, res) => {
|
|
19
|
+
const installed = isWtInstalled();
|
|
20
|
+
json(res, { installed });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Get worktree status for a feature
|
|
24
|
+
router.get('/api/features/:name/wt', async (req, res, { params, root }) => {
|
|
25
|
+
const installed = isWtInstalled();
|
|
26
|
+
if (!installed) {
|
|
27
|
+
return json(res, {
|
|
28
|
+
installed: false,
|
|
29
|
+
hasWorktree: false,
|
|
30
|
+
path: null,
|
|
31
|
+
hasServices: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const branch = getFeatureBranch(params.name);
|
|
36
|
+
const wtPath = getWorktreePath(branch, root);
|
|
37
|
+
const hasWt = wtPath !== null;
|
|
38
|
+
const hasServices = hasWt && hasDockerServices(wtPath);
|
|
39
|
+
|
|
40
|
+
json(res, {
|
|
41
|
+
installed: true,
|
|
42
|
+
hasWorktree: hasWt,
|
|
43
|
+
path: wtPath,
|
|
44
|
+
branch,
|
|
45
|
+
hasServices,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create a worktree for a feature
|
|
50
|
+
router.post('/api/features/:name/wt/create', async (req, res, { params, root }) => {
|
|
51
|
+
const installed = isWtInstalled();
|
|
52
|
+
if (!installed) {
|
|
53
|
+
return error(res, 'Worktrunk (wt) CLI is not installed', 400);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const branch = getFeatureBranch(params.name);
|
|
57
|
+
|
|
58
|
+
// Check if worktree already exists
|
|
59
|
+
if (hasWorktree(branch, root)) {
|
|
60
|
+
return error(res, `Worktree for branch "${branch}" already exists`, 400);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
createWorktree(branch, root);
|
|
65
|
+
const wtPath = getWorktreePath(branch, root);
|
|
66
|
+
json(res, {
|
|
67
|
+
ok: true,
|
|
68
|
+
branch,
|
|
69
|
+
path: wtPath,
|
|
70
|
+
}, 201);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
error(res, `Failed to create worktree: ${err.message}`, 500);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Remove a worktree for a feature
|
|
77
|
+
router.delete('/api/features/:name/wt', async (req, res, { params, root }) => {
|
|
78
|
+
const installed = isWtInstalled();
|
|
79
|
+
if (!installed) {
|
|
80
|
+
return error(res, 'Worktrunk (wt) CLI is not installed', 400);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const branch = getFeatureBranch(params.name);
|
|
84
|
+
|
|
85
|
+
if (!hasWorktree(branch, root)) {
|
|
86
|
+
return error(res, `No worktree found for branch "${branch}"`, 404);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
removeWorktree(branch, root);
|
|
91
|
+
json(res, { ok: true });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
error(res, `Failed to remove worktree: ${err.message}`, 500);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Start docker services in worktree
|
|
98
|
+
router.post('/api/features/:name/wt/services/start', async (req, res, { params, root }) => {
|
|
99
|
+
const branch = getFeatureBranch(params.name);
|
|
100
|
+
const wtPath = getWorktreePath(branch, root);
|
|
101
|
+
|
|
102
|
+
if (!wtPath) {
|
|
103
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!hasDockerServices(wtPath)) {
|
|
107
|
+
return error(res, 'No docker-compose.wt.yml found in worktree', 404);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
startServices(wtPath);
|
|
112
|
+
json(res, { ok: true });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
error(res, `Failed to start services: ${err.message}`, 500);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Stop docker services in worktree
|
|
119
|
+
router.post('/api/features/:name/wt/services/stop', async (req, res, { params, root }) => {
|
|
120
|
+
const branch = getFeatureBranch(params.name);
|
|
121
|
+
const wtPath = getWorktreePath(branch, root);
|
|
122
|
+
|
|
123
|
+
if (!wtPath) {
|
|
124
|
+
return error(res, 'No worktree found for this feature', 404);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!hasDockerServices(wtPath)) {
|
|
128
|
+
return error(res, 'No docker-compose.wt.yml found in worktree', 404);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
stopServices(wtPath);
|
|
133
|
+
json(res, { ok: true });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
error(res, `Failed to stop services: ${err.message}`, 500);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Get services status
|
|
140
|
+
router.get('/api/features/:name/wt/services', async (req, res, { params, root }) => {
|
|
141
|
+
const branch = getFeatureBranch(params.name);
|
|
142
|
+
const wtPath = getWorktreePath(branch, root);
|
|
143
|
+
|
|
144
|
+
if (!wtPath) {
|
|
145
|
+
return json(res, { services: [], hasServices: false });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hasServices = hasDockerServices(wtPath);
|
|
149
|
+
const services = hasServices ? getServicesStatus(wtPath) : [];
|
|
150
|
+
|
|
151
|
+
json(res, { services, hasServices });
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -63,6 +63,188 @@ function FileList({ title, files, selectedFile, onSelect }) {
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function UserPreferences() {
|
|
67
|
+
const [prefs, setPrefs] = React.useState({
|
|
68
|
+
user_name: '',
|
|
69
|
+
communication_language: 'English',
|
|
70
|
+
});
|
|
71
|
+
const [configPath, setConfigPath] = React.useState('');
|
|
72
|
+
const [loading, setLoading] = React.useState(true);
|
|
73
|
+
const [saving, setSaving] = React.useState(false);
|
|
74
|
+
const [dirty, setDirty] = React.useState(false);
|
|
75
|
+
const [msg, setMsg] = React.useState(null);
|
|
76
|
+
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
api.get('/user-config').then(data => {
|
|
79
|
+
const parsed = data.parsed || {};
|
|
80
|
+
setPrefs({
|
|
81
|
+
user_name: parsed.user_name || '',
|
|
82
|
+
communication_language: parsed.communication_language || 'English',
|
|
83
|
+
});
|
|
84
|
+
setConfigPath(data.path || '');
|
|
85
|
+
}).catch(() => {}).finally(() => setLoading(false));
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const handleChange = (field, value) => {
|
|
89
|
+
setPrefs(p => ({ ...p, [field]: value }));
|
|
90
|
+
setDirty(true);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const save = async () => {
|
|
94
|
+
setSaving(true);
|
|
95
|
+
setMsg(null);
|
|
96
|
+
try {
|
|
97
|
+
await api.patch('/user-config', prefs);
|
|
98
|
+
setDirty(false);
|
|
99
|
+
setMsg({ type: 'ok', text: 'Saved.' });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
setMsg({ type: 'err', text: e.message });
|
|
102
|
+
}
|
|
103
|
+
setSaving(false);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (loading) return React.createElement('p', { className: 'text-slate-500 text-sm' }, 'Loading...');
|
|
107
|
+
|
|
108
|
+
const languages = ['English', 'French', 'Spanish', 'German', 'Portuguese', 'Italian', 'Dutch', 'Russian', 'Chinese', 'Japanese', 'Korean'];
|
|
109
|
+
|
|
110
|
+
return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4 space-y-4' },
|
|
111
|
+
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
112
|
+
React.createElement('h3', { className: 'text-sm font-semibold text-cyan-400' }, 'User Preferences'),
|
|
113
|
+
React.createElement('div', { className: 'flex gap-2 items-center' },
|
|
114
|
+
dirty && React.createElement('span', { className: 'text-xs text-amber-400' }, 'unsaved'),
|
|
115
|
+
msg && React.createElement('span', { className: `text-xs ${msg.type === 'ok' ? 'text-emerald-400' : 'text-red-400'}` }, msg.text),
|
|
116
|
+
React.createElement('button', {
|
|
117
|
+
onClick: save,
|
|
118
|
+
disabled: saving || !dirty,
|
|
119
|
+
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',
|
|
120
|
+
}, saving ? '...' : 'Save'),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
|
|
125
|
+
// User name
|
|
126
|
+
React.createElement('div', { className: 'space-y-1' },
|
|
127
|
+
React.createElement('label', { className: 'text-xs text-slate-400' }, 'Your Name'),
|
|
128
|
+
React.createElement('input', {
|
|
129
|
+
type: 'text',
|
|
130
|
+
value: prefs.user_name,
|
|
131
|
+
onChange: e => handleChange('user_name', e.target.value),
|
|
132
|
+
placeholder: 'Optional',
|
|
133
|
+
className: 'w-full 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',
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
|
|
137
|
+
// Communication language
|
|
138
|
+
React.createElement('div', { className: 'space-y-1' },
|
|
139
|
+
React.createElement('label', { className: 'text-xs text-slate-400' }, 'Communication Language'),
|
|
140
|
+
React.createElement('select', {
|
|
141
|
+
value: prefs.communication_language,
|
|
142
|
+
onChange: e => handleChange('communication_language', e.target.value),
|
|
143
|
+
className: 'w-full bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 focus:border-aia-accent focus:outline-none',
|
|
144
|
+
},
|
|
145
|
+
...languages.map(lang => React.createElement('option', { key: lang, value: lang }, lang)),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
|
|
150
|
+
React.createElement('p', { className: 'text-xs text-slate-500' },
|
|
151
|
+
'Your personal preferences. Stored globally in ~/.aia/ and apply to all projects.'
|
|
152
|
+
),
|
|
153
|
+
configPath && React.createElement('p', { className: 'text-xs text-slate-600' },
|
|
154
|
+
`Location: ${configPath}`
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ProjectSettings({ onSaved }) {
|
|
160
|
+
const [settings, setSettings] = React.useState({
|
|
161
|
+
projectName: '',
|
|
162
|
+
document_output_language: 'English',
|
|
163
|
+
});
|
|
164
|
+
const [loading, setLoading] = React.useState(true);
|
|
165
|
+
const [saving, setSaving] = React.useState(false);
|
|
166
|
+
const [dirty, setDirty] = React.useState(false);
|
|
167
|
+
const [msg, setMsg] = React.useState(null);
|
|
168
|
+
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
api.get('/config').then(data => {
|
|
171
|
+
const parsed = data.parsed || {};
|
|
172
|
+
setSettings({
|
|
173
|
+
projectName: parsed.projectName || '',
|
|
174
|
+
document_output_language: parsed.document_output_language || 'English',
|
|
175
|
+
});
|
|
176
|
+
}).catch(() => {}).finally(() => setLoading(false));
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
const handleChange = (field, value) => {
|
|
180
|
+
setSettings(p => ({ ...p, [field]: value }));
|
|
181
|
+
setDirty(true);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const save = async () => {
|
|
185
|
+
setSaving(true);
|
|
186
|
+
setMsg(null);
|
|
187
|
+
try {
|
|
188
|
+
await api.patch('/config/project', settings);
|
|
189
|
+
setDirty(false);
|
|
190
|
+
setMsg({ type: 'ok', text: 'Saved.' });
|
|
191
|
+
if (onSaved) onSaved();
|
|
192
|
+
} catch (e) {
|
|
193
|
+
setMsg({ type: 'err', text: e.message });
|
|
194
|
+
}
|
|
195
|
+
setSaving(false);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (loading) return React.createElement('p', { className: 'text-slate-500 text-sm' }, 'Loading...');
|
|
199
|
+
|
|
200
|
+
const languages = ['English', 'French', 'Spanish', 'German', 'Portuguese', 'Italian', 'Dutch', 'Russian', 'Chinese', 'Japanese', 'Korean'];
|
|
201
|
+
|
|
202
|
+
return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4 space-y-4' },
|
|
203
|
+
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
204
|
+
React.createElement('h3', { className: 'text-sm font-semibold text-violet-400' }, 'Project Settings'),
|
|
205
|
+
React.createElement('div', { className: 'flex gap-2 items-center' },
|
|
206
|
+
dirty && React.createElement('span', { className: 'text-xs text-amber-400' }, 'unsaved'),
|
|
207
|
+
msg && React.createElement('span', { className: `text-xs ${msg.type === 'ok' ? 'text-emerald-400' : 'text-red-400'}` }, msg.text),
|
|
208
|
+
React.createElement('button', {
|
|
209
|
+
onClick: save,
|
|
210
|
+
disabled: saving || !dirty,
|
|
211
|
+
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',
|
|
212
|
+
}, saving ? '...' : 'Save'),
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
|
|
216
|
+
React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
|
|
217
|
+
// Project name
|
|
218
|
+
React.createElement('div', { className: 'space-y-1' },
|
|
219
|
+
React.createElement('label', { className: 'text-xs text-slate-400' }, 'Project Name'),
|
|
220
|
+
React.createElement('input', {
|
|
221
|
+
type: 'text',
|
|
222
|
+
value: settings.projectName,
|
|
223
|
+
onChange: e => handleChange('projectName', e.target.value),
|
|
224
|
+
placeholder: 'My Project',
|
|
225
|
+
className: 'w-full 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',
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
|
|
229
|
+
// Document output language
|
|
230
|
+
React.createElement('div', { className: 'space-y-1' },
|
|
231
|
+
React.createElement('label', { className: 'text-xs text-slate-400' }, 'Document Output Language'),
|
|
232
|
+
React.createElement('select', {
|
|
233
|
+
value: settings.document_output_language,
|
|
234
|
+
onChange: e => handleChange('document_output_language', e.target.value),
|
|
235
|
+
className: 'w-full bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 focus:border-aia-accent focus:outline-none',
|
|
236
|
+
},
|
|
237
|
+
...languages.map(lang => React.createElement('option', { key: lang, value: lang }, lang)),
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
|
|
242
|
+
React.createElement('p', { className: 'text-xs text-slate-500' },
|
|
243
|
+
'Project-wide settings. Stored in .aia/config.yaml and shared with your team.'
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
66
248
|
export function ConfigView() {
|
|
67
249
|
const [contextFiles, setContextFiles] = React.useState([]);
|
|
68
250
|
const [knowledgeCategories, setKnowledgeCategories] = React.useState([]);
|
|
@@ -71,12 +253,17 @@ export function ConfigView() {
|
|
|
71
253
|
const [selectedCategory, setSelectedCategory] = React.useState(null);
|
|
72
254
|
const [logs, setLogs] = React.useState('');
|
|
73
255
|
const [showLogs, setShowLogs] = React.useState(false);
|
|
256
|
+
const [configVersion, setConfigVersion] = React.useState(0);
|
|
74
257
|
|
|
75
258
|
React.useEffect(() => {
|
|
76
259
|
api.get('/context').then(setContextFiles).catch(() => {});
|
|
77
260
|
api.get('/knowledge').then(setKnowledgeCategories).catch(() => {});
|
|
78
261
|
}, []);
|
|
79
262
|
|
|
263
|
+
const handlePreferencesSaved = () => {
|
|
264
|
+
setConfigVersion(v => v + 1);
|
|
265
|
+
};
|
|
266
|
+
|
|
80
267
|
function selectContext(f) {
|
|
81
268
|
setSelectedFile(f);
|
|
82
269
|
setSelectedType('context');
|
|
@@ -100,9 +287,16 @@ export function ConfigView() {
|
|
|
100
287
|
return React.createElement('div', { className: 'space-y-6' },
|
|
101
288
|
React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, 'Configuration'),
|
|
102
289
|
|
|
103
|
-
//
|
|
290
|
+
// Two columns: User Preferences + Project Settings
|
|
291
|
+
React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
|
|
292
|
+
React.createElement(UserPreferences),
|
|
293
|
+
React.createElement(ProjectSettings, { onSaved: handlePreferencesSaved }),
|
|
294
|
+
),
|
|
295
|
+
|
|
296
|
+
// config.yaml (advanced)
|
|
104
297
|
React.createElement(YamlEditor, {
|
|
105
|
-
|
|
298
|
+
key: `config-${configVersion}`,
|
|
299
|
+
title: 'config.yaml (advanced)',
|
|
106
300
|
loadFn: async () => (await api.get('/config')).content,
|
|
107
301
|
saveFn: async (content) => api.put('/config', { content }),
|
|
108
302
|
}),
|
|
@@ -24,7 +24,13 @@ function FeatureCard({ feature }) {
|
|
|
24
24
|
className: 'block bg-aia-card border border-aia-border rounded-lg p-4 hover:border-aia-accent/50 transition-colors',
|
|
25
25
|
},
|
|
26
26
|
React.createElement('div', { className: 'flex items-center justify-between mb-3' },
|
|
27
|
-
React.createElement('
|
|
27
|
+
React.createElement('div', { className: 'flex items-center gap-2' },
|
|
28
|
+
React.createElement('h3', { className: 'text-slate-100 font-semibold' }, feature.name),
|
|
29
|
+
feature.hasWorktree && React.createElement('span', {
|
|
30
|
+
className: 'bg-orange-500/20 text-orange-400 text-xs px-1.5 py-0.5 rounded',
|
|
31
|
+
title: 'Git worktree active',
|
|
32
|
+
}, 'wt'),
|
|
33
|
+
),
|
|
28
34
|
React.createElement('span', { className: 'text-xs text-slate-500' },
|
|
29
35
|
`${doneCount}/${totalCount} steps`
|
|
30
36
|
),
|
|
@@ -167,9 +173,40 @@ function QuickTicketForm({ onDone }) {
|
|
|
167
173
|
);
|
|
168
174
|
}
|
|
169
175
|
|
|
176
|
+
function FilterTabs({ filter, onChange, counts }) {
|
|
177
|
+
const tabs = [
|
|
178
|
+
{ key: 'all', label: 'All', count: counts.all },
|
|
179
|
+
{ key: 'with-wt', label: 'With Worktree', count: counts.withWt },
|
|
180
|
+
{ key: 'without-wt', label: 'Without Worktree', count: counts.withoutWt },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
return React.createElement('div', { className: 'flex gap-1 border-b border-aia-border' },
|
|
184
|
+
...tabs.map(tab =>
|
|
185
|
+
React.createElement('button', {
|
|
186
|
+
key: tab.key,
|
|
187
|
+
onClick: () => onChange(tab.key),
|
|
188
|
+
className: `px-3 py-1.5 text-xs border-b-2 transition-colors flex items-center gap-1.5 ${
|
|
189
|
+
filter === tab.key
|
|
190
|
+
? 'border-aia-accent text-aia-accent'
|
|
191
|
+
: 'border-transparent text-slate-500 hover:text-slate-300'
|
|
192
|
+
}`,
|
|
193
|
+
},
|
|
194
|
+
tab.label,
|
|
195
|
+
tab.key === 'with-wt' && tab.count > 0 && React.createElement('span', {
|
|
196
|
+
className: 'bg-orange-500/20 text-orange-400 text-xs px-1.5 py-0.5 rounded',
|
|
197
|
+
}, tab.count),
|
|
198
|
+
tab.key !== 'with-wt' && React.createElement('span', {
|
|
199
|
+
className: 'text-slate-600',
|
|
200
|
+
}, `(${tab.count})`),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
170
206
|
export function Dashboard() {
|
|
171
207
|
const [features, setFeatures] = React.useState([]);
|
|
172
208
|
const [loading, setLoading] = React.useState(true);
|
|
209
|
+
const [filter, setFilter] = React.useState('all');
|
|
173
210
|
|
|
174
211
|
async function load() {
|
|
175
212
|
try {
|
|
@@ -181,6 +218,20 @@ export function Dashboard() {
|
|
|
181
218
|
|
|
182
219
|
React.useEffect(() => { load(); }, []);
|
|
183
220
|
|
|
221
|
+
// Filter features
|
|
222
|
+
const filteredFeatures = features.filter(f => {
|
|
223
|
+
if (filter === 'with-wt') return f.hasWorktree;
|
|
224
|
+
if (filter === 'without-wt') return !f.hasWorktree;
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Counts for tabs
|
|
229
|
+
const counts = {
|
|
230
|
+
all: features.length,
|
|
231
|
+
withWt: features.filter(f => f.hasWorktree).length,
|
|
232
|
+
withoutWt: features.filter(f => !f.hasWorktree).length,
|
|
233
|
+
};
|
|
234
|
+
|
|
184
235
|
return React.createElement('div', { className: 'space-y-6' },
|
|
185
236
|
React.createElement('div', { className: 'flex items-center justify-between' },
|
|
186
237
|
React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, 'Features'),
|
|
@@ -190,12 +241,21 @@ export function Dashboard() {
|
|
|
190
241
|
// Quick ticket
|
|
191
242
|
React.createElement(QuickTicketForm, { onDone: load }),
|
|
192
243
|
|
|
244
|
+
// Filter tabs
|
|
245
|
+
!loading && features.length > 0 && React.createElement(FilterTabs, {
|
|
246
|
+
filter,
|
|
247
|
+
onChange: setFilter,
|
|
248
|
+
counts,
|
|
249
|
+
}),
|
|
250
|
+
|
|
193
251
|
loading
|
|
194
252
|
? React.createElement('p', { className: 'text-slate-500' }, 'Loading...')
|
|
195
253
|
: features.length === 0
|
|
196
254
|
? React.createElement('p', { className: 'text-slate-500' }, 'No features yet. Create one to get started.')
|
|
197
|
-
:
|
|
198
|
-
|
|
199
|
-
|
|
255
|
+
: filteredFeatures.length === 0
|
|
256
|
+
? React.createElement('p', { className: 'text-slate-500' }, 'No features match this filter.')
|
|
257
|
+
: React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' },
|
|
258
|
+
...filteredFeatures.map(f => React.createElement(FeatureCard, { key: f.name, feature: f }))
|
|
259
|
+
)
|
|
200
260
|
);
|
|
201
261
|
}
|