diff --git a/src/common/api.ts b/src/common/api.ts index 850004693..fc318261d 100644 --- a/src/common/api.ts +++ b/src/common/api.ts @@ -46,6 +46,7 @@ import { BranchInfo, WorktreeIntegrationStatus, MemoryEmbeddingProgress, + ShellInfo, } from '@common/types'; export interface ApplicationAPI { @@ -183,6 +184,7 @@ export interface ApplicationAPI { // Terminal operations isTerminalSupported: () => boolean; + getAvailableShells: () => Promise; createTerminal: (baseDir: string, taskId: string, cols?: number, rows?: number) => Promise; writeToTerminal: (terminalId: string, data: string) => Promise; resizeTerminal: (terminalId: string, cols: number, rows: number) => Promise; diff --git a/src/common/locales/en.json b/src/common/locales/en.json index 09ee205ed..ee925e56c 100644 --- a/src/common/locales/en.json +++ b/src/common/locales/en.json @@ -366,6 +366,7 @@ "aider": "Aider", "agents": "Agents", "tasks": "Tasks", + "terminal": "Terminal", "memory": "Memory", "voice": "Voice", "hotkeys": "Hotkeys", @@ -443,6 +444,17 @@ "showTaskStateActions": "Show Task State Actions", "showTaskStateActionsTooltip": "When enabled, task state actions are displayed at the bottom of messages. Disable to hide these actions." }, + "terminal": { + "shell": "Terminal Shell", + "shellPath": "Shell Path", + "autoDetect": "Auto Detect", + "autoDetectDescription": "Automatically use the system default shell", + "selectFromList": "Select from list", + "customPath": "Custom path", + "customShellPath": "Custom Shell Path", + "customShellPathPlaceholder": "e.g. /bin/zsh or C:\\Program Files\\Git\\bin\\bash.exe", + "customShellPathDescription": "Enter a custom shell path if your preferred shell is not in the list." + }, "language": "Language", "notifications": { "title": "Notifications" @@ -989,416 +1001,26 @@ }, "fileEdit": { "title": "Edit", - "searchTerm": "Search Term", - "regex": "Regex", - "replacementText": "Replacement Text", - "replaceAll": "Replace All" + "original": "Original", + "updated": "Updated" }, "glob": { - "title": "Find files matching pattern:", - "foundFiles": "Found {{count}} files", - "noMatches": "No matches found.", - "for": "for", - "contextLines": "({{count}} lines of context)", - "matchesCount": "({{count}} matches)", - "lineLabel": "Line {{lineNumber}}:", - "contextLabel": "Context:" + "title": "Glob", + "pattern": "Pattern" }, "grep": { - "title": "Search files matching", - "for": "for", - "foundMatches": "Found {{count}} matches", - "noMatches": "No matches found.", - "contextLines": "({{count}} lines of context)", - "matchesCount": "({{count}} matches)", - "lineLabel": "Line {{lineNumber}}:", - "contextLabel": "Context:" - }, - "bash": { - "title": "Run command", - "exitCode": "Exit Code", - "stdout": "Stdout", - "stderr": "Stderr", - "commandFailed": "Command failed" + "title": "Grep", + "pattern": "Pattern", + "searchTerm": "Search term" }, "semanticSearch": { - "title": "Semantic search:", - "in": "in", - "noMatches": "No matches found.", - "foundMatches": "Found {{count}} matches", - "score": " (score: {{score}})", - "resultLabel": "Result {{index}}" + "title": "Semantic Search", + "query": "Query" }, "fetch": { - "title": "Fetch" + "title": "Fetch", + "url": "URL" } - }, - "memory": { - "group": "Memory", - "storingMemory": "Storing memory...", - "retrievingMemories": "Retrieving memories...", - "deletingMemory": "Deleting memory...", - "updatingMemory": "Updating memory...", - "listingMemories": "Listing memories...", - "memoryStored": "Memory stored.", - "memoryUpdated": "Memory updated.", - "memoryDeleted": "Memory deleted.", - "memoryId": "Memory ID", - "type": "Type", - "content": "Content", - "newContent": "New Content", - "query": "Query", - "limit": "Limit", - "foundMemories": "Found {{count}} memories.", - "noMemoriesFound": "No relevant memories have been found.", - "noRelevantMemories": "No relevant memories have been found", - "storedMemoryDetails": "Stored Memory Details", - "updatedMemoryDetails": "Updated Memory Details", - "deletedMemoryDetails": "Deleted Memory Details", - "retrievedMemories": "Retrieved Memories", - "memoryList": "Memory List", - "listedMemories": "Listing {{count}} memories.", - "ofType": "of type {{type}}", - "noMemoriesOfType": "No memories found of type: {{type}}", - "all": "All", - "delete": "Delete", - "memoryNotFound": "Not found", - "deleteDialogTitle": "Delete memory", - "deleteDialogText": "Are you sure you want to delete this memory?" - }, - "subagents": { - "groupRunning": "{{name}}: Subagent is working...", - "groupCompleted": "{{name}}: Subagent finished the task.", - "running": "Subagent is working...", - "completed": "Subagent finished the task.", - "prompt": "Prompt" - }, - "errors": { - "noSuchTool": "Attempted to use unknown tool: {{toolName}}", - "invalidToolArguments": "Invalid arguments for tool: {{toolName}}" - } - }, - "codeBlock": { - "revertChanges": "Revert changes" - }, - "diffViewer": { - "sideBySide": "Side by Side", - "unified": "Unified", - "compact": "Compact", - "noChanges": "No changes have been made. Content is identical." - }, - "promptField": { - "startRecording": "Start listening", - "stopRecording": "Stop listening", - "savePrompt": "Save Prompt", - "savePromptDescription": "Save prompt without executing", - "saveError": "Failed to save prompt: {{error}}", - "autoApprove": "Auto Approve", - "autoApproveTooltip": "When enabled, automatically approve tool executions without asking for confirmation", - "autoApproveLockTooltip": "Locking keeps Auto Approve enabled for newly created tasks", - "placeholders": { - "0": "Update the UserValidator class to add email format validation...", - "1": "Fix the memory leak in DataProcessor.processBatch() by closing...", - "2": "Add error handling to PaymentService.chargeCard() with proper...", - "3": "Create a React component UserProfileCard with props interface...", - "4": "Optimize the SQL query in UserRepository.findByEmail() by adding...", - "5": "Add unit tests for AuthController.login() using Jest and...", - "6": "Implement the missing handleResize() method in ChartComponent...", - "7": "Update package.json to add TypeScript dependency and configure...", - "8": "Add input validation to ContactForm's handleSubmit function...", - "9": "Fix the bug in calculateTotal() where decimal values are being...", - "10": "Add logging to API middleware using Winston for request...", - "11": "Create a Dockerfile for Node.js application with multi-stage...", - "12": "Add rate limiting middleware to /api/users endpoint using...", - "13": "Implement the missing deleteTodo() function in TodoService...", - "14": "Add TypeScript interfaces for API response objects in...", - "15": "Fix the CSS issue where mobile menu is not displaying...", - "16": "Add environment variable validation using dotenv and...", - "17": "Create database migration to add 'last_login' column to...", - "18": "Add error boundaries to React app to catch component...", - "19": "Implement pagination logic in ProductList's fetchProducts..." - }, - "questionPlaceholder": "...or suggest something else", - "answers": { - "yes": "(Y)es", - "no": "(N)o", - "always": "(A)lways", - "dontAsk": "(D)on't ask again" - }, - "invalidCommand": "Invalid command", - "stopResponse": "Stop response", - "sendMessage": "Send message (Enter)", - "clearChat": "Clear chat", - "agentModeOnly": "Available in Agent mode only" - }, - "agentProfiles": { - "title": "Agents", - "profileSettings": "Agent Profile: {{profileName}}", - "newProfileName": "New Profile", - "manageProfiles": "Manage Profiles", - "createNew": "New Profile", - "profileName": "Name", - "profileDescription": "Description", - "profileDescriptionPlaceholder": "Describe this agent profile (purpose, tools, etc.)", - "tools": "Tools", - "model": "Model", - "selectProfile": "Select...", - "saveAsProjectDefault": "Save as project default" - }, - "projectBar": { - "editFormatTooltip": "This is the format used for editing code. Changing it might cause the model to not perform as expected.", - "aider": "Aider", - "toggleSidebar": "Toggle sidebar", - "modelLibrary": "Model Library", - "worktree": "Worktree", - "worktreeTooltip": "Toggle worktree for this task" - }, - "workingMode": { - "local": "Local", - "worktree": "Worktree", - "switching": "Switching...", - "confirmLocalTitle": "Switch to Local Mode", - "confirmLocalMessage": "Warning: This worktree has unsaved work:\n{{warnings}}\n\nRemoving the worktree will delete this work. If you want to keep this work, use Merge action to merge it to the main branch first.\n\nAre you sure you want to continue and remove the worktree?", - "confirmLocalAction": "Remove and switch" - }, - "workingModeTooltip": { - "local": "Make changes directly in the local project directory for this task", - "worktree": "Use git worktree to make isolated changes for this task" - }, - "reasoningEffort": { - "label": "Reasoning effort", - "tooltip": "Reasoning effort determines how much the model will think before taking action. High reasoning effort means the model will think more before taking action, but will also take longer to respond.", - "none": "None", - "minimal": "Minimal", - "low": "Low", - "medium": "Medium", - "high": "High", - "max": "Max" - }, - "commands": { - "add": "Add files to the context", - "agent": "Switch to agent mode. If already in agent mode, opens the agent model selector. Any text after the command is used as a prompt", - "architect": "Switch to architect mode. Architect mode is used to plan changes to the code base and execute them", - "ask": "Switch to ask mode. Ask questions about the code base without editing any files", - "clear": "Clear the chat history", - "clear-logs": "Remove only log messages from chat", - "code": "Switch to code mode. Ask for changes to your code", - "commit": "Commit unstaged changes to the repo", - "context": "Switch to context mode. Ask for changes and Aider will add relevant files to the context", - "compact": "Summarize the conversation", - "copy-context": "Copy the current chat context as markdown, suitable to paste into a web UI", - "drop": "Remove files from the context to free up context space", - "edit-last": "Edits the last user message", - "init": "Initializes AGENTS.md rule file with project information", - "agentModeOnly": "Agent mode only", - "map": "Print out the current repository map", - "map-refresh": "Force a refresh of the repository map", - "model": "Open the model selector", - "read-only": "Add files to the chat that are for reference only, or turn added files to read-only", - "redo": "Redoes the last user prompt.", - "reasoning-effort": "Set the reasoning effort level (values: minimal/low/medium/high depending on model)", - "reset": "Drop all files and clear the chat history", - "run": "Run a shell command and optionally add the output to the chat", - "task-info": "Show task information", - "test": "Run a predefined test command and add the output to the chat on non-zero exit code", - "think-tokens": "Set the thinking token budget (supports formats like 8096, 8k, 10.5k, 0.5M)", - "tokens": "Report on the number of tokens used by the current chat context", - "undo": "Undo the last git commit if it was done by Aider", - "web": "Scrape a webpage and save it to a file. Use /web url [file] with optional [file]." - }, - "telemetry": { - "title": "Telemetry Information", - "message": "To help us improve AiderDesk, anonymous telemetry data is collected. This includes information like mode usage (agent, code, ask...) and some specifics when Agent mode is used. No personal data or prompts are ever collected.", - "fullInfo": "Full information about what is collected can be found here.", - "enabledLabel": "Enable anonymous telemetry" - }, - "multiselect": { - "noneSelected": "None selected", - "allSelected": "All selected", - "someSelected": "{{count}} selected", - "selectAll": "Select All" - }, - "usageDashboard": { - "title": "Usage Dashboard", - "refresh": "Refresh", - "projects": "Projects", - "models": "Models", - "dateRange": "Date Range", - "total": "Total", - "thisMonth": "This month", - "today": "Today", - "all": "All", - "groupBy": "Group by", - "periods": { - "hour": "Hour", - "day": "Day", - "month": "Month", - "year": "Year" - }, - "tabs": { - "table": "Table View", - "charts": "Charts View" - }, - "charts": { - "tokenUsageTrend": "Token Usage Trend", - "costBreakdown": "Cost Breakdown", - "messageBreakdown": "Message Breakdown", - "modelUsageDistribution": "Model Usage Distribution", - "inputTokens": "Input Tokens", - "outputTokens": "Output Tokens", - "noData": "No data available for the selected period", - "tokens": "Tokens", - "cost": "Cost", - "share": "Share", - "moreItems": "+{{count}} more" - }, - "error": { - "fetch": "Failed to fetch usage data." - }, - "table": { - "date": "Date", - "project": "Project", - "model": "Model", - "input": "Input", - "output": "Output", - "cacheRead": "Cache Read", - "cacheWrite": "Cache Write", - "totalTokens": "Total Tokens", - "cost": "Cost" - } - }, - "terminal": { - "copyOutput": "Copy terminal output to prompt", - "minimize": "Minimize terminal", - "addTerminal": "Add new terminal" - }, - "contextMenu": { - "copy": "Copy", - "paste": "Paste", - "close": "Close", - "closeOtherTabs": "Close other tabs", - "closeAllTabs": "Close all tabs" - }, - "taskSidebar": { - "title": "Tasks", - "noTasks": "No tasks available", - "deleteTask": "Delete task", - "deleteConfirm": "Are you sure you want to delete this task?", - "createTask": "Create new task", - "rename": "Rename", - "pinTask": "Pin task", - "unpinTask": "Unpin task", - "taskNamePlaceholder": "Enter task name...", - "untitled": "[Untitled]", - "exportAsMarkdown": "Export as markdown", - "exportAsImage": "Export as image", - "taskIdCopied": "Task ID {{taskId}} copied to clipboard", - "duplicateTask": "Duplicate Task", - "search": "Search tasks", - "searchPlaceholder": "Search tasks...", - "archiveTask": "Archive Task", - "unarchiveTask": "Unarchive Task", - "showArchived": "Show archived", - "hideArchived": "Hide archived", - "archived": "Archived", - "multiselectMode": "Multiselect Mode", - "selectedCount": "{{count}} selected", - "closeMultiselect": "Exit multiselect", - "deleteSelected": "Delete selected", - "archiveSelected": "Archive selected", - "unarchiveSelected": "Unarchive selected", - "deleteSelectedConfirm": "Are you sure you want to delete {{count}} selected tasks?", - "archiveSelectedConfirm": "Are you sure you want to archive {{count}} selected tasks?", - "unarchiveSelectedConfirm": "Are you sure you want to unarchive {{count}} selected tasks?", - "generatingName": "Generating name..." - }, - "taskInfo": { - "createdAt": "Created at", - "updatedAt": "Updated at", - "worktreeDirectory": "Worktree Directory", - "baseBranch": "Base Branch", - "baseCommit": "Base Commit", - "targetBranch": "Target Branch", - "lastMergeState": "Last Merge State", - "beforeMergeCommit": "Before Merge Commit", - "mergeTimestamp": "Merge Timestamp", - "openFolder": "Open Folder", - "taskId": "Task ID", - "state": "State", - "messageCount": "Message count" - }, - "taskState": { - "TODO": "Todo", - "IN_PROGRESS": "In Progress", - "INTERRUPTED": "Interrupted", - "MORE_INFO_NEEDED": "More Info Needed", - "READY_FOR_REVIEW": "Ready for Review", - "READY_FOR_IMPLEMENTATION": "Ready for Implementation", - "DONE": "Done" - }, - "worktree": { - "aheadCommits": "↑ {{count}} commits", - "uncommittedFiles": "{{count}} files", - "conflicts": "conflicts", - "conflictsPredicted": "Conflicts predicted", - "conflictsPresent": "Conflicts present", - "merge": "Merge", - "squash": "Squash & Merge", - "onlyUncommitted": "Only uncommitted", - "rebaseFromBranch": "Rebase from branch", - "abortRebase": "Abort rebase", - "continueRebase": "Continue rebase", - "rebaseConflicts": "Rebase failed due to conflicts. Please resolve them manually or use the AI agent to help.", - "confirmRebaseTitle": "Rebase worktree", - "confirmRebaseMessage": "This will rebase your worktree onto the selected branch. If there are conflicts, you will need to resolve them.", - "confirmAbortRebaseTitle": "Abort rebase", - "confirmAbortRebaseMessage": "This will abort the in-progress rebase and restore the worktree to its pre-rebase state.", - "confirmContinueRebaseTitle": "Continue rebase", - "confirmContinueRebaseMessage": "This will try to continue the rebase. Make sure all conflicts are resolved and staged.", - "resolveConflictsWithAgent": "Resolve conflicts with agent", - "confirmResolveConflictsWithAgentTitle": "Resolve conflicts with agent", - "confirmResolveConflictsWithAgentMessage": "This will try to automatically resolve all conflicted files and stage them. Review results carefully before continuing the rebase.", - "commitMessage": "Commit message", - "commitMessagePlaceholder": "Enter commit message...", - "selectBranch": "Select branch", - "selectTargetBranchTitle": "Select target branch", - "selectSourceBranchTitle": "Select source branch", - "branchHasWorktree": "has worktree", - "mergeTooltip": "Merge worktree changes to a branch", - "revertTooltip": "Revert the last merged changes", - "confirmMerge": "Are you sure you want to {{action}} the worktree changes to the main branch? This will include any uncommitted changes.", - "confirmRevert": "Are you sure you want to revert the last merge? This will reset the main branch to its previous state.", - "confirmMergeTitle": "Confirm Merge", - "confirmSquashTitle": "Confirm Squash", - "confirmOnlyUncommittedTitle": "Confirm Apply Uncommitted Changes", - "confirmMergeMessage": "Are you sure you want to merge the worktree changes to the selected branch?", - "confirmSquashMessage": "Are you sure you want to squash and merge the worktree changes to the selected branch?", - "confirmOnlyUncommittedMessage": "Are you sure you want to apply uncommitted changes from worktree to the selected branch? No git merge will be performed.", - "mergeConflicts": "Merge failed due to conflicts between your worktree and the target branch. Please rebase your worktree first to resolve them.", - "applyUncommittedConflicts": "Failed to apply uncommitted changes due to conflicts with the target branch. Please rebase your worktree first to align your changes.", - "dontAskAgain": "Don't ask me again", - "confirmRevertTitle": "Confirm Revert", - "confirmRevertMessage": "Are you sure you want to revert the last merge? This will undo the changes that were merged from the worktree back to the main branch.", - "revert": "Revert", - "noChanges": "No changes" - }, - "welcomeMessage": { - "title": "AiderDesk", - "subtitle": "Your AI-powered coding assistant", - "features": { - "title": "What you can do:", - "aiCoding": "Generate and modify code with AI assistance", - "contextManagement": "Add files to context for better results", - "multiModel": "Choose from multiple AI models and providers", - "modes": "Switch between Agent and Aider modes", - "commands": "Execute commands and run tests" - }, - "tips": { - "title": "Getting started:", - "addFiles": "Add files to context using the sidebar or /add command", - "askQuestion": "Type your request in the prompt field below", - "useCommands": "Use / commands for quick actions", - "switchMode": "Try Agent mode for autonomous task completion" } } } diff --git a/src/common/locales/zh.json b/src/common/locales/zh.json index d21a0cb56..e7a6fd62c 100644 --- a/src/common/locales/zh.json +++ b/src/common/locales/zh.json @@ -368,6 +368,7 @@ "server": "服务器", "memory": "记忆", "tasks": "任务", + "terminal": "终端", "voice": "语音", "about": "关于" }, @@ -442,6 +443,17 @@ "showTaskStateActions": "显示任务状态操作", "showTaskStateActionsTooltip": "启用后,任务状态操作将显示在消息底部。禁用以隐藏这些操作。" }, + "terminal": { + "shell": "终端 Shell", + "shellPath": "Shell 路径", + "autoDetect": "自动检测", + "autoDetectDescription": "自动使用系统默认 Shell", + "selectFromList": "从列表中选择", + "customPath": "自定义路径", + "customShellPath": "自定义 Shell 路径", + "customShellPathPlaceholder": "例如 /bin/zsh 或 C:\\Program Files\\Git\\bin\\bash.exe", + "customShellPathDescription": "如果列表中没有您首选的 shell,请输入自定义 shell 路径。" + }, "language": "语言", "notifications": { "title": "通知" @@ -990,422 +1002,19 @@ "title": "执行 shell 命令", "exitCode": "退出码", "stdout": "标准输出", - "stderr": "标准错误", - "commandFailed": "命令失败" + "stderr": "标准错误" }, "semanticSearch": { - "title": "语义搜索:", - "in": "在", - "noMatches": "未找到匹配项。", + "title": "语义搜索", + "query": "查询", "foundMatches": "找到 {{count}} 个匹配项", - "score": " (得分: {{score}})", - "resultLabel": "结果 {{index}}" + "noMatches": "未找到匹配项。" }, "fetch": { - "title": "抓取网页内容" + "title": "获取 URL", + "url": "URL", + "content": "内容" } - }, - "memory": { - "group": "记忆", - "storingMemory": "正在存储记忆...", - "retrievingMemories": "正在检索记忆...", - "deletingMemory": "正在删除记忆...", - "updatingMemory": "正在更新记忆...", - "listingMemories": "正在列出记忆...", - "memoryStored": "记忆已存储。", - "memoryUpdated": "记忆已更新。", - "memoryDeleted": "记忆已删除。", - "memoryId": "记忆ID", - "type": "类型", - "content": "内容", - "newContent": "新内容", - "query": "查询", - "limit": "限制", - "foundMemories": "找到 {{count}} 条记忆。", - "noMemoriesFound": "未找到相关记忆。", - "noRelevantMemories": "未找到相关记忆", - "storedMemoryDetails": "存储的记忆详情", - "updatedMemoryDetails": "更新的记忆详情", - "deletedMemoryDetails": "删除的记忆详情", - "retrievedMemories": "检索到的记忆", - "memoryList": "记忆列表", - "listedMemories": "正在列出记忆。", - "ofType": "类型为 {{type}}", - "noMemoriesOfType": "未找到类型为 {{type}} 的记忆", - "all": "全部", - "delete": "删除", - "memoryNotFound": "未找到", - "deleteDialogTitle": "删除记忆", - "deleteDialogText": "确定要删除这条记忆吗?" - }, - "subagents": { - "groupRunning": "{{name}}: 子代理正在工作...", - "groupCompleted": "{{name}}: 子代理已完成任务。", - "running": "正在运行子代理...", - "completed": "子代理执行了以下查询:", - "prompt": "提示" - }, - "errors": { - "noSuchTool": "尝试使用未知工具: {{toolName}}", - "invalidToolArguments": "工具参数无效: {{toolName}}" - } - }, - "codeBlock": { - "revertChanges": "撤销更改" - }, - "diffViewer": { - "sideBySide": "并排", - "unified": "统一", - "compact": "紧凑", - "noChanges": "没有进行任何更改。内容相同。" - }, - "promptField": { - "savePrompt": "保存提示", - "savePromptDescription": "保存提示而不执行", - "saveError": "保存提示失败: {{error}}", - "autoApprove": "自动批准", - "autoApproveTooltip": "启用后,自动批准工具执行而无需请求确认", - "autoApproveLockTooltip": "锁定后,新建任务将保持自动批准开启", - "placeholders": { - "0": "更新 UserValidator 类以添加邮箱格式验证...", - "1": "修复 DataProcessor.processBatch() 中的内存泄漏,通过关闭...", - "2": "为 PaymentService.chargeCard() 添加错误处理,使用适当的...", - "3": "创建 React 组件 UserProfileCard,包含 props 接口...", - "4": "通过添加索引来优化 UserRepository.findByEmail() 中的 SQL 查询...", - "5": "使用 Jest 和模拟为 AuthController.login() 添加单元测试...", - "6": "在 ChartComponent 中实现缺失的 handleResize() 方法...", - "7": "更新 package.json 添加 TypeScript 依赖并配置...", - "8": "为 ContactForm 的 handleSubmit 函数添加输入验证...", - "9": "修复 calculateTotal() 中小数值被错误处理的 bug...", - "10": "使用 Winston 为 API 中间件添加请求日志记录...", - "11": "为 Node.js 应用创建多阶段构建的 Dockerfile...", - "12": "使用 express-rate-limit 为 /api/users 端点添加限流中间件...", - "13": "在 TodoService 中实现缺失的 deleteTodo() 函数...", - "14": "为 API 响应对象添加 TypeScript 接口...", - "15": "修复移动端菜单显示不正确的 CSS 问题...", - "16": "使用 dotenv 和 zod 添加环境变量验证...", - "17": "创建数据库迁移,为 users 表添加 'last_login' 列...", - "18": "为 React 应用添加错误边界以捕获组件错误...", - "19": "在 ProductList 的 fetchProducts 方法中实现分页逻辑..." - }, - "questionPlaceholder": "...或建议其他内容", - "answers": { - "yes": "(Y)是", - "no": "(N)否", - "always": "(A)总是", - "dontAsk": "(D)不再询问" - }, - "invalidCommand": "无效命令", - "stopResponse": "停止响应", - "sendMessage": "发送消息(Enter)", - "clearChat": "清除聊天", - "agentModeOnly": "仅在代理模式下可用" - }, - "agentProfiles": { - "title": "代理配置", - "profileSettings": "代理配置: {{profileName}}", - "newProfileName": "新配置", - "manageProfiles": "管理配置", - "createNew": "创建新配置", - "profileName": "名称", - "profileDescription": "描述", - "profileDescriptionPlaceholder": "描述此代理配置(用途、工具等)", - "tools": "工具", - "model": "模型", - "selectProfile": "选择...", - "saveAsProjectDefault": "保存为项目默认" - }, - "projectBar": { - "editFormatTooltip": "这是用于编辑代码的格式。更改它可能会导致模型表现不如预期。", - "aider": "Aider", - "toggleSidebar": "切换侧边栏", - "modelLibrary": "模型库", - "worktree": "工作树", - "worktreeTooltip": "切换此任务的工作树" - }, - "workingMode": { - "local": "本地", - "worktree": "工作树", - "switching": "切换中...", - "confirmLocalTitle": "切换到本地模式", - "confirmLocalMessage": "警告:此工作树有未保存的工作:\n{{warnings}}\n\n删除工作树将删除这些工作。如果您想保留这些工作,请先使用合并操作将其合并到主分支。\n\n您确定要继续并删除工作树吗?", - "confirmLocalAction": "删除并切换" - }, - "workingModeTooltip": { - "local": "在此任务中直接在本地项目目录中进行更改", - "worktree": "使用 git 工作树为此任务进行隔离的更改" - }, - "reasoningEffort": { - "label": "推理工作量", - "tooltip": "推理工作量决定模型在采取行动前的思考程度。高推理工作量意味着模型会在采取行动前思考更多,但响应时间也会更长。", - "none": "无", - "minimal": "最小", - "low": "低", - "medium": "中", - "high": "高", - "max": "最大" - }, - "commands": { - "add": "将文件添加到上下文。", - "agent": "切换到代理模式。如果已在代理模式下,则打开代理模型选择器。命令后的任何文本都将用作提示。", - "architect": "切换到架构师模式。架构师模式用于规划和执行代码库的更改。", - "ask": "切换到提问模式。在不编辑任何文件的情况下询问有关代码库的问题。", - "clear": "清除聊天历史记录", - "clear-logs": "从聊天中移除日志消息", - "code": "切换到代码模式。请求更改您的代码。", - "commit": "提交仓库中未暂存的更改", - "context": "切换到上下文模式。请求更改,Aider 将添加相关文件到上下文。", - "compact": "总结对话", - "copy-context": "将当前聊天上下文复制为 Markdown 格式,适合粘贴到 Web UI 中。", - "drop": "从上下文中删除文件以释放上下文空间。", - "edit-last": "编辑最后一条用户消息", - "init": "使用项目信息初始化 AGENTS.md 规则文件", - "agentModeOnly": "仅在代理模式下可用", - "map": "打印当前仓库地图", - "map-refresh": "强制刷新仓库地图", - "model": "打开模型选择器", - "read-only": "将文件添加到聊天中,这些文件仅供参考,或将添加的文件设为只读。", - "redo": "重做上一个用户提示。", - "reasoning-effort": "设置推理工作量级别(值:数字或低/中/高,取决于模型)", - "reset": "删除所有文件并清除聊天历史记录", - "run": "运行 shell 命令并可选地将输出添加到聊天中", - "test": "运行预定义的测试命令,并在非零退出代码时将输出添加到聊天中", - "think-tokens": "设置思考令牌预算(支持 8096、8k、10.5k、0.5M 等格式)", - "tokens": "报告当前聊天上下文使用的令牌数量", - "undo": "撤消 Aider 完成的最后一次 git 提交", - "task-info": "显示任务信息", - "web": "抓取网页内容并保存到文件。使用 /web url [文件] 格式,其中 [文件] 为可选参数" - }, - "telemetry": { - "title": "遥测信息", - "message": "为了帮助我们改进 AiderDesk,我们会收集匿名遥测数据。这包括功能使用情况和性能指标等信息。绝不会收集个人数据或代码。", - "enabledLabel": "启用匿名遥测" - }, - "promptBehavior": { - "title": "提示行为", - "showSuggestions": "显示建议", - "automaticallyWhileTyping": "输入时自动显示", - "onlyWhenTabPressed": "仅在按 TAB 键时显示", - "suggestionsDelay": "建议延迟", - "requireCommandConfirmation": "需要命令确认", - "addCommand": "/add", - "readOnlyCommand": "/read-only", - "modelCommand": "/model", - "modeSwitchingCommands": "模式切换命令 (/code, /agent, ...)", - "modeAtSign": "@ 提及模式" - }, - "server": { - "description": "启用 服务器,允许您通过浏览器在端口 24337 访问 AiderDesk", - "serverStatus": "服务器状态", - "running": "运行中", - "enableBasicAuth": "启用基本身份验证", - "enableBasicAuthDescription": "需要用户名和密码用于浏览器访问和 API 调用", - "username": "用户名", - "password": "密码", - "controlServer": "REST API 服务器", - "starting": "正在启动...", - "stopping": "正在停止...", - "start": "启动", - "stop": "停止", - "stopped": "已停止", - "tunnelStatus": "隧道状态", - "tunnelRunning": "运行中", - "tunnelStopped": "已停止", - "tunnelStarting": "正在启动...", - "tunnelStopping": "正在停止...", - "copyUrl": "复制 URL", - "authentication": "身份验证", - "serverControl": "服务器控制", - "tunnelManagement": "隧道管理", - "tunnelDescription": "创建 Cloudflare 隧道以安全地从外部网络访问您的服务器。当对外暴露服务器时,请务必使用身份验证。" - }, - "multiselect": { - "noneSelected": "未选择", - "allSelected": "已全选", - "someSelected": "已选择 {{count}} 个", - "selectAll": "全选" - }, - "usageDashboard": { - "title": "使用情况", - "refresh": "刷新", - "projects": "项目", - "models": "模型", - "dateRange": "日期范围", - "total": "总计", - "thisMonth": "本月", - "today": "今天", - "all": "全部", - "groupBy": "分组方式", - "periods": { - "hour": "小时", - "day": "天", - "month": "月", - "year": "年" - }, - "tabs": { - "table": "表格视图", - "charts": "图表视图" - }, - "charts": { - "tokenUsageTrend": "令牌使用趋势", - "costBreakdown": "成本明细", - "messageBreakdown": "消息明细", - "modelUsageDistribution": "模型使用分布", - "inputTokens": "输入令牌", - "outputTokens": "输出令牌", - "noData": "所选时间段内无数据", - "tokens": "令牌", - "cost": "成本", - "share": "份额", - "moreItems": "+{{count}} 更多" - }, - "error": { - "fetch": "获取使用数据失败。" - }, - "table": { - "date": "日期", - "project": "项目", - "model": "模型", - "input": "输入", - "output": "输出", - "cacheRead": "缓存读取", - "cacheWrite": "缓存写入", - "totalTokens": "总令牌数", - "cost": "成本" - } - }, - "terminal": { - "copyOutput": "复制终端输出到提示", - "minimize": "最小化终端", - "addTerminal": "添加新终端" - }, - "contextMenu": { - "copy": "复制", - "paste": "粘贴", - "close": "关闭", - "closeOtherTabs": "关闭其他标签页", - "closeAllTabs": "关闭所有标签页" - }, - "taskSidebar": { - "title": "任务", - "noTasks": "没有可用的任务", - "deleteTask": "删除任务", - "deleteConfirm": "您确定要删除这个任务吗?", - "createTask": "创建新任务", - "rename": "重命名", - "pinTask": "固定任务", - "unpinTask": "取消固定任务", - "taskNamePlaceholder": "输入任务名称...", - "untitled": "[未命名]", - "exportAsMarkdown": "导出为 Markdown", - "exportAsImage": "导出为图片", - "taskIdCopied": "任务 ID {{taskId}} 已复制到剪贴板", - "duplicateTask": "复制任务", - "search": "搜索任务", - "searchPlaceholder": "搜索任务...", - "archiveTask": "归档任务", - "unarchiveTask": "取消归档任务", - "showArchived": "显示已归档", - "hideArchived": "隐藏已归档", - "archived": "已归档", - "multiselectMode": "多选模式", - "selectedCount": "已选择 {{count}} 个", - "closeMultiselect": "退出多选", - "deleteSelected": "删除选中项", - "archiveSelected": "归档选中项", - "unarchiveSelected": "取消归档选中项", - "deleteSelectedConfirm": "您确定要删除 {{count}} 个选中的任务吗?", - "archiveSelectedConfirm": "您确定要归档 {{count}} 个选中的任务吗?", - "unarchiveSelectedConfirm": "您确定要取消归档 {{count}} 个选中的任务吗?", - "generatingName": "正在生成名称..." - }, - "taskInfo": { - "createdAt": "创建于", - "updatedAt": "更新于", - "worktreeDirectory": "工作树目录", - "baseBranch": "基础分支", - "baseCommit": "基础提交", - "targetBranch": "目标分支", - "lastMergeState": "最后合并状态", - "beforeMergeCommit": "合并前提交", - "mergeTimestamp": "合并时间戳", - "openFolder": "在文件夹中打开", - "taskId": "任务 ID", - "state": "状态", - "messageCount": "消息数量" - }, - "taskState": { - "TODO": "待办", - "IN_PROGRESS": "进行中", - "INTERRUPTED": "已中断", - "MORE_INFO_NEEDED": "需要更多信息", - "READY_FOR_REVIEW": "待审核", - "READY_FOR_IMPLEMENTATION": "待实现", - "DONE": "已完成" - }, - "worktree": { - "aheadCommits": "↑ {{count}} 次提交", - "uncommittedFiles": "{{count}} 个文件", - "conflicts": "冲突", - "conflictsPredicted": "预计会发生冲突", - "conflictsPresent": "存在冲突", - "merge": "合并", - "squash": "压缩合并", - "onlyUncommitted": "仅未提交的更改", - "rebaseFromBranch": "从分支变基", - "abortRebase": "中止变基", - "continueRebase": "继续变基", - "rebaseConflicts": "变基由于冲突失败。请手动解决冲突或使用 AI 代理协助。", - "confirmRebaseTitle": "变基工作树", - "confirmRebaseMessage": "这将把工作树变基到所选分支上。如有冲突,需要你进行解决。", - "confirmAbortRebaseTitle": "中止变基", - "confirmAbortRebaseMessage": "这将中止正在进行的变基并把工作树恢复到变基前的状态。", - "confirmContinueRebaseTitle": "继续变基", - "confirmContinueRebaseMessage": "这将尝试继续变基。请确保所有冲突已解决并已暂存。", - "resolveConflictsWithAgent": "使用智能体解决冲突", - "confirmResolveConflictsWithAgentTitle": "使用智能体解决冲突", - "confirmResolveConflictsWithAgentMessage": "这将尝试自动解决所有冲突文件并将其暂存。继续变基前请仔细检查结果。", - "commitMessage": "提交说明", - "commitMessagePlaceholder": "输入提交说明...", - "selectBranch": "选择分支", - "selectTargetBranchTitle": "选择目标分支", - "selectSourceBranchTitle": "选择源分支", - "branchHasWorktree": "已有工作树", - "mergeTooltip": "将工作树更改合并到分支", - "revertTooltip": "撤销最后一次合并的更改", - "confirmMerge": "您确定要{{action}}工作树更改到主分支吗?这将包含任何未提交的更改。", - "confirmRevert": "您确定要撤销最后一次合并吗?这将把主分支重置到之前的状态。", - "confirmMergeTitle": "确认合并", - "confirmSquashTitle": "确认压缩合并", - "confirmOnlyUncommittedTitle": "确认应用未提交的更改", - "confirmMergeMessage": "您确定要将工作树更改合并到所选分支吗?", - "confirmSquashMessage": "您确定要压缩并合并工作树更改到所选分支吗?", - "confirmOnlyUncommittedMessage": "您确定要将工作树中的未提交更改应用到所选分支吗?不会执行 git 合并操作。", - "mergeConflicts": "由于工作树与目标分支之间存在冲突,合并失败。请先对工作树进行变基以解决冲突。", - "applyUncommittedConflicts": "由于与目标分支存在冲突,无法应用未提交的更改。请先对工作树进行变基以对齐您的更改。", - "dontAskAgain": "不再询问", - "confirmRevertTitle": "确认撤销", - "confirmRevertMessage": "您确定要撤销最后一次合并吗?这将撤销从工作树合并回主分支的更改。", - "revert": "撤销", - "noChanges": "没有更改" - }, - "welcomeMessage": { - "title": "AiderDesk", - "subtitle": "您的 AI 驱动编程助手", - "features": { - "title": "您可以做什么:", - "aiCoding": "使用 AI 辅助生成和修改代码", - "contextManagement": "添加文件到上下文以获得更好的结果", - "multiModel": "选择多种 AI 模型和提供商", - "modes": "在代理和 Aider 模式之间切换", - "commands": "执行命令和运行测试" - }, - "tips": { - "title": "入门指南:", - "addFiles": "使用侧边栏或 /add 命令添加文件到上下文", - "askQuestion": "在下方的提示字段中输入您的请求", - "useCommands": "使用 / 命令进行快速操作", - "switchMode": "尝试代理模式以实现自主任务完成" } } } diff --git a/src/common/types.ts b/src/common/types.ts index 766ea92fc..c51266ef6 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -595,6 +595,9 @@ export interface SettingsData { }; memory: MemoryConfig; taskSettings: TaskSettings; + terminal: { + shell: string; + }; hotkeyConfig?: HotkeyConfig; } @@ -898,3 +901,8 @@ export interface BranchInfo { isCurrent: boolean; hasWorktree: boolean; } + +export interface ShellInfo { + path: string; + name: string; +} diff --git a/src/main/events-handler.ts b/src/main/events-handler.ts index 94c01d985..573d32275 100644 --- a/src/main/events-handler.ts +++ b/src/main/events-handler.ts @@ -46,6 +46,7 @@ import { getDefaultProjectSettings, getEffectiveEnvironmentVariable, getFilePath import { AIDER_DESK_TMP_DIR, LOGS_DIR } from '@/constants'; import { EventManager } from '@/events'; import { isElectron } from '@/app'; +import { getAvailableShells, ShellInfo } from '@/utils/shell'; export class EventsHandler { constructor( @@ -912,4 +913,8 @@ export class EventsHandler { getMemoryEmbeddingProgress() { return this.memoryManager.getProgress(); } + + getAvailableShells(): ShellInfo[] { + return getAvailableShells(); + } } diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index a44064ce8..875efdda4 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -460,4 +460,8 @@ export const setupIpcHandlers = (eventsHandler: EventsHandler, serverController: ipcMain.handle('clipboard-write-text', async (_, text: string) => { clipboard.writeText(text); }); + + ipcMain.handle('get-available-shells', async () => { + return eventsHandler.getAvailableShells(); + }); }; diff --git a/src/main/managers.ts b/src/main/managers.ts index 626308d34..ad0762c80 100644 --- a/src/main/managers.ts +++ b/src/main/managers.ts @@ -80,7 +80,7 @@ export const initManagers = async (store: Store, mainWindow: BrowserWindow | nul ); // Initialize terminal manager - const terminalManager = new TerminalManager(eventManager, worktreeManager, telemetryManager); + const terminalManager = new TerminalManager(eventManager, worktreeManager, store, telemetryManager); // Initialize Versions Manager const versionsManager = new VersionsManager(eventManager, store); diff --git a/src/main/server/rest-api/settings-api.ts b/src/main/server/rest-api/settings-api.ts index 76b0dc0fd..033dfe380 100644 --- a/src/main/server/rest-api/settings-api.ts +++ b/src/main/server/rest-api/settings-api.ts @@ -48,6 +48,15 @@ export class SettingsApi extends BaseApi { }), ); + // Get available shells + router.get( + '/settings/available-shells', + this.handleRequest(async (_, res) => { + const shells = this.eventsHandler.getAvailableShells(); + res.status(200).json(shells); + }), + ); + // Update settings router.post( '/settings', diff --git a/src/main/store/migrations/v17-to-v18.ts b/src/main/store/migrations/v17-to-v18.ts new file mode 100644 index 000000000..f6030a11a --- /dev/null +++ b/src/main/store/migrations/v17-to-v18.ts @@ -0,0 +1,10 @@ +import { SettingsData } from '@common/types'; + +export const migrateSettingsV17toV18 = async (settings: SettingsData): Promise => { + if (!settings.terminal) { + settings.terminal = { + shell: '', + }; + } + return settings; +}; diff --git a/src/main/store/store.ts b/src/main/store/store.ts index 7d29b3ae1..91e138aec 100644 --- a/src/main/store/store.ts +++ b/src/main/store/store.ts @@ -32,6 +32,7 @@ import { migrateProvidersV13toV14 } from '@/store/migrations/v13-to-v14'; import { migrateSettingsV14toV15 } from '@/store/migrations/v14-to-v15'; import { migrateSettingsV15toV16 } from '@/store/migrations/v15-to-v16'; import { migrateSettingsV16toV17 } from '@/store/migrations/v16-to-v17'; +import { migrateSettingsV17toV18 } from '@/store/migrations/v17-to-v18'; export const DEFAULT_SETTINGS: SettingsData = { language: 'en', @@ -87,6 +88,9 @@ export const DEFAULT_SETTINGS: SettingsData = { autoGenerateTaskName: true, showTaskStateActions: true, }, + terminal: { + shell: '', + }, }; const compareBaseDirs = (baseDir1: string, baseDir2: string): boolean => { @@ -104,7 +108,7 @@ interface StoreSchema { userId?: string; } -const CURRENT_SETTINGS_VERSION = 17; +const CURRENT_SETTINGS_VERSION = 18; export class Store { // @ts-expect-error expected to be initialized @@ -172,6 +176,10 @@ export class Store { ...DEFAULT_SETTINGS.taskSettings, ...settings?.taskSettings, }, + terminal: { + ...DEFAULT_SETTINGS.terminal, + ...settings?.terminal, + }, }; } @@ -268,6 +276,11 @@ export class Store { settingsVersion = 17; } + if (settingsVersion === 17) { + settings = await migrateSettingsV17toV18(settings); + settingsVersion = 18; + } + this.store.set('settings', settings as SettingsData); this.store.set('openProjects', openProjects || []); this.store.set('providers', providers); diff --git a/src/main/terminal/terminal-manager.ts b/src/main/terminal/terminal-manager.ts index 67ad77dc7..bc867787c 100644 --- a/src/main/terminal/terminal-manager.ts +++ b/src/main/terminal/terminal-manager.ts @@ -1,4 +1,5 @@ import * as os from 'os'; +import * as path from 'path'; import * as pty from '@homebridge/node-pty-prebuilt-multiarch'; import { v4 as uuidv4 } from 'uuid'; @@ -7,6 +8,7 @@ import logger from '@/logger'; import { TelemetryManager } from '@/telemetry'; import { EventManager } from '@/events'; import { WorktreeManager } from '@/worktrees/worktree-manager'; +import { Store } from '@/store'; export interface TerminalInstance { id: string; @@ -23,10 +25,16 @@ export class TerminalManager { constructor( private readonly eventManager: EventManager, private readonly worktreeManager: WorktreeManager, + private readonly store: Store, private readonly telemetryManager?: TelemetryManager, ) {} private getShellCommand(): string { + const settings = this.store.getSettings(); + if (settings.terminal?.shell) { + return settings.terminal.shell; + } + const platform = os.platform(); if (platform === 'win32') { @@ -39,14 +47,15 @@ export class TerminalManager { } private getShellArgs(): string[] { - const platform = os.platform(); + const shell = this.getShellCommand(); + const shellName = path.basename(shell).toLowerCase(); - if (platform === 'win32') { - const shell = this.getShellCommand(); - if (shell.includes('powershell')) { - return ['-NoLogo']; - } - return []; + if (shellName.includes('powershell') || shellName.includes('pwsh')) { + return ['-NoLogo']; + } + + if (shellName === 'bash' || shellName === 'zsh' || shellName === 'fish' || shellName === 'sh') { + return ['-i']; } return []; diff --git a/src/main/utils/shell.ts b/src/main/utils/shell.ts index ad9e5e87c..62b3a3fad 100644 --- a/src/main/utils/shell.ts +++ b/src/main/utils/shell.ts @@ -12,10 +12,9 @@ const execAsync = promisify(exec); let cachedPath: string | null = null; let isFirstCall: boolean = true; -interface ShellInfo { +export interface ShellInfo { path: string; name: string; - args?: string[]; } class ShellDetector { @@ -74,7 +73,7 @@ class ShellDetector { const envShell = process.env.SHELL; if (envShell && fs.existsSync(envShell)) { const name = path.basename(envShell); - return { path: envShell, name, args: this.getShellArgs(name) }; + return { path: envShell, name }; } // On macOS, try to get the default shell from Directory Services @@ -87,7 +86,7 @@ class ShellDetector { const shellPath = match[1].trim(); if (fs.existsSync(shellPath)) { const name = path.basename(shellPath); - return { path: shellPath, name, args: this.getShellArgs(name) }; + return { path: shellPath, name }; } } } catch { @@ -105,7 +104,7 @@ class ShellDetector { const shellPath = parts[6]; if (shellPath && fs.existsSync(shellPath)) { const name = path.basename(shellPath); - return { path: shellPath, name, args: this.getShellArgs(name) }; + return { path: shellPath, name }; } } } catch { @@ -129,12 +128,12 @@ class ShellDetector { for (const shellPath of commonShells) { if (fs.existsSync(shellPath)) { const name = path.basename(shellPath); - return { path: shellPath, name, args: this.getShellArgs(name) }; + return { path: shellPath, name }; } } // Last resort - use sh - return { path: '/bin/sh', name: 'sh', args: ['-i'] }; + return { path: '/bin/sh', name: 'sh' }; } private static findExecutable(name: string): string | null { @@ -156,22 +155,6 @@ class ShellDetector { return null; } - private static getShellArgs(shellName: string): string[] { - // Return appropriate arguments for interactive shell sessions - switch (shellName) { - case 'bash': - case 'sh': - case 'zsh': - case 'fish': - return ['-i']; // Interactive mode - case 'pwsh': - case 'powershell': - return ['-NoExit']; // Keep PowerShell open - default: - return []; - } - } - /** * Get shell-specific command execution arguments * @param command The command to execute @@ -205,6 +188,72 @@ class ShellDetector { return false; } } + + /** + * Get all available shells on the system + * @returns Array of available shells + */ + static getAvailableShells(): ShellInfo[] { + const shells: ShellInfo[] = []; + const platform = process.platform; + + if (platform === 'win32') { + const pwshPath = this.findExecutable('pwsh.exe'); + if (pwshPath) shells.push({ path: pwshPath, name: 'pwsh' }); + + const powershellPath = this.findExecutable('powershell.exe'); + if (powershellPath) shells.push({ path: powershellPath, name: 'powershell' }); + + const cmdPath = path.join(process.env.SYSTEMROOT || 'C:\\Windows', 'System32', 'cmd.exe'); + if (fs.existsSync(cmdPath)) shells.push({ path: cmdPath, name: 'cmd' }); + + // Git Bash + const gitBashPath = path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'); + if (fs.existsSync(gitBashPath)) shells.push({ path: gitBashPath, name: 'git-bash' }); + } else { + const commonShells = [ + '/usr/local/bin/zsh', + '/bin/zsh', + '/usr/bin/zsh', + '/usr/local/bin/fish', + '/usr/bin/fish', + '/usr/local/bin/bash', + '/bin/bash', + '/usr/bin/bash', + '/bin/sh', + '/usr/bin/sh', + ]; + + const seen = new Set(); + for (const shellPath of commonShells) { + if (fs.existsSync(shellPath) && !seen.has(shellPath)) { + const name = path.basename(shellPath); + shells.push({ path: shellPath, name }); + seen.add(shellPath); + } + } + + // Also check /etc/shells + try { + if (fs.existsSync('/etc/shells')) { + const content = fs.readFileSync('/etc/shells', 'utf8'); + const lines = content.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#') && fs.existsSync(trimmed) && !seen.has(trimmed)) { + const name = path.basename(trimmed); + shells.push({ path: trimmed, name }); + seen.add(trimmed); + } + } + } + } catch (error) { + logger.warn('Failed to read /etc/shells, continuing without it.', { error: error instanceof Error ? error.message : String(error) }); + } + } + + return shells; + } } /** @@ -629,6 +678,10 @@ export const findExecutableInPath = (executable: string): string | null => { return null; }; +export const getAvailableShells = (): ShellInfo[] => { + return ShellDetector.getAvailableShells(); +}; + // Wrapper for execAsync that includes enhanced PATH export const execWithShellPath = async (command: string, options?: { cwd?: string; env?: NodeJS.ProcessEnv }): Promise<{ stdout: string; stderr: string }> => { const shellPath = getShellPath(); diff --git a/src/preload/index.ts b/src/preload/index.ts index 01d2b0e24..5df43f535 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -538,6 +538,7 @@ const api: ApplicationAPI = { // Terminal operations isTerminalSupported: () => true, + getAvailableShells: () => ipcRenderer.invoke('get-available-shells'), createTerminal: (baseDir, taskId, cols, rows) => ipcRenderer.invoke('terminal-create', baseDir, taskId, cols, rows), writeToTerminal: (terminalId, data) => ipcRenderer.invoke('terminal-write', terminalId, data), resizeTerminal: (terminalId, cols, rows) => ipcRenderer.invoke('terminal-resize', terminalId, cols, rows), diff --git a/src/renderer/src/__tests__/mocks/api.ts b/src/renderer/src/__tests__/mocks/api.ts index f08d246c9..e3eb3de99 100644 --- a/src/renderer/src/__tests__/mocks/api.ts +++ b/src/renderer/src/__tests__/mocks/api.ts @@ -22,6 +22,7 @@ import { AgentProfile, MemoryEntry, MemoryEmbeddingProgress, + ShellInfo, } from '@common/types'; /** @@ -194,6 +195,7 @@ export const createMockApi = (overrides: Partial = {}): MockedOb // Terminal operations isTerminalSupported: vi.fn((): boolean => true), + getAvailableShells: vi.fn((): Promise => Promise.resolve([])), createTerminal: vi.fn((): Promise => Promise.resolve('mock-terminal-id')), writeToTerminal: vi.fn((): Promise => Promise.resolve(true)), resizeTerminal: vi.fn((): Promise => Promise.resolve(true)), diff --git a/src/renderer/src/api/browser-api.ts b/src/renderer/src/api/browser-api.ts index 2f6ae9ca0..78df01b29 100644 --- a/src/renderer/src/api/browser-api.ts +++ b/src/renderer/src/api/browser-api.ts @@ -45,6 +45,7 @@ import { MemoryEmbeddingProgress, WorktreeIntegrationStatus, WorktreeIntegrationStatusUpdatedData, + ShellInfo, } from '@common/types'; import { ApplicationAPI } from '@common/api'; import axios, { type AxiosInstance } from 'axios'; @@ -708,6 +709,9 @@ export class BrowserApi implements ApplicationAPI { isTerminalSupported(): boolean { return false; } + getAvailableShells(): Promise { + return this.get('/settings/available-shells'); + } createTerminal(baseDir: string, taskId: string, cols?: number, rows?: number): Promise { void baseDir; void taskId; diff --git a/src/renderer/src/components/settings/TerminalSettings.tsx b/src/renderer/src/components/settings/TerminalSettings.tsx new file mode 100644 index 000000000..4ad662f08 --- /dev/null +++ b/src/renderer/src/components/settings/TerminalSettings.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SettingsData, ShellInfo } from '@common/types'; +import { Section } from '@/components/common/Section'; +import { Input } from '@/components/common/Input'; +import { RadioButton } from '@/components/common/RadioButton'; + +type Props = { + settings: SettingsData; + setSettings: (settings: SettingsData) => void; +}; + +type ShellMode = 'auto' | 'preset' | 'custom'; + +export const TerminalSettings = ({ settings, setSettings }: Props) => { + const { t } = useTranslation(); + const [availableShells, setAvailableShells] = useState([]); + const [mode, setMode] = useState('auto'); + + useEffect(() => { + window.api.getAvailableShells().then(setAvailableShells); + }, []); + + const currentShell = settings.terminal?.shell || ''; + + // Sync mode with settings + useEffect(() => { + if (currentShell === '') { + setMode('auto'); + } else if (availableShells.some((s) => s.path === currentShell)) { + setMode('preset'); + } else { + setMode('custom'); + } + }, [currentShell, availableShells]); + + const handleModeChange = (newMode: ShellMode) => { + setMode(newMode); + if (newMode === 'auto') { + updateShell(''); + } else if (newMode === 'preset') { + // If current shell is not in presets, default to first available + if (!availableShells.some((s) => s.path === currentShell)) { + updateShell(availableShells[0]?.path || ''); + } + } else if (newMode === 'custom') { + // Keep current value, user can edit it + } + }; + + const updateShell = (path: string) => { + setSettings({ + ...settings, + terminal: { + ...settings.terminal, + shell: path, + }, + }); + }; + + return ( +
+
+
+
+ {/* Auto Detect */} + handleModeChange('auto')} + label={ +
+ {t('settings.terminal.autoDetect')} +

+ {t('settings.terminal.autoDetectDescription', 'Automatically use the system default shell')} +

+
+ } + /> + + {/* Preset List */} +
+ handleModeChange('preset')} + label={{t('settings.terminal.selectFromList', 'Select from list')}} + /> + + {mode === 'preset' && ( +
+ +
+ )} +
+ + {/* Custom Path */} +
+ handleModeChange('custom')} + label={{t('settings.terminal.customPath', 'Custom path')}} + /> + + {mode === 'custom' && ( +
+ updateShell(e.target.value)} + placeholder={t('settings.terminal.customShellPathPlaceholder')} + /> +

{t('settings.terminal.customShellPathDescription')}

+
+ )} +
+
+
+
+
+ ); +}; diff --git a/src/renderer/src/pages/Settings.tsx b/src/renderer/src/pages/Settings.tsx index a568d62d3..ea193748e 100644 --- a/src/renderer/src/pages/Settings.tsx +++ b/src/renderer/src/pages/Settings.tsx @@ -17,6 +17,7 @@ import { MemorySettings } from '@/components/settings/MemorySettings'; import { VoiceSettings } from '@/components/settings/VoiceSettings'; import { HotkeysSettings } from '@/components/settings/HotkeysSettings'; import { TaskSettings } from '@/components/settings/TaskSettings'; +import { TerminalSettings } from '@/components/settings/TerminalSettings'; type Props = { settings: SettingsData; @@ -35,7 +36,7 @@ type Props = { setProviders?: (providers: ProviderProfile[]) => void; }; -type PageId = 'general' | 'aider' | 'agents' | 'tasks' | 'memory' | 'voice' | 'hotkeys' | 'server' | 'about'; +type PageId = 'general' | 'aider' | 'agents' | 'tasks' | 'terminal' | 'memory' | 'voice' | 'hotkeys' | 'server' | 'about'; interface SidebarItem { id: string; @@ -129,6 +130,12 @@ export const Settings = ({ label: t('settings.tabs.tasks'), icon: , }, + { + id: 'terminal', + pageId: 'terminal', + label: t('settings.tabs.terminal'), + icon: , + }, { id: 'memory', pageId: 'memory', @@ -248,6 +255,8 @@ export const Settings = ({ return ; case 'tasks': return ; + case 'terminal': + return ; case 'voice': return ; case 'hotkeys':