Build1 publisher2 min readPublished
Six runtime events map to six of eight UI states in this Angular agent walkthrough
A dev.to walkthrough swaps the agent spinner for a typed state machine covering planning, approval, retry and blocked, and its reducer reads only the newest event, so the server has to name each state first.
The Engineer · Build desk

What happened
- Planning, waiting for a tool, requesting approval, retrying and suppressing an unsafe action all render as one spinner in most chat UIs, so users cannot tell whether the system is working, blocked or about to change something.
- The walkthrough replaces that with an eight-variant union: idle, planning, using_tool, waiting_for_approval, recovering, blocked, completed and failed, each carrying the field its label needs.
- The post tells readers to take the state names from actual runtime events and not to fabricate a thinking narrative implying access to private chain-of-thought.
- aria-busy is set only while work is genuinely progressing, an approval state is not busy, and focus moves to the Approve and Reject controls.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost The component layer is a few dozen lines of TypeScript; the bill lands on whoever owns the agent runtime, which now has to emit six typed lifecycle events with the payload fields, run identifiers and ordering guarantees the labels read.
- decision Making approval its own state forces a product call on what the review card must contain before a consequential tool call: the tool, its bounded arguments, evidence freshness and the scope the approval covers.
- contradiction The code declares a blocked state with a reasonCode and an alert branch in the template, yet no declared event maps to it, so a policy suppression cannot currently reach the screen this design was drawn for.
`state` is a `computed()` that reads exactly one item, `events().at(-1)`, and returns idle when the array is empty [5]. The current label is a projection of the newest event. Whatever the label shows has to travel inside that event: `attempt` for the retry counter, `proposalId` for the approval, `outcome` and `message` for the two terminal states [4]. If the server omits the attempt number, the UI has no way to count retries itself.
Seven of the eight kinds in the union have a producer: six event types map into it, and an empty array supplies idle [2]. The one left out is `blocked`, which carries a `reasonCode` [3] and gets a `role="alert"` branch in the template [8]. Reaching it takes a seventh event type and one more `case` in the switch.
Two failure modes get more space in the post than the happy path, and both are transport problems. A cancelled request A can land a late event that overwrites the state of request B, so events are tagged with a run ID and `acceptEvent` drops anything whose run is no longer active [10]. Reconnectable streams get an event ID or a monotonically increasing sequence number, duplicate suppression, gap detection, and a snapshot request when the UI cannot safely rebuild state [12]. "A signal will faithfully render bad ordering if the transport contract never defined ordering," the post says [12].
The Angular guidance here is narrow. Angular's signals guide recommends `computed()` for derived state and warns against using effects to propagate state changes, and the walkthrough reserves `effect()` for a real side effect such as analytics or persistence, kept independent of the transition [7]. Updates go through `events.update(...)` with a new array, because the readonly signal surface does not prevent deep mutation of an object or array [13].
Approval is handled as a pause on one proposal and a new user decision [15]. "Cancellation should be a runtime operation as well as a visual one," the post says, and removing a spinner does not stop a network request or tool call [11].
Neither the typed union nor the run-ID filter is an Angular feature, so both port to another framework unchanged in substance. The assumption underneath does not port: a runtime that already publishes discrete lifecycle events, each with a run identifier and enough payload to render a label. An agent server that streams tokens down one channel and nothing else satisfies none of that, and the state machine has to be built there first.
What to watch
- Whether the walkthrough adds a blocked event type to the AgentEvent union, closing the gap between the state list and its producers.
- Whether agent runtimes converge on a lifecycle event schema with run IDs and sequence numbers; that would remove most of the server-side work this design assumes.
- Whether Angular's signals guide keeps recommending computed() for derived state and warning against effects for propagation in later releases.