From ca570b84381c5c97c6569b72553822a11d0de6ca Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Fri, 7 Aug 2026 16:42:41 +0800 Subject: [PATCH] feat(agents): choose working directory for Pi and Codex CLI --- src-tauri/src/main.rs | 41 +++++++++++- src/i18n/ja.ts | 6 +- src/i18n/resources.ts | 12 ++-- src/pages/AgentsPage.tsx | 137 +++++++++++++++++++++++---------------- 4 files changed, 128 insertions(+), 68 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 640dccd..8824e57 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1953,6 +1953,29 @@ async fn uninstall_pi_provider( Ok(result) } +fn validate_agent_working_directory(value: &str) -> Result { + let value = value.trim(); + if value.is_empty() { + return Err( + "AGENT_WORKING_DIRECTORY_REQUIRED: Please select a working directory".to_string(), + ); + } + if value.chars().any(char::is_control) { + return Err("The working directory contains invalid characters".to_string()); + } + let path = PathBuf::from(value); + if !path.is_absolute() { + return Err("The working directory must be an absolute path".to_string()); + } + if !path.is_dir() { + return Err(format!( + "AGENT_WORKING_DIRECTORY_INVALID: The working directory does not exist: {}", + path_to_string(&path) + )); + } + Ok(path) +} + fn validate_claude_code_working_directory(value: &str) -> Result { let value = value.trim(); if value.is_empty() { @@ -2746,7 +2769,12 @@ async fn launch_agent( } let executable = find_pi_executable(&home).ok_or_else(|| "未找到 Pi CLI 可执行文件".to_string())?; - return launch_cli_agent(&executable, PI_AGENT_NAME, &home, &[], &[]); + let launch_directory = working_directory + .as_deref() + .map(validate_agent_working_directory) + .transpose()? + .unwrap_or_else(|| home.clone()); + return launch_cli_agent(&executable, PI_AGENT_NAME, &launch_directory, &[], &[]); } let client = AgentClient::parse(&client)?; if !client.supported_platform() { @@ -2889,7 +2917,16 @@ async fn launch_agent( &environment_to_remove, ); } - launch_cli_agent(&executable, client.name(), &home, &[], &[]) + let launch_directory = if client == AgentClient::Codex && requested_target == Some("cli") { + working_directory + .as_deref() + .map(validate_agent_working_directory) + .transpose()? + .unwrap_or_else(|| home.clone()) + } else { + home.clone() + }; + launch_cli_agent(&executable, client.name(), &launch_directory, &[], &[]) } fn validate_agent_launch_modification( diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 3ecd065..3d69e62 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -584,13 +584,13 @@ export const jaOverrides = { 'agents.claudeCodeLaunch.chooseDirectory': 'ディレクトリを選択', 'agents.claudeCodeLaunch.selectedDirectory': '選択した作業ディレクトリ', 'agents.claudeCodeLaunch.choosing': '選択中', - 'agents.claudeCodeLaunch.dialogTitle': 'Claude Code の作業ディレクトリを選択', - 'agents.claudeCodeLaunch.dialogDescription': '環境変数を設定して Claude Code を起動する前に、作業ディレクトリを選択してください。', + 'agents.claudeCodeLaunch.dialogTitle': '{client} の起動ディレクトリを選択', + 'agents.claudeCodeLaunch.dialogDescription': '{client} を起動する前に、作業ディレクトリを選択してください。', 'agents.claudeCodeLaunch.workingDirectory': '作業ディレクトリ', 'agents.claudeCodeLaunch.browse': '参照', 'agents.claudeCodeLaunch.neverAskAgain': '今後この確認を表示しない', 'agents.claudeCodeLaunch.directoryRequired': '先に作業ディレクトリを選択してください', - 'agents.claudeCodeLaunch.launch': 'Claude Code を起動', + 'agents.claudeCodeLaunch.launch': '{client} を起動', 'agents.oauthLoginRequired.title': 'Codex へのログインが必要です', 'agents.oauthLoginRequired.enableDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定方式を有効にできません。Codex アプリで ChatGPT アカウントにログインしてください。Codex アプリでログインを開始できない場合は、先に', 'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 35f914d..81d4d2a 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -582,13 +582,13 @@ export const zhCN = { 'agents.claudeCodeLaunch.chooseDirectory': '选择目录', 'agents.claudeCodeLaunch.selectedDirectory': '已选择的工作目录', 'agents.claudeCodeLaunch.choosing': '选择中', - 'agents.claudeCodeLaunch.dialogTitle': '选择 Claude Code 工作目录', - 'agents.claudeCodeLaunch.dialogDescription': '需要先选择工作目录,然后才能设置环境变量并启动 Claude Code。', + 'agents.claudeCodeLaunch.dialogTitle': '选择 {client} 启动目录', + 'agents.claudeCodeLaunch.dialogDescription': '需要先选择工作目录,然后才能启动 {client}。', 'agents.claudeCodeLaunch.workingDirectory': '工作目录', 'agents.claudeCodeLaunch.browse': '浏览', 'agents.claudeCodeLaunch.neverAskAgain': '永久关闭此提示', 'agents.claudeCodeLaunch.directoryRequired': '请先选择工作目录', - 'agents.claudeCodeLaunch.launch': '启动 Claude Code', + 'agents.claudeCodeLaunch.launch': '启动 {client}', 'agents.model.readingAvailable': '正在读取可用模型', 'agents.model.current': '当前已应用:{model}', 'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项', @@ -1543,13 +1543,13 @@ export const en: Record = { 'agents.claudeCodeLaunch.chooseDirectory': 'Choose Directory', 'agents.claudeCodeLaunch.selectedDirectory': 'Selected working directory', 'agents.claudeCodeLaunch.choosing': 'Choosing', - 'agents.claudeCodeLaunch.dialogTitle': 'Choose a Claude Code Working Directory', - 'agents.claudeCodeLaunch.dialogDescription': 'Select a working directory before setting the environment variables and starting Claude Code.', + 'agents.claudeCodeLaunch.dialogTitle': 'Choose a Launch Directory for {client}', + 'agents.claudeCodeLaunch.dialogDescription': 'Select a working directory before starting {client}.', 'agents.claudeCodeLaunch.workingDirectory': 'Working directory', 'agents.claudeCodeLaunch.browse': 'Browse', 'agents.claudeCodeLaunch.neverAskAgain': 'Never show this prompt again', 'agents.claudeCodeLaunch.directoryRequired': 'Select a working directory first', - 'agents.claudeCodeLaunch.launch': 'Start Claude Code', + 'agents.claudeCodeLaunch.launch': 'Start {client}', 'agents.model.readingAvailable': 'Loading available models', 'agents.model.current': 'Currently applied: {model}', 'agents.model.firstSelection': '{count} models available; the first is selected by default', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index ebeadf6..5725632 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -555,10 +555,11 @@ export function AgentsPage() { createClaudeModelMappings(''), ); const [claudeCustomMapping, setClaudeCustomMapping] = useState(false); - const [claudeCodeLaunchDialogOpen, setClaudeCodeLaunchDialogOpen] = useState(false); - const [claudeCodeLaunchDirectory, setClaudeCodeLaunchDirectory] = useState(''); - const [claudeCodeSuppressDirectoryPrompt, setClaudeCodeSuppressDirectoryPrompt] = useState(false); - const [claudeCodeDirectoryError, setClaudeCodeDirectoryError] = useState(''); + const [launchDirectoryDialogOpen, setLaunchDirectoryDialogOpen] = useState(false); + const [launchDirectory, setLaunchDirectory] = useState(''); + const [launchDirectoryTarget, setLaunchDirectoryTarget] = useState(null); + const [suppressLaunchDirectoryPrompt, setSuppressLaunchDirectoryPrompt] = useState(false); + const [launchDirectoryError, setLaunchDirectoryError] = useState(''); const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); const [busyAction, setBusyAction] = useState< @@ -683,8 +684,9 @@ export function AgentsPage() { setCloseAppConfirmOpen(false); setOauthLoginRequiredAction(null); setOauthConfigurationDraft(null); - setClaudeCodeLaunchDialogOpen(false); - setClaudeCodeDirectoryError(''); + setLaunchDirectoryDialogOpen(false); + setLaunchDirectoryTarget(null); + setLaunchDirectoryError(''); claudeModelMappingsDirtyRef.current = false; }, [selected]); @@ -818,6 +820,12 @@ export function AgentsPage() { || (activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')), ); const canLaunchTarget = (target: AgentLaunchTarget | null) => launchEnabled && Boolean(target); + const requiresLaunchDirectory = (target: AgentLaunchTarget | null) => Boolean( + target + && (selected === 'claude-code' + || selected === 'pi' + || (selected === 'codex' && target.id === 'cli')), + ); const modelHint = modelSelectionError || modelError || (modelLoading @@ -1092,28 +1100,31 @@ export function AgentsPage() { } }; - const openClaudeCodeLaunchDialog = () => { - setClaudeCodeLaunchDirectory(activeStatus?.claudeCodeWorkingDirectory ?? ''); - setClaudeCodeSuppressDirectoryPrompt(false); - setClaudeCodeDirectoryError(''); - setClaudeCodeLaunchDialogOpen(true); + const openLaunchDirectoryDialog = (target: AgentLaunchTarget) => { + setLaunchDirectory( + selected === 'claude-code' ? activeStatus?.claudeCodeWorkingDirectory ?? '' : '', + ); + setLaunchDirectoryTarget(target); + setSuppressLaunchDirectoryPrompt(false); + setLaunchDirectoryError(''); + setLaunchDirectoryDialogOpen(true); }; - const chooseClaudeCodeWorkingDirectory = async () => { + const chooseLaunchDirectory = async () => { setBusyAction('directory'); - setClaudeCodeDirectoryError(''); + setLaunchDirectoryError(''); try { const selectedDirectory = await open({ directory: true, multiple: false, - defaultPath: claudeCodeLaunchDirectory || activeStatus?.claudeCodeWorkingDirectory || undefined, - title: t('agents.claudeCodeLaunch.dialogTitle'), + defaultPath: launchDirectory || activeStatus?.claudeCodeWorkingDirectory || undefined, + title: t('agents.claudeCodeLaunch.dialogTitle', { client: activeDefinition.name }), }); if (typeof selectedDirectory === 'string') { - setClaudeCodeLaunchDirectory(selectedDirectory); + setLaunchDirectory(selectedDirectory); } } catch (requestError) { - setClaudeCodeDirectoryError(String(requestError)); + setLaunchDirectoryError(String(requestError)); } finally { setBusyAction(null); } @@ -1140,9 +1151,12 @@ export function AgentsPage() { workingDirectory, suppressWorkingDirectoryPrompt, }); - if (selected === 'claude-code' && workingDirectory) { - setClaudeCodeLaunchDialogOpen(false); - await reloadStatusesAfterAction(); + if (requiresLaunchDirectory(target) && workingDirectory) { + setLaunchDirectoryDialogOpen(false); + setLaunchDirectoryTarget(null); + if (selected === 'claude-code') { + await reloadStatusesAfterAction(); + } } } catch (requestError) { const message = String(requestError); @@ -1151,12 +1165,12 @@ export function AgentsPage() { && (message.includes('CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED') || message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID')) ) { - openClaudeCodeLaunchDialog(); - setClaudeCodeDirectoryError( + openLaunchDirectoryDialog(target); + setLaunchDirectoryError( message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID') ? message : '', ); - } else if (selected === 'claude-code' && workingDirectory) { - setClaudeCodeDirectoryError(message); + } else if (requiresLaunchDirectory(target) && workingDirectory) { + setLaunchDirectoryError(message); } else if (!handleOAuthLoginError(requestError, 'launch')) { setLaunchError(message); } @@ -1167,26 +1181,29 @@ export function AgentsPage() { const launchAgent = async (target: AgentLaunchTarget | null) => { if (!target) return; - if (selected === 'claude-code' && !activeStatus?.claudeCodeWorkingDirectoryPromptDisabled) { - openClaudeCodeLaunchDialog(); + if ( + requiresLaunchDirectory(target) + && !(selected === 'claude-code' && activeStatus?.claudeCodeWorkingDirectoryPromptDisabled) + ) { + openLaunchDirectoryDialog(target); return; } await invokeAgentLaunch(target); }; - const launchClaudeCodeFromDialog = async () => { - const target = defaultLaunchTarget; + const launchFromDirectoryDialog = async () => { + const target = launchDirectoryTarget; if (!target) return; - const workingDirectory = claudeCodeLaunchDirectory.trim(); + const workingDirectory = launchDirectory.trim(); if (!workingDirectory) { - setClaudeCodeDirectoryError(t('agents.claudeCodeLaunch.directoryRequired')); + setLaunchDirectoryError(t('agents.claudeCodeLaunch.directoryRequired')); return; } - setClaudeCodeDirectoryError(''); + setLaunchDirectoryError(''); await invokeAgentLaunch( target, workingDirectory, - claudeCodeSuppressDirectoryPrompt, + selected === 'claude-code' ? suppressLaunchDirectoryPrompt : null, ); }; @@ -1683,41 +1700,42 @@ export function AgentsPage() { - {claudeCodeLaunchDialogOpen ? ( + {launchDirectoryDialogOpen ? (
{ if (event.currentTarget === event.target && !busy) { - setClaudeCodeLaunchDialogOpen(false); + setLaunchDirectoryDialogOpen(false); + setLaunchDirectoryTarget(null); } }}>
-

- {t('agents.claudeCodeLaunch.dialogTitle')} +

+ {t('agents.claudeCodeLaunch.dialogTitle', { client: activeDefinition.name })}

-

{t('agents.claudeCodeLaunch.dialogDescription')}

+

{t('agents.claudeCodeLaunch.dialogDescription', { client: activeDefinition.name })}

{t('agents.claudeCodeLaunch.workingDirectory')}
- - {claudeCodeDirectoryError ? ( + {selected === 'claude-code' ? ( + + ) : null} + {launchDirectoryError ? ( - {claudeCodeDirectoryError} + {launchDirectoryError} ) : null}