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.
Files changed (65) hide show
  1. package/README.md +0 -29
  2. package/docs/session-status-detection.md +258 -0
  3. package/next.config.ts +11 -0
  4. package/package.json +14 -1
  5. package/postcss.config.mjs +7 -0
  6. package/public/file.svg +1 -0
  7. package/public/globe.svg +1 -0
  8. package/public/next.svg +1 -0
  9. package/public/readme-cover.png +0 -0
  10. package/public/vercel.svg +1 -0
  11. package/public/window.svg +1 -0
  12. package/src/app/api/opencode-config/route.ts +304 -0
  13. package/src/app/api/opencode-config/status/route.ts +31 -0
  14. package/src/app/api/opencode-events/route.ts +86 -0
  15. package/src/app/api/opencode-models/route.test.ts +135 -0
  16. package/src/app/api/opencode-models/route.ts +58 -0
  17. package/src/app/api/profiles/[id]/apply/route.ts +49 -0
  18. package/src/app/api/profiles/[id]/route.ts +160 -0
  19. package/src/app/api/profiles/route.ts +107 -0
  20. package/src/app/api/sessions/[id]/archive/route.ts +35 -0
  21. package/src/app/api/sessions/[id]/delete/route.ts +26 -0
  22. package/src/app/api/sessions/[id]/route.ts +45 -0
  23. package/src/app/api/sessions/route.ts +596 -0
  24. package/src/app/favicon.ico +0 -0
  25. package/src/app/globals.css +66 -0
  26. package/src/app/layout.tsx +37 -0
  27. package/src/app/page.tsx +239 -0
  28. package/src/components/ErrorBoundary.tsx +72 -0
  29. package/src/components/KanbanBoard.tsx +442 -0
  30. package/src/components/LoadingState.tsx +37 -0
  31. package/src/components/ProjectCard.tsx +382 -0
  32. package/src/components/QueryProvider.tsx +25 -0
  33. package/src/components/SessionCard.tsx +291 -0
  34. package/src/components/SessionList.tsx +60 -0
  35. package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
  36. package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
  37. package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
  38. package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
  39. package/src/components/opencode-config/ConfigButton.tsx +43 -0
  40. package/src/components/opencode-config/ConfigPanel.tsx +91 -0
  41. package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
  42. package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
  43. package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
  44. package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
  45. package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
  46. package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
  47. package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
  48. package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
  49. package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
  50. package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
  51. package/src/components/ui/Tabs.tsx +59 -0
  52. package/src/hooks/useOpencodeSync.ts +378 -0
  53. package/src/index.ts +2 -0
  54. package/src/lib/notificationSound.ts +266 -0
  55. package/src/lib/opencodeConfig.test.ts +81 -0
  56. package/src/lib/opencodeConfig.ts +48 -0
  57. package/src/lib/opencodeDiscovery.ts +154 -0
  58. package/src/lib/profiles/storage.ts +264 -0
  59. package/src/lib/transform.ts +84 -0
  60. package/src/test/setup.ts +8 -0
  61. package/src/types/index.ts +89 -0
  62. package/src/types/opencodeConfig.ts +133 -0
  63. package/src/types/testing-library-vitest.d.ts +17 -0
  64. package/tsconfig.json +34 -0
  65. 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 };