vibepulse 0.1.1 → 0.1.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 +0 -29
- package/docs/session-status-detection.md +258 -0
- package/next.config.ts +11 -0
- package/package.json +14 -1
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/readme-cover.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/opencode-config/route.ts +304 -0
- package/src/app/api/opencode-config/status/route.ts +31 -0
- package/src/app/api/opencode-events/route.ts +86 -0
- package/src/app/api/opencode-models/route.test.ts +135 -0
- package/src/app/api/opencode-models/route.ts +58 -0
- package/src/app/api/profiles/[id]/apply/route.ts +49 -0
- package/src/app/api/profiles/[id]/route.ts +160 -0
- package/src/app/api/profiles/route.ts +107 -0
- package/src/app/api/sessions/[id]/archive/route.ts +35 -0
- package/src/app/api/sessions/[id]/delete/route.ts +26 -0
- package/src/app/api/sessions/[id]/route.ts +45 -0
- package/src/app/api/sessions/route.ts +596 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +66 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/page.tsx +239 -0
- package/src/components/ErrorBoundary.tsx +72 -0
- package/src/components/KanbanBoard.tsx +442 -0
- package/src/components/LoadingState.tsx +37 -0
- package/src/components/ProjectCard.tsx +382 -0
- package/src/components/QueryProvider.tsx +25 -0
- package/src/components/SessionCard.tsx +291 -0
- package/src/components/SessionList.tsx +60 -0
- package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
- package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
- package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
- package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
- package/src/components/opencode-config/ConfigButton.tsx +43 -0
- package/src/components/opencode-config/ConfigPanel.tsx +91 -0
- package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
- package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
- package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
- package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
- package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
- package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
- package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
- package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
- package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
- package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
- package/src/components/ui/Tabs.tsx +59 -0
- package/src/hooks/useOpencodeSync.ts +378 -0
- package/src/index.ts +2 -0
- package/src/lib/notificationSound.ts +266 -0
- package/src/lib/opencodeConfig.test.ts +81 -0
- package/src/lib/opencodeConfig.ts +48 -0
- package/src/lib/opencodeDiscovery.ts +154 -0
- package/src/lib/profiles/storage.ts +264 -0
- package/src/lib/transform.ts +84 -0
- package/src/test/setup.ts +8 -0
- package/src/types/index.ts +89 -0
- package/src/types/opencodeConfig.ts +133 -0
- package/src/types/testing-library-vitest.d.ts +17 -0
- package/tsconfig.json +34 -0
- package/tsconfig.lib.json +17 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { Loader2, AlertCircle, Check } from 'lucide-react';
|
|
6
|
+
import type { Profile, ProfileConfig } from '../../../types/opencodeConfig';
|
|
7
|
+
import { ProfileList } from './ProfileList';
|
|
8
|
+
import { ProfileEditor } from './ProfileEditor';
|
|
9
|
+
|
|
10
|
+
interface ProfilesResponse {
|
|
11
|
+
profiles: Profile[];
|
|
12
|
+
activeProfileId: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProfileManagerProps {
|
|
16
|
+
onSaveSuccess?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ProfileManager({ onSaveSuccess }: ProfileManagerProps) {
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const [editingProfile, setEditingProfile] = React.useState<Profile | null>(null);
|
|
22
|
+
const [editingProfileConfig, setEditingProfileConfig] = React.useState<ProfileConfig | undefined>(undefined);
|
|
23
|
+
const [isCreating, setIsCreating] = React.useState(false);
|
|
24
|
+
const [appliedProfileId, setAppliedProfileId] = React.useState<string | null>(null);
|
|
25
|
+
const [toast, setToast] = React.useState<{
|
|
26
|
+
type: 'success' | 'error';
|
|
27
|
+
message: string;
|
|
28
|
+
} | null>(null);
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
data,
|
|
32
|
+
isLoading,
|
|
33
|
+
isError,
|
|
34
|
+
error,
|
|
35
|
+
} = useQuery<ProfilesResponse>({
|
|
36
|
+
queryKey: ['profiles'],
|
|
37
|
+
queryFn: async () => {
|
|
38
|
+
const res = await fetch('/api/profiles');
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error('Failed to fetch profiles');
|
|
41
|
+
}
|
|
42
|
+
return res.json();
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (data?.activeProfileId) {
|
|
48
|
+
setAppliedProfileId(data.activeProfileId);
|
|
49
|
+
}
|
|
50
|
+
}, [data?.activeProfileId]);
|
|
51
|
+
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (toast) {
|
|
54
|
+
const timer = setTimeout(() => setToast(null), 3000);
|
|
55
|
+
return () => clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}, [toast]);
|
|
58
|
+
|
|
59
|
+
const applyMutation = useMutation({
|
|
60
|
+
mutationFn: async (profileId: string) => {
|
|
61
|
+
const res = await fetch(`/api/profiles/${profileId}/apply`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const errorData = await res.json().catch(() => ({}));
|
|
67
|
+
throw new Error(errorData.error || 'Failed to apply profile');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return res.json();
|
|
71
|
+
},
|
|
72
|
+
onSuccess: (_, profileId) => {
|
|
73
|
+
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
|
74
|
+
queryClient.invalidateQueries({ queryKey: ['opencode-config'] });
|
|
75
|
+
setAppliedProfileId(profileId);
|
|
76
|
+
setToast({ type: 'success', message: 'Profile applied successfully' });
|
|
77
|
+
},
|
|
78
|
+
onError: (err: Error) => {
|
|
79
|
+
setToast({ type: 'error', message: err.message });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const createMutation = useMutation({
|
|
84
|
+
mutationFn: async ({
|
|
85
|
+
profile,
|
|
86
|
+
config,
|
|
87
|
+
}: {
|
|
88
|
+
profile: Partial<Profile>;
|
|
89
|
+
config: ProfileConfig;
|
|
90
|
+
}) => {
|
|
91
|
+
const res = await fetch('/api/profiles', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ profile, config }),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
const errorData = await res.json().catch(() => ({}));
|
|
99
|
+
throw new Error(errorData.error || 'Failed to create profile');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return res.json();
|
|
103
|
+
},
|
|
104
|
+
onSuccess: () => {
|
|
105
|
+
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
|
106
|
+
setToast({ type: 'success', message: 'Profile created successfully' });
|
|
107
|
+
setIsCreating(false);
|
|
108
|
+
onSaveSuccess?.();
|
|
109
|
+
},
|
|
110
|
+
onError: (err: Error) => {
|
|
111
|
+
setToast({ type: 'error', message: err.message });
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const updateMutation = useMutation({
|
|
116
|
+
mutationFn: async ({
|
|
117
|
+
id,
|
|
118
|
+
profile,
|
|
119
|
+
config,
|
|
120
|
+
}: {
|
|
121
|
+
id: string;
|
|
122
|
+
profile: Partial<Profile>;
|
|
123
|
+
config: ProfileConfig;
|
|
124
|
+
}) => {
|
|
125
|
+
const res = await fetch(`/api/profiles/${id}`, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ profile, config }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const errorData = await res.json().catch(() => ({}));
|
|
133
|
+
throw new Error(errorData.error || 'Failed to update profile');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return res.json();
|
|
137
|
+
},
|
|
138
|
+
onSuccess: () => {
|
|
139
|
+
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
|
140
|
+
setToast({ type: 'success', message: 'Profile updated successfully' });
|
|
141
|
+
setEditingProfile(null);
|
|
142
|
+
onSaveSuccess?.();
|
|
143
|
+
},
|
|
144
|
+
onError: (err: Error) => {
|
|
145
|
+
setToast({ type: 'error', message: err.message });
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const deleteMutation = useMutation({
|
|
150
|
+
mutationFn: async (profileId: string) => {
|
|
151
|
+
const res = await fetch(`/api/profiles/${profileId}`, {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
const errorData = await res.json().catch(() => ({}));
|
|
157
|
+
throw new Error(errorData.error || 'Failed to delete profile');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return res.json();
|
|
161
|
+
},
|
|
162
|
+
onSuccess: () => {
|
|
163
|
+
queryClient.invalidateQueries({ queryKey: ['profiles'] });
|
|
164
|
+
setToast({ type: 'success', message: 'Profile deleted successfully' });
|
|
165
|
+
},
|
|
166
|
+
onError: (err: Error) => {
|
|
167
|
+
setToast({ type: 'error', message: err.message });
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const handleApply = (profileId: string) => {
|
|
172
|
+
applyMutation.mutate(profileId);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleEdit = async (profile: Profile) => {
|
|
176
|
+
setEditingProfile(profile);
|
|
177
|
+
setIsCreating(false);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const res = await fetch(`/api/profiles/${profile.id}`);
|
|
181
|
+
if (res.ok) {
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
setEditingProfileConfig(data.config);
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
setEditingProfileConfig(undefined);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleCreate = () => {
|
|
191
|
+
setIsCreating(true);
|
|
192
|
+
setEditingProfile(null);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleCancelEdit = () => {
|
|
196
|
+
setEditingProfile(null);
|
|
197
|
+
setEditingProfileConfig(undefined);
|
|
198
|
+
setIsCreating(false);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const handleSave = ({
|
|
202
|
+
profile,
|
|
203
|
+
config,
|
|
204
|
+
}: {
|
|
205
|
+
profile: Partial<Profile>;
|
|
206
|
+
config: ProfileConfig;
|
|
207
|
+
}) => {
|
|
208
|
+
if (isCreating) {
|
|
209
|
+
createMutation.mutate({ profile, config });
|
|
210
|
+
} else if (editingProfile) {
|
|
211
|
+
updateMutation.mutate({ id: editingProfile.id, profile, config });
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const handleDelete = (profileId: string) => {
|
|
216
|
+
deleteMutation.mutate(profileId);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (isLoading) {
|
|
220
|
+
return (
|
|
221
|
+
<div className="flex items-center justify-center py-12">
|
|
222
|
+
<Loader2 className="h-6 w-6 animate-spin text-zinc-400" />
|
|
223
|
+
<span className="ml-2 text-sm text-zinc-500 dark:text-zinc-400">
|
|
224
|
+
Loading profiles...
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isError) {
|
|
231
|
+
return (
|
|
232
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
|
|
233
|
+
<div className="flex items-center gap-2">
|
|
234
|
+
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
|
|
235
|
+
<div>
|
|
236
|
+
<p className="text-sm font-medium text-red-800 dark:text-red-300">
|
|
237
|
+
Failed to load profiles
|
|
238
|
+
</p>
|
|
239
|
+
<p className="text-xs text-red-600 dark:text-red-400">
|
|
240
|
+
{error instanceof Error ? error.message : 'An unknown error occurred'}
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const profiles = data?.profiles ?? [];
|
|
249
|
+
const activeProfileId = data?.activeProfileId ?? null;
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="space-y-4">
|
|
253
|
+
{toast && (
|
|
254
|
+
<div
|
|
255
|
+
role="alert"
|
|
256
|
+
className={`flex items-center gap-2 rounded-lg px-4 py-3 text-sm ${
|
|
257
|
+
toast.type === 'success'
|
|
258
|
+
? 'bg-emerald-50 text-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-300'
|
|
259
|
+
: 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300'
|
|
260
|
+
}`}
|
|
261
|
+
>
|
|
262
|
+
{toast.type === 'success' ? (
|
|
263
|
+
<Check className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
264
|
+
) : (
|
|
265
|
+
<AlertCircle className="h-4 w-4 shrink-0" aria-hidden="true" />
|
|
266
|
+
)}
|
|
267
|
+
<span>{toast.message}</span>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{editingProfile || isCreating ? (
|
|
272
|
+
<ProfileEditor
|
|
273
|
+
profile={editingProfile ?? undefined}
|
|
274
|
+
initialConfig={editingProfileConfig}
|
|
275
|
+
onSave={handleSave}
|
|
276
|
+
onCancel={handleCancelEdit}
|
|
277
|
+
/>
|
|
278
|
+
) : (
|
|
279
|
+
<ProfileList
|
|
280
|
+
profiles={profiles}
|
|
281
|
+
activeProfileId={activeProfileId}
|
|
282
|
+
appliedProfileId={appliedProfileId}
|
|
283
|
+
onApply={handleApply}
|
|
284
|
+
onEdit={handleEdit}
|
|
285
|
+
onDelete={handleDelete}
|
|
286
|
+
onCreateNew={handleCreate}
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default ProfileManager;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
5
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
6
|
+
import { twMerge } from 'tailwind-merge';
|
|
7
|
+
|
|
8
|
+
function cn(...inputs: ClassValue[]) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Tabs = TabsPrimitive.Root;
|
|
13
|
+
|
|
14
|
+
const TabsList = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
17
|
+
>(({ className, ...props }, ref) => (
|
|
18
|
+
<TabsPrimitive.List
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
'inline-flex h-10 items-center justify-center rounded-lg bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
));
|
|
27
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
28
|
+
|
|
29
|
+
const TabsTrigger = React.forwardRef<
|
|
30
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
31
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
32
|
+
>(({ className, ...props }, ref) => (
|
|
33
|
+
<TabsPrimitive.Trigger
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn(
|
|
36
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow-sm dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[state=active]:bg-zinc-950 dark:data-[state=active]:text-zinc-50',
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
));
|
|
42
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
43
|
+
|
|
44
|
+
const TabsContent = React.forwardRef<
|
|
45
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
46
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<TabsPrimitive.Content
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(
|
|
51
|
+
'mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300',
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
));
|
|
57
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
58
|
+
|
|
59
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|