index ↓
webapp sdk
a webapp is one page in the car thing's kiosk. it reaches the daemon through one typed client,@bridgething/client.
d1 - start a project
react, vite, tailwind, and this client come preinstalled.
bun create bridgething my-app--launcherscaffolds a home screen for the device's launcher slot. seelaunchers and overlays.--overlayscaffolds a system overlay, drawn over every webapp.--extensionadds a deno process on the desktop. combines with either. seeextensions.--no-install,--no-git
every system overlay is on by default. turn one off in the manifest when you draw it yourself.
{
"overlays": { "volume": false }
}source and issues on github.
d2 - settings pages and extensions
a settings page is one html file in your bundle that the companion app or the desktop app renders, with its own sdk for config,fetch, and oauth.
an extension is a deno process the desktop app runs for your webapp, with host access. your webapp reaches it over client.forward.
d3 - connect
construct the client once and reuse it.
import { BridgethingClient } from '@bridgething/client';
const client = new BridgethingClient(); // auto-connects, auto-reconnectsd4 - the three call shapes
events
the daemon pushes, you subscribe.
const off = client.player.onSnapshot(r => render(r.state));
off(); // stop listeningrequests
you ask, the daemon answers with a tagged result.
const res = await client.player.stateGet();
if (res.ok) render(res.response.state);commands
resolves once the daemon has the message. a failure comes back later as an error response on client.on, not on the promise.
await client.player.skipNext();d5 - request results
a request resolves to a tagged result. narrow it on ok.
type TypedRequestResult<Response, Error> =
| { ok: true; response: Response }
| { ok: false; kind: 'domain'; error: Error }
| { ok: false; kind: 'protocol'; error: WireError };kindis'domain'for a failure the operation defines, such as an asset that is missing, or'protocol'when the daemon could not dispatch the request.
every request also accepts an optional second argument { timeoutMs }.
type WireError =
| { type: 'unsupported' }
| { type: 'unimplemented' }
| { type: 'malformed'; data: { reason: string } }
| { type: 'handlerFailed'; data: { reason: string } };d6 - what is available where
some surfaces depend on the connected companion app, and companions differ.
branch on client.capabilities, which reports what the connected companion supplies.
| surface | ios | android | macos | linux | windows | notes |
|---|---|---|---|---|---|---|
| asset | yes | yes | yes | yes | yes | |
| audio | yes | yes | yes | yes | yes | |
| bluetooth | yes | yes | yes | yes | yes | |
| capabilities | yes | yes | yes | yes | yes | |
| config | yes | yes | yes | yes | yes | |
| doc | yes | yes | yes | yes | yes | |
| forward | no | no | yes | yes | yes | available.forward is true while an extension for the active webapp is running. |
| geo | yes | yes | yes | yes | yes | available.geo reflects the location grant on the companion. |
| hardware | yes | yes | yes | yes | yes | |
| library | yes | yes | yes | yes | yes | spotify everywhere, apple music on ios. capabilities.musicProvider names the one in use. |
| lyrics | yes | yes | yes | yes | yes | resolves through the music provider, so a lookup can answer notSupported. |
| net | yes | yes | yes | yes | yes | |
| notifications | yes | yes | no | yes | no | ancs on ios, the notification listener on android, freedesktop on linux. |
| peer | yes | yes | yes | yes | yes | |
| phone | yes | yes | no | no | no | |
| player | yes | yes | yes | yes | yes | transferTo needs available.playbackTargets, which depends on the music provider. |
| store | yes | yes | yes | yes | yes | |
| system | yes | yes | yes | yes | yes | |
| time | yes | yes | yes | yes | yes | |
| voice | yes | yes | yes | yes | yes | a turn resolves with stage: noModel until the companion has the voice model installed. |
| webapp | yes | yes | yes | yes | yes |
macos, linux, and windows are the bridgething desktop app. ios and android are the companion phone app.
d7 - voice
the companion app transcribes the mic and resolves an intent. the daemon dispatches it.
client.voice.onActivity(a => {
// a.phase 'idle' | 'listening' | 'thinking' | 'done' | 'failed'
// a.transcript the recognized text, once there is one
// a.intent the resolved intent name, e.g. 'PAUSE'
// a.slots its parsed arguments
// a.stage 'fastPath' | 'model' | 'rejectedNoIntent' | 'rejectedClarify' | 'noModel'
// a.target 'playback' | 'device' | 'phone' | 'display' | 'webappSwitch'
// a.error { code, msg } when the turn ended without dispatching
render(a);
});display intents
the daemon hands three intents to the active webapp to draw: search, showView, andmoreLikeThis.
client.voice.onIntent(i => {
// the spoken words are i.transcript. i.slots.target is the entity the nlu
// found in them, when it found one.
if (i.intent === 'search') return showSearch(i.slots.target ?? i.transcript, i.slots.targetType);
if (i.intent === 'showView') return navigate(i.slots.view);
if (i.intent === 'moreLikeThis') return showSimilar(i.slots);
});declare rendersVoiceDisplay in your manifest to receive them. otherwise the turn fails witherror.code: 'notDispatchable'.
{
"rendersVoiceDisplay": true
}driving the mic
await client.voice.pushToTalk(); // start a turn
await client.voice.release(); // stop capturing, let the turn finish
await client.voice.cancel(); // drop the turn
await client.voice.muteMic({ preserve: true });
await client.voice.unmuteMic({ preserve: true });
const res = await client.voice.stateGet();
client.voice.onStateChanged(s => setMic(s.muted, s.capturing, s.phase));preserve keeps the mute in place after the turn ends. seeclient.voice for the full signatures.
d8 - surfaces
22 surfaces on client.<name>
client.assetBinary assets such as cover art, addressed by an opaque id.
client.audioVolume, mute, speech, and short sounds on the device.
client.blobclient.bluetoothBluetooth pairing and connection state.
client.capabilitiesWhat the connected companion app can do.
client.configRead-only settings the companion app holds for the active webapp.
client.docKey/value state the active webapp shares with the companion app.
client.forwardclient.geoPosition fixes from the connected phone.
client.hardwareThe display backlight and the ambient light sensor.
client.libraryThe music library on the connected phone.
client.lyricsclient.netHTTP, WebSocket, and streaming network access for a webapp, proxied through the connected companion app.
client.notificationsPhone notifications mirrored to a webapp.
client.peerEvery device the daemon knows about, with its pairing and companion-app connection state.
client.phoneCall state from the connected phone.
client.playerPlayback state and transport for a webapp.
client.storeclient.systemDevice identity, health, logs, and power control for a webapp.
client.timeWall clock, locale, and timezone for a webapp.
client.voiceVoice capture and intent results for a webapp.
client.webappThe webapps installed on the device.