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

Top comments (0)