DEV Community

Ramu Narasinga
Ramu Narasinga

Posted on

XState, a state machine for status detection.

In this article, we review XState usage in Claude Code UI codebase. We will look at:

  1. What is XState?

  2. 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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
          },
        },
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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

References:

  1. https://xstate.js.org/api/index.html

  2. KyleAMathews/claude-code-ui?tab=r…-state-machine

  3. https://xstate.js.org/api/index.html#super-quick-start

  4. KyleAMathews/claude-code-ui/packages/daemon/src/status-machine.ts#L260

Top comments (0)