Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 176 additions & 25 deletions app/src-tauri/src/claude_code.rs
Original file line number Diff line number Diff line change
@@ -1,71 +1,222 @@
//! Tauri commands for the Claude Code CLI provider.
//!
//! Provides a cross-platform "open a terminal and run `claude login`"
//! Provides a cross-platform "open a terminal and run `claude auth login`"
//! helper. The CLI's OAuth flow is interactive (it prints a URL and
//! waits for the user to paste a code), so we can't host it in-app — we
//! detach into the user's native terminal so they complete login there,
//! then return to OpenHuman and click Recheck in the settings card.

use std::process::Command;

/// Open the user's native terminal and run `claude login` inside it.
/// The CLI's authentication entry point, as argv.
///
/// Authentication lives under the `auth` subcommand: `claude auth login`.
/// There is no top-level `login` command, and because `claude` accepts a
/// positional `[prompt]`, the old `claude login` was not rejected — it was
/// read as a prompt and started an ordinary session, so the sign-in flow
/// never ran and the settings card kept reporting the account as signed out.
///
/// `--claudeai` is the CLI's own default; it is passed explicitly so the
/// launcher does not silently follow a future change of that default into
/// console/API billing.
const CLAUDE_LOGIN_ARGV: &[&str] = &["claude", "auth", "login", "--claudeai"];

/// The same command as one shell word, for terminals that take a string
/// rather than an argv (`cmd /k`, AppleScript `do script`, `xfce4-terminal -e`).
fn login_command_line() -> String {
CLAUDE_LOGIN_ARGV.join(" ")
}

/// Argv for `cmd /c start "" cmd /k <login command>`.
/// Compiled when testing on any host too, so a Linux CI run still checks the
/// Windows argv; otherwise only where it is actually spawned. Same for the
/// macOS and Linux helpers below.
#[cfg(any(test, target_os = "windows"))]
fn windows_launch_args() -> Vec<String> {
// `start ""` opens a new console window; the empty quoted title
// prevents cmd from interpreting the first arg as a title.
// `cmd /k` keeps the window open after the command exits so the
// user can read any final output.
vec![
"/c".into(),
"start".into(),
"".into(),
"cmd".into(),
"/k".into(),
login_command_line(),
]
}

/// The AppleScript handed to `osascript`.
#[cfg(any(test, target_os = "macos"))]
fn macos_launch_script() -> String {
format!(
r#"tell application "Terminal"
activate
do script "{}"
end tell"#,
login_command_line()
)
}

/// Linux terminal emulators to try, in order, with the args each one needs.
#[cfg(any(test, target_os = "linux"))]
fn linux_launch_candidates() -> Vec<(&'static str, Vec<String>)> {
let argv: Vec<String> = CLAUDE_LOGIN_ARGV.iter().map(|s| (*s).to_string()).collect();
let line = login_command_line();
vec![
("x-terminal-emulator", {
let mut a = vec!["-e".to_string()];
a.extend(argv.clone());
a
}),
("gnome-terminal", {
let mut a = vec!["--".to_string()];
a.extend(argv.clone());
a
}),
("konsole", {
let mut a = vec!["-e".to_string()];
a.extend(argv.clone());
a
}),
// xfce4-terminal takes the command as a single string, not as argv.
("xfce4-terminal", vec!["-e".to_string(), line]),
("xterm", {
let mut a = vec!["-e".to_string()];
a.extend(argv);
a
}),
]
}

/// Open the user's native terminal and run `claude auth login` inside it.
///
/// Returns the name of the terminal emulator we launched (for UI
/// confirmation) or an error string if no terminal could be opened.
///
/// Platform behaviour:
/// - Windows: `cmd /c start "" cmd /k claude login`
/// - macOS: `osascript` → Terminal.app `do script "claude login"`
/// - Windows: `cmd /c start "" cmd /k claude auth login --claudeai`
/// - macOS: `osascript` → Terminal.app `do script "claude auth login --claudeai"`
/// - Linux: try `x-terminal-emulator`, then `gnome-terminal`,
/// `konsole`, `xterm` in that order
/// `konsole`, `xfce4-terminal`, `xterm` in that order
#[tauri::command]
pub fn claude_code_login_launch() -> Result<String, String> {
#[cfg(target_os = "windows")]
{
// `start ""` opens a new console window; the empty quoted title
// prevents cmd from interpreting the first arg as a title.
// `cmd /k` keeps the window open after `claude login` exits so
// the user can read any final output.
Command::new("cmd")
.args(["/c", "start", "", "cmd", "/k", "claude login"])
.args(windows_launch_args())
.spawn()
.map_err(|e| format!("failed to open cmd: {e}"))?;
return Ok("cmd".into());
}

#[cfg(target_os = "macos")]
{
let script = r#"tell application "Terminal"
activate
do script "claude login"
end tell"#;
Command::new("osascript")
.args(["-e", script])
.args(["-e", &macos_launch_script()])
.spawn()
.map_err(|e| format!("failed to open Terminal.app: {e}"))?;
Ok("Terminal.app".into())
}

#[cfg(target_os = "linux")]
{
let terminals: &[(&str, &[&str])] = &[
("x-terminal-emulator", &["-e", "claude", "login"]),
("gnome-terminal", &["--", "claude", "login"]),
("konsole", &["-e", "claude", "login"]),
("xfce4-terminal", &["-e", "claude login"]),
("xterm", &["-e", "claude", "login"]),
];
for (term, args) in terminals {
match Command::new(term).args(*args).spawn() {
for (term, args) in linux_launch_candidates() {
match Command::new(term).args(&args).spawn() {
Ok(_) => return Ok(term.to_string()),
Err(_) => continue,
}
}
Err("no terminal emulator found (tried x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, xterm). Run `claude login` manually.".into())
Err(format!(
"no terminal emulator found (tried x-terminal-emulator, gnome-terminal, konsole, \
xfce4-terminal, xterm). Run `{}` manually.",
login_command_line()
))
}

#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
Err("claude_code_login_launch is not supported on this platform".into())
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Every launcher must reach the `auth` subcommand. `claude login` is not a
/// command — `claude` takes a positional `[prompt]`, so it silently became a
/// prompt instead of starting the OAuth flow.
fn assert_reaches_auth_login(rendered: &str, what: &str) {
assert!(
rendered.contains("claude auth login"),
"{what} must invoke `claude auth login`, got: {rendered}"
);
assert!(
!rendered.contains("claude login"),
"{what} still constructs the obsolete `claude login`, got: {rendered}"
);
}

#[test]
fn login_command_line_is_the_auth_subcommand() {
assert_eq!(login_command_line(), "claude auth login --claudeai");
}

#[test]
fn argv_keeps_auth_and_login_as_separate_words() {
// Split-argument terminals pass these straight to execvp, so `auth` and
// `login` have to be distinct argv entries rather than one "auth login".
assert_eq!(
CLAUDE_LOGIN_ARGV,
&["claude", "auth", "login", "--claudeai"]
);
}

#[test]
fn windows_launcher_reaches_auth_login() {
assert_reaches_auth_login(&windows_launch_args().join(" "), "the Windows launcher");
}

#[test]
fn windows_launcher_keeps_the_empty_start_title() {
// `start` reads a bare first argument as the window title, which would
// swallow `cmd` and open an empty shell.
let args = windows_launch_args();
assert_eq!(args[1], "start");
assert_eq!(args[2], "", "the empty title placeholder must survive");
}

#[test]
fn macos_launcher_reaches_auth_login() {
assert_reaches_auth_login(&macos_launch_script(), "the macOS AppleScript");
}

#[test]
fn macos_script_quotes_the_command_for_do_script() {
assert!(macos_launch_script().contains(r#"do script "claude auth login --claudeai""#));
}

#[test]
fn every_linux_candidate_reaches_auth_login() {
let candidates = linux_launch_candidates();
assert_eq!(candidates.len(), 5, "all five emulators stay covered");
for (term, args) in candidates {
assert_reaches_auth_login(&args.join(" "), term);
}
}

#[test]
fn xfce4_terminal_gets_one_string_and_the_others_get_argv() {
let candidates = linux_launch_candidates();
for (term, args) in candidates {
match term {
// -e plus a single command string
"xfce4-terminal" => assert_eq!(args.len(), 2, "xfce4-terminal takes one string"),
// separator plus four argv words
_ => assert_eq!(args.len(), 5, "{term} takes the command as argv"),
}
}
}
}
2 changes: 1 addition & 1 deletion app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4428,7 +4428,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'تسجيل الدخول عبر Claude',
'settings.ai.claudeCode.reconnect': 'إعادة الاتصال',
'settings.ai.claudeCode.loginHint':
'يفتح طرفية تُشغّل claude login. بعد اكتمالها، انقر على إعادة الفحص.',
'يفتح طرفية تُشغّل claude auth login --claudeai. بعد اكتمالها، انقر على إعادة الفحص.',
'settings.ai.claudeCode.loginError': 'تعذّر فتح طرفية تسجيل الدخول. يُرجى المحاولة مرة أخرى.',
'settings.ai.claudeCode.fullAccess': 'وصول كامل',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4539,7 +4539,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Claude দিয়ে সাইন ইন করুন',
'settings.ai.claudeCode.reconnect': 'পুনরায় সংযুক্ত করুন',
'settings.ai.claudeCode.loginHint':
'claude login চালানো একটি টার্মিনাল খোলে। সম্পন্ন হলে, পুনরায় যাচাই-এ ক্লিক করুন।',
'claude auth login --claudeai চালানো একটি টার্মিনাল খোলে। সম্পন্ন হলে, পুনরায় যাচাই-এ ক্লিক করুন।',
'settings.ai.claudeCode.loginError': 'লগইন টার্মিনাল খোলা যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।',
'settings.ai.claudeCode.fullAccess': 'সম্পূর্ণ অ্যাক্সেস',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4662,7 +4662,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Mit Claude anmelden',
'settings.ai.claudeCode.reconnect': 'Erneut verbinden',
'settings.ai.claudeCode.loginHint':
'Öffnet ein Terminal, das claude login ausführt. Klicke nach Abschluss auf „Erneut prüfen“.',
'Öffnet ein Terminal, das claude auth login --claudeai ausführt. Klicke nach Abschluss auf „Erneut prüfen“.',
'settings.ai.claudeCode.loginError':
'Das Anmelde-Terminal konnte nicht geöffnet werden. Bitte versuche es erneut.',
'settings.ai.claudeCode.fullAccess': 'Voller Zugriff',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5148,7 +5148,7 @@ const en: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Sign in with Claude',
'settings.ai.claudeCode.reconnect': 'Reconnect',
'settings.ai.claudeCode.loginHint':
'Opens a terminal running claude login. After it completes, click Recheck.',
'Opens a terminal running claude auth login --claudeai. After it completes, click Recheck.',
'settings.ai.claudeCode.loginError': 'Could not open the login terminal. Please try again.',
'settings.ai.claudeCode.fullAccess': 'Full access',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4615,7 +4615,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Iniciar sesión con Claude',
'settings.ai.claudeCode.reconnect': 'Reconectar',
'settings.ai.claudeCode.loginHint':
'Abre un terminal que ejecuta claude login. Cuando termine, haz clic en Volver a comprobar.',
'Abre un terminal que ejecuta claude auth login --claudeai. Cuando termine, haz clic en Volver a comprobar.',
'settings.ai.claudeCode.loginError':
'No se pudo abrir el terminal de inicio de sesión. Inténtalo de nuevo.',
'settings.ai.claudeCode.fullAccess': 'Acceso completo',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4642,7 +4642,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Se connecter avec Claude',
'settings.ai.claudeCode.reconnect': 'Reconnecter',
'settings.ai.claudeCode.loginHint':
'Ouvre un terminal exécutant claude login. Une fois terminé, cliquez sur Revérifier.',
'Ouvre un terminal exécutant claude auth login --claudeai. Une fois terminé, cliquez sur Revérifier.',
'settings.ai.claudeCode.loginError':
"Impossible d'ouvrir le terminal de connexion. Veuillez réessayer.",
'settings.ai.claudeCode.fullAccess': 'Accès complet',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4537,7 +4537,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Claude से साइन इन करें',
'settings.ai.claudeCode.reconnect': 'फिर से कनेक्ट करें',
'settings.ai.claudeCode.loginHint':
'claude login चलाने वाला एक टर्मिनल खोलता है। पूरा होने के बाद, फिर से जाँचें पर क्लिक करें।',
'claude auth login --claudeai चलाने वाला एक टर्मिनल खोलता है। पूरा होने के बाद, फिर से जाँचें पर क्लिक करें।',
'settings.ai.claudeCode.loginError': 'लॉगिन टर्मिनल नहीं खोला जा सका। कृपया पुनः प्रयास करें।',
'settings.ai.claudeCode.fullAccess': 'पूर्ण पहुँच',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4555,7 +4555,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Masuk dengan Claude',
'settings.ai.claudeCode.reconnect': 'Hubungkan ulang',
'settings.ai.claudeCode.loginHint':
'Membuka terminal yang menjalankan claude login. Setelah selesai, klik Periksa ulang.',
'Membuka terminal yang menjalankan claude auth login --claudeai. Setelah selesai, klik Periksa ulang.',
'settings.ai.claudeCode.loginError': 'Tidak dapat membuka terminal login. Silakan coba lagi.',
'settings.ai.claudeCode.fullAccess': 'Akses penuh',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4609,7 +4609,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Accedi con Claude',
'settings.ai.claudeCode.reconnect': 'Riconnetti',
'settings.ai.claudeCode.loginHint':
'Apre un terminale che esegue claude login. Al termine, fai clic su Ricontrolla.',
'Apre un terminale che esegue claude auth login --claudeai. Al termine, fai clic su Ricontrolla.',
'settings.ai.claudeCode.loginError': 'Impossibile aprire il terminale di accesso. Riprova.',
'settings.ai.claudeCode.fullAccess': 'Accesso completo',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4488,7 +4488,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Claude로 로그인',
'settings.ai.claudeCode.reconnect': '다시 연결',
'settings.ai.claudeCode.loginHint':
'claude login을 실행하는 터미널을 엽니다. 완료된 후 다시 확인을 클릭하세요.',
'claude auth login --claudeai을 실행하는 터미널을 엽니다. 완료된 후 다시 확인을 클릭하세요.',
'settings.ai.claudeCode.loginError': '로그인 터미널을 열 수 없습니다. 다시 시도해 주세요.',
'settings.ai.claudeCode.fullAccess': '전체 액세스',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4603,7 +4603,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Zaloguj się przez Claude',
'settings.ai.claudeCode.reconnect': 'Połącz ponownie',
'settings.ai.claudeCode.loginHint':
'Otwiera terminal z poleceniem claude login. Po zakończeniu kliknij Sprawdź ponownie.',
'Otwiera terminal z poleceniem claude auth login --claudeai. Po zakończeniu kliknij Sprawdź ponownie.',
'settings.ai.claudeCode.loginError': 'Nie można otworzyć terminala logowania. Spróbuj ponownie.',
'settings.ai.claudeCode.fullAccess': 'Pełny dostęp',
'settings.ai.claudeCode.fullAccessOn':
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4603,7 +4603,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Entrar com Claude',
'settings.ai.claudeCode.reconnect': 'Reconectar',
'settings.ai.claudeCode.loginHint':
'Abre um terminal executando claude login. Quando terminar, clique em Verificar novamente.',
'Abre um terminal executando claude auth login --claudeai. Quando terminar, clique em Verificar novamente.',
'settings.ai.claudeCode.loginError':
'Não foi possível abrir o terminal de login. Tente novamente.',
'settings.ai.claudeCode.fullAccess': 'Acesso total',
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4578,7 +4578,7 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.signIn': 'Войти через Claude',
'settings.ai.claudeCode.reconnect': 'Переподключить',
'settings.ai.claudeCode.loginHint':
'Открывает терминал с командой claude login. После завершения нажмите «Проверить снова».',
'Открывает терминал с командой claude auth login --claudeai. После завершения нажмите «Проверить снова».',
'settings.ai.claudeCode.loginError':
'Не удалось открыть терминал входа. Пожалуйста, попробуйте снова.',
'settings.ai.claudeCode.fullAccess': 'Полный доступ',
Expand Down
3 changes: 2 additions & 1 deletion app/src/lib/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4293,7 +4293,8 @@ const messages: TranslationMap = {
'settings.ai.claudeCode.openingTerminal': '正在打开终端…',
'settings.ai.claudeCode.signIn': '使用 Claude 登录',
'settings.ai.claudeCode.reconnect': '重新连接',
'settings.ai.claudeCode.loginHint': '打开运行 claude login 的终端。完成后,点击“重新检查”。',
'settings.ai.claudeCode.loginHint':
'打开运行 claude auth login --claudeai 的终端。完成后,点击“重新检查”。',
'settings.ai.claudeCode.loginError': '无法打开登录终端。请重试。',
'settings.ai.claudeCode.fullAccess': '完全访问权限',
'settings.ai.claudeCode.fullAccessOn': 'Claude Code 可以运行命令、使用网络并生成子智能体。',
Expand Down
2 changes: 1 addition & 1 deletion app/src/utils/tauriCommands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ export async function openhumanClaudeCodeSetFullAccess(
}

/**
* Open the user's native terminal and run `claude login` inside it. The
* Open the user's native terminal and run `claude auth login` inside it. The
* CLI's OAuth flow is interactive, so we can't host it in-app — we
* detach into a terminal window and let the user complete the flow
* there, then click Recheck back in the settings card.
Expand Down
Loading