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
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,13 @@ data class CloudCapabilities(
val apiKeys: Boolean,
val claudeSetupToken: Boolean,
val codexSignIn: Boolean,
val claudeSignIn: Boolean,
) {
companion object {
fun parse(json: JsonObject?): CloudCapabilities? = json?.let {
CloudCapabilities(
apiKeys = it["apiKeys"].bool == true,
claudeSetupToken = it["claudeSetupToken"].bool == true,
codexSignIn = it["codexSignIn"].bool == true,
claudeSignIn = it["claudeSignIn"].bool == true,
)
}

Expand All @@ -162,7 +160,6 @@ data class CloudCapabilities(
apiKeys = true,
claudeSetupToken = false,
codexSignIn = CloudProviders.CODEX in signIns,
claudeSignIn = CloudProviders.CLAUDE in signIns,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
Expand Down Expand Up @@ -75,8 +74,8 @@ import kotlinx.serialization.Serializable
/** ChatGPT's device sign-in, kept in [to] (a [CloudDestination] id) when it says. */
@Serializable data class CloudChatGptRoute(val connectionId: String, val to: String? = null)

/** Claude's sign-in, or its setup token ([token]). */
@Serializable data class CloudClaudeRoute(val connectionId: String, val to: String? = null, val token: Boolean = false)
/** Claude's setup token, kept in [to] when it says. */
@Serializable data class CloudClaudeRoute(val connectionId: String, val to: String? = null)

/** An OpenAI or Anthropic API key; [vendor] picks the vendor first. */
@Serializable data class CloudApiKeyRoute(val connectionId: String, val vendor: String? = null, val to: String? = null)
Expand Down Expand Up @@ -115,7 +114,7 @@ fun NavGraphBuilder.cloudProviderRoutes(graph: AppGraph, nav: NavHostController)
}
page<CloudClaudeRoute> { entry ->
val route = entry.toRoute<CloudClaudeRoute>()
CloudClaudePage(graph, nav, route.connectionId, CloudDestination.of(route.to), route.token)
CloudClaudePage(graph, nav, route.connectionId, CloudDestination.of(route.to))
}
page<CloudApiKeyRoute> { entry ->
val route = entry.toRoute<CloudApiKeyRoute>()
Expand Down Expand Up @@ -167,7 +166,7 @@ private fun CloudProvidersPage(graph: AppGraph, nav: NavHostController, connecti
choosing = false
when (flow) {
CloudProviderFlow.ChatGpt -> nav.navigate(CloudChatGptRoute(connectionId, to))
CloudProviderFlow.Claude -> nav.navigate(CloudClaudeRoute(connectionId, to, token = capabilities?.claudeSignIn != true))
CloudProviderFlow.Claude -> nav.navigate(CloudClaudeRoute(connectionId, to))
is CloudProviderFlow.ApiKey -> nav.navigate(CloudApiKeyRoute(connectionId, flow.vendor, to))
CloudProviderFlow.Choose -> Unit
}
Expand Down Expand Up @@ -305,9 +304,9 @@ internal fun CloudAddChooser(capabilities: CloudCapabilities?, onPick: (CloudPro
onClick = { pick(CloudProviderFlow.ChatGpt) },
)
}
if (capabilities?.claudeSignIn == true || capabilities?.claudeSetupToken == true) {
if (capabilities?.claudeSetupToken == true) {
Item(
title = stringResource(if (capabilities.claudeSignIn) R.string.cloud_providers_add_claude else R.string.cloud_providers_add_setup_token),
title = stringResource(R.string.cloud_providers_add_setup_token),
icon = { ProviderIcon(ProviderIconKind.Claude, size = 24.dp) },
showChevron = false,
onClick = { pick(CloudProviderFlow.Claude) },
Expand Down Expand Up @@ -450,114 +449,27 @@ private fun Outcome(title: String, message: String?, onRetry: (() -> Unit)?) {
private fun failureTitle(code: String?): String =
stringResource(if (code == "expired") R.string.cloud_providers_expired else R.string.cloud_providers_failed)

// ---- Claude (pasted code, or a setup token) --------------------------------------------------
// ---- Claude (a setup token) --------------------------------------------------------------------

@Composable
private fun CloudClaudePage(graph: AppGraph, nav: NavHostController, connectionId: String, asked: CloudDestination?, startWithToken: Boolean) {
private fun CloudClaudePage(graph: AppGraph, nav: NavHostController, connectionId: String, asked: CloudDestination?) {
val session = remember(connectionId) { graph.agentSessions.session(connectionId) } ?: return
val allowed = remember(session) { destinations(session) }
var destination by rememberSaveable { mutableStateOf(destinationFor(allowed, asked)) }
var token by rememberSaveable { mutableStateOf(startWithToken) }
val capabilities by produceState<CloudCapabilities?>(null, session) {
value = try {
CloudProviders.capabilities(session.client)
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
null
}
}
// A link to Claude where only its setup token is offered opens that.
LaunchedEffect(capabilities) {
if (capabilities?.let { !it.claudeSignIn && it.claudeSetupToken } == true) token = true
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
val dark = HappyTheme.colors.dark
if (token) {
CloudSecretFlow(
title = stringResource(R.string.cloud_providers_add_setup_token),
field = stringResource(R.string.cloud_providers_setup_token),
hint = stringResource(R.string.cloud_providers_setup_token_hint),
done = stringResource(R.string.cloud_providers_claude_done),
allowed = allowed,
destination = destination,
onDestination = { destination = it },
vendors = emptyList(),
add = { _, secret, to -> CloudProviders.addCredential(session.client, CloudProviders.container(to, CloudProviders.CLAUDE_CODE), CloudProviders.CLAUDE_CODE, "oauth_token", secret) },
nav = nav,
)
return
}
val run = remember(session, destination) { CloudSignInRun(session.client, scope, CloudProviders.ANTHROPIC, destination).also { it.start() } }
DisposableEffect(run) { onDispose { run.close() } }
val state by run.current.collectAsStateWithLifecycle()
LeaveWhenDone(state == CloudSignInRun.State.Done, nav)
CloudClaudeScreen(
state = state,
CloudSecretFlow(
title = stringResource(R.string.cloud_providers_add_setup_token),
field = stringResource(R.string.cloud_providers_setup_token),
hint = stringResource(R.string.cloud_providers_setup_token_hint),
done = stringResource(R.string.cloud_providers_claude_done),
allowed = allowed,
destination = destination,
onDestination = { destination = it },
onOpen = { url -> openSignInPage(context, url, dark) },
onCode = run::sendCode,
onUseToken = { token = true }.takeIf { capabilities?.claudeSetupToken == true },
onRetry = run::start,
onBack = { nav.popBackStack() },
vendors = emptyList(),
add = { _, secret, to -> CloudProviders.addCredential(session.client, CloudProviders.container(to, CloudProviders.CLAUDE_CODE), CloudProviders.CLAUDE_CODE, "oauth_token", secret) },
nav = nav,
)
}

/**
* Claude's sign-in: Open Claude (a Custom Tab), then the code its page shows
* pasted here and sent to the server, which exchanges it. Or a setup token
* instead, where the server takes them ([onUseToken]). The code is never
* kept on the phone.
*/
@Composable
internal fun CloudClaudeScreen(
state: CloudSignInRun.State,
allowed: List<CloudDestination>,
destination: CloudDestination,
onDestination: (CloudDestination) -> Unit,
onOpen: (String) -> Unit,
onCode: (String) -> Unit,
onUseToken: (() -> Unit)?,
onRetry: () -> Unit,
onBack: () -> Unit,
) {
var code by remember { mutableStateOf("") }
Screen(header = { StackHeader(stringResource(R.string.cloud_providers_add_claude), onBack) }) {
ItemList {
SaveTo(allowed, destination, enabled = state !is CloudSignInRun.State.Done, onPick = onDestination)
when (state) {
CloudSignInRun.State.Starting -> ItemGroup { Item(title = stringResource(R.string.cloud_providers_add_claude), loading = true, showChevron = false) }
is CloudSignInRun.State.Waiting -> ItemGroup {
FlowColumn {
Text(stringResource(R.string.cloud_providers_claude_body), style = happyTextStyle(fontSize = 15, lineHeight = 21), color = HappyTheme.colors.textSecondary, textAlign = TextAlign.Center)
state.url?.let { url ->
RoundButton(title = stringResource(R.string.cloud_providers_open_claude), onClick = { onOpen(url) }, modifier = Modifier.testTag(CloudProvidersTags.OPEN))
}
SecretField(code, { code = it }, stringResource(R.string.cloud_providers_code), enabled = !state.sendingCode, hidden = false)
RoundButton(
title = stringResource(R.string.cloud_providers_connect),
onClick = { onCode(code) },
enabled = code.isNotBlank(),
loading = state.sendingCode,
modifier = Modifier.testTag(CloudProvidersTags.SUBMIT),
)
}
}
CloudSignInRun.State.Done -> Outcome(stringResource(R.string.cloud_providers_claude_done), null, null)
is CloudSignInRun.State.Failed -> Outcome(failureTitle(state.code), state.message ?: stringResource(R.string.cloud_failed), onRetry)
}
if (state !is CloudSignInRun.State.Done && onUseToken != null) {
ItemGroup {
Item(title = stringResource(R.string.cloud_providers_use_token), titleColor = HappyTheme.colors.textLink, showChevron = false, onClick = onUseToken)
}
}
}
}
}

@Composable
private fun SecretField(value: String, onChange: (String) -> Unit, placeholder: String, enabled: Boolean, hidden: Boolean) {
HappyTextField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
<string name="cloud_providers_empty">尚未添加任何内容</string>
<string name="cloud_providers_add">添加</string>
<string name="cloud_providers_add_chatgpt">使用 ChatGPT 登录</string>
<string name="cloud_providers_add_claude">使用 Claude 登录</string>
<string name="cloud_providers_add_setup_token">添加 Claude 设置令牌</string>
<string name="cloud_providers_add_setup_token">使用 Claude 设置令牌</string>
<string name="cloud_providers_add_key">添加 API 密钥</string>
<string name="cloud_providers_save_to">保存到</string>
<string name="cloud_providers_device_enter">在 OpenAI 输入此代码</string>
<string name="cloud_providers_open_openai">打开 OpenAI</string>
<string name="cloud_providers_waiting">正在等待你完成…</string>
<string name="cloud_providers_claude_body">在浏览器中登录 Claude,然后粘贴显示的代码。</string>
<string name="cloud_providers_open_claude">打开 Claude</string>
<string name="cloud_providers_code">代码</string>
<string name="cloud_providers_connect">连接</string>
<string name="cloud_providers_use_token">改用设置令牌</string>
<string name="cloud_providers_setup_token">设置令牌</string>
<string name="cloud_providers_setup_token_hint">在你的电脑上运行 `claude setup-token` 获取。</string>
<string name="cloud_providers_key">API 密钥</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
<string name="cloud_providers_empty">尚未新增任何內容</string>
<string name="cloud_providers_add">新增</string>
<string name="cloud_providers_add_chatgpt">使用 ChatGPT 登入</string>
<string name="cloud_providers_add_claude">使用 Claude 登入</string>
<string name="cloud_providers_add_setup_token">新增 Claude 設定權杖</string>
<string name="cloud_providers_add_setup_token">使用 Claude 設定權杖</string>
<string name="cloud_providers_add_key">新增 API 金鑰</string>
<string name="cloud_providers_save_to">儲存到</string>
<string name="cloud_providers_device_enter">在 OpenAI 輸入此代碼</string>
<string name="cloud_providers_open_openai">開啟 OpenAI</string>
<string name="cloud_providers_waiting">正在等待你完成…</string>
<string name="cloud_providers_claude_body">在瀏覽器中登入 Claude,然後貼上顯示的代碼。</string>
<string name="cloud_providers_open_claude">開啟 Claude</string>
<string name="cloud_providers_code">代碼</string>
<string name="cloud_providers_connect">連線</string>
<string name="cloud_providers_use_token">改用設定權杖</string>
<string name="cloud_providers_setup_token">設定權杖</string>
<string name="cloud_providers_setup_token_hint">在你的電腦上執行 `claude setup-token` 取得。</string>
<string name="cloud_providers_key">API 金鑰</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
<string name="cloud_providers_empty">Encara no hi ha res</string>
<string name="cloud_providers_add">Afegeix</string>
<string name="cloud_providers_add_chatgpt">Inicia sessió amb ChatGPT</string>
<string name="cloud_providers_add_claude">Inicia sessió amb Claude</string>
<string name="cloud_providers_add_setup_token">Afegeix un token de Claude</string>
<string name="cloud_providers_add_setup_token">Fes servir un token de configuració de Claude</string>
<string name="cloud_providers_add_key">Afegeix una clau d\'API</string>
<string name="cloud_providers_save_to">Desa a</string>
<string name="cloud_providers_device_enter">Introdueix aquest codi a OpenAI</string>
<string name="cloud_providers_open_openai">Obre OpenAI</string>
<string name="cloud_providers_waiting">Esperant que acabis…</string>
<string name="cloud_providers_claude_body">Inicia sessió a Claude al navegador i enganxa el codi que mostra.</string>
<string name="cloud_providers_open_claude">Obre Claude</string>
<string name="cloud_providers_code">Codi</string>
<string name="cloud_providers_connect">Connecta</string>
<string name="cloud_providers_use_token">Fes servir un token de configuració</string>
<string name="cloud_providers_setup_token">Token de configuració</string>
<string name="cloud_providers_setup_token_hint">De `claude setup-token` al teu ordinador.</string>
<string name="cloud_providers_key">Clau d\'API</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
<string name="cloud_providers_empty">Aún no hay nada</string>
<string name="cloud_providers_add">Añadir</string>
<string name="cloud_providers_add_chatgpt">Iniciar sesión con ChatGPT</string>
<string name="cloud_providers_add_claude">Iniciar sesión con Claude</string>
<string name="cloud_providers_add_setup_token">Añadir token de Claude</string>
<string name="cloud_providers_add_setup_token">Usar un token de configuración de Claude</string>
<string name="cloud_providers_add_key">Añadir clave de API</string>
<string name="cloud_providers_save_to">Guardar en</string>
<string name="cloud_providers_device_enter">Introduce este código en OpenAI</string>
<string name="cloud_providers_open_openai">Abrir OpenAI</string>
<string name="cloud_providers_waiting">Esperando a que termines…</string>
<string name="cloud_providers_claude_body">Inicia sesión en Claude en el navegador y pega el código que aparece.</string>
<string name="cloud_providers_open_claude">Abrir Claude</string>
<string name="cloud_providers_code">Código</string>
<string name="cloud_providers_connect">Conectar</string>
<string name="cloud_providers_use_token">Usar un token de configuración</string>
<string name="cloud_providers_setup_token">Token de configuración</string>
<string name="cloud_providers_setup_token_hint">De `claude setup-token` en tu ordenador.</string>
<string name="cloud_providers_key">Clave de API</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
<string name="cloud_providers_empty">Ancora nulla</string>
<string name="cloud_providers_add">Aggiungi</string>
<string name="cloud_providers_add_chatgpt">Accedi con ChatGPT</string>
<string name="cloud_providers_add_claude">Accedi con Claude</string>
<string name="cloud_providers_add_setup_token">Aggiungi token di Claude</string>
<string name="cloud_providers_add_setup_token">Usa un token di configurazione di Claude</string>
<string name="cloud_providers_add_key">Aggiungi chiave API</string>
<string name="cloud_providers_save_to">Salva in</string>
<string name="cloud_providers_device_enter">Inserisci questo codice su OpenAI</string>
<string name="cloud_providers_open_openai">Apri OpenAI</string>
<string name="cloud_providers_waiting">In attesa che tu finisca…</string>
<string name="cloud_providers_claude_body">Accedi a Claude nel browser, poi incolla il codice mostrato.</string>
<string name="cloud_providers_open_claude">Apri Claude</string>
<string name="cloud_providers_code">Codice</string>
<string name="cloud_providers_connect">Collega</string>
<string name="cloud_providers_use_token">Usa un token di configurazione</string>
<string name="cloud_providers_setup_token">Token di configurazione</string>
<string name="cloud_providers_setup_token_hint">Da `claude setup-token` sul tuo computer.</string>
<string name="cloud_providers_key">Chiave API</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@
<string name="cloud_providers_empty">まだ何も追加されていません</string>
<string name="cloud_providers_add">追加</string>
<string name="cloud_providers_add_chatgpt">ChatGPT でサインイン</string>
<string name="cloud_providers_add_claude">Claude でサインイン</string>
<string name="cloud_providers_add_setup_token">Claude のセットアップトークンを追加</string>
<string name="cloud_providers_add_setup_token">Claude のセットアップトークンを使う</string>
<string name="cloud_providers_add_key">API キーを追加</string>
<string name="cloud_providers_save_to">保存先</string>
<string name="cloud_providers_device_enter">このコードを OpenAI で入力してください</string>
<string name="cloud_providers_open_openai">OpenAI を開く</string>
<string name="cloud_providers_waiting">完了を待っています…</string>
<string name="cloud_providers_claude_body">ブラウザで Claude にサインインし、表示されたコードを貼り付けてください。</string>
<string name="cloud_providers_open_claude">Claude を開く</string>
<string name="cloud_providers_code">コード</string>
<string name="cloud_providers_connect">接続</string>
<string name="cloud_providers_use_token">セットアップトークンを使う</string>
<string name="cloud_providers_setup_token">セットアップトークン</string>
<string name="cloud_providers_setup_token_hint">コンピュータで `claude setup-token` を実行して取得します。</string>
<string name="cloud_providers_key">API キー</string>
Expand Down
Loading
Loading