How AwakeMode Became Agent-Friendly

How AwakeMode uses stable selectors, semantic HTML, DOM state, llms.txt, agent.json, and client-side wake lock constraints to help AI browser agents operate the app reliably.

Agent-readable browser utility interface

AI agents are getting better at using websites, but most websites still make agents guess.

They force browser agents, QA automation, and LLM coding tools to infer intent from button text, styling classes, visual layout, or brittle DOM structure. That works until a designer changes copy, a Tailwind class moves, a card layout shifts, or a primary action changes from “Start” to “Stop.”

AwakeMode is a small browser utility with a specific job: help a user keep their computer awake during focused work, downloads, dashboards, screen sharing, and long-running tasks. Because the product is simple, it is a good place to make the interface deliberately agent-friendly without adding fake complexity.

This is the engineering model we use:

  • Stable selectors for browser agents and tests.
  • Real semantic HTML controls.
  • Explicit state on the app wrapper.
  • Generated agent-facing metadata.
  • Clear LLM-readable documentation.
  • No fake backend endpoint pretending to start wake lock.

The goal is not vague “AI SEO.” The goal is a page that humans can use, browser agents can operate, test suites can verify, and future developers can understand.

Why agent-friendly UI matters

An AI browser agent needs to answer basic questions before it acts:

  • Where is the app?
  • Which control selects timer mode?
  • Which input accepts custom minutes?
  • Which button starts the session?
  • Did the session become active?
  • Is the browser unsupported, blocked, paused, or released?
  • If the main control is out of view, where is the compact active-session dock?

If the only available signals are CSS classes and visible text, the agent has to guess. For example, a button with class="bg-accent-500" might be a start action today and a delete action tomorrow. A text label might change from “Start session” to “Begin” during a copy pass. A visual card might move in the layout while the product behavior stays the same.

Stable machine-readable hooks solve that.

Stable data-agent selectors

AwakeMode exposes stable data-agent attributes on the core app controls. These are intentionally separate from styling classes.

Examples:

<section
  id="awake-control"
  data-agent="awake-app"
  data-session-state="released"
  data-session-mode="indefinite"
  data-wake-lock-supported="false"
>
  ...
</section>

Mode controls use real radio inputs, but the clickable label is what gets the agent selector:

<label data-agent="mode-timer">
  <input type="radio" name="awake-session-mode" value="timer" />
  <span>Timer</span>
</label>

That means a browser agent can do the obvious thing:

await page.click("[data-agent='mode-timer']");
await page.fill("[data-agent='timer-minutes']", "120");
await page.click("[data-agent='start-session']");

The important selectors include:

  • data-agent="awake-app"
  • data-agent="session-mode-group"
  • data-agent="mode-indefinite"
  • data-agent="mode-timer"
  • data-agent="timer-minutes"
  • data-agent="timer-preset-15"
  • data-agent="timer-preset-30"
  • data-agent="timer-preset-60"
  • data-agent="timer-preset-120"
  • data-agent="start-session"
  • data-agent="stop-session"
  • data-agent="pause-session"
  • data-agent="resume-session"
  • data-agent="session-status"
  • data-agent="time-remaining"
  • data-agent="progress-bar"
  • data-agent="settings-toggle"
  • data-agent="browser-compatibility"
  • data-agent="mini-session-dock"

These names are product contracts. CSS classes can change. Layout can change. The selectors should remain stable unless the agent docs and tests are updated at the same time.

Explicit DOM state

Selectors tell an agent where to act. State attributes tell an agent what happened.

The AwakeMode app wrapper exposes:

data-session-state="released"
data-session-mode="indefinite"
data-wake-lock-supported="true"

Valid session states are:

  • released
  • active
  • paused
  • blocked
  • unsupported

Valid modes are:

  • indefinite
  • timer

This matters because agents should not have to parse prose such as “Wake lock is active and the countdown is running.” That sentence is useful for people. The DOM state is useful for agents and tests.

A test or browser agent can assert:

await expect(page.locator("[data-agent='awake-app']")).toHaveAttribute(
  "data-session-state",
  "active"
);

That is more reliable than looking for text, color, or a specific card layout.

Dynamic start and stop actions

AwakeMode has one primary action area, but the available action changes with state.

When no session is active:

<button data-agent="start-session">Start session</button>

When a session is running:

<button data-agent="stop-session" data-agent-location="main">
  Stop session
</button>

When a timer is paused:

<button data-agent="resume-session" data-agent-location="main">
  Resume session
</button>

The selector reflects the current action. This lets an agent wait for the next available control instead of guessing from button text.

The mini dock can also expose a stop action. To avoid ambiguity, duplicated actions include location:

<button data-agent="stop-session" data-agent-location="mini">Stop</button>

The shared selector still works:

await page.click("[data-agent='stop-session']");

But more precise agents can choose:

await page.click("[data-agent='stop-session'][data-agent-location='mini']");

Semantic controls first

Agent-friendly UI is not separate from accessible UI. It starts with real controls.

AwakeMode uses:

  • <button> for actions.
  • <input type="radio"> for session mode.
  • <input type="number"> for custom timer minutes.
  • aria-live="polite" for status and time remaining.
  • Clear labels for interactive controls.

That helps keyboard users, screen readers, browser automation, and LLM-driven agents at the same time.

The app does not use clickable div elements for core actions. It does not make agents click visual wrappers that have no semantic meaning. The browser already has a control model, and agents benefit when we use it.

Agent-facing files

AwakeMode publishes machine-readable and LLM-readable files:

  • /llms.txt
  • /llms-full.txt
  • /agent.json
  • /api/capabilities.json

These are not control endpoints. They are documentation and metadata surfaces.

/llms.txt is the short summary. It tells an LLM what AwakeMode does, where the primary app lives, what actions are available, and what browser limitations matter.

/llms-full.txt is the detailed guide. It includes user flow, stable selectors, app state attributes, browser limitations, and instruction not to rely on CSS classes.

/agent.json is the structured machine-readable map. It includes:

  • App name and production URL.
  • StarterBuild creator metadata.
  • Account and install requirements.
  • Wake lock capabilities.
  • Stable selectors.
  • Valid state values.
  • Action recipes for starting and stopping sessions.

/api/capabilities.json is a compact capabilities description. It lists supported modes, timer presets, timer limits, and the key wake lock rule: the wake lock is requested client-side in the user’s visible browser document.

Why there is no backend wake lock endpoint

AwakeMode deliberately does not expose:

POST /api/start-wake-lock
POST /api/stop-wake-lock

Those endpoints would be misleading.

The server cannot keep a user’s computer awake. Screen Wake Lock is controlled by the browser. It must be requested from the user’s active, visible browser document. It can fail if the browser is unsupported, the document is hidden, the system blocks the request, or the device policy changes.

Backend endpoints are useful for describing the app. They are not useful for pretending to control local browser wake lock state.

That distinction is important for trustworthy agent SEO. A browser agent should understand what the app can do, but it should not be told that a server API can perform a client-only browser action.

The selector source of truth

AwakeMode keeps selector names in code rather than scattering them across docs.

The source of truth is:

src/lib/awake/agentSelectors.ts

That file exports:

  • agentSelectors
  • sessionStates
  • sessionModes
  • timerPresetsMinutes
  • timerLimits

The generated metadata files import from that source. Tests import from it too. This prevents documentation drift.

If the selector for timer minutes changes, /agent.json, /llms-full.txt, and tests should all reflect the same update.

Mini dock behavior for agents

When the main control panel scrolls out of view and a session is active, AwakeMode shows a compact in-page mini dock.

It is not Picture-in-Picture. It is not a separate window. It is not a permission prompt. It is just a quiet status and control dock inside the page.

The dock exposes:

data-agent="mini-session-dock"
data-visible="true"
data-session-state="active"
data-session-mode="timer"

Timer mode can show:

ACTIVE | 1:59:35 | Pause | Stop

Indefinite mode can show:

ACTIVE | Indefinite | Stop

Agents can inspect the dock when the main panel is out of view, but the main app wrapper remains the primary state source.

SEO for agents is still SEO

Agent-readable pages also help traditional search when they are honest and specific.

This article uses the same terms developers and AI systems are likely to search for:

  • AI agent friendly website
  • browser agent selectors
  • llms.txt
  • agent.json
  • stable data-agent selectors
  • semantic HTML for agents
  • browser automation
  • wake lock API
  • client-side wake lock
  • app state attributes
  • machine-readable metadata

But the point is not keyword stuffing. The point is accurate language around real implementation details.

AwakeMode is now easier for:

  • Humans using the page.
  • QA automation verifying the page.
  • Browser agents operating the page.
  • LLM coding tools changing the repo later.
  • Search systems trying to understand what the app does.

The practical agent flow

A browser agent should operate AwakeMode like this:

  1. Open https://awakemode.com/.
  2. Locate [data-agent='awake-app'].
  3. Inspect data-wake-lock-supported.
  4. Select [data-agent='mode-indefinite'] or [data-agent='mode-timer'].
  5. If timer mode is selected, fill [data-agent='timer-minutes'] or click a preset.
  6. Click [data-agent='start-session'].
  7. Confirm [data-agent='awake-app'] has data-session-state="active".
  8. Watch [data-agent='time-remaining'] if timer mode is active.
  9. Stop with [data-agent='stop-session'].
  10. Confirm the app returns to data-session-state="released".

That is the shape of a utility that agents can use without guessing.

What we avoided

We did not add a futuristic dashboard. We did not add fake metrics. We did not add a backend endpoint that claims to start wake lock. We did not make a separate Picture-in-Picture surface.

The app remains simple:

  • Choose a mode.
  • Start a wake session.
  • Monitor the state.
  • Stop cleanly.

The agent layer supports that flow instead of distracting from it.

What comes next

The next useful step is browser-level testing with mocked wake lock support. Dependency-light Vitest tests can verify selector constants and generated metadata. A future Playwright test can mock navigator.wakeLock.request, click the same data-agent selectors agents use, and assert state changes on the app wrapper.

That would close the loop:

  • The UI exposes stable controls.
  • The metadata documents those controls.
  • Tests operate the same way agents do.
  • Future code changes keep the contract intact.

That is the real value of making a site agent-friendly.