1package soy.iko.opencode.ui.chat
3import androidx.compose.foundation.layout.Arrangement
4import androidx.compose.foundation.layout.Row
5import androidx.compose.foundation.layout.fillMaxWidth
6import androidx.compose.foundation.layout.padding
7import androidx.compose.foundation.layout.size
8import androidx.compose.foundation.layout.widthIn
9import androidx.compose.foundation.horizontalScroll
10import androidx.compose.foundation.rememberScrollState
11import androidx.compose.material.icons.Icons
12import androidx.compose.material.icons.filled.Close
13import androidx.compose.material.icons.filled.Description
14import androidx.compose.material3.CircularProgressIndicator
15import androidx.compose.material3.Icon
16import androidx.compose.material3.IconButton
17import androidx.compose.material3.MaterialTheme
18import androidx.compose.material3.Surface
19import androidx.compose.material3.Text
20import androidx.compose.runtime.Composable
21import androidx.compose.ui.Alignment
22import androidx.compose.ui.Modifier
23import androidx.compose.ui.draw.clip
24import androidx.compose.ui.layout.ContentScale
25import androidx.compose.ui.res.stringResource
26import androidx.compose.ui.semantics.contentDescription
27import androidx.compose.ui.semantics.semantics
28import androidx.compose.ui.text.style.TextOverflow
29import androidx.compose.ui.unit.dp
30import coil.compose.AsyncImage
31import soy.iko.opencode.R
34 * The staged-attachment UI for the chat composer — extracted from ChatScreen so that (large)
35 * file stays focused on the conversation surface. `internal` so ChatScreen can call it.
38internal fun AttachmentStrip(
39 attachments: List<PendingAttachment>,
40 onRemove: (String) -> Unit,
41 staging: Boolean = false,
43 // Show the strip while an encode is in flight even before the first chip exists, so the
44 // user gets immediate feedback that their pick is being processed.
45 if (attachments.isEmpty() && !staging) return
46 // Horizontally-scrollable strip of staged attachment thumbnails/chips, each removable.
50 .horizontalScroll(rememberScrollState())
51 .padding(start = 8.dp, end = 8.dp, top = 8.dp),
52 horizontalArrangement = Arrangement.spacedBy(8.dp),
54 attachments.forEach { att ->
55 AttachmentChip(att, onRemove = { onRemove(att.id) })
57 // A placeholder chip with an indeterminate spinner while any pick is still being
58 // read + base64-encoded off the main thread (chips only materialize once done).
59 if (staging) StagingChip()
63/** A placeholder chip shown while an attachment is being encoded. */
65private fun StagingChip() {
66 val stagingLabel = stringResource(R.string.attachment_staging)
68 shape = MaterialTheme.shapes.small,
69 color = MaterialTheme.colorScheme.surfaceVariant,
70 tonalElevation = 1.dp,
73 verticalAlignment = Alignment.CenterVertically,
74 modifier = Modifier.padding(horizontal = 10.dp),
76 CircularProgressIndicator(
77 Modifier.size(18.dp).semantics { contentDescription = stagingLabel },
82 style = MaterialTheme.typography.labelMedium,
84 modifier = Modifier.padding(start = 8.dp, top = 10.dp, bottom = 10.dp),
90/** A single staged attachment: an image thumbnail (or a generic file icon) with its name
91 * and a remove button. */
93private fun AttachmentChip(attachment: PendingAttachment, onRemove: () -> Unit) {
95 shape = MaterialTheme.shapes.small,
96 color = MaterialTheme.colorScheme.surfaceVariant,
97 tonalElevation = 1.dp,
99 Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 6.dp)) {
100 if (attachment.previewModel != null) {
102 model = attachment.previewModel,
103 contentDescription = attachment.name,
104 contentScale = ContentScale.Crop,
105 modifier = Modifier.size(36.dp).clip(MaterialTheme.shapes.extraSmall),
109 Icons.Filled.Description,
110 contentDescription = null,
111 modifier = Modifier.size(24.dp),
112 tint = MaterialTheme.colorScheme.onSurfaceVariant,
117 style = MaterialTheme.typography.labelMedium,
119 overflow = TextOverflow.Ellipsis,
120 modifier = Modifier.widthIn(max = 120.dp).padding(horizontal = 6.dp),
122 // No .size() override so the IconButton keeps its default 48dp touch target
123 // (a11y minimum) while the visual X stays small (16dp).
124 IconButton(onClick = onRemove) {
127 contentDescription = stringResource(R.string.remove),
128 modifier = Modifier.size(16.dp),
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