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 @@ -137,6 +137,26 @@ data class OpenCodeModel(
val status: String? = null,
val limit: OpenCodeModelLimit? = null,
val variants: Map<String, JsonElement> = emptyMap(),
/**
* Codex only: the service-tier id that means "Fast" for this model (the app-server's `priority`
* tier, or a legacy `fast`), or null when the model offers no fast mode. Named so no OpenCode
* payload carries the key; left serialized so the disk catalogue cache keeps it.
*/
val codexFastServiceTier: String? = null,
/** Codex only: the server's description of the fast tier, shown beside the fast-mode toggle. */
val codexFastDescription: String? = null,
/** Codex only: true when the server defaults this model to the fast tier. */
val codexFastByDefault: Boolean = false,
/**
* Codex only: the model's `defaultReasoningEffort`. Sent explicitly on every turn, because the
* app-server applies `effort` to subsequent turns too and omitting it would inherit the previous one.
*/
val codexDefaultEffort: String? = null,
/**
* Codex only: the model's `defaultServiceTier`, or null for standard speed. Sent explicitly on
* every turn for the same reason as [codexDefaultEffort].
*/
val codexDefaultServiceTier: String? = null,
)

@Serializable
Expand Down Expand Up @@ -295,6 +315,11 @@ data class PromptRequest(
val variant: String? = null,
val attachments: List<PromptAttachment> = emptyList(),
val noReply: Boolean = false,
/**
* Codex only: the service tier for this turn. Null leaves the choice to Codex; `"default"`
* forces standard speed. Built field by field for OpenCode, so it is never sent to that server.
*/
val serviceTier: String? = null,
)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,18 @@ class SecureSettingsRepository(context: Context) : RuntimeConnectionStore, Unrea
get() = preferences.getBoolean(KEY_AUTO_EXPAND_REASONING, false)
set(value) = preferences.edit().putBoolean(KEY_AUTO_EXPAND_REASONING, value).apply()

/**
* Codex fast mode: null follows each model's own default, true or false is an explicit choice.
* Stored as a removable key so "never chosen" and "chosen" stay distinct across restarts.
*/
var codexFastMode: Boolean?
get() = if (preferences.contains(KEY_CODEX_FAST_MODE)) preferences.getBoolean(KEY_CODEX_FAST_MODE, false) else null
set(value) {
val editor = preferences.edit()
if (value == null) editor.remove(KEY_CODEX_FAST_MODE) else editor.putBoolean(KEY_CODEX_FAST_MODE, value)
editor.apply()
}

var sendBehavior: String
get() = preferences.getString(KEY_SEND_BEHAVIOR, "interrupt") ?: "interrupt"
set(value) = preferences.edit().putString(KEY_SEND_BEHAVIOR, value).apply()
Expand Down Expand Up @@ -592,6 +604,7 @@ class SecureSettingsRepository(context: Context) : RuntimeConnectionStore, Unrea
private const val KEY_SYNTAX_THEME = "syntax_theme"
private const val KEY_TOOL_CALL_DETAIL_LEVEL = "tool_call_detail_level"
private const val KEY_AUTO_EXPAND_REASONING = "auto_expand_reasoning"
private const val KEY_CODEX_FAST_MODE = "codex_fast_mode"
private const val KEY_SEND_BEHAVIOR = "send_behavior"
private const val KEY_ENTER_TO_SEND = "enter_to_send"
private const val KEY_SIDEBAR_GROUPING = "sidebar_grouping"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ data class AppPreferences(
val syntaxTheme: String = "one-dark",
val toolCallDetailLevel: String = "detailed",
val autoExpandReasoning: Boolean = false,
/** Codex fast mode: null follows each model's default, otherwise the user's explicit choice. */
val codexFastMode: Boolean? = null,
val sendBehavior: String = "interrupt",
val enterToSend: Boolean = false,
val sidebarGrouping: String = "project",
Expand Down Expand Up @@ -93,6 +95,7 @@ class AppPreferencesRepository(
syntaxTheme = settings.syntaxTheme,
toolCallDetailLevel = settings.toolCallDetailLevel,
autoExpandReasoning = settings.autoExpandReasoning,
codexFastMode = settings.codexFastMode,
sendBehavior = settings.sendBehavior,
enterToSend = settings.enterToSend,
sidebarGrouping = settings.sidebarGrouping,
Expand Down Expand Up @@ -302,6 +305,11 @@ class AppPreferencesRepository(
mutableState.update { it.copy(autoExpandReasoning = enabled) }
}

fun setCodexFastMode(enabled: Boolean?) {
settings.codexFastMode = enabled
mutableState.update { it.copy(codexFastMode = enabled) }
}

fun setSendBehavior(behavior: String) {
settings.sendBehavior = behavior
mutableState.update { it.copy(sendBehavior = behavior) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.yugahashimoto.andcode.di

import com.yugahashimoto.andcode.AndCodeApplication
import com.yugahashimoto.andcode.data.connection.SecureSettingsRepository
import com.yugahashimoto.andcode.data.settings.AppPreferencesRepository
import com.yugahashimoto.andcode.feature.activity.ActivityViewModel
import com.yugahashimoto.andcode.feature.chat.ChatViewModel
import com.yugahashimoto.andcode.feature.settings.SettingsViewModel
Expand All @@ -17,9 +19,14 @@ val viewModelModule =

viewModel {
val app = androidContext().applicationContext as AndCodeApplication
val preferences: AppPreferencesRepository = get()
val settings: SecureSettingsRepository = get()
ChatViewModel(
draftRepo = get(),
pullRequestStatuses = get(),
// Same Codex fast-mode wiring as the factory in ui/AndCodeApp.kt; keep the two in step.
onCodexFastModeChanged = preferences::setCodexFastMode,
initialCodexFastMode = settings.codexFastMode,
// Kept in step with the hand-rolled factory in ui/AndCodeApp.kt: both construction
// paths have to park the connection probe and stall watchdog while the app is
// backgrounded, or whichever one the screen happens to use decides whether those
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
Expand All @@ -53,6 +54,7 @@ import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.CameraAlt
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
Expand Down Expand Up @@ -112,6 +114,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
Expand All @@ -128,6 +131,7 @@ import androidx.core.content.ContextCompat
import com.yugahashimoto.andcode.R
import com.yugahashimoto.andcode.core.api.OpenCodeAgent
import com.yugahashimoto.andcode.core.api.OpenCodeCommand
import com.yugahashimoto.andcode.core.api.OpenCodeModel
import com.yugahashimoto.andcode.core.api.OpenCodeProvider
import com.yugahashimoto.andcode.core.api.OpenCodeSkill
import com.yugahashimoto.andcode.core.api.PromptAttachment
Expand All @@ -152,6 +156,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonPrimitive

/** What the long-press action sheet offers for one message: its id, its text to copy, and whether "Edit & resend" applies. */
private data class MessageActionTarget(
Expand All @@ -178,6 +183,11 @@ fun ChatHomeScreen(
onSelectAgent: (String) -> Unit,
selectedVariant: String? = null,
onSelectVariant: (String?) -> Unit = {},
/**
* Turns Codex's fast service tier on or off. The chip for it shows only while
* [ChatUiState.fastModeAvailable], i.e. for a Codex model that lists the tier.
*/
onToggleFastMode: (Boolean) -> Unit = {},
attachments: List<PromptAttachment> = emptyList(),
onAttach: () -> Unit = {},
onRemoveAttachment: (Int) -> Unit = {},
Expand Down Expand Up @@ -651,6 +661,10 @@ fun ChatHomeScreen(
?.variants?.keys?.toList() ?: emptyList(),
selectedVariant = state.selectedVariant,
onSelectVariant = onSelectVariant,
fastModeAvailable = state.fastModeAvailable,
fastModeEnabled = state.fastModeEnabled,
fastModeDescription = state.fastModeDescription,
onToggleFastMode = onToggleFastMode,
attachments = state.attachments,
onAttach = onAttach,
onRemoveAttachment = onRemoveAttachment,
Expand Down Expand Up @@ -1241,6 +1255,10 @@ private fun ChatComposer(
thinkingOptions: List<String>,
selectedVariant: String?,
onSelectVariant: (String?) -> Unit,
fastModeAvailable: Boolean,
fastModeEnabled: Boolean,
fastModeDescription: String?,
onToggleFastMode: (Boolean) -> Unit,
attachments: List<PromptAttachment>,
onAttach: () -> Unit,
onRemoveAttachment: (Int) -> Unit,
Expand Down Expand Up @@ -1619,6 +1637,15 @@ private fun ChatComposer(
onSelect = onSelectVariant,
)
}
// Right after the effort chip: both decide how the model runs the next turn, so they
// read as a pair. Only a Codex model with a fast tier offers it.
if (fastModeAvailable) {
FastModeChip(
enabled = fastModeEnabled,
description = fastModeDescription,
onToggle = onToggleFastMode,
)
}
if (systemPromptPresets.isNotEmpty()) {
SystemPromptChip(
presets = systemPromptPresets,
Expand Down Expand Up @@ -1722,6 +1749,66 @@ private fun ThinkingChip(
}
}

/**
* Turns Codex's fast service tier on or off for the turns that follow.
*
* A plain on/off pill in the same tokens as the thinking and auto-accept chips, so it reads as one
* more setting on the row rather than a new kind of control. It carries switch semantics rather
* than a click, so a screen reader also says whether it is on. The server's own wording for the
* tier ("1.5x speed, increased usage") goes to the screen reader: a phone-width row has no room
* for it.
*/
@Composable
private fun FastModeChip(
enabled: Boolean,
description: String?,
onToggle: (Boolean) -> Unit,
) {
val containerColor =
if (enabled) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant
}
val contentColor =
if (enabled) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
modifier =
Modifier
.clip(RoundedCornerShape(100.dp))
.toggleable(value = enabled, role = Role.Switch, onValueChange = onToggle)
.testTag("chat-fast-mode"),
shape = RoundedCornerShape(100.dp),
color = containerColor,
contentColor = contentColor,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Default.Bolt,
contentDescription =
description
?.takeIf { it.isNotBlank() }
?.let { stringResource(R.string.cd_fast_mode_with_description, it) }
?: stringResource(R.string.cd_fast_mode),
modifier = Modifier.size(14.dp),
)
Text(
stringResource(R.string.chat_fast_mode),
style = MaterialTheme.typography.labelMedium,
maxLines = 1,
)
}
}
}

/**
* Switches the system-prompt preset this chat sends with, without leaving it.
*
Expand Down Expand Up @@ -1800,20 +1887,26 @@ private fun SystemPromptChip(
/**
* A model variant's display label for the thinking chip.
*
* Claude Code and Antigravity both offer reasoning-effort slugs ("low", "medium", "high", plus
* Claude Code's own "xhigh" and "max"); those get their proper names ("Extra High", "Max") instead
* of the raw slug, which used to make the CLI's highest level ("max") look like just another word
* next to "xhigh" rather than the new top tier it is. Any other provider's variant id - OpenCode's
* own models offer different ones - falls back to capitalizing the raw value, as before.
* Claude Code, Antigravity and Codex all offer reasoning-effort slugs ("low", "medium", "high",
* plus Claude Code's and Codex's "xhigh" and "max"); those get their proper names ("Extra High",
* "Max") instead of the raw slug, which used to make the CLI's highest level ("max") look like just
* another word next to "xhigh" rather than the new top tier it is. Codex adds "ultra" above "max"
* and "minimal" and "none" below "low"; "none" reads "Off", since the chip then names thinking
* that is switched off, and a bare "None" would look like no choice was made. Any other provider's
* variant id - OpenCode's own models offer different ones - falls back to capitalizing the raw
* value, as before.
*/
@Composable
private fun thinkingLevelLabel(level: String): String =
when (level.lowercase()) {
"none" -> stringResource(R.string.chat_thinking_level_none)
"minimal" -> stringResource(R.string.chat_thinking_level_minimal)
"low" -> stringResource(R.string.chat_thinking_level_low)
"medium" -> stringResource(R.string.chat_thinking_level_medium)
"high" -> stringResource(R.string.chat_thinking_level_high)
"xhigh" -> stringResource(R.string.chat_thinking_level_xhigh)
"max" -> stringResource(R.string.chat_thinking_level_max)
"ultra" -> stringResource(R.string.chat_thinking_level_ultra)
else -> level.replaceFirstChar { it.uppercase() }
}

Expand Down Expand Up @@ -2215,3 +2308,62 @@ private fun ChatHomeScreenEmptyPreview() {
)
}
}

/**
* A Codex model with the full effort range and its fast tier switched on, at phone width, to check
* that the effort and Fast chips sit side by side and the row still fits.
*/
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ChatHomeScreenCodexFastPreview() {
val efforts = listOf("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra")
AndCodeTheme {
ChatHomeScreen(
state =
ChatUiState(
backendName = "Codex",
isConnected = true,
selectedVariant = "ultra",
fastModeAvailable = true,
fastModeEnabled = true,
fastModeDescription = "1.5x speed, increased usage",
),
providers =
listOf(
OpenCodeProvider(
id = "openai",
name = "OpenAI (Codex)",
models =
mapOf(
"gpt-6-astra" to
OpenCodeModel(
id = "gpt-6-astra",
providerId = "openai",
name = "GPT-6-Astra",
variants = efforts.associateWith { JsonPrimitive(it) },
),
),
),
),
agents = emptyList(),
selectedProviderId = "openai",
selectedModelId = "gpt-6-astra",
selectedAgentId = null,
runtimeTargets = emptyList(),
selectedRuntimeId = null,
onSelectRuntime = {},
onSelectModel = { _, _ -> },
onSelectAgent = {},
onSelectQuestionAnswer = { _, _, _ -> },
onSubmitQuestion = {},
onSendMessage = {},
onPermission = { _, _, _ -> },
onAbort = {},
onMic = {},
onNewChat = {},
onOpenLocalSetup = {},
onOpenRemoteSetup = {},
onOpenDrawer = {},
)
}
}
Loading
Loading