CLAUDE.md · git:20261015.049beef · 2026-10-15 · sha256 6e041f1c9f66e111
CLAUDE.md git:20261015.049beefA
Immutable. This exact content never changes and is served at /api/v1/blob/6e041f1c9f66e111.
# tinyjs — working notes for agent sessions
Runtime + CLI for tiny native apps: txiki.js backend (`runtime/bridge.js`),
webview client (`runtime/tiny.js`, compiled into each launcher via
`native/gen-client.sh`), three native launchers. Wire protocol: newline-
delimited lines over a Unix socket; `CMD@<winid>` targets a window, bare
`CMD` means main — window-scoped ops must dispatch via `forWin(m)`, not the
global app handle.
Releases are git tags (`vX.Y.Z` on main); a tag push builds macOS + Windows
+ linux-x86_64 + linux-arm64 in CI. Update CHANGELOG.md AND
docs/changelog.html before tagging. If the API changed (tiny.d.ts, bridge.js's
`app`, tiny.js), also regenerate docs/api-map.html with
`scripts/api-map/run.sh` — never hand-edit it. Linux legs build on ubuntu-22.04
runners ON PURPOSE — the linker bakes the build image's glibc floor into
tjs and the launcher, and 24.04-built binaries refuse to load on Ubuntu
22.04 / Debian 12 (shipped broken through v0.32.0). A verify step asserts
the floor (GLIBC ≤ 2.35, GLIBCXX ≤ 3.4.30), the arch, and the
optionally-linked features before packaging; tjs needs gcc-12 + the
#pragma region strip in setup.sh (details in release.yml's comments).
Never "modernize" the runners or the cache key without re-reading those.
App tarballs (amp etc.) are packaged separately in 22.04 containers —
runbook in ../tinyjsapp-examples/CLAUDE.md. Linux burn-down + platform notes:
TODO-linux.md. Native-filters-on-other-OSes plan: TODO-audio-filters.md.
Sampled-SFX mixer (`tiny.audio.sampler`, native on Linux / Web Audio
elsewhere): TODO-audio-sampler.md — shipped 2026-07-31, verified on all
three platforms same day (Linux instrumented, mac/win hand-tested;
TODO-verify.md). PDF pagination + header/footer plan:
TODO-pdf.md (macOS `printToPDF` emits ONE tall page today; Windows and
Linux paginate).
Site wrappers (a hosted third-party page as the app's main frame — JS
dialogs, downloads, navigation policy, popups, find, and the tinyjs.json
`"api"` capability gate): TODO-site-wrapper.md, shipped on all three
platforms 2026-08-04/06. The gate's per-origin keyholes trust the origin
the LAUNCHER stamps onto each CALL, so anything touching that path is
security code — the origin is always the LAST element of the CALL array
(every launcher appends it; reading `[1]` trusted the page on Windows).
Verify it the way a hostile page would, never only through our own client:
adversarial probes in that file, per-OS ticks in TODO-verify.md.
Anything built on one OS but never watched run on another goes in
TODO-verify.md — tick a box there only after seeing it work on that OS,
since a fire-and-forget op a shell ignores looks just like an unimplemented
one. `test/appsurface.html` is the self-driving page for those checks.
Example-app payloads (all platforms) live on GitHub Releases in
tinyjsapp-examples (tag `<dir>-v<version>`; binaries purged from its git
history 2026-07-25) — runbook in ../tinyjsapp-examples/CLAUDE.md.
## Linux specifics that cost real debugging — read before touching
- `tinyjs dev` auto-rebuilds the launcher when launcher-linux.cc or
runtime/tiny.js is newer than the binary. Manual build: see setup.sh.
- Web Audio reaching ctx.destination crackles under WebKitGTK (graph renders
on a normal-priority thread; rtkit won't promote it). This is measured
fact, not theory — don't retry latencyHint/buffering "fixes".
- PipeWire limits (all measured, PipeWire 1.0.5): filter-chain segfaults
past ~30 declared nodes; the `gain` builtin kills any config containing
it (use `linear`); `pw-cli set-param` silently emits an EMPTY pod past
~20 key/value pairs — chunk to ≤18 (launcher does).
- NEVER make system-wide audio changes (pw-metadata clock.force-quantum
etc.) — a forced quantum once broke ALL system audio including Firefox.
Per-app nodes only, named `tinyjs-<kind>-<pid>`, destroyed by exact
node.name match with the awk id-carry pattern (a windowed grep once
destroyed OTHER apps' nodes and silenced Firefox).
## Testing recipes (headless, no clicking)
- Self-driving test page: `TINYJS_HTML=/abs/path/page.html tinyjs dev`
(from any app dir). Page saves results via `tiny.store.set(...)`; read
them from `~/.local/share/<app-id>/store.json`. Wait ~2s for `tiny`.
- Drive a running app via MPRIS:
`gdbus call --session -d org.mpris.MediaPlayer2.<app_id_underscored> -o
/org/mpris/MediaPlayer2 -m org.mpris.MediaPlayer2.Player.Play` (also
Stop, Properties.Get Position/Volume, Properties.Set Volume).
- Kill launchers by exact name: `for p in $(pgrep -x launcher-linux); do
kill $p; done`. NEVER `pkill -f` a pattern that matches your own shell —
it kills the agent's command (exit 144) and reads as a mystery failure.
A BUILT app's copy is named `launcher` (dist/), not `launcher-linux`, so
that loop misses it — `pgrep -x launcher` too, or kill by pid. A survivor
is not harmless: it keeps its modal dialogs up, it swallows the XTest keys
the next run wanted, and (apps being single-instance) a second launch just
forwards its argv to the old process and exits. Host `pgrep` also sees
processes inside running docker containers — a cleanup loop during a
containerised check.py run kills ITS launchers (reads as a timeout).
- Audio measurement: `pw-cat --record --target <sink-or-name> -P
'{ stream.capture.sink=true }'` + a few lines of Python (rms/peak).
CAUTION: the built-in ALSA sink's monitor MONO-ISES — L/R comparisons
through it are meaningless. For stereo truth, create a null sink
(`support.null-audio-sink`, audio.position=[FL FR]), point the stream at
it via `pw-metadata <id> target.object <name>`, capture ITS monitor —
and validate the rig with a hard-panned reference first.
Xruns: `pw-top -b` ERR column. Destroy test sinks when done.
If EVERY capture reads pure zeros (rig included): check
`~/.local/state/wireplumber/restore-stream` for a stale
`Output/Audio:media.role:Music:channelVolumes=0.0;0.0;` — pw-cat tags its
playback `media.role Music`, WirePlumber restores the saved zero onto it,
and nothing anywhere says so (links active, volume 1.0, mute false — the
zeros ride channelVolumes). Cost hours on 2026-07-31.