1package soy.iko.opencode.data.model
3import androidx.compose.runtime.Immutable
4import kotlinx.serialization.Serializable
5import kotlinx.serialization.builtins.ListSerializer
6import kotlinx.serialization.json.Json
7import kotlinx.serialization.json.JsonElement
9/** The tool name opencode uses to write the agent's task plan. */
10const val TODO_WRITE_TOOL = "todowrite"
13 * A single entry in an agent's task plan, as written by opencode's `todowrite` tool.
14 * Every field is defaulted and decoding ignores unknown keys (see [parseTodos]) so a
15 * change to the tool's payload shape degrades to a best-effort render instead of
16 * dropping the whole plan.
22 val content: String = "",
23 val status: String = "pending",
24 val priority: String? = null,
27/** Normalized lifecycle of a [TodoItem]; [UNKNOWN] covers any status the client doesn't model. */
28enum class TodoStatus { PENDING, IN_PROGRESS, COMPLETED, CANCELLED, UNKNOWN }
30/** Normalized priority of a [TodoItem]; [NONE] covers null/unmodelled priorities. The order
31 * matters: HIGH > MEDIUM > LOW, used by [TodoPlan] tinting and a11y labels. */
32enum class TodoPriority { HIGH, MEDIUM, LOW, NONE }
34fun TodoItem.priorityEnum(): TodoPriority = when ((priority ?: "").trim().lowercase()) {
35 "high", "h", "1", "urgent", "critical" -> TodoPriority.HIGH
36 "medium", "med", "m", "normal", "2" -> TodoPriority.MEDIUM
37 "low", "l", "3", "minor", "backlog" -> TodoPriority.LOW
38 else -> TodoPriority.NONE
41fun TodoItem.statusEnum(): TodoStatus = when (status.trim().lowercase()) {
42 "pending", "todo", "" -> TodoStatus.PENDING
43 "in_progress", "in-progress", "inprogress", "running", "active" -> TodoStatus.IN_PROGRESS
44 "completed", "complete", "done" -> TodoStatus.COMPLETED
45 "cancelled", "canceled", "skipped" -> TodoStatus.CANCELLED
46 else -> TodoStatus.UNKNOWN
50private data class TodoWriteInput(val todos: List<TodoItem> = emptyList())
52// A lenient decoder mirroring OpencodeJson's resilience (unknown keys ignored, nulls
53// coerced to defaults) without pulling the network layer into data/model.
54private val todoJson = Json {
55 ignoreUnknownKeys = true
57 coerceInputValues = true
62 * Extract the todo list from a `todowrite` tool call's input JSON. The tool sends the
63 * full list on every write, so a single call's input is a complete plan snapshot.
64 * Accepts either the documented `{ "todos": [...] }` object or a bare array, and returns
65 * an empty list on anything unparseable.
67fun parseTodos(input: JsonElement?): List<TodoItem> {
68 if (input == null) return emptyList()
70 todoJson.decodeFromJsonElement(TodoWriteInput.serializer(), input).todos
72 todoJson.decodeFromJsonElement(ListSerializer(TodoItem.serializer()), input)
73 }.getOrDefault(emptyList())
77 * The current task plan for a conversation: the todos of the most recent `todowrite`
78 * tool call that carries a non-empty, parseable list. opencode rewrites the whole list
79 * on every write, so a single call is a complete snapshot; scanning newest-first and
80 * skipping empties avoids a mid-stream write (input not yet arrived) briefly blanking a
81 * plan that's still valid.
83fun currentTodoPlan(messages: List<MessageWithParts>): List<TodoItem> {
84 for (mi in messages.indices.reversed()) {
85 val parts = messages[mi].parts
86 for (pi in parts.indices.reversed()) {
88 if (part is ToolPart && part.tool.equals(TODO_WRITE_TOOL, ignoreCase = true)) {
89 val todos = parseTodos(part.state.inputElement())
90 if (todos.isNotEmpty()) return todos
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