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.
| Member | Behavior |
|---|---|
id, signal | The 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, aiSite | Read-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 undefineduseGenerate() 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.
| Task | Contract |
|---|---|
summary | Exactly twelve words. |
translation | Text that differs from the input. |
horoscope | One paragraph of at most 600 characters that starts Today is a N., N from 0 to 10. |
forecast | Six 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. |
podcast | A 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.