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 }
30fun TodoItem.statusEnum(): TodoStatus = when (status.trim().lowercase()) {
31 "pending", "todo", "" -> TodoStatus.PENDING
32 "in_progress", "in-progress", "inprogress", "running", "active" -> TodoStatus.IN_PROGRESS
33 "completed", "complete", "done" -> TodoStatus.COMPLETED
34 "cancelled", "canceled", "skipped" -> TodoStatus.CANCELLED
35 else -> TodoStatus.UNKNOWN
39private data class TodoWriteInput(val todos: List<TodoItem> = emptyList())
41// A lenient decoder mirroring OpencodeJson's resilience (unknown keys ignored, nulls
42// coerced to defaults) without pulling the network layer into data/model.
43private val todoJson = Json {
44 ignoreUnknownKeys = true
46 coerceInputValues = true
51 * Extract the todo list from a `todowrite` tool call's input JSON. The tool sends the
52 * full list on every write, so a single call's input is a complete plan snapshot.
53 * Accepts either the documented `{ "todos": [...] }` object or a bare array, and returns
54 * an empty list on anything unparseable.
56fun parseTodos(input: JsonElement?): List<TodoItem> {
57 if (input == null) return emptyList()
59 todoJson.decodeFromJsonElement(TodoWriteInput.serializer(), input).todos
61 todoJson.decodeFromJsonElement(ListSerializer(TodoItem.serializer()), input)
62 }.getOrDefault(emptyList())
66 * The current task plan for a conversation: the todos of the most recent `todowrite`
67 * tool call that carries a non-empty, parseable list. opencode rewrites the whole list
68 * on every write, so a single call is a complete snapshot; scanning newest-first and
69 * skipping empties avoids a mid-stream write (input not yet arrived) briefly blanking a
70 * plan that's still valid.
72fun currentTodoPlan(messages: List<MessageWithParts>): List<TodoItem> {
73 for (mi in messages.indices.reversed()) {
74 val parts = messages[mi].parts
75 for (pi in parts.indices.reversed()) {
77 if (part is ToolPart && part.tool.equals(TODO_WRITE_TOOL, ignoreCase = true)) {
78 val todos = parseTodos(part.state.inputElement())
79 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