Compare commits

..

17 Commits

Author SHA1 Message Date
Haileyesus Dessie
38745bdf85 Merge pull request #327 from NobitaYuan/feat/add-i18n
update: Add translations for more components
2026-01-23 15:26:51 +03:00
Haileyesus Dessie
9da7c1cbae Merge pull request #314 from EricBlanquer/feature/delete-project-with-sessions
feat: allow deleting projects with sessions and add styled confirmation modal
2026-01-23 15:13:25 +03:00
YuanNiancai
844677caee Merge branch 'feat/add-i18n' of https://github.com/NobitaYuan/claudecodeui into feat/add-i18n 2026-01-23 09:27:14 +08:00
NobitaYuan
e1c67fd5d0 Merge branch 'main' into feat/add-i18n 2026-01-23 09:26:58 +08:00
YuanNiancai
9cd0cfc88f fix: add missing translation 2026-01-23 09:26:43 +08:00
viper151
09f1021c59 Merge pull request #332 from siteboon/fix/turn-on-extended-thinking-mode 2026-01-22 11:45:27 +01:00
viper151
053d94ab9d Merge pull request #331 from siteboon/fix/turn-on-extended-thinking-mode 2026-01-22 11:39:13 +01:00
YuanNiancai
e85cc746b1 fix: add missing translation 2026-01-22 15:24:27 +08:00
YuanNiancai
cc3368c591 add translations for Shell.jsx 2026-01-22 15:12:01 +08:00
YuanNiancai
5131d2ae27 add some translations for CodeEditor.jsx、QuickSettingsPanel.jsx 2026-01-22 14:57:32 +08:00
YuanNiancai
394b95ae29 add some translations for chatInterface.jsx 2026-01-22 14:38:24 +08:00
YuanNiancai
4948aa3d64 fix:Fix missing imports 2026-01-22 10:07:40 +08:00
NobitaYuan
6e07f140e3 Merge branch 'main' into feat/add-i18n 2026-01-22 09:56:11 +08:00
YuanNiancai
fea8e30725 update: Add translations for some components 2026-01-22 09:49:19 +08:00
Eric Blanquer​
9f534ce15b fix: use i18next v4+ pluralization format and add sessionTitle fallback 2026-01-21 23:14:41 +01:00
Eric Blanquer​
8cb34a73b5 fix: localize delete confirmation modal strings 2026-01-21 22:38:29 +01:00
Eric Blanquer​
74640a7f31 feat: allow deleting projects with sessions and add styled confirmation modal
- Add force delete option to delete projects with existing sessions
- Add styled confirmation modal with session count warning
- Add deletingProjects state to show loading indicator during deletion
- Delete associated Codex sessions when deleting a project (with limit: 0)
- Delete associated Cursor sessions directory when deleting a project
- Add fallback to extractProjectDirectory when projectPath undefined
- Use finally block for deletingProjects cleanup
- Add fallback name in delete modal
2026-01-21 22:05:00 +01:00
21 changed files with 592 additions and 140 deletions

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img src="public/logo.svg" alt="Claude Code UI" width="64" height="64"> <img src="public/logo.svg" alt="Claude Code UI" width="64" height="64">
<h1>Cloud CLI (aka Claude Code UI)</h1> <h1>Cloud CLI (又名 Claude Code UI)</h1>
</div> </div>

View File

@@ -455,11 +455,12 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
} }
}); });
// Delete project endpoint (only if empty) // Delete project endpoint (force=true to delete with sessions)
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => { app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
try { try {
const { projectName } = req.params; const { projectName } = req.params;
await deleteProject(projectName); const force = req.query.force === 'true';
await deleteProject(projectName, force);
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });

View File

@@ -1026,25 +1026,56 @@ async function isProjectEmpty(projectName) {
} }
} }
// Delete an empty project // Delete a project (force=true to delete even with sessions)
async function deleteProject(projectName) { async function deleteProject(projectName, force = false) {
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName); const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
try { try {
// First check if the project is empty
const isEmpty = await isProjectEmpty(projectName); const isEmpty = await isProjectEmpty(projectName);
if (!isEmpty) { if (!isEmpty && !force) {
throw new Error('Cannot delete project with existing sessions'); throw new Error('Cannot delete project with existing sessions');
} }
// Remove the project directory
await fs.rm(projectDir, { recursive: true, force: true });
// Remove from project config
const config = await loadProjectConfig(); const config = await loadProjectConfig();
let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
// Fallback to extractProjectDirectory if projectPath is not in config
if (!projectPath) {
projectPath = await extractProjectDirectory(projectName);
}
// Remove the project directory (includes all Claude sessions)
await fs.rm(projectDir, { recursive: true, force: true });
// Delete all Codex sessions associated with this project
if (projectPath) {
try {
const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
for (const session of codexSessions) {
try {
await deleteCodexSession(session.id);
} catch (err) {
console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
}
}
} catch (err) {
console.warn('Failed to delete Codex sessions:', err.message);
}
// Delete Cursor sessions directory if it exists
try {
const hash = crypto.createHash('md5').update(projectPath).digest('hex');
const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
await fs.rm(cursorProjectDir, { recursive: true, force: true });
} catch (err) {
// Cursor dir may not exist, ignore
}
}
// Remove from project config
delete config[projectName]; delete config[projectName];
await saveProjectConfig(config); await saveProjectConfig(config);
return true; return true;
} catch (error) { } catch (error) {
console.error(`Error deleting project ${projectName}:`, error); console.error(`Error deleting project ${projectName}:`, error);
@@ -1055,17 +1086,17 @@ async function deleteProject(projectName) {
// Add a project manually to the config (without creating folders) // Add a project manually to the config (without creating folders)
async function addProjectManually(projectPath, displayName = null) { async function addProjectManually(projectPath, displayName = null) {
const absolutePath = path.resolve(projectPath); const absolutePath = path.resolve(projectPath);
try { try {
// Check if the path exists // Check if the path exists
await fs.access(absolutePath); await fs.access(absolutePath);
} catch (error) { } catch (error) {
throw new Error(`Path does not exist: ${absolutePath}`); throw new Error(`Path does not exist: ${absolutePath}`);
} }
// Generate project name (encode path for use as directory name) // Generate project name (encode path for use as directory name)
const projectName = absolutePath.replace(/\//g, '-'); const projectName = absolutePath.replace(/\//g, '-');
// Check if project already exists in config // Check if project already exists in config
const config = await loadProjectConfig(); const config = await loadProjectConfig();
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName); const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
@@ -1076,13 +1107,13 @@ async function addProjectManually(projectPath, displayName = null) {
// Allow adding projects even if the directory exists - this enables tracking // Allow adding projects even if the directory exists - this enables tracking
// existing Claude Code or Cursor projects in the UI // existing Claude Code or Cursor projects in the UI
// Add to config as manually added project // Add to config as manually added project
config[projectName] = { config[projectName] = {
manuallyAdded: true, manuallyAdded: true,
originalPath: absolutePath originalPath: absolutePath
}; };
if (displayName) { if (displayName) {
config[projectName].displayName = displayName; config[projectName].displayName = displayName;
} }
@@ -1214,7 +1245,8 @@ async function getCursorSessions(projectPath) {
// Fetch Codex sessions for a given project path // Fetch Codex sessions for a given project path
async function getCodexSessions(projectPath) { async function getCodexSessions(projectPath, options = {}) {
const { limit = 5 } = options;
try { try {
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions'); const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
const sessions = []; const sessions = [];
@@ -1279,8 +1311,8 @@ async function getCodexSessions(projectPath) {
// Sort sessions by last activity (newest first) // Sort sessions by last activity (newest first)
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity)); sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
// Return only the first 5 sessions for performance // Return limited sessions for performance (0 = unlimited for deletion)
return sessions.slice(0, 5); return limit > 0 ? sessions.slice(0, limit) : sessions;
} catch (error) { } catch (error) {
console.error('Error fetching Codex sessions:', error); console.error('Error fetching Codex sessions:', error);

View File

@@ -36,7 +36,7 @@ import ProtectedRoute from './components/ProtectedRoute';
import { useVersionCheck } from './hooks/useVersionCheck'; import { useVersionCheck } from './hooks/useVersionCheck';
import useLocalStorage from './hooks/useLocalStorage'; import useLocalStorage from './hooks/useLocalStorage';
import { api, authenticatedFetch } from './utils/api'; import { api, authenticatedFetch } from './utils/api';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider, useTranslation } from 'react-i18next';
import i18n from './i18n/config.js'; import i18n from './i18n/config.js';
@@ -44,6 +44,7 @@ import i18n from './i18n/config.js';
function AppContent() { function AppContent() {
const navigate = useNavigate(); const navigate = useNavigate();
const { sessionId } = useParams(); const { sessionId } = useParams();
const { t } = useTranslation('common');
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
const [showVersionModal, setShowVersionModal] = useState(false); const [showVersionModal, setShowVersionModal] = useState(false);
@@ -579,6 +580,7 @@ function AppContent() {
// Version Upgrade Modal Component // Version Upgrade Modal Component
const VersionUpgradeModal = () => { const VersionUpgradeModal = () => {
const { t } = useTranslation('common');
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [updateOutput, setUpdateOutput] = useState(''); const [updateOutput, setUpdateOutput] = useState('');
const [updateError, setUpdateError] = useState(''); const [updateError, setUpdateError] = useState('');
@@ -639,7 +641,7 @@ function AppContent() {
<button <button
className="fixed inset-0 bg-black/50 backdrop-blur-sm" className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={() => setShowVersionModal(false)} onClick={() => setShowVersionModal(false)}
aria-label="Close version upgrade modal" aria-label={t('versionUpdate.ariaLabels.closeModal')}
/> />
{/* Modal */} {/* Modal */}
@@ -653,9 +655,9 @@ function AppContent() {
</svg> </svg>
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Update Available</h2> <h2 className="text-lg font-semibold text-gray-900 dark:text-white">{t('versionUpdate.title')}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{releaseInfo?.title || 'A new version is ready'} {releaseInfo?.title || t('versionUpdate.newVersionReady')}
</p> </p>
</div> </div>
</div> </div>
@@ -672,11 +674,11 @@ function AppContent() {
{/* Version Info */} {/* Version Info */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg"> <div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Current Version</span> <span className="text-sm font-medium text-gray-700 dark:text-gray-300">{t('versionUpdate.currentVersion')}</span>
<span className="text-sm text-gray-900 dark:text-white font-mono">{currentVersion}</span> <span className="text-sm text-gray-900 dark:text-white font-mono">{currentVersion}</span>
</div> </div>
<div className="flex justify-between items-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-700"> <div className="flex justify-between items-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-700">
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Latest Version</span> <span className="text-sm font-medium text-blue-700 dark:text-blue-300">{t('versionUpdate.latestVersion')}</span>
<span className="text-sm text-blue-900 dark:text-blue-100 font-mono">{latestVersion}</span> <span className="text-sm text-blue-900 dark:text-blue-100 font-mono">{latestVersion}</span>
</div> </div>
</div> </div>
@@ -685,7 +687,7 @@ function AppContent() {
{releaseInfo?.body && ( {releaseInfo?.body && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-white">What's New:</h3> <h3 className="text-sm font-medium text-gray-900 dark:text-white">{t('versionUpdate.whatsNew')}</h3>
{releaseInfo?.htmlUrl && ( {releaseInfo?.htmlUrl && (
<a <a
href={releaseInfo.htmlUrl} href={releaseInfo.htmlUrl}
@@ -693,7 +695,7 @@ function AppContent() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline flex items-center gap-1" className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline flex items-center gap-1"
> >
View full release {t('versionUpdate.viewFullRelease')}
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg> </svg>
@@ -711,7 +713,7 @@ function AppContent() {
{/* Update Output */} {/* Update Output */}
{updateOutput && ( {updateOutput && (
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Update Progress:</h3> <h3 className="text-sm font-medium text-gray-900 dark:text-white">{t('versionUpdate.updateProgress')}</h3>
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 border border-gray-700 max-h-48 overflow-y-auto"> <div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 border border-gray-700 max-h-48 overflow-y-auto">
<pre className="text-xs text-green-400 font-mono whitespace-pre-wrap">{updateOutput}</pre> <pre className="text-xs text-green-400 font-mono whitespace-pre-wrap">{updateOutput}</pre>
</div> </div>
@@ -721,14 +723,14 @@ function AppContent() {
{/* Upgrade Instructions */} {/* Upgrade Instructions */}
{!isUpdating && !updateOutput && ( {!isUpdating && !updateOutput && (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Manual upgrade:</h3> <h3 className="text-sm font-medium text-gray-900 dark:text-white">{t('versionUpdate.manualUpgrade')}</h3>
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border"> <div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono"> <code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
git checkout main && git pull && npm install git checkout main && git pull && npm install
</code> </code>
</div> </div>
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-600 dark:text-gray-400">
Or click "Update Now" to run the update automatically. {t('versionUpdate.manualUpgradeHint')}
</p> </p>
</div> </div>
)} )}
@@ -739,7 +741,7 @@ function AppContent() {
onClick={() => setShowVersionModal(false)} onClick={() => setShowVersionModal(false)}
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors" className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
> >
{updateOutput ? 'Close' : 'Later'} {updateOutput ? t('versionUpdate.buttons.close') : t('versionUpdate.buttons.later')}
</button> </button>
{!updateOutput && ( {!updateOutput && (
<> <>
@@ -749,7 +751,7 @@ function AppContent() {
}} }}
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors" className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
> >
Copy Command {t('versionUpdate.buttons.copyCommand')}
</button> </button>
<button <button
onClick={handleUpdateNow} onClick={handleUpdateNow}
@@ -759,10 +761,10 @@ function AppContent() {
{isUpdating ? ( {isUpdating ? (
<> <>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Updating... {t('versionUpdate.buttons.updating')}
</> </>
) : ( ) : (
'Update Now' t('versionUpdate.buttons.updateNow')
)} )}
</button> </button>
</> </>
@@ -813,8 +815,8 @@ function AppContent() {
<button <button
onClick={() => setSidebarVisible(true)} onClick={() => setSidebarVisible(true)}
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group" className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
aria-label="Show sidebar" aria-label={t('versionUpdate.ariaLabels.showSidebar')}
title="Show sidebar" title={t('versionUpdate.ariaLabels.showSidebar')}
> >
<svg <svg
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform" className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
@@ -830,8 +832,8 @@ function AppContent() {
<button <button
onClick={() => setShowSettings(true)} onClick={() => setShowSettings(true)}
className="p-2 hover:bg-accent rounded-md transition-colors duration-200" className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
aria-label="Settings" aria-label={t('versionUpdate.ariaLabels.settings')}
title="Settings" title={t('versionUpdate.ariaLabels.settings')}
> >
<SettingsIcon className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" /> <SettingsIcon className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
</button> </button>
@@ -841,8 +843,8 @@ function AppContent() {
<button <button
onClick={() => setShowVersionModal(true)} onClick={() => setShowVersionModal(true)}
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200" className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
aria-label="Update available" aria-label={t('versionUpdate.ariaLabels.updateAvailable')}
title="Update available" title={t('versionUpdate.ariaLabels.updateAvailable')}
> >
<Sparkles className="w-5 h-5 text-blue-500" /> <Sparkles className="w-5 h-5 text-blue-500" />
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" /> <span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
@@ -870,7 +872,7 @@ function AppContent() {
e.stopPropagation(); e.stopPropagation();
setSidebarOpen(false); setSidebarOpen(false);
}} }}
aria-label="Close sidebar" aria-label={t('versionUpdate.ariaLabels.closeSidebar')}
/> />
<div <div
className={`relative w-[85vw] max-w-sm sm:w-80 h-full bg-card border-r border-border transform transition-transform duration-150 ease-out ${ className={`relative w-[85vw] max-w-sm sm:w-80 h-full bg-card border-r border-border transform transition-transform duration-150 ease-out ${

View File

@@ -4789,16 +4789,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<div className="text-center text-gray-500 dark:text-gray-400 mt-8"> <div className="text-center text-gray-500 dark:text-gray-400 mt-8">
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400"></div>
<p>Loading session messages...</p> <p>{t('session.loading.sessionMessages')}</p>
</div> </div>
</div> </div>
) : chatMessages.length === 0 ? ( ) : chatMessages.length === 0 ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
{!selectedSession && !currentSessionId && ( {!selectedSession && !currentSessionId && (
<div className="text-center px-6 sm:px-4 py-8"> <div className="text-center px-6 sm:px-4 py-8">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">Choose Your AI Assistant</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">{t('providerSelection.title')}</h2>
<p className="text-gray-600 dark:text-gray-400 mb-8"> <p className="text-gray-600 dark:text-gray-400 mb-8">
Select a provider to start a new conversation {t('providerSelection.description')}
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8"> <div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8">
@@ -4820,7 +4820,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<ClaudeLogo className="w-10 h-10" /> <ClaudeLogo className="w-10 h-10" />
<div> <div>
<p className="font-semibold text-gray-900 dark:text-white">Claude</p> <p className="font-semibold text-gray-900 dark:text-white">Claude</p>
<p className="text-xs text-gray-500 dark:text-gray-400">by Anthropic</p> <p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.anthropic')}</p>
</div> </div>
</div> </div>
{provider === 'claude' && ( {provider === 'claude' && (
@@ -4852,7 +4852,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<CursorLogo className="w-10 h-10" /> <CursorLogo className="w-10 h-10" />
<div> <div>
<p className="font-semibold text-gray-900 dark:text-white">Cursor</p> <p className="font-semibold text-gray-900 dark:text-white">Cursor</p>
<p className="text-xs text-gray-500 dark:text-gray-400">AI Code Editor</p> <p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.cursorEditor')}</p>
</div> </div>
</div> </div>
{provider === 'cursor' && ( {provider === 'cursor' && (
@@ -4884,7 +4884,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<CodexLogo className="w-10 h-10" /> <CodexLogo className="w-10 h-10" />
<div> <div>
<p className="font-semibold text-gray-900 dark:text-white">Codex</p> <p className="font-semibold text-gray-900 dark:text-white">Codex</p>
<p className="text-xs text-gray-500 dark:text-gray-400">by OpenAI</p> <p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.openai')}</p>
</div> </div>
</div> </div>
{provider === 'codex' && ( {provider === 'codex' && (
@@ -4902,7 +4902,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
{/* Model Selection - Always reserve space to prevent jumping */} {/* Model Selection - Always reserve space to prevent jumping */}
<div className={`mb-6 transition-opacity duration-200 ${provider ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}> <div className={`mb-6 transition-opacity duration-200 ${provider ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Model {t('providerSelection.selectModel')}
</label> </label>
{provider === 'claude' ? ( {provider === 'claude' ? (
<select <select
@@ -4952,12 +4952,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
{provider === 'claude' {provider === 'claude'
? `Ready to use Claude with ${claudeModel}. Start typing your message below.` ? t('providerSelection.readyPrompt.claude', { model: claudeModel })
: provider === 'cursor' : provider === 'cursor'
? `Ready to use Cursor with ${cursorModel}. Start typing your message below.` ? t('providerSelection.readyPrompt.cursor', { model: cursorModel })
: provider === 'codex' : provider === 'codex'
? `Ready to use Codex with ${codexModel}. Start typing your message below.` ? t('providerSelection.readyPrompt.codex', { model: codexModel })
: 'Select a provider above to begin' : t('providerSelection.readyPrompt.default')
} }
</p> </p>
@@ -4974,9 +4974,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
)} )}
{selectedSession && ( {selectedSession && (
<div className="text-center text-gray-500 dark:text-gray-400 px-6 sm:px-4"> <div className="text-center text-gray-500 dark:text-gray-400 px-6 sm:px-4">
<p className="font-bold text-lg sm:text-xl mb-3">Continue your conversation</p> <p className="font-bold text-lg sm:text-xl mb-3">{t('session.continue.title')}</p>
<p className="text-sm sm:text-base leading-relaxed"> <p className="text-sm sm:text-base leading-relaxed">
Ask questions about your code, request changes, or get help with development tasks {t('session.continue.description')}
</p> </p>
{/* Show NextTaskBanner for existing sessions too, only if TaskMaster is installed */} {/* Show NextTaskBanner for existing sessions too, only if TaskMaster is installed */}
@@ -4998,7 +4998,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<div className="text-center text-gray-500 dark:text-gray-400 py-3"> <div className="text-center text-gray-500 dark:text-gray-400 py-3">
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400"></div>
<p className="text-sm">Loading older messages...</p> <p className="text-sm">{t('session.loading.olderMessages')}</p>
</div> </div>
</div> </div>
)} )}
@@ -5008,8 +5008,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700"> <div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
{totalMessages > 0 && ( {totalMessages > 0 && (
<span> <span>
Showing {sessionMessages.length} of {totalMessages} messages {t('session.messages.showingOf', { shown: sessionMessages.length, total: totalMessages })}
<span className="text-xs">Scroll up to load more</span> <span className="text-xs">{t('session.messages.scrollToLoad')}</span>
</span> </span>
)} )}
</div> </div>
@@ -5018,12 +5018,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
{/* Legacy message count indicator (for non-paginated view) */} {/* Legacy message count indicator (for non-paginated view) */}
{!hasMoreMessages && chatMessages.length > visibleMessageCount && ( {!hasMoreMessages && chatMessages.length > visibleMessageCount && (
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700"> <div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
Showing last {visibleMessageCount} messages ({chatMessages.length} total) {t('session.messages.showingLast', { count: visibleMessageCount, total: chatMessages.length })}
<button <button
className="ml-1 text-blue-600 hover:text-blue-700 underline" className="ml-1 text-blue-600 hover:text-blue-700 underline"
onClick={loadEarlierMessages} onClick={loadEarlierMessages}
> >
Load earlier messages {t('session.messages.loadEarlier')}
</button> </button>
</div> </div>
)} )}

View File

@@ -12,8 +12,10 @@ import { unifiedMergeView, getChunks } from '@codemirror/merge';
import { showMinimap } from '@replit/codemirror-minimap'; import { showMinimap } from '@replit/codemirror-minimap';
import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react'; import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { useTranslation } from 'react-i18next';
function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) { function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) {
const { t } = useTranslation('codeEditor');
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -125,13 +127,13 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
toolbarHTML += '<div style="display: flex; align-items: center; gap: 8px;">'; toolbarHTML += '<div style="display: flex; align-items: center; gap: 8px;">';
if (hasDiff) { if (hasDiff) {
toolbarHTML += ` toolbarHTML += `
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span> <span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} ${t('toolbar.changes')}</span>
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}> <button class="cm-diff-nav-btn cm-diff-nav-prev" title="${t('toolbar.previousChange')}" ${chunkCount === 0 ? 'disabled' : ''}>
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg> </svg>
</button> </button>
<button class="cm-diff-nav-btn cm-diff-nav-next" title="Next change" ${chunkCount === 0 ? 'disabled' : ''}> <button class="cm-diff-nav-btn cm-diff-nav-next" title="${t('toolbar.nextChange')}" ${chunkCount === 0 ? 'disabled' : ''}>
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
@@ -146,7 +148,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
// Show/hide diff button (only if there's diff info) // Show/hide diff button (only if there's diff info)
if (file.diffInfo) { if (file.diffInfo) {
toolbarHTML += ` toolbarHTML += `
<button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? 'Hide diff highlighting' : 'Show diff highlighting'}"> <button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? t('toolbar.hideDiff') : t('toolbar.showDiff')}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${showDiff ? ${showDiff ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />' : '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />' :
@@ -159,7 +161,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
// Settings button // Settings button
toolbarHTML += ` toolbarHTML += `
<button class="cm-toolbar-btn cm-settings-btn" title="Editor Settings"> <button class="cm-toolbar-btn cm-settings-btn" title="${t('toolbar.settings')}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
@@ -169,7 +171,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
// Expand button (only in sidebar mode) // Expand button (only in sidebar mode)
if (isSidebar && onToggleExpand) { if (isSidebar && onToggleExpand) {
toolbarHTML += ` toolbarHTML += `
<button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? 'Collapse editor' : 'Expand editor to full width'}"> <button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? t('toolbar.collapse') : t('toolbar.expand')}">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${isExpanded ? ${isExpanded ?
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />' : '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />' :
@@ -463,7 +465,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<div className="w-full h-full flex items-center justify-center bg-background"> <div className="w-full h-full flex items-center justify-center bg-background">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span> <span className="text-gray-900 dark:text-white">{t('loading', { fileName: file.name })}</span>
</div> </div>
</div> </div>
) : ( ) : (
@@ -471,7 +473,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center"> <div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span> <span className="text-gray-900 dark:text-white">{t('loading', { fileName: file.name })}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -574,7 +576,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3> <h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
{file.diffInfo && ( {file.diffInfo && (
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap"> <span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap">
Showing changes {t('header.showingChanges')}
</span> </span>
)} )}
</div> </div>
@@ -586,7 +588,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<button <button
onClick={handleDownload} onClick={handleDownload}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center" className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title="Download file" title={t('actions.download')}
> >
<Download className="w-5 h-5 md:w-4 md:h-4" /> <Download className="w-5 h-5 md:w-4 md:h-4" />
</button> </button>
@@ -605,12 +607,12 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<svg className="w-5 h-5 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="hidden sm:inline">Saved!</span> <span className="hidden sm:inline">{t('actions.saved')}</span>
</> </>
) : ( ) : (
<> <>
<Save className="w-5 h-5 md:w-4 md:h-4" /> <Save className="w-5 h-5 md:w-4 md:h-4" />
<span className="hidden sm:inline">{saving ? 'Saving...' : 'Save'}</span> <span className="hidden sm:inline">{saving ? t('actions.saving') : t('actions.save')}</span>
</> </>
)} )}
</button> </button>
@@ -619,7 +621,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<button <button
onClick={toggleFullscreen} onClick={toggleFullscreen}
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center" className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} title={isFullscreen ? t('actions.exitFullscreen') : t('actions.fullscreen')}
> >
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />} {isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button> </button>
@@ -628,7 +630,7 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
<button <button
onClick={onClose} onClick={onClose}
className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center" className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
title="Close" title={t('actions.close')}
> >
<X className="w-6 h-6 md:w-4 md:h-4" /> <X className="w-6 h-6 md:w-4 md:h-4" />
</button> </button>
@@ -686,12 +688,12 @@ function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-between p-3 border-t border-border bg-muted flex-shrink-0"> <div className="flex items-center justify-between p-3 border-t border-border bg-muted flex-shrink-0">
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<span>Lines: {content.split('\n').length}</span> <span>{t('footer.lines')} {content.split('\n').length}</span>
<span>Characters: {content.length}</span> <span>{t('footer.characters')} {content.length}</span>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
Press Ctrl+S to save Esc to close {t('footer.shortcuts')}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -233,8 +233,8 @@ const QuickSettingsPanel = ({
isDragging ? 'cursor-grabbing' : 'cursor-pointer' isDragging ? 'cursor-grabbing' : 'cursor-pointer'
} touch-none`} } touch-none`}
style={{ ...getPositionStyle(), touchAction: 'none', WebkitTouchCallout: 'none', WebkitUserSelect: 'none' }} style={{ ...getPositionStyle(), touchAction: 'none', WebkitTouchCallout: 'none', WebkitUserSelect: 'none' }}
aria-label={isDragging ? 'Dragging handle' : localIsOpen ? 'Close settings panel' : 'Open settings panel'} aria-label={isDragging ? t('quickSettings.dragHandle.dragging') : localIsOpen ? t('quickSettings.dragHandle.closePanel') : t('quickSettings.dragHandle.openPanel')}
title={isDragging ? 'Dragging...' : 'Click to toggle, drag to move'} title={isDragging ? t('quickSettings.dragHandle.draggingStatus') : t('quickSettings.dragHandle.toggleAndMove')}
> >
{isDragging ? ( {isDragging ? (
<GripVertical className="h-5 w-5 text-blue-500 dark:text-blue-400" /> <GripVertical className="h-5 w-5 text-blue-500 dark:text-blue-400" />
@@ -383,10 +383,10 @@ const QuickSettingsPanel = ({
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white"> <span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white">
<Mic className="h-4 w-4 text-gray-600 dark:text-gray-400" /> <Mic className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Default Mode {t('quickSettings.whisper.modes.default')}
</span> </span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Direct transcription of your speech {t('quickSettings.whisper.modes.defaultDescription')}
</p> </p>
</div> </div>
</label> </label>
@@ -407,10 +407,10 @@ const QuickSettingsPanel = ({
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white"> <span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white">
<Sparkles className="h-4 w-4 text-gray-600 dark:text-gray-400" /> <Sparkles className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Prompt Enhancement {t('quickSettings.whisper.modes.prompt')}
</span> </span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Transform rough ideas into clear, detailed AI prompts {t('quickSettings.whisper.modes.promptDescription')}
</p> </p>
</div> </div>
</label> </label>
@@ -431,10 +431,10 @@ const QuickSettingsPanel = ({
<div className="ml-3 flex-1"> <div className="ml-3 flex-1">
<span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white"> <span className="flex items-center gap-2 text-sm font-medium text-gray-900 dark:text-white">
<FileText className="h-4 w-4 text-gray-600 dark:text-gray-400" /> <FileText className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Vibe Mode {t('quickSettings.whisper.modes.vibe')}
</span> </span>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Format ideas as clear agent instructions with details {t('quickSettings.whisper.modes.vibeDescription')}
</p> </p>
</div> </div>
</label> </label>

View File

@@ -4,6 +4,7 @@ import { FitAddon } from '@xterm/addon-fit';
import { WebglAddon } from '@xterm/addon-webgl'; import { WebglAddon } from '@xterm/addon-webgl';
import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css'; import '@xterm/xterm/css/xterm.css';
import { useTranslation } from 'react-i18next';
const xtermStyles = ` const xtermStyles = `
.xterm .xterm-screen { .xterm .xterm-screen {
@@ -25,6 +26,7 @@ if (typeof document !== 'undefined') {
} }
function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell = false, onProcessComplete, minimal = false, autoConnect = false }) { function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell = false, onProcessComplete, minimal = false, autoConnect = false }) {
const { t } = useTranslation('chat');
const terminalRef = useRef(null); const terminalRef = useRef(null);
const terminal = useRef(null); const terminal = useRef(null);
const fitAddon = useRef(null); const fitAddon = useRef(null);
@@ -373,8 +375,8 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<h3 className="text-lg font-semibold mb-2">Select a Project</h3> <h3 className="text-lg font-semibold mb-2">{t('shell.selectProject.title')}</h3>
<p>Choose a project to open an interactive shell in that directory</p> <p>{t('shell.selectProject.description')}</p>
</div> </div>
</div> </div>
); );
@@ -400,13 +402,13 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
</span> </span>
)} )}
{!selectedSession && ( {!selectedSession && (
<span className="text-xs text-gray-400">(New Session)</span> <span className="text-xs text-gray-400">{t('shell.status.newSession')}</span>
)} )}
{!isInitialized && ( {!isInitialized && (
<span className="text-xs text-yellow-400">(Initializing...)</span> <span className="text-xs text-yellow-400">{t('shell.status.initializing')}</span>
)} )}
{isRestarting && ( {isRestarting && (
<span className="text-xs text-blue-400">(Restarting...)</span> <span className="text-xs text-blue-400">{t('shell.status.restarting')}</span>
)} )}
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
@@ -414,12 +416,12 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
<button <button
onClick={disconnectFromShell} onClick={disconnectFromShell}
className="px-3 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700 flex items-center space-x-1" className="px-3 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700 flex items-center space-x-1"
title="Disconnect from shell" title={t('shell.actions.disconnectTitle')}
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
<span>Disconnect</span> <span>{t('shell.actions.disconnect')}</span>
</button> </button>
)} )}
@@ -427,12 +429,12 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
onClick={restartShell} onClick={restartShell}
disabled={isRestarting || isConnected} disabled={isRestarting || isConnected}
className="text-xs text-gray-400 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1" className="text-xs text-gray-400 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
title="Restart Shell (disconnect first)" title={t('shell.actions.restartTitle')}
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
<span>Restart</span> <span>{t('shell.actions.restart')}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -443,7 +445,7 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
{!isInitialized && ( {!isInitialized && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90"> <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
<div className="text-white">Loading terminal...</div> <div className="text-white">{t('shell.loading')}</div>
</div> </div>
)} )}
@@ -453,19 +455,19 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
<button <button
onClick={connectToShell} onClick={connectToShell}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center space-x-2 text-base font-medium w-full sm:w-auto" className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center space-x-2 text-base font-medium w-full sm:w-auto"
title="Connect to shell" title={t('shell.actions.connectTitle')}
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<span>Continue in Shell</span> <span>{t('shell.actions.connect')}</span>
</button> </button>
<p className="text-gray-400 text-sm mt-3 px-2"> <p className="text-gray-400 text-sm mt-3 px-2">
{isPlainShell ? {isPlainShell ?
`Run ${initialCommand || 'command'} in ${selectedProject.displayName}` : t('shell.runCommand', { command: initialCommand || t('shell.defaultCommand'), projectName: selectedProject.displayName }) :
selectedSession ? selectedSession ?
`Resume session: ${sessionDisplayNameLong}...` : t('shell.resumeSession', { displayName: sessionDisplayNameLong }) :
'Start a new Claude session' t('shell.startSession')
} }
</p> </p>
</div> </div>
@@ -477,12 +479,12 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
<div className="text-center max-w-sm w-full"> <div className="text-center max-w-sm w-full">
<div className="flex items-center justify-center space-x-3 text-yellow-400"> <div className="flex items-center justify-center space-x-3 text-yellow-400">
<div className="w-6 h-6 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div> <div className="w-6 h-6 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div>
<span className="text-base font-medium">Connecting to shell...</span> <span className="text-base font-medium">{t('shell.connecting')}</span>
</div> </div>
<p className="text-gray-400 text-sm mt-3 px-2"> <p className="text-gray-400 text-sm mt-3 px-2">
{isPlainShell ? {isPlainShell ?
`Running ${initialCommand || 'command'} in ${selectedProject.displayName}` : t('shell.runCommand', { command: initialCommand || t('shell.defaultCommand'), projectName: selectedProject.displayName }) :
`Starting Claude CLI in ${selectedProject.displayName}` t('shell.startCli', { projectName: selectedProject.displayName })
} }
</p> </p>
</div> </div>

View File

@@ -6,7 +6,7 @@ import { Badge } from './ui/badge';
import { Input } from './ui/input'; import { Input } from './ui/input';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react'; import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search, AlertTriangle } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import ClaudeLogo from './ClaudeLogo'; import ClaudeLogo from './ClaudeLogo';
import CursorLogo from './CursorLogo.jsx'; import CursorLogo from './CursorLogo.jsx';
@@ -80,6 +80,9 @@ function Sidebar({
const [editingSessionName, setEditingSessionName] = useState(''); const [editingSessionName, setEditingSessionName] = useState('');
const [generatingSummary, setGeneratingSummary] = useState({}); const [generatingSummary, setGeneratingSummary] = useState({});
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const [deletingProjects, setDeletingProjects] = useState(new Set());
const [deleteConfirmation, setDeleteConfirmation] = useState(null); // { project, sessionCount }
const [sessionDeleteConfirmation, setSessionDeleteConfirmation] = useState(null); // { projectName, sessionId, sessionTitle, provider }
// TaskMaster context // TaskMaster context
const { setCurrentProject, mcpServerStatus } = useTaskMaster(); const { setCurrentProject, mcpServerStatus } = useTaskMaster();
@@ -306,10 +309,15 @@ function Sidebar({
setEditingName(''); setEditingName('');
}; };
const deleteSession = async (projectName, sessionId, provider = 'claude') => { const showDeleteSessionConfirmation = (projectName, sessionId, sessionTitle, provider = 'claude') => {
if (!confirm(t('messages.deleteSessionConfirm'))) { setSessionDeleteConfirmation({ projectName, sessionId, sessionTitle, provider });
return; };
}
const confirmDeleteSession = async () => {
if (!sessionDeleteConfirmation) return;
const { projectName, sessionId, provider } = sessionDeleteConfirmation;
setSessionDeleteConfirmation(null);
try { try {
console.log('[Sidebar] Deleting session:', { projectName, sessionId, provider }); console.log('[Sidebar] Deleting session:', { projectName, sessionId, provider });
@@ -343,18 +351,26 @@ function Sidebar({
} }
}; };
const deleteProject = async (projectName) => { const deleteProject = (project) => {
if (!confirm(t('messages.deleteProjectConfirm'))) { const sessionCount = getAllSessions(project).length;
return; setDeleteConfirmation({ project, sessionCount });
} };
const confirmDeleteProject = async () => {
if (!deleteConfirmation) return;
const { project, sessionCount } = deleteConfirmation;
const isEmpty = sessionCount === 0;
setDeleteConfirmation(null);
setDeletingProjects(prev => new Set([...prev, project.name]));
try { try {
const response = await api.deleteProject(projectName); const response = await api.deleteProject(project.name, !isEmpty);
if (response.ok) { if (response.ok) {
// Call parent callback if provided
if (onProjectDelete) { if (onProjectDelete) {
onProjectDelete(projectName); onProjectDelete(project.name);
} }
} else { } else {
const error = await response.json(); const error = await response.json();
@@ -364,6 +380,12 @@ function Sidebar({
} catch (error) { } catch (error) {
console.error('Error deleting project:', error); console.error('Error deleting project:', error);
alert(t('messages.deleteProjectError')); alert(t('messages.deleteProjectError'));
} finally {
setDeletingProjects(prev => {
const next = new Set(prev);
next.delete(project.name);
return next;
});
} }
}; };
@@ -488,6 +510,110 @@ function Sidebar({
document.body document.body
)} )}
{/* Delete Confirmation Modal */}
{deleteConfirmation && ReactDOM.createPortal(
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-2">
{t('deleteConfirmation.deleteProject')}
</h3>
<p className="text-sm text-muted-foreground mb-1">
{t('deleteConfirmation.confirmDelete')}{' '}
<span className="font-medium text-foreground">
{deleteConfirmation.project.displayName || deleteConfirmation.project.name}
</span>?
</p>
{deleteConfirmation.sessionCount > 0 && (
<div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-700 dark:text-red-300 font-medium">
{t('deleteConfirmation.sessionCount', { count: deleteConfirmation.sessionCount })}
</p>
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
{t('deleteConfirmation.allConversationsDeleted')}
</p>
</div>
)}
<p className="text-xs text-muted-foreground mt-3">
{t('deleteConfirmation.cannotUndo')}
</p>
</div>
</div>
</div>
<div className="flex gap-3 p-4 bg-muted/30 border-t border-border">
<Button
variant="outline"
className="flex-1"
onClick={() => setDeleteConfirmation(null)}
>
{t('actions.cancel')}
</Button>
<Button
variant="destructive"
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
onClick={confirmDeleteProject}
>
<Trash2 className="w-4 h-4 mr-2" />
{t('actions.delete')}
</Button>
</div>
</div>
</div>,
document.body
)}
{/* Session Delete Confirmation Modal */}
{sessionDeleteConfirmation && ReactDOM.createPortal(
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
<div className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-2">
{t('deleteConfirmation.deleteSession')}
</h3>
<p className="text-sm text-muted-foreground mb-1">
{t('deleteConfirmation.confirmDelete')}{' '}
<span className="font-medium text-foreground">
{sessionDeleteConfirmation.sessionTitle || t('sessions.unnamed')}
</span>?
</p>
<p className="text-xs text-muted-foreground mt-3">
{t('deleteConfirmation.cannotUndo')}
</p>
</div>
</div>
</div>
<div className="flex gap-3 p-4 bg-muted/30 border-t border-border">
<Button
variant="outline"
className="flex-1"
onClick={() => setSessionDeleteConfirmation(null)}
>
{t('actions.cancel')}
</Button>
<Button
variant="destructive"
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
onClick={confirmDeleteSession}
>
<Trash2 className="w-4 h-4 mr-2" />
{t('actions.delete')}
</Button>
</div>
</div>
</div>,
document.body
)}
<div <div
className="h-full flex flex-col bg-card md:select-none" className="h-full flex flex-col bg-card md:select-none"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}} style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
@@ -669,7 +795,7 @@ function Sidebar({
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t('projects.fetchingProjects')} {t('projects.fetchingProjects')}
</p> </p>
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">Loading projects...</h3> <h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('projects.loadingProjects')}</h3>
{loadingProgress && loadingProgress.total > 0 ? ( {loadingProgress && loadingProgress.total > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="w-full bg-muted rounded-full h-2 overflow-hidden"> <div className="w-full bg-muted rounded-full h-2 overflow-hidden">
@@ -679,7 +805,7 @@ function Sidebar({
/> />
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{loadingProgress.current}/{loadingProgress.total} projects {loadingProgress.current}/{loadingProgress.total} {t('projects.projects')}
</p> </p>
{loadingProgress.currentProject && ( {loadingProgress.currentProject && (
<p className="text-xs text-muted-foreground/70 truncate max-w-[200px] mx-auto" title={loadingProgress.currentProject}> <p className="text-xs text-muted-foreground/70 truncate max-w-[200px] mx-auto" title={loadingProgress.currentProject}>
@@ -689,7 +815,7 @@ function Sidebar({
</div> </div>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Fetching your Claude projects and sessions {t('projects.fetchingProjects')}
</p> </p>
)} )}
</div> </div>
@@ -718,9 +844,10 @@ function Sidebar({
const isExpanded = expandedProjects.has(project.name); const isExpanded = expandedProjects.has(project.name);
const isSelected = selectedProject?.name === project.name; const isSelected = selectedProject?.name === project.name;
const isStarred = isProjectStarred(project.name); const isStarred = isProjectStarred(project.name);
const isDeleting = deletingProjects.has(project.name);
return ( return (
<div key={project.name} className="md:space-y-1"> <div key={project.name} className={cn("md:space-y-1", isDeleting && "opacity-50 pointer-events-none")}>
{/* Project Header */} {/* Project Header */}
<div className="group md:group"> <div className="group md:group">
{/* Mobile Project Item */} {/* Mobile Project Item */}
@@ -849,18 +976,16 @@ function Sidebar({
: "text-gray-600 dark:text-gray-400" : "text-gray-600 dark:text-gray-400"
)} /> )} />
</button> </button>
{getAllSessions(project).length === 0 && ( <button
<button
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800" className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
deleteProject(project.name); deleteProject(project);
}} }}
onTouchEnd={handleTouchClick(() => deleteProject(project.name))} onTouchEnd={handleTouchClick(() => deleteProject(project))}
> >
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" /> <Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
</button> </button>
)}
<button <button
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30" className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30"
onClick={(e) => { onClick={(e) => {
@@ -1009,18 +1134,16 @@ function Sidebar({
> >
<Edit3 className="w-3 h-3" /> <Edit3 className="w-3 h-3" />
</div> </div>
{getAllSessions(project).length === 0 && ( <div
<div
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center justify-center rounded cursor-pointer touch:opacity-100" className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center justify-center rounded cursor-pointer touch:opacity-100"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
deleteProject(project.name); deleteProject(project);
}} }}
title={t('tooltips.deleteProject')} title={t('tooltips.deleteProject')}
> >
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" /> <Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
</div> </div>
)}
{isExpanded ? ( {isExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" /> <ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
) : ( ) : (
@@ -1152,9 +1275,9 @@ function Sidebar({
className="w-5 h-5 rounded-md bg-red-50 dark:bg-red-900/20 flex items-center justify-center active:scale-95 transition-transform opacity-70 ml-1" className="w-5 h-5 rounded-md bg-red-50 dark:bg-red-900/20 flex items-center justify-center active:scale-95 transition-transform opacity-70 ml-1"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
deleteSession(project.name, session.id, session.__provider); showDeleteSessionConfirmation(project.name, session.id, sessionName, session.__provider);
}} }}
onTouchEnd={handleTouchClick(() => deleteSession(project.name, session.id, session.__provider))} onTouchEnd={handleTouchClick(() => showDeleteSessionConfirmation(project.name, session.id, sessionName, session.__provider))}
> >
<Trash2 className="w-2.5 h-2.5 text-red-600 dark:text-red-400" /> <Trash2 className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
</button> </button>
@@ -1271,7 +1394,7 @@ function Sidebar({
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center" className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
deleteSession(project.name, session.id, session.__provider); showDeleteSessionConfirmation(project.name, session.id, sessionName, session.__provider);
}} }}
title={t('tooltips.deleteSession')} title={t('tooltips.deleteSession')}
> >

View File

@@ -19,12 +19,14 @@ import enSettings from './locales/en/settings.json';
import enAuth from './locales/en/auth.json'; import enAuth from './locales/en/auth.json';
import enSidebar from './locales/en/sidebar.json'; import enSidebar from './locales/en/sidebar.json';
import enChat from './locales/en/chat.json'; import enChat from './locales/en/chat.json';
import enCodeEditor from './locales/en/codeEditor.json';
import zhCommon from './locales/zh-CN/common.json'; import zhCommon from './locales/zh-CN/common.json';
import zhSettings from './locales/zh-CN/settings.json'; import zhSettings from './locales/zh-CN/settings.json';
import zhAuth from './locales/zh-CN/auth.json'; import zhAuth from './locales/zh-CN/auth.json';
import zhSidebar from './locales/zh-CN/sidebar.json'; import zhSidebar from './locales/zh-CN/sidebar.json';
import zhChat from './locales/zh-CN/chat.json'; import zhChat from './locales/zh-CN/chat.json';
import zhCodeEditor from './locales/zh-CN/codeEditor.json';
// Import supported languages configuration // Import supported languages configuration
import { languages } from './languages.js'; import { languages } from './languages.js';
@@ -56,6 +58,7 @@ i18n
auth: enAuth, auth: enAuth,
sidebar: enSidebar, sidebar: enSidebar,
chat: enChat, chat: enChat,
codeEditor: enCodeEditor,
}, },
'zh-CN': { 'zh-CN': {
common: zhCommon, common: zhCommon,
@@ -63,6 +66,7 @@ i18n
auth: zhAuth, auth: zhAuth,
sidebar: zhSidebar, sidebar: zhSidebar,
chat: zhChat, chat: zhChat,
codeEditor: zhCodeEditor,
}, },
}, },
@@ -76,7 +80,7 @@ i18n
debug: import.meta.env.DEV, debug: import.meta.env.DEV,
// Namespaces - load only what's needed // Namespaces - load only what's needed
ns: ['common', 'settings', 'auth', 'sidebar', 'chat'], ns: ['common', 'settings', 'auth', 'sidebar', 'chat', 'codeEditor'],
defaultNS: 'common', defaultNS: 'common',
// Key separator for nested keys (default: '.') // Key separator for nested keys (default: '.')

View File

@@ -143,5 +143,63 @@
} }
}, },
"buttonTitle": "Thinking mode: {{mode}}" "buttonTitle": "Thinking mode: {{mode}}"
},
"providerSelection": {
"title": "Choose Your AI Assistant",
"description": "Select a provider to start a new conversation",
"selectModel": "Select Model",
"providerInfo": {
"anthropic": "by Anthropic",
"openai": "by OpenAI",
"cursorEditor": "AI Code Editor"
},
"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.",
"default": "Select a provider above to begin"
}
},
"session": {
"continue": {
"title": "Continue your conversation",
"description": "Ask questions about your code, request changes, or get help with development tasks"
},
"loading": {
"olderMessages": "Loading older messages...",
"sessionMessages": "Loading session messages..."
},
"messages": {
"showingOf": "Showing {{shown}} of {{total}} messages",
"scrollToLoad": "Scroll up to load more",
"showingLast": "Showing last {{count}} messages ({{total}} total)",
"loadEarlier": "Load earlier messages"
}
},
"shell": {
"selectProject": {
"title": "Select a Project",
"description": "Choose a project to open an interactive shell in that directory"
},
"status": {
"newSession": "New Session",
"initializing": "Initializing...",
"restarting": "Restarting..."
},
"actions": {
"disconnect": "Disconnect",
"disconnectTitle": "Disconnect from shell",
"restart": "Restart",
"restartTitle": "Restart Shell (disconnect first)",
"connect": "Continue in Shell",
"connectTitle": "Connect to shell"
},
"loading": "Loading terminal...",
"connecting": "Connecting to shell...",
"startSession": "Start a new Claude session",
"resumeSession": "Resume session: {{displayName}}...",
"runCommand": "Run {{command}} in {{projectName}}",
"startCli": "Starting Claude CLI in {{projectName}}",
"defaultCommand": "command"
} }
} }

View File

@@ -0,0 +1,30 @@
{
"toolbar": {
"changes": "changes",
"previousChange": "Previous change",
"nextChange": "Next change",
"hideDiff": "Hide diff highlighting",
"showDiff": "Show diff highlighting",
"settings": "Editor Settings",
"collapse": "Collapse editor",
"expand": "Expand editor to full width"
},
"loading": "Loading {{fileName}}...",
"header": {
"showingChanges": "Showing changes"
},
"actions": {
"download": "Download file",
"save": "Save",
"saving": "Saving...",
"saved": "Saved!",
"exitFullscreen": "Exit fullscreen",
"fullscreen": "Fullscreen",
"close": "Close"
},
"footer": {
"lines": "Lines:",
"characters": "Characters:",
"shortcuts": "Press Ctrl+S to save • Esc to close"
}
}

View File

@@ -186,5 +186,33 @@
"providePath": "Please provide a workspace path", "providePath": "Please provide a workspace path",
"failedToCreate": "Failed to create workspace" "failedToCreate": "Failed to create workspace"
} }
},
"versionUpdate": {
"title": "Update Available",
"newVersionReady": "A new version is ready",
"currentVersion": "Current Version",
"latestVersion": "Latest Version",
"whatsNew": "What's New:",
"viewFullRelease": "View full release",
"updateProgress": "Update Progress:",
"manualUpgrade": "Manual upgrade:",
"manualUpgradeHint": "Or click \"Update Now\" to run the update automatically.",
"updateCompleted": "Update completed successfully!",
"restartServer": "Please restart the server to apply changes.",
"updateFailed": "Update failed",
"buttons": {
"close": "Close",
"later": "Later",
"copyCommand": "Copy Command",
"updateNow": "Update Now",
"updating": "Updating..."
},
"ariaLabels": {
"closeModal": "Close version upgrade modal",
"showSidebar": "Show sidebar",
"settings": "Settings",
"updateAvailable": "Update available",
"closeSidebar": "Close sidebar"
}
} }
} }

View File

@@ -64,7 +64,24 @@
"showThinking": "Show thinking", "showThinking": "Show thinking",
"autoScrollToBottom": "Auto-scroll to bottom", "autoScrollToBottom": "Auto-scroll to bottom",
"sendByCtrlEnter": "Send by Ctrl+Enter", "sendByCtrlEnter": "Send by Ctrl+Enter",
"sendByCtrlEnterDescription": "When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends." "sendByCtrlEnterDescription": "When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends.",
"dragHandle": {
"dragging": "Dragging handle",
"closePanel": "Close settings panel",
"openPanel": "Open settings panel",
"draggingStatus": "Dragging...",
"toggleAndMove": "Click to toggle, drag to move"
},
"whisper": {
"modes": {
"default": "Default Mode",
"defaultDescription": "Direct transcription of your speech",
"prompt": "Prompt Enhancement",
"promptDescription": "Transform rough ideas into clear, detailed AI prompts",
"vibe": "Vibe Mode",
"vibeDescription": "Format ideas as clear agent instructions with details"
}
}
}, },
"mainTabs": { "mainTabs": {
"agents": "Agents", "agents": "Agents",

View File

@@ -14,6 +14,7 @@
"newSession": "New Session", "newSession": "New Session",
"codexSession": "Codex Session", "codexSession": "Codex Session",
"fetchingProjects": "Fetching your Claude projects and sessions", "fetchingProjects": "Fetching your Claude projects and sessions",
"projects": "projects",
"noMatchingProjects": "No matching projects", "noMatchingProjects": "No matching projects",
"tryDifferentSearch": "Try adjusting your search term", "tryDifferentSearch": "Try adjusting your search term",
"runClaudeCli": "Run Claude CLI in a project directory to get started" "runClaudeCli": "Run Claude CLI in a project directory to get started"
@@ -98,5 +99,14 @@
}, },
"version": { "version": {
"updateAvailable": "Update available" "updateAvailable": "Update available"
},
"deleteConfirmation": {
"deleteProject": "Delete Project",
"deleteSession": "Delete Session",
"confirmDelete": "Are you sure you want to delete",
"sessionCount_one": "This project contains {{count}} conversation.",
"sessionCount_other": "This project contains {{count}} conversations.",
"allConversationsDeleted": "All conversations will be permanently deleted.",
"cannotUndo": "This action cannot be undone."
} }
} }

View File

@@ -143,5 +143,63 @@
} }
}, },
"buttonTitle": "思考模式:{{mode}}" "buttonTitle": "思考模式:{{mode}}"
},
"providerSelection": {
"title": "选择您的 AI 助手",
"description": "选择一个供应商以开始新对话",
"selectModel": "选择模型",
"providerInfo": {
"anthropic": "Anthropic",
"openai": "OpenAI",
"cursorEditor": "AI 代码编辑器"
},
"readyPrompt": {
"claude": "已准备好使用 Claude {{model}}。在下方输入您的消息。",
"cursor": "已准备好使用 Cursor {{model}}。在下方输入您的消息。",
"codex": "已准备好使用 Codex {{model}}。在下方输入您的消息。",
"default": "请在上方选择一个供应商以开始"
}
},
"session": {
"continue": {
"title": "继续您的对话",
"description": "询问有关代码的问题、请求更改或获取开发任务的帮助"
},
"loading": {
"olderMessages": "正在加载更早的消息...",
"sessionMessages": "正在加载会话消息..."
},
"messages": {
"showingOf": "显示 {{shown}} / {{total}} 条消息",
"scrollToLoad": "向上滚动以加载更多",
"showingLast": "显示最近 {{count}} 条消息(共 {{total}} 条)",
"loadEarlier": "加载更早的消息"
}
},
"shell": {
"selectProject": {
"title": "选择项目",
"description": "选择一个项目以在该目录中打开交互式 Shell"
},
"status": {
"newSession": "新会话",
"initializing": "初始化中...",
"restarting": "重启中..."
},
"actions": {
"disconnect": "断开连接",
"disconnectTitle": "断开 Shell 连接",
"restart": "重启",
"restartTitle": "重启 Shell请先断开连接",
"connect": "在 Shell 中继续",
"connectTitle": "连接到 Shell"
},
"loading": "正在加载终端...",
"connecting": "正在连接到 Shell...",
"startSession": "启动新的 Claude 会话",
"resumeSession": "恢复会话:{{displayName}}...",
"runCommand": "在 {{projectName}} 中运行 {{command}}",
"startCli": "在 {{projectName}} 中启动 Claude CLI",
"defaultCommand": "命令"
} }
} }

View File

@@ -0,0 +1,30 @@
{
"toolbar": {
"changes": "个更改",
"previousChange": "上一个更改",
"nextChange": "下一个更改",
"hideDiff": "隐藏差异高亮",
"showDiff": "显示差异高亮",
"settings": "编辑器设置",
"collapse": "折叠编辑器",
"expand": "展开编辑器到全宽"
},
"loading": "正在加载 {{fileName}}...",
"header": {
"showingChanges": "显示更改"
},
"actions": {
"download": "下载文件",
"save": "保存",
"saving": "保存中...",
"saved": "已保存!",
"exitFullscreen": "退出全屏",
"fullscreen": "全屏",
"close": "关闭"
},
"footer": {
"lines": "行数:",
"characters": "字符数:",
"shortcuts": "按 Ctrl+S 保存 • Esc 关闭"
}
}

View File

@@ -186,5 +186,33 @@
"providePath": "请提供工作区路径", "providePath": "请提供工作区路径",
"failedToCreate": "创建工作区失败" "failedToCreate": "创建工作区失败"
} }
},
"versionUpdate": {
"title": "有可用更新",
"newVersionReady": "新版本已准备就绪",
"currentVersion": "当前版本",
"latestVersion": "最新版本",
"whatsNew": "新内容:",
"viewFullRelease": "查看完整发布",
"updateProgress": "更新进度:",
"manualUpgrade": "手动升级:",
"manualUpgradeHint": "或点击'立即更新'以自动运行更新。",
"updateCompleted": "更新成功完成!",
"restartServer": "请重启服务器以应用更改。",
"updateFailed": "更新失败",
"buttons": {
"close": "关闭",
"later": "稍后",
"copyCommand": "复制命令",
"updateNow": "立即更新",
"updating": "更新中..."
},
"ariaLabels": {
"closeModal": "关闭版本升级模态框",
"showSidebar": "显示侧边栏",
"settings": "设置",
"updateAvailable": "有可用更新",
"closeSidebar": "关闭侧边栏"
}
} }
} }

View File

@@ -64,7 +64,24 @@
"showThinking": "显示思考过程", "showThinking": "显示思考过程",
"autoScrollToBottom": "自动滚动到底部", "autoScrollToBottom": "自动滚动到底部",
"sendByCtrlEnter": "使用 Ctrl+Enter 发送", "sendByCtrlEnter": "使用 Ctrl+Enter 发送",
"sendByCtrlEnterDescription": "启用后,按 Ctrl+Enter 发送消息,而不是仅按 Enter。这对于使用输入法的用户可以避免意外发送。" "sendByCtrlEnterDescription": "启用后,按 Ctrl+Enter 发送消息,而不是仅按 Enter。这对于使用输入法的用户可以避免意外发送。",
"dragHandle": {
"dragging": "正在拖拽手柄",
"closePanel": "关闭设置面板",
"openPanel": "打开设置面板",
"draggingStatus": "正在拖拽...",
"toggleAndMove": "点击切换,拖拽移动"
},
"whisper": {
"modes": {
"default": "默认模式",
"defaultDescription": "直接转录您的语音",
"prompt": "提示词增强",
"promptDescription": "将粗略的想法转化为清晰、详细的 AI 提示词",
"vibe": "Vibe 模式",
"vibeDescription": "将想法格式化为带有详细说明的清晰智能体指令"
}
}
}, },
"mainTabs": { "mainTabs": {
"agents": "智能体", "agents": "智能体",

View File

@@ -14,6 +14,7 @@
"newSession": "新会话", "newSession": "新会话",
"codexSession": "Codex 会话", "codexSession": "Codex 会话",
"fetchingProjects": "正在获取您的 Claude 项目和会话", "fetchingProjects": "正在获取您的 Claude 项目和会话",
"projects": "项目",
"noMatchingProjects": "未找到匹配的项目", "noMatchingProjects": "未找到匹配的项目",
"tryDifferentSearch": "尝试调整您的搜索词", "tryDifferentSearch": "尝试调整您的搜索词",
"runClaudeCli": "在项目目录中运行 Claude CLI 以开始使用" "runClaudeCli": "在项目目录中运行 Claude CLI 以开始使用"
@@ -98,5 +99,14 @@
}, },
"version": { "version": {
"updateAvailable": "有可用更新" "updateAvailable": "有可用更新"
},
"deleteConfirmation": {
"deleteProject": "删除项目",
"deleteSession": "删除会话",
"confirmDelete": "您确定要删除",
"sessionCount_one": "此项目包含 {{count}} 个对话。",
"sessionCount_other": "此项目包含 {{count}} 个对话。",
"allConversationsDeleted": "所有对话将被永久删除。",
"cannotUndo": "此操作无法撤销。"
} }
} }

View File

@@ -79,8 +79,8 @@ export const api = {
authenticatedFetch(`/api/codex/sessions/${sessionId}`, { authenticatedFetch(`/api/codex/sessions/${sessionId}`, {
method: 'DELETE', method: 'DELETE',
}), }),
deleteProject: (projectName) => deleteProject: (projectName, force = false) =>
authenticatedFetch(`/api/projects/${projectName}`, { authenticatedFetch(`/api/projects/${projectName}${force ? '?force=true' : ''}`, {
method: 'DELETE', method: 'DELETE',
}), }),
createProject: (path) => createProject: (path) =>