kolu
Docs

awareness

kolu knows what your agents are doing.

kolu watches the foreground process of every terminal. When that process is a coding agent it recognizes — Claude Code, Codex, Grok Build, or OpenCode — it reads that agent’s own on-disk session to derive whether it’s thinking, running a tool, waiting on you, or done. That state paints the tile chrome, the dock row, and the minimap, so a glance across a workspace of twenty terminals tells you who needs you.

There is no agent registry to configure and no per-agent setup. Run a supported agent in any terminal and detection lights up on its own.

One state vocabulary

Every agent folds onto the same small set of states, drawn with the same glyphs everywhere kolu shows agent status. The shape carries the meaning, so the distinction survives a peripheral glance or reduced color sensitivity.

The five agent states and their indicators: thinking is a pulsing accent dot, tool use a pulsing amber dot, awaiting you a pulsing alert dot, working-in-background a spinning teal ring, and waiting a dim dot.ThinkinggeneratingTool userunning toolsAwaiting youblocked on inputWorkingbackground taskWaitingidle, your turn

The awaiting-you hue sits deliberately outside the warm/pending family, so an agent that needs you never reads as a build still churning. When an agent finishes in the background and you haven’t looked yet, its whole row washes amber — and a small amber unread badge appears on the pip — until you focus it.

How detection works

The pipeline is the same for every agent; only the last step — which on-disk store holds the session — differs.

Detection reads each terminal’s foreground process id, checks whether it’s a known agent CLI, reads that agent’s own session store (a JSONL transcript or a SQLite database), and derives the live state shown on the dock and tile.foregroundprocess idknown agent?claude · codex · …session storeJSONL · SQLitelive statedock · tile · minimap

kolu asks each terminal for its foreground process id (tcgetpgrp, POSIX — Linux and macOS alike), matches it against the known agents, and reads that agent’s session from disk — tailing a JSONL transcript, or querying a SQLite database and watching its write-ahead log. Those file-change watches are backed by a low-rate stat reconciliation, so if the OS drops the change notification for a fast turn’s final write, the state self-heals within about a second instead of stranding on its last reading. In a normal shell terminal that’s enough: the shell prints a marker naming each command as it runs — part of kolu’s shell integration — and the agent’s process becomes the foreground. When an agent is launched as the terminal’s root process — no shell wrapping it, e.g. kaval-tui create -- codex — there is no shell to print that marker, so kolu instead remembers the exact command the terminal was spawned with and uses it as the recognition hint (and reads that root’s foreground as busy, since the agent is the root). kolu never scrapes a running process’s argv from /proc; it uses only the shell’s marks and the command it was handed at spawn. Adding a new agent is one small adapter and one line of registration; nothing server-wide changes.

Claude Code

kolu finds a Claude session by checking whether ~/.claude/sessions/<foreground-pid>.json exists, then tails its JSONL transcript to derive state. The tile also shows the session’s title and a running token count.

StateIndicatorMeaning
Thinkingpulsing accent dotan API call is in flight — Claude is generating
Tool usepulsing amber dotClaude is executing tools
Running in backgroundspinning teal ringClaude ended its turn while a dynamic workflow it launched is still running — busy-waiting, not blocked on you
Awaiting inputpulsing alertblocked on you — an AskUserQuestion prompt or a tool-permission gate
Waitingdim dotfinished responding (or interrupted with Esc) and idle

The awaiting-input state is the subtle one: while a question or permission prompt is pending, the transcript doesn’t reveal it, so kolu reads the rendered screen for the framework’s prompt chrome and buckets it as awaiting you.

What it can’t detect:

  • Plan approval (ExitPlanMode) — its on-screen shape has no arrow-nav footer to anchor on yet; a deliberate follow-up.
  • Streaming progress — only final state transitions, not intermediate tokens.
  • Wrapped invocations — launched via a wrapper (e.g. script -q out.log claude), the foreground pid is the wrapper, so the session lookup misses.
  • Sub-agents — nested spawns aren’t tracked as separate sessions. (When the parent waits on a launched dynamic workflow, kolu does surface that workflow’s name and live sub-agent count.)

Codex

When the foreground process is codex, kolu queries Codex’s threads SQLite database (the highest-numbered ~/.codex/state_<N>.sqlite) for the live threads whose working directory matches the terminal — most recently updated first — takes the best one no other terminal already holds, and tails that thread’s rollout JSONL for state and context-token count. Live updates ride a watch on the SQLite write-ahead log.

StateIndicatorMeaning
Thinkingpulsing accent dotlatest lifecycle event is task_started, no open tool call
Tool usespinning ambera function_call is open that isn’t an awaiting-user tool
Awaiting inputpulsing alertevery open call is an awaiting-user tool — request_user_input, request_permissions, or request_plugin_install
Waitingdim dotlatest lifecycle event is task_complete

The context count is Codex’s own last_token_usage.input_tokens — the number its /status command shows — not a sum that would double-count cache re-reads.

What it can’t detect (yet):

  • Task-progress checklist — Codex has no user-visible todo list to read.
  • Column-schema changes — if an upstream release renames a depended-on column, detection returns no match; set KOLU_CODEX_DB to pin a known-good database while a fix ships.
  • Which thread is which — Codex records no process id, only a thread creation time, so with several threads in one directory kolu can guarantee that no two terminals show the same one and that a terminal ends up on a thread created after its own harness started — not which of several such threads is really its own. A thread started inside a running Codex (/new) is therefore indistinguishable from one started elsewhere in that directory, so the tile stays on the previous thread until you exit the agent.

Grok

When the foreground process is grok, kolu prefers the entry in ~/.grok/active_sessions.json whose pid matches the terminal; with no pid to go on it takes the most recently updated session under ~/.grok/sessions/ that no other terminal already holds. That file is only ever used to find a session, never to decide one has ended — Grok rewrites the whole list from each session’s own point of view, so a session starting or ending erases the entries of every other Grok running at that moment. Once a terminal is matched, only the process going away — or the terminal moving on to something else — ends the match. State comes from tailing that session’s events.jsonlphase_changed for thinking / tool use / awaiting you, turn_ended for waiting. An open ask_user_question promotes to awaiting you even mid-tool, so a multiple-choice prompt never reads as “running tools”.

Resume uses grok -c (most recent in the directory) or grok --resume <uuid> (an exact conversation). Launch-shape flags such as --model / -m, --always-approve, --no-plan, --no-subagents, and --no-alt-screen (inline TUI instead of the alternate screen) are kept on the recent agents list and on sleep/wake resume. Context tokens come from signals.json (contextTokensUsed); task progress stays null until Grok’s plan checklist is a stable first-class count. Transcript export reads chat_history.jsonl (unwraps Grok’s <user_query> harness so the HTML shows the human prompt).

OpenCode

When the foreground process is opencode, kolu queries OpenCode’s SQLite database directly at ~/.local/share/opencode/opencode.db for the live sessions whose directory matches the terminal — most recently updated first — and takes the best one no other terminal already holds. The database’s write-ahead log drives live updates.

StateIndicatorMeaning
Thinkingpulsing accent dotlatest assistant message has no time.completed
Tool usespinning ambera running part whose tool is neither question nor plan_exit
Awaiting inputpulsing alertevery running part is question or plan_exit — blocked on a reply
Waitingdim dotlatest assistant message is completed with finish: "stop"

Todo progress comes free from a count over OpenCode’s todo table, and the tile shows the running token count OpenCode pre-sums itself.

What it can’t detect (yet):

  • Which session is which — OpenCode records no process id, only the time its first message was written, so with several sessions in one directory kolu can guarantee that no two terminals show the same one and that a terminal ends up on a session begun after its own harness started — not which of several such sessions is really its own. A session started inside a running OpenCode is therefore indistinguishable from one started elsewhere in that directory, so the tile stays on the previous session until you exit the agent.
  • Non-default database location — set KOLU_OPENCODE_DB to override the path.

Adding an agent

Because detection is agent-agnostic, a new CLI doesn’t wait on kolu. Once you’ve run a known agent in any terminal it also surfaces in the command palette — as a launch option in the worktree-naming flow and in the Recent agents group. Teaching kolu to read a brand-new agent’s state is a single adapter that reads its session store and emits the same five states; everything downstream — the dock, the tile border, the minimap, the fleet view — lights up unchanged.