hearth keeps a screen awake while a long job runs, and keeps it true black while it waits. Every screensaver is a handful of lit pixels, so an OLED panel leaves the rest off. No install, no account, no server: a screen wake lock, an inaudible media stream, and five dark screensavers.
Live at https://hearth.anjula.dev. One bookmark starts the whole thing.
Running AI agents means waiting. The jobs are long, they are often in another tab, and the machine decides the screen has been idle and goes to sleep. That is the wrong time to sleep: a display that has dimmed is how you lose the last five minutes of a job to a locked screen.
hearth holds the screen on, and it does it honestly. It tells you which of its two holds is carrying the watch right now, and it never pretends a web page can change a power plan.
| Hold | What it is | When it works |
|---|---|---|
| Screen wake lock | navigator.wakeLock.request('screen') |
While hearth's document is on screen |
| Media hold | a 2x2 canvas stream and a 40 Hz tone at about -80 dB, on a hidden video element | While the tab is in the background, because a tab that is playing media is exempt from the browser's freezer and timer throttling |
| Mini window | document picture-in-picture: a second, visible document with its own lock | Chrome and Edge, while you work in the window running the agents |
The three are complementary, not alternatives. The wake lock is the strong one and the browser releases it the moment the tab goes away. The media hold is what carries the watch in between. The mini window is the only way to keep the strong hold while the main tab is in the background.
The tab title and the favicon say the same thing as the panel: the ember is lit while a screen lock is held, dimmed while only the media stream is holding, and out when nothing is.
What a page cannot do. It cannot change an operating system's power settings. If the display or the machine still sleeps, that is the power plan, and the panel lists the commands for each platform. The honest summary is in the app, under Straight answers.
The stage is true black, so an OLED panel leaves those pixels off. Every mode is a handful of lit pixels, and every animation only moves or fades, which the compositor can do without a repaint.
| Mode | What it shows | Updates |
|---|---|---|
| Clock | the time, the date, and the length of the watch | once a second, or once a minute with seconds off |
| Ember | one small fire, breathing | CSS animation only |
| Stars | dim points drifting upward in three slow layers | CSS animation only |
| Agents | a count of the jobs, each breathing on its own schedule | once every 45 seconds |
| Minimal | a dot and the elapsed time, black almost everywhere | once every five seconds |
Four things keep a panel out of trouble, all on by default except rotation: the whole stage shifts a few pixels every two minutes, the dim steps fall away while nobody is there, rotation walks through the modes every ten minutes, and the palette is warm rather than white. There is no animation frame loop anywhere in the app; the only recurring timer stops entirely while the tab is hidden.
- Bookmark: drag the Wake the hearth link from the panel to your bookmarks bar. It
opens hearth with
?start=1, and opening it again continues the same watch instead of restarting the clock. - Link:
https://hearth.anjula.dev/?start=1.#startworks too, and a link can carry?mode=stars,?agents=5,?dim=deepor?mode=rotate. - App: install it from the browser menu. The service worker keeps the shell, so it opens offline.
| Key | What it does |
|---|---|
Space |
start or stop the watch |
V |
next mode: screensaver, panel, mini window |
M |
next screensaver |
D |
next dim step |
F |
fullscreen |
N |
mini window |
Esc |
leave fullscreen, or a preview |
While the watch runs it can live in one of three places, chosen in the status card before or during a watch: the Screensaver (the black stage), the Panel (the regular screen, with the watch running behind it), or the Mini window (a small always-on-top window, so you can work in the one running the agents). Fullscreen sits in the same control and belongs to the screensaver, so starting never grabs the screen unless you asked for it.
The stage bar carries the same modes, plus a split button for the screensaver: the name advances it, the chevron opens the list of five.
Settings live in local storage, so they come back with the tab. So does a running watch: reload the page, or reopen it after the browser was closed, and hearth picks up where it left off.
| Command | What it does |
|---|---|
npm run dev |
Vite dev server on http://localhost:5174 |
npm run build |
typecheck, then the production build into dist/ |
npm run check |
vue-tsc over the app, the tests and the scripts |
npm test |
unit tests, Vitest |
npm run test:e2e |
build, then the interface tests, Playwright |
npm run test:e2e:update |
the same, rewriting the screenshot baselines |
npm run smoke |
handshake with the deployed site, after a deploy |
npm run verify:headed |
the two things a headless browser cannot check, in a real window |
npm run assets |
the icons, the social card, the manifest, the CNAME (needs ImageMagick) |
Unit tests cover the parts that are worth proving without a browser: the link options, the clock and duration formatting, the mode rotation, the day's total, and the one function that decides which hold the UI reports.
Interface tests run the built site in Chromium at a fixed minute, with a fixed wake lock and battery. They check behaviour (the media stream really has a video and an audio track, the keyboard drives the stage, dimming follows the clock, the pixel shift moves, stopping clears the session) and they compare screenshots: the stage and every screensaver against baselines, on every platform. The two full page panel screenshots are generated on the runner, because a long page of prose wraps a line or two differently on Windows than on Linux, and no pixel tolerance can tell that apart from antialiasing.
npx playwright install chromium # once
npm run test:e2eLive check. npm run smoke loads https://hearth.anjula.dev/?start=1 in a real browser
and asserts what only production can show: the domain answers, the stage is black, the media
stream has both of its tracks and is playing, the tab title reports the hold, the service
worker is activated, and a reload continues the watch instead of restarting it. It exits
non-zero, so it works as a post-deploy gate.
Headed check. Headless Chromium denies the screen wake lock, so two things are outside
what the suite above can prove. npm run verify:headed opens a real window and checks them:
that the browser grants the lock and the tab says screen lock held, and that the mini window
opens, holds its own lock, and keeps holding it while the main tab is hidden. It exits
non-zero too, so run it after touching awake.ts or mini.ts.
GitHub Pages serves the repository root of the custom subdomain. The workflow builds with
BASE_PATH=/, and public/CNAME carries the domain.
The two repository settings are made once, and here they can be made with the API rather than by hand:
gh api -X POST /repos/<owner>/hearth/pages -f build_type=workflow
gh api -X PUT /repos/<owner>/hearth/pages -f cname=hearth.anjula.devhttps_enforced cannot be turned on until GitHub has provisioned its own certificate for
the domain, which a Cloudflare-proxied record may never ask for. The site is served over
HTTPS either way. The DNS record points hearth.anjula.dev at GitHub, the way the other
subdomains do.
Nothing leaves the page. There is no analytics and no account, and the type is served from this origin rather than a font host, so after the files load there are no outside requests at all. The mode, the settings and the day's total live in your own local storage, and nowhere else. The service worker keeps the shell and the fonts, so hearth opens with no network either.
MIT. Built by Anjula Karunarathne. The palette and the type are the shared design system, published at https://anjula.dev/design/tokens.css.