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,284 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SelectPrimitive from '@radix-ui/react-select';
5
+ import { useQuery } from '@tanstack/react-query';
6
+ import { Check, ChevronDown, Search, AlertCircle, RefreshCw } from 'lucide-react';
7
+ import { clsx, type ClassValue } from 'clsx';
8
+ import { twMerge } from 'tailwind-merge';
9
+
10
+ function cn(...inputs: ClassValue[]) {
11
+ return twMerge(clsx(inputs));
12
+ }
13
+
14
+ interface OpencodeModelsResponse {
15
+ models: string[];
16
+ source: string;
17
+ error?: string;
18
+ }
19
+
20
+ interface AgentModelSelectorProps {
21
+ value?: string;
22
+ onValueChange?: (value: string) => void;
23
+ placeholder?: string;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ const SelectTrigger = React.forwardRef<
28
+ React.ComponentRef<typeof SelectPrimitive.Trigger>,
29
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
30
+ >(({ className, children, ...props }, ref) => (
31
+ <SelectPrimitive.Trigger
32
+ ref={ref}
33
+ className={cn(
34
+ 'flex h-10 w-full items-center justify-between rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm',
35
+ 'ring-offset-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2',
36
+ 'disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950',
37
+ 'dark:placeholder:text-zinc-400 dark:focus:ring-zinc-300',
38
+ className
39
+ )}
40
+ {...props}
41
+ >
42
+ {children}
43
+ <SelectPrimitive.Icon asChild>
44
+ <ChevronDown className="h-4 w-4 opacity-50" />
45
+ </SelectPrimitive.Icon>
46
+ </SelectPrimitive.Trigger>
47
+ ));
48
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
49
+
50
+ const SelectContent = React.forwardRef<
51
+ React.ComponentRef<typeof SelectPrimitive.Content>,
52
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
53
+ >(({ className, children, position = 'popper', ...props }, ref) => (
54
+ <SelectPrimitive.Portal>
55
+ <SelectPrimitive.Content
56
+ ref={ref}
57
+ className={cn(
58
+ 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-zinc-200 bg-white text-zinc-950 shadow-md',
59
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
60
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2',
61
+ 'data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
62
+ 'dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50',
63
+ position === 'popper' &&
64
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
65
+ className
66
+ )}
67
+ position={position}
68
+ {...props}
69
+ >
70
+ <SelectPrimitive.Viewport
71
+ className={cn(
72
+ 'p-1',
73
+ position === 'popper' &&
74
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
75
+ )}
76
+ >
77
+ {children}
78
+ </SelectPrimitive.Viewport>
79
+ </SelectPrimitive.Content>
80
+ </SelectPrimitive.Portal>
81
+ ));
82
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
83
+
84
+ const SelectItem = React.forwardRef<
85
+ React.ComponentRef<typeof SelectPrimitive.Item>,
86
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
87
+ >(({ className, children, ...props }, ref) => (
88
+ <SelectPrimitive.Item
89
+ ref={ref}
90
+ className={cn(
91
+ 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
92
+ 'focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
93
+ 'dark:focus:bg-zinc-800 dark:focus:text-zinc-50',
94
+ className
95
+ )}
96
+ {...props}
97
+ >
98
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
99
+ <SelectPrimitive.ItemIndicator>
100
+ <Check className="h-4 w-4" />
101
+ </SelectPrimitive.ItemIndicator>
102
+ </span>
103
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
104
+ </SelectPrimitive.Item>
105
+ ));
106
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
107
+
108
+ const SelectValue = SelectPrimitive.Value;
109
+
110
+ function parseModelName(model: string): { provider: string; model: string } {
111
+ if (!model) return { provider: 'unknown', model: '' };
112
+
113
+ const slashIndex = model.indexOf('/');
114
+ if (slashIndex !== -1) {
115
+ return {
116
+ provider: model.substring(0, slashIndex),
117
+ model: model.substring(slashIndex + 1)
118
+ };
119
+ }
120
+ return { provider: 'unknown', model };
121
+ }
122
+
123
+ export function AgentModelSelector({
124
+ value,
125
+ onValueChange,
126
+ placeholder = 'Select a model...',
127
+ disabled = false,
128
+ }: AgentModelSelectorProps) {
129
+ const [searchQuery, setSearchQuery] = React.useState('');
130
+
131
+ const { data, isLoading, isError, error, refetch } = useQuery<OpencodeModelsResponse>({
132
+ queryKey: ['opencode-models'],
133
+ queryFn: async () => {
134
+ const res = await fetch('/api/opencode-models');
135
+ const data = await res.json();
136
+ if (!res.ok || data.error) {
137
+ throw new Error(data.error || 'Failed to fetch models');
138
+ }
139
+ return data;
140
+ },
141
+ retry: false,
142
+ });
143
+
144
+ // Ensure the currently selected model is in the list (for echo display)
145
+ const allModels = React.useMemo(() => {
146
+ const models = data?.models ?? [];
147
+ const modelSet = new Set(models);
148
+ if (value && !modelSet.has(value)) {
149
+ modelSet.add(value);
150
+ }
151
+ return Array.from(modelSet).sort();
152
+ }, [data?.models, value]);
153
+
154
+ const filteredModels = React.useMemo(() => {
155
+ if (!searchQuery.trim()) return allModels;
156
+ const query = searchQuery.toLowerCase();
157
+ return allModels.filter((model) => model.toLowerCase().includes(query));
158
+ }, [allModels, searchQuery]);
159
+
160
+ const groupedModels = React.useMemo(() => {
161
+ const groups: Record<string, string[]> = {};
162
+ filteredModels.forEach((model) => {
163
+ const { provider } = parseModelName(model);
164
+ if (!groups[provider]) {
165
+ groups[provider] = [];
166
+ }
167
+ groups[provider].push(model);
168
+ });
169
+ return groups;
170
+ }, [filteredModels]);
171
+
172
+ const providers = Object.keys(groupedModels).sort();
173
+
174
+ const handleOpenChange = React.useCallback((open: boolean) => {
175
+ if (!open) {
176
+ setSearchQuery('');
177
+ }
178
+ }, []);
179
+
180
+ const selectedModel = value ? parseModelName(value) : null;
181
+
182
+ return (
183
+ <SelectPrimitive.Root
184
+ value={value}
185
+ onValueChange={onValueChange}
186
+ disabled={disabled || isLoading}
187
+ onOpenChange={handleOpenChange}
188
+ >
189
+ <SelectTrigger>
190
+ <SelectValue placeholder={placeholder}>
191
+ {selectedModel && (
192
+ <span className="flex items-center gap-2">
193
+ <span className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
194
+ {selectedModel.provider}
195
+ </span>
196
+ <span className="text-zinc-500 dark:text-zinc-400">/</span>
197
+ <span className="text-zinc-900 dark:text-zinc-100">
198
+ {selectedModel.model}
199
+ </span>
200
+ </span>
201
+ )}
202
+ </SelectValue>
203
+ </SelectTrigger>
204
+ <SelectContent>
205
+ <div className="sticky top-0 z-10 border-b border-zinc-200 bg-white px-2 pb-2 pt-1 dark:border-zinc-800 dark:bg-zinc-950">
206
+ <div className="relative">
207
+ <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" />
208
+ <input
209
+ type="text"
210
+ value={searchQuery}
211
+ onChange={(e) => setSearchQuery(e.target.value)}
212
+ placeholder="Search models..."
213
+ className={cn(
214
+ 'h-9 w-full rounded-md border border-zinc-200 bg-transparent pl-8 pr-3 text-sm',
215
+ 'outline-none placeholder:text-zinc-500 focus:border-zinc-400 focus:ring-0',
216
+ 'dark:border-zinc-800 dark:placeholder:text-zinc-400'
217
+ )}
218
+ onKeyDown={(e) => e.stopPropagation()}
219
+ />
220
+ </div>
221
+ </div>
222
+
223
+ <div className="max-h-64 overflow-auto">
224
+ {isLoading ? (
225
+ <div className="px-2 py-4 text-center text-sm text-zinc-500">
226
+ Loading models...
227
+ </div>
228
+ ) : isError ? (
229
+ <div className="px-4 py-6 text-center">
230
+ <div className="flex flex-col items-center gap-3">
231
+ <div className="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
232
+ <AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
233
+ </div>
234
+ <div className="space-y-1">
235
+ <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
236
+ Failed to load models
237
+ </p>
238
+ <p className="text-xs text-zinc-500 dark:text-zinc-400 max-w-[200px]">
239
+ {error instanceof Error ? error.message : 'Please check your OpenCode installation'}
240
+ </p>
241
+ </div>
242
+ <button
243
+ type="button"
244
+ onClick={(e) => {
245
+ e.stopPropagation();
246
+ refetch();
247
+ }}
248
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700 transition-colors"
249
+ >
250
+ <RefreshCw className="h-3.5 w-3.5" />
251
+ Retry
252
+ </button>
253
+ </div>
254
+ </div>
255
+ ) : filteredModels.length === 0 ? (
256
+ <div className="px-2 py-4 text-center text-sm text-zinc-500">
257
+ No models found
258
+ </div>
259
+ ) : (
260
+ providers.map((provider) => (
261
+ <div key={provider}>
262
+ <div className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
263
+ {provider}
264
+ </div>
265
+ {groupedModels[provider].map((model) => {
266
+ const { model: modelName } = parseModelName(model);
267
+ return (
268
+ <SelectItem key={model} value={model}>
269
+ <span className="flex items-center gap-2">
270
+ <span className="text-zinc-900 dark:text-zinc-100">
271
+ {modelName}
272
+ </span>
273
+ </span>
274
+ </SelectItem>
275
+ );
276
+ })}
277
+ </div>
278
+ ))
279
+ )}
280
+ </div>
281
+ </SelectContent>
282
+ </SelectPrimitive.Root>
283
+ );
284
+ }
@@ -0,0 +1,162 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { useQuery } from '@tanstack/react-query';
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/Tabs';
6
+ import { AgentConfigForm } from './AgentConfigForm';
7
+ import { Loader2 } from 'lucide-react';
8
+
9
+ interface AgentConfig {
10
+ model?: string;
11
+ temperature?: number;
12
+ top_p?: number;
13
+ }
14
+
15
+ interface OpencodeConfigResponse {
16
+ agents: Record<string, AgentConfig>;
17
+ }
18
+
19
+ interface AgentDefinition {
20
+ key: string;
21
+ name: string;
22
+ description: string;
23
+ }
24
+
25
+ const PREDEFINED_AGENTS: AgentDefinition[] = [
26
+ {
27
+ key: 'default',
28
+ name: 'Default',
29
+ description: 'Default agent configuration used as fallback for all agents',
30
+ },
31
+ {
32
+ key: 'sisyphus',
33
+ name: 'Sisyphus',
34
+ description: 'Task execution agent - focused on completing specific tasks',
35
+ },
36
+ {
37
+ key: 'hephaestus',
38
+ name: 'Hephaestus',
39
+ description: 'Build and automation agent - handles CI/CD and deployment',
40
+ },
41
+ {
42
+ key: 'prometheus',
43
+ name: 'Prometheus',
44
+ description: 'Planning agent - creates and manages project plans',
45
+ },
46
+ {
47
+ key: 'oracle',
48
+ name: 'Oracle',
49
+ description: 'Knowledge and research agent - provides insights and answers',
50
+ },
51
+ {
52
+ key: 'metis',
53
+ name: 'Metis',
54
+ description: 'Strategy and consultation agent - advises on best practices',
55
+ },
56
+ {
57
+ key: 'momus',
58
+ name: 'Momus',
59
+ description: 'Review and critique agent - evaluates code and decisions',
60
+ },
61
+ {
62
+ key: 'atlas',
63
+ name: 'Atlas',
64
+ description: 'Execution-focused agent for task completion',
65
+ },
66
+ {
67
+ key: 'librarian',
68
+ name: 'Librarian',
69
+ description: 'Documentation and code exploration agent',
70
+ },
71
+ {
72
+ key: 'explore',
73
+ name: 'Explore',
74
+ description: 'Code navigation and discovery agent',
75
+ },
76
+ ];
77
+
78
+ interface AgentsConfigPanelProps {
79
+ onSaveSuccess?: () => void;
80
+ }
81
+
82
+ export function AgentsConfigPanel({ onSaveSuccess }: AgentsConfigPanelProps) {
83
+ const [activeTab, setActiveTab] = React.useState('default');
84
+
85
+ const { data: config, isLoading } = useQuery<OpencodeConfigResponse>({
86
+ queryKey: ['opencode-config'],
87
+ queryFn: async () => {
88
+ const res = await fetch('/api/opencode-config');
89
+ if (!res.ok) {
90
+ throw new Error('Failed to fetch config');
91
+ }
92
+ return res.json();
93
+ },
94
+ });
95
+
96
+ const configuredAgents = React.useMemo(() => {
97
+ const agents = config?.agents || {};
98
+ return PREDEFINED_AGENTS.map((agent) => ({
99
+ ...agent,
100
+ isConfigured: agent.key in agents,
101
+ }));
102
+ }, [config]);
103
+
104
+ if (isLoading) {
105
+ return (
106
+ <div className="flex items-center justify-center py-12">
107
+ <Loader2 className="h-6 w-6 animate-spin text-zinc-400" />
108
+ <span className="ml-2 text-sm text-zinc-500 dark:text-zinc-400">
109
+ Loading agent configurations...
110
+ </span>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <div className="space-y-6">
117
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
118
+ <TabsList className="flex flex-wrap h-auto gap-1 p-1.5">
119
+ {configuredAgents.map((agent) => (
120
+ <TabsTrigger
121
+ key={agent.key}
122
+ value={agent.key}
123
+ className="text-xs px-3 py-1.5 data-[state=active]:bg-white data-[state=active]:text-zinc-900 dark:data-[state=active]:bg-zinc-900 dark:data-[state=active]:text-zinc-50"
124
+ >
125
+ <span className="flex items-center gap-1.5">
126
+ {agent.name}
127
+ {agent.isConfigured && (
128
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
129
+ )}
130
+ </span>
131
+ </TabsTrigger>
132
+ ))}
133
+ </TabsList>
134
+
135
+ {PREDEFINED_AGENTS.map((agent) => (
136
+ <TabsContent
137
+ key={agent.key}
138
+ value={agent.key}
139
+ className="mt-4 focus-visible:outline-none"
140
+ >
141
+ <div className="space-y-4">
142
+ <div className="pb-4 border-b border-zinc-200 dark:border-zinc-700">
143
+ <h3 className="text-base font-semibold text-zinc-900 dark:text-zinc-100">
144
+ {agent.name}
145
+ </h3>
146
+ <p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
147
+ {agent.description}
148
+ </p>
149
+ </div>
150
+ <AgentConfigForm
151
+ agentName={agent.key}
152
+ onSaveSuccess={onSaveSuccess}
153
+ />
154
+ </div>
155
+ </TabsContent>
156
+ ))}
157
+ </Tabs>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ export default AgentsConfigPanel;
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { Settings } from 'lucide-react';
5
+
6
+ interface OpencodeConfigStatus {
7
+ hasConfig: boolean;
8
+ hasPlugin: boolean;
9
+ path?: string;
10
+ }
11
+
12
+ interface ConfigButtonProps {
13
+ onClick: () => void;
14
+ }
15
+
16
+ export function ConfigButton({ onClick }: ConfigButtonProps) {
17
+ const { data: status } = useQuery<OpencodeConfigStatus>({
18
+ queryKey: ['opencode-config', 'status'],
19
+ queryFn: async () => {
20
+ const res = await fetch('/api/opencode-config/status');
21
+ if (!res.ok) {
22
+ throw new Error('Failed to fetch config status');
23
+ }
24
+ return res.json();
25
+ },
26
+ });
27
+
28
+ if (!status?.hasPlugin) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <button
34
+ type="button"
35
+ onClick={onClick}
36
+ className="inline-flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-zinc-800 transition-colors duration-200"
37
+ aria-label="OpenCode Settings"
38
+ title="OpenCode Settings"
39
+ >
40
+ <Settings className="w-5 h-5" />
41
+ </button>
42
+ );
43
+ }
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import * as Dialog from '@radix-ui/react-dialog';
4
+ import { X } from 'lucide-react';
5
+ import { AgentsConfigPanel } from './AgentsConfigPanel';
6
+
7
+ interface ConfigPanelProps {
8
+ open: boolean;
9
+ onOpenChange: (open: boolean) => void;
10
+ title?: string;
11
+ description?: string;
12
+ }
13
+
14
+ export function ConfigPanel({
15
+ open,
16
+ onOpenChange,
17
+ title = 'Configuration',
18
+ description,
19
+ }: ConfigPanelProps) {
20
+ return (
21
+ <Dialog.Root open={open} onOpenChange={onOpenChange}>
22
+ <Dialog.Portal>
23
+ <Dialog.Overlay
24
+ className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
25
+ />
26
+
27
+ <Dialog.Content
28
+ className="fixed z-50 gap-4 bg-white dark:bg-zinc-800 p-0 shadow-2xl outline-none
29
+ inset-x-0 bottom-0 rounded-t-xl border-t border-gray-200 dark:border-zinc-700
30
+ h-[85vh] flex flex-col
31
+ sm:inset-auto sm:left-1/2 sm:top-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2
32
+ sm:w-full sm:max-w-lg sm:h-auto sm:max-h-[85vh] sm:rounded-xl sm:border"
33
+ >
34
+ <div className="flex items-center justify-between px-5 py-4 border-b border-gray-100 dark:border-zinc-700/50 flex-shrink-0">
35
+ <div className="flex flex-col gap-0.5">
36
+ <Dialog.Title className="text-base font-semibold text-gray-900 dark:text-gray-100">
37
+ {title}
38
+ </Dialog.Title>
39
+ {description && (
40
+ <Dialog.Description className="text-sm text-gray-500 dark:text-gray-400">
41
+ {description}
42
+ </Dialog.Description>
43
+ )}
44
+ </div>
45
+ <Dialog.Close asChild>
46
+ <button
47
+ type="button"
48
+ className="w-7 h-7 flex items-center justify-center rounded-md text-gray-400
49
+ hover:text-gray-600 hover:bg-gray-100
50
+ dark:text-gray-500 dark:hover:text-gray-300 dark:hover:bg-zinc-700
51
+ transition-colors"
52
+ aria-label="Close panel"
53
+ >
54
+ <X className="w-4 h-4" />
55
+ </button>
56
+ </Dialog.Close>
57
+ </div>
58
+
59
+ <div className="flex-1 overflow-y-auto p-5 scrollbar-thin">
60
+ <AgentsConfigPanel />
61
+ </div>
62
+
63
+ <div className="flex items-center justify-end gap-2 px-5 py-4 border-t border-gray-100 dark:border-zinc-700/50 bg-gray-50/50 dark:bg-zinc-800/50 flex-shrink-0">
64
+ <Dialog.Close asChild>
65
+ <button
66
+ type="button"
67
+ className="px-3 py-1.5 text-xs font-medium rounded-md text-gray-600
68
+ hover:bg-gray-200 hover:text-gray-800
69
+ dark:text-gray-400 dark:hover:bg-zinc-700 dark:hover:text-gray-200
70
+ transition-colors"
71
+ >
72
+ Cancel
73
+ </button>
74
+ </Dialog.Close>
75
+ <button
76
+ type="button"
77
+ className="px-3 py-1.5 text-xs font-medium rounded-md bg-blue-600 text-white
78
+ hover:bg-blue-700 shadow-sm
79
+ dark:bg-blue-600 dark:hover:bg-blue-500
80
+ transition-colors"
81
+ >
82
+ Save Changes
83
+ </button>
84
+ </div>
85
+ </Dialog.Content>
86
+ </Dialog.Portal>
87
+ </Dialog.Root>
88
+ );
89
+ }
90
+
91
+ export default ConfigPanel;