Todo
OpenCode v2 plugin: an OMP-style phased todo tool with reminders, /todo commands, and a todo-discipline skill.
0
0
20.0
Multi-signal model
2 days ago
2026-10-02
Install and configure
opencode.jsonWrites to this project's opencode.json — applies to this repository only.
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@falentio/opencode-todo@0.1.0"]
}Writes to ~/.config/opencode/opencode.json — applies to every project.
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@falentio/opencode-todo@0.1.0"]
}If you want to modify the plugin locally, install it into the project and reference the local path.
shell
pnpm add -D @falentio/opencode-todoOpenCode loads npm dependencies through its embedded runtime on startup and caches them locally — no manual global install needed.
An OpenCode v2 plugin that gives the agent a phased todo list, and gives you a
/todo command to inspect and edit it.
A port of @gamaraan/todos-tool,
a pi coding-agent extension, which is itself a port of Oh My Pi's
todo tool. The tool's semantics, prompts, and tests come from that lineage.
Todo 2/5 done
I. Foundation 1/2
✓ Scaffold crate
○ Wire workspace
II. Auth 1/3
○ Port credential store
What the agent gets
A todo tool with nine operations. Tasks are addressed by their exact content
string, never by a generated ID.
op |
Fields | Effect |
|---|---|---|
init |
list: [{phase, items}] |
Replace the whole list |
start |
task |
Mark in progress |
done |
task or phase |
Mark completed |
drop |
task or phase |
Mark abandoned |
block |
task or phase; optional reason |
Mark blocked, waiting on something external |
unblock |
task or phase |
Blocked task back to pending |
rm |
optional task or phase |
Remove; omit both to clear |
append |
phase; items |
Add tasks, creating the phase if needed |
view |
none | Read-only echo |
Three behaviors matter when reading the code:
- Auto-promote fires only when nothing is in progress. Completing a task with no in-progress task promotes the earliest open one. Out-of-order work can move the pointer back to an earlier phase, which is expected; a completed task never reverts.
- A failing batch is discarded whole. A half-applied batch makes a retry hit "already exists" for the operations that did land, so nothing is applied and the previous state stands.
- A missing
opis repaired when the shape is unambiguous.{list: [...]}isinit,{phase, items}isappend, and bareitemson an empty list isinit. Every other shape is an error.
What you get
/todo Show current todos
/todo edit Open todos in $EDITOR
/todo copy Print todos as Markdown
/todo export [<path>] Write todos to a file (default TODO.md)
/todo import [<path>] Replace todos from a file (default TODO.md)
/todo append [<phase>] <task...> Append a task, creating the phase if needed
/todo start <task> Mark a task in progress
/todo done [<task|phase>] Mark a task, a phase, or everything completed
/todo drop [<task|phase>] Mark abandoned
/todo rm [<task|phase>] Remove
Task and phase arguments match fuzzily: exact first, then a unique prefix, then a unique substring. Manual edits are recorded and the model is told what changed, including an explicit "do not recreate" instruction after a removal.
Session behavior
While the tool is enabled, the plugin watches the session and can inject three hidden messages.
- Eager prelude. With
eagerset topreferredoralways, the first turn of a new session asks the model to lay out a phased plan with oneinitcall. A resumed session never injects it, and neither does a prompt ending in?or!. OpenCode cannot force a tool call, soalwaysinjects a MUST-call reminder rather than atool_choice. - Mid-run nudge. After 12 successful mutating tool calls with work still open, a nudge asks the model to mark finished tasks done. At most two per cycle.
- Completion reminder. When the model stops with work still open, a reminder
lists the open items and starts a fresh turn. It never fires while the model is
waiting on your answer, and it pauses until the model makes progress.
remindersMaxcaps it at 3 per cycle by default.
The plugin also contributes a todo-discipline skill. Its description sits in
every system prompt, and it mandates a phased init before multi-step work and
marking each task done as it finishes rather than batching at the end.
Sidebar
The todo list renders live in the session sidebar. The sidebar is part of the TUI, so it needs a TUI plugin entrypoint rather than the server plugin that provides the tool:
~/.config/opencode/cli.json
{
"plugins": ["@falentio/opencode-todo"]
}
The list appears when the terminal is wide enough, because session.sidebar
defaults to "auto" and only opens above 120 columns. Set it to "auto" or
"show" in cli.json to control that.
Rows lead with the overall count, then each phase with its own done count, then
that phase's open tasks. A phase with more tasks than fit collapses to the most
relevant ones and a … N more line, the same walking viewport the pi extension's
HUD used.
1/3 done
Foundation 1/2
✓ scaffold crate
○ wire workspace
Auth 0/1
○ port credential store
The view reads the todo state the tool already reports, so nothing extra is
persisted and there is no side channel. It updates on each todo tool result.
A /todo edit changes the stored list without producing a tool result, so it
appears on the next todo tool result rather than immediately.
Install
Two plugin entrypoints, so two config files. The server plugin registers the
todo tool, the /todo command, the skill, and the session reminders. The TUI
plugin paints the sidebar.
From npm, add the server plugin to opencode.json:
{
"plugins": ["@falentio/opencode-todo"]
}
Add the same package to cli.json to get the sidebar:
{
"plugins": ["@falentio/opencode-todo"]
}
From a checkout, symlink the package directory into both discovery paths:
vp install && vp pack
ln -s "$PWD" ~/.config/opencode/plugins/opencode-todo
A checkout resolves the server entrypoint through .opencode/plugins/, and the
TUI entrypoint through the plugins array in cli.json or opencode.json.
Configure
Settings come from four places, each overriding the last:
- built-in defaults
~/.config/opencode/todo.json<project>/.opencode/todo.jsonOPENCODE_TODO_*environment variables, then the plugin entry'soptions
{ "enabled": true, "reminders": true, "remindersMax": 3, "eager": "default" }
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Registers the tool and every session behavior. A false in the global file is a floor; nothing can re-enable it. |
reminders |
true |
Stop-time reminders. |
remindersMax |
3 |
Reminder attempts per cycle. |
eager |
"default" |
"default", "preferred", or "always". |
The environment variables are OPENCODE_TODO_ENABLED, OPENCODE_TODO_REMINDERS,
OPENCODE_TODO_REMINDERS_MAX, and OPENCODE_TODO_EAGER. An invalid value warns
and keeps the previous one instead of silently falling back to the default.
How it is built
State lives in OpenCode's plugin storage, one key per session. There is no sidecar file.
ctx.tool.transform → the todo tool
ctx.command.transform → /todo
ctx.skill.transform → todo-discipline
ctx.session.hook → reads history, feeds the reminder tracker
ctx.session.synthetic → injects a reminder and starts a turn
ctx.storage → the persisted phase list
ctx.ui.slot (TUI) → the sidebar view
src/ splits into a host-free core and thin v2 adapters:
src/
types.ts TodoStatus, TodoPhase, the tool's JSON Schema
validate.ts raw arguments and stored snapshots → typed values
state.ts pure reducers: every op, normalization, op inference
execute.ts one op against the phase list, with batch atomicity
format.ts the summary the model reads
hud.ts the sidebar view model (rows, counts, truncation)
markdown.ts Markdown round-trip for export, import, and edit
persistence.ts one storage key per session
config.ts file, environment, and option precedence
prompts.ts tool description and injected-message text
tracker.ts eager prelude, mid-run nudge, completion reminder
command.ts the /todo verbs
skill.ts loads the bundled skill
index.ts wiring only
tui.tsx the sidebar slot adapter
The pure modules carry no OpenCode import. They are testable alone, and the
ported test suite exercises them directly. tui.tsx is the one exception and
stays a thin shell over hud.ts, so the view logic is unit tested rather than
eyeballed in a terminal.
Develop
vp install
vp test run # 174 unit tests
npx tsc --noEmit # typecheck
vp check # format and lint
vp pack # build dist/
node scripts/smoke.mjs
node scripts/sidebar-check.mjs
node scripts/publish-probe.mjs
scripts/smoke.mjs drives two real opencode processes in a sandbox directory,
then asserts against the exported session transcript. It checks that the tool is
in the model's catalog, that init ran and persisted, that a second process read
the list back with view, that the reminder fired, and that the command and
skill registered. Add --load to skip the model calls.
The transcript is the evidence rather than the model's reply, because a model will sometimes report "there is no todo tool" while the tool result sits in the transcript.
scripts/sidebar-check.mjs proves the sidebar render. It boots one
opencode serve, attaches both a CLI and a TUI to it, drives a real todo list,
and asserts against the painted terminal log, because the sidebar is drawn by
the TUI and never appears in the transcript. Add --boot to skip the model
calls. Two processes with --standalone would each get a private server, so no
event could cross between them; one shared server is what makes the check real.
scripts/publish-probe.mjs proves the packed artifact works the way a published
install uses it. It runs two arms. The first npm-installs the tarball and imports
the entrypoint with a bare node process, which has no access to the checkout's
node_modules. The second unpacks the tarball under a node_modules path and
boots the TUI against it, then reads the loader log for both entrypoints.
Both arms catch failures a checkout hides. dist/index.mjs must not import a
package it does not declare, because an optional peer installs nothing and the
import throws only on a real install. The TUI entrypoint must not be raw .tsx,
because the host's JSX transform skips paths under node_modules, so a .tsx
entry works from a symlinked checkout and fails once published. Add --keep to
keep the sandbox.
Differences from the pi extension
| pi extension | This plugin |
|---|---|
| Session branch replay for persistence | ctx.storage, one key per session |
| Custom TUI rendering (roman numerals, collapsed viewport) | The session sidebar; no roman numerals |
| Desktop notifications over the pi EventBus | Dropped; v2 has no plugin-facing notification bus |
--todo-* CLI flags |
Environment variables and plugin options |
$EDITOR fallback outside the TUI |
$EDITOR only; v2 has no plugin editor dialog |
OSC 52 clipboard for /todo copy |
Prints the Markdown |
License
MIT. Ported from Oh My Pi (MIT, © Can Bölük) and pi (MIT, © Mario Zechner).
Similar plugins
Skills Tui
opencode-skills-tui
OpenCode TUI plugin that shows skills and loaded state in the sidebar
Sidebar Token Metrics
opencode-sidebar-token-metrics
OpenCode sidebar plugin showing generation speed (tok/s), cache hit rate, and average time-to-first-token (TTFT).
Tui Quota Usage
opencode-tui-quota-usage
OpenCode quota usage sidebar tracker for OpenCode V2 CLI/TUI