feat: add terminal dropdown with inbuilt and external options in task review (#347)

* feat: add dropdown to select inbuilt or external terminal in task review

Replace the single terminal button on the task review modal with a dropdown
menu that allows users to choose between:
- Opening in an inbuilt terminal tab (existing behavior)
- Opening in the system's default external terminal application

Changes:
- Add IPC channel SHELL_OPEN_TERMINAL for opening paths in system terminal
- Create IPC handler with cross-platform support (macOS, Windows, Linux)
- Update ShellAPI with openTerminal method
- Extend useTerminalHandler hook to support both terminal types
- Create TerminalDropdown component with dropdown menu UI
- Update WorkspaceStatus to use dropdown for both terminal buttons

Cross-platform support:
- macOS: Uses 'open -a Terminal' to open Terminal.app
- Windows: Uses 'start cmd' to open Command Prompt
- Linux: Tries common terminal emulators (gnome-terminal, konsole, xfce4-terminal, xterm)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: correct import paths in TerminalDropdown component

* feat: add modal close and view switch for inbuilt terminal option

When opening the inbuilt terminal from the task review modal, the UI now:
- Closes the task detail modal
- Switches to the Agent Terminals view
- Creates the terminal in that view

This provides a better user experience by automatically navigating to where
the terminal is created, rather than keeping the user in the modal.

Changes:
- Added onSwitchToTerminals prop to TaskDetailModal, TaskReview, and WorkspaceStatus
- Updated TerminalDropdown handlers to call onClose and onSwitchToTerminals before creating terminal
- Wired up App.tsx to pass setActiveView callback to TaskDetailModal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: pass terminal creation callback to parent to prevent unmount race condition

The previous implementation called openTerminal from the WorkspaceStatus component,
but when the modal closed and view switched, the component unmounted before the
terminal could be created.

This fix passes terminal creation parameters up to App.tsx where the modal close,
view switch, and terminal creation are handled in the correct order at the parent level.

Changes:
- Added onOpenInbuiltTerminal callback prop through component hierarchy
  (TaskDetailModal → TaskReview → WorkspaceStatus)
- Created handleOpenInbuiltTerminal in App.tsx that:
  1. Closes the modal (setSelectedTask(null))
  2. Switches to terminals view (setActiveView('terminals'))
  3. Creates the terminal (window.electronAPI.createTerminal)
- Updated TerminalDropdown handlers in WorkspaceStatus to call the callback
  instead of creating terminal directly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add terminal to frontend store to display in UI

The previous implementation created the terminal in the backend but didn't
add it to the frontend terminal store, so TerminalGrid had no terminal to render.

The correct flow is:
1. Add terminal to store (creates Terminal object in frontend)
2. Terminal component mounts
3. usePtyProcess hook creates backend PTY process

Changes:
- Updated handleOpenInbuiltTerminal to use useTerminalStore.getState().addTerminal()
- Removed direct window.electronAPI.createTerminal() call
- Terminal now appears in TerminalGrid after creation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add openTerminal to ElectronAPI type and browser mock

TypeScript was complaining about missing openTerminal method:
- Added openTerminal to ElectronAPI interface in ipc.ts
- Added openTerminal mock to infrastructure-mock.ts for browser mode

This fixes the typecheck errors in CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: use node: prefix for child_process import for better TypeScript resolution

Changed from 'child_process' to 'node:child_process' to ensure TypeScript
properly resolves the execSync import in all environments including CI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: improve terminal command security, deterministic mounting, and i18n

This commit addresses several issues with the terminal dropdown feature:

1. **Improved Windows Command Security** (settings-handlers.ts):
   - Removed fragile nested quotes from Windows terminal command
   - Changed from `"cd /d "${dirPath}""` to `cd /d "${sanitizedPath}"`
   - Added path sanitization to escape double quotes and prevent command injection
   - Simplified command structure for better reliability

2. **Deterministic Component Readiness** (App.tsx, TerminalGrid.tsx):
   - Replaced hardcoded 100ms timeout with deterministic readiness signal
   - Added `onMounted` prop to TerminalGrid that fires when component mounts
   - Created Promise-based waiting mechanism in handleOpenInbuiltTerminal
   - Checks if terminals view is already active to avoid unnecessary waiting
   - Ensures Terminal component is mounted before creating backend PTY

3. **i18n Compliance** (TerminalDropdown.tsx):
   - Replaced all hardcoded English strings with translation keys
   - Added useTranslation hook with 'taskReview' namespace
   - Updated button title: "Open terminal" → t('terminal.openTerminal')
   - Updated menu items:
     - "Open in Inbuilt Terminal" → t('terminal.openInbuilt')
     - "Open in External Terminal" → t('terminal.openExternal')
   - Created taskReview.json translation files for English and French

Files Changed:
- src/main/ipc-handlers/settings-handlers.ts
- src/renderer/App.tsx
- src/renderer/components/TerminalGrid.tsx
- src/renderer/components/task-detail/task-review/TerminalDropdown.tsx
- src/shared/i18n/locales/en/taskReview.json (new)
- src/shared/i18n/locales/fr/taskReview.json (new)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: use execFileSync with argument arrays to prevent command injection

Security Fix: Replaced all execSync shell commands with execFileSync and
argument arrays to completely eliminate command injection vulnerabilities.

Previous issue:
- Incomplete escaping: only escaped double quotes, not backslashes
- Shell interpretation could lead to command injection with crafted paths

Solution:
- macOS: execFileSync('open', ['-a', 'Terminal', dirPath])
- Windows: execFileSync('cmd.exe', ['/K', 'cd', '/d', dirPath], {shell: false})
- Linux: execFileSync(terminal, ['--working-directory', dirPath])

Benefits:
- No shell interpretation - arguments passed directly to executables
- No escaping needed - OS handles path special characters correctly
- Prevents all forms of command injection
- More reliable cross-platform behavior

For xterm (Linux fallback), single quotes are properly escaped using the
pattern: dirPath.replace(/'/g, "'\\''") which handles single quotes in paths.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: register taskReview namespace with i18n configuration

The taskReview translation files were created but not registered with the
i18n configuration, causing translation keys to be displayed literally
instead of the actual translated text.

Changes:
- Imported enTaskReview and frTaskReview translation files
- Added taskReview to resources object for both en and fr
- Added 'taskReview' to the ns (namespaces) array in i18n.init()

This fixes the dropdown menu displaying "terminal.openInbuilt" instead of
"Open in Inbuilt Terminal".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove unused onMounted callback and Promise-based readiness signaling

- Remove handleTerminalGridMounted function reference that caused runtime error
- Remove onMounted prop from TerminalGrid interface
- Remove useEffect hook that called onMounted callback
- Simplify handleOpenInbuiltTerminal to directly add terminal to store
- TerminalGrid is always mounted (just hidden), so no readiness signaling needed

This fixes "handleTerminalGridMounted is not defined" error and simplifies
the terminal creation flow.

* chore: remove unused imports (useRef, useCallback) from App.tsx

* refactor: add input validation and remove unused import in terminal handler

Security and code quality improvements:

1. Add comprehensive input validation for dirPath:
   - Check for non-empty string
   - Resolve to absolute path with path.resolve()
   - Verify path exists with existsSync()
   - Confirm it's a directory with statSync().isDirectory()
   - Return clear, actionable error messages if any check fails

2. Replace all uses of dirPath with validated resolvedPath

3. Remove unused execSync import from node:child_process

4. Add statSync to fs imports for directory validation

This prevents potential issues with invalid paths and improves error
handling with specific error messages for each validation failure.

* refactor: rename unused id parameter to _id in handleOpenInbuiltTerminal

- Prefix parameter with underscore to indicate intentionally unused
- Add comment explaining terminal ID is auto-generated by addTerminal()
- Keep parameter for callback signature consistency with callers
- Remove id from console.log since it's not used in the logic

This satisfies linter requirements while maintaining callback compatibility.

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
JoshuaRileyDev
2025-12-27 16:29:29 +00:00
committed by GitHub
parent 9d43abedde
commit fef07c9517
14 changed files with 281 additions and 33 deletions
@@ -1,6 +1,6 @@
import { ipcMain, dialog, app, shell } from 'electron';
import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { execFileSync } from 'child_process';
import { existsSync, writeFileSync, mkdirSync, statSync } from 'fs';
import { execFileSync } from 'node:child_process';
import path from 'path';
import { is } from '@electron-toolkit/utils';
import { IPC_CHANNELS, DEFAULT_APP_SETTINGS } from '../../shared/constants';
@@ -345,4 +345,96 @@ export function registerSettingsHandlers(
await shell.openExternal(url);
}
);
ipcMain.handle(
IPC_CHANNELS.SHELL_OPEN_TERMINAL,
async (_, dirPath: string): Promise<IPCResult<void>> => {
try {
// Validate dirPath input
if (!dirPath || typeof dirPath !== 'string' || dirPath.trim() === '') {
return {
success: false,
error: 'Directory path is required and must be a non-empty string'
};
}
// Resolve to absolute path
const resolvedPath = path.resolve(dirPath);
// Verify path exists
if (!existsSync(resolvedPath)) {
return {
success: false,
error: `Directory does not exist: ${resolvedPath}`
};
}
// Verify it's a directory
try {
if (!statSync(resolvedPath).isDirectory()) {
return {
success: false,
error: `Path is not a directory: ${resolvedPath}`
};
}
} catch (statError) {
return {
success: false,
error: `Cannot access path: ${resolvedPath}`
};
}
const platform = process.platform;
if (platform === 'darwin') {
// macOS: Use execFileSync with argument array to prevent injection
execFileSync('open', ['-a', 'Terminal', resolvedPath], { stdio: 'ignore' });
} else if (platform === 'win32') {
// Windows: Use cmd.exe directly with argument array
// /C tells cmd to execute the command and terminate
// /K keeps the window open after executing cd
execFileSync('cmd.exe', ['/K', 'cd', '/d', resolvedPath], {
stdio: 'ignore',
windowsHide: false,
shell: false // Explicitly disable shell to prevent injection
});
} else {
// Linux: Try common terminal emulators with argument arrays
const terminals: Array<{ cmd: string; args: string[] }> = [
{ cmd: 'gnome-terminal', args: ['--working-directory', resolvedPath] },
{ cmd: 'konsole', args: ['--workdir', resolvedPath] },
{ cmd: 'xfce4-terminal', args: ['--working-directory', resolvedPath] },
{ cmd: 'xterm', args: ['-e', 'bash', '-c', `cd '${resolvedPath.replace(/'/g, "'\\''")}' && exec bash`] }
];
let opened = false;
for (const { cmd, args } of terminals) {
try {
execFileSync(cmd, args, { stdio: 'ignore' });
opened = true;
break;
} catch {
// Try next terminal
continue;
}
}
if (!opened) {
return {
success: false,
error: 'No supported terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal, or xterm.'
};
}
}
return { success: true };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
error: `Failed to open terminal: ${errorMsg}`
};
}
}
);
}
@@ -1,11 +1,13 @@
import { IPC_CHANNELS } from '../../../shared/constants';
import { invokeIpc } from './ipc-utils';
import type { IPCResult } from '../../../shared/types';
/**
* Shell Operations API
*/
export interface ShellAPI {
openExternal: (url: string) => Promise<void>;
openTerminal: (dirPath: string) => Promise<IPCResult<void>>;
}
/**
@@ -13,5 +15,7 @@ export interface ShellAPI {
*/
export const createShellAPI = (): ShellAPI => ({
openExternal: (url: string): Promise<void> =>
invokeIpc(IPC_CHANNELS.SHELL_OPEN_EXTERNAL, url)
invokeIpc(IPC_CHANNELS.SHELL_OPEN_EXTERNAL, url),
openTerminal: (dirPath: string): Promise<IPCResult<void>> =>
invokeIpc(IPC_CHANNELS.SHELL_OPEN_TERMINAL, dirPath)
});
+25
View File
@@ -391,6 +391,29 @@ export function App() {
setSelectedTask(null);
};
const handleOpenInbuiltTerminal = (_id: string, cwd: string) => {
// Note: _id parameter is intentionally unused - terminal ID is auto-generated by addTerminal()
// Parameter kept for callback signature consistency with callers
console.log('[App] Opening inbuilt terminal:', { cwd });
// Switch to terminals view
setActiveView('terminals');
// Close modal
setSelectedTask(null);
// Add terminal to store - this will trigger Terminal component to mount
// which will then create the backend PTY via usePtyProcess
// Note: TerminalGrid is always mounted (just hidden), so no need to wait
const terminal = useTerminalStore.getState().addTerminal(cwd, selectedProject?.path);
if (!terminal) {
console.error('[App] Failed to add terminal to store (max terminals reached?)');
} else {
console.log('[App] Terminal added to store:', terminal.id);
}
};
const handleAddProject = async () => {
try {
const path = await window.electronAPI.selectDirectory();
@@ -716,6 +739,8 @@ export function App() {
open={!!selectedTask}
task={selectedTask}
onOpenChange={(open) => !open && handleCloseTaskDetail()}
onSwitchToTerminals={() => setActiveView('terminals')}
onOpenInbuiltTerminal={handleOpenInbuiltTerminal}
/>
{/* Dialogs */}
@@ -44,9 +44,11 @@ interface TaskDetailModalProps {
open: boolean;
task: Task | null;
onOpenChange: (open: boolean) => void;
onSwitchToTerminals?: () => void;
onOpenInbuiltTerminal?: (id: string, cwd: string) => void;
}
export function TaskDetailModal({ open, task, onOpenChange }: TaskDetailModalProps) {
export function TaskDetailModal({ open, task, onOpenChange, onSwitchToTerminals, onOpenInbuiltTerminal }: TaskDetailModalProps) {
// Don't render anything if no task
if (!task) {
return null;
@@ -57,12 +59,14 @@ export function TaskDetailModal({ open, task, onOpenChange }: TaskDetailModalPro
open={open}
task={task}
onOpenChange={onOpenChange}
onSwitchToTerminals={onSwitchToTerminals}
onOpenInbuiltTerminal={onOpenInbuiltTerminal}
/>
);
}
// Separate component to use hooks only when task exists
function TaskDetailModalContent({ open, task, onOpenChange }: { open: boolean; task: Task; onOpenChange: (open: boolean) => void }) {
function TaskDetailModalContent({ open, task, onOpenChange, onSwitchToTerminals, onOpenInbuiltTerminal }: { open: boolean; task: Task; onOpenChange: (open: boolean) => void; onSwitchToTerminals?: () => void; onOpenInbuiltTerminal?: (id: string, cwd: string) => void }) {
const state = useTaskDetail({ task });
const progressPercent = calculateProgress(task.subtasks);
const completedSubtasks = task.subtasks.filter(s => s.status === 'completed').length;
@@ -408,6 +412,8 @@ function TaskDetailModalContent({ open, task, onOpenChange }: { open: boolean; t
onShowConflictDialog={state.setShowConflictDialog}
onLoadMergePreview={state.loadMergePreview}
onClose={handleClose}
onSwitchToTerminals={onSwitchToTerminals}
onOpenInbuiltTerminal={onOpenInbuiltTerminal}
/>
</>
)}
@@ -40,6 +40,8 @@ interface TaskReviewProps {
onShowConflictDialog: (show: boolean) => void;
onLoadMergePreview: () => void;
onClose?: () => void;
onSwitchToTerminals?: () => void;
onOpenInbuiltTerminal?: (id: string, cwd: string) => void;
}
/**
@@ -79,7 +81,9 @@ export function TaskReview({
onStageOnlyChange,
onShowConflictDialog,
onLoadMergePreview,
onClose
onClose,
onSwitchToTerminals,
onOpenInbuiltTerminal
}: TaskReviewProps) {
return (
<div className="space-y-4">
@@ -115,6 +119,9 @@ export function TaskReview({
onLoadMergePreview={onLoadMergePreview}
onStageOnlyChange={onStageOnlyChange}
onMerge={onMerge}
onClose={onClose}
onSwitchToTerminals={onSwitchToTerminals}
onOpenInbuiltTerminal={onOpenInbuiltTerminal}
/>
) : task.stagedInMainProject && !stagedSuccess ? (
<StagedInProjectMessage
@@ -2,12 +2,15 @@ import { useState } from 'react';
/**
* Hook for handling terminal creation with proper error handling and loading states.
* Fixes silent failures when terminal buttons are clicked.
* Supports both inbuilt terminal and external system terminal.
*/
export function useTerminalHandler() {
const [error, setError] = useState<string | null>(null);
const [isOpening, setIsOpening] = useState(false);
/**
* Open an inbuilt terminal tab
*/
const openTerminal = async (id: string, cwd: string) => {
setIsOpening(true);
setError(null);
@@ -28,5 +31,28 @@ export function useTerminalHandler() {
}
};
return { openTerminal, error, isOpening };
/**
* Open the path in the system's default external terminal application
*/
const openExternalTerminal = async (cwd: string) => {
setIsOpening(true);
setError(null);
try {
const result = await window.electronAPI.openTerminal(cwd);
if (!result.success) {
setError(result.error || 'Failed to open external terminal');
console.error('[Terminal] Failed to open external:', result.error);
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
setError(`Failed to open external terminal: ${errorMsg}`);
console.error('[Terminal] Exception:', err);
} finally {
setIsOpening(false);
}
};
return { openTerminal, openExternalTerminal, error, isOpening };
}
@@ -0,0 +1,55 @@
import { Terminal, ExternalLink, ChevronDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '../../ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '../../ui/dropdown-menu';
interface TerminalDropdownProps {
onOpenInbuilt: () => void;
onOpenExternal: () => void;
disabled?: boolean;
className?: string;
}
/**
* Dropdown button for selecting terminal type (inbuilt or external)
*/
export function TerminalDropdown({
onOpenInbuilt,
onOpenExternal,
disabled = false,
className
}: TerminalDropdownProps) {
const { t } = useTranslation('taskReview');
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={disabled}
className={className}
title={t('terminal.openTerminal')}
>
<Terminal className="h-3.5 w-3.5" />
<ChevronDown className="h-3 w-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={onOpenInbuilt}>
<Terminal className="h-4 w-4 mr-2" />
{t('terminal.openInbuilt')}
</DropdownMenuItem>
<DropdownMenuItem onClick={onOpenExternal}>
<ExternalLink className="h-4 w-4 mr-2" />
{t('terminal.openExternal')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -19,6 +19,7 @@ import { Checkbox } from '../../ui/checkbox';
import { cn } from '../../../lib/utils';
import type { Task, WorktreeStatus, MergeConflict, MergeStats, GitConflictInfo } from '../../../../shared/types';
import { useTerminalHandler } from '../hooks/useTerminalHandler';
import { TerminalDropdown } from './TerminalDropdown';
interface WorkspaceStatusProps {
task: Task;
@@ -35,6 +36,9 @@ interface WorkspaceStatusProps {
onLoadMergePreview: () => void;
onStageOnlyChange: (value: boolean) => void;
onMerge: () => void;
onClose?: () => void;
onSwitchToTerminals?: () => void;
onOpenInbuiltTerminal?: (id: string, cwd: string) => void;
}
/**
@@ -54,9 +58,12 @@ export function WorkspaceStatus({
onShowConflictDialog,
onLoadMergePreview,
onStageOnlyChange,
onMerge
onMerge,
onClose,
onSwitchToTerminals,
onOpenInbuiltTerminal
}: WorkspaceStatusProps) {
const { openTerminal, error: terminalError, isOpening } = useTerminalHandler();
const { openTerminal, openExternalTerminal, error: terminalError, isOpening } = useTerminalHandler();
const hasGitConflicts = mergePreview?.gitConflicts?.hasConflicts;
const hasUncommittedChanges = mergePreview?.uncommittedChanges?.hasChanges;
const uncommittedCount = mergePreview?.uncommittedChanges?.count || 0;
@@ -98,16 +105,16 @@ export function WorkspaceStatus({
View
</Button>
{worktreeStatus.worktreePath && (
<Button
variant="ghost"
size="sm"
onClick={() => openTerminal(`open-${task.id}`, worktreeStatus.worktreePath!)}
className="h-7 px-2"
title="Open in terminal"
<TerminalDropdown
onOpenInbuilt={() => {
if (onOpenInbuiltTerminal) {
onOpenInbuiltTerminal(`open-${task.id}`, worktreeStatus.worktreePath!);
}
}}
onOpenExternal={() => openExternalTerminal(worktreeStatus.worktreePath!)}
disabled={isOpening}
>
<Terminal className="h-3.5 w-3.5" />
</Button>
className="h-7 px-2"
/>
)}
</div>
</div>
@@ -177,21 +184,22 @@ export function WorkspaceStatus({
<p className="text-xs text-muted-foreground mt-0.5">
Commit or stash them before staging to avoid conflicts.
</p>
<Button
variant="outline"
size="sm"
onClick={() => {
<TerminalDropdown
onOpenInbuilt={() => {
const mainProjectPath = worktreeStatus.worktreePath?.replace('.worktrees/' + task.specId, '') || '';
if (mainProjectPath) {
openTerminal(`stash-${task.id}`, mainProjectPath);
if (mainProjectPath && onOpenInbuiltTerminal) {
onOpenInbuiltTerminal(`stash-${task.id}`, mainProjectPath);
}
}}
onOpenExternal={() => {
const mainProjectPath = worktreeStatus.worktreePath?.replace('.worktrees/' + task.specId, '') || '';
if (mainProjectPath) {
openExternalTerminal(mainProjectPath);
}
}}
className="text-xs h-6 mt-2"
disabled={isOpening}
>
<Terminal className="h-3 w-3 mr-1" />
{isOpening ? 'Opening...' : 'Open Terminal'}
</Button>
className="text-xs h-6 mt-2"
/>
</div>
</div>
)}
@@ -184,5 +184,10 @@ export const infrastructureMock = {
openExternal: async (url: string) => {
console.warn('[Browser Mock] openExternal:', url);
window.open(url, '_blank');
},
openTerminal: async (dirPath: string) => {
console.warn('[Browser Mock] openTerminal:', dirPath);
return { success: true };
}
};
@@ -117,6 +117,7 @@ export const IPC_CHANNELS = {
// Shell operations
SHELL_OPEN_EXTERNAL: 'shell:openExternal',
SHELL_OPEN_TERMINAL: 'shell:openTerminal',
// Roadmap operations
ROADMAP_GET: 'roadmap:get',
+7 -3
View File
@@ -9,6 +9,7 @@ import enTasks from './locales/en/tasks.json';
import enWelcome from './locales/en/welcome.json';
import enOnboarding from './locales/en/onboarding.json';
import enDialogs from './locales/en/dialogs.json';
import enTaskReview from './locales/en/taskReview.json';
// Import French translation resources
import frCommon from './locales/fr/common.json';
@@ -18,6 +19,7 @@ import frTasks from './locales/fr/tasks.json';
import frWelcome from './locales/fr/welcome.json';
import frOnboarding from './locales/fr/onboarding.json';
import frDialogs from './locales/fr/dialogs.json';
import frTaskReview from './locales/fr/taskReview.json';
export const defaultNS = 'common';
@@ -29,7 +31,8 @@ export const resources = {
tasks: enTasks,
welcome: enWelcome,
onboarding: enOnboarding,
dialogs: enDialogs
dialogs: enDialogs,
taskReview: enTaskReview
},
fr: {
common: frCommon,
@@ -38,7 +41,8 @@ export const resources = {
tasks: frTasks,
welcome: frWelcome,
onboarding: frOnboarding,
dialogs: frDialogs
dialogs: frDialogs,
taskReview: frTaskReview
}
} as const;
@@ -49,7 +53,7 @@ i18n
lng: 'en', // Default language (will be overridden by settings)
fallbackLng: 'en',
defaultNS,
ns: ['common', 'navigation', 'settings', 'tasks', 'welcome', 'onboarding', 'dialogs'],
ns: ['common', 'navigation', 'settings', 'tasks', 'welcome', 'onboarding', 'dialogs', 'taskReview'],
interpolation: {
escapeValue: false // React already escapes values
},
@@ -0,0 +1,7 @@
{
"terminal": {
"openTerminal": "Open terminal",
"openInbuilt": "Open in Inbuilt Terminal",
"openExternal": "Open in External Terminal"
}
}
@@ -0,0 +1,7 @@
{
"terminal": {
"openTerminal": "Ouvrir le terminal",
"openInbuilt": "Ouvrir dans le terminal intégré",
"openExternal": "Ouvrir dans le terminal externe"
}
}
+1
View File
@@ -452,6 +452,7 @@ export interface ElectronAPI {
// Shell operations
openExternal: (url: string) => Promise<void>;
openTerminal: (dirPath: string) => Promise<IPCResult<void>>;
// Auto Claude source environment operations
getSourceEnv: () => Promise<IPCResult<SourceEnvConfig>>;