1package soy.iko.opencode.ui.chat
3import androidx.compose.foundation.background
4import androidx.compose.foundation.layout.Arrangement
5import androidx.compose.foundation.layout.Column
6import androidx.compose.foundation.layout.Row
7import androidx.compose.foundation.layout.fillMaxWidth
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.widthIn
10import androidx.compose.foundation.shape.RoundedCornerShape
11import androidx.compose.material3.MaterialTheme
12import androidx.compose.material3.Text
13import androidx.compose.runtime.Composable
14import androidx.compose.ui.Alignment
15import androidx.compose.ui.Modifier
16import androidx.compose.ui.draw.clip
17import androidx.compose.ui.unit.dp
18import soy.iko.opencode.data.model.AssistantMessage
19import soy.iko.opencode.data.model.MessageWithParts
20import soy.iko.opencode.data.model.UserMessage
22/** A single message: user prompts right-aligned in a bubble, assistant output full-width. */
24fun MessageBubble(message: MessageWithParts, isRunning: Boolean = false, modifier: Modifier = Modifier) {
26 is UserMessage -> UserBubble(message, modifier)
27 else -> AssistantBlock(message, isRunning, modifier)
32private fun UserBubble(message: MessageWithParts, modifier: Modifier) {
33 Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
36 .widthIn(max = 320.dp)
37 .clip(RoundedCornerShape(16.dp))
38 .background(MaterialTheme.colorScheme.primaryContainer)
40 verticalArrangement = Arrangement.spacedBy(6.dp),
42 for (part in message.parts) PartView(part)
48private fun AssistantBlock(message: MessageWithParts, isRunning: Boolean, modifier: Modifier) {
50 modifier = modifier.fillMaxWidth(),
51 verticalArrangement = Arrangement.spacedBy(8.dp),
53 val info = message.info
54 if (info is AssistantMessage && info.modelID != null) {
57 style = MaterialTheme.typography.labelSmall,
58 color = MaterialTheme.colorScheme.onSurfaceVariant,
61 for (part in message.parts) {
62 PartView(part, isRunning = isRunning, modifier = Modifier.fillMaxWidth())
64 if (info is AssistantMessage) {
66 val tokens = info.tokens
67 if (info.isComplete && (cost != null || tokens != null)) {
68 val parts = buildList {
69 tokens?.let { add("${it.input}→${it.output} tok") }
70 cost?.takeIf { it > 0 }?.let { add("$%.4f".format(it)) }
72 if (parts.isNotEmpty()) {
74 parts.joinToString(" • "),
75 style = MaterialTheme.typography.labelSmall,
76 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