feat(proxy): add OpenCode IDE config and fix service availability
- Add opencode to SupportedIde with JSON providers.json output - Add defaultModel parameter for IDE config generation - Add model selector dropdown in ToolsTab for IDE configurations - Use getServiceOptional for graceful degradation when proxy is stopped - Add fallback IDE config generation using ConfigManager when NestJS is offline - Update IPC router schema to accept defaultModel parameter
This commit is contained in:
1 parent
641c739b64
commit
713c4497c6
5 files changed
+321
-68
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useQueries } from '@tanstack/react-query';
|
||||
import { ipc } from '@/ipc/manager';
|
||||
@@ -6,17 +6,42 @@ import { ipc } from '@/ipc/manager';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { StatCard } from '@/components/usage/StatCard';
|
||||
import { Loader2, Copy, CheckCircle, ShieldAlert, XCircle, Terminal, Code2, Braces } from 'lucide-react';
|
||||
import {
|
||||
Loader2,
|
||||
Copy,
|
||||
CheckCircle,
|
||||
ShieldAlert,
|
||||
XCircle,
|
||||
Terminal,
|
||||
Code2,
|
||||
Braces,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
const IDE_CONFIGS = [
|
||||
{ key: 'opencode', label: 'OpenCode', icon: Sparkles },
|
||||
{ key: 'vscode', label: 'VS Code', icon: Code2 },
|
||||
{ key: 'cursor', label: 'Cursor', icon: Terminal },
|
||||
{ key: 'claude-code', label: 'Claude Code', icon: Braces },
|
||||
] as const;
|
||||
|
||||
export const ToolsTab: React.FC = () => {
|
||||
interface ToolsTabProps {
|
||||
availableModelIds?: string[];
|
||||
}
|
||||
|
||||
export const ToolsTab: React.FC<ToolsTabProps> = ({
|
||||
availableModelIds = ['gemini-3.1-pro-high'],
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [selectedModel, setSelectedModel] = useState<string>(availableModelIds[0]);
|
||||
|
||||
const { data: parityData, isLoading: parityLoading } = useQuery({
|
||||
queryKey: ['proxyAdvanced', 'parityCounters'],
|
||||
@@ -25,16 +50,11 @@ export const ToolsTab: React.FC = () => {
|
||||
});
|
||||
|
||||
const ideQueries = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', 'vscode'],
|
||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'vscode' }),
|
||||
},
|
||||
{
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', 'cursor'],
|
||||
queryFn: () => ipc.client.proxyAdvanced.generateIdeConfig({ ide: 'cursor' }),
|
||||
},
|
||||
],
|
||||
queries: IDE_CONFIGS.map(({ key }) => ({
|
||||
queryKey: ['proxyAdvanced', 'ideConfig', key, selectedModel],
|
||||
queryFn: () =>
|
||||
ipc.client.proxyAdvanced.generateIdeConfig({ ide: key, defaultModel: selectedModel }),
|
||||
})),
|
||||
});
|
||||
|
||||
const parity = parityData?.success ? parityData.data : undefined;
|
||||
@@ -44,15 +64,21 @@ export const ToolsTab: React.FC = () => {
|
||||
toast({ title: t('proxy.advanced.tools.copyConfig') });
|
||||
};
|
||||
|
||||
const getIdeInstructions = (key: string): string => {
|
||||
const getIdeInstructions = (key: string, index: number): string => {
|
||||
if (key === 'claude-code') {
|
||||
return 'export ANTHROPIC_BASE_URL=http://localhost:8045/v1\nexport ANTHROPIC_API_KEY=YOUR_API_KEY';
|
||||
}
|
||||
const index = key === 'vscode' ? 0 : 1;
|
||||
const result = ideQueries[index].data;
|
||||
const result = ideQueries[index]?.data;
|
||||
if (result?.success && result.data) {
|
||||
const d = result.data;
|
||||
return `${d.instructions}\n${d.proxySetting}: http://localhost:8045\n${d.apiKeySetting}: YOUR_API_KEY`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const d = result.data as any;
|
||||
if (d.format === 'json') {
|
||||
return JSON.stringify(d.content, null, 2);
|
||||
}
|
||||
if (d.format === 'env' || d.format === 'shell') {
|
||||
return typeof d.content === 'string' ? d.content : JSON.stringify(d.content, null, 2);
|
||||
}
|
||||
return `${d.instructions || ''}\n${d.proxySetting || 'URL'}: http://localhost:8045\n${d.apiKeySetting || 'Key'}: YOUR_API_KEY`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -87,24 +113,45 @@ export const ToolsTab: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* IDE Quick Setup */}
|
||||
<div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
|
||||
<div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
||||
<p className="text-xs text-muted-foreground">Quick configuration for popular IDEs</p>
|
||||
<div className="bg-card overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
|
||||
<p className="text-muted-foreground text-xs">Quick configuration for popular IDEs</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">Default Model:</span>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger className="h-8 w-[180px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableModelIds.map((id) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
{id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="space-y-3 p-5">
|
||||
{isIdeLoading ? (
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
IDE_CONFIGS.map(({ key, label, icon: Icon }) => {
|
||||
const instructions = getIdeInstructions(key);
|
||||
IDE_CONFIGS.map(({ key, label, icon: Icon }, index) => {
|
||||
const instructions = getIdeInstructions(key, index);
|
||||
const resultData = ideQueries[index]?.data;
|
||||
const hasError = resultData && !resultData.success;
|
||||
const errorMsg = resultData?.error || '';
|
||||
|
||||
return (
|
||||
<div key={key} className="overflow-hidden rounded-xl border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
<Icon className="text-muted-foreground h-4 w-4" />
|
||||
<span className="text-[13px] font-medium">{label}</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -112,13 +159,19 @@ export const ToolsTab: React.FC = () => {
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
onClick={() => copyToClipboard(instructions)}
|
||||
disabled={!instructions || hasError}
|
||||
>
|
||||
<Copy className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t('proxy.advanced.tools.copyConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
{instructions && (
|
||||
<pre className="overflow-x-auto border-t border-white/[0.06] bg-card p-3 font-mono text-xs whitespace-pre-wrap text-foreground/90">
|
||||
{hasError && (
|
||||
<div className="bg-card border-t border-white/[0.06] p-3 font-mono text-xs text-amber-500/80">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{instructions && !hasError && (
|
||||
<pre className="bg-card text-foreground/90 overflow-x-auto border-t border-white/[0.06] p-3 font-mono text-xs whitespace-pre-wrap">
|
||||
{instructions}
|
||||
</pre>
|
||||
)}
|
||||
@@ -128,7 +181,6 @@ export const ToolsTab: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user