Music Player
What's playing on the user's music app, with remote controls, or a playlist the agent built for them to preview, reorder and save.
Experience · Shares patterns with Audio Player, Kanban Board
≈ 2,047 tokens per use (tool 1,050 · guidance 299 · typical payload 698)How token costs are estimated
Example: Music Player
Make me a focus playlist
Deep focus
Instrumental and steady.
8 tracks, 32 minutes
Press Space or Enter to pick up this track. Then use the up and down arrow keys to move it, Space or Enter to drop it, or Escape to cancel.
Slow CurrentLow Tide Club3:58
Lantern HoursPaper Lanterns3:35
Northbound GlassMarlo Quinn4:22- Field NotesHalden Rue3:19
Quiet EngineCobalt Orchard4:11
Still Water StudyWren Atlas4:04
Drift IndexMoss Arcade4:33
Morning StaticWren Atlas4:08
When to use
- The user asks what's playing, or to control playback on their music app. Use mode
now-playingwith the state the provider reports. - The agent built a playlist or found tracks to add to one. Use mode
playlist, so the user can preview, reorder, remove and save.
When not to use
- The agent doesn't have the tracks from the user's music provider. Never invent tracks, artwork or preview URLs.
- It's a recording, a podcast clip or a voice note. Use the Audio Player.
- The user only wants a name or two. Say them in text.
How the agent should use it
- Pass tracks, artwork,
previewUrl,urlandnowPlayingexactly as the provider returns them. The provider is data: its name appears as text ("Open in Music app"), with no logos or brand colors. - Now-playing: put the current track first in
tracks, then what's up next. Each control sends a request back. Carry it out with the provider, then show the new state. - Playlist: save nothing until
save-playlistarrives, then save exactlytrackIds, in that order, undertitle. - On
open-in-provider, hand off to the user's music app. Ondismiss, save nothing.
Playback and previews
Two kinds of playback, kept apart:
| Where it plays | How | |
|---|---|---|
| The user's music app | On their device, e.g. "Kitchen speaker" | Now-playing controls (play, pause, previous, next, seek, like, shuffle, repeat) don't play anything in the chat. Each one returns a result for the agent to carry out, and the card updates optimistically until the host passes a new nowPlaying |
| Previews | In the chat | A track's previewUrl (about 30 s) plays in a real <audio> element. Only one preview plays at a time. The playing row is marked aria-current and shows a progress line |
Full tracks always hand off to the provider: the now-playing card links to the current track's url, and the playlist's "Open in provider.name" item (in the footer's "More actions" menu, next to Dismiss) returns open-in-provider.
Layout
- Now playing: the device ("Playing on Kitchen speaker"), then the artwork with the track title large and the artists, the seek bar with its times, and the transport row (shuffle, previous, play/pause, next, repeat as an icon toggle). Up next follows. The album is read to screen readers only.
- Playlist: the title and one quiet line with the totals, then the tracks. The footer holds Undo (after a removal), a "More actions" menu (Open in the provider, Dismiss) and Save playlist. The playlist
descriptionis read to screen readers only.
Editing a playlist
With editable (the default), the user reorders and removes tracks before saving. Nothing reaches the agent until they save.
| Input | How |
|---|---|
| Mouse, touch and pen | Drag a track by its handle. A line shows where it will land, and a long list scrolls near its edges |
| Keyboard | Focus a track's handle, press Space or Enter to pick it up, use ↑/↓ (or Home/End) to move it, then Space or Enter to drop. Escape cancels. Each step is announced to screen readers |
| Remove | Each row's remove button takes the track out. Undo in the footer restores the most recent removal, and can be pressed again for earlier ones |
The header totals ("8 tracks · 32 min") follow the edits. Past 6 tracks the list scrolls inside the card.
Installation
npx shadcn@latest add https://your-site.com/r/music-player.json
Usage
import { tool } from "ai"
import { musicPlayerTool } from "@/components/genui/music-player.schema"
const tools = {
// No execute(): the call is completed by the user's action in the UI.
[musicPlayerTool.name]: tool({
description: musicPlayerTool.description,
inputSchema: musicPlayerTool.inputSchema,
}),
}<MusicPlayer
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) => sendToAgent(part.toolCallId, result)}
/>In now-playing mode onResult fires on every control, so route results to the agent (or straight to the provider) rather than completing the tool call with the first one. The agent receives one of:
// Now-playing. trackId is the track on screen when the control was used.
{ action: "play" | "pause" | "next" | "previous", trackId: "tidewater" }
{ action: "like" | "unlike" | "shuffle-on" | "shuffle-off", trackId: "tidewater" }
{ action: "repeat", value: "off" | "all" | "one", trackId: "tidewater" }
{ action: "seek", positionSeconds: 95, trackId: "tidewater" } // sent once, on release
// Playlist
{ action: "save-playlist", title: "Deep focus", trackIds: ["graphite", "slow-current"], removed: ["field-notes"] }
{ action: "open-in-provider" }
{ action: "dismiss" }Keyboard seeks (arrow keys on the progress slider) are sent 0.6 s after the last key press.
With LangGraph
Push it from a graph node with ui.push({ name: "show_music", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored playlist result back as result. A saved playlist shows exactly the saved tracks, in order, read-only, with "Saved to your library" in the footer, and never calls onResult again. A dismissed playlist reads "Dismissed. Nothing was saved."
<MusicPlayer {...part.input} result={part.state === "output-available" ? part.output : undefined} />Now-playing never locks, so result is ignored there. Pass a fresh nowPlaying to show the provider's latest state.
Schema
| Prop | Type | Notes |
|---|---|---|
mode | "now-playing" | "playlist" | |
title | string? | Playlist name, e.g. "Deep focus" |
description | string? | One line about the playlist. Screen readers only |
provider | { name }? | Data-bound. The user's music app, shown as text |
tracks | MusicTrack[] | Data-bound. 1–50. Now-playing: current track first, then up next |
nowPlaying | { trackId, positionSeconds?, isPlaying?, device?, shuffle?, repeat? } | Data-bound. Playback state reported by the provider |
editable | boolean | Playlist: reorder and remove before saving. Default true |
totalDurationSeconds | number? | Data-bound. Computed from the tracks when absent or after edits |
MusicTrack
| Field | Type | Notes |
|---|---|---|
id | string | Returned in results |
title | string | |
artists | string[] | At least one |
album | string? | Read to screen readers; the fallback for the cover's alt text and initials |
durationSeconds | number | Shown as "3:40" |
artwork | { src, alt }? | http(s) or data:image only. Without it, a neutral tile with the album initials |
previewUrl | string? | http(s), blob: or data:audio. Plays in the chat |
explicit | boolean? | Shows an "E" marker, read as "explicit" |
liked | boolean? | In the user's liked songs. Sets the Like button |
url | string? | http(s) link that opens the track in the provider; opens in a new tab |
Host props: status (streaming, ready, submitting, saved, dismissed), onResult, result and className.