package soy.iko.opencode.ui.chat import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Search import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.Box import soy.iko.opencode.data.model.Agent import soy.iko.opencode.R /** Bottom sheet that lists every agent and lets the user pick one. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun AgentPickerSheet( agents: List, selected: String?, loading: Boolean, error: Boolean, onSelect: (Agent?) -> Unit, onRetry: () -> Unit, onDismiss: () -> Unit, ) { ModalBottomSheet(onDismissRequest = onDismiss) { Text( stringResource(R.string.agent), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 8.dp), ) if (loading) { val loadingLabel = stringResource(R.string.loading) Box( modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), contentAlignment = Alignment.Center, ) { CircularProgressIndicator(Modifier.semantics { contentDescription = loadingLabel }) } } else if (error && agents.isEmpty()) { Column( modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( stringResource(R.string.load_failed), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, ) TextButton(onClick = onRetry) { Text(stringResource(R.string.retry)) } } } else if (agents.isEmpty()) { Text( stringResource(R.string.no_agents), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), ) } else { var query by rememberSaveable { mutableStateOf("") } val keyboardController = LocalSoftwareKeyboardController.current OutlinedTextField( value = query, onValueChange = { query = it }, modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp), placeholder = { Text(stringResource(R.string.search_agents)) }, leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) }, singleLine = true, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions(onSearch = { keyboardController?.hide() }), ) val filtered = remember(agents, query) { val q = query.trim() if (q.isEmpty()) agents else agents.filter { it.name.contains(q, ignoreCase = true) || it.displayDescription.contains(q, ignoreCase = true) } } // The "Default" option participates in the search too: show it when the query is // empty or matches its label/description. Without this it either always showed // (ignoring the filter) or, when a query matched no named agent, disappeared with // the whole list — leaving the default unselectable. val defaultLabel = stringResource(R.string.default_agent) val defaultDesc = stringResource(R.string.default_agent_desc) val q = query.trim() val defaultMatches = q.isEmpty() || defaultLabel.contains(q, ignoreCase = true) || defaultDesc.contains(q, ignoreCase = true) if (filtered.isEmpty() && !defaultMatches) { Text( stringResource(R.string.no_agents_match, query.trim()), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), ) } else { LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 420.dp)) { if (defaultMatches) { item(key = "__default") { Column( modifier = Modifier .fillMaxWidth() .semantics(mergeDescendants = true) { if (selected == null) this.selected = true } .clickable(role = Role.RadioButton) { onSelect(null); onDismiss() } .padding(horizontal = 24.dp, vertical = 12.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( stringResource(R.string.default_agent), style = MaterialTheme.typography.bodyLarge, color = if (selected == null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), ) if (selected == null) { Icon( Icons.Filled.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp), ) } } Text( stringResource(R.string.default_agent_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } items(filtered, key = { it.name }) { agent -> val isSelected = agent.name == selected Column( modifier = Modifier .fillMaxWidth() .semantics(mergeDescendants = true) { if (isSelected) this.selected = true } .clickable(role = Role.RadioButton) { onSelect(agent); onDismiss() } .padding(horizontal = 24.dp, vertical = 12.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( agent.name, style = MaterialTheme.typography.bodyLarge, color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, modifier = Modifier.weight(1f), ) if (isSelected) { Icon( Icons.Filled.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp), ) } } Text( agent.displayDescription, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } } } } }