feat(agents): choose working directory for Pi and Codex CLI
This commit is contained in:
1 parent
25c5c3cf61
commit
ca570b8438
4 files changed
+128
-68
No files matched your search
+39
-2
@@ -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
@@ -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': '設定を消去',
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user