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.
- package/README.md +7 -13
- package/bin/vibepulse.js +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/session-status-detection.md +258 -0
- package/next.config.ts +11 -0
- package/package.json +17 -11
- 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,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;
|