1package soy.iko.opencode.ui.chat
3import androidx.compose.foundation.clickable
4import androidx.compose.foundation.layout.Column
5import androidx.compose.foundation.layout.Row
6import androidx.compose.foundation.layout.fillMaxWidth
7import androidx.compose.foundation.layout.heightIn
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.size
10import androidx.compose.foundation.lazy.LazyColumn
11import androidx.compose.foundation.lazy.items
12import androidx.compose.foundation.text.KeyboardActions
13import androidx.compose.foundation.text.KeyboardOptions
14import androidx.compose.material.icons.Icons
15import androidx.compose.material.icons.filled.Check
16import androidx.compose.material.icons.filled.Search
17import androidx.compose.material3.CircularProgressIndicator
18import androidx.compose.material3.ExperimentalMaterial3Api
19import androidx.compose.material3.Icon
20import androidx.compose.material3.MaterialTheme
21import androidx.compose.material3.ModalBottomSheet
22import androidx.compose.material3.OutlinedTextField
23import androidx.compose.material3.Text
24import androidx.compose.material3.TextButton
25import androidx.compose.runtime.Composable
26import androidx.compose.runtime.getValue
27import androidx.compose.runtime.mutableStateOf
28import androidx.compose.runtime.remember
29import androidx.compose.runtime.saveable.rememberSaveable
30import androidx.compose.runtime.setValue
31import androidx.compose.ui.Alignment
32import androidx.compose.ui.Modifier
33import androidx.compose.ui.platform.LocalSoftwareKeyboardController
34import androidx.compose.ui.res.stringResource
35import androidx.compose.ui.semantics.Role
36import androidx.compose.ui.semantics.contentDescription
37import androidx.compose.ui.semantics.role
38import androidx.compose.ui.semantics.selected
39import androidx.compose.ui.semantics.semantics
40import androidx.compose.ui.text.input.ImeAction
41import androidx.compose.ui.unit.dp
42import androidx.compose.foundation.layout.Box
43import soy.iko.opencode.data.model.Agent
44import soy.iko.opencode.R
46/** Bottom sheet that lists every agent and lets the user pick one. */
47@OptIn(ExperimentalMaterial3Api::class)
54 onSelect: (Agent?) -> Unit,
56 onDismiss: () -> Unit,
58 ModalBottomSheet(onDismissRequest = onDismiss) {
60 stringResource(R.string.agent),
61 style = MaterialTheme.typography.titleMedium,
62 modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 8.dp),
65 val loadingLabel = stringResource(R.string.loading)
67 modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
68 contentAlignment = Alignment.Center,
70 CircularProgressIndicator(Modifier.semantics { contentDescription = loadingLabel })
72 } else if (error && agents.isEmpty()) {
74 modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
75 horizontalAlignment = Alignment.CenterHorizontally,
78 stringResource(R.string.load_failed),
79 style = MaterialTheme.typography.bodyMedium,
80 color = MaterialTheme.colorScheme.error,
82 TextButton(onClick = onRetry) { Text(stringResource(R.string.retry)) }
84 } else if (agents.isEmpty()) {
86 stringResource(R.string.no_agents),
87 style = MaterialTheme.typography.bodyMedium,
88 color = MaterialTheme.colorScheme.onSurfaceVariant,
89 modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
92 var query by rememberSaveable { mutableStateOf("") }
93 val keyboardController = LocalSoftwareKeyboardController.current
96 onValueChange = { query = it },
97 modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp),
98 placeholder = { Text(stringResource(R.string.search_agents)) },
99 leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
101 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
102 keyboardActions = KeyboardActions(onSearch = { keyboardController?.hide() }),
104 val filtered = remember(agents, query) {
106 if (q.isEmpty()) agents
108 it.name.contains(q, ignoreCase = true) ||
109 it.displayDescription.contains(q, ignoreCase = true)
112 // The "Default" option participates in the search too: show it when the query is
113 // empty or matches its label/description. Without this it either always showed
114 // (ignoring the filter) or, when a query matched no named agent, disappeared with
115 // the whole list — leaving the default unselectable.
116 val defaultLabel = stringResource(R.string.default_agent)
117 val defaultDesc = stringResource(R.string.default_agent_desc)
119 val defaultMatches = q.isEmpty() ||
120 defaultLabel.contains(q, ignoreCase = true) ||
121 defaultDesc.contains(q, ignoreCase = true)
122 if (filtered.isEmpty() && !defaultMatches) {
124 stringResource(R.string.no_agents_match, query.trim()),
125 style = MaterialTheme.typography.bodyMedium,
126 color = MaterialTheme.colorScheme.onSurfaceVariant,
127 modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
130 LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 420.dp)) {
131 if (defaultMatches) {
132 item(key = "__default") {
136 .semantics(mergeDescendants = true) {
137 if (selected == null) this.selected = true
139 .clickable(role = Role.RadioButton) { onSelect(null); onDismiss() }
140 .padding(horizontal = 24.dp, vertical = 12.dp),
142 Row(verticalAlignment = Alignment.CenterVertically) {
144 stringResource(R.string.default_agent),
145 style = MaterialTheme.typography.bodyLarge,
146 color = if (selected == null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
147 modifier = Modifier.weight(1f),
149 if (selected == null) {
152 contentDescription = null,
153 tint = MaterialTheme.colorScheme.primary,
154 modifier = Modifier.size(18.dp),
159 stringResource(R.string.default_agent_desc),
160 style = MaterialTheme.typography.bodySmall,
161 color = MaterialTheme.colorScheme.onSurfaceVariant,
166 items(filtered, key = { it.name }) { agent ->
167 val isSelected = agent.name == selected
171 .semantics(mergeDescendants = true) {
172 if (isSelected) this.selected = true
174 .clickable(role = Role.RadioButton) { onSelect(agent); onDismiss() }
175 .padding(horizontal = 24.dp, vertical = 12.dp),
177 Row(verticalAlignment = Alignment.CenterVertically) {
180 style = MaterialTheme.typography.bodyLarge,
181 color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
182 modifier = Modifier.weight(1f),
187 contentDescription = null,
188 tint = MaterialTheme.colorScheme.primary,
189 modifier = Modifier.size(18.dp),
194 agent.displayDescription,
195 style = MaterialTheme.typography.bodySmall,
196 color = MaterialTheme.colorScheme.onSurfaceVariant,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list