Delivery Tracker
Live tracking of one delivery, with stages, the arrival time, minutes left, the courier and quick actions.
Experience · Shares patterns with Timeline, Flight Status
≈ 2,087 tokens per use (tool 1,497 · guidance 315 · typical payload 275)How token costs are estimated
Example: Delivery Tracker
Where's my food?
On the way…
Green Bowl Kitchen
3 minutes away
Arriving 10:30 AM
- Order received, done, 10:02 AM
- Preparing, done, 10:04 AM
- Ready for pickup, done, 10:17 AM
- On the way, in progress, 10:20 AM
- Delivered, upcoming, expected 10:30 AM
Richard O., rated 4.6 out of 5
Scooter · 12-AB-34
Delivering to 27 Elm Road
When to use
- The user asks where their order, groceries, parcel or ride is: "Where's my food?", "When will the pizza get here?", "Track my parcel".
- The agent has the stages and ETA from a tracking tool result.
When not to use
- The agent doesn't have live tracking data. Never guess stages, times or couriers.
- The user wants to place, change or cancel an order. Use the ordering components, such as the Cart.
- It's a flight. Use Flight Status.
How the agent should use it
- Pass every stage in order, in the source's words, with one
currentstage and the finished onesdone. A pizza tracker is just its stages: Order received, Preparing, Baking, Quality check, Out for delivery. - Pass times with the delivery address's UTC offset, as the source gives them. The component shows them on that clock and works out the minutes left.
- Only add
courieronce someone is assigned, andphoneAvailableonly when the service can connect a call. - Only list
actionsthe service supports. On a result, carry it out through the service and confirm in text. The component never shows phone numbers. - For live tracking, re-render the same component with new props instead of calling the tool again.
- Lifecycle: this is live data, so set
fetchedAtto the time of the latest update andstaleAfterMs(about 2 minutes), so a feed that stops reporting shows "may have changed" instead of a frozen ETA. Drop both once it's delivered: a finished delivery is a record. NoexpiresAt. When a new tracking call replaces the card (the user asks again), supersede the old one.
What it shows
- Header: a heading for the current stage ("On the way…", "Baking…", or "Delivered at 10:41 AM" once every stage is done) and the merchant's name as one quiet line.
- Arrival: the minutes left in large type ("3 min"; "Now" under a minute and up to 5 minutes past; "Late" after that; hours when it's 90 minutes or more away), with "Arriving 10:30 AM" under it. When it's a day or more away, the window itself is the large line: "Arriving tomorrow, 9:00 AM–1:00 PM". Screen readers hear "3 minutes away". Hidden once delivered.
- Segmented progress: one equal segment per stage. Done stages are filled, the current one is filled and pulses gently (no pulse with reduced motion), upcoming ones are muted. Stage names and times are read to screen readers ("Step 4 of 5, On the way"); the heading already names the current stage.
- Issue: a warning icon and the source's message, such as "Nobody was home at 2:10 PM."
- Courier: photo (or initials), name, the vehicle, and a round call button labelled "Call Richard O.". The rating is read to screen readers only.
- Items: "3 items · Order #4821", which opens to list the items. The address is read to screen readers only.
- Actions: "Contact restaurant" (store, sender or operator for other kinds), "Delivery instructions" and "Get help". With three footer actions, "Get help" becomes an icon button with the same label.
No illustrations. A merchant's own logo (as a small app-icon tile beside the title) and brand colour (which tints the progress bar) appear only when its data provides them.
Times
Every time shows on the clock of the ISO string it came from: 10:30+01:00 is 10:30 for every viewer, because that's the time at the delivery address. Formatting uses Intl only; the helpers are in delivery-tracker-utils.ts (formatEtaWindow, remaining, headingText, resolveActions, …).
Minutes left are computed after mount and refreshed every 30 seconds, so server and client render the same markup. Pass now to measure from a fixed moment instead.
Actions
| Action | Shown when | Sends |
|---|---|---|
| Call button | courier.phoneAvailable, not delivered | { action: "contact-courier" } |
| Contact restaurant | Food and grocery by default | { action: "contact-merchant" } |
| Delivery instructions | Everything but rides by default. Opens a text box inline | { action: "change-instructions", text } |
| Get help | Always by default; the only one left once delivered | { action: "get-help" } |
When actions is set, it's the full list (the call button still needs phoneAvailable). The card never locks: each action shows a quiet confirmation in the footer, such as "Instructions sent", and the user can act again.
Collapsed by exception. The item list starts collapsed behind its summary, because it's secondary to where the delivery is.
Live updates
Re-render with new props as the tracking source changes. Segments fill with a short animation (a transform, so it's skipped under reduced motion), and stage changes and new issues are announced in a polite live region, such as "Step 4 of 5, Out for delivery" or "Delivered at 10:41 AM". Nothing is announced on the first render.
Installation
npx shadcn@latest add https://your-site.com/r/delivery-tracker.json
Usage
import { tool } from "ai"
import { deliveryTrackerTool } from "@/components/genui/delivery-tracker.schema"
const tools = {
[deliveryTrackerTool.name]: tool({
description: deliveryTrackerTool.description,
inputSchema: deliveryTrackerTool.inputSchema,
}),
}Render it, refreshing the props from your tracking feed:
import { DeliveryTracker } from "@/components/genui/delivery-tracker"
function ToolPart({ part }) {
if (part.type !== "tool-track_delivery") return null
const live = useLiveTracking(part.input) // your polling or push feed
return (
<DeliveryTracker
{...live}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={savedAction(part.toolCallId)}
onResult={(result) => {
// Hand it to the service (call bridge, merchant chat, support), then tell the agent.
handleDeliveryAction(part.input.orderNumber, result)
sendMessage({ text: `Delivery action: ${JSON.stringify(result)}` })
}}
/>
)
}The agent receives one of:
{ action: "contact-courier" }
{ action: "contact-merchant" }
{ action: "change-instructions", text: "Leave it with the concierge" }
{ action: "get-help" }With LangGraph
Push it from a graph node with ui.push({ name: "track_delivery", props: { toolCallId, ...args } }, { message }), and push it again with the same UI message id as the tracking changes. Render the thread's UI messages with the Renderer.
Restoring from history
Pass the last stored action back as result. Its confirmation shows in the footer and onResult isn't called for it. The card stays usable, since a delivery can need more than one action.
<DeliveryTracker {...props} result={{ action: "change-instructions", text: "Ring twice" }} />Schema
| Prop | Type | Notes |
|---|---|---|
kind | "food" | "grocery" | "parcel" | "ride" | Picks icons, labels and default actions |
merchant | { name, logo?: { src, alt }, color? }? | Restaurant, store, sender or service. Shown as text, with its logo (app-icon tile) and #rrggbb brand colour only when the data provides them. Data-bound |
orderNumber | string? | Data-bound |
stages | { id, label, state, at? }[] | 3–7, in order. state is "done" | "current" | "upcoming". Data-bound |
eta | { at, windowEnd? }? | ISO 8601 with the local offset. Data-bound |
courier | { name, initials, rating?, vehicle?, phoneAvailable?, photo? }? | rating out of 5, read to screen readers; photo is { src, alt }, http(s) or data:image only. Data-bound |
items | { name, quantity }[]? | Up to 8 lines. Data-bound |
address | string? | Only the street (before the first comma) is read, to screen readers. Data-bound |
issue | { kind, message }? | kind is "delayed" | "address-problem" | "failed-attempt". Data-bound |
asOf | string | When the tracking data was read. Data-bound |
actions | ("contact-courier" | "contact-merchant" | "change-instructions" | "get-help")[]? | Defaults by kind and stage |
Host props: status (streaming or ready), now, onResult, result and className.