1package soy.iko.opencode.ui.chat
3import androidx.compose.animation.AnimatedVisibility
4import androidx.compose.foundation.clickable
5import androidx.compose.foundation.layout.Arrangement
6import androidx.compose.foundation.layout.Column
7import androidx.compose.foundation.layout.Row
8import androidx.compose.foundation.layout.Spacer
9import androidx.compose.foundation.layout.defaultMinSize
10import androidx.compose.foundation.layout.fillMaxWidth
11import androidx.compose.foundation.layout.heightIn
12import androidx.compose.foundation.layout.padding
13import androidx.compose.foundation.layout.size
14import androidx.compose.foundation.rememberScrollState
15import androidx.compose.foundation.verticalScroll
16import androidx.compose.material.icons.Icons
17import androidx.compose.material.icons.filled.Autorenew
18import androidx.compose.material.icons.filled.Cancel
19import androidx.compose.material.icons.filled.CheckCircle
20import androidx.compose.material.icons.filled.Checklist
21import androidx.compose.material.icons.filled.ExpandLess
22import androidx.compose.material.icons.filled.ExpandMore
23import androidx.compose.material.icons.filled.RadioButtonUnchecked
24import androidx.compose.material3.Icon
25import androidx.compose.material3.MaterialTheme
26import androidx.compose.material3.Surface
27import androidx.compose.material3.Text
28import androidx.compose.runtime.Composable
29import androidx.compose.runtime.getValue
30import androidx.compose.runtime.mutableStateOf
31import androidx.compose.runtime.remember
32import androidx.compose.runtime.saveable.rememberSaveable
33import androidx.compose.runtime.setValue
34import androidx.compose.ui.Alignment
35import androidx.compose.ui.Modifier
36import androidx.compose.ui.res.stringResource
37import androidx.compose.ui.semantics.Role
38import androidx.compose.ui.semantics.contentDescription
39import androidx.compose.ui.semantics.semantics
40import androidx.compose.ui.semantics.stateDescription
41import androidx.compose.ui.text.font.FontWeight
42import androidx.compose.ui.text.style.TextDecoration
43import androidx.compose.ui.unit.dp
44import soy.iko.opencode.R
45import soy.iko.opencode.data.model.TodoItem
46import soy.iko.opencode.data.model.TodoPriority
47import soy.iko.opencode.data.model.TodoStatus
48import soy.iko.opencode.data.model.priorityEnum
49import soy.iko.opencode.data.model.statusEnum
50import soy.iko.opencode.ui.components.rememberVisibilityTransitions
52/** Renders a task plan as a checklist: a status icon per row plus its description, with
53 * completed/cancelled items struck through and the in-progress item emphasized. */
55fun TodoPlanChecklist(todos: List<TodoItem>, modifier: Modifier = Modifier) {
56 Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
57 for (todo in todos) TodoRow(todo)
62private fun TodoRow(todo: TodoItem) {
63 val status = todo.statusEnum()
64 val priority = todo.priorityEnum()
65 val struck = status == TodoStatus.COMPLETED || status == TodoStatus.CANCELLED
66 Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
67 TodoStatusIcon(status)
70 style = MaterialTheme.typography.bodySmall,
72 status == TodoStatus.IN_PROGRESS -> MaterialTheme.colorScheme.primary
73 struck -> MaterialTheme.colorScheme.onSurfaceVariant
74 else -> MaterialTheme.colorScheme.onSurface
76 fontWeight = if (status == TodoStatus.IN_PROGRESS) FontWeight.Medium else null,
77 textDecoration = if (struck) TextDecoration.LineThrough else null,
78 modifier = Modifier.padding(start = 8.dp).weight(1f),
80 // Priority badge: a small uppercase label to the right of the content. Tinted to
81 // distinguish high (error) from medium (tertiary) from low (muted); skipped entirely
82 // when the server didn't report a priority (NONE) so a plain todo doesn't get a
83 // meaningless "LOW" chip. Struck-through items keep the badge but muted, so a
84 // completed/cancelled high-priority task still records its original urgency.
85 if (priority != TodoPriority.NONE) {
86 val label = when (priority) {
87 TodoPriority.HIGH -> stringResource(R.string.todo_priority_high)
88 TodoPriority.MEDIUM -> stringResource(R.string.todo_priority_medium)
89 TodoPriority.LOW -> stringResource(R.string.todo_priority_low)
90 TodoPriority.NONE -> ""
92 val tint = when (priority) {
93 TodoPriority.HIGH -> if (struck) MaterialTheme.colorScheme.onSurfaceVariant
94 else MaterialTheme.colorScheme.error
95 TodoPriority.MEDIUM -> if (struck) MaterialTheme.colorScheme.onSurfaceVariant
96 else MaterialTheme.colorScheme.tertiary
97 TodoPriority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant
98 TodoPriority.NONE -> MaterialTheme.colorScheme.onSurfaceVariant
100 val a11yLabel = stringResource(R.string.todo_priority_label, label)
103 style = MaterialTheme.typography.labelSmall,
106 .padding(start = 8.dp, top = 2.dp)
107 .semantics { contentDescription = a11yLabel },
114private fun TodoStatusIcon(status: TodoStatus) {
115 val icon = when (status) {
116 TodoStatus.COMPLETED -> Icons.Filled.CheckCircle
117 TodoStatus.IN_PROGRESS -> Icons.Filled.Autorenew
118 TodoStatus.CANCELLED -> Icons.Filled.Cancel
119 else -> Icons.Filled.RadioButtonUnchecked
121 val tint = when (status) {
122 TodoStatus.COMPLETED, TodoStatus.IN_PROGRESS -> MaterialTheme.colorScheme.primary
123 else -> MaterialTheme.colorScheme.onSurfaceVariant
125 val desc = when (status) {
126 TodoStatus.COMPLETED -> R.string.todo_completed
127 TodoStatus.IN_PROGRESS -> R.string.todo_in_progress
128 TodoStatus.CANCELLED -> R.string.todo_cancelled
129 else -> R.string.todo_pending
133 contentDescription = stringResource(desc),
135 modifier = Modifier.size(16.dp),
139/** A collapsible plan bar pinned above the composer: the header shows completed/total and
140 * a chevron; expanding reveals the full (scrollable, height-capped) checklist. Renders
141 * nothing when there's no plan, so the caller can place it unconditionally. */
143fun TodoPlanBar(todos: List<TodoItem>, modifier: Modifier = Modifier) {
144 if (todos.isEmpty()) return
145 var expanded by rememberSaveable { mutableStateOf(false) }
146 val completed = remember(todos) { todos.count { it.statusEnum() == TodoStatus.COMPLETED } }
147 val expandedState = stringResource(R.string.state_expanded)
148 val collapsedState = stringResource(R.string.state_collapsed)
150 color = MaterialTheme.colorScheme.surfaceVariant,
151 tonalElevation = 2.dp,
152 modifier = modifier.fillMaxWidth(),
158 .defaultMinSize(minHeight = 48.dp)
159 .clickable(role = Role.Button) { expanded = !expanded }
160 .semantics { stateDescription = if (expanded) expandedState else collapsedState }
161 .padding(horizontal = 12.dp, vertical = 6.dp),
162 verticalAlignment = Alignment.CenterVertically,
165 Icons.Filled.Checklist,
166 contentDescription = null,
167 tint = MaterialTheme.colorScheme.onSurfaceVariant,
168 modifier = Modifier.size(18.dp),
171 stringResource(R.string.plan),
172 style = MaterialTheme.typography.labelLarge,
173 modifier = Modifier.padding(start = 8.dp),
176 stringResource(R.string.plan_progress, completed, todos.size),
177 style = MaterialTheme.typography.labelMedium,
178 color = MaterialTheme.colorScheme.onSurfaceVariant,
179 modifier = Modifier.padding(start = 8.dp),
181 Spacer(Modifier.weight(1f))
183 if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
184 contentDescription = null,
185 tint = MaterialTheme.colorScheme.onSurfaceVariant,
186 modifier = Modifier.size(20.dp),
189 val expandMotion = rememberVisibilityTransitions()
192 enter = expandMotion.enter,
193 exit = expandMotion.exit,
198 .heightIn(max = 240.dp)
199 .verticalScroll(rememberScrollState())
200 .padding(start = 12.dp, end = 12.dp, bottom = 10.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