mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 06:17:09 +00:00
feat(websearch): enhance Gemini CLI integration, package manager detection, and WebSearch status
- Improve Gemini CLI integration in websearch hook: use gemini-2.5-flash & --yolo. - Update `dev-install.sh` for better package manager (npm/bun) auto-detection. - Introduce `displayWebSearchStatus` for improved user experience. - Refactor `mcp-manager.ts` to track CCS-managed MCP servers.
This commit is contained in:
@@ -99,7 +99,9 @@ function processHook() {
|
||||
const geminiResult = tryGeminiSearch(query);
|
||||
|
||||
if (geminiResult.success) {
|
||||
// Success! Return results via deny with reason
|
||||
// Success! Use deny with clear success messaging
|
||||
// Note: "deny" prevents native WebSearch from running (which would fail anyway)
|
||||
// The permissionDecisionReason contains the actual search results
|
||||
const output = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PreToolUse',
|
||||
@@ -143,12 +145,18 @@ function tryGeminiSearch(query) {
|
||||
const prompt = buildGeminiPrompt(query);
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(`[CCS Hook] Executing: gemini -p "${prompt.substring(0, 100)}..."`);
|
||||
console.error(`[CCS Hook] Executing: gemini --model gemini-2.5-flash --yolo -p "..."`);
|
||||
}
|
||||
|
||||
// Execute gemini CLI using spawnSync to avoid shell injection
|
||||
// Note: gemini CLI uses OAuth auth, no API key needed
|
||||
const spawnResult = spawnSync('gemini', ['-p', prompt], {
|
||||
// Execute gemini CLI with required flags:
|
||||
// --model gemini-2.5-flash: Use the flash model for fast responses
|
||||
// --yolo: Skip confirmation prompts for tool use
|
||||
// -p: Provide prompt
|
||||
const spawnResult = spawnSync('gemini', [
|
||||
'--model', 'gemini-2.5-flash',
|
||||
'--yolo',
|
||||
'-p', prompt
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: timeoutMs,
|
||||
maxBuffer: 1024 * 1024 * 2, // 2MB buffer for large responses
|
||||
@@ -209,7 +217,7 @@ function tryGeminiSearch(query) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Gemini CLI not found (not installed or not in PATH)',
|
||||
error: 'Gemini CLI not installed. Install with: npm install -g @google/gemini-cli',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,18 +265,15 @@ function buildGeminiPrompt(query) {
|
||||
*/
|
||||
function formatGeminiResults(query, content) {
|
||||
return [
|
||||
'[WebSearch Results via Gemini]',
|
||||
'=== WEBSEARCH COMPLETED SUCCESSFULLY ===',
|
||||
'(via Gemini CLI - this is NOT an error)',
|
||||
'',
|
||||
`Query: "${query}"`,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
content,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'Search completed successfully. You can use this information to answer the user\'s question.',
|
||||
'If you need more specific information, you can search again with a refined query.',
|
||||
'=========================================',
|
||||
'Use this information to answer the user. Search again if needed.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -285,6 +290,10 @@ function getMcpFallbackMessage(query, error) {
|
||||
'',
|
||||
`Gemini CLI failed: ${error || 'Unknown error'}`,
|
||||
'',
|
||||
'To enable Gemini CLI WebSearch:',
|
||||
' 1. Install: npm install -g @google/gemini-cli',
|
||||
' 2. Authenticate: gemini auth',
|
||||
'',
|
||||
'The native WebSearch tool is not available with your current provider.',
|
||||
'Please use one of the following MCP tools instead:',
|
||||
'',
|
||||
|
||||
+54
-5
@@ -4,13 +4,20 @@
|
||||
#
|
||||
# Options:
|
||||
# --skip-validate Skip validation (faster, use when you're sure code is good)
|
||||
# --npm Force npm install (default: auto-detect, fallback to bun)
|
||||
# --bun Force bun install
|
||||
|
||||
set -e
|
||||
|
||||
SKIP_VALIDATE=false
|
||||
FORCE_NPM=false
|
||||
FORCE_BUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--skip-validate) SKIP_VALIDATE=true ;;
|
||||
--npm) FORCE_NPM=true ;;
|
||||
--bun) FORCE_BUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -19,6 +26,42 @@ echo "[i] CCS Dev Install - Starting..."
|
||||
# Get to the right directory
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Detect installation method
|
||||
# Priority: CLI flags > existing global install location > bun (default)
|
||||
detect_pkg_manager() {
|
||||
if [ "$FORCE_NPM" = true ]; then
|
||||
echo "npm"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$FORCE_BUN" = true ]; then
|
||||
echo "bun"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check existing ccs installation location
|
||||
CCS_PATH=$(which ccs 2>/dev/null || true)
|
||||
|
||||
if [ -n "$CCS_PATH" ]; then
|
||||
# Check if installed via bun
|
||||
if [[ "$CCS_PATH" == *".bun"* ]]; then
|
||||
echo "bun"
|
||||
return
|
||||
fi
|
||||
# Check if installed via npm
|
||||
if [[ "$CCS_PATH" == *"npm"* ]] || [[ "$CCS_PATH" == *"node_modules"* ]]; then
|
||||
echo "npm"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Default fallback: bun (preferred)
|
||||
echo "bun"
|
||||
}
|
||||
|
||||
PKG_MANAGER=$(detect_pkg_manager)
|
||||
echo "[i] Detected package manager: $PKG_MANAGER"
|
||||
|
||||
# Build TypeScript first
|
||||
echo "[i] Building TypeScript..."
|
||||
bun run build
|
||||
@@ -27,10 +70,10 @@ bun run build
|
||||
echo "[i] Creating package..."
|
||||
if [ "$SKIP_VALIDATE" = true ]; then
|
||||
# Skip validation, just pack
|
||||
bun pm pack --ignore-scripts
|
||||
npm pack --ignore-scripts 2>/dev/null || bun pm pack --ignore-scripts
|
||||
else
|
||||
# Full pack with validation (runs prepublishOnly)
|
||||
bun pm pack
|
||||
npm pack 2>/dev/null || bun pm pack
|
||||
fi
|
||||
|
||||
# Find the tarball
|
||||
@@ -43,9 +86,15 @@ fi
|
||||
|
||||
echo "[i] Found tarball: $TARBALL"
|
||||
|
||||
# Install globally using npm (handles bin linking correctly)
|
||||
echo "[i] Installing globally with npm..."
|
||||
npm install -g "$TARBALL"
|
||||
# Install globally using detected package manager
|
||||
echo "[i] Installing globally with $PKG_MANAGER..."
|
||||
|
||||
if [ "$PKG_MANAGER" = "bun" ]; then
|
||||
# Bun requires file: protocol for local tarballs
|
||||
bun add -g "file:$(pwd)/$TARBALL"
|
||||
else
|
||||
npm install -g "$TARBALL"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
echo "[i] Cleaning up..."
|
||||
|
||||
+8
-1
@@ -14,7 +14,11 @@ import { detectClaudeCli } from './utils/claude-detector';
|
||||
import { getSettingsPath } from './utils/config-manager';
|
||||
import { ErrorManager } from './utils/error-manager';
|
||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
||||
import { ensureMcpWebSearch, installWebSearchHook } from './utils/mcp-manager';
|
||||
import {
|
||||
ensureMcpWebSearch,
|
||||
installWebSearchHook,
|
||||
displayWebSearchStatus,
|
||||
} from './utils/mcp-manager';
|
||||
|
||||
// Import extracted command handlers
|
||||
import { handleVersionCommand } from './commands/version-command';
|
||||
@@ -384,6 +388,9 @@ async function main(): Promise<void> {
|
||||
ensureMcpWebSearch();
|
||||
installWebSearchHook();
|
||||
|
||||
// Display WebSearch status (single line, equilibrium UX)
|
||||
displayWebSearchStatus();
|
||||
|
||||
// Check if this is GLMT profile (requires proxy)
|
||||
if (profileInfo.name === 'glmt') {
|
||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
||||
|
||||
@@ -39,7 +39,11 @@ import {
|
||||
} from './account-manager';
|
||||
import { getPortCheckCommand, getCatCommand, killProcessOnPort } from '../utils/platform-commands';
|
||||
import { getPortProcess, isCLIProxyProcess } from '../utils/port-utils';
|
||||
import { ensureMcpWebSearch, installWebSearchHook } from '../utils/mcp-manager';
|
||||
import {
|
||||
ensureMcpWebSearch,
|
||||
installWebSearchHook,
|
||||
displayWebSearchStatus,
|
||||
} from '../utils/mcp-manager';
|
||||
|
||||
/** Default executor configuration */
|
||||
const DEFAULT_CONFIG: ExecutorConfig = {
|
||||
@@ -122,6 +126,9 @@ export async function execClaudeWithCLIProxy(
|
||||
// Hook intercepts WebSearch, tries Gemini CLI first, falls back to MCP
|
||||
installWebSearchHook();
|
||||
|
||||
// Display WebSearch status (single line, equilibrium UX)
|
||||
displayWebSearchStatus();
|
||||
|
||||
// Validate provider
|
||||
const providerConfig = getProviderConfig(provider);
|
||||
log(`Provider: ${providerConfig.displayName}`);
|
||||
|
||||
+232
-1
@@ -14,7 +14,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { info, warn } from './ui';
|
||||
import { ok, info, warn, fail } from './ui';
|
||||
import { getWebSearchConfig } from '../config/unified-config-loader';
|
||||
|
||||
// MCP configuration file path
|
||||
@@ -30,6 +30,7 @@ interface McpServerConfig {
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
_managedBy?: 'ccs'; // CCS-managed marker (Option C hybrid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,7 @@ const WEB_SEARCH_PRIME_CONFIG: McpServerConfig = {
|
||||
type: 'http',
|
||||
url: 'https://api.z.ai/api/mcp/web_search_prime/mcp',
|
||||
headers: {},
|
||||
_managedBy: 'ccs',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,6 +63,7 @@ const BRAVE_SEARCH_CONFIG: McpServerConfig = {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-brave-search'],
|
||||
env: {},
|
||||
_managedBy: 'ccs',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,6 +77,7 @@ const TAVILY_CONFIG: McpServerConfig = {
|
||||
command: 'npx',
|
||||
args: ['-y', '@tavily/mcp-server'],
|
||||
env: {},
|
||||
_managedBy: 'ccs',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -571,3 +575,230 @@ function ensureWebSearchHookConfigInternal(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Phase 1: MCP Status Functions ==========
|
||||
|
||||
/**
|
||||
* MCP web search status with ownership tracking
|
||||
*/
|
||||
export interface McpWebSearchStatus {
|
||||
configured: boolean;
|
||||
ccsManaged: string[]; // CCS-managed server names
|
||||
userAdded: string[]; // User-added server names
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive MCP web search status
|
||||
*
|
||||
* Distinguishes CCS-managed vs user-added MCP servers
|
||||
* based on _managedBy marker.
|
||||
*/
|
||||
export function getMcpWebSearchStatus(): McpWebSearchStatus {
|
||||
const result: McpWebSearchStatus = {
|
||||
configured: false,
|
||||
ccsManaged: [],
|
||||
userAdded: [],
|
||||
};
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(MCP_CONFIG_PATH)) return result;
|
||||
|
||||
const content = fs.readFileSync(MCP_CONFIG_PATH, 'utf8');
|
||||
const config: McpConfig = JSON.parse(content);
|
||||
|
||||
if (!config.mcpServers) return result;
|
||||
|
||||
for (const [name, server] of Object.entries(config.mcpServers)) {
|
||||
// Check if it's a web search server
|
||||
const lowerName = name.toLowerCase();
|
||||
const isWebSearch =
|
||||
lowerName.includes('web-search') ||
|
||||
lowerName.includes('websearch') ||
|
||||
lowerName.includes('tavily') ||
|
||||
lowerName.includes('brave');
|
||||
|
||||
if (!isWebSearch) continue;
|
||||
|
||||
if (server._managedBy === 'ccs') {
|
||||
result.ccsManaged.push(name);
|
||||
} else {
|
||||
result.userAdded.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
result.configured = result.ccsManaged.length > 0 || result.userAdded.length > 0;
|
||||
return result;
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CCS-managed web search MCP is configured
|
||||
*/
|
||||
export function hasCcsManagedWebSearch(): boolean {
|
||||
const status = getMcpWebSearchStatus();
|
||||
return status.ccsManaged.length > 0;
|
||||
}
|
||||
|
||||
// ========== Phase 2: Gemini CLI Detection ==========
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* Gemini CLI installation status
|
||||
*/
|
||||
export interface GeminiCliStatus {
|
||||
installed: boolean;
|
||||
path: string | null;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
// Cache for Gemini CLI status (per process)
|
||||
let geminiCliCache: GeminiCliStatus | null = null;
|
||||
|
||||
/**
|
||||
* Check if Gemini CLI is installed globally
|
||||
*
|
||||
* Requires global install: `npm install -g @google/gemini-cli`
|
||||
* No npx fallback - must be in PATH
|
||||
*
|
||||
* @returns Gemini CLI status with path and version
|
||||
*/
|
||||
export function getGeminiCliStatus(): GeminiCliStatus {
|
||||
// Return cached result if available
|
||||
if (geminiCliCache) {
|
||||
return geminiCliCache;
|
||||
}
|
||||
|
||||
const result: GeminiCliStatus = {
|
||||
installed: false,
|
||||
path: null,
|
||||
version: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const whichCmd = isWindows ? 'where gemini' : 'which gemini';
|
||||
|
||||
const pathResult = execSync(whichCmd, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const geminiPath = pathResult.trim().split('\n')[0]; // First result on Windows
|
||||
|
||||
if (geminiPath) {
|
||||
result.installed = true;
|
||||
result.path = geminiPath;
|
||||
|
||||
// Try to get version
|
||||
try {
|
||||
const versionResult = execSync('gemini --version', {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
result.version = versionResult.trim();
|
||||
} catch {
|
||||
// Version check failed, but CLI is installed
|
||||
result.version = 'unknown';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Command not found - Gemini CLI not installed
|
||||
}
|
||||
|
||||
// Cache result
|
||||
geminiCliCache = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Gemini CLI is available (quick boolean check)
|
||||
*/
|
||||
export function hasGeminiCli(): boolean {
|
||||
return getGeminiCliStatus().installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Gemini CLI cache (for testing or after installation)
|
||||
*/
|
||||
export function clearGeminiCliCache(): void {
|
||||
geminiCliCache = null;
|
||||
}
|
||||
|
||||
// ========== Phase 3: WebSearch Readiness Status ==========
|
||||
|
||||
/**
|
||||
* WebSearch availability status for third-party profiles
|
||||
*/
|
||||
export type WebSearchReadiness = 'ready' | 'mcp-only' | 'unavailable';
|
||||
|
||||
/**
|
||||
* WebSearch status for display
|
||||
*/
|
||||
export interface WebSearchStatus {
|
||||
readiness: WebSearchReadiness;
|
||||
geminiCli: boolean;
|
||||
mcpConfigured: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebSearch readiness status for display
|
||||
*
|
||||
* Called on third-party profile startup to inform user.
|
||||
*/
|
||||
export function getWebSearchReadiness(): WebSearchStatus {
|
||||
const geminiCli = hasGeminiCli();
|
||||
const mcpStatus = getMcpWebSearchStatus();
|
||||
const mcpConfigured = mcpStatus.configured;
|
||||
|
||||
if (geminiCli) {
|
||||
return {
|
||||
readiness: 'ready',
|
||||
geminiCli: true,
|
||||
mcpConfigured,
|
||||
message: 'Ready (Gemini CLI)',
|
||||
};
|
||||
}
|
||||
|
||||
if (mcpConfigured) {
|
||||
return {
|
||||
readiness: 'mcp-only',
|
||||
geminiCli: false,
|
||||
mcpConfigured: true,
|
||||
message: 'MCP fallback only',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
readiness: 'unavailable',
|
||||
geminiCli: false,
|
||||
mcpConfigured: false,
|
||||
message: 'Unavailable (run: ccs config)',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display WebSearch status (single line, equilibrium UX)
|
||||
*
|
||||
* Only call for third-party profiles.
|
||||
*/
|
||||
export function displayWebSearchStatus(): void {
|
||||
const status = getWebSearchReadiness();
|
||||
|
||||
switch (status.readiness) {
|
||||
case 'ready':
|
||||
console.error(ok(`WebSearch: ${status.message}`));
|
||||
break;
|
||||
case 'mcp-only':
|
||||
console.error(info(`WebSearch: ${status.message}`));
|
||||
break;
|
||||
case 'unavailable':
|
||||
console.error(fail(`WebSearch: ${status.message}`));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ import { getWebSearchConfig } from '../config/unified-config-loader';
|
||||
import type { WebSearchConfig } from '../config/unified-config-types';
|
||||
import { isUnifiedConfig } from '../config/unified-config-types';
|
||||
import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys';
|
||||
import {
|
||||
getWebSearchReadiness,
|
||||
getMcpWebSearchStatus,
|
||||
getGeminiCliStatus,
|
||||
} from '../utils/mcp-manager';
|
||||
|
||||
export const apiRoutes = Router();
|
||||
|
||||
@@ -1475,3 +1480,34 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/websearch/status - Get comprehensive WebSearch status
|
||||
* Returns: { geminiCli, mcpServers, readiness }
|
||||
*/
|
||||
apiRoutes.get('/websearch/status', (_req: Request, res: Response): void => {
|
||||
try {
|
||||
const geminiCli = getGeminiCliStatus();
|
||||
const mcpStatus = getMcpWebSearchStatus();
|
||||
const readiness = getWebSearchReadiness();
|
||||
|
||||
res.json({
|
||||
geminiCli: {
|
||||
installed: geminiCli.installed,
|
||||
path: geminiCli.path,
|
||||
version: geminiCli.version,
|
||||
},
|
||||
mcpServers: {
|
||||
configured: mcpStatus.configured,
|
||||
ccsManaged: mcpStatus.ccsManaged,
|
||||
userAdded: mcpStatus.userAdded,
|
||||
},
|
||||
readiness: {
|
||||
status: readiness.readiness,
|
||||
message: readiness.message,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
+122
-1
@@ -23,16 +23,36 @@ interface WebSearchConfig {
|
||||
fallback: boolean;
|
||||
}
|
||||
|
||||
interface WebSearchStatus {
|
||||
geminiCli: {
|
||||
installed: boolean;
|
||||
path: string | null;
|
||||
version: string | null;
|
||||
};
|
||||
mcpServers: {
|
||||
configured: boolean;
|
||||
ccsManaged: string[];
|
||||
userAdded: string[];
|
||||
};
|
||||
readiness: {
|
||||
status: 'ready' | 'mcp-only' | 'unavailable';
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const [config, setConfig] = useState<WebSearchConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [status, setStatus] = useState<WebSearchStatus | null>(null);
|
||||
const [statusLoading, setStatusLoading] = useState(true);
|
||||
|
||||
// Load config on mount
|
||||
// Load config and status on mount
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchStatus();
|
||||
}, []);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
@@ -50,6 +70,20 @@ export function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
setStatusLoading(true);
|
||||
const res = await fetch('/api/websearch/status');
|
||||
if (!res.ok) throw new Error('Failed to load status');
|
||||
const data = await res.json();
|
||||
setStatus(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch WebSearch status:', err);
|
||||
} finally {
|
||||
setStatusLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async (updates: Partial<WebSearchConfig>) => {
|
||||
if (!config) return;
|
||||
|
||||
@@ -131,6 +165,93 @@ export function SettingsPage() {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* WebSearch Status Panel */}
|
||||
<div className="space-y-4 pb-4 border-b">
|
||||
<h4 className="font-medium flex items-center gap-2">
|
||||
Status
|
||||
<Button variant="ghost" size="sm" onClick={fetchStatus} disabled={statusLoading}>
|
||||
<RefreshCw className={`w-3 h-3 ${statusLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</h4>
|
||||
|
||||
{statusLoading ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
Checking status...
|
||||
</div>
|
||||
) : status ? (
|
||||
<div className="space-y-3">
|
||||
{/* Overall Readiness */}
|
||||
<div className="flex items-center gap-2">
|
||||
{status.readiness.status === 'ready' && (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||
)}
|
||||
{status.readiness.status === 'mcp-only' && (
|
||||
<Info className="w-4 h-4 text-blue-600" />
|
||||
)}
|
||||
{status.readiness.status === 'unavailable' && (
|
||||
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||
)}
|
||||
<span className="font-medium">{status.readiness.message}</span>
|
||||
</div>
|
||||
|
||||
{/* Gemini CLI Status */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full mt-2 ${
|
||||
status.geminiCli.installed ? 'bg-green-500' : 'bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">Gemini CLI</p>
|
||||
{status.geminiCli.installed ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Installed {status.geminiCli.version && `(${status.geminiCli.version})`}
|
||||
</p>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>Not installed</p>
|
||||
<code className="text-xs bg-muted px-1 py-0.5 rounded">
|
||||
npm install -g @google/gemini-cli
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MCP Servers */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">MCP Servers</p>
|
||||
{status.mcpServers.ccsManaged.length === 0 &&
|
||||
status.mcpServers.userAdded.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No web search MCP configured</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{status.mcpServers.ccsManaged.map((name) => (
|
||||
<div key={name} className="flex items-center gap-2 text-sm">
|
||||
<span className="px-1.5 py-0.5 rounded text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||
Managed by CCS
|
||||
</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
))}
|
||||
{status.mcpServers.userAdded.map((name) => (
|
||||
<div key={name} className="flex items-center gap-2 text-sm">
|
||||
<span className="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
User-added
|
||||
</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Status unavailable</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Enable/Disable */}
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user