Skip to content

Activity detection

Kangentic monitors each agent’s activity in real-time, so you always know what your agents are doing without opening their terminals. All fifteen supported agents (Claude Code, Codex CLI, Gemini CLI, Cursor CLI, GitHub Copilot CLI, Grok Build, Antigravity CLI, Goose CLI, OpenCode, Aider, Qwen Code, Kimi Code, Factory Droid, Warp/Oz CLI, and Ollama) feed into a single activity state machine that drives the card indicators, but each uses a different underlying strategy because each CLI exposes activity differently.

Each card shows its agent’s current state, and the state updates as events arrive. The full set of things a card can show is on Card indicators; the two states below are the ones activity detection drives.

StateDescription
Thinking (pulsing animation)Agent is processing, running tools, or generating a response
Idle (static icon)Agent has stopped, hit a permission prompt, or is asking a question
Queued (label)Waiting for a concurrent session slot to open. A session status rather than an activity state. The card shows it before there is an agent to detect
How a session's activity state is decided Four signals feed the activity engine: agent hook events (prompt, tool start, subagent start, background shell start, idle, interrupted), which set and clear every hold; the status heartbeat, which keeps a turn active while output tokens are still growing; raw PTY output with a silence timer, for agents without hooks; and a background shell watcher that polls the process tree every two seconds. The engine holds four facts: whether a permission prompt is pending, whether a turn is active, whether subagents are running, and whether background shells are running. The state is read off those holds in a fixed order. If a permission prompt is pending, the state is permission, shown on the card as Idle with the tooltip Awaiting permission. Otherwise, if a turn is active or subagents or background shells are running, the state is thinking, shown as a pulsing ring. Otherwise the state is idle, shown as a steady amber ring. Two timers guard the edges: thinking settles to idle only after 400 milliseconds of quiet, and a turn that never reports its end is released by a 180 second watchdog. Queued is a session status, not an activity state. SIGNALS Agent hooks set and clear holds Status heartbeat tokens still growing PTY output plus a silence timer Bg-shell watcher 2 s process poll HOLDS permission pending turn active subagents running background shells STATE pending Permission 1 shown as Idle other holds Thinking 2 pulsing ring no hold Idle 3 steady amber
The activity state is a pure function of four holds, checked in order: a pending permission prompt wins (the card shows Idle and the tooltip says Awaiting permission), any live work reads as Thinking, and only nothing at all reads as Idle. Thinking settles to Idle after 400 ms of quiet, and a turn that never reports its end is released after 180 s. Queued is a session status, not an activity state.

Kangentic tracks these events from each agent session. They feed into both the card indicators and the Activity tab in the bottom panel.

EventKeyEffect on IndicatorDescription
Tool starttool_startThinkingAgent began using a tool (Read, Edit, Bash, etc.)
Tool endtool_end(no change)Tool execution completed
PromptpromptThinkingUser submitted a message or the agent resumed after approval
IdleidleIdleAgent stopped, hit a permission wall, or asked a question
InterruptedinterruptedIdleUser interrupted the agent (Escape or interrupt signal)

These events are logged but don’t directly change the card indicator - they support subagent tracking, context management, and system-level operations. Not every agent emits every event (see per-agent strategies below).

EventKeyEffect on IndicatorEmitted byDescription
Session startsession_start(no change)Claude, Gemini, Grok, Qwen, OpenCodeSession was created or resumed, reported by the agents whose hooks carry a session-start event
Session endsession_end(no change)AllSession exited
Notificationnotification(no change)Claude, Gemini, Grok, Qwen, KimiInformational message from the agent (e.g., “context getting full”). Grok also reports a denied tool call here
Turn failedturn_failedIdleClaude, GrokThe turn ended on a service or API error (rate limit, overload, server error) instead of a normal stop, sourced from the CLI’s StopFailure hook. Treated like an interrupt: resets in-flight counters and commits idle immediately, with the error type carried in the event detail so the log reads “service error”
Turn retryingturn_retryingThinking (conditional)Claude, GrokA transient, auto-retried API error (overloaded, server error, rate limit) that the CLI is retrying rather than a final abort. Unlike turn_failed, it keeps a genuinely live turn in the thinking state through the retry backoff instead of showing a false idle, deferring the idle decision to the watchdog timers. If the turn had already wound down or ended, it is treated exactly like turn_failed
Idle hintidle_hintIdle (conditional)ClaudeAgent signaled out-of-band that it is waiting for input (e.g. a Claude “waiting for your input” notice). Settles the session to idle only when nothing else is keeping it thinking (no pending tools, subagents, background shells, or permission prompt); otherwise log-only.
CompactcompactThinkingClaude, Gemini, Copilot, Grok, Qwen, KimiContext compaction occurred
Subagent startsubagent_startThinkingClaude, Grok, KimiA subagent was spawned (increases depth counter)
Subagent stopsubagent_stop(no change)Claude, Grok, KimiA subagent finished (decreases depth counter)
Teammate idleteammate_idle(no change)ClaudeAnother agent in the same project went idle
Task completedtask_completed(no change)ClaudeA task was marked complete
Config changeconfig_change(no change)ClaudeA configuration setting was modified
Worktree createworktree_createThinkingClaudeA git worktree was created
Worktree removeworktree_remove(no change)ClaudeA git worktree was removed
Background shell startbackground_shell_startThinkingClaudeAgent launched a backgrounded Bash tool. Kangentic tracks the detached child so the session isn’t falsely flagged idle while it’s still running.
Background shell endbackground_shell_end(no change)ClaudeA tracked background shell exited. Releases a deferred idle when the last background child finishes.
Model startmodel_start(no change)Gemini, QwenFired before each LLM API call within an agent turn - per-call visibility in the Activity tab.
Model endmodel_end(no change)Gemini, QwenFired after each LLM API call returns. Log-only counterpart to model start.
Tool selection starttool_selection_start(no change)Gemini, QwenFired when the agent is choosing which tool to invoke next.

Kangentic uses a different detection strategy for each agent because each CLI exposes activity differently. All fifteen strategies feed into the same unified state machine, so card indicators behave identically regardless of which agent is running.

AgentStrategy
Claude CodeHooks only
Codex CLIPTY silence timer
Gemini CLIHooks primary, PTY fallback
Cursor CLIPTY silence timer + stream-JSON init event for session ID
GitHub Copilot CLIHooks primary, PTY fallback
Grok BuildHooks (Claude-compatible set) primary, PTY fallback
Antigravity CLIHooks primary, PTY fallback + idle-footer regex
Goose CLIPTY silence timer
OpenCodePlugin events primary (tool.execute.before/after, session.idle), PTY fallback
AiderPTY prompt regex
Qwen CodeHooks (BeforeModel / AfterModel / BeforeToolSelection) primary, PTY fallback
Kimi CodeWire-protocol JSONL primary, PTY fallback
Factory DroidPTY silence timer + cursor-hide first-output marker
Warp (Oz CLI)PTY silence timer
OllamaPTY silence timer (single-turn ollama run)

Claude Code’s native hooks system is the sole source of truth. Kangentic injects an event-bridge.js shim into the session that fires on Claude Code hook events and writes them to a structured log file that Kangentic watches incrementally.

Claude Code hookEffect
PreToolUseThe agent is about to use a tool. Sets thinking
PostToolUseA tool finished. No state change
PostToolUseFailureA tool call failed. No state change, since the agent may retry
UserPromptSubmitYou sent a message. Sets thinking
StopThe agent stopped on its own. Sets idle
StopFailureThe turn ended on a service or API error. Sets idle, logged as turn_failed
PermissionRequestThe agent needs approval. Sets idle
SubagentStart / SubagentStopTrack subagent nesting depth
Notification / PreCompactLogged as internal events
SessionStart / SessionEndLogged as internal events
TeammateIdle / TaskCompletedLogged as internal events
ConfigChangeLogged as an internal event
WorktreeCreate / WorktreeRemoveWorktree lifecycle; create counts as thinking

Hook injection is automatic - no manual setup. All Kangentic artifacts stay in the .kangentic/ directory; nothing is written to your .claude/settings.local.json. Claude is the only agent Kangentic parses a status.json file for (context window percentage, token counts, cost, rate limits).

Codex’s Rust CLI (0.118+) does not read .codex/hooks.json at runtime, so Kangentic falls back to a pure PTY strategy: after each burst of terminal output, a 3-second silence timer marks the session idle. Content deduplication filters repeated TUI frames automatically so resize redraws don’t look like activity.

Codex’s › prompt character stays visible even during tool execution, so a prompt regex can’t be used - the silence timer is the only reliable signal. Kangentic no longer writes a Codex hook file at all: on spawn it only strips any Kangentic-owned entries left in a legacy .codex/hooks.json by an older install, preserving hooks you added yourself and deleting the file when nothing but Kangentic’s entries remained.

Gemini CLI, hooks primary with PTY fallback

Section titled “Gemini CLI, hooks primary with PTY fallback”

Gemini supports hook injection via .gemini/settings.json (reference-counted so concurrent sessions don’t clobber each other). Hooks fire on BeforeAgent, AfterAgent, BeforeModel, AfterModel, BeforeToolSelection, BeforeTool, AfterTool, SessionStart, SessionEnd, Notification, and PreCompress events.

If hooks don’t fire (e.g., early in startup or during a hang), a PTY-based fallback kicks in: Kangentic watches for Gemini’s closed-box TUI border (╰────╯) or the “I’m ready.” message to detect idle, backed by the same 3-second silence timer as Codex. Once hooks start firing, the PTY fallback defers to them.

Cursor’s terminal agent (cursor-agent) has no hooks and no status file. Activity detection is a PTY silence timer, same 3-second threshold as Codex and Warp. Kangentic spawns Cursor in non-interactive mode (cursor-agent -p ... --output-format stream-json) by default so the first line of NDJSON output is an init event containing the model and session ID. That event lets ContextBar resolve the model pill immediately and lets --resume=<id> work reliably across restarts. There is no subagent tracking and no compaction event.

GitHub Copilot CLI, hooks primary with PTY fallback

Section titled “GitHub Copilot CLI, hooks primary with PTY fallback”

Copilot (v1.0+) supports inline hooks written to a per-session config.json in the copilot-config/ directory beside the Kangentic events file. Kangentic wires preToolUse, postToolUse, agentStop, and preCompact hooks; these are the primary activity signal. A PTY silence timer backs them up because Copilot’s statusLine is best-effort (empirically it doesn’t always fire in ConPTY). Stream-output parsing picks up the model label either from the NDJSON stream when invoked with --output-format json or from the interactive TUI’s status bar.

Grok Build, hooks primary with PTY fallback

Section titled “Grok Build, hooks primary with PTY fallback”

Grok Build deliberately clones Claude Code’s hook surface, so Kangentic gets the full Claude-compatible hook set - PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStart/SubagentStop, and friends - written to a Kangentic-owned .grok/hooks/kangentic.json in the working directory. Hooks are the primary signal, but grok gates project hooks on its own folder trust, so a PTY silence timer carries activity until trust lands. There is no idle prompt regex: grok’s ❯ prompt stays visible during active work, the same always-visible-prompt trap as Codex’s ›. Grok has no statusline either; context percentage, token counts, and cost come from tailing the session’s updates.jsonl log.

Antigravity CLI, hooks primary with PTY fallback

Section titled “Antigravity CLI, hooks primary with PTY fallback”

Antigravity (agy) has a lifecycle hook system; Kangentic writes one named hook into .agents/hooks.json in the working directory, wiring PreInvocation (prompt), PostToolUse (tool end), and Stop (idle, with agy’s fullyIdle flag). Hooks don’t fire in print mode, so a PTY fallback covers those spawns: the standard silence timer plus an idle regex on agy’s footer - ? for shortcuts shows at rest, while a running turn shows esc to cancel instead. Conversation IDs arrive in every hook payload, which is also how session capture works.

Like Droid, Antigravity streams no live telemetry to Kangentic, so its context bar shows the same Telemetry: TUI only affordance rather than spinning forever. The agy footer carries the active model and effort, and per-thought token counts appear inline in its own output. Its brain-directory transcript is still parsed for the Transcript tab.

Goose has no hooks wired in this adapter, so activity detection is a PTY silence timer only, with no prompt regex alongside it. That is “not wired” rather than “not possible”: Goose ships its own hook system (plugin-scoped hooks/hooks.json under .agents/plugins/), which is the obvious path to structured activity in a future release. Session IDs need no capture pathway at all, because Kangentic names the session itself and passes -n <id> at spawn.

OpenCode loads plugins from a project-shared .opencode/plugins/ directory at TUI startup. Kangentic copies an activity-stream plugin into that directory at spawn (refcounted across concurrent same-project sessions) so its tool.execute.before, tool.execute.after, event (for session.created / session.idle / session.error) callbacks fire and stream activity back to Kangentic. The plugin captures the native session ID (ses_<id>) from event.properties.info.id. A PTY silence timer covers the gap between plugin idle events.

Aider doesn’t support hook injection, so detection is pure PTY. Kangentic strips ANSI escapes from the output stream and matches a prompt regex at the end of the buffer:

/(?:^|\n)\s*(?:architect|ask|help|context)?>\s*$/

Any terminal output flips the session to thinking; a prompt match flips it back to idle. There is no subagent tracking, no status.json, and no compaction event. This is consistent with Aider’s edit-first, fire-and-forget design.

Qwen Code, hooks primary with PTY fallback

Section titled “Qwen Code, hooks primary with PTY fallback”

Qwen Code is a soft fork of Gemini CLI, so it inherits Gemini’s hook system unchanged in .qwen/settings.json - the same hook events Kangentic registers for Gemini, including the BeforeModel, AfterModel, and BeforeToolSelection hooks that distinguish the model-call boundary from tool selection. Hook files are reference-counted across concurrent sessions in the same project. The PTY fallback is the same closed-box-TUI border + 3-second silence timer as Gemini.

Kimi Code, wire-protocol JSONL with PTY fallback

Section titled “Kimi Code, wire-protocol JSONL with PTY fallback”

Kimi emits a structured wire-protocol JSONL stream to ~/.kimi/sessions/<work_dir_hash>/<id>/wire.jsonl on every spawn. Kangentic watches the file incrementally for tool events, model start/end, plan markdown content, hook requests, and subagent lifecycle markers. The same stream feeds the ContextBar’s context-window percentage, token counts, and cost. PTY silence timer backs it up if the wire file is slow to flush.

Factory Droid, PTY silence + cursor-hide first-output

Section titled “Factory Droid, PTY silence + cursor-hide first-output”

Droid 0.109 ignores hooks injected through --settings, so Kangentic uses pure PTY detection: a 3-second silence timer for idle plus the \x1b[?25l cursor-hide marker for first usable output. Session UUIDs are captured from ~/.factory/sessions/<cwd-slug>/<id>.jsonl rather than stdout.

Droid does not stream live telemetry to Kangentic - /cost and /context are post-hoc TUI commands and the OTEL endpoint is out-of-band. The context bar shows a “Telemetry: TUI only” affordance instead of spinning forever; run those commands inside the Droid TUI to see model, token, and cost details.

Warp’s oz CLI is the simplest adapter: no hooks, no status file, no structured event output, no trust mechanism, and no caller-specified session IDs. Activity detection is a PTY silence timer only, the same bare timer Codex and Cursor use: oz agent run prints no prompt to match, so there is no prompt regex and the silence timer is the sole idle signal.

Ollama has no hooks and no native session IDs, so detection is PTY-only. A one-shot ollama run streams its answer and exits, which means the silence timer normally carries the idle transition on its own. A prompt regex sits alongside it for the other case: spawning with no prompt drops you into Ollama’s interactive REPL, and matching its >>> prompt marks idle the moment the model stops rather than waiting the timer out.

The card indicators are designed to avoid false signals:

  • Tool end doesn’t set idle. Between consecutive tool calls there’s a brief gap - showing idle here would cause the spinner to flicker. For agents that emit explicit events (Claude, Gemini), only Stop or PermissionRequest sets idle.
  • Tool failure doesn’t set idle. The agent continues processing after a failure and may retry.
  • Notifications don’t change state. Informational notifications (like “context getting full”) fire unpredictably and shouldn’t flip the indicator.
  • Frame redraws don’t wake a session. For PTY-based agents, content-deduplication filters resize redraws so they don’t look like real activity.
  • Denying a permission prompt clears the card. Claude fires a hook when a tool succeeds and when auto-mode denies one, but typing “no” yourself fires nothing: the turn aborts rather than ending, so there is no event to listen for. Before v0.41.0 the card sat on “needs input” until somebody typed into that session again, which on a task you answered from your phone meant forever. Kangentic now reads the answer out of Claude’s own transcript, polled every two seconds and only while a session is waiting on a prompt, so the card clears within a couple of seconds of your denial. There is deliberately no timeout: a prompt nobody has answered should stay pending, because timing it out would drop the card and silence the push notification that is the point of knowing.

Claude Code’s Agent tool lets a main agent spawn subagents to delegate work. Kangentic tracks the subagent depth from Claude’s SubagentStart / SubagentStop hook events so the card indicator stays accurate during nested operations:

  • While any subagent is still running, the card stays thinking, even after the main agent’s own turn has stopped: subagent depth counts as activity
  • When all subagents finish and the main agent resumes, the card still shows thinking, now for the main agent’s own turn
  • Main-agent Stop is deferred until any running subagent finishes, so a pending user decision isn’t hidden
  • User prompts and interrupts always take effect immediately regardless of subagent state

Grok Build fires the same Claude-compatible SubagentStart / SubagentStop hooks, with one simplification: grok’s Stop fires only for the main agent, so the depth gating above isn’t needed. The other agents either don’t expose subagents (Codex, Gemini, Cursor, Copilot, Antigravity, Goose, OpenCode, Aider, Qwen Code, Droid, Warp, Ollama) or surface them through their own JSONL stream (Kimi Code, via SubagentEvent envelopes in the wire protocol).

Background shell tracking (Claude Code only)

Section titled “Background shell tracking (Claude Code only)”

When Claude Code launches a backgrounded Bash tool (e.g., npm run dev &), the agent’s main loop returns to idle while the child process keeps running. Without tracking, Kangentic would flag the session idle the moment Bash returned even though work is still happening. The bg-shell watcher tracks each backgrounded child PID until it exits, deferring the idle transition until the last tracked child finishes. Two event types - background_shell_start and background_shell_end - record this in the Activity Log.

When a card’s state looks wrong, the Activity Engine Debugger shows what the engine is holding it on. Turn on Activity engine debug overlay on the Developer tab of Settings, or press Ctrl+Shift+D / Cmd+Shift+D from anywhere. The panel floats over the board with one section per running session in the current project, and refreshes every two seconds.

Each section names the session’s state and, while it is thinking, what holds it there: the tool it is running, open subagents, live background shells, or a turn that has not ended. Below that are the counters the rules above describe (pending tools, subagents, background shells, whether a turn is active, whether a permission prompt is open), the last five transitions with the event or timer that caused each, and a timeline of the last two minutes. While the overlay is on, the engine also writes a snapshot of each session to .kangentic/debug/ on every state change.

The Activity Engine Debugger open over the board: a panel per session naming its state, how long since its last signal, its pending tools, subagents and background shells, whether a turn is active, a log of recent transitions, and a timeline. v0.46.0 The real app on a sample install. The projects and terminals are recordings, connected to nothing, so click anything.

Retiring an agent that exited under its shell

Section titled “Retiring an agent that exited under its shell”

This is not an activity signal: it changes a session’s status, not its state (added in v0.36.0).

Kangentic spawns a shell and writes the agent CLI command into it, so the agent is a child of the terminal’s root process rather than the root itself. When the agent exits on its own - you type /exit, the CLI crashes, a launch fails - the shell survives, the terminal never reports an exit, and nothing marked the session finished. The record stayed Running indefinitely: the status bar counted a phantom agent, the card read active, and the bottom panel kept a tab whose agent was gone.

Kangentic now sweeps for that case and retires the session cleanly, exactly as an ordinary exit would. The sweep piggybacks on the watcher that already holds each session’s root process and takes one process snapshot per cycle, so it adds no new timer or polling loop, and it runs at most once a minute while a task agent is running.

Marking a live agent dead is far worse than the phantom, so the sweep only acts when the root process is alive, the process snapshot is healthy, the agent is missing on two consecutive checks, and the session is past a 30-second spawn grace. Command Terminal sessions are excluded outright: they are bare shells by design, and would otherwise be retired the moment you opened one. A session running under a WSL shell is skipped too, because a healthy WSL agent shows no Windows child processes and cannot be told from a phantom.

For Claude Code, Codex, Gemini, Grok Build, Antigravity, OpenCode, Qwen Code, Kimi Code, and Factory Droid, Kangentic reads each agent’s own native session history file (in addition to the hook/PTY activity stream) to produce higher-fidelity telemetry: token counts and tool call transcripts. Kangentic watches these files incrementally so the Activity tab and context bar stay in sync without polling the CLI.

A context percentage needs a second number the history file does not always carry: how large the model’s context window is. Kangentic never guesses it from the model name. Where the agent reports a window size (Claude in its status.json, Qwen on every assistant event), the context bar draws a progress bar and the model dropdown shows a context-size badge. Where it does not, the bar hides and the pill shows the token count alone. Gemini is the one agent that lost its bar in v0.41.0: its session JSON carries no window size and the CLI has no command that reports one, so the hardcoded lookup table that used to fill the gap was deleted rather than kept current by hand.

The same files are what handoff context points at when a task moves between agents: Kangentic hands the next agent the file’s path rather than a manufactured summary, and the agent reads it directly.

AgentNative history path
Claude Code~/.claude/projects/<sanitized-cwd>/<session-id>.jsonl
Codex CLI~/.codex/sessions/.../rollout-*.jsonl
Gemini CLI~/.gemini/tmp/<cwd>/chats/session-*.jsonl (current builds; .json on older ones, and both generations are matched)
Grok Build~/.grok/sessions/<encoded-cwd>/<session-id>/updates.jsonl
Antigravity CLI~/.gemini/antigravity-cli/brain/<conversation-id>/.system_generated/logs/transcript.jsonl (parsed for the Transcript tab; no live telemetry)
OpenCode~/.local/share/opencode/opencode.db (one SQLite database for every session rather than a file per session; the handoff points the next agent at the database path)
Aider<cwd>/.aider.chat.history.md (parsed for session cost only, but readable in full by a receiving agent)
Qwen Code~/.qwen/projects/<sanitized-cwd>/chats/<session-id>.jsonl (Qwen forked Gemini but moved chat persistence, so it does not use Gemini’s layout)
Kimi Code~/.kimi/sessions/<work_dir_hash>/<id>/wire.jsonl
Factory Droid~/.factory/sessions/<cwd-slug>/<session-id>.jsonl (parsed for the Transcript tab; no live telemetry)

Copilot, Warp, and Ollama expose no file at all. Goose keeps named sessions of its own (that is what resume rides on), but Kangentic reads no file from them. Cursor does write one, at ~/.cursor/projects/<cwd-slug>/agent-transcripts/<session-id>/<session-id>.jsonl, but Kangentic deliberately leaves it unread: its records carry no timestamp, and the stored text is wrapped in <user_query> markup rather than kept verbatim, so neither a staleness bound nor an exact match against what was submitted is possible. For all five, the context bar falls back to PTY-derived heuristics and a task moving out of one of them carries no handoff context.

For agents that ship their own OAuth/credential flow, Kangentic probes the auth state at detection time - a credentials directory, or a lightweight CLI probe - and surfaces an amber warning when a fresh spawn would die at the auth wall. This shows up next to the agent’s entry in the Welcome screen and in the column / settings agent picker.

AgentProbeResolve by running
Grok Buildgrok models (its output reports “not authenticated”)grok login
OpenCode~/.local/share/opencode/auth.jsonopencode auth login
Kimi Code~/.kimi/credentials/kimi login

The board shows the state on each card, the Agent Monitor shows it for every project on the machine, and when an agent goes idle Notifications can raise a desktop alert, an idle badge, or a push to your phone.

Kangentic is free and open source. A star helps other people find it.

Star on GitHub