vibepulse 0.1.0 → 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 (68) hide show
  1. package/README.md +7 -13
  2. package/bin/vibepulse.js +1 -0
  3. package/dist/index.js +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/docs/session-status-detection.md +258 -0
  6. package/next.config.ts +11 -0
  7. package/package.json +17 -11
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/readme-cover.png +0 -0
  13. package/public/vercel.svg +1 -0
  14. package/public/window.svg +1 -0
  15. package/src/app/api/opencode-config/route.ts +304 -0
  16. package/src/app/api/opencode-config/status/route.ts +31 -0
  17. package/src/app/api/opencode-events/route.ts +86 -0
  18. package/src/app/api/opencode-models/route.test.ts +135 -0
  19. package/src/app/api/opencode-models/route.ts +58 -0
  20. package/src/app/api/profiles/[id]/apply/route.ts +49 -0
  21. package/src/app/api/profiles/[id]/route.ts +160 -0
  22. package/src/app/api/profiles/route.ts +107 -0
  23. package/src/app/api/sessions/[id]/archive/route.ts +35 -0
  24. package/src/app/api/sessions/[id]/delete/route.ts +26 -0
  25. package/src/app/api/sessions/[id]/route.ts +45 -0
  26. package/src/app/api/sessions/route.ts +596 -0
  27. package/src/app/favicon.ico +0 -0
  28. package/src/app/globals.css +66 -0
  29. package/src/app/layout.tsx +37 -0
  30. package/src/app/page.tsx +239 -0
  31. package/src/components/ErrorBoundary.tsx +72 -0
  32. package/src/components/KanbanBoard.tsx +442 -0
  33. package/src/components/LoadingState.tsx +37 -0
  34. package/src/components/ProjectCard.tsx +382 -0
  35. package/src/components/QueryProvider.tsx +25 -0
  36. package/src/components/SessionCard.tsx +291 -0
  37. package/src/components/SessionList.tsx +60 -0
  38. package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
  39. package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
  40. package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
  41. package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
  42. package/src/components/opencode-config/ConfigButton.tsx +43 -0
  43. package/src/components/opencode-config/ConfigPanel.tsx +91 -0
  44. package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
  45. package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
  46. package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
  47. package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
  48. package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
  49. package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
  50. package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
  51. package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
  52. package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
  53. package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
  54. package/src/components/ui/Tabs.tsx +59 -0
  55. package/src/hooks/useOpencodeSync.ts +378 -0
  56. package/src/index.ts +2 -0
  57. package/src/lib/notificationSound.ts +266 -0
  58. package/src/lib/opencodeConfig.test.ts +81 -0
  59. package/src/lib/opencodeConfig.ts +48 -0
  60. package/src/lib/opencodeDiscovery.ts +154 -0
  61. package/src/lib/profiles/storage.ts +264 -0
  62. package/src/lib/transform.ts +84 -0
  63. package/src/test/setup.ts +8 -0
  64. package/src/types/index.ts +89 -0
  65. package/src/types/opencodeConfig.ts +133 -0
  66. package/src/types/testing-library-vitest.d.ts +17 -0
  67. package/tsconfig.json +34 -0
  68. package/tsconfig.lib.json +17 -0
@@ -0,0 +1,445 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { useForm, Controller } from 'react-hook-form';
5
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
6
+ import { AgentModelSelector } from './AgentModelSelector';
7
+ import { Check, AlertCircle, Loader2, AlertTriangle } from 'lucide-react';
8
+
9
+ interface AgentConfig {
10
+ model?: string;
11
+ temperature?: number;
12
+ top_p?: number;
13
+ variant?: string;
14
+ prompt_append?: string;
15
+ }
16
+
17
+ interface CategoryConfig {
18
+ model?: string;
19
+ variant?: string;
20
+ temperature?: number;
21
+ top_p?: number;
22
+ prompt_append?: string;
23
+ description?: string;
24
+ }
25
+
26
+ interface OpencodeConfigResponse {
27
+ agents: Record<string, AgentConfig>;
28
+ categories?: Record<string, CategoryConfig>;
29
+ defaultAgent?: AgentConfig;
30
+ }
31
+
32
+ interface OpencodeModelsResponse {
33
+ models: string[];
34
+ source: string;
35
+ }
36
+
37
+ interface AgentConfigFormData {
38
+ model: string;
39
+ temperature: number;
40
+ top_p: number;
41
+ variant: string;
42
+ prompt_append: string;
43
+ }
44
+
45
+ interface AgentConfigFormProps {
46
+ agentName?: string;
47
+ onSaveSuccess?: () => void;
48
+ }
49
+
50
+ const AGENT_CATEGORY_MAP: Record<string, string> = {
51
+ prometheus: 'ultrabrain',
52
+ hephaestus: 'deep',
53
+ oracle: 'ultrabrain',
54
+ metis: 'ultrabrain',
55
+ momus: 'quick',
56
+ atlas: 'unspecified-high',
57
+ librarian: 'writing',
58
+ explore: 'explore',
59
+ sisyphus: 'unspecified-high',
60
+ default: 'unspecified-low',
61
+ };
62
+
63
+ export function AgentConfigForm({
64
+ agentName = 'default',
65
+ onSaveSuccess
66
+ }: AgentConfigFormProps) {
67
+ const queryClient = useQueryClient();
68
+ const [toast, setToast] = React.useState<{
69
+ type: 'success' | 'error';
70
+ message: string;
71
+ } | null>(null);
72
+
73
+ const { data: config, isLoading } = useQuery<OpencodeConfigResponse>({
74
+ queryKey: ['opencode-config'],
75
+ queryFn: async () => {
76
+ const res = await fetch('/api/opencode-config');
77
+ if (!res.ok) {
78
+ throw new Error('Failed to fetch config');
79
+ }
80
+ return res.json();
81
+ },
82
+ });
83
+ const {
84
+ control,
85
+ handleSubmit,
86
+ reset,
87
+ formState: { isSubmitting },
88
+ } = useForm<AgentConfigFormData>({
89
+ defaultValues: {
90
+ model: '',
91
+ temperature: 0.7,
92
+ top_p: 1,
93
+ variant: '',
94
+ prompt_append: '',
95
+ },
96
+ });
97
+
98
+
99
+ const { data: modelsData } = useQuery<OpencodeModelsResponse>({
100
+ queryKey: ['opencode-models'],
101
+ queryFn: async () => {
102
+ const res = await fetch('/api/opencode-models');
103
+ if (!res.ok) throw new Error('Failed to fetch models');
104
+ return res.json();
105
+ },
106
+ });
107
+
108
+ const availableModels = React.useMemo(
109
+ () => new Set(modelsData?.models ?? []),
110
+ [modelsData]
111
+ );
112
+ React.useEffect(() => {
113
+ if (config) {
114
+ const currentAgentConfig = config.agents?.[agentName] || {};
115
+ reset({
116
+ model: currentAgentConfig.model || '',
117
+ temperature: currentAgentConfig.temperature ?? 0.7,
118
+ top_p: currentAgentConfig.top_p ?? 1,
119
+ variant: currentAgentConfig.variant || '',
120
+ prompt_append: currentAgentConfig.prompt_append || '',
121
+ });
122
+ }
123
+ }, [config, agentName, reset]);
124
+
125
+ React.useEffect(() => {
126
+ if (toast) {
127
+ const timer = setTimeout(() => setToast(null), 3000);
128
+ return () => clearTimeout(timer);
129
+ }
130
+ }, [toast]);
131
+
132
+ const saveMutation = useMutation({
133
+ mutationFn: async (data: AgentConfigFormData) => {
134
+ const res = await fetch('/api/opencode-config', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({
138
+ agents: {
139
+ [agentName]: data,
140
+ },
141
+ }),
142
+ });
143
+
144
+ if (!res.ok) {
145
+ const error = await res.json();
146
+ throw new Error(error.error || 'Failed to save config');
147
+ }
148
+
149
+ return res.json();
150
+ },
151
+ onSuccess: () => {
152
+ queryClient.invalidateQueries({ queryKey: ['opencode-config'] });
153
+ setToast({ type: 'success', message: 'Configuration saved successfully' });
154
+ onSaveSuccess?.();
155
+ },
156
+ onError: (error: Error) => {
157
+ setToast({ type: 'error', message: error.message });
158
+ },
159
+ });
160
+
161
+ const onSubmit = (data: AgentConfigFormData) => {
162
+ saveMutation.mutate(data);
163
+ };
164
+
165
+ const currentAgentConfig = config?.agents?.[agentName];
166
+ const hasPresetConfig = !!currentAgentConfig?.model;
167
+
168
+ // Model status checks
169
+ const currentModel = currentAgentConfig?.model;
170
+ const isModelInvalid = currentModel && availableModels.size > 0 && !availableModels.has(currentModel);
171
+ const isModelMissing = !currentModel;
172
+
173
+ const targetCategory = AGENT_CATEGORY_MAP[agentName] || 'unspecified-low';
174
+ const categoryModel = config?.categories?.[targetCategory]?.model;
175
+ const defaultModel = config?.defaultAgent?.model;
176
+
177
+ if (isLoading) {
178
+ return (
179
+ <div className="flex items-center justify-center py-12">
180
+ <Loader2 className="h-6 w-6 animate-spin text-zinc-400" />
181
+ <span className="ml-2 text-sm text-zinc-500">Loading configuration...</span>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-6" aria-label="Agent configuration form">
188
+ {hasPresetConfig && (
189
+ <div className="rounded-lg bg-blue-50 border border-blue-200 px-4 py-3 dark:bg-blue-900/20 dark:border-blue-800">
190
+ <p className="text-sm text-blue-800 dark:text-blue-300">
191
+ <span className="font-medium">Preset applied:</span> This agent is configured with model <span className="font-mono bg-blue-100 dark:bg-blue-800 px-1.5 py-0.5 rounded">{currentAgentConfig.model}</span>
192
+ {currentAgentConfig.variant && <span> (variant: {currentAgentConfig.variant})</span>}
193
+ </p>
194
+ </div>
195
+ )}
196
+
197
+ {toast && (
198
+ <div
199
+ role="alert"
200
+ className={`flex items-center gap-2 rounded-lg px-4 py-3 text-sm ${
201
+ toast.type === 'success'
202
+ ? 'bg-emerald-50 text-emerald-800 dark:bg-emerald-900/20 dark:text-emerald-300'
203
+ : 'bg-red-50 text-red-800 dark:bg-red-900/20 dark:text-red-300'
204
+ }`}
205
+ >
206
+ {toast.type === 'success' ? (
207
+ <Check className="h-4 w-4 shrink-0" aria-hidden="true" />
208
+ ) : (
209
+ <AlertCircle className="h-4 w-4 shrink-0" aria-hidden="true" />
210
+ )}
211
+ <span>{toast.message}</span>
212
+ </div>
213
+ )}
214
+
215
+ {isModelMissing && (
216
+ <div className="flex items-start gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 dark:border-zinc-800 dark:bg-zinc-900/20">
217
+ <AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-zinc-500 dark:text-zinc-400" />
218
+ <div className="text-sm text-zinc-700 dark:text-zinc-300">
219
+ <span className="font-medium">Category fallback</span> — this agent will inherit its model from the <code className="rounded bg-zinc-200 px-1 py-0.5 text-xs dark:bg-zinc-800">{targetCategory}</code> category.
220
+ {categoryModel ? (
221
+ <span className="ml-1">Currently resolving to model <code className="rounded bg-blue-100 px-1 py-0.5 text-xs text-blue-800 dark:bg-blue-900/50 dark:text-blue-200">{categoryModel}</code>.</span>
222
+ ) : defaultModel ? (
223
+ <span className="ml-1">Falling back to Default Agent resolving to <code className="rounded bg-blue-100 px-1 py-0.5 text-xs text-blue-800 dark:bg-blue-900/50 dark:text-blue-200">{defaultModel}</code>.</span>
224
+ ) : (
225
+ <span className="ml-1">Resolving to system default model.</span>
226
+ )}
227
+ <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">Select a model below only if you want to override it.</p>
228
+ </div>
229
+ </div>
230
+ )}
231
+
232
+ {isModelInvalid && (
233
+ <div className="flex items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-800 dark:bg-amber-900/20">
234
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500 dark:text-amber-400" />
235
+ <p className="text-sm text-amber-700 dark:text-amber-300">
236
+ <span className="font-medium">Model unavailable</span> — <code className="rounded bg-amber-100 px-1 py-0.5 text-xs dark:bg-amber-800">{currentModel}</code> is missing from current providers. Please check your provider settings or select a different model.
237
+ </p>
238
+ </div>
239
+ )}
240
+
241
+ <div className="space-y-2">
242
+ <label htmlFor="model-selector" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
243
+ Model
244
+ </label>
245
+ <Controller
246
+ name="model"
247
+ control={control}
248
+ rules={{ required: 'Please select a model' }}
249
+ render={({ field, fieldState }) => (
250
+ <>
251
+ <div id="model-selector">
252
+ <AgentModelSelector
253
+ value={field.value}
254
+ onValueChange={field.onChange}
255
+ placeholder="Select a model..."
256
+ />
257
+ </div>
258
+ {fieldState.error && (
259
+ <p className="text-xs text-red-600 dark:text-red-400" role="alert">
260
+ {fieldState.error.message}
261
+ </p>
262
+ )}
263
+ </>
264
+ )}
265
+ />
266
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
267
+ The AI model used for this agent.
268
+ </p>
269
+ </div>
270
+
271
+ <div className="space-y-2">
272
+ <label htmlFor="variant-selector" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
273
+ Variant
274
+ </label>
275
+ <Controller
276
+ name="variant"
277
+ control={control}
278
+ render={({ field }) => (
279
+ <select
280
+ id="variant-selector"
281
+ value={field.value}
282
+ onChange={(e) => field.onChange(e.target.value)}
283
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm dark:border-zinc-800 dark:bg-zinc-950"
284
+ >
285
+ <option value="">Not set</option>
286
+ <option value="max">max</option>
287
+ <option value="high">high</option>
288
+ <option value="medium">medium</option>
289
+ <option value="low">low</option>
290
+ <option value="xhigh">xhigh</option>
291
+ </select>
292
+ )}
293
+ />
294
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
295
+ Model reasoning variant. Higher values mean more thinking.
296
+ </p>
297
+ </div>
298
+
299
+ <div className="space-y-3">
300
+ <div className="flex items-center justify-between">
301
+ <label htmlFor="temperature-slider" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
302
+ Temperature
303
+ </label>
304
+ <Controller
305
+ name="temperature"
306
+ control={control}
307
+ render={({ field }) => (
308
+ <span className="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
309
+ {field.value.toFixed(1)}
310
+ </span>
311
+ )}
312
+ />
313
+ </div>
314
+ <Controller
315
+ name="temperature"
316
+ control={control}
317
+ render={({ field }) => (
318
+ <div className="flex items-center gap-3">
319
+ <input
320
+ id="temperature-slider"
321
+ type="range"
322
+ min={0}
323
+ max={2}
324
+ step={0.1}
325
+ value={field.value}
326
+ onChange={(e) => field.onChange(parseFloat(e.target.value))}
327
+ className="flex-1 h-2 cursor-pointer appearance-none rounded-lg bg-zinc-200 accent-blue-600 dark:bg-zinc-700"
328
+ aria-label="Temperature slider"
329
+ />
330
+ <input
331
+ type="number"
332
+ min={0}
333
+ max={2}
334
+ step={0.1}
335
+ value={field.value}
336
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
337
+ className="w-16 rounded-lg border border-zinc-200 bg-white px-2 py-1 text-center text-sm dark:border-zinc-800 dark:bg-zinc-950"
338
+ aria-label="Temperature value"
339
+ />
340
+ </div>
341
+ )}
342
+ />
343
+ <div className="flex justify-between text-xs text-zinc-500 dark:text-zinc-400">
344
+ <span>Precise (0)</span>
345
+ <span>Balanced (1)</span>
346
+ <span>Creative (2)</span>
347
+ </div>
348
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
349
+ Controls randomness: lower values make responses more deterministic.
350
+ </p>
351
+ </div>
352
+
353
+ <div className="space-y-3">
354
+ <div className="flex items-center justify-between">
355
+ <label htmlFor="top-p-slider" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
356
+ Top P
357
+ </label>
358
+ <Controller
359
+ name="top_p"
360
+ control={control}
361
+ render={({ field }) => (
362
+ <span className="rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
363
+ {field.value.toFixed(2)}
364
+ </span>
365
+ )}
366
+ />
367
+ </div>
368
+ <Controller
369
+ name="top_p"
370
+ control={control}
371
+ render={({ field }) => (
372
+ <div className="flex items-center gap-3">
373
+ <input
374
+ id="top-p-slider"
375
+ type="range"
376
+ min={0}
377
+ max={1}
378
+ step={0.05}
379
+ value={field.value}
380
+ onChange={(e) => field.onChange(parseFloat(e.target.value))}
381
+ className="flex-1 h-2 cursor-pointer appearance-none rounded-lg bg-zinc-200 accent-blue-600 dark:bg-zinc-700"
382
+ aria-label="Top P slider"
383
+ />
384
+ <input
385
+ type="number"
386
+ min={0}
387
+ max={1}
388
+ step={0.05}
389
+ value={field.value}
390
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
391
+ className="w-16 rounded-lg border border-zinc-200 bg-white px-2 py-1 text-center text-sm dark:border-zinc-800 dark:bg-zinc-950"
392
+ aria-label="Top P value"
393
+ />
394
+ </div>
395
+ )}
396
+ />
397
+ <div className="flex justify-between text-xs text-zinc-500 dark:text-zinc-400">
398
+ <span>Diverse (0)</span>
399
+ <span>Default (1)</span>
400
+ </div>
401
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
402
+ Controls nucleus sampling: lower values sample from more likely tokens.
403
+ </p>
404
+ </div>
405
+
406
+ <div className="space-y-2">
407
+ <label htmlFor="prompt-append" className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
408
+ Prompt Append
409
+ </label>
410
+ <Controller
411
+ name="prompt_append"
412
+ control={control}
413
+ render={({ field }) => (
414
+ <textarea
415
+ id="prompt-append"
416
+ value={field.value}
417
+ onChange={(e) => field.onChange(e.target.value)}
418
+ rows={4}
419
+ className="w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm resize-none dark:border-zinc-800 dark:bg-zinc-950"
420
+ placeholder="Additional system instructions to append..."
421
+ />
422
+ )}
423
+ />
424
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
425
+ Additional instructions appended to the system prompt.
426
+ </p>
427
+ </div>
428
+
429
+ <div className="flex items-center justify-end gap-3 pt-2">
430
+ <button
431
+ type="submit"
432
+ disabled={isSubmitting || saveMutation.isPending}
433
+ className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-blue-600 dark:hover:bg-blue-500"
434
+ >
435
+ {(isSubmitting || saveMutation.isPending) && (
436
+ <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
437
+ )}
438
+ Save Changes
439
+ </button>
440
+ </div>
441
+ </form>
442
+ );
443
+ }
444
+
445
+ export default AgentConfigForm;