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:
kaitranntt
2025-12-17 00:16:03 -05:00
parent 6d69379ead
commit 14c53d575f
6 changed files with 342 additions and 184 deletions
+22 -7
View File
@@ -11,6 +11,7 @@
* CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1)
* CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55)
* 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_GROK=1 - Enable Grok CLI provider
* 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 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) {
console.error(`[CCS Hook] Executing: gemini --model ${config.model} --yolo -p "..."`);
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
}
const spawnResult = spawnSync(
'gemini',
['--model', config.model, '--yolo', '-p', prompt],
['--model', model, '--yolo', '-p', prompt],
{
encoding: 'utf8',
timeout: timeoutMs,
@@ -441,29 +445,40 @@ function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
*/
function formatSearchResults(query, content, providerName) {
return [
'=== WEBSEARCH COMPLETED SUCCESSFULLY ===',
`(via ${providerName} - this is NOT an error)`,
`[WebSearch Result via ${providerName}]`,
'',
`Query: "${query}"`,
'',
content,
'',
'=========================================',
'Use this information to answer the user. Search again if needed.',
'---',
'Use this information to answer the user.',
].join('\n');
}
/**
* 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) {
const formattedResults = formatSearchResults(query, content, providerName);
const output = {
decision: 'block',
reason: `WebSearch completed via ${providerName}`,
// Nice message for user (shows as "says:" - info style)
systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: formatSearchResults(query, content, providerName),
// Full results here - Claude reads this
permissionDecisionReason: formattedResults,
},
};
+20 -8
View File
@@ -83,14 +83,17 @@ export function loadUnifiedConfig(): UnifiedConfig | 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) {
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 {
saveUnifiedConfig(parsed);
saveUnifiedConfig(upgraded);
if (process.env.CCS_DEBUG) {
console.error(`[i] Config upgraded to v${UNIFIED_CONFIG_VERSION}`);
}
return upgraded;
} catch {
// 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?.gemini?.enabled ?? // Legacy fallback
true,
model: partial.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
timeout:
partial.websearch?.providers?.gemini?.timeout ??
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('#');
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('# 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(
yaml
@@ -362,6 +372,7 @@ export function setDefaultProfile(name: string): void {
*/
export interface GeminiWebSearchInfo {
enabled: boolean;
model: string;
timeout: number;
}
@@ -386,6 +397,7 @@ export function getWebSearchConfig(): {
const geminiConfig: GeminiWebSearchInfo = {
enabled:
config.websearch?.providers?.gemini?.enabled ?? config.websearch?.gemini?.enabled ?? true,
model: config.websearch?.providers?.gemini?.model ?? 'gemini-2.5-flash',
timeout:
config.websearch?.providers?.gemini?.timeout ?? config.websearch?.gemini?.timeout ?? 55,
};
+13 -1
View File
@@ -12,7 +12,7 @@
/**
* Unified config version.
* 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;
@@ -107,6 +107,8 @@ export interface PreferencesConfig {
export interface GeminiWebSearchConfig {
/** Enable Gemini CLI for WebSearch (default: true) */
enabled?: boolean;
/** Model to use (default: gemini-2.5-flash) */
model?: string;
/** Timeout in seconds (default: 55) */
timeout?: number;
}
@@ -237,6 +239,16 @@ export function createEmptyUnifiedConfig(): UnifiedConfig {
providers: {
gemini: {
enabled: true,
model: 'gemini-2.5-flash',
timeout: 55,
},
opencode: {
enabled: false,
model: 'opencode/grok-code',
timeout: 90,
},
grok: {
enabled: false,
timeout: 55,
},
},
+3
View File
@@ -654,6 +654,9 @@ export function getWebSearchHookEnv(): Record<string, string> {
// Hook will only use providers that are BOTH enabled AND installed
if (wsConfig.providers?.gemini?.enabled) {
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);
}
+4
View File
@@ -1477,6 +1477,10 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
providers.gemini?.enabled ??
existingConfig.websearch?.providers?.gemini?.enabled ??
true,
model:
providers.gemini?.model ??
existingConfig.websearch?.providers?.gemini?.model ??
'gemini-2.5-flash',
timeout:
providers.gemini?.timeout ??
existingConfig.websearch?.providers?.gemini?.timeout ??
+280 -168
View File
@@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import {
Globe,
RefreshCw,
@@ -20,22 +21,21 @@ import {
GripVertical,
Terminal,
ExternalLink,
ChevronDown,
ChevronUp,
} from 'lucide-react';
import { CodeEditor } from '@/components/code-editor';
interface ProviderConfig {
enabled?: boolean;
timeout?: number;
}
interface OpenCodeProviderConfig extends ProviderConfig {
model?: string;
timeout?: number;
}
interface WebSearchProvidersConfig {
gemini?: ProviderConfig;
grok?: ProviderConfig;
opencode?: OpenCodeProviderConfig;
opencode?: ProviderConfig;
}
interface WebSearchConfig {
@@ -71,6 +71,13 @@ export function SettingsPage() {
const [rawConfig, setRawConfig] = useState<string | null>(null);
const [rawConfigLoading, setRawConfigLoading] = 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
useEffect(() => {
@@ -79,6 +86,14 @@ export function SettingsPage() {
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 () => {
try {
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) {
return (
<div className="h-[calc(100vh-100px)] flex items-center justify-center">
@@ -313,201 +362,264 @@ export function SettingsPage() {
{/* Gemini CLI Provider */}
<div
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
isGeminiEnabled
? 'border-primary/50 bg-primary/5'
: 'border-border bg-background'
className={`rounded-lg border transition-colors ${
isGeminiEnabled ? 'border-primary border-l-4' : 'border-border'
}`}
>
<div className="flex items-center gap-3">
<Terminal
className={`w-5 h-5 ${isGeminiEnabled ? 'text-primary' : 'text-muted-foreground'}`}
/>
<div>
<div className="flex items-center gap-2">
<p className="font-mono font-medium">gemini</p>
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
FREE
</span>
{status?.geminiCli?.installed ? (
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Terminal
className={`w-5 h-5 ${isGeminiEnabled ? 'text-primary' : 'text-muted-foreground'}`}
/>
<div>
<div className="flex items-center gap-2">
<p className="font-mono font-medium">gemini</p>
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
installed
FREE
</span>
) : (
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
not installed
</span>
)}
{status?.geminiCli?.installed ? (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
installed
</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>
<p className="text-sm text-muted-foreground">
Google Gemini CLI (1000 req/day free)
</p>
</div>
<Switch
checked={isGeminiEnabled}
onCheckedChange={toggleGemini}
disabled={saving || !status?.geminiCli?.installed}
/>
</div>
<Switch
checked={isGeminiEnabled}
onCheckedChange={toggleGemini}
disabled={saving || !status?.geminiCli?.installed}
/>
</div>
{/* Gemini Installation hint when not installed */}
{!status?.geminiCli?.installed && !statusLoading && (
<div className="p-4 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900/50 dark:bg-amber-900/20">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200 mb-2">
Gemini CLI not installed
</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mb-3">
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">
npm install -g @google/gemini-cli
</code>
<div className="mt-3">
<a
href="https://github.com/google-gemini/gemini-cli"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-amber-700 dark:text-amber-300 hover:underline inline-flex items-center gap-1"
{/* Model input when enabled */}
{isGeminiEnabled && (
<div className="px-4 pb-4 pt-0">
<div className="flex items-center gap-2">
<label className="text-sm text-muted-foreground whitespace-nowrap">
Model:
</label>
<Input
value={geminiModelInput}
onChange={(e) => setGeminiModelInput(e.target.value)}
onBlur={saveGeminiModel}
placeholder="gemini-2.5-flash"
className="h-8 text-sm font-mono"
disabled={saving}
/>
</div>
</div>
)}
{/* Installation hint when not installed - inside card */}
{!status?.geminiCli?.installed && !statusLoading && (
<div className="px-4 pb-4 pt-0 border-t border-border/50">
<button
onClick={() => setShowGeminiHint(!showGeminiHint)}
className="flex items-center gap-2 text-sm text-amber-600 dark:text-amber-400 hover:underline w-full py-2"
>
<ExternalLink className="w-3 h-3" />
View documentation
</a>
{showGeminiHint ? (
<ChevronUp className="w-4 h-4" />
) : (
<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>
{/* OpenCode CLI Provider */}
<div
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
isOpenCodeEnabled
? 'border-primary/50 bg-primary/5'
: 'border-border bg-background'
className={`rounded-lg border transition-colors ${
isOpenCodeEnabled ? 'border-primary border-l-4' : 'border-border'
}`}
>
<div className="flex items-center gap-3">
<Terminal
className={`w-5 h-5 ${isOpenCodeEnabled ? 'text-primary' : 'text-muted-foreground'}`}
/>
<div>
<div className="flex items-center gap-2">
<p className="font-mono font-medium">opencode</p>
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
FREE
</span>
{status?.opencodeCli?.installed ? (
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Terminal
className={`w-5 h-5 ${isOpenCodeEnabled ? 'text-primary' : 'text-muted-foreground'}`}
/>
<div>
<div className="flex items-center gap-2">
<p className="font-mono font-medium">opencode</p>
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
installed
FREE
</span>
) : (
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
not installed
</span>
)}
{status?.opencodeCli?.installed ? (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
installed
</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>
<p className="text-sm text-muted-foreground">
OpenCode (web search via Zen)
</p>
</div>
<Switch
checked={isOpenCodeEnabled}
onCheckedChange={toggleOpenCode}
disabled={saving || !status?.opencodeCli?.installed}
/>
</div>
<Switch
checked={isOpenCodeEnabled}
onCheckedChange={toggleOpenCode}
disabled={saving || !status?.opencodeCli?.installed}
/>
</div>
{/* OpenCode Installation hint when not installed */}
{!status?.opencodeCli?.installed && !statusLoading && (
<div className="p-4 rounded-lg border border-purple-200 bg-purple-50 dark:border-purple-900/50 dark:bg-purple-900/20">
<p className="text-sm font-medium text-purple-800 dark:text-purple-200 mb-2">
OpenCode not installed
</p>
<p className="text-sm text-purple-700 dark:text-purple-300 mb-3">
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">
curl -fsSL https://opencode.ai/install | bash
</code>
<div className="mt-3">
<a
href="https://github.com/sst/opencode"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-purple-700 dark:text-purple-300 hover:underline inline-flex items-center gap-1"
{/* Model input when enabled */}
{isOpenCodeEnabled && (
<div className="px-4 pb-4 pt-0">
<div className="flex items-center gap-2">
<label className="text-sm text-muted-foreground whitespace-nowrap">
Model:
</label>
<Input
value={opencodeModelInput}
onChange={(e) => setOpencodeModelInput(e.target.value)}
onBlur={saveOpencodeModel}
placeholder="opencode/grok-code"
className="h-8 text-sm font-mono"
disabled={saving}
/>
</div>
</div>
)}
{/* Installation hint when not installed - inside card */}
{!status?.opencodeCli?.installed && !statusLoading && (
<div className="px-4 pb-4 pt-0 border-t border-border/50">
<button
onClick={() => setShowOpencodeHint(!showOpencodeHint)}
className="flex items-center gap-2 text-sm text-purple-600 dark:text-purple-400 hover:underline w-full py-2"
>
<ExternalLink className="w-3 h-3" />
View documentation
</a>
{showOpencodeHint ? (
<ChevronUp className="w-4 h-4" />
) : (
<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>
{/* Grok CLI Provider */}
<div
className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
isGrokEnabled
? 'border-primary/50 bg-primary/5'
: 'border-border bg-background'
className={`rounded-lg border transition-colors ${
isGrokEnabled ? 'border-primary border-l-4' : 'border-border'
}`}
>
<div className="flex items-center gap-3">
<Terminal
className={`w-5 h-5 ${isGrokEnabled ? 'text-primary' : 'text-muted-foreground'}`}
/>
<div>
<div className="flex items-center gap-2">
<p className="font-mono font-medium">grok</p>
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 font-medium">
GROK_API_KEY
</span>
{status?.grokCli?.installed ? (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 font-medium">
installed
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Terminal
className={`w-5 h-5 ${isGrokEnabled ? 'text-primary' : 'text-muted-foreground'}`}
/>
<div>
<div className="flex items-center gap-2">
<p className="font-mono font-medium">grok</p>
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 font-medium">
GROK_API_KEY
</span>
) : (
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-600 font-medium">
not installed
</span>
)}
{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 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>
<p className="text-sm text-muted-foreground">
xAI Grok CLI (web + X search)
</p>
</div>
<Switch
checked={isGrokEnabled}
onCheckedChange={toggleGrok}
disabled={saving || !status?.grokCli?.installed}
/>
</div>
<Switch
checked={isGrokEnabled}
onCheckedChange={toggleGrok}
disabled={saving || !status?.grokCli?.installed}
/>
</div>
{/* 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"
{/* Installation hint when not installed - inside card */}
{!status?.grokCli?.installed && !statusLoading && (
<div className="px-4 pb-4 pt-0 border-t border-border/50">
<button
onClick={() => setShowGrokHint(!showGrokHint)}
className="flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline w-full py-2"
>
<ExternalLink className="w-3 h-3" />
View documentation
</a>
{showGrokHint ? (
<ChevronUp className="w-4 h-4" />
) : (
<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>
</ScrollArea>