XState, a state machine for status detection.
In this article, we review XState usage in Claude Code UI codebase. We will look at:
-
What is XState?
-
XState usage in claude-code-ui

What is XState?
Xstate provides finite state machines and statecharts for the modern web.
I found the below quick start guide in the docs.
npm install xstate import { Machine, interpret } from 'xstate'; // Stateless machine definition // machine.transition(...) is a pure function used by the interpreter. const toggleMachine = Machine({ id: 'toggle', initial: 'inactive', states: { inactive: { on: { TOGGLE: 'active' } }, active: { on: { TOGGLE: 'inactive' } } } }); // Machine instance with internal state const toggleService = interpret(toggleMachine) .onTransition(state => console.log(state.value)) .start(); // => 'inactive' toggleService.send('TOGGLE'); // => 'active' toggleService.send('TOGGLE'); // => 'inactive'
XState usage in claude-code-ui
The daemon uses an XState state machine to determine session status:
┌─────────────────┐ │ idle │ └────────┬────────┘ │ USER_PROMPT ▼ ┌─────────────────┐ TOOL_RESULT ┌─────────────────┐ │ waiting_for_ │◄──────────────│ working │ │ approval │ └────────┬────────┘ └────────┬────────┘ │ │ ┌────────────┼────────────┐ │ │ │ │ │ TURN_END ASSISTANT_ STALE_ │ │ TOOL_USE TIMEOUT │ ▼ │ │ │ ┌─────────────────┐ │ │ │ │ waiting_for_ │◄─┘ │ └───────────▶│ input │◄──────────────┘ IDLE_ └─────────────────┘ TIMEOUT
States
I found the following states in the claude-code-ui Readme.
-
idle — No activity for 5+ minutes — Idle
-
working — Claude is actively processing — Working
-
waiting_for_approval — Tool use needs approval — Needs Approval
-
waiting_for_input — Claude finished, waiting for user — Waiting
I found the file, state-machine.ts, that has the states defined. Very similar to our example in quick start guide.
export const statusMachine = setup({ types: { context: {} as StatusContext, events: {} as StatusEvent, }, }).createMachine({ id: "sessionStatus", initial: "waiting_for_input", // Use a factory function to ensure each actor gets a fresh context context: () => ({ lastActivityAt: "", messageCount: 0, hasPendingToolUse: false, pendingToolIds: [], }), states: { working: { on: { USER_PROMPT: { // Another user prompt while working (e.g., turn ended without system event) actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.messageCount += 1; context.hasPendingToolUse = false; context.pendingToolIds = []; }, }, ASSISTANT_STREAMING: { actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; }, }, ASSISTANT_TOOL_USE: { // Immediately transition to waiting_for_approval - tools that need approval // will wait for user action, auto-approved tools are already filtered out target: "waiting_for_approval", actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.messageCount += 1; context.hasPendingToolUse = true; context.pendingToolIds = event.toolUseIds; }, }, TOOL_RESULT: { // Tool completed - clear pending state, stay working actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.messageCount += 1; const remaining = context.pendingToolIds.filter( (id) => !event.toolUseIds.includes(id) ); context.pendingToolIds = remaining; context.hasPendingToolUse = remaining.length > 0; }, }, TURN_END: { target: "waiting_for_input", actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.hasPendingToolUse = false; context.pendingToolIds = []; }, }, STALE_TIMEOUT: { target: "waiting_for_input", actions: ({ context }) => { context.hasPendingToolUse = false; }, }, }, }, waiting_for_approval: { on: { TOOL_RESULT: { target: "working", actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.messageCount += 1; // Remove approved tools from pending const remaining = context.pendingToolIds.filter( (id) => !event.toolUseIds.includes(id) ); context.pendingToolIds = remaining; context.hasPendingToolUse = remaining.length > 0; }, }, USER_PROMPT: { // User started new turn - clears pending approval target: "working", actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.messageCount += 1; context.hasPendingToolUse = false; context.pendingToolIds = []; }, }, TURN_END: { // Turn ended without approval (e.g., session closed) target: "waiting_for_input", actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.hasPendingToolUse = false; context.pendingToolIds = []; }, }, STALE_TIMEOUT: { // Approval pending too long - likely already resolved target: "waiting_for_input", actions: ({ context }) => { context.hasPendingToolUse = false; context.pendingToolIds = []; }, }, }, }, waiting_for_input: { on: { USER_PROMPT: { target: "working", actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; context.messageCount += 1; }, }, // Handle assistant events for partial logs (e.g., resumed sessions) ASSISTANT_STREAMING: { actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; }, }, TURN_END: { actions: ({ context, event }) => { context.lastActivityAt = event.timestamp; }, }, }, }, }, });
About me:
Hey, my name is Ramu Narasinga. I study codebase architecture in large open-source projects.
Email: ramu.narasinga@gmail.com
I spent 200+ hours analyzing Supabase, shadcn/ui, LobeChat. Found the patterns that separate AI slop from production code. Stop refactoring AI slop. Start with proven patterns. Check out production-grade projects at thinkthroo.com