Every sub-app runs as plain HTML/CSS/JS inside the app's own WebView.
Beyond the normal DOM, Canvas, Web Audio and localStorage (which
is automatically persisted for you — no bridge call needed), the app injects
a window.chaaga namespace with the capabilities below.
if (window.chaaga && chaaga.filePicker) { … } —
so a sub-app still opens sensibly if it's ever loaded outside the app (e.g. a
plain browser tab), and so it degrades instead of throwing on a capability a
given platform/OS version doesn't support.
Promise. A rejection is always a plain Error whose
message is a short, stable code (e.g. permission_denied,
unsupported_platform) — check e.message, not the
exact wording, since the text around it isn't guaranteed to stay the same.
"User cancelled/backed out" is not an error on most of these — it
resolves null instead, noted per method below.
A plain string — this sub-app's own id. Available immediately,
with no await, unlike everything else on this page.
console.log(chaaga.subAppId); // "a1b2c3"
Native file/photo picking.
await chaaga.filePicker.pick({ type: 'image' | 'any' })
Returns { name, mimeType, dataBase64 },
or null if the user cancelled.
dataBase64 is the raw file — turn it into a
Blob + URL.createObjectURL(...) rather than a
giant data: URL. There's no picker-imposed size limit; cap
it yourself, e.g. reject anything where
dataBase64.length * 0.75 > 10 * 1024 * 1024.
Fetching reference data and making LLM calls, both routed natively.
await chaaga.llm.isAvailable()
Returns a boolean. Call this first and degrade
gracefully if it's false.
await chaaga.llm.webDownload(url)
Returns { url, status, contentType, text, truncated }.
text is the page reduced to readable plain text (HTML
stripped) or the raw body for non-HTML, truncated if long.
Runs natively, so it ignores the page's cross-origin rules; the promise rejects on a network failure. Read-only GET — not a way around "no server-side features", just a way to pull in reference data.
await chaaga.llm.chat.completions.create({ messages, model })
Shaped like OpenAI's Chat Completions. messages is
[{ role: 'system' | 'user' | 'assistant', content }] — a
system prompt is just a system message at the front, there's
no separate argument for it.
content is a string or an array of blocks:
{type:'text', text} or
{type:'image_url', image_url:{url}} where url
is a data: URL (what filePicker gives you) or a
remote URL.
Returns { role: 'assistant', content: '...' } —
push it back onto messages to continue a conversation.
model is optional; omit it for the default. No
tool/function calling yet.
const reply = await chaaga.llm.chat.completions.create({
messages: [{ role: 'user', content: 'Say hi in five words.' }]
});
console.log(reply.content);
Records the whole screen via a foreground service — survives the app
being backgrounded. On iOS, every method below rejects
unsupported_platform (no iOS implementation yet); build the
feature but say plainly it's Android-only if asked about iOS.
await chaaga.screenrecording.start({ useMic })
Resolves once actually recording. useMic defaults to
false (video only); pass { useMic: true } to
also capture microphone audio (triggers a mic permission prompt).
await chaaga.screenrecording.stop()
Returns { path, url, durationMs, sizeBytes, mimeType }
once stopped and finalized. Use url (not path)
to play it back — e.g. videoEl.src = result.url — it's a
same-origin URL this app serves the file from; path is a
native filesystem path and won't load in a
<video>/<img>/fetch.
await chaaga.screenrecording.getStatus()
Returns { recording, since }.
await chaaga.screenrecording.share()
No argument — opens the native save/share sheet for whatever
recording currently exists. Call this only once the user's previewed it
(e.g. a <video src="result.url">) and decided to keep
it — not automatically on stop(). Doesn't delete the file,
so it's fine to call again (e.g. sharing to more than one place).
await chaaga.screenrecording.deleteFile()
Explicitly discards the current recording (e.g. a "Discard" button). Resolves either way — safe to call even if nothing's there. Also swept automatically the next time a recording starts, so there's no need to call this "just in case".
chaaga.screenrecording.onStatusChange = fn
Settable hook, fired fn(event) on
{type:'started'} / {type:'stopped', file} /
{type:'error', message} / {type:'statusSync', recording} —
including when recording is stopped from outside the app (the
Android notification's Stop action), not just from a stop() call.
Keeps the screen on.
await chaaga.wakelock.enable()await chaaga.wakelock.disable()await chaaga.wakelock.isEnabled()
Returns a boolean.
Automatically released when the app leaves this sub-app, so it never
needs disabling "just in case" on unmount — only call disable()
when the app's own UI state says the screen no longer needs to stay on
(e.g. a workout finished).
await chaaga.scanner.scan()
Returns the decoded string, or null if
the user backed out without scanning anything. Opens a full-screen
native camera view — there's no way to embed the scanner inline in the
page.
await chaaga.contacts.pick()
Returns { name, phoneNumbers, emails }
(the latter two are arrays of strings, can be empty), or
null if the user backed out without picking anyone.
Local notifications — one-shot, recurring, or immediate — with tap routing back into this sub-app.
await chaaga.notifications.requestPermission()
Returns a boolean. Call this before the first notification that actually matters — a decline means every later call below silently shows nothing.
await chaaga.notifications.show({ title, body })
Shows one immediately.
await chaaga.notifications.schedule({ id, title, body, whenMs })
Fires once at a future time (whenMs is epoch
milliseconds). id is any string you choose — reused later
to cancel() this specific one.
await chaaga.notifications.recurring({ id, title, body, hour, minute, dayOfWeek, dayOfMonth })
hour (0-23) and minute (0-59) are required.
Pass at most one of dayOfWeek (0-6, Sunday is 0, for a
weekly reminder) or dayOfMonth (1-31, for monthly) — pass
neither for daily.
await chaaga.notifications.cancel(id)
Cancels one — works for either schedule() or
recurring(). Resolves whether or not it was still pending.
await chaaga.notifications.list()
Returns [{ id, title, body, whenMs, recurrence }, ...] —
every notification this app currently has pending. Useful if you've
lost track of an id (a reinstall, cleared storage, etc).
await chaaga.notifications.cancelAll()
Cancels every notification this app has pending — never another sub-app's.
chaaga.notifications.onTapped = fn
Settable hook, called fn({ id, payload }) when the user
taps a notification this app scheduled — even hours or days later, even
if the app was fully closed when they tapped it. Don't assume any
in-memory state from when it was scheduled is still around.
schedule()) is consumed once it
fires — it drops off list() afterward. A recurring one
(recurring()) keeps firing indefinitely until you
cancel() it; note that after its first fire, the
whenMs a recurring entry reports from list()
stays at that original time rather than updating to the true next
occurrence.
await chaaga.notifications.requestPermission();
await chaaga.notifications.recurring({
id: 'water-reminder',
title: 'Drink some water',
body: 'Stay hydrated!',
hour: 14,
minute: 0
}); // every day at 14:00
chaaga.notifications.onTapped = ({ id }) => {
console.log('tapped', id);
};
Not part of window.chaaga — use the standard
navigator.mediaDevices.getUserMedia(...) as you would in any
browser; the app grants the underlying OS permission automatically when
the page asks.