Every release is a git tag; tinyjs update gets you the latest.
dev/build/publish and auto-update all work cross-platform — X11 and Wayland, Ubuntu 24.04+ and current distros with webkit2gtk-4.1. Built apps register their own .desktop entry on first run (app menu, icon, deep links, file associations, single instance); publish emits <name>-<version>-linux-<arch>.tar.gz with a per-arch linux manifest block beside the mac and win ones. Nearly the whole API surface is live — windows and frameless chrome, menus, tray, dialogs, clipboard, notifications with actions, secrets, global hotkeys, MPRIS nowPlaying + media keys, speech, spotlight, printToPDF, and more; the gaps fail cleanly so cross-platform code can feature-detect. Burn-down: TODO-linux.md.audioTap on Linux. scope:'app' is real per-app capture — a private PipeWire null sink fed by the app's own output ports, so it hears itself and nothing else, playback untouched.tiny.audio — native DSP on your own output (Linux). Web Audio's graph crackles on WebKitGTK (rendered on a normal-priority thread), so audio.filters() runs the EQ/crossover/correction chain in PipeWire instead: up to 28 filters, live gapless retunes, per-channel gains, and it covers audio the page never sees — raw radio, native HLS. audio.balance() pans on the chain's output. Gated by capabilities().audioFilters; on macOS/Windows keep using Web Audio, which works there.tiny.system — know the machine, name what's missing. Sync os()/isLinux() & friends, info(), capabilities(). On Linux, requirements() maps missing features to per-distro packages, and promptMissing() shows a native dialog with a Copy install command button — and shows nothing when all is well, so call it straight from a failure path.data-tiny-noresize), win.startResize(edge) for custom handles, and setResizable(false) no longer blocks the app's own setSize. Plus, cross-cutting: win.setMinSize() / minSize in win.open (a WM-enforced floor), and win.setZoom() — native page zoom for crisp double-size modes on hi-dpi screens.windowPlacement manifest key. Apps whose design is windows placing each other opt into X11/XWayland, where self-placement works; everything else stays native Wayland.curl -fsSL https://tinyjs.app/install | sh detects Linux; releases ship tinyjs-linux-x86_64 and -arm64 tarballs plus standalone tjs builds, checksummed beside the macOS and Windows assets.launcher.exe (the WebView2 host), so pinning it pinned a binary that can't start on its own — the pin was dead on next launch. Every tinyjs window now carries System.AppUserModel relaunch properties (command/name/icon pointing at the app exe, plus a stable per-app AppUserModelID), so pins and Start-Menu launches start the actual app. The toast-notification Start-Menu shortcut targets the app exe too.tjs.spawn can't pass CREATE_NO_WINDOW, so every console child (tasklist, tar, reg, …) spawned from a GUI-subsystem app popped a visible console. New launcher-win.exe --run mode runs any console tool hidden with stdio and exit code passed through; the bridge and updater route their own spawns through it, and apps get app.spawnHidden(args, opts) — plain tjs.spawn on macOS, hidden on Windows.tinyjs build stamps the icon into a clean copy of the runtime before app compile appends the TPK bundle, so dist/<name>.exe shows the real icon in Explorer, the taskbar, and registry DefaultIcon entries.dist/frontend/ and dist/icon.png — a built app is <name>.exe + launcher.exe, nothing else. Older dists keep working.win.notes field in the update manifest overrides the top-level notes on Windows.url/sha256 stay the macOS zip; a win: { url, sha256 } block carries the Windows build. Windows apps overlay it on check and report "no update" when a release ships no Windows zip — never a mac download. tinyjs publish on Windows names the zip <name>-<version>-win.zip and emits the win block; publishing both platforms merges the two manifests (mac owns the top-level fields).tiny.fileURL(path) — one correct file:// builder for both OSes (drive letters, backslashes, UNC hosts, percent-encoding). A hand-rolled 'file://' + path was the single most common porting bug; stop writing it.file://server/share URLs (Parallels shared folders, network drives) are host-bearing and therefore cross-origin to Chromium — a WebResourceRequested interceptor now serves them with CORS headers and full Range/206 support, so seeking and WebAudio analysis work like local files.win.open webview now gets the identical wiring — context menus, the UNC interceptor, drag-and-drop forwarding, menu accelerators — and Chromium's autoplay gesture-gate is off, so satellite windows (a visualizer analysing a silent twin stream) start without ever being clicked.WS_EX_NOREDIRECTIONBITMAP at creation), setChrome transparency applied at any time, and transparent main windows — declare those in tinyjs.json "chrome"; a Windows window can host transparency or a Win32 menu bar, never both.emoji: tray icons — the asset-free twin of macOS sf: symbols. tray.set({ icon: 'emoji:🍵' }) renders the glyph as a monochrome silhouette that follows the taskbar theme; branch per-OS and neither platform ships an icon file.audioTap scope 'app' rides the system loopback rather than rejecting — honest degradation until true per-process capture lands.store.set bursts are serialized and renames retry — Defender/indexers transiently lock fresh files on Windows, which surfaced as EPERM under load.window.chrome shadowing. A page declaring its own chrome no longer severs the RPC channel (the post hook is stashed at document-start). Related rule now in the skill: never declare a top-level chrome identifier — on WebView2 a const chrome is a parse-time SyntaxError that kills the entire script.SWP_NOACTIVATE), new windows clamp onto the virtual screen (stale saved positions can't open apps in the void), and win.open position/size honor DPI scaling everywhere.setPosition, win.open size/position, SIZE, getState, mousePosition, screens, tray.position. Also: WebView2 now gets --ignore-gpu-blocklist --enable-unsafe-webgpu, so navigator.gpu yields a real adapter even on virtualized GPUs — WebGPU parity with the macOS launcher.file:// URL an opaque origin, so createMediaElementSource on a local track was CORS-tainted and output pure zeros — record players, EQs, and visualizers ran silent while the element "played". The bridge now passes --allow-file-access-from-files to WebView2 (via a small patch to the vendored webview loader, which ignored WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS), making file:// same-origin like WKWebView treats page-dir files on macOS. Verified end-to-end with platter: art + audio both work.win.open — each window hosts its own WebView2 with the full bridge; <winid>:<seq> call routing like macOS), drag & drop with real paths both directions (IDropTarget in, DoDragDrop/CF_HDROP out), printToPDF, captureScreen (no permission dance), thumbnail (shell image factory), say/voices (SAPI), clipboard image write (CF_DIB), menu key: accelerators (Ctrl+<key> via WebView2 AcceleratorKeyPressed), launchAtLogin for built apps (HKCU Run), chrome transparent + vibrancy→mica/acrylic backdrops (Win11 22H2+), accessory apps drop the taskbar button, and tinyjs publish + app auto-update: manifest sha256 via WebCrypto (no shasum spawn on either OS), bsdtar zip, and an in-place file-by-file swap on install (Windows can't rename a folder holding a running exe; locked exes park as *.update-old, swept next update) — verified end-to-end with a real 1.0.0→1.0.1 self-update. Launchers now chdir out of the app folder so updates never block on a cwd handle.irm https://tinyjs.app/install.ps1 | iex installs a prebuilt tinyjs-windows-x86_64.zip (built by the release workflow's new windows-latest job, checksummed alongside the macOS tarballs) into %LOCALAPPDATA%\tinyjs and adds it to the user PATH — no git, no compiler. tinyjs update re-runs the installer on Windows too. Dev nicety: tinyjs dev from a source checkout rebuilds launcher-win.exe automatically when native/launcher-win.cc or runtime/tiny.js changed.native/launcher-win.cc, WebView2 via the same vendored webview library) speaks the identical wire protocol over a named pipe, and the CLI, bridge, and build work cross-platform. Working: the full page↔backend bridge (calls, events, tiny.fetch), dev mode with hot reload, Vite devUrl frontends, tinyjs build (portable dist/ folder with <name>.exe), native file/save/folder dialogs, alert/confirm/prompt, menu bar + tray (+ balloon notifications), custom context menus, clipboard (text/html/files/image read), global hotkeys, keystroke synthesis (cmd maps to Ctrl), shell open/reveal/trash, secrets (Credential Manager), power.preventSleep, theme + sleep/wake events, window ops (chrome/fullscreen/ontop/click-through/…). The macOS-specific APIs (Quick Look, OCR, AppleScript, Touch ID, vibrancy, Dock, Spaces…) reject or report 'unsupported' cleanly so cross-platform app code can feature-detect.tiny.app.ai (on-device LLM) — landed behind an opt-in build, not yet in the released tarballs. ai.generate(prompt, { instructions }) runs Apple's FoundationModels locally (offline, no API key, private); ai.availability() → 'available' | 'unavailable' | 'unsupported'. Implemented as a small Swift shim linked into the launcher with TINYJS_AI=1 ./setup.sh (needs the macOS 26 SDK); the binary keeps the macOS 14 floor and weak-links FoundationModels, so stock builds and older systems just report 'unsupported'. Shipping it in the release tarballs needs the CI runner bumped to a macOS 26 image — held for a deliberate pipeline change. Proven working end-to-end locally (real generation, ~250ms).createApp({ userAgent }) or "userAgent" in tinyjs.json overrides the webview's User-Agent string (packaged apps read the TinyjsUserAgent Info.plist key the build writes; dev passes it via TINYJS_UA). WKWebView's default UA omits the trailing Version/x Safari/x, so UA-sniffing sites reject it as an unknown browser — mainly useful when pointing a devUrl at a real hosted site. Caveat: a UA alone often isn't enough. Many SaaS apps (Slack, etc.) also feature-detect at boot and refuse embedded browsers, and an embedded WKWebView genuinely lacks some of what they require (e.g. Web Push) — so this gets you recognized as a browser, not necessarily in.TINYJS_INJECT (footgun, dev-only). An env var that injects arbitrary JS into every page at document-start on any origin, for shimming a third-party site wrapped via devUrl. Deliberately undocumented in the public API with no packaged-app equivalent — it runs your code inside someone else's origin with full page privileges. A debugging tool, not for shipping.tiny.proxyURL now plays live internet radio through the Web Audio / EQ graph. A live (icecast/shoutcast) stream answers 200 with no Content-Length and no byte-range support, so WKWebView's custom-scheme media loader rejected the <audio> with error 4 (SRC_NOT_SUPPORTED) — the limitation noted in 0.25.0. The proxy now detects a length-less audio/*/video/* response and serves it with a synthetic large Content-Length (dropping the contradictory range/chunked headers), so the element plays it progressively as one long non-seekable resource and MediaElementSource taps it for real. Verified end-to-end on live StreamTheWorld MP3, both direct and via the redirect: error 4 → playing with live analyser data. Redirects were never the issue. Tradeoff: for a live stream audio.duration/currentTime are meaningless (the synthetic length maps to a huge timeline) — don't wire a seekbar to one. The fake length is gated to media MIME types, so ordinary fetch() through proxyURL is unaffected.tjs runtime is Apple-Silicon-only, so a distributed app used to launch on an Intel Mac and then hang on a backend that could never start. The launcher now detects this (posix_spawn returns EBADARCH) and shows a plain "This app needs an Apple Silicon Mac" apology, then exits cleanly. The tinyjs CLI also prints a non-fatal heads-up when run on an Intel Mac. tinyjs remains Apple-Silicon-first."contextMenu": false in tinyjs.json suppresses WebKit's default right-click menu (Reload / Back / Forward / Inspect Element…) for an app-like feel. Off by default — the browser menu shows unless you opt out. A custom tiny.menu.setContext(...) still overrides, and clearing it falls back to suppressed (not the WebKit default) while the option is on.tiny.audioTap TCC behavior. The first start() does prompt for "System Audio Recording" — even scope:'app', because WKWebView renders audio in a separate com.apple.WebKit.GPU helper, so the tap is a cross-process capture (the grant then persists per app, which is why repeat runs don't re-prompt). Authorization is deferred to the first start() (declaring the manifest key does nothing at runtime), so the tap can be lazy-armed. Under tinyjs dev the audio owner is the terminal, so the tap delivers real PCM only if that terminal holds the grant (else silent chunks). No code change — the implementation already defers to start().tiny.audioTap — read the app's (or the system's) rendered audio output as PCM. For VU meters and visualizers — including audio that never touches Web Audio (native HLS, CORS-tainted streams, other apps). Read-only: it observes the mix in sync with what's audible, it can't process it (EQ still needs the signal in the graph — that's proxyURL; the two compose). await tiny.audioTap.start({ scope: 'app' | 'system', interval }) then tiny.audioTap.on(({ pcm, sampleRate, channels, frames, t }) => …) where pcm is base64 of interleaved little-endian Int16; stop() (or the owning window closing) tears it down. Gated by an "audioTap": "app" | "system" key in tinyjs.json (the build bakes NSAudioCaptureUsageDescription). Implemented with Core Audio process taps (macOS 14.2+): a CATapDescription → AudioHardwareCreateProcessTap → an aggregate device → an IOProc that downmixes to stereo Int16. scope:'app' selects exactly this app's audio by matching the responsible pid (WKWebView renders audio in a com.apple.WebKit.GPU XPC helper that isn't a child process). Authorization is deferred to the first start() (declaring the manifest does nothing until then), so the tap can be lazy-armed; that first start() prompts for "System Audio Recording" — even scope:'app', since the GPU-helper tap is a cross-process capture — and once granted yields real audio (verified in a real bundle: RMS matched a test tone to 3 decimals). scope:'system' also hears other apps. Survives output-device changes by re-arming. macOS-only for now; cross-platform by design. Note: under tinyjs dev the audio owner is the terminal, not the app, so the tap delivers only if that terminal holds the grant (else silence); a built .app owns its own grant.tiny.proxyURL — driving <audio> through the proxy needs a byte-range-capable (HTTP 206) stream; a non-seekable live stream that answers 200 can't play through the proxy element (fall back to the raw URL). Redirects were never the issue.tiny.proxyURL(url) — a cross-origin stream (internet radio) into Web Audio. A MediaElementSource on a cross-origin <audio> outputs silence by spec, so radio can't drive an EQ or analyser. proxyURL returns a same-app tiny-media://… URL that streams the remote http(s) resource through the native layer (NSURLSession — redirects, byte-range/seek, native buffering) and injects Access-Control-Allow-Origin: *; with <audio crossorigin="anonymous"> the element is CORS-approved and untainted, so the full Web Audio graph gets real samples. Implemented as a WKURLSchemeHandler registered on every webview via a swizzle of -[WKWebView initWithFrame:configuration:] — no backend hop, no base64, no TCP port. Verified end-to-end: a MediaElementSource on a proxied SomaFM stream drove a live AnalyserNode with real (non-silent) waveform data. macOS only for now; the design maps to WebView2 and WebKitGTK when those backends land.tiny.fetch(url, init) — a fetch that runs in the backend, so no CORS. The request runs in the native process, so it has no CORS, CSP, or mixed-content limits: the page can reach any origin. Same shape as window.fetch and resolves to a real Response (res.json() / res.text() / res.headers / res.ok all work); request bodies can be strings, ArrayBuffer/typed arrays, Blob, or URLSearchParams. Pass { stream: true } for a live streaming body — the backend holds the connection open and the page pulls chunks on demand (res.body.getReader()) with natural backpressure, which is what an endless source like internet radio needs (a buffered fetch would never resolve); reader.cancel() or closing the window tears the upstream connection down. Reuses the existing request/response bridge (base64 chunks over CALL→RET), so no new wire frame and no launcher change. A page with CORS-free network reach has the app's full network reach — not a new trust boundary (every page already holds an RPC channel to a backend with full system access), but worth knowing.tinyjs notarize --dmg rebuilds the installer from the stapled .app. stapler mutates the bundle in place after Apple's verdict, so a dmg made at build --dmg time holds the pre-staple .app with no ticket embedded — online Gatekeeper hides this (it fetches the ticket), but offline / first launch before network it blocks the app. notarize now rebuilds the dmg from the stapled bundle when --dmg is passed, and refreshes an existing dmg on disk automatically (that copy is guaranteed stale). Ship a dmg with tinyjs build --dmg && tinyjs notarize --dmg.chrome: { acceptsFirstMouse: true }. Opt a window into delivering the click that focuses it straight through to the page. macOS normally swallows that first click into web content ("click once to focus, again to act"), which also bites between an app's own windows and makes an unfocused window's DOM drag region need an extra click. On per window (main or secondary), via tiny.win.setChrome, app.openWindow's chrome, or tinyjs.json "chrome" (applied before first paint); reported back in getState().chrome. Off by default — matches the platform, no change for existing apps.app.eval, a window handle's eval, and the event pushes behind app.push / push — was flattened onto a single line before it crossed the socket, so a // line comment silently commented out everything after it (a whole multi-line snippet could vanish after its first comment). Snippets now travel wire-escaped and are unescaped in the launcher, so real newlines survive intact; event payloads containing newlines or backslashes round-trip unchanged.tinyjs notarize fails fast on the wrong signature. It now inspects what the built .app is actually signed with (via codesign) and stops immediately unless that's a Developer ID Application certificate — naming what it found (ad-hoc, unsigned, or e.g. an "Apple Development" cert) instead of uploading, waiting minutes, and coming back with Apple's opaque "Invalid". It checks the artifact on disk, not just the configured identity, so a build that predates a config change is caught too.TINYJS_SIGN_IDENTITY env var, not just signIdentity in tinyjs.json — both have always worked (as does TINYJS_NOTARY_PROFILE for the notary profile); the message just never said so.tinyjs uninstall. Cleanly removes the install (~/.tinyjs, or $TINYJS_HOME) and the tinyjs PATH symlink — the exact inverse of the installer. It removes only a symlink that points back into the install it's running from (an unrelated tinyjs on your PATH is left alone), prints what it'll delete, and prompts first (--yes skips the prompt; a non-interactive run without --yes refuses rather than deleting silently). It won't touch a source checkout, /, or a directory containing .git. The shell-profile PATH line is left in place with a note.tinyjs: command not found, run hash -r or open a new one.sf: SF Symbol icons were never affected.chrome.squareCorners — drop macOS's rounded window corners. setChrome({ squareCorners: true }) (or "chrome": { "squareCorners": true } in tinyjs.json, applied before first paint) makes the window borderless: square, no titlebar, no traffic lights. Deliberate/un-native — you lose the native titlebar drag (use data-tiny-drag) but keep resize edges, shadow, and keyboard focus (a canBecomeKeyWindow override restores focus). Works on main + secondary windows; survives setSize; reported in getState().chrome.store.set()/delete() can no longer crash the backend — a write failure used to surface as an unhandled rejection and take the process down. The store now swallows its own write errors (resolves false, keeps the in-memory value) and uses a unique temp file per write so un-awaited bursts don't race.openWindow gains chrome + { x, y }, applied before first paint — a frameless/vibrancy panel never flashes its titlebar and a positioned window opens where you asked. getState().chrome is now per-window.readAccess option — widen the file:// read root (createApp({ readAccess: true }) = home, or a path; also "readAccess" in tinyjs.json) so <audio>/<img> can load media outside the frontend dir without a base64 round-trip.win.printToPDF(path) — render the page to a vector PDF (WKWebView) → { path }; invoices/reports without a dialog.app.haptic(pattern) — trackpad haptics ('generic'|'alignment'|'level').app.dockIcon(pngPath) — replace the Dock icon (render a canvas for progress rings/badges); '' resets.app.battery() — { percent, charging, plugged, minutesRemaining } | null (IOPowerSources).app.wifi() — { ssid, bssid, rssi, noise, txRate } | null (CoreWLAN; ssid needs Location).app.spotlight(query) — find files by name/content (NSMetadataQuery, up to 100 paths) — no mdfind spawn.win.setClickThrough(bool) (mouse events pass through — overlays), win.setLevel('normal'|'floating'|'overlay'|'desktop') (overlay floats above fullscreen apps, desktop pins behind windows for pets/wallpaper), win.setAllSpaces(bool) (follow onto every Space). All round-trip through getState(); backend twins on app and app.window(id).app.selectedText() — the text selected in the frontmost app (PopClip-style popovers; Accessibility).app.otherWindows() / app.moveWindow(pid, rect) — enumerate and arrange other apps' windows (Rectangle/Magnet; Accessibility).tray.position() — the tray icon's on-screen rect, to anchor a dropdown window under it.app.recorder — record a display to an .mp4: start({ path, screenId? }) resolves once capturing, stop() resolves { path, duration } once finalized. SCStream → H.264 → AVAssetWriter, video only for now, one at a time. Needs the 'screen' permission + macOS 14 (preflighted, so an ungranted start rejects immediately instead of hanging).app.nowPlaying — set({ title, artist, album, duration, elapsed, playing }) shows your app in Control Center / the lock screen and arms the hardware media keys; presses (F7/F8/F9, AirPods, Control Center) arrive as the media-key event / onMediaKey export with { command, time? }.app.say(text, { voice, rate }) — text-to-speech (AVSpeechSynthesizer), resolves when playback finishes; app.voices() lists installed voices, app.stopSpeaking() interrupts.tiny.notify(title, body, { actions: [{ id, title, reply?, destructive? }] }) adds buttons / a reply field (packaged apps); taps arrive as notification-action / onNotificationAction with { id, action, reply }.app.pickColor() — the system eyedropper: any pixel, any app, no screen-recording permission. '#rrggbb' or null on cancel.app.ocr(path) — on-device OCR (Vision) → { text, blocks } with normalized boxes. captureScreen() + ocr() = screenshot-to-text in two calls.app.thumbnail(path, size?) — a preview png for any file type Quick Look understands (PSD, video, 3D…), rendered @2x.app.secrets — Keychain get/set/delete under the app id (the keytar/safeStorage role) — tokens go here, never in tiny.store.app.authenticate(reason) — Touch ID (or the account-password sheet) → true | false.app.applescript(source) — AppleScript in-process, no osascript spawn, under the existing automation permission."update": { "auto": "launch" | "daily" } background checks fire an update-available event / onUpdateAvailable export with { current, latest, notes }; tinyjs publish --notes "…" ships release notes in the manifest.app.quickLook(paths) — the real Finder-spacebar preview panel (QLPreviewPanel, no qlmanage spawn); an array pages with the arrow keys, quickLook() closes.app.captureScreen(screenId?) — screenshot a display via ScreenCaptureKit → { path, width, height } (png in the temp dir, yours to keep). Display id from screens(), default primary. Needs the 'screen' permission + macOS 14; rejects with the reason otherwise.app.idleTime() — seconds since the user's last input, session-wide (pause pollers when they walk away).app.power — preventSleep(reason, { display? }) / allowSleep(): one IOPMAssertion instead of spawning caffeinate — it dies with the process, so a crashed app never wedges the Mac's sleep. The reason shows in pmset -g assertions.app.frontmostApp() — { name, bundleId, pid } of the active app (who focus returns to after hide() — pairs with paste()).app.beep() / app.playSound(target) — system beep, a system sound by name ('Ping'), or an audio file path; false if it didn't load. No more afplay spawns.win.share({ text?, url?, paths?, x?, y? }) — the native share sheet, anchored at page coordinates (pass the click's clientX/clientY). Backend twin: app.window(id).share(opts).app.shell — open(target) (URL with any scheme, or a file path, in its default app), reveal(path) (show in Finder), trash(path) (move to the Trash — recoverable, prefer it over deleting user files). Each resolves true or rejects with the reason. No more open spawns.app.launchAtLogin — get()/set(v) → enabled | disabled | requires-approval | unsupported via SMAppService (packaged .apps, macOS 13+). requires-approval: the user must allow it in System Settings → Login Items.app.screens() — every display in the same top-left coordinates as setPosition: { id, name, x, y, width, height, visible, scale, primary }; visible excludes the menu bar and Dock, so multi-monitor palette placement is one call.app.paths — { home, data, cache, logs, temp, downloads, desktop, documents }, per-app-id where it matters — prefer over hardcoding ~/Library paths.app.dock — setBadge('3') / setBadge(''), bounce() / bounce({ critical: true }).'screen' permission never reads undetermined (macOS only exposes a yes/no preflight); dev-mode TCC grants attach to the shared launcher binary, not your app.clipboard.read() gains concealed (org.nspasteboard Concealed/Transient markers — clipboard-history apps skip password-manager copies by default), sourceApp ({ name, bundleId } the content came from; exact while watch() runs), sourceURL (the page a Chromium-browser copy came from), and imageSize (px — no more sips for dimensions).export function onClipboardChange(e) was silently dead in tinyjs dev/build apps — the generated entry didn't forward it to createApp.getUserMedia() now just works in tinyjs pages: the launcher answers WebKit's per-origin media prompt itself, so users see a single system dialog naming your app instead of a double prompt naming file://. tiny.app.permissions.check/request() gain microphone and camera for onboarding flows."permissions" in tinyjs.json — packaged apps declare { "microphone": "why", "camera": "why" }: injected as the required Info.plist usage strings, and builds signed with signIdentity get the matching hardened-runtime device entitlements — without which the hardened runtime denies capture even after the TCC grant.tiny.clipboard read/writes text, html, multiple files, images (png in/out), and colors via NSPasteboard in the launcher process; changeCount() plus watch()/onChange change events with a self flag for your own writes. No more per-second osascript/pbpaste spawns, scratch files, or lost multi-file writes.tiny.win.startDrag({ files, image? }) from a page mousedown starts a real native drag session: drop into Finder, Slack, anywhere.tiny.app.keystroke('cmd+v') / tiny.app.paste() post a CGEvent from the launcher: one permission (Accessibility) whose prompt names your app, not osascript or the terminal.tiny.app.permissions.check/request() for accessibility, screen, notifications, automation[:<bundle-id>] → granted / denied / undetermined, so apps can onboard instead of failing at first use.hide() now hides the app — macOS returns focus to the previously active app on its own, so palettes can hide-then-paste with no frontmost bookkeeping. show({ activate: false }) surfaces overlay/HUD windows without stealing focus.tiny.app.mousePosition() — the cursor in global, window-relative, and screen coordinates (open a palette at the mouse).signIdentity sign with the hardened runtime + secure timestamp, as notarization requires; codesign errors are no longer swallowed.app.notify() could crash the backend in dev builds (unhandled rejection from the osascript fallback). notify never rejects now — it resolves false if delivery failed.backend/tsconfig.json so editors resolve the tinyjs types in the backend folder.tinyjs new myapp --template react-ts; HMR dev server inside the native window, built assets ship as usual.tiny injected everywhere — no script tag; full TypeScript definitions ship in scaffolds."activation": "accessory" in tinyjs.json launches with no Dock icon and the window hidden, with no flash of either; tiny.win.show() when wanted, setDockVisible(true) to become a regular app.tiny.tray.set({ icon: 'sf:cup.and.saucer.fill' }): crisp, auto-templating menu-bar icons with zero shipped assets (png paths still work).primaryAction: true splits clicks: left click fires onClick (the Caffeine-style toggle), right/ctrl-click opens the menu.tiny.win.open('settings', { page: 'settings.html' }): any frontend html file becomes a window with the full tiny.* bridge; win.* targets the caller's window; backend app.window(id) handles, broadcasts, close events, and meta.window caller identity.tiny.win.setChrome({ frame, trafficLights, transparent, vibrancy }) (or "chrome" in tinyjs.json, applied before first paint in packaged apps); native resize/focus/fullscreen kept.data-tiny-drag elements move the window natively; double-click zooms.tiny.win.zoom(), startDrag(); getState() reports chrome.setResizable(false) survive later resizes.tiny.app.onNotificationClick; notify gains { id, subtitle, sound }. Ad-hoc/dev builds fall back to osascript automatically.checked, enabled: false, and nested submenu items everywhere; tiny.menu.update(id, patch) patches live items, tiny.menu.get(id) reads them back.tiny.win.getState(): position, size, fullscreen/minimized/visible/focused/on-top/resizable + screen info; plus restore() and absolute setFullscreen(bool).tiny.app.info(): app version, the tinyjs it was built with, and the runtime."urlScheme": "myapp" in tinyjs.json; myapp://… opens deliver tiny.app.onOpenUrl, cold starts included."fileExtensions": ["md"]; "Open With" and Dock drops arrive as real paths via tiny.app.onOpenFiles.open activates the running app instead of launching a copy.tiny.hotkey.register('boss', 'cmd+shift+k'); fires system-wide, even while other apps are focused.tiny.theme.get() / tiny.theme.on() for dark/light, plus sleep / wake events.get / set / delete / all, atomic writes.tiny.menu.setContext(items) replaces the right-click menu with native items.tiny.win.print() opens the native print panel.tinyjs build --dmg emits a classic installer image.tinyjs notarize wraps notarytool + stapler for Developer ID builds.tjs:sqlite, built into the runtime) documented with examples.tiny.tray.set(…), hide-on-close, Dock-less accessory mode.src/frontend/ ships as real files and relative assets just work. Hot reload bypasses caches.tinyjs publish + update.check() / update.install(): sha256 + code-signature verified, Team-ID pinned, atomic swap with rollback, relaunch.tiny.notify(title, body).file:// documents; navigator.gpu works out of the box.tinyjs update (and a once-a-day notice in tinyjs dev).tiny.api.call/on, tiny.win.*, tiny.menu.*.new / dev / build, hot reload, native file dialogs, codesigned .app bundles, ~6 MB shipped.