Bundleware, Toolbar Script Reference

Last site update 30th October 2026

Context

useToolbar() returns the context of the component's mount. Everything created through it ends with that mount. The Dispose functions that methods return only end something sooner.

MemberBehavior
id, signalThe toolbar's @id; an AbortSignal aborted when the mount ends.
onCleanup(fn)Runs fn when the mount ends; cleanups run newest first.
timeout(fn, ms), interval(fn, ms)Timers. Intervals are at least 16 ms.
listen(target, type, fn, options?)An event listener on any target, such as window or a widget element.
openWindow(target, { title })Opens target, a component element, in a window; returns { close() }.
showPopup(spec)Shows a canned dialog.
page, activity, aiSiteRead-only observations of this page, described below.
speech(onChange?)Speech with a local English voice, described below.
sound(onChange?), levels(count?, options?)The toolbar's own music and its levels, described below.
generate(request)Optional local parody AI, described below.
openLocalAiSetup()Opens Bundleware's local model preparation page.

Shared state

Make a store at the top level of the script, then read it with useStore anywhere:

const counter = store({ count: 0 });
function Counter() {
  const [state, setState] = useStore(counter);
  return <button onClick={() => setState({ count: state.count + 1 })}>{state.count}</button>;
}

setState takes a value or an updater, previous => next, and every component using the store re-renders. Outside components, the store itself has get(), set(next) and subscribe(fn), so an onAction handler or a helper function can use it directly. Plain objects and arrays in a store are frozen, so replace them instead of changing them in place (setState({ ...state, done: true })); other objects, such as class instances, are stored as they are. Pass a function, store(() => …), to compute the first value lazily. Stores last until the toolbar is hidden or the page reloads; see lifetimes. store(initial, { persist: 'settings' }) keeps the value across pages, tabs and restarts, for this toolbar only: JSON data of at most 8 KiB, at most 16 such stores and 32 KiB in all, and the kept value replaces initial. Type a store where you create it: store<{ items: string[] }>({ items: [] }) (in a .jsx script with // @ts-check, store({ items: /** @type {string[]} */ ([]) })).

Errors. Misuse throws where you call it: invalid parts, popups or windows, out-of-range lengths, hooks called outside a component. Errors thrown by commands, event handlers, effects, renders and the callbacks you register (timers, listeners, subscriptions) appear on the toolbar and in the Dashboard's Editor without stopping the others. A component that throws while rendering shows nothing until the toolbar remounts; a throw during its first render fails the mount. After a mount ends, reading or registering through its context throws, while showPopup, timers and listeners do nothing. Only generate returns a promise.

Page, activity and AI sites

Member
page.read({ maxLength? })A frozen { url, title, text, selection }. maxLength is 0–12,000 characters of text, default 4,000.
page.subscribe(fn, { maxLength? })Calls fn(snapshot, 'initial') immediately, then with 'content', 'selection' or 'navigation' after changes, debounced by 150 ms. 'content' comes only when the snapshot at this maxLength changed.
activity.read(), .subscribe(fn)Up to twelve newest-first { kind, url, at } entries: page visits, route changes and AI-link clicks in this document.
aiSite.read(), .subscribe(fn){ service, name, prompt, promptUpdatedAt, simulatedRegion } for the AI chat website open in this tab, if any. simulatedRegion is invented.

usePage, useActivity and useAiSite read these in components and re-render after changes. subscribe calls its listener immediately with the current value, so there is no need to read() first. Page text is the visible text of the top document, skipping Bundleware, scripts, styles, hidden elements, form fields, editable regions and frames. selection is up to 2,000 characters of selected page text, or the selection captured before focus moved into the toolbar, so commands can use it. Values stay in memory; logged-in pages can be sensitive, so do not send them elsewhere without an explicit user action.

Windows and popups

<Action.OpenWindow> and context.openWindow open a window. A window opened from a part hangs from the bar under that part; one opened later, after a timer or an await, floats at the top right under the bars. A toolbar has one hanging window at a time. Options besides title (1–160 characters): icon, ink, badge, strip (the top strip's ink) and width (320–720 px). Opening the same component again replaces its window. Props stay in your script, so they can carry callbacks as well as data; share changing state with a store. Window bodies style headings, buttons, fields, tables, blockquote, .actions (a button row) and .engine (muted small print).

<Action.ShowPopup popup={…} /> and context.showPopup({ variant, title, body?, cta?, badge?, source?, bundles? }) show a period dialog drawn by the host. Variants are loading, upsell, installer, virus, survey, congrats and unavailable; unavailable reads as a plain notice.

Speech

useSpeech() returns { speaking, available, speak(text, options?), stop() } using a local English voice, so text is not sent to a speech service, and re-renders as speech starts and stops. speak returns false when no such voice is available. Its options are rate, from 0.5 to 2 relative to the voice's own pace (default 1), and onEnd, which runs when the text has been said in full, not when it is stopped or replaced, so a toolbar can read one sentence after another. Outside components, context.speech(onChange?) returns { speak, stop } and reports { speaking, available } to onChange. Speech stops when the mount ends.

Sound

A toolbar can make its own music with useSound() and hear it with useLevels(). The sound is made with Web Audio in the toolbar's world, so it needs no permission and never leaves the page; browsers start it only after a click, so call play from an action or a click handler.

const { music, useSound, useLevels } = Bundleware;
const tune = music.readMidi(TUNE); // A MIDI file as Base64, kept in your script.

function Jukebox() {
  const sound = useSound({ onEnd: () => sound.play(tune) }); // Play it again.
  const levels = useLevels(5, { smoothing: 0.7 });
  return (
    <>
      <button onClick={() => (sound.state === 'playing' ? sound.pause() : sound.play(tune, { instrument: 'vibes' }))}>
        {sound.state === 'playing' ? 'Pause' : 'Play'}
      </button>
      {levels.map((level, band) => <i key={band} class="bar" style={`--level: ${level}`} />)}
    </>
  );
}
useSound({ onEnd? }), context.sound(onChange?)A deck: { state, ended, play, pause, resume, stop, set }. state is 'stopped', 'playing' or 'paused'; ended is true when a score finished, which is also when onEnd runs. A deck stops when its mount ends; a toolbar's decks share one AudioContext, closed with the last.
play(score, options?)Plays from the start, replacing what the deck was playing. Options: speed (0.25–4), volume and room (0–1, the small room echo), loop, and an instrument for notes without one. Notes are scheduled 1.5 seconds ahead, so a busy page does not stall them.
pause(), resume(), stop(), set({ volume, room })resume continues where pause left off; set changes the mix while playing.
useLevels(count = 7, { low, high, smoothing }), context.levels(count, { low, high })count levels from 0 to 1 of everything the toolbar is playing, spaced evenly in pitch from low to high hertz (60 to 8,000). The hook re-renders every frame while sound plays; with smoothing (0 up to 1), levels rise at once and fall away gradually.
music.readMidi(data)A Standard MIDI File, as bytes or Base64, read into { duration, notes, tracks }. Each note is { at, length, pitch, velocity, track, channel }, in seconds through the file's tempo map.
music.melody(notes), music.hz(pitch)The Set of a part's top-line notes, such as a tune played over its own chords; a pitch's frequency.

A score is music.readMidi(file) or any { notes } you write or transform: a note is { at, length, pitch, velocity?, instrument? }, in seconds, with MIDI pitches (60 is middle C). The instruments are sax, trumpet and flute, which play one note at a time, and piano, keys (an electric piano), vibes, bass and pad. A note without one plays its track's General MIDI program family, or the piano; percussion is silent. Frames, such as an embedded video player, have no JSX type: a toolbar's media is part of the toolbar.

The bundled Jazz Bar keeps public-domain MIDI files in its source, voices each song with music.melody (the melody on a sax or piano, low notes on the bass), moves to the next song on onEnd, and draws its equalizer with useLevels.

Local AI

const generation = useGenerate();
const result = await generation.run({ task: 'summary', text, fallback: mySummary(text) });
// result: { text, engine: 'Chrome local AI' | 'Classic routine', feedback }, or undefined

useGenerate() runs one request at a time for its component and re-renders with busy, result, error and cancelled. A newer run, cancel() or unmounting supersedes the request in flight, which then resolves undefined rather than rejecting. context.generate(request) is the same request without that bookkeeping.

generate uses Chrome's local model only when the user enabled it and it is ready; otherwise it returns your fallback with feedback explaining why. The first 4,000 characters of text are used. Both outputs must meet the task's contract, and every output is at most 4,000 characters; an invalid fallback rejects.

TaskContract
summaryExactly twelve words.
translationText that differs from the input.
horoscopeOne paragraph of at most 600 characters that starts Today is a N., N from 0 to 10.
forecastSix lines: the condition now in one or two Title Case words of at most 16 characters, then one sentence of at most 90 characters for each of five days.
podcastA transcript of 6 to 12 lines alternating RANA: and DEV: , from RANA, each line at most 160 characters.

horoscope, forecast and podcast need Bundleware 0.4 or later. A request has a 30-second deadline (60 seconds for horoscope and podcast), and aborting signal or ending the mount rejects with an AbortError (for useGenerate, sets cancelled). Render results as text and always show result.engine. openLocalAiSetup() opens the model preparation page.