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:
Gemini Nexus Bot committed 2026-05-02 00:30:18 -03:00
1 parent 641c739b64
commit 713c4497c6
5 files changed
+321 -68

No files matched your search

+81 -29
View File
@@ -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>
);
};