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 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.
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.
| State | Indicator | Meaning |
|---|---|---|
| Thinking | pulsing accent dot | an API call is in flight — Claude is generating |
| Tool use | pulsing amber dot | Claude is executing tools |
| Running in background | spinning teal ring | Claude ended its turn while a dynamic workflow it launched is still running — busy-waiting, not blocked on you |
| Awaiting input | pulsing alert | blocked on you — an AskUserQuestion prompt or a tool-permission gate |
| Waiting | dim dot | finished 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.
| State | Indicator | Meaning |
|---|---|---|
| Thinking | pulsing accent dot | latest lifecycle event is task_started, no open tool call |
| Tool use | spinning amber | a function_call is open that isn’t an awaiting-user tool |
| Awaiting input | pulsing alert | every open call is an awaiting-user tool — request_user_input, request_permissions, or request_plugin_install |
| Waiting | dim dot | latest 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_DBto 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.jsonl — phase_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.
| State | Indicator | Meaning |
|---|---|---|
| Thinking | pulsing accent dot | latest assistant message has no time.completed |
| Tool use | spinning amber | a running part whose tool is neither question nor plan_exit |
| Awaiting input | pulsing alert | every running part is question or plan_exit — blocked on a reply |
| Waiting | dim dot | latest 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_DBto 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.