Build Better Agent UX: Streaming Progress, Status, and File Ops with LangChain
Source: Dev.to
What you’ll build
A simple pattern:
- Your tool emits events (progress/status/file ops) while it runs
- The frontend subscribes and renders those events immediately
- Type guards keep the UI logic safe and predictable
No polling loops. No guessing. No “thinking…” placeholders.
1) Emit typed custom events from a tool call
Inside the tool call, write custom events as the work progresses:
config.writer?.({
type: "progress",
id: analysisId, // stable id => update in place
step: steps[i].step,
message: steps[i].message,
progress: Math.round(((i + 1) / steps.length) * 100),
totalSteps: steps.length,
currentStep: i + 1,
toolCall: config.toolCall,
} satisfies ProgressData);
This is the key shift: tools aren’t just functions — they’re event producers.
2) Receive those events in React
In the UI, pass a handler into the stream hook:
onCustomEvent: handleCustomEvent,
Now every event emitted by your tool arrives in the client as it happens.
3) Narrow event types and update UI state predictably
Treat incoming events as unknown, then narrow with type guards and update state maps keyed by id:
if (isProgressData(data)) {
/* update progress */
} else if (isStatusData(data)) {
/* update status */
} else if (isFileStatusData(data)) {
/* update file ops */
}
This keeps the frontend stable:
- progress updates in place
- minimal re‑renders
- no stringly‑typed event spaghetti
Why it matters (beyond “nice UI”)
When the UI reflects real execution:
- users trust the agent more
- debugging becomes dramatically easier
- failures are understandable without digging through logs
- you can build better UX: step indicators, timelines, file operation feeds, etc.
🎥 Video: