mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-01 12:07:37 +00:00
feat: Google's gemini-cli integration (#422)
* feat: integrate Gemini AI agent provider - Core Backend: Ported gemini-cli.js and gemini-response-handler.js to establish the CLI bridge. Registered 'gemini' as an active provider within index.js. - Core Frontend: Extended QuickSettingsPanel.jsx, Settings.jsx, and AgentListItem.jsx to render the Gemini provider option, models (gemini-pro, gemini-flash, etc.), and handle OAuth states. - WebSocket Pipeline: Added support for gemini-command executions in backend and payload processing of gemini-response and gemini-error streams in useChatRealtimeHandlers.ts. Resolved JSON double-stringification and sessionId stripping issues in the transmission handler. - Platform Compatibility: Added scripts/fix-node-pty.js postinstall script and modified posix_spawnp calls with sh -c wrapper to prevent ENOEXEC and MacOS permission errors when spawning the gemini headless binary. - UX & Design: Imported official Google Gemini branding via GeminiLogo.jsx and gemini-ai-icon.svg. Updated translations (chat.json) for en, zh-CN, and ko locales. * fix: propagate gemini permission mode from settings to cli - Added Gemini Permissions UI in Settings to toggle Auto Edit and YOLO modes - Synced gemini permission mode to localStorage - Passed permissionMode in useChatComposerState for Gemini commands - Mapped frontend permission modes to --yolo and --approval-mode options in gemini-cli.js * feat(gemini): Refactor Gemini CLI integration to use stream-json - Replaced regex buffering text-system with NDJSON stream parsing - Added fallback for restricted models like gemini-3.1-pro-preview * feat(gemini): Render tool_use and tool_result UI bubbles - Forwarded gemini tool NDJSON objects to the websocket - Added React state handlers in useChatRealtimeHandlers to match Claude's tool UI behavior * feat(gemini): Add native session resumption and UI token tracking - Captured cliSessionId from init events to map ClaudeCodeUI's chat sessionId directly into Gemini's internal session manager. - Updated gemini-cli.js spawn arguments to append the --resume proxy flag instead of naively dumping the accumulated chat history into the command prompt. - Handled result stream objects by proxying total_tokens back into the frontend's claude-status tracker to natively populate the UI label. - Eliminated gemini-3 model proxy filter entirely. * fix(gemini): Fix static 'Claude' name rendering in chat UI header - Added "gemini": "Gemini" translation strings to messageTypes across English, Korean, and Chinese loc dictionaries. - Updated AssistantThinkingIndicator and MessageComponent ternary checks to identify provider === 'gemini' and render the appropriate brand label instead of statically defaulting to Claude. * feat: Add Gemini session persistence API mapping and Sidebar UI * fix(gemini): Watch ~/.gemini/sessions for live UI updates Added the .gemini/sessions directory to PROVIDER_WATCH_PATHS so that Chokidar emits projects_updated websocket events when new Gemini sessions are created or modified, fixing live sidebar updates. * fix(gemini): Fix Gemini authentication status display in Settings UI - Injected 'checkGeminiAuthStatus' into the Settings.jsx React effect hook so that the UI can poll and render the 'geminiAuthStatus' state. - Updated 'checkGeminiCredentials()' inside server/routes/cli-auth.js to read from '~/.gemini/oauth_creds.json' and '~/.gemini/google_accounts.json', resolving the email address correctly. * Use logo-only icon for gemini * feat(gemini): Add Gemini 3 preview models to UI selection list * Fix Gemini CLI session resume bug and PR #422 review nitpicks * Fix Gemini tool calls disappearing from UI after completion * fix(gemini): resolve outstanding PR #422 feedback and stabilize gemini CLI timeouts * fix(gemini): resolve resume flag and shell session initialization issues This commit addresses the remaining PR comments for the Gemini CLI integration: - Moves the `--resume` flag logic outside the prompt command block, ensuring Gemini sessions correctly resume even when a new prompt isn't passed. - Updates `handleShellConnection` to correctly lookup the native `cliSessionId` from the internal `sessionId` when spawning Gemini sessions in a plain shell. - Refactors dynamic import of `sessionManager.js` back to a native static import for code consistency. * chore: fix TypeScript errors and remove gemini CLI dependency * fix: use cross-spawn on Windows to resolve gemini.cmd correctly --------- Co-authored-by: Haileyesus <118998054+blackmammoth@users.noreply.github.com>
This commit is contained in:
9
src/components/GeminiLogo.jsx
Normal file
9
src/components/GeminiLogo.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const GeminiLogo = ({className = 'w-5 h-5'}) => {
|
||||
return (
|
||||
<img src="/icons/gemini-ai-icon.svg" alt="Gemini" className={className} />
|
||||
);
|
||||
};
|
||||
|
||||
export default GeminiLogo;
|
||||
90
src/components/GeminiStatus.jsx
Normal file
90
src/components/GeminiStatus.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
function GeminiStatus({ status, onAbort, isLoading }) {
|
||||
const [elapsedTime, setElapsedTime] = useState(0);
|
||||
const [animationPhase, setAnimationPhase] = useState(0);
|
||||
|
||||
// Update elapsed time every second
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setElapsedTime(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const timer = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
setElapsedTime(elapsed);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [isLoading]);
|
||||
|
||||
// Animate the status indicator
|
||||
useEffect(() => {
|
||||
if (!isLoading) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setAnimationPhase(prev => (prev + 1) % 4);
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [isLoading]);
|
||||
|
||||
if (!isLoading) return null;
|
||||
|
||||
// Clever action words that cycle
|
||||
const actionWords = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning'];
|
||||
const actionIndex = Math.floor(elapsedTime / 3) % actionWords.length;
|
||||
|
||||
// Parse status data
|
||||
const statusText = status?.text || actionWords[actionIndex];
|
||||
const canInterrupt = status?.can_interrupt !== false;
|
||||
|
||||
// Animation characters
|
||||
const spinners = ['✻', '✹', '✸', '✶'];
|
||||
const currentSpinner = spinners[animationPhase];
|
||||
|
||||
return (
|
||||
<div className="w-full mb-6 animate-in slide-in-from-bottom duration-300">
|
||||
<div className="flex items-center justify-between max-w-4xl mx-auto bg-gradient-to-r from-cyan-900 to-blue-900 dark:from-cyan-950 dark:to-blue-950 text-white rounded-lg shadow-lg px-4 py-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Animated spinner */}
|
||||
<span className={cn(
|
||||
"text-xl transition-all duration-500",
|
||||
animationPhase % 2 === 0 ? "text-cyan-400 scale-110" : "text-cyan-300"
|
||||
)}>
|
||||
{currentSpinner}
|
||||
</span>
|
||||
|
||||
{/* Status text - first line */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{statusText}...</span>
|
||||
<span className="text-gray-400 text-sm">({elapsedTime}s)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interrupt button */}
|
||||
{canInterrupt && onAbort && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAbort}
|
||||
className="ml-3 text-xs bg-red-600 hover:bg-red-700 text-white px-2.5 py-1 sm:px-3 sm:py-1.5 rounded-md transition-colors flex items-center gap-1.5 flex-shrink-0"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Stop</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeminiStatus;
|
||||
@@ -1,14 +1,14 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { X, ExternalLink, KeyRound } from 'lucide-react';
|
||||
import StandaloneShell from './standalone-shell/view/StandaloneShell';
|
||||
import { IS_PLATFORM } from '../constants/config';
|
||||
|
||||
/**
|
||||
* Reusable login modal component for Claude, Cursor, and Codex CLI authentication
|
||||
* Reusable login modal component for Claude, Cursor, Codex, and Gemini CLI authentication
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - Whether the modal is visible
|
||||
* @param {Function} props.onClose - Callback when modal is closed
|
||||
* @param {'claude'|'cursor'|'codex'} props.provider - Which CLI provider to authenticate with
|
||||
* @param {'claude'|'cursor'|'codex'|'gemini'} props.provider - Which CLI provider to authenticate with
|
||||
* @param {Object} props.project - Project object containing name and path information
|
||||
* @param {Function} props.onComplete - Callback when login process completes (receives exitCode)
|
||||
* @param {string} props.customCommand - Optional custom command to override defaults
|
||||
@@ -36,6 +36,9 @@ function LoginModal({
|
||||
return 'cursor-agent login';
|
||||
case 'codex':
|
||||
return IS_PLATFORM ? 'codex login --device-auth' : 'codex login';
|
||||
case 'gemini':
|
||||
// No explicit interactive login command for gemini CLI exists yet similar to Claude, so we'll just check status or instruct the user to configure `.gemini.json`
|
||||
return 'gemini status';
|
||||
default:
|
||||
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : isOnboarding ? 'claude /exit --dangerously-skip-permissions' : 'claude /login --dangerously-skip-permissions';
|
||||
}
|
||||
@@ -49,6 +52,8 @@ function LoginModal({
|
||||
return 'Cursor CLI Login';
|
||||
case 'codex':
|
||||
return 'Codex CLI Login';
|
||||
case 'gemini':
|
||||
return 'Gemini CLI Configuration';
|
||||
default:
|
||||
return 'CLI Login';
|
||||
}
|
||||
@@ -77,12 +82,68 @@ function LoginModal({
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<StandaloneShell
|
||||
project={project}
|
||||
command={getCommand()}
|
||||
onComplete={handleComplete}
|
||||
minimal={true}
|
||||
/>
|
||||
{provider === 'gemini' ? (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8 text-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-6">
|
||||
<KeyRound className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
|
||||
<h4 className="text-xl font-medium text-gray-900 dark:text-white mb-3">
|
||||
Setup Gemini API Access
|
||||
</h4>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-400 max-w-md mb-8">
|
||||
The Gemini CLI requires an API key to function. Unlike Claude, you'll need to configure this directly in your terminal first.
|
||||
</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 max-w-lg w-full text-left shadow-sm">
|
||||
<ol className="space-y-4">
|
||||
<li className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 flex items-center justify-center text-sm font-medium">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">Get your API Key</p>
|
||||
<a
|
||||
href="https://aistudio.google.com/app/apikey"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 inline-flex"
|
||||
>
|
||||
Google AI Studio <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 flex items-center justify-center text-sm font-medium">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">Run configuration</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">Open your terminal and run:</p>
|
||||
<code className="block bg-gray-100 dark:bg-gray-900 px-3 py-2 rounded text-sm text-pink-600 dark:text-pink-400 font-mono">
|
||||
gemini config set api_key YOUR_KEY
|
||||
</code>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-8 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<StandaloneShell
|
||||
project={project}
|
||||
command={getCommand()}
|
||||
onComplete={handleComplete}
|
||||
minimal={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,13 @@ const Onboarding = ({ onComplete }) => {
|
||||
error: null
|
||||
});
|
||||
|
||||
const [geminiAuthStatus, setGeminiAuthStatus] = useState({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
const { user } = useAuth();
|
||||
|
||||
const prevActiveLoginProviderRef = useRef(undefined);
|
||||
@@ -69,22 +76,23 @@ const Onboarding = ({ onComplete }) => {
|
||||
checkClaudeAuthStatus();
|
||||
checkCursorAuthStatus();
|
||||
checkCodexAuthStatus();
|
||||
checkGeminiAuthStatus();
|
||||
}
|
||||
}, [activeLoginProvider]);
|
||||
|
||||
const checkClaudeAuthStatus = async () => {
|
||||
const checkProviderAuthStatus = async (provider, setter) => {
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/cli/claude/status');
|
||||
const response = await authenticatedFetch(`/api/cli/${provider}/status`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setClaudeAuthStatus({
|
||||
setter({
|
||||
authenticated: data.authenticated,
|
||||
email: data.email,
|
||||
loading: false,
|
||||
error: data.error || null
|
||||
});
|
||||
} else {
|
||||
setClaudeAuthStatus({
|
||||
setter({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
@@ -92,8 +100,8 @@ const Onboarding = ({ onComplete }) => {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking Claude auth status:', error);
|
||||
setClaudeAuthStatus({
|
||||
console.error(`Error checking ${provider} auth status:`, error);
|
||||
setter({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
@@ -102,69 +110,15 @@ const Onboarding = ({ onComplete }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkCursorAuthStatus = async () => {
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/cli/cursor/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCursorAuthStatus({
|
||||
authenticated: data.authenticated,
|
||||
email: data.email,
|
||||
loading: false,
|
||||
error: data.error || null
|
||||
});
|
||||
} else {
|
||||
setCursorAuthStatus({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: 'Failed to check authentication status'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking Cursor auth status:', error);
|
||||
setCursorAuthStatus({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkCodexAuthStatus = async () => {
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/cli/codex/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCodexAuthStatus({
|
||||
authenticated: data.authenticated,
|
||||
email: data.email,
|
||||
loading: false,
|
||||
error: data.error || null
|
||||
});
|
||||
} else {
|
||||
setCodexAuthStatus({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: 'Failed to check authentication status'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking Codex auth status:', error);
|
||||
setCodexAuthStatus({
|
||||
authenticated: false,
|
||||
email: null,
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
const checkClaudeAuthStatus = () => checkProviderAuthStatus('claude', setClaudeAuthStatus);
|
||||
const checkCursorAuthStatus = () => checkProviderAuthStatus('cursor', setCursorAuthStatus);
|
||||
const checkCodexAuthStatus = () => checkProviderAuthStatus('codex', setCodexAuthStatus);
|
||||
const checkGeminiAuthStatus = () => checkProviderAuthStatus('gemini', setGeminiAuthStatus);
|
||||
|
||||
const handleClaudeLogin = () => setActiveLoginProvider('claude');
|
||||
const handleCursorLogin = () => setActiveLoginProvider('cursor');
|
||||
const handleCodexLogin = () => setActiveLoginProvider('codex');
|
||||
const handleGeminiLogin = () => setActiveLoginProvider('gemini');
|
||||
|
||||
const handleLoginComplete = (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
@@ -174,6 +128,8 @@ const Onboarding = ({ onComplete }) => {
|
||||
checkCursorAuthStatus();
|
||||
} else if (activeLoginProvider === 'codex') {
|
||||
checkCodexAuthStatus();
|
||||
} else if (activeLoginProvider === 'gemini') {
|
||||
checkGeminiAuthStatus();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -337,11 +293,10 @@ const Onboarding = ({ onComplete }) => {
|
||||
{/* Agent Cards Grid */}
|
||||
<div className="space-y-3">
|
||||
{/* Claude */}
|
||||
<div className={`border rounded-lg p-4 transition-colors ${
|
||||
claudeAuthStatus.authenticated
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className={`border rounded-lg p-4 transition-colors ${claudeAuthStatus.authenticated
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
|
||||
@@ -354,7 +309,7 @@ const Onboarding = ({ onComplete }) => {
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{claudeAuthStatus.loading ? 'Checking...' :
|
||||
claudeAuthStatus.authenticated ? claudeAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
claudeAuthStatus.authenticated ? claudeAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,11 +325,10 @@ const Onboarding = ({ onComplete }) => {
|
||||
</div>
|
||||
|
||||
{/* Cursor */}
|
||||
<div className={`border rounded-lg p-4 transition-colors ${
|
||||
cursorAuthStatus.authenticated
|
||||
? 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className={`border rounded-lg p-4 transition-colors ${cursorAuthStatus.authenticated
|
||||
? 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
|
||||
@@ -387,7 +341,7 @@ const Onboarding = ({ onComplete }) => {
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{cursorAuthStatus.loading ? 'Checking...' :
|
||||
cursorAuthStatus.authenticated ? cursorAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
cursorAuthStatus.authenticated ? cursorAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,11 +357,10 @@ const Onboarding = ({ onComplete }) => {
|
||||
</div>
|
||||
|
||||
{/* Codex */}
|
||||
<div className={`border rounded-lg p-4 transition-colors ${
|
||||
codexAuthStatus.authenticated
|
||||
? 'bg-gray-100 dark:bg-gray-800/50 border-gray-300 dark:border-gray-600'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className={`border rounded-lg p-4 transition-colors ${codexAuthStatus.authenticated
|
||||
? 'bg-gray-100 dark:bg-gray-800/50 border-gray-300 dark:border-gray-600'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
||||
@@ -420,7 +373,7 @@ const Onboarding = ({ onComplete }) => {
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{codexAuthStatus.loading ? 'Checking...' :
|
||||
codexAuthStatus.authenticated ? codexAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
codexAuthStatus.authenticated ? codexAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,6 +387,38 @@ const Onboarding = ({ onComplete }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gemini */}
|
||||
<div className={`border rounded-lg p-4 transition-colors ${geminiAuthStatus.authenticated
|
||||
? 'bg-teal-50 dark:bg-teal-900/20 border-teal-200 dark:border-teal-800'
|
||||
: 'border-border bg-card'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-teal-100 dark:bg-teal-900/30 rounded-full flex items-center justify-center">
|
||||
<SessionProviderLogo provider="gemini" className="w-5 h-5 text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground flex items-center gap-2">
|
||||
Gemini
|
||||
{geminiAuthStatus.authenticated && <Check className="w-4 h-4 text-green-500" />}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{geminiAuthStatus.loading ? 'Checking...' :
|
||||
geminiAuthStatus.authenticated ? geminiAuthStatus.email || 'Connected' : 'Not connected'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!geminiAuthStatus.authenticated && !geminiAuthStatus.loading && (
|
||||
<button
|
||||
onClick={handleGeminiLogin}
|
||||
className="bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground pt-2">
|
||||
@@ -452,7 +437,7 @@ const Onboarding = ({ onComplete }) => {
|
||||
case 0:
|
||||
return gitName.trim() && gitEmail.trim() && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(gitEmail);
|
||||
case 1:
|
||||
return true;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -468,11 +453,10 @@ const Onboarding = ({ onComplete }) => {
|
||||
{steps.map((step, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center border-2 transition-colors duration-200 ${
|
||||
index < currentStep ? 'bg-green-500 border-green-500 text-white' :
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center border-2 transition-colors duration-200 ${index < currentStep ? 'bg-green-500 border-green-500 text-white' :
|
||||
index === currentStep ? 'bg-blue-600 border-blue-600 text-white' :
|
||||
'bg-background border-border text-muted-foreground'
|
||||
}`}>
|
||||
'bg-background border-border text-muted-foreground'
|
||||
}`}>
|
||||
{index < currentStep ? (
|
||||
<Check className="w-6 h-6" />
|
||||
) : typeof step.icon === 'function' ? (
|
||||
@@ -482,9 +466,8 @@ const Onboarding = ({ onComplete }) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-center">
|
||||
<p className={`text-sm font-medium ${
|
||||
index === currentStep ? 'text-foreground' : 'text-muted-foreground'
|
||||
}`}>
|
||||
<p className={`text-sm font-medium ${index === currentStep ? 'text-foreground' : 'text-muted-foreground'
|
||||
}`}>
|
||||
{step.title}
|
||||
</p>
|
||||
{step.required && (
|
||||
@@ -493,9 +476,8 @@ const Onboarding = ({ onComplete }) => {
|
||||
</div>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`flex-1 h-0.5 mx-2 transition-colors duration-200 ${
|
||||
index < currentStep ? 'bg-green-500' : 'bg-border'
|
||||
}`} />
|
||||
<div className={`flex-1 h-0.5 mx-2 transition-colors duration-200 ${index < currentStep ? 'bg-green-500' : 'bg-border'
|
||||
}`} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
@@ -41,6 +41,7 @@ interface UseChatComposerStateArgs {
|
||||
cursorModel: string;
|
||||
claudeModel: string;
|
||||
codexModel: string;
|
||||
geminiModel: string;
|
||||
isLoading: boolean;
|
||||
canAbortSession: boolean;
|
||||
tokenBudget: Record<string, unknown> | null;
|
||||
@@ -93,6 +94,7 @@ export function useChatComposerState({
|
||||
cursorModel,
|
||||
claudeModel,
|
||||
codexModel,
|
||||
geminiModel,
|
||||
isLoading,
|
||||
canAbortSession,
|
||||
tokenBudget,
|
||||
@@ -289,7 +291,7 @@ export function useChatComposerState({
|
||||
projectName: selectedProject.name,
|
||||
sessionId: currentSessionId,
|
||||
provider,
|
||||
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : claudeModel,
|
||||
model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : claudeModel,
|
||||
tokenUsage: tokenBudget,
|
||||
};
|
||||
|
||||
@@ -343,6 +345,7 @@ export function useChatComposerState({
|
||||
codexModel,
|
||||
currentSessionId,
|
||||
cursorModel,
|
||||
geminiModel,
|
||||
handleBuiltInCommand,
|
||||
handleCustomCommand,
|
||||
input,
|
||||
@@ -581,8 +584,10 @@ export function useChatComposerState({
|
||||
provider === 'cursor'
|
||||
? 'cursor-tools-settings'
|
||||
: provider === 'codex'
|
||||
? 'codex-settings'
|
||||
: 'claude-settings';
|
||||
? 'codex-settings'
|
||||
: provider === 'gemini'
|
||||
? 'gemini-settings'
|
||||
: 'claude-settings';
|
||||
const savedSettings = safeLocalStorage.getItem(settingsKey);
|
||||
if (savedSettings) {
|
||||
return JSON.parse(savedSettings);
|
||||
@@ -630,6 +635,21 @@ export function useChatComposerState({
|
||||
permissionMode: permissionMode === 'plan' ? 'default' : permissionMode,
|
||||
},
|
||||
});
|
||||
} else if (provider === 'gemini') {
|
||||
sendMessage({
|
||||
type: 'gemini-command',
|
||||
command: messageContent,
|
||||
sessionId: effectiveSessionId,
|
||||
options: {
|
||||
cwd: resolvedProjectPath,
|
||||
projectPath: resolvedProjectPath,
|
||||
sessionId: effectiveSessionId,
|
||||
resume: Boolean(effectiveSessionId),
|
||||
model: geminiModel,
|
||||
permissionMode,
|
||||
toolsSettings,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
sendMessage({
|
||||
type: 'claude-command',
|
||||
@@ -669,6 +689,7 @@ export function useChatComposerState({
|
||||
currentSessionId,
|
||||
cursorModel,
|
||||
executeCommand,
|
||||
geminiModel,
|
||||
isLoading,
|
||||
onSessionActive,
|
||||
onSessionProcessing,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { authenticatedFetch } from '../../../utils/api';
|
||||
import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS } from '../../../../shared/modelConstants';
|
||||
import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS } from '../../../../shared/modelConstants';
|
||||
import type { PendingPermissionRequest, PermissionMode, Provider } from '../types/types';
|
||||
import type { ProjectSession, SessionProvider } from '../../../types/app';
|
||||
|
||||
@@ -23,6 +23,9 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
|
||||
const [codexModel, setCodexModel] = useState<string>(() => {
|
||||
return localStorage.getItem('codex-model') || CODEX_MODELS.DEFAULT;
|
||||
});
|
||||
const [geminiModel, setGeminiModel] = useState<string>(() => {
|
||||
return localStorage.getItem('gemini-model') || GEMINI_MODELS.DEFAULT;
|
||||
});
|
||||
|
||||
const lastProviderRef = useRef(provider);
|
||||
|
||||
@@ -105,6 +108,8 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
|
||||
setClaudeModel,
|
||||
codexModel,
|
||||
setCodexModel,
|
||||
geminiModel,
|
||||
setGeminiModel,
|
||||
permissionMode,
|
||||
setPermissionMode,
|
||||
pendingPermissionRequests,
|
||||
|
||||
@@ -145,6 +145,7 @@ export function useChatRealtimeHandlers({
|
||||
'claude-error',
|
||||
'cursor-error',
|
||||
'codex-error',
|
||||
'gemini-error',
|
||||
]);
|
||||
|
||||
const isClaudeSystemInit =
|
||||
@@ -162,8 +163,8 @@ export function useChatRealtimeHandlers({
|
||||
const systemInitSessionId = isClaudeSystemInit
|
||||
? structuredMessageData?.session_id
|
||||
: isCursorSystemInit
|
||||
? rawStructuredData?.session_id
|
||||
: null;
|
||||
? rawStructuredData?.session_id
|
||||
: null;
|
||||
|
||||
const activeViewSessionId =
|
||||
selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
||||
@@ -176,7 +177,8 @@ export function useChatRealtimeHandlers({
|
||||
!pendingViewSessionRef.current.sessionId &&
|
||||
(latestMessage.type === 'claude-error' ||
|
||||
latestMessage.type === 'cursor-error' ||
|
||||
latestMessage.type === 'codex-error');
|
||||
latestMessage.type === 'codex-error' ||
|
||||
latestMessage.type === 'gemini-error');
|
||||
|
||||
const handleBackgroundLifecycle = (sessionId?: string) => {
|
||||
if (!sessionId) {
|
||||
@@ -225,12 +227,6 @@ export function useChatRealtimeHandlers({
|
||||
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||
}
|
||||
console.log(
|
||||
'Skipping message for different session:',
|
||||
latestMessage.sessionId,
|
||||
'current:',
|
||||
activeViewSessionId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -297,11 +293,6 @@ export function useChatRealtimeHandlers({
|
||||
structuredMessageData.session_id !== currentSessionId &&
|
||||
isSystemInitForView
|
||||
) {
|
||||
console.log('Claude CLI session duplication detected:', {
|
||||
originalSession: currentSessionId,
|
||||
newSession: structuredMessageData.session_id,
|
||||
});
|
||||
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(structuredMessageData.session_id);
|
||||
return;
|
||||
@@ -314,10 +305,6 @@ export function useChatRealtimeHandlers({
|
||||
!currentSessionId &&
|
||||
isSystemInitForView
|
||||
) {
|
||||
console.log('New session init detected:', {
|
||||
newSession: structuredMessageData.session_id,
|
||||
});
|
||||
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(structuredMessageData.session_id);
|
||||
return;
|
||||
@@ -331,7 +318,6 @@ export function useChatRealtimeHandlers({
|
||||
structuredMessageData.session_id === currentSessionId &&
|
||||
isSystemInitForView
|
||||
) {
|
||||
console.log('System init message for current session, ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -583,17 +569,12 @@ export function useChatRealtimeHandlers({
|
||||
}
|
||||
|
||||
if (currentSessionId && cursorData.session_id !== currentSessionId) {
|
||||
console.log('Cursor session switch detected:', {
|
||||
originalSession: currentSessionId,
|
||||
newSession: cursorData.session_id,
|
||||
});
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(cursorData.session_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('Cursor new session init detected:', { newSession: cursorData.session_id });
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(cursorData.session_id);
|
||||
return;
|
||||
@@ -612,9 +593,8 @@ export function useChatRealtimeHandlers({
|
||||
...previous,
|
||||
{
|
||||
type: 'assistant',
|
||||
content: `Using tool: ${latestMessage.tool} ${
|
||||
latestMessage.input ? `with ${latestMessage.input}` : ''
|
||||
}`,
|
||||
content: `Using tool: ${latestMessage.tool} ${latestMessage.input ? `with ${latestMessage.input}` : ''
|
||||
}`,
|
||||
timestamp: new Date(),
|
||||
isToolUse: true,
|
||||
toolName: latestMessage.tool,
|
||||
@@ -897,7 +877,6 @@ export function useChatRealtimeHandlers({
|
||||
onNavigateToSession?.(codexActualSessionId);
|
||||
}
|
||||
sessionStorage.removeItem('pendingSessionId');
|
||||
console.log('Codex session complete, ID set to:', codexPendingSessionId);
|
||||
}
|
||||
|
||||
if (selectedProject) {
|
||||
@@ -919,6 +898,91 @@ export function useChatRealtimeHandlers({
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'gemini-response': {
|
||||
const geminiData = latestMessage.data;
|
||||
|
||||
if (geminiData && geminiData.type === 'message' && typeof geminiData.content === 'string') {
|
||||
const content = decodeHtmlEntities(geminiData.content);
|
||||
|
||||
if (content) {
|
||||
streamBufferRef.current += streamBufferRef.current ? `\n${content}` : content;
|
||||
}
|
||||
|
||||
if (!geminiData.isPartial) {
|
||||
// Immediate flush and finalization for the last chunk
|
||||
if (streamTimerRef.current) {
|
||||
clearTimeout(streamTimerRef.current);
|
||||
streamTimerRef.current = null;
|
||||
}
|
||||
const chunk = streamBufferRef.current;
|
||||
streamBufferRef.current = '';
|
||||
|
||||
if (chunk) {
|
||||
appendStreamingChunk(setChatMessages, chunk, true);
|
||||
}
|
||||
finalizeStreamingMessage(setChatMessages);
|
||||
} else if (!streamTimerRef.current && streamBufferRef.current) {
|
||||
streamTimerRef.current = window.setTimeout(() => {
|
||||
const chunk = streamBufferRef.current;
|
||||
streamBufferRef.current = '';
|
||||
streamTimerRef.current = null;
|
||||
|
||||
if (chunk) {
|
||||
appendStreamingChunk(setChatMessages, chunk, true);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'gemini-error':
|
||||
setIsLoading(false);
|
||||
setCanAbortSession(false);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
type: 'error',
|
||||
content: latestMessage.error || 'An error occurred with Gemini',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'gemini-tool-use':
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
type: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
isToolUse: true,
|
||||
toolName: latestMessage.toolName,
|
||||
toolInput: latestMessage.parameters ? JSON.stringify(latestMessage.parameters, null, 2) : '',
|
||||
toolId: latestMessage.toolId,
|
||||
toolResult: null,
|
||||
}
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'gemini-tool-result':
|
||||
setChatMessages((previous) =>
|
||||
previous.map((message) => {
|
||||
if (message.isToolUse && message.toolId === latestMessage.toolId) {
|
||||
return {
|
||||
...message,
|
||||
toolResult: {
|
||||
content: latestMessage.output || `Status: ${latestMessage.status}`,
|
||||
isError: latestMessage.status === 'error',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
};
|
||||
}
|
||||
return message;
|
||||
}),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'session-aborted': {
|
||||
const pendingSessionId =
|
||||
typeof window !== 'undefined' ? sessionStorage.getItem('pendingSessionId') : null;
|
||||
|
||||
@@ -64,6 +64,8 @@ function ChatInterface({
|
||||
setClaudeModel,
|
||||
codexModel,
|
||||
setCodexModel,
|
||||
geminiModel,
|
||||
setGeminiModel,
|
||||
permissionMode,
|
||||
pendingPermissionRequests,
|
||||
setPendingPermissionRequests,
|
||||
@@ -174,6 +176,7 @@ function ChatInterface({
|
||||
cursorModel,
|
||||
claudeModel,
|
||||
codexModel,
|
||||
geminiModel,
|
||||
isLoading,
|
||||
canAbortSession,
|
||||
tokenBudget,
|
||||
@@ -251,7 +254,9 @@ function ChatInterface({
|
||||
? t('messageTypes.cursor')
|
||||
: provider === 'codex'
|
||||
? t('messageTypes.codex')
|
||||
: t('messageTypes.claude');
|
||||
: provider === 'gemini'
|
||||
? t('messageTypes.gemini')
|
||||
: t('messageTypes.claude');
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
@@ -287,6 +292,8 @@ function ChatInterface({
|
||||
setCursorModel={setCursorModel}
|
||||
codexModel={codexModel}
|
||||
setCodexModel={setCodexModel}
|
||||
geminiModel={geminiModel}
|
||||
setGeminiModel={setGeminiModel}
|
||||
tasksEnabled={tasksEnabled}
|
||||
isTaskMasterInstalled={isTaskMasterInstalled}
|
||||
onShowAllTasks={onShowAllTasks}
|
||||
@@ -374,8 +381,10 @@ function ChatInterface({
|
||||
provider === 'cursor'
|
||||
? t('messageTypes.cursor')
|
||||
: provider === 'codex'
|
||||
? t('messageTypes.codex')
|
||||
: t('messageTypes.claude'),
|
||||
? t('messageTypes.codex')
|
||||
: provider === 'gemini'
|
||||
? t('messageTypes.gemini')
|
||||
: t('messageTypes.claude'),
|
||||
})}
|
||||
isTextareaExpanded={isTextareaExpanded}
|
||||
sendByCtrlEnter={sendByCtrlEnter}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function AssistantThinkingIndicator({ selectedProvider }: Assista
|
||||
<SessionProviderLogo provider={selectedProvider} className="w-full h-full" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : 'Claude'}
|
||||
{selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : selectedProvider === 'gemini' ? 'Gemini' : 'Claude'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full text-sm text-gray-500 dark:text-gray-400 pl-3 sm:pl-0">
|
||||
|
||||
@@ -26,6 +26,8 @@ interface ChatMessagesPaneProps {
|
||||
setCursorModel: (model: string) => void;
|
||||
codexModel: string;
|
||||
setCodexModel: (model: string) => void;
|
||||
geminiModel: string;
|
||||
setGeminiModel: (model: string) => void;
|
||||
tasksEnabled: boolean;
|
||||
isTaskMasterInstalled: boolean | null;
|
||||
onShowAllTasks?: (() => void) | null;
|
||||
@@ -70,6 +72,8 @@ export default function ChatMessagesPane({
|
||||
setCursorModel,
|
||||
codexModel,
|
||||
setCodexModel,
|
||||
geminiModel,
|
||||
setGeminiModel,
|
||||
tasksEnabled,
|
||||
isTaskMasterInstalled,
|
||||
onShowAllTasks,
|
||||
@@ -152,6 +156,8 @@ export default function ChatMessagesPane({
|
||||
setCursorModel={setCursorModel}
|
||||
codexModel={codexModel}
|
||||
setCodexModel={setCodexModel}
|
||||
geminiModel={geminiModel}
|
||||
setGeminiModel={setGeminiModel}
|
||||
tasksEnabled={tasksEnabled}
|
||||
isTaskMasterInstalled={isTaskMasterInstalled}
|
||||
onShowAllTasks={onShowAllTasks}
|
||||
|
||||
@@ -60,6 +60,7 @@ export default function ClaudeStatus({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Note: showThinking only controls the reasoning accordion in messages, not this processing indicator
|
||||
const actionIndex = Math.floor(elapsedTime / 3) % ACTION_WORDS.length;
|
||||
const statusText = status?.text || ACTION_WORDS[actionIndex];
|
||||
const tokens = status?.tokens || fakeTokens;
|
||||
@@ -101,6 +102,7 @@ export default function ClaudeStatus({
|
||||
|
||||
{canInterrupt && onAbort && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAbort}
|
||||
className="ml-2 sm:ml-3 text-xs bg-red-600 hover:bg-red-700 active:bg-red-800 text-white px-2 py-1 sm:px-3 sm:py-1.5 rounded-md transition-colors flex items-center gap-1 sm:gap-1.5 flex-shrink-0 font-medium"
|
||||
>
|
||||
|
||||
@@ -45,10 +45,10 @@ type PermissionGrantState = 'idle' | 'granted' | 'error';
|
||||
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const isGrouped = prevMessage && prevMessage.type === message.type &&
|
||||
((prevMessage.type === 'assistant') ||
|
||||
(prevMessage.type === 'user') ||
|
||||
(prevMessage.type === 'tool') ||
|
||||
(prevMessage.type === 'error'));
|
||||
((prevMessage.type === 'assistant') ||
|
||||
(prevMessage.type === 'user') ||
|
||||
(prevMessage.type === 'tool') ||
|
||||
(prevMessage.type === 'error'));
|
||||
const messageRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const permissionSuggestion = getClaudePermissionSuggestion(message, provider);
|
||||
@@ -154,11 +154,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{message.type === 'error' ? t('messageTypes.error') : message.type === 'tool' ? t('messageTypes.tool') : (provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : t('messageTypes.claude'))}
|
||||
{message.type === 'error' ? t('messageTypes.error') : message.type === 'tool' ? t('messageTypes.tool') : (provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : provider === 'gemini' ? t('messageTypes.gemini') : t('messageTypes.claude'))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="w-full">
|
||||
|
||||
{message.isToolUse ? (
|
||||
@@ -188,7 +188,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
subagentState={message.subagentState}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Tool Result Section */}
|
||||
{message.toolResult && !shouldHideToolResult(message.toolName || 'UnknownTool', message.toolResult) && (
|
||||
message.toolResult.isError ? (
|
||||
@@ -222,11 +222,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
}
|
||||
}}
|
||||
disabled={permissionSuggestion.isAllowed || permissionGrantState === 'granted'}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border transition-colors ${
|
||||
permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? 'bg-green-100 dark:bg-green-900/30 border-green-300/70 dark:border-green-800/60 text-green-800 dark:text-green-200 cursor-default'
|
||||
: 'bg-white/80 dark:bg-gray-900/40 border-red-300/70 dark:border-red-800/60 text-red-700 dark:text-red-200 hover:bg-white dark:hover:bg-gray-900/70'
|
||||
}`}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border transition-colors ${permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? 'bg-green-100 dark:bg-green-900/30 border-green-300/70 dark:border-green-800/60 text-green-800 dark:text-green-200 cursor-default'
|
||||
: 'bg-white/80 dark:bg-gray-900/40 border-red-300/70 dark:border-red-800/60 text-red-700 dark:text-red-200 hover:bg-white dark:hover:bg-gray-900/70'
|
||||
}`}
|
||||
>
|
||||
{permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? t('permissions.added')
|
||||
@@ -294,7 +293,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
const lines = (message.content || '').split('\n').filter((line) => line.trim());
|
||||
const questionLine = lines.find((line) => line.includes('?')) || lines[0] || '';
|
||||
const options: InteractiveOption[] = [];
|
||||
|
||||
|
||||
// Parse the menu options
|
||||
lines.forEach((line) => {
|
||||
// Match lines like "❯ 1. Yes" or " 2. No"
|
||||
@@ -308,31 +307,29 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200 mb-4">
|
||||
{questionLine}
|
||||
</p>
|
||||
|
||||
|
||||
{/* Option buttons */}
|
||||
<div className="space-y-2 mb-4">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.number}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${
|
||||
option.isSelected
|
||||
? 'bg-amber-600 dark:bg-amber-700 text-white border-amber-600 dark:border-amber-700 shadow-md'
|
||||
: 'bg-white dark:bg-gray-800 text-amber-900 dark:text-amber-100 border-amber-300 dark:border-amber-700'
|
||||
} cursor-not-allowed opacity-75`}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${option.isSelected
|
||||
? 'bg-amber-600 dark:bg-amber-700 text-white border-amber-600 dark:border-amber-700 shadow-md'
|
||||
: 'bg-white dark:bg-gray-800 text-amber-900 dark:text-amber-100 border-amber-300 dark:border-amber-700'
|
||||
} cursor-not-allowed opacity-75`}
|
||||
disabled
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
option.isSelected
|
||||
? 'bg-white/20'
|
||||
: 'bg-amber-100 dark:bg-amber-800/50'
|
||||
}`}>
|
||||
<span className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${option.isSelected
|
||||
? 'bg-white/20'
|
||||
: 'bg-amber-100 dark:bg-amber-800/50'
|
||||
}`}>
|
||||
{option.number}
|
||||
</span>
|
||||
<span className="text-sm sm:text-base font-medium flex-1">
|
||||
@@ -345,7 +342,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-amber-100 dark:bg-amber-800/30 rounded-lg p-3">
|
||||
<p className="text-amber-900 dark:text-amber-100 text-sm font-medium mb-1">
|
||||
{t('interactive.waiting')}
|
||||
@@ -399,7 +396,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
// Detect if content is pure JSON (starts with { or [)
|
||||
const trimmedContent = content.trim();
|
||||
if ((trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) &&
|
||||
(trimmedContent.endsWith('}') || trimmedContent.endsWith(']'))) {
|
||||
(trimmedContent.endsWith('}') || trimmedContent.endsWith(']'))) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmedContent);
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
@@ -439,7 +436,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!isGrouped && (
|
||||
<div className="text-[11px] text-gray-400 dark:text-gray-500 mt-1">
|
||||
{formattedTime}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Check, ChevronDown } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
||||
import NextTaskBanner from '../../../NextTaskBanner.jsx';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../../../../shared/modelConstants';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, GEMINI_MODELS } from '../../../../../shared/modelConstants';
|
||||
import type { ProjectSession, SessionProvider } from '../../../../types/app';
|
||||
|
||||
interface ProviderSelectionEmptyStateProps {
|
||||
@@ -18,6 +18,8 @@ interface ProviderSelectionEmptyStateProps {
|
||||
setCursorModel: (model: string) => void;
|
||||
codexModel: string;
|
||||
setCodexModel: (model: string) => void;
|
||||
geminiModel: string;
|
||||
setGeminiModel: (model: string) => void;
|
||||
tasksEnabled: boolean;
|
||||
isTaskMasterInstalled: boolean | null;
|
||||
onShowAllTasks?: (() => void) | null;
|
||||
@@ -58,17 +60,27 @@ const PROVIDERS: ProviderDef[] = [
|
||||
ring: 'ring-emerald-600/15',
|
||||
check: 'bg-emerald-600 dark:bg-emerald-500 text-white',
|
||||
},
|
||||
{
|
||||
id: 'gemini',
|
||||
name: 'Gemini',
|
||||
infoKey: 'providerSelection.providerInfo.google',
|
||||
accent: 'border-blue-500 dark:border-blue-400',
|
||||
ring: 'ring-blue-500/15',
|
||||
check: 'bg-blue-500 text-white',
|
||||
},
|
||||
];
|
||||
|
||||
function getModelConfig(p: SessionProvider) {
|
||||
if (p === 'claude') return CLAUDE_MODELS;
|
||||
if (p === 'codex') return CODEX_MODELS;
|
||||
if (p === 'gemini') return GEMINI_MODELS;
|
||||
return CURSOR_MODELS;
|
||||
}
|
||||
|
||||
function getModelValue(p: SessionProvider, c: string, cu: string, co: string) {
|
||||
function getModelValue(p: SessionProvider, c: string, cu: string, co: string, g: string) {
|
||||
if (p === 'claude') return c;
|
||||
if (p === 'codex') return co;
|
||||
if (p === 'gemini') return g;
|
||||
return cu;
|
||||
}
|
||||
|
||||
@@ -84,6 +96,8 @@ export default function ProviderSelectionEmptyState({
|
||||
setCursorModel,
|
||||
codexModel,
|
||||
setCodexModel,
|
||||
geminiModel,
|
||||
setGeminiModel,
|
||||
tasksEnabled,
|
||||
isTaskMasterInstalled,
|
||||
onShowAllTasks,
|
||||
@@ -101,11 +115,12 @@ export default function ProviderSelectionEmptyState({
|
||||
const handleModelChange = (value: string) => {
|
||||
if (provider === 'claude') { setClaudeModel(value); localStorage.setItem('claude-model', value); }
|
||||
else if (provider === 'codex') { setCodexModel(value); localStorage.setItem('codex-model', value); }
|
||||
else if (provider === 'gemini') { setGeminiModel(value); localStorage.setItem('gemini-model', value); }
|
||||
else { setCursorModel(value); localStorage.setItem('cursor-model', value); }
|
||||
};
|
||||
|
||||
const modelConfig = getModelConfig(provider);
|
||||
const currentModel = getModelValue(provider, claudeModel, cursorModel, codexModel);
|
||||
const currentModel = getModelValue(provider, claudeModel, cursorModel, codexModel, geminiModel);
|
||||
|
||||
/* ── New session — provider picker ── */
|
||||
if (!selectedSession && !currentSessionId) {
|
||||
@@ -123,7 +138,7 @@ export default function ProviderSelectionEmptyState({
|
||||
</div>
|
||||
|
||||
{/* Provider cards — horizontal row, equal width */}
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-2.5 mb-6">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-2.5 mb-6">
|
||||
{PROVIDERS.map((p) => {
|
||||
const active = provider === p.id;
|
||||
return (
|
||||
@@ -179,13 +194,14 @@ export default function ProviderSelectionEmptyState({
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground/70">
|
||||
{provider === 'claude'
|
||||
? t('providerSelection.readyPrompt.claude', { model: claudeModel })
|
||||
: provider === 'cursor'
|
||||
? t('providerSelection.readyPrompt.cursor', { model: cursorModel })
|
||||
: provider === 'codex'
|
||||
? t('providerSelection.readyPrompt.codex', { model: codexModel })
|
||||
: t('providerSelection.readyPrompt.default')}
|
||||
{
|
||||
{
|
||||
claude: t('providerSelection.readyPrompt.claude', { model: claudeModel }),
|
||||
cursor: t('providerSelection.readyPrompt.cursor', { model: cursorModel }),
|
||||
codex: t('providerSelection.readyPrompt.codex', { model: codexModel }),
|
||||
gemini: t('providerSelection.readyPrompt.gemini', { model: geminiModel }),
|
||||
}[provider]
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SessionProvider } from '../../types/app';
|
||||
import ClaudeLogo from './ClaudeLogo';
|
||||
import CodexLogo from './CodexLogo';
|
||||
import CursorLogo from './CursorLogo';
|
||||
import GeminiLogo from '../GeminiLogo';
|
||||
|
||||
type SessionProviderLogoProps = {
|
||||
provider?: SessionProvider | string | null;
|
||||
@@ -20,5 +21,9 @@ export default function SessionProviderLogo({
|
||||
return <CodexLogo className={className} />;
|
||||
}
|
||||
|
||||
if (provider === 'gemini') {
|
||||
return <GeminiLogo className={className} />;
|
||||
}
|
||||
|
||||
return <ClaudeLogo className={className} />;
|
||||
}
|
||||
|
||||
@@ -91,4 +91,5 @@ export const AUTH_STATUS_ENDPOINTS: Record<AgentProvider, string> = {
|
||||
claude: '/api/cli/claude/status',
|
||||
cursor: '/api/cli/cursor/status',
|
||||
codex: '/api/cli/codex/status',
|
||||
gemini: '/api/cli/gemini/status',
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
CodexMcpFormState,
|
||||
CodexPermissionMode,
|
||||
CursorPermissionsState,
|
||||
GeminiPermissionMode,
|
||||
McpServer,
|
||||
McpToolsResult,
|
||||
McpTestResult,
|
||||
@@ -204,6 +205,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
createEmptyCursorPermissions()
|
||||
));
|
||||
const [codexPermissionMode, setCodexPermissionMode] = useState<CodexPermissionMode>('default');
|
||||
const [geminiPermissionMode, setGeminiPermissionMode] = useState<GeminiPermissionMode>('default');
|
||||
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([]);
|
||||
const [cursorMcpServers, setCursorMcpServers] = useState<McpServer[]>([]);
|
||||
@@ -224,6 +226,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
const [claudeAuthStatus, setClaudeAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [cursorAuthStatus, setCursorAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [codexAuthStatus, setCodexAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
const [geminiAuthStatus, setGeminiAuthStatus] = useState<AuthStatus>(DEFAULT_AUTH_STATUS);
|
||||
|
||||
const setAuthStatusByProvider = useCallback((provider: AgentProvider, status: AuthStatus) => {
|
||||
if (provider === 'claude') {
|
||||
@@ -236,6 +239,11 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'gemini') {
|
||||
setGeminiAuthStatus(status);
|
||||
return;
|
||||
}
|
||||
|
||||
setCodexAuthStatus(status);
|
||||
}, []);
|
||||
|
||||
@@ -655,6 +663,12 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
);
|
||||
setCodexPermissionMode(toCodexPermissionMode(savedCodexSettings.permissionMode));
|
||||
|
||||
const savedGeminiSettings = parseJson<{ permissionMode?: GeminiPermissionMode }>(
|
||||
localStorage.getItem('gemini-settings'),
|
||||
{},
|
||||
);
|
||||
setGeminiPermissionMode(savedGeminiSettings.permissionMode || 'default');
|
||||
|
||||
await Promise.all([
|
||||
fetchMcpServers(),
|
||||
fetchCursorMcpServers(),
|
||||
@@ -710,6 +724,11 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
lastUpdated: now,
|
||||
}));
|
||||
|
||||
localStorage.setItem('gemini-settings', JSON.stringify({
|
||||
permissionMode: geminiPermissionMode,
|
||||
lastUpdated: now,
|
||||
}));
|
||||
|
||||
setSaveStatus('success');
|
||||
if (closeTimerRef.current !== null) {
|
||||
window.clearTimeout(closeTimerRef.current);
|
||||
@@ -771,6 +790,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
void checkAuthStatus('claude');
|
||||
void checkAuthStatus('cursor');
|
||||
void checkAuthStatus('codex');
|
||||
void checkAuthStatus('gemini');
|
||||
}, [checkAuthStatus, initialTab, isOpen, loadSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -830,6 +850,9 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
geminiPermissionMode,
|
||||
setGeminiPermissionMode,
|
||||
openLoginForProvider,
|
||||
showLoginModal,
|
||||
setShowLoginModal,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks';
|
||||
export type AgentProvider = 'claude' | 'cursor' | 'codex';
|
||||
export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
||||
export type ProjectSortOrder = 'name' | 'date';
|
||||
export type SaveStatus = 'success' | 'error' | null;
|
||||
export type CodexPermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
|
||||
export type GeminiPermissionMode = 'default' | 'auto_edit' | 'yolo';
|
||||
export type McpImportMode = 'form' | 'json';
|
||||
export type McpScope = 'user' | 'local';
|
||||
export type McpTransportType = 'stdio' | 'sse' | 'http';
|
||||
|
||||
@@ -65,6 +65,9 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
geminiPermissionMode,
|
||||
setGeminiPermissionMode,
|
||||
openLoginForProvider,
|
||||
showLoginModal,
|
||||
setShowLoginModal,
|
||||
@@ -86,10 +89,10 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
const isAuthenticated = loginProvider === 'claude'
|
||||
? claudeAuthStatus.authenticated
|
||||
: loginProvider === 'cursor'
|
||||
? cursorAuthStatus.authenticated
|
||||
: loginProvider === 'codex'
|
||||
? codexAuthStatus.authenticated
|
||||
: false;
|
||||
? cursorAuthStatus.authenticated
|
||||
: loginProvider === 'codex'
|
||||
? codexAuthStatus.authenticated
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[9999] md:p-4 bg-background/95">
|
||||
@@ -133,15 +136,19 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
claudeAuthStatus={claudeAuthStatus}
|
||||
cursorAuthStatus={cursorAuthStatus}
|
||||
codexAuthStatus={codexAuthStatus}
|
||||
geminiAuthStatus={geminiAuthStatus}
|
||||
onClaudeLogin={() => openLoginForProvider('claude')}
|
||||
onCursorLogin={() => openLoginForProvider('cursor')}
|
||||
onCodexLogin={() => openLoginForProvider('codex')}
|
||||
onGeminiLogin={() => openLoginForProvider('gemini')}
|
||||
claudePermissions={claudePermissions}
|
||||
onClaudePermissionsChange={setClaudePermissions}
|
||||
cursorPermissions={cursorPermissions}
|
||||
onCursorPermissionsChange={setCursorPermissions}
|
||||
codexPermissionMode={codexPermissionMode}
|
||||
onCodexPermissionModeChange={setCodexPermissionMode}
|
||||
geminiPermissionMode={geminiPermissionMode}
|
||||
onGeminiPermissionModeChange={setGeminiPermissionMode}
|
||||
mcpServers={mcpServers}
|
||||
cursorMcpServers={cursorMcpServers}
|
||||
codexMcpServers={codexMcpServers}
|
||||
|
||||
@@ -12,7 +12,7 @@ type AgentListItemProps = {
|
||||
|
||||
type AgentConfig = {
|
||||
name: string;
|
||||
color: 'blue' | 'purple' | 'gray';
|
||||
color: 'blue' | 'purple' | 'gray' | 'indigo';
|
||||
};
|
||||
|
||||
const agentConfig: Record<AgentProvider, AgentConfig> = {
|
||||
@@ -28,6 +28,10 @@ const agentConfig: Record<AgentProvider, AgentConfig> = {
|
||||
name: 'Codex',
|
||||
color: 'gray',
|
||||
},
|
||||
gemini: {
|
||||
name: 'Gemini',
|
||||
color: 'indigo',
|
||||
}
|
||||
};
|
||||
|
||||
const colorClasses = {
|
||||
@@ -49,6 +53,12 @@ const colorClasses = {
|
||||
bg: 'bg-gray-100 dark:bg-gray-800/50',
|
||||
dot: 'bg-gray-700 dark:bg-gray-300',
|
||||
},
|
||||
indigo: {
|
||||
border: 'border-l-indigo-500 md:border-l-indigo-500',
|
||||
borderBottom: 'border-b-indigo-500',
|
||||
bg: 'bg-indigo-50 dark:bg-indigo-900/20',
|
||||
dot: 'bg-indigo-500',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default function AgentListItem({
|
||||
@@ -66,11 +76,10 @@ export default function AgentListItem({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex-1 text-center py-3 px-2 border-b-2 transition-colors ${
|
||||
isSelected
|
||||
? `${colors.borderBottom} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
className={`flex-1 text-center py-3 px-2 border-b-2 transition-colors ${isSelected
|
||||
? `${colors.borderBottom} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<SessionProviderLogo provider={agentId} className="w-5 h-5" />
|
||||
@@ -86,11 +95,10 @@ export default function AgentListItem({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full text-left p-3 border-l-4 transition-colors ${
|
||||
isSelected
|
||||
? `${colors.border} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
className={`w-full text-left p-3 border-l-4 transition-colors ${isSelected
|
||||
? `${colors.border} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<SessionProviderLogo provider={agentId} className="w-4 h-4" />
|
||||
|
||||
@@ -9,15 +9,19 @@ export default function AgentsSettingsTab({
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
onClaudeLogin,
|
||||
onCursorLogin,
|
||||
onCodexLogin,
|
||||
onGeminiLogin,
|
||||
claudePermissions,
|
||||
onClaudePermissionsChange,
|
||||
cursorPermissions,
|
||||
onCursorPermissionsChange,
|
||||
codexPermissionMode,
|
||||
onCodexPermissionModeChange,
|
||||
geminiPermissionMode,
|
||||
onGeminiPermissionModeChange,
|
||||
mcpServers,
|
||||
cursorMcpServers,
|
||||
codexMcpServers,
|
||||
@@ -48,13 +52,19 @@ export default function AgentsSettingsTab({
|
||||
authStatus: codexAuthStatus,
|
||||
onLogin: onCodexLogin,
|
||||
},
|
||||
gemini: {
|
||||
authStatus: geminiAuthStatus,
|
||||
onLogin: onGeminiLogin,
|
||||
},
|
||||
}), [
|
||||
claudeAuthStatus,
|
||||
codexAuthStatus,
|
||||
cursorAuthStatus,
|
||||
geminiAuthStatus,
|
||||
onClaudeLogin,
|
||||
onCodexLogin,
|
||||
onCursorLogin,
|
||||
onGeminiLogin,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -81,6 +91,8 @@ export default function AgentsSettingsTab({
|
||||
onCursorPermissionsChange={onCursorPermissionsChange}
|
||||
codexPermissionMode={codexPermissionMode}
|
||||
onCodexPermissionModeChange={onCodexPermissionModeChange}
|
||||
geminiPermissionMode={geminiPermissionMode}
|
||||
onGeminiPermissionModeChange={onGeminiPermissionModeChange}
|
||||
mcpServers={mcpServers}
|
||||
cursorMcpServers={cursorMcpServers}
|
||||
codexMcpServers={codexMcpServers}
|
||||
|
||||
@@ -18,6 +18,7 @@ type AgentVisualConfig = {
|
||||
textClass: string;
|
||||
subtextClass: string;
|
||||
buttonClass: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
@@ -45,6 +46,15 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
subtextClass: 'text-gray-700 dark:text-gray-300',
|
||||
buttonClass: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600',
|
||||
},
|
||||
gemini: {
|
||||
name: 'Gemini',
|
||||
description: 'Google Gemini AI assistant',
|
||||
bgClass: 'bg-indigo-50 dark:bg-indigo-900/20',
|
||||
borderClass: 'border-indigo-200 dark:border-indigo-800',
|
||||
textClass: 'text-indigo-900 dark:text-indigo-100',
|
||||
subtextClass: 'text-indigo-700 dark:text-indigo-300',
|
||||
buttonClass: 'bg-indigo-600 hover:bg-indigo-700',
|
||||
},
|
||||
};
|
||||
|
||||
export default function AccountContent({ agent, authStatus, onLogin }: AccountContentProps) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AlertTriangle, Plus, Shield, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../../../../../../ui/button';
|
||||
import { Input } from '../../../../../../ui/input';
|
||||
import type { CodexPermissionMode } from '../../../../../types/types';
|
||||
import type { CodexPermissionMode, GeminiPermissionMode } from '../../../../../types/types';
|
||||
|
||||
const COMMON_CLAUDE_TOOLS = [
|
||||
'Bash(git log:*)',
|
||||
@@ -489,11 +489,10 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
<p className="text-sm text-muted-foreground">{t('permissions.codex.description')}</p>
|
||||
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('default')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
@@ -514,11 +513,10 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
permissionMode === 'acceptEdits'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'acceptEdits'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('acceptEdits')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
@@ -539,11 +537,10 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
permissionMode === 'bypassPermissions'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'bypassPermissions'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('bypassPermissions')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
@@ -582,7 +579,111 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
);
|
||||
}
|
||||
|
||||
type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps;
|
||||
type GeminiPermissionsProps = {
|
||||
agent: 'gemini';
|
||||
permissionMode: GeminiPermissionMode;
|
||||
onPermissionModeChange: (value: GeminiPermissionMode) => void;
|
||||
};
|
||||
|
||||
// Gemini Permissions
|
||||
function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<GeminiPermissionsProps, 'agent'>) {
|
||||
const { t } = useTranslation(['settings', 'chat']);
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-5 h-5 text-green-500" />
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
{t('gemini.permissionMode')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('gemini.description')}
|
||||
</p>
|
||||
|
||||
{/* Default Mode */}
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('default')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="geminiPermissionMode"
|
||||
checked={permissionMode === 'default'}
|
||||
onChange={() => onPermissionModeChange('default')}
|
||||
className="mt-1 w-4 h-4 text-green-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{t('gemini.modes.default.title')}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('gemini.modes.default.description')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Auto Edit Mode */}
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'auto_edit'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('auto_edit')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="geminiPermissionMode"
|
||||
checked={permissionMode === 'auto_edit'}
|
||||
onChange={() => onPermissionModeChange('auto_edit')}
|
||||
className="mt-1 w-4 h-4 text-green-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-green-900 dark:text-green-100">{t('gemini.modes.autoEdit.title')}</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">
|
||||
{t('gemini.modes.autoEdit.description')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* YOLO Mode */}
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'yolo'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('yolo')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="geminiPermissionMode"
|
||||
checked={permissionMode === 'yolo'}
|
||||
onChange={() => onPermissionModeChange('yolo')}
|
||||
className="mt-1 w-4 h-4 text-orange-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100 flex items-center gap-2">
|
||||
{t('gemini.modes.yolo.title')}
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="text-sm text-orange-700 dark:text-orange-300">
|
||||
{t('gemini.modes.yolo.description')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps | GeminiPermissionsProps;
|
||||
|
||||
export default function PermissionsContent(props: PermissionsContentProps) {
|
||||
if (props.agent === 'claude') {
|
||||
@@ -593,5 +694,9 @@ export default function PermissionsContent(props: PermissionsContentProps) {
|
||||
return <CursorPermissions {...props} />;
|
||||
}
|
||||
|
||||
if (props.agent === 'gemini') {
|
||||
return <GeminiPermissions {...props} />;
|
||||
}
|
||||
|
||||
return <CodexPermissions {...props} />;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import type {
|
||||
AuthStatus,
|
||||
AgentCategory,
|
||||
ClaudePermissionsState,
|
||||
CodexPermissionMode,
|
||||
CursorPermissionsState,
|
||||
CodexPermissionMode,
|
||||
GeminiPermissionMode,
|
||||
McpServer,
|
||||
McpToolsResult,
|
||||
McpTestResult,
|
||||
@@ -21,15 +22,19 @@ export type AgentsSettingsTabProps = {
|
||||
claudeAuthStatus: AuthStatus;
|
||||
cursorAuthStatus: AuthStatus;
|
||||
codexAuthStatus: AuthStatus;
|
||||
geminiAuthStatus: AuthStatus;
|
||||
onClaudeLogin: () => void;
|
||||
onCursorLogin: () => void;
|
||||
onCodexLogin: () => void;
|
||||
onGeminiLogin: () => void;
|
||||
claudePermissions: ClaudePermissionsState;
|
||||
onClaudePermissionsChange: (value: ClaudePermissionsState) => void;
|
||||
cursorPermissions: CursorPermissionsState;
|
||||
onCursorPermissionsChange: (value: CursorPermissionsState) => void;
|
||||
codexPermissionMode: CodexPermissionMode;
|
||||
onCodexPermissionModeChange: (value: CodexPermissionMode) => void;
|
||||
geminiPermissionMode: GeminiPermissionMode;
|
||||
onGeminiPermissionModeChange: (value: GeminiPermissionMode) => void;
|
||||
mcpServers: McpServer[];
|
||||
cursorMcpServers: McpServer[];
|
||||
codexMcpServers: McpServer[];
|
||||
@@ -66,6 +71,8 @@ export type AgentCategoryContentSectionProps = {
|
||||
onCursorPermissionsChange: (value: CursorPermissionsState) => void;
|
||||
codexPermissionMode: CodexPermissionMode;
|
||||
onCodexPermissionModeChange: (value: CodexPermissionMode) => void;
|
||||
geminiPermissionMode: GeminiPermissionMode;
|
||||
onGeminiPermissionModeChange: (value: GeminiPermissionMode) => void;
|
||||
mcpServers: McpServer[];
|
||||
cursorMcpServers: McpServer[];
|
||||
codexMcpServers: McpServer[];
|
||||
|
||||
@@ -277,10 +277,14 @@ export function useSidebarController({
|
||||
setSessionDeleteConfirmation(null);
|
||||
|
||||
try {
|
||||
const response =
|
||||
provider === 'codex'
|
||||
? await api.deleteCodexSession(sessionId)
|
||||
: await api.deleteSession(projectName, sessionId);
|
||||
let response;
|
||||
if (provider === 'codex') {
|
||||
response = await api.deleteCodexSession(sessionId);
|
||||
} else if (provider === 'gemini') {
|
||||
response = await api.deleteGeminiSession(sessionId);
|
||||
} else {
|
||||
response = await api.deleteSession(projectName, sessionId);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
onSessionDelete?.(sessionId);
|
||||
|
||||
@@ -44,6 +44,7 @@ export type SidebarProps = {
|
||||
export type SessionViewModel = {
|
||||
isCursorSession: boolean;
|
||||
isCodexSession: boolean;
|
||||
isGeminiSession: boolean;
|
||||
isActive: boolean;
|
||||
sessionName: string;
|
||||
sessionTime: string;
|
||||
|
||||
@@ -48,7 +48,7 @@ export const getSessionDate = (session: SessionWithProvider): Date => {
|
||||
return new Date(session.createdAt || session.lastActivity || 0);
|
||||
}
|
||||
|
||||
return new Date(session.lastActivity || 0);
|
||||
return new Date(session.lastActivity || session.createdAt || 0);
|
||||
};
|
||||
|
||||
export const getSessionName = (session: SessionWithProvider, t: TFunction): string => {
|
||||
@@ -60,6 +60,10 @@ export const getSessionName = (session: SessionWithProvider, t: TFunction): stri
|
||||
return session.summary || session.name || t('projects.codexSession');
|
||||
}
|
||||
|
||||
if (session.__provider === 'gemini') {
|
||||
return session.summary || session.name || t('projects.newSession');
|
||||
}
|
||||
|
||||
return session.summary || t('projects.newSession');
|
||||
};
|
||||
|
||||
@@ -72,7 +76,7 @@ export const getSessionTime = (session: SessionWithProvider): string => {
|
||||
return String(session.createdAt || session.lastActivity || '');
|
||||
}
|
||||
|
||||
return String(session.lastActivity || '');
|
||||
return String(session.lastActivity || session.createdAt || '');
|
||||
};
|
||||
|
||||
export const createSessionViewModel = (
|
||||
@@ -86,6 +90,7 @@ export const createSessionViewModel = (
|
||||
return {
|
||||
isCursorSession: session.__provider === 'cursor',
|
||||
isCodexSession: session.__provider === 'codex',
|
||||
isGeminiSession: session.__provider === 'gemini',
|
||||
isActive: diffInMinutes < 10,
|
||||
sessionName: getSessionName(session, t),
|
||||
sessionTime: getSessionTime(session),
|
||||
@@ -112,7 +117,12 @@ export const getAllSessions = (
|
||||
__provider: 'codex' as const,
|
||||
}));
|
||||
|
||||
return [...claudeSessions, ...cursorSessions, ...codexSessions].sort(
|
||||
const geminiSessions = (project.geminiSessions || []).map((session) => ({
|
||||
...session,
|
||||
__provider: 'gemini' as const,
|
||||
}));
|
||||
|
||||
return [...claudeSessions, ...cursorSessions, ...codexSessions, ...geminiSessions].sort(
|
||||
(a, b) => getSessionDate(b).getTime() - getSessionDate(a).getTime(),
|
||||
);
|
||||
};
|
||||
@@ -205,8 +215,8 @@ export const normalizeProjectForSettings = (project: Project): SettingsProject =
|
||||
typeof project.fullPath === 'string' && project.fullPath.length > 0
|
||||
? project.fullPath
|
||||
: typeof project.path === 'string'
|
||||
? project.path
|
||||
: '';
|
||||
? project.path
|
||||
: '';
|
||||
|
||||
return {
|
||||
name: project.name,
|
||||
|
||||
@@ -40,7 +40,8 @@ const projectsHaveChanges = (
|
||||
nextProject.displayName !== prevProject.displayName ||
|
||||
nextProject.fullPath !== prevProject.fullPath ||
|
||||
serialize(nextProject.sessionMeta) !== serialize(prevProject.sessionMeta) ||
|
||||
serialize(nextProject.sessions) !== serialize(prevProject.sessions);
|
||||
serialize(nextProject.sessions) !== serialize(prevProject.sessions) ||
|
||||
serialize(nextProject.taskmaster) !== serialize(prevProject.taskmaster);
|
||||
|
||||
if (baseChanged) {
|
||||
return true;
|
||||
@@ -52,7 +53,8 @@ const projectsHaveChanges = (
|
||||
|
||||
return (
|
||||
serialize(nextProject.cursorSessions) !== serialize(prevProject.cursorSessions) ||
|
||||
serialize(nextProject.codexSessions) !== serialize(prevProject.codexSessions)
|
||||
serialize(nextProject.codexSessions) !== serialize(prevProject.codexSessions) ||
|
||||
serialize(nextProject.geminiSessions) !== serialize(prevProject.geminiSessions)
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -62,6 +64,7 @@ const getProjectSessions = (project: Project): ProjectSession[] => {
|
||||
...(project.sessions ?? []),
|
||||
...(project.codexSessions ?? []),
|
||||
...(project.cursorSessions ?? []),
|
||||
...(project.geminiSessions ?? []),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -333,6 +336,21 @@ export function useProjectsState({
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const geminiSession = project.geminiSessions?.find((session) => session.id === sessionId);
|
||||
if (geminiSession) {
|
||||
const shouldUpdateProject = selectedProject?.name !== project.name;
|
||||
const shouldUpdateSession =
|
||||
selectedSession?.id !== sessionId || selectedSession.__provider !== 'gemini';
|
||||
|
||||
if (shouldUpdateProject) {
|
||||
setSelectedProject(project);
|
||||
}
|
||||
if (shouldUpdateSession) {
|
||||
setSelectedSession({ ...geminiSession, __provider: 'gemini' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [sessionId, projects, selectedProject?.name, selectedSession?.id, selectedSession?.__provider]);
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"tool": "Tool",
|
||||
"claude": "Claude",
|
||||
"cursor": "Cursor",
|
||||
"codex": "Codex"
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini"
|
||||
},
|
||||
"tools": {
|
||||
"settings": "Tool Settings",
|
||||
@@ -93,6 +94,24 @@
|
||||
},
|
||||
"technicalDetails": "Technical details"
|
||||
},
|
||||
"gemini": {
|
||||
"permissionMode": "Gemini Permission Mode",
|
||||
"description": "Control how Gemini CLI handles operation approvals.",
|
||||
"modes": {
|
||||
"default": {
|
||||
"title": "Standard (Ask for Approval)",
|
||||
"description": "Gemini will prompt for approval before executing commands, writing files, and fetching web resources."
|
||||
},
|
||||
"autoEdit": {
|
||||
"title": "Auto Edit (Skip File Approvals)",
|
||||
"description": "Gemini will automatically approve file edits and web fetches, but will still prompt for shell commands."
|
||||
},
|
||||
"yolo": {
|
||||
"title": "YOLO (Bypass All Permissions)",
|
||||
"description": "Gemini will execute all operations without asking for approval. Exercise caution."
|
||||
}
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"placeholder": "Type / for commands, @ for files, or ask {{provider}} anything...",
|
||||
"placeholderDefault": "Type your message...",
|
||||
@@ -153,12 +172,14 @@
|
||||
"providerInfo": {
|
||||
"anthropic": "by Anthropic",
|
||||
"openai": "by OpenAI",
|
||||
"cursorEditor": "AI Code Editor"
|
||||
"cursorEditor": "AI Code Editor",
|
||||
"google": "by Google"
|
||||
},
|
||||
"readyPrompt": {
|
||||
"claude": "Ready to use Claude with {{model}}. Start typing your message below.",
|
||||
"cursor": "Ready to use Cursor with {{model}}. Start typing your message below.",
|
||||
"codex": "Ready to use Codex with {{model}}. Start typing your message below.",
|
||||
"gemini": "Ready to use Gemini with {{model}}. Start typing your message below.",
|
||||
"default": "Select a provider above to begin"
|
||||
}
|
||||
},
|
||||
@@ -214,4 +235,4 @@
|
||||
"tasks": {
|
||||
"nextTaskPrompt": "Start the next task"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
"tool": "도구",
|
||||
"claude": "Claude",
|
||||
"cursor": "Cursor",
|
||||
"codex": "Codex"
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini"
|
||||
},
|
||||
"tools": {
|
||||
"settings": "도구 설정",
|
||||
@@ -151,15 +152,17 @@
|
||||
"description": "새 대화를 시작할 프로바이더를 선택하세요",
|
||||
"selectModel": "모델 선택",
|
||||
"providerInfo": {
|
||||
"anthropic": "by Anthropic",
|
||||
"openai": "by OpenAI",
|
||||
"cursorEditor": "AI 코드 에디터"
|
||||
"anthropic": "Anthropic 제공",
|
||||
"openai": "OpenAI 제공",
|
||||
"cursorEditor": "AI 코드 에디터",
|
||||
"google": "Google 제공"
|
||||
},
|
||||
"readyPrompt": {
|
||||
"claude": "{{model}}로 Claude를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"cursor": "{{model}}로 Cursor를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"codex": "{{model}}로 Codex를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"default": "시작하려면 위에서 프로바이더를 선택하세요"
|
||||
"claude": "{{model}} 모델로 Claude를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"cursor": "{{model}} 모델로 Cursor를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"codex": "{{model}} 모델로 Codex를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"gemini": "{{model}} 모델로 Gemini를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
|
||||
"default": "시작하려면 위에서 제공자를 선택하세요"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
@@ -214,4 +217,4 @@
|
||||
"tasks": {
|
||||
"nextTaskPrompt": "다음 작업 시작"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
"tool": "工具",
|
||||
"claude": "Claude",
|
||||
"cursor": "Cursor",
|
||||
"codex": "Codex"
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini"
|
||||
},
|
||||
"tools": {
|
||||
"settings": "工具设置",
|
||||
@@ -151,15 +152,17 @@
|
||||
"description": "选择一个供应商以开始新对话",
|
||||
"selectModel": "选择模型",
|
||||
"providerInfo": {
|
||||
"anthropic": "Anthropic",
|
||||
"openai": "OpenAI",
|
||||
"cursorEditor": "AI 代码编辑器"
|
||||
"anthropic": "由 Anthropic 提供",
|
||||
"openai": "由 OpenAI 提供",
|
||||
"cursorEditor": "AI 代码编辑器",
|
||||
"google": "由 Google 提供"
|
||||
},
|
||||
"readyPrompt": {
|
||||
"claude": "已准备好使用 Claude {{model}}。在下方输入您的消息。",
|
||||
"cursor": "已准备好使用 Cursor {{model}}。在下方输入您的消息。",
|
||||
"codex": "已准备好使用 Codex {{model}}。在下方输入您的消息。",
|
||||
"default": "请在上方选择一个供应商以开始"
|
||||
"claude": "准备好使用带有 {{model}} 的 Claude。请在下方开始输入您的消息。",
|
||||
"cursor": "准备好使用带有 {{model}} 的 Cursor。请在下方开始输入您的消息。",
|
||||
"codex": "准备好使用带有 {{model}} 的 Codex。请在下方开始输入您的消息。",
|
||||
"gemini": "准备好使用带有 {{model}} 的 Gemini。请在下方开始输入您的消息。",
|
||||
"default": "请在上方选择一个提供者以开始"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
@@ -214,4 +217,4 @@
|
||||
"tasks": {
|
||||
"nextTaskPrompt": "开始下一个任务"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SessionProvider = 'claude' | 'cursor' | 'codex';
|
||||
export type SessionProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
|
||||
|
||||
export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'preview';
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface Project {
|
||||
sessions?: ProjectSession[];
|
||||
cursorSessions?: ProjectSession[];
|
||||
codexSessions?: ProjectSession[];
|
||||
geminiSessions?: ProjectSession[];
|
||||
sessionMeta?: ProjectSessionMeta;
|
||||
taskmaster?: ProjectTaskmasterInfo;
|
||||
[key: string]: unknown;
|
||||
@@ -66,4 +67,4 @@ export interface LoadingProgressMessage extends LoadingProgress {
|
||||
export type AppSocketMessage =
|
||||
| LoadingProgressMessage
|
||||
| ProjectsUpdatedMessage
|
||||
| { type?: string; [key: string]: unknown };
|
||||
| { type?: string;[key: string]: unknown };
|
||||
|
||||
@@ -46,7 +46,7 @@ export const api = {
|
||||
// Protected endpoints
|
||||
// config endpoint removed - no longer needed (frontend uses window.location)
|
||||
projects: () => authenticatedFetch('/api/projects'),
|
||||
sessions: (projectName, limit = 5, offset = 0) =>
|
||||
sessions: (projectName, limit = 5, offset = 0) =>
|
||||
authenticatedFetch(`/api/projects/${projectName}/sessions?limit=${limit}&offset=${offset}`),
|
||||
sessionMessages: (projectName, sessionId, limit = null, offset = 0, provider = 'claude') => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -56,12 +56,13 @@ export const api = {
|
||||
}
|
||||
const queryString = params.toString();
|
||||
|
||||
// Route to the correct endpoint based on provider
|
||||
let url;
|
||||
if (provider === 'codex') {
|
||||
url = `/api/codex/sessions/${sessionId}/messages${queryString ? `?${queryString}` : ''}`;
|
||||
} else if (provider === 'cursor') {
|
||||
url = `/api/cursor/sessions/${sessionId}/messages${queryString ? `?${queryString}` : ''}`;
|
||||
} else if (provider === 'gemini') {
|
||||
url = `/api/gemini/sessions/${sessionId}/messages${queryString ? `?${queryString}` : ''}`;
|
||||
} else {
|
||||
url = `/api/projects/${projectName}/sessions/${sessionId}/messages${queryString ? `?${queryString}` : ''}`;
|
||||
}
|
||||
@@ -80,6 +81,10 @@ export const api = {
|
||||
authenticatedFetch(`/api/codex/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
deleteGeminiSession: (sessionId) =>
|
||||
authenticatedFetch(`/api/gemini/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
deleteProject: (projectName, force = false) =>
|
||||
authenticatedFetch(`/api/projects/${projectName}${force ? '?force=true' : ''}`, {
|
||||
method: 'DELETE',
|
||||
@@ -113,18 +118,18 @@ export const api = {
|
||||
// TaskMaster endpoints
|
||||
taskmaster: {
|
||||
// Initialize TaskMaster in a project
|
||||
init: (projectName) =>
|
||||
init: (projectName) =>
|
||||
authenticatedFetch(`/api/taskmaster/init/${projectName}`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
|
||||
|
||||
// Add a new task
|
||||
addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
|
||||
authenticatedFetch(`/api/taskmaster/add-task/${projectName}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
|
||||
}),
|
||||
|
||||
|
||||
// Parse PRD to generate tasks
|
||||
parsePRD: (projectName, { fileName, numTasks, append }) =>
|
||||
authenticatedFetch(`/api/taskmaster/parse-prd/${projectName}`, {
|
||||
@@ -150,7 +155,7 @@ export const api = {
|
||||
body: JSON.stringify(updates),
|
||||
}),
|
||||
},
|
||||
|
||||
|
||||
// Browse filesystem for project suggestions
|
||||
browseFilesystem: (dirPath = null) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
Reference in New Issue
Block a user