diff --git a/.gitignore b/.gitignore index 34be0c9bca..625a1059f3 100755 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,4 @@ tasks/ cloudcli-sidebar-app-source.tar.gz cloudcli-sidebar.html electron/*.tar.gz +MERGE-CONFLICTS.md diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 280ff73075..4ff4a7094b 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -251,6 +251,7 @@ function runCommand(command: string, args: string[]): Promise { shell: false, stdio: ['ignore', 'pipe', 'pipe'], }); + console.info('[Browser] Running:', command, args.join(' '), '| cwd:', process.cwd()); const output: string[] = []; let settled = false; const finish = (fn: () => void) => { @@ -279,7 +280,9 @@ function runCommand(command: string, args: string[]): Promise { return; } - reject(new Error(output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`)); + const errorMsg = output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`; + console.error('[Browser] Command failed:', errorMsg.slice(0, 500)); + reject(new Error(errorMsg)); })); }); } diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index b5b5f2a972..4b64a5c8d9 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Bot, Clock3, @@ -65,24 +66,24 @@ async function readJson(response: Response): Promise { return data as T; } -function formatRelativeTime(value: string | null): string { - if (!value) return 'Never'; +function formatRelativeTime(value: string | null, t: (key: string, options?: Record) => string): string { + if (!value) return t('browserPanel.never'); const timestamp = Date.parse(value); - if (!Number.isFinite(timestamp)) return 'Unknown'; + if (!Number.isFinite(timestamp)) return t('browserPanel.unknown'); const elapsedSeconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000)); - if (elapsedSeconds < 10) return 'Just now'; - if (elapsedSeconds < 60) return `${elapsedSeconds}s ago`; + if (elapsedSeconds < 10) return t('browserPanel.justNow'); + if (elapsedSeconds < 60) return `${elapsedSeconds}s`; const elapsedMinutes = Math.round(elapsedSeconds / 60); - if (elapsedMinutes < 60) return `${elapsedMinutes}m ago`; + if (elapsedMinutes < 60) return `${elapsedMinutes}m`; const elapsedHours = Math.round(elapsedMinutes / 60); - if (elapsedHours < 24) return `${elapsedHours}h ago`; - return `${Math.round(elapsedHours / 24)}d ago`; + if (elapsedHours < 24) return `${elapsedHours}h`; + return `${Math.round(elapsedHours / 24)}d`; } -function getDomain(url: string | null): string { - if (!url) return 'No page loaded'; +function getDomain(url: string | null, t: (key: string) => string): string { + if (!url) return t('browserPanel.noPageLoaded'); try { return new URL(url).hostname; @@ -91,8 +92,8 @@ function getDomain(url: string | null): string { } } -function formatAction(action: string | null): string { - if (!action) return 'Waiting'; +function formatAction(action: string | null, t: (key: string) => string): string { + if (!action) return t('browserPanel.waiting'); return action.replace(/_/g, ' ').replace(/:/g, ': '); } @@ -119,12 +120,8 @@ function getStatusDot(status: BrowserUseSession['status']): string { return 'bg-border'; } -const PROMPTS = [ - 'Use Browser to inspect the checkout flow and report any broken UI states.', - 'Open with Browser, interact with the page, and summarize what changed after each step.', -]; - export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUsePanelProps) { + const { t } = useTranslation('common'); const [status, setStatus] = useState(null); const [sessions, setSessions] = useState([]); const [selectedSessionId, setSelectedSessionId] = useState(null); @@ -142,12 +139,12 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs const activeSessions = sessions.filter((session) => session.status === 'ready'); const needsBrowserBinaries = Boolean(status?.enabled && (!status.playwrightInstalled || !status.chromiumInstalled)); const runtimeLabel = !status?.enabled - ? 'Disabled' + ? t('browserPanel.runtimeDisabled') : status.available - ? 'Ready' + ? t('browserPanel.runtimeReady') : status.installInProgress || isInstalling - ? 'Installing' - : 'Setup required'; + ? t('browserPanel.runtimeInstalling') + : t('browserPanel.runtimeSetupRequired'); const cursorStyle = selectedSession?.cursor && selectedSession.viewport ? { @@ -175,11 +172,11 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs )); setError(null); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load Browser'); + setError(err instanceof Error ? err.message : t('browserPanel.failedToLoad')); } finally { setIsRefreshing(false); } - }, []); + }, [t]); useEffect(() => { if (!isVisible) return; @@ -193,11 +190,11 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs await action(); await refresh(); } catch (err) { - setError(err instanceof Error ? err.message : 'Browser action failed'); + setError(err instanceof Error ? err.message : t('browserPanel.actionFailed')); } finally { setIsBusy(false); } - }, [refresh]); + }, [refresh, t]); const stopSession = () => runAction(async () => { if (!selectedSession) return; @@ -240,9 +237,9 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
-
{session.title || getDomain(session.url)}
+
{session.title || getDomain(session.url, t)}
-
{getDomain(session.url)}
+
{getDomain(session.url, t)}
{session.status} @@ -250,8 +247,8 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
- {formatRelativeTime(session.updatedAt)} - - {formatAction(session.lastAction)} + {formatRelativeTime(session.updatedAt, t)} + - {formatAction(session.lastAction, t)}
); @@ -266,19 +263,19 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
- {status?.enabled ? 'No browser sessions yet' : 'Browser is disabled'} + {status?.enabled ? t('browserPanel.noSessionsYet') : t('browserPanel.browserDisabled')}

{status?.enabled - ? 'Agent browser sessions appear here while an AI task is using Browser.' - : 'Enable Browser in settings to let agents open monitored browser sessions.'} + ? t('browserPanel.sessionsWillAppear') + : t('browserPanel.enableInSettings')}

{needsBrowserBinaries && (
-
Runtime setup required
+
{t('browserPanel.runtimeSetupRequired')}

{status?.message}

)}
- {PROMPTS.map((prompt) => ( + {[t('browserPanel.promptExample1'), t('browserPanel.promptExample2')].map((prompt) => (
- Prompt + {t('browserPanel.prompt')}

{prompt}

@@ -318,7 +315,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
Browser session screenshot {cursorStyle && ( @@ -333,8 +330,8 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs ) : (
-
{selectedSession?.message || 'Waiting for screenshot'}
-

The next agent browser snapshot will render here.

+
{selectedSession?.message || t('browserPanel.waitingForScreenshot')}
+

{t('browserPanel.screenshotWillAppear')}

)}
@@ -346,12 +343,12 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
-

Browser

+

{t('browserPanel.browserTitle')}

{runtimeLabel}
-

Monitor browser sessions opened by AI agents.

+

{t('browserPanel.monitorDescription')}

{onShowSettings && ( @@ -360,8 +357,8 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs size="sm" className="h-7 w-7 p-0" onClick={() => onShowSettings('browser')} - title="Open Browser settings" - aria-label="Open Browser settings" + title={t('browserPanel.openSettings')} + aria-label={t('browserPanel.openSettings')} > @@ -372,8 +369,8 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs className="h-7 w-7 p-0" onClick={() => void refresh()} disabled={isRefreshing || isBusy} - title="Refresh browser sessions" - aria-label="Refresh browser sessions" + title={t('browserPanel.refreshSessions')} + aria-label={t('browserPanel.refreshSessions')} > @@ -403,7 +400,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs > - {session.title || getDomain(session.url)} + {session.title || getDomain(session.url, t)} ))} @@ -415,12 +412,12 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
- {activeSessions.length} active + {activeSessions.length} {t('browserPanel.active')} / - {sessions.length} total + {sessions.length} {t('browserPanel.total')}
- Updated {formatRelativeTime(selectedSession?.updatedAt || null)} + {t('browserPanel.updated')} {formatRelativeTime(selectedSession?.updatedAt || null, t)}
@@ -431,27 +428,27 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
- {selectedSession?.status || 'empty'} + {selectedSession?.status || t('browserPanel.empty')}
- {selectedSession?.title || getDomain(selectedSession?.url || null)} + {selectedSession?.title || getDomain(selectedSession?.url || null, t)}
- {selectedSession?.url || 'No page loaded'} + {selectedSession?.url || t('browserPanel.noPageLoaded')}
- {formatAction(selectedSession?.lastAction || null)} + {formatAction(selectedSession?.lastAction || null, t)}
- - -
@@ -465,10 +462,10 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
-
Sessions
-
{sessions.length} total
+
{t('browserPanel.sessions')}
+
{sessions.length} {t('browserPanel.total')}
- {activeSessions.length} active + {activeSessions.length} {t('browserPanel.active')}
@@ -477,7 +474,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
{sessions.map(renderSessionItem)}
) : (
- No agent browser sessions. + {t('browserPanel.noAgentSessions')}
)}
@@ -486,30 +483,30 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
- Selected + {t('browserPanel.selected')}
- Status - {selectedSession?.status || 'None'} + {t('browserPanel.status')} + {selectedSession?.status || t('browserPanel.none')}
- Last action - {formatAction(selectedSession?.lastAction || null)} + {t('browserPanel.lastAction')} + {formatAction(selectedSession?.lastAction || null, t)}
- Profile - {selectedSession?.profileName || 'Temporary'} + {t('browserPanel.profile')} + {selectedSession?.profileName || t('browserPanel.temporary')}
@@ -521,10 +518,10 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
-
{selectedSession.title || selectedSession.url || 'Browser session'}
+
{selectedSession.title || selectedSession.url || t('browserPanel.browserSession')}
{renderBrowserSurface(true)} diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx index 6d97ca886d..7334796391 100644 --- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx +++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx @@ -230,9 +230,9 @@ export default function ProviderSelectionEmptyState({ - Model Selector + {t("providerSelection.modelSelector", { defaultValue: "Model Selector" })}
-

Choose a model

+

{t("providerSelection.chooseAmodel", { defaultValue: "Choose a model" })}

(null); @@ -129,17 +131,17 @@ export default function GitPanelHeader({ {remoteStatus?.hasRemote && ( {aheadCount > 0 && ( - + ↑{aheadCount} )} {behindCount > 0 && ( - + ↓{behindCount} )} {remoteStatus.isUpToDate && ( - + )} )} @@ -174,7 +176,7 @@ export default function GitPanelHeader({ className="flex w-full items-center space-x-2 px-4 py-2 text-left text-sm transition-colors hover:bg-accent" > - Create new branch + {t('gitPanel.header.createNewBranch')}
@@ -193,7 +195,7 @@ export default function GitPanelHeader({ title={`Publish "${currentBranch}" to ${remoteName}`} > - {!isMobile && {isPublishing ? 'Publishing…' : 'Publish'}} + {!isMobile && {isPublishing ? t('gitPanel.header.publishing') : t('gitPanel.header.publish')}} ) : ( <> @@ -205,7 +207,7 @@ export default function GitPanelHeader({ title={`Fetch from ${remoteName}`} > - {!isMobile && {isFetching ? 'Fetching…' : 'Fetch'}} + {!isMobile && {isFetching ? t('gitPanel.header.fetching') : t('gitPanel.header.fetch')}} {behindCount > 0 && ( @@ -216,7 +218,7 @@ export default function GitPanelHeader({ title={`Pull ${behindCount} from ${remoteName}`} > - {!isMobile && {isPulling ? 'Pulling…' : `Pull ${behindCount}`}} + {!isMobile && {isPulling ? t('gitPanel.header.pulling') : `${t('gitPanel.header.pull')} ${behindCount}`}} )} @@ -228,7 +230,7 @@ export default function GitPanelHeader({ title={`Push ${aheadCount} to ${remoteName}`} > - {!isMobile && {isPushing ? 'Pushing…' : `Push ${aheadCount}`}} + {!isMobile && {isPushing ? t('gitPanel.header.pushing') : `${t('gitPanel.header.push')} ${aheadCount}`}} )} @@ -240,7 +242,7 @@ export default function GitPanelHeader({ onClick={requestRevertLocalCommitConfirmation} disabled={isRevertingLocalCommit} className={`rounded-lg transition-colors hover:bg-accent disabled:opacity-50 ${isMobile ? 'p-1' : 'p-1.5'}`} - title="Revert latest local commit" + title={t('gitPanel.header.revertLatestCommit')} > diff --git a/src/components/git-panel/view/GitViewTabs.tsx b/src/components/git-panel/view/GitViewTabs.tsx index 1c0aaba6ab..250a4af961 100644 --- a/src/components/git-panel/view/GitViewTabs.tsx +++ b/src/components/git-panel/view/GitViewTabs.tsx @@ -1,4 +1,5 @@ import { FileText, GitBranch, History } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import type { GitPanelView } from '../types/types'; type GitViewTabsProps = { @@ -8,13 +9,15 @@ type GitViewTabsProps = { onChange: (view: GitPanelView) => void; }; -const TABS: { id: GitPanelView; label: string; Icon: typeof FileText }[] = [ - { id: 'changes', label: 'Changes', Icon: FileText }, - { id: 'history', label: 'Commits', Icon: History }, - { id: 'branches', label: 'Branches', Icon: GitBranch }, -]; - export default function GitViewTabs({ activeView, isHidden, changeCount, onChange }: GitViewTabsProps) { + const { t } = useTranslation('common'); + + const TABS: { id: GitPanelView; label: string; Icon: typeof FileText }[] = [ + { id: 'changes', label: t('gitPanel.tabs.changes'), Icon: FileText }, + { id: 'history', label: t('gitPanel.tabs.commits'), Icon: History }, + { id: 'branches', label: t('gitPanel.tabs.branches'), Icon: GitBranch }, + ]; + return (
{isCurrent && ( - current + {t('gitPanel.branches.current')} )} {isRemote && !isCurrent && ( - remote + {t('gitPanel.branches.remote')} )}
@@ -71,10 +73,10 @@ function BranchRow({ name, isCurrent, isRemote, aheadCount, behindCount, isMobil {isCurrent && (aheadCount > 0 || behindCount > 0) && (
{aheadCount > 0 && ( - ↑{aheadCount} ahead + ↑{aheadCount} {t('gitPanel.branches.ahead')} )} {behindCount > 0 && ( - ↓{behindCount} behind + ↓{behindCount} {t('gitPanel.branches.behind')} )}
)} @@ -91,7 +93,7 @@ function BranchRow({ name, isCurrent, isRemote, aheadCount, behindCount, isMobil className="rounded-md px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground" title={`Switch to ${name}`} > - Switch + {t('gitPanel.branches.switch')}
); @@ -137,6 +139,7 @@ export default function BranchesView({ onDeleteBranch, onRequestConfirmation, }: BranchesViewProps) { + const { t } = useTranslation('common'); const [showNewBranchModal, setShowNewBranchModal] = useState(false); const aheadCount = remoteStatus?.ahead ?? 0; @@ -166,19 +169,22 @@ export default function BranchesView({ ); } + const remoteSuffix = remoteBranches.length > 0 ? `, ${remoteBranches.length} ${t('gitPanel.branches.remote')}` : ''; + const branchCountText = `${localBranches.length} ${t('gitPanel.branches.local')}${remoteSuffix}`; + return (
{/* Create branch button */}
- {localBranches.length} local{remoteBranches.length > 0 ? `, ${remoteBranches.length} remote` : ''} + {branchCountText}
@@ -186,7 +192,7 @@ export default function BranchesView({
{localBranches.length > 0 && ( <> - + {localBranches.map((branch) => ( 0 && ( <> - + {remoteBranches.map((branch) => ( -

No branches found

+

{t('gitPanel.branches.noBranchesFound')}

)}
diff --git a/src/components/git-panel/view/changes/ChangesView.tsx b/src/components/git-panel/view/changes/ChangesView.tsx index ddc21e959d..b9659a1fba 100644 --- a/src/components/git-panel/view/changes/ChangesView.tsx +++ b/src/components/git-panel/view/changes/ChangesView.tsx @@ -1,4 +1,5 @@ import { GitBranch, GitCommit, RefreshCw } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ConfirmationRequest, FileStatusCode, GitDiffMap, GitStatusResponse } from '../../types/types'; import { getAllChangedFiles, hasChangedFiles } from '../../utils/gitPanelUtils'; @@ -43,6 +44,7 @@ export default function ChangesView({ onRequestConfirmation, onExpandedFilesChange, }: ChangesViewProps) { + const { t } = useTranslation('common'); const [expandedFiles, setExpandedFiles] = useState>(new Set()); const [selectedFiles, setSelectedFiles] = useState>(new Set()); @@ -161,9 +163,9 @@ export default function ChangesView({
-

No commits yet

+

{t('gitPanel.changes.noCommitsYet')}

- This repository doesn't have any commits yet. Create your first commit to start tracking changes. + {t('gitPanel.changes.noCommitsDesc')}

@@ -186,26 +188,26 @@ export default function ChangesView({ ) : !gitStatus || !hasChangedFiles(gitStatus) ? (
-

No changes detected

+

{t('gitPanel.changes.noChangesDetected')}

) : (
{/* STAGED section */}
- - Staged ({selectedFiles.size}) + + {t('gitPanel.changes.staged')} ({selectedFiles.size}) {selectedFiles.size > 0 && ( )}
{selectedFiles.size === 0 ? ( -
No staged files
+
{t('gitPanel.changes.noStagedFiles')}
) : ( - - Changes ({unstagedFiles.size}) + + {t('gitPanel.changes.changes')} ({unstagedFiles.size}) {unstagedFiles.size > 0 && ( )}
{unstagedFiles.size === 0 ? ( -
All changes staged
+
{t('gitPanel.changes.allChangesStaged')}
) : ( commitMessageCache.get(projectPath) ?? ''); const setCommitMessage = (msg: string) => { @@ -101,7 +103,9 @@ export default function CommitComposer({ className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground transition-colors hover:bg-primary/90" > - Commit {selectedFileCount} file{selectedFileCount !== 1 ? 's' : ''} + + {selectedFileCount === 1 ? t('gitPanel.composer.commitFile', { count: selectedFileCount }) : t('gitPanel.composer.commitFiles', { count: selectedFileCount })} +
@@ -109,7 +113,7 @@ export default function CommitComposer({
{isMobile && (
- Commit Changes + {t('gitPanel.composer.commitChanges')}