mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-01-23 18:07:34 +00:00
Compare commits
17 Commits
fix/turn-o
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38745bdf85 | ||
|
|
9da7c1cbae | ||
|
|
844677caee | ||
|
|
e1c67fd5d0 | ||
|
|
9cd0cfc88f | ||
|
|
09f1021c59 | ||
|
|
053d94ab9d | ||
|
|
e85cc746b1 | ||
|
|
cc3368c591 | ||
|
|
5131d2ae27 | ||
|
|
394b95ae29 | ||
|
|
4948aa3d64 | ||
|
|
6e07f140e3 | ||
|
|
fea8e30725 | ||
|
|
9f534ce15b | ||
|
|
8cb34a73b5 | ||
|
|
74640a7f31 |
@@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
46
src/App.jsx
46
src/App.jsx
@@ -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 ${
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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: '.')
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/i18n/locales/en/codeEditor.json
Normal file
30
src/i18n/locales/en/codeEditor.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "命令"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/i18n/locales/zh-CN/codeEditor.json
Normal file
30
src/i18n/locales/zh-CN/codeEditor.json
Normal 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 关闭"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": "关闭侧边栏"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "智能体",
|
||||||
|
|||||||
@@ -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": "此操作无法撤销。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user