feat(agents): choose working directory for Pi and Codex CLI

This commit is contained in:
lzt404 committed 2026-08-07 16:42:41 +08:00
1 parent 25c5c3cf61
commit ca570b8438
4 files changed
+128 -68

No files matched your search

+39 -2
View File
@@ -1953,6 +1953,29 @@ async fn uninstall_pi_provider(
Ok(result)
}
fn validate_agent_working_directory(value: &str) -> Result<PathBuf, String> {
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<PathBuf, String> {
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(
+3 -3
View File
@@ -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': '設定を消去',
+6 -6
View File
@@ -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<MessageKey, string> = {
'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',
+80 -57
View File
@@ -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<AgentLaunchTarget | null>(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() {
</section>
</div>
{claudeCodeLaunchDialogOpen ? (
{launchDirectoryDialogOpen ? (
<div className="config-dialog-backdrop" onMouseDown={(event) => {
if (event.currentTarget === event.target && !busy) {
setClaudeCodeLaunchDialogOpen(false);
setLaunchDirectoryDialogOpen(false);
setLaunchDirectoryTarget(null);
}
}}>
<section
className="config-dialog agent-claude-code-launch-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="agent-claude-code-launch-title"
aria-labelledby="agent-launch-directory-title"
>
<div className="config-dialog-heading">
<div>
<h2 id="agent-claude-code-launch-title">
{t('agents.claudeCodeLaunch.dialogTitle')}
<h2 id="agent-launch-directory-title">
{t('agents.claudeCodeLaunch.dialogTitle', { client: activeDefinition.name })}
</h2>
</div>
</div>
<p>{t('agents.claudeCodeLaunch.dialogDescription')}</p>
<p>{t('agents.claudeCodeLaunch.dialogDescription', { client: activeDefinition.name })}</p>
<div className="config-dialog-field">
<span>{t('agents.claudeCodeLaunch.workingDirectory')}</span>
<button
type="button"
className="agent-claude-code-directory-picker"
onClick={() => void chooseClaudeCodeWorkingDirectory()}
onClick={() => void chooseLaunchDirectory()}
disabled={busy}
autoFocus
>
<span>
<small>{claudeCodeLaunchDirectory
<small>{launchDirectory
? t('agents.claudeCodeLaunch.selectedDirectory')
: t('agents.claudeCodeLaunch.noDirectory')}</small>
<strong title={claudeCodeLaunchDirectory || undefined}>
{claudeCodeLaunchDirectory || t('agents.claudeCodeLaunch.chooseDirectory')}
<strong title={launchDirectory || undefined}>
{launchDirectory || t('agents.claudeCodeLaunch.chooseDirectory')}
</strong>
</span>
<b>{busyAction === 'directory'
@@ -1725,25 +1743,30 @@ export function AgentsPage() {
: t('agents.claudeCodeLaunch.browse')}</b>
</button>
</div>
<label className="agent-claude-code-suppress-prompt">
<input
type="checkbox"
checked={claudeCodeSuppressDirectoryPrompt}
onChange={(event) => setClaudeCodeSuppressDirectoryPrompt(event.currentTarget.checked)}
disabled={busy}
/>
<span>{t('agents.claudeCodeLaunch.neverAskAgain')}</span>
</label>
{claudeCodeDirectoryError ? (
{selected === 'claude-code' ? (
<label className="agent-claude-code-suppress-prompt">
<input
type="checkbox"
checked={suppressLaunchDirectoryPrompt}
onChange={(event) => setSuppressLaunchDirectoryPrompt(event.currentTarget.checked)}
disabled={busy}
/>
<span>{t('agents.claudeCodeLaunch.neverAskAgain')}</span>
</label>
) : null}
{launchDirectoryError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{claudeCodeDirectoryError}
{launchDirectoryError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button
type="button"
className="secondary-button"
onClick={() => setClaudeCodeLaunchDialogOpen(false)}
onClick={() => {
setLaunchDirectoryDialogOpen(false);
setLaunchDirectoryTarget(null);
}}
disabled={busy}
>
{t('common.cancel')}
@@ -1751,11 +1774,11 @@ export function AgentsPage() {
<button
type="button"
className="primary-button"
onClick={() => void launchClaudeCodeFromDialog()}
disabled={busy || !claudeCodeLaunchDirectory.trim()}
onClick={() => void launchFromDirectoryDialog()}
disabled={busy || !launchDirectory.trim() || !launchDirectoryTarget}
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : null}
{t('agents.claudeCodeLaunch.launch')}
{t('agents.claudeCodeLaunch.launch', { client: activeDefinition.name })}
</button>
</div>
</section>