mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 04:14:57 +00:00
feat(websearch): add model config + improve hook UX
- Add model field to Gemini/OpenCode WebSearch providers - Config.yaml now explicitly stores model (gemini-2.5-flash, opencode/grok-code) - UI: blur-to-save for model inputs (no save on every keystroke) - UI: collapsible install hints inside provider cards - UI: left accent border for enabled providers (better light theme contrast) - Hook: use systemMessage for info-styled confirmation - Hook: cleaner result format without defensive "NOT an error" language
This commit is contained in:
1 parent
6d69379ead
commit
14c53d575f
6 files changed
+342
-184
No files matched your search
@@ -11,6 +11,7 @@
|
|||||||
* CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1)
|
* CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1)
|
||||||
* CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55)
|
* CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55)
|
||||||
* CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider
|
* CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider
|
||||||
|
* CCS_WEBSEARCH_GEMINI_MODEL - Gemini model (default: gemini-2.5-flash)
|
||||||
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
|
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
|
||||||
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider
|
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider
|
||||||
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code)
|
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code)
|
||||||
@@ -258,13 +259,16 @@ function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|||||||
const config = PROVIDER_CONFIG.gemini;
|
const config = PROVIDER_CONFIG.gemini;
|
||||||
const prompt = buildPrompt('gemini', query);
|
const prompt = buildPrompt('gemini', query);
|
||||||
|
|
||||||
|
// Allow model override via env var
|
||||||
|
const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
|
||||||
|
|
||||||
if (process.env.CCS_DEBUG) {
|
if (process.env.CCS_DEBUG) {
|
||||||
console.error(`[CCS Hook] Executing: gemini --model ${config.model} --yolo -p "..."`);
|
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const spawnResult = spawnSync(
|
const spawnResult = spawnSync(
|
||||||
'gemini',
|
'gemini',
|
||||||
['--model', config.model, '--yolo', '-p', prompt],
|
['--model', model, '--yolo', '-p', prompt],
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
@@ -441,29 +445,40 @@ function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|||||||
*/
|
*/
|
||||||
function formatSearchResults(query, content, providerName) {
|
function formatSearchResults(query, content, providerName) {
|
||||||
return [
|
return [
|
||||||
'=== WEBSEARCH COMPLETED SUCCESSFULLY ===',
|
`[WebSearch Result via ${providerName}]`,
|
||||||
`(via ${providerName} - this is NOT an error)`,
|
|
||||||
'',
|
'',
|
||||||
`Query: "${query}"`,
|
`Query: "${query}"`,
|
||||||
'',
|
'',
|
||||||
content,
|
content,
|
||||||
'',
|
'',
|
||||||
'=========================================',
|
'---',
|
||||||
'Use this information to answer the user. Search again if needed.',
|
'Use this information to answer the user.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output success response and exit
|
* Output success response and exit
|
||||||
|
*
|
||||||
|
* Key insight from Claude Code docs:
|
||||||
|
* - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
|
||||||
|
* - systemMessage → shown to USER only (nice styling but AI doesn't see)
|
||||||
|
*
|
||||||
|
* So we MUST put results in permissionDecisionReason for Claude to use them.
|
||||||
|
* systemMessage provides the nice UI for the user.
|
||||||
*/
|
*/
|
||||||
function outputSuccess(query, content, providerName) {
|
function outputSuccess(query, content, providerName) {
|
||||||
|
const formattedResults = formatSearchResults(query, content, providerName);
|
||||||
|
|
||||||
const output = {
|
const output = {
|
||||||
decision: 'block',
|
decision: 'block',
|
||||||
reason: `WebSearch completed via ${providerName}`,
|
reason: `WebSearch completed via ${providerName}`,
|
||||||
|
// Nice message for user (shows as "says:" - info style)
|
||||||
|
systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
|
||||||
hookSpecificOutput: {
|
hookSpecificOutput: {
|
||||||
hookEventName: 'PreToolUse',
|
hookEventName: 'PreToolUse',
|
||||||
permissionDecision: 'deny',
|
permissionDecision: 'deny',
|
||||||
permissionDecisionReason: formatSearchResults(query, content, providerName),
|
// Full results here - Claude reads this
|
||||||
|
permissionDecisionReason: formattedResults,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,17 @@ export function loadUnifiedConfig(): UnifiedConfig | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-upgrade if version is outdated (regenerates YAML with new comments)
|
// Auto-upgrade if version is outdated (regenerates YAML with new comments and fields)
|
||||||
if ((parsed.version ?? 1) < UNIFIED_CONFIG_VERSION) {
|
if ((parsed.version ?? 1) < UNIFIED_CONFIG_VERSION) {
|
||||||
parsed.version = UNIFIED_CONFIG_VERSION;
|
// Merge with defaults to add new fields (e.g., model for websearch providers)
|
||||||
|
const upgraded = mergeWithDefaults(parsed);
|
||||||
|
upgraded.version = UNIFIED_CONFIG_VERSION;
|
||||||
try {
|
try {
|
||||||
saveUnifiedConfig(parsed);
|
saveUnifiedConfig(upgraded);
|
||||||
if (process.env.CCS_DEBUG) {
|
if (process.env.CCS_DEBUG) {
|
||||||
console.error(`[i] Config upgraded to v${UNIFIED_CONFIG_VERSION}`);
|
console.error(`[i] Config upgraded to v${UNIFIED_CONFIG_VERSION}`);
|
||||||
}
|
}
|
||||||
|
return upgraded;
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore save errors during upgrade - config still works
|
// Ignore save errors during upgrade - config still works
|
||||||
}
|
}
|
||||||
@@ -137,6 +140,7 @@ function mergeWithDefaults(partial: Partial<UnifiedConfig>): UnifiedConfig {
|
|||||||
partial.websearch?.providers?.gemini?.enabled ??
|
partial.websearch?.providers?.gemini?.enabled ??
|
||||||
partial.websearch?.gemini?.enabled ?? // Legacy fallback
|
partial.websearch?.gemini?.enabled ?? // Legacy fallback
|
||||||
true,
|
true,
|
||||||
|
model: partial.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
|
||||||
timeout:
|
timeout:
|
||||||
partial.websearch?.providers?.gemini?.timeout ??
|
partial.websearch?.providers?.gemini?.timeout ??
|
||||||
partial.websearch?.gemini?.timeout ?? // Legacy fallback
|
partial.websearch?.gemini?.timeout ?? // Legacy fallback
|
||||||
@@ -277,12 +281,18 @@ function generateYamlWithComments(config: UnifiedConfig): string {
|
|||||||
lines.push("# Anthropic's WebSearch tool. These CLI tools provide fallback web search.");
|
lines.push("# Anthropic's WebSearch tool. These CLI tools provide fallback web search.");
|
||||||
lines.push('#');
|
lines.push('#');
|
||||||
lines.push('# Fallback chain: Gemini -> OpenCode -> Grok (tries in order until success)');
|
lines.push('# Fallback chain: Gemini -> OpenCode -> Grok (tries in order until success)');
|
||||||
lines.push('# providers:');
|
|
||||||
lines.push('# gemini: Gemini CLI (FREE) - npm i -g @google/gemini-cli');
|
|
||||||
lines.push('# opencode: OpenCode (FREE) - curl -fsSL https://opencode.ai/install | bash');
|
|
||||||
lines.push('# grok: Grok CLI (paid) - npm i -g @vibe-kit/grok-cli (needs GROK_API_KEY)');
|
|
||||||
lines.push('#');
|
lines.push('#');
|
||||||
lines.push('# enabled: Master switch - auto-disables when no providers enabled');
|
lines.push(
|
||||||
|
'# Gemini models: gemini-2.5-flash (default), gemini-2.5-pro, gemini-2.5-flash-lite'
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
'# OpenCode models: opencode/grok-code (default), opencode/gpt-4o, opencode/claude-3.5-sonnet'
|
||||||
|
);
|
||||||
|
lines.push('#');
|
||||||
|
lines.push('# Install commands:');
|
||||||
|
lines.push('# gemini: npm i -g @google/gemini-cli (FREE - 1000 req/day)');
|
||||||
|
lines.push('# opencode: curl -fsSL https://opencode.ai/install | bash (FREE via Zen)');
|
||||||
|
lines.push('# grok: npm i -g @vibe-kit/grok-cli (requires GROK_API_KEY)');
|
||||||
lines.push('# ----------------------------------------------------------------------------');
|
lines.push('# ----------------------------------------------------------------------------');
|
||||||
lines.push(
|
lines.push(
|
||||||
yaml
|
yaml
|
||||||
@@ -362,6 +372,7 @@ export function setDefaultProfile(name: string): void {
|
|||||||
*/
|
*/
|
||||||
export interface GeminiWebSearchInfo {
|
export interface GeminiWebSearchInfo {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
model: string;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +397,7 @@ export function getWebSearchConfig(): {
|
|||||||
const geminiConfig: GeminiWebSearchInfo = {
|
const geminiConfig: GeminiWebSearchInfo = {
|
||||||
enabled:
|
enabled:
|
||||||
config.websearch?.providers?.gemini?.enabled ?? config.websearch?.gemini?.enabled ?? true,
|
config.websearch?.providers?.gemini?.enabled ?? config.websearch?.gemini?.enabled ?? true,
|
||||||
|
model: config.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
|
||||||
timeout:
|
timeout:
|
||||||
config.websearch?.providers?.gemini?.timeout ?? config.websearch?.gemini?.timeout ?? 55,
|
config.websearch?.providers?.gemini?.timeout ?? config.websearch?.gemini?.timeout ?? 55,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
/**
|
/**
|
||||||
* Unified config version.
|
* Unified config version.
|
||||||
* Version 2 = YAML unified format
|
* Version 2 = YAML unified format
|
||||||
* Version 3 = WebSearch config comments update
|
* Version 3 = WebSearch config with model configuration for Gemini/OpenCode
|
||||||
*/
|
*/
|
||||||
export const UNIFIED_CONFIG_VERSION = 3;
|
export const UNIFIED_CONFIG_VERSION = 3;
|
||||||
|
|
||||||
@@ -107,6 +107,8 @@ export interface PreferencesConfig {
|
|||||||
export interface GeminiWebSearchConfig {
|
export interface GeminiWebSearchConfig {
|
||||||
/** Enable Gemini CLI for WebSearch (default: true) */
|
/** Enable Gemini CLI for WebSearch (default: true) */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
/** Model to use (default: gemini-2.5-flash) */
|
||||||
|
model?: string;
|
||||||
/** Timeout in seconds (default: 55) */
|
/** Timeout in seconds (default: 55) */
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
@@ -237,6 +239,16 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
|
|||||||
providers: {
|
providers: {
|
||||||
gemini: {
|
gemini: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
model: 'gemini-2.5-flash',
|
||||||
|
timeout: 55,
|
||||||
|
},
|
||||||
|
opencode: {
|
||||||
|
enabled: false,
|
||||||
|
model: 'opencode/grok-code',
|
||||||
|
timeout: 90,
|
||||||
|
},
|
||||||
|
grok: {
|
||||||
|
enabled: false,
|
||||||
timeout: 55,
|
timeout: 55,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -654,6 +654,9 @@ export function getWebSearchHookEnv(): Record<string, string> {
|
|||||||
// Hook will only use providers that are BOTH enabled AND installed
|
// Hook will only use providers that are BOTH enabled AND installed
|
||||||
if (wsConfig.providers?.gemini?.enabled) {
|
if (wsConfig.providers?.gemini?.enabled) {
|
||||||
env.CCS_WEBSEARCH_GEMINI = '1';
|
env.CCS_WEBSEARCH_GEMINI = '1';
|
||||||
|
if (wsConfig.providers.gemini.model) {
|
||||||
|
env.CCS_WEBSEARCH_GEMINI_MODEL = wsConfig.providers.gemini.model;
|
||||||
|
}
|
||||||
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.gemini.timeout || 55);
|
env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.gemini.timeout || 55);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1477,6 +1477,10 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
|
|||||||
providers.gemini?.enabled ??
|
providers.gemini?.enabled ??
|
||||||
existingConfig.websearch?.providers?.gemini?.enabled ??
|
existingConfig.websearch?.providers?.gemini?.enabled ??
|
||||||
true,
|
true,
|
||||||
|
model:
|
||||||
|
providers.gemini?.model ??
|
||||||
|
existingConfig.websearch?.providers?.gemini?.model ??
|
||||||
|
'gemini-2.5-flash',
|
||||||
timeout:
|
timeout:
|
||||||
providers.gemini?.timeout ??
|
providers.gemini?.timeout ??
|
||||||
existingConfig.websearch?.providers?.gemini?.timeout ??
|
existingConfig.websearch?.providers?.gemini?.timeout ??
|
||||||
|
|||||||
+280
-168
@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
Globe,
|
Globe,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -20,22 +21,21 @@ import {
|
|||||||
GripVertical,
|
GripVertical,
|
||||||
Terminal,
|
Terminal,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { CodeEditor } from '@/components/code-editor';
|
import { CodeEditor } from '@/components/code-editor';
|
||||||
|
|
||||||
interface ProviderConfig {
|
interface ProviderConfig {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OpenCodeProviderConfig extends ProviderConfig {
|
|
||||||
model?: string;
|
model?: string;
|
||||||
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSearchProvidersConfig {
|
interface WebSearchProvidersConfig {
|
||||||
gemini?: ProviderConfig;
|
gemini?: ProviderConfig;
|
||||||
grok?: ProviderConfig;
|
grok?: ProviderConfig;
|
||||||
opencode?: OpenCodeProviderConfig;
|
opencode?: ProviderConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSearchConfig {
|
interface WebSearchConfig {
|
||||||
@@ -71,6 +71,13 @@ export function SettingsPage() {
|
|||||||
const [rawConfig, setRawConfig] = useState<string | null>(null);
|
const [rawConfig, setRawConfig] = useState<string | null>(null);
|
||||||
const [rawConfigLoading, setRawConfigLoading] = useState(false);
|
const [rawConfigLoading, setRawConfigLoading] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
// Local model input state (to avoid saving on every keystroke)
|
||||||
|
const [geminiModelInput, setGeminiModelInput] = useState('');
|
||||||
|
const [opencodeModelInput, setOpencodeModelInput] = useState('');
|
||||||
|
// Collapsible install hints state
|
||||||
|
const [showGeminiHint, setShowGeminiHint] = useState(false);
|
||||||
|
const [showOpencodeHint, setShowOpencodeHint] = useState(false);
|
||||||
|
const [showGrokHint, setShowGrokHint] = useState(false);
|
||||||
|
|
||||||
// Load config and status on mount
|
// Load config and status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,6 +86,14 @@ export function SettingsPage() {
|
|||||||
fetchRawConfig();
|
fetchRawConfig();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Sync local model inputs when config changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (config) {
|
||||||
|
setGeminiModelInput(config.providers?.gemini?.model ?? 'gemini-2.5-flash');
|
||||||
|
setOpencodeModelInput(config.providers?.opencode?.model ?? 'opencode/grok-code');
|
||||||
|
}
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -235,6 +250,40 @@ export function SettingsPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save Gemini model on blur (only if changed)
|
||||||
|
const saveGeminiModel = () => {
|
||||||
|
const currentModel = config?.providers?.gemini?.model ?? 'gemini-2.5-flash';
|
||||||
|
if (geminiModelInput !== currentModel) {
|
||||||
|
const providers = config?.providers || {};
|
||||||
|
saveConfig({
|
||||||
|
providers: {
|
||||||
|
...providers,
|
||||||
|
gemini: {
|
||||||
|
...providers.gemini,
|
||||||
|
model: geminiModelInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save OpenCode model on blur (only if changed)
|
||||||
|
const saveOpencodeModel = () => {
|
||||||
|
const currentModel = config?.providers?.opencode?.model ?? 'opencode/grok-code';
|
||||||
|
if (opencodeModelInput !== currentModel) {
|
||||||
|
const providers = config?.providers || {};
|
||||||
|
saveConfig({
|
||||||
|
providers: {
|
||||||
|
...providers,
|
||||||
|
opencode: {
|
||||||
|
...providers.opencode,
|
||||||
|
model: opencodeModelInput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-100px)] flex items-center justify-center">
|
<div className="h-[calc(100vh-100px)] flex items-center justify-center">
|
||||||
@@ -313,201 +362,264 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
{/* Gemini CLI Provider */}
|
{/* Gemini CLI Provider */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
|
className={`rounded-lg border transition-colors ${
|
||||||
isGeminiEnabled
|
isGeminiEnabled ? 'border-primary border-l-4' : 'border-border'
|
||||||
? 'border-primary/50 bg-primary/5'
|
|
||||||
: 'border-border bg-background'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between p-4">
|
||||||
<Terminal
|
<div className="flex items-center gap-3">
|
||||||
className={`w-5 h-5 ${isGeminiEnabled ? 'text-primary' : 'text-muted-foreground'}`}
|
<Terminal
|
||||||
/>
|
className={`w-5 h-5 ${isGeminiEnabled ? 'text-primary' : 'text-muted-foreground'}`}
|
||||||
<div>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<p className="font-mono font-medium">gemini</p>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
<p className="font-mono font-medium">gemini</p>
|
||||||
FREE
|
|
||||||
</span>
|
|
||||||
{status?.geminiCli?.installed ? (
|
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
||||||
installed
|
FREE
|
||||||
</span>
|
</span>
|
||||||
) : (
|
{status?.geminiCli?.installed ? (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
||||||
not installed
|
installed
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
|
||||||
|
not installed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Google Gemini CLI (1000 req/day free)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Google Gemini CLI (1000 req/day free)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isGeminiEnabled}
|
||||||
|
onCheckedChange={toggleGemini}
|
||||||
|
disabled={saving || !status?.geminiCli?.installed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
{/* Model input when enabled */}
|
||||||
checked={isGeminiEnabled}
|
{isGeminiEnabled && (
|
||||||
onCheckedChange={toggleGemini}
|
<div className="px-4 pb-4 pt-0">
|
||||||
disabled={saving || !status?.geminiCli?.installed}
|
<div className="flex items-center gap-2">
|
||||||
/>
|
<label className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
</div>
|
Model:
|
||||||
|
</label>
|
||||||
{/* Gemini Installation hint when not installed */}
|
<Input
|
||||||
{!status?.geminiCli?.installed && !statusLoading && (
|
value={geminiModelInput}
|
||||||
<div className="p-4 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20">
|
onChange={(e) => setGeminiModelInput(e.target.value)}
|
||||||
<p className="text-sm font-medium text-amber-800 dark:text-amber-200 mb-2">
|
onBlur={saveGeminiModel}
|
||||||
Gemini CLI not installed
|
placeholder="gemini-2.5-flash"
|
||||||
</p>
|
className="h-8 text-sm font-mono"
|
||||||
<p className="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
disabled={saving}
|
||||||
Install globally (FREE tier available):
|
/>
|
||||||
</p>
|
</div>
|
||||||
<code className="text-sm bg-amber-100 dark:bg-amber-900/40 px-2 py-1 rounded font-mono">
|
</div>
|
||||||
npm install -g @google/gemini-cli
|
)}
|
||||||
</code>
|
{/* Installation hint when not installed - inside card */}
|
||||||
<div className="mt-3">
|
{!status?.geminiCli?.installed && !statusLoading && (
|
||||||
<a
|
<div className="px-4 pb-4 pt-0 border-t border-border/50">
|
||||||
href="https://github.com/google-gemini/gemini-cli"
|
<button
|
||||||
target="_blank"
|
onClick={() => setShowGeminiHint(!showGeminiHint)}
|
||||||
rel="noopener noreferrer"
|
className="flex items-center gap-2 text-sm text-amber-600 dark:text-amber-400 hover:underline w-full py-2"
|
||||||
className="text-sm text-amber-700 dark:text-amber-300 hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-3 h-3" />
|
{showGeminiHint ? (
|
||||||
View documentation
|
<ChevronUp className="w-4 h-4" />
|
||||||
</a>
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
How to install Gemini CLI
|
||||||
|
</button>
|
||||||
|
{showGeminiHint && (
|
||||||
|
<div className="mt-2 p-3 rounded-md bg-amber-50 dark:bg-amber-900/20 text-sm">
|
||||||
|
<p className="text-amber-700 dark:text-amber-300 mb-2">
|
||||||
|
Install globally (FREE tier available):
|
||||||
|
</p>
|
||||||
|
<code className="text-sm bg-amber-100 dark:bg-amber-900/40 px-2 py-1 rounded font-mono block mb-2">
|
||||||
|
npm install -g @google/gemini-cli
|
||||||
|
</code>
|
||||||
|
<a
|
||||||
|
href="https://github.com/google-gemini/gemini-cli"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-amber-700 dark:text-amber-300 hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
View documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* OpenCode CLI Provider */}
|
{/* OpenCode CLI Provider */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
|
className={`rounded-lg border transition-colors ${
|
||||||
isOpenCodeEnabled
|
isOpenCodeEnabled ? 'border-primary border-l-4' : 'border-border'
|
||||||
? 'border-primary/50 bg-primary/5'
|
|
||||||
: 'border-border bg-background'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between p-4">
|
||||||
<Terminal
|
<div className="flex items-center gap-3">
|
||||||
className={`w-5 h-5 ${isOpenCodeEnabled ? 'text-primary' : 'text-muted-foreground'}`}
|
<Terminal
|
||||||
/>
|
className={`w-5 h-5 ${isOpenCodeEnabled ? 'text-primary' : 'text-muted-foreground'}`}
|
||||||
<div>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<p className="font-mono font-medium">opencode</p>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
<p className="font-mono font-medium">opencode</p>
|
||||||
FREE
|
|
||||||
</span>
|
|
||||||
{status?.opencodeCli?.installed ? (
|
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
||||||
installed
|
FREE
|
||||||
</span>
|
</span>
|
||||||
) : (
|
{status?.opencodeCli?.installed ? (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
||||||
not installed
|
installed
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
|
||||||
|
not installed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
OpenCode (web search via Zen)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
OpenCode (web search via Zen)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isOpenCodeEnabled}
|
||||||
|
onCheckedChange={toggleOpenCode}
|
||||||
|
disabled={saving || !status?.opencodeCli?.installed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
{/* Model input when enabled */}
|
||||||
checked={isOpenCodeEnabled}
|
{isOpenCodeEnabled && (
|
||||||
onCheckedChange={toggleOpenCode}
|
<div className="px-4 pb-4 pt-0">
|
||||||
disabled={saving || !status?.opencodeCli?.installed}
|
<div className="flex items-center gap-2">
|
||||||
/>
|
<label className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
</div>
|
Model:
|
||||||
|
</label>
|
||||||
{/* OpenCode Installation hint when not installed */}
|
<Input
|
||||||
{!status?.opencodeCli?.installed && !statusLoading && (
|
value={opencodeModelInput}
|
||||||
<div className="p-4 rounded-lg border border-purple-200 bg-purple-50 dark:border-purple-900/50 dark:bg-purple-900/20">
|
onChange={(e) => setOpencodeModelInput(e.target.value)}
|
||||||
<p className="text-sm font-medium text-purple-800 dark:text-purple-200 mb-2">
|
onBlur={saveOpencodeModel}
|
||||||
OpenCode not installed
|
placeholder="opencode/grok-code"
|
||||||
</p>
|
className="h-8 text-sm font-mono"
|
||||||
<p className="text-sm text-purple-700 dark:text-purple-300 mb-3">
|
disabled={saving}
|
||||||
Install globally (FREE tier available):
|
/>
|
||||||
</p>
|
</div>
|
||||||
<code className="text-sm bg-purple-100 dark:bg-purple-900/40 px-2 py-1 rounded font-mono">
|
</div>
|
||||||
curl -fsSL https://opencode.ai/install | bash
|
)}
|
||||||
</code>
|
{/* Installation hint when not installed - inside card */}
|
||||||
<div className="mt-3">
|
{!status?.opencodeCli?.installed && !statusLoading && (
|
||||||
<a
|
<div className="px-4 pb-4 pt-0 border-t border-border/50">
|
||||||
href="https://github.com/sst/opencode"
|
<button
|
||||||
target="_blank"
|
onClick={() => setShowOpencodeHint(!showOpencodeHint)}
|
||||||
rel="noopener noreferrer"
|
className="flex items-center gap-2 text-sm text-purple-600 dark:text-purple-400 hover:underline w-full py-2"
|
||||||
className="text-sm text-purple-700 dark:text-purple-300 hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-3 h-3" />
|
{showOpencodeHint ? (
|
||||||
View documentation
|
<ChevronUp className="w-4 h-4" />
|
||||||
</a>
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
How to install OpenCode
|
||||||
|
</button>
|
||||||
|
{showOpencodeHint && (
|
||||||
|
<div className="mt-2 p-3 rounded-md bg-purple-50 dark:bg-purple-900/20 text-sm">
|
||||||
|
<p className="text-purple-700 dark:text-purple-300 mb-2">
|
||||||
|
Install globally (FREE tier available):
|
||||||
|
</p>
|
||||||
|
<code className="text-sm bg-purple-100 dark:bg-purple-900/40 px-2 py-1 rounded font-mono block mb-2">
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
</code>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sst/opencode"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-purple-700 dark:text-purple-300 hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
View documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Grok CLI Provider */}
|
{/* Grok CLI Provider */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
|
className={`rounded-lg border transition-colors ${
|
||||||
isGrokEnabled
|
isGrokEnabled ? 'border-primary border-l-4' : 'border-border'
|
||||||
? 'border-primary/50 bg-primary/5'
|
|
||||||
: 'border-border bg-background'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between p-4">
|
||||||
<Terminal
|
<div className="flex items-center gap-3">
|
||||||
className={`w-5 h-5 ${isGrokEnabled ? 'text-primary' : 'text-muted-foreground'}`}
|
<Terminal
|
||||||
/>
|
className={`w-5 h-5 ${isGrokEnabled ? 'text-primary' : 'text-muted-foreground'}`}
|
||||||
<div>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<p className="font-mono font-medium">grok</p>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 font-medium">
|
<p className="font-mono font-medium">grok</p>
|
||||||
GROK_API_KEY
|
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 font-medium">
|
||||||
</span>
|
GROK_API_KEY
|
||||||
{status?.grokCli?.installed ? (
|
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
|
||||||
installed
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
{status?.grokCli?.installed ? (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
|
||||||
not installed
|
installed
|
||||||
</span>
|
</span>
|
||||||
)}
|
) : (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
|
||||||
|
not installed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
xAI Grok CLI (web + X search)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
xAI Grok CLI (web + X search)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={isGrokEnabled}
|
||||||
|
onCheckedChange={toggleGrok}
|
||||||
|
disabled={saving || !status?.grokCli?.installed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
{/* Installation hint when not installed - inside card */}
|
||||||
checked={isGrokEnabled}
|
{!status?.grokCli?.installed && !statusLoading && (
|
||||||
onCheckedChange={toggleGrok}
|
<div className="px-4 pb-4 pt-0 border-t border-border/50">
|
||||||
disabled={saving || !status?.grokCli?.installed}
|
<button
|
||||||
/>
|
onClick={() => setShowGrokHint(!showGrokHint)}
|
||||||
</div>
|
className="flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline w-full py-2"
|
||||||
|
|
||||||
{/* Grok Installation hint when not installed */}
|
|
||||||
{!status?.grokCli?.installed && !statusLoading && (
|
|
||||||
<div className="p-4 rounded-lg border border-blue-200 bg-blue-50 dark:border-blue-900/50 dark:bg-blue-900/20">
|
|
||||||
<p className="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">
|
|
||||||
Grok CLI not installed
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-blue-700 dark:text-blue-300 mb-3">
|
|
||||||
Install globally (requires xAI API key):
|
|
||||||
</p>
|
|
||||||
<code className="text-sm bg-blue-100 dark:bg-blue-900/40 px-2 py-1 rounded font-mono">
|
|
||||||
npm install -g @vibe-kit/grok-cli
|
|
||||||
</code>
|
|
||||||
<div className="mt-3">
|
|
||||||
<a
|
|
||||||
href="https://github.com/superagent-ai/grok-cli"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-sm text-blue-700 dark:text-blue-300 hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-3 h-3" />
|
{showGrokHint ? (
|
||||||
View documentation
|
<ChevronUp className="w-4 h-4" />
|
||||||
</a>
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
How to install Grok CLI
|
||||||
|
</button>
|
||||||
|
{showGrokHint && (
|
||||||
|
<div className="mt-2 p-3 rounded-md bg-blue-50 dark:bg-blue-900/20 text-sm">
|
||||||
|
<p className="text-blue-700 dark:text-blue-300 mb-2">
|
||||||
|
Install globally (requires xAI API key):
|
||||||
|
</p>
|
||||||
|
<code className="text-sm bg-blue-100 dark:bg-blue-900/40 px-2 py-1 rounded font-mono block mb-2">
|
||||||
|
npm install -g @vibe-kit/grok-cli
|
||||||
|
</code>
|
||||||
|
<a
|
||||||
|
href="https://github.com/superagent-ai/grok-cli"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-700 dark:text-blue-300 hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
View documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
Reference in new issue
Block a user