settings refactor
This commit is contained in:
+215
@@ -0,0 +1,215 @@
|
||||
import { useState } from 'react';
|
||||
import { Copy, Check } from 'lucide-react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useServerSettings } from '@/state/useServerSettings';
|
||||
|
||||
type VersionInfo = { version: string | null; path: string | null; globalPath: string | null };
|
||||
type ClaudeAuthInfo = { authenticated: boolean; loggedIn?: boolean; subscriptionType?: string };
|
||||
type OpencodeAuthInfo = { authenticated: boolean; providers: string[] };
|
||||
|
||||
export const AIHarnessesSection = () => {
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const { aiHarnesses, saveSettings } = useServerSettings();
|
||||
const [installing, setInstalling] = useState<{ claudeCode: boolean; opencode: boolean }>({
|
||||
claudeCode: false,
|
||||
opencode: false,
|
||||
});
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const { data: opencodeVersion, isLoading: opencodeLoading } = useQuery({
|
||||
queryKey: ['OPENCODE_VERSION'],
|
||||
queryFn: () => client.get<VersionInfo>('/server-settings/opencode/version'),
|
||||
enabled: !!aiHarnesses?.opencode,
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data;
|
||||
return data?.version && !data?.globalPath ? 1000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: claudeVersion, isLoading: claudeLoading } = useQuery({
|
||||
queryKey: ['CLAUDE_CODE_VERSION'],
|
||||
queryFn: () => client.get<VersionInfo>('/server-settings/claude-code/version'),
|
||||
enabled: !!aiHarnesses?.claudeCode,
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data;
|
||||
return data?.version && !data?.globalPath ? 1000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: opencodeAuth } = useQuery({
|
||||
queryKey: ['OPENCODE_AUTH'],
|
||||
queryFn: () => client.get<OpencodeAuthInfo>('/server-settings/opencode/auth'),
|
||||
enabled: !!opencodeVersion?.version,
|
||||
refetchInterval: (query) => (!query.state.data?.authenticated ? 2000 : false),
|
||||
});
|
||||
|
||||
const { data: claudeAuth } = useQuery({
|
||||
queryKey: ['CLAUDE_CODE_AUTH'],
|
||||
queryFn: () => client.get<ClaudeAuthInfo>('/server-settings/claude-code/auth'),
|
||||
enabled: !!claudeVersion?.version,
|
||||
refetchInterval: (query) => (!query.state.data?.authenticated ? 2000 : false),
|
||||
});
|
||||
|
||||
const toggleHarness = (key: 'claudeCode' | 'opencode', checked: boolean) => {
|
||||
const updated = { ...aiHarnesses, [key]: checked };
|
||||
saveSettings({ aiHarnesses: updated });
|
||||
};
|
||||
|
||||
const installClaude = async () => {
|
||||
setInstalling((prev) => ({ ...prev, claudeCode: true }));
|
||||
try {
|
||||
const result = await client.post<VersionInfo>('/server-settings/claude-code/install');
|
||||
queryClient.setQueryData(['CLAUDE_CODE_VERSION'], result);
|
||||
} finally {
|
||||
setInstalling((prev) => ({ ...prev, claudeCode: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const installOpencode = async () => {
|
||||
setInstalling((prev) => ({ ...prev, opencode: true }));
|
||||
try {
|
||||
const result = await client.post<VersionInfo>('/server-settings/opencode/install');
|
||||
queryClient.setQueryData(['OPENCODE_VERSION'], result);
|
||||
} finally {
|
||||
setInstalling((prev) => ({ ...prev, opencode: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(text);
|
||||
setTimeout(() => setCopied(null), 1500);
|
||||
};
|
||||
|
||||
const CopyCommand = ({ command }: { command: string }) => (
|
||||
<div className="mt-2 text-xs text-amber-600">
|
||||
Not globally accessible. Run:
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<code className="flex-1 bg-duck-dark/5 rounded px-2 py-1 text-duck-dark/70">{command}</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(command)}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
|
||||
>
|
||||
{copied === command ? (
|
||||
<Check className="h-3.5 w-3.5 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5 text-duck-dark/50" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={!!aiHarnesses?.opencode}
|
||||
onCheckedChange={(checked) => toggleHarness('opencode', !!checked)}
|
||||
/>
|
||||
<span className="text-sm font-medium text-duck-dark">Opencode</span>
|
||||
</label>
|
||||
{aiHarnesses?.opencode && (
|
||||
<div className="ml-7 mt-2 text-xs text-duck-dark/50">
|
||||
{opencodeLoading ? (
|
||||
'Checking version...'
|
||||
) : opencodeVersion?.version ? (
|
||||
<>
|
||||
<div>{opencodeVersion.version}</div>
|
||||
<div>{opencodeVersion.path}</div>
|
||||
{opencodeAuth && (
|
||||
<div className={`mt-1 ${opencodeAuth.authenticated ? 'text-green-600' : 'text-amber-600'}`}>
|
||||
{opencodeAuth.authenticated ? (
|
||||
`Logged in (${opencodeAuth.providers.join(', ')})`
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Not logged in</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-duck-teal text-duck-yellow hover:bg-duck-teal/90"
|
||||
onClick={() => client.post('/server-settings/opencode/auth/login')}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!opencodeVersion.globalPath && opencodeVersion.path && (
|
||||
<CopyCommand command={`sudo ln -s ${opencodeVersion.path} /usr/local/bin/opencode`} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-duck-teal text-duck-yellow hover:bg-duck-teal/90"
|
||||
onClick={installOpencode}
|
||||
disabled={installing.opencode}
|
||||
>
|
||||
{installing.opencode ? 'Installing...' : 'Install'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<Checkbox
|
||||
checked={!!aiHarnesses?.claudeCode}
|
||||
onCheckedChange={(checked) => toggleHarness('claudeCode', !!checked)}
|
||||
/>
|
||||
<span className="text-sm font-medium text-duck-dark">Claude Code</span>
|
||||
</label>
|
||||
{aiHarnesses?.claudeCode && (
|
||||
<div className="ml-7 mt-2 text-xs text-duck-dark/50">
|
||||
{claudeLoading ? (
|
||||
'Checking version...'
|
||||
) : claudeVersion?.version ? (
|
||||
<>
|
||||
<div>{claudeVersion.version}</div>
|
||||
<div>{claudeVersion.path}</div>
|
||||
{claudeAuth && (
|
||||
<div className={`mt-1 ${claudeAuth.authenticated ? 'text-green-600' : 'text-amber-600'}`}>
|
||||
{claudeAuth.authenticated ? (
|
||||
`Logged in (${claudeAuth.subscriptionType ?? 'unknown plan'})`
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Not logged in</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-duck-teal text-duck-yellow hover:bg-duck-teal/90"
|
||||
onClick={() => client.post('/server-settings/claude-code/auth/login')}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!claudeVersion.globalPath && claudeVersion.path && (
|
||||
<CopyCommand command={`sudo ln -s ${claudeVersion.path} /usr/local/bin/claude`} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-duck-teal text-duck-yellow hover:bg-duck-teal/90"
|
||||
onClick={installClaude}
|
||||
disabled={installing.claudeCode}
|
||||
>
|
||||
{installing.claudeCode ? 'Installing...' : 'Install'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user