Skip to content

Embed the monitor

<ca-monitor> is the complete monitor: header, run switcher, tabs, filters, views and notifications. It holds the data (a MonitorStore) and the view route. Its attributes (base-url, export, history, shortcuts, run, locale, theme) are described in custom-elements.json. history="url" suits a page that is the monitor; inside an application that manages its own URL, use history="memory".

With history="memory", the monitor leaves the page to its host:

history="url" (the monitor is the page) history="memory" (embedded)
View route (run, tab, filters) the URL’s query; Back and Forward the element; the URL and the history are the host’s
Scroll positions, open sections the page’s history entry, under contraagent the element
document.title <title> monitor not written
<html lang>, <html data-ca-theme> follow the monitor’s language and theme not written; lang and data-ca-theme are set on <ca-monitor>
Single-key shortcuts anywhere in the page (shortcuts="page") only while the focus is inside the monitor (shortcuts="element")
The monitor’s listeners on document and window keydown, popstate, pagehide none

The single-key shortcuts are 1 to 9 (the tabs), r (the run switcher) and / (the filter). In every scope they never take a key typed into a text field (input, select, textarea, an editable element), a key pressed with Ctrl, Alt or Meta, or a key a listener of the host already handled with preventDefault(). shortcuts="off" turns them off, and the tab bar then shows no key hint; shortcuts="page" gives an embedded monitor the whole page.

While the run switcher or the theme menu is open, a click elsewhere in the page closes it (for the theme menu, a press of the pointer). The monitor listens on the document for that event only while one of them is open, and neither stops nor prevents it.

Two monitors on one page each keep their own view route, scroll positions, open sections and shortcuts. They share what is one per page:

  • the language: Lit Localize has one locale per page, so locale on one monitor, or its language switch, changes the other’s too;
  • the reader’s choices of language and theme, remembered in localStorage (contraagent:locale, contraagent:theme), as the monitor’s switches do;
  • the themes’ rules, added once to the document’s adopted style sheets; they match only [data-ca-theme], so the host’s own elements are not restyled;
  • Lit Localize’s lit-localize-status event on window, which every element listens for to change its language.

e2e/embed.spec.ts (in the repository’s web/) checks this on a host page with its own title, text field and single-key shortcuts, and two monitors.

base-url names the monitor server, such as http://127.0.0.1:8787; unset, it is the page’s own origin. A path alone, such as /monitor, names the monitor under that path of the page’s own origin: the page that contraagent monitor --base-path /monitor serves behind a reverse proxy sets it so. Every attribute takes effect whenever it is set, as an attribute or a property, before or after the element connects or the package loads:

Attribute A change after connecting
base-url closes the old server’s feeds and reads the new server: the run named by run, else its newest run
history memory keeps the view route shown; url takes the address bar’s
run opens that run
locale, theme, shortcuts show at once

A request to the API that gets no JSON answer is shown above the view, with its URL and its cause, and written to the console with console.error; the monitor then never shows “No runs yet”. The causes (ApiError.failure in @contraagent/api):

Cause Typical reason
the response is HTML, not JSON base-url names a server without the API, such as a development server that answers every path with the page (index.html)
the response is not JSON the server at base-url answers with an empty body or with text
the response status is 404 base-url names a server, or a path on it, without the API
network error, no response the monitor server is not running, or it does not allow the page’s origin (--cors-origin, below)

The run list (/api/runs) is asked for every 5 seconds, and the title and the custom views (/api/info) with it until they arrive, so a server that starts later is read when it answers, and the message goes.

export names an export’s data/ directory (contraagent monitor export) in place of a server: <ca-monitor export="data/">, relative to the page or absolute; set, it outranks base-url. The monitor then makes no request to an API and opens no feed: it reads the export’s files, and its header shows when the export was written in place of the connection. An export holds no transcripts and no processes.

docs/examples/embed.html
<!doctype html>
<!--
The whole monitor inside a page of your own, with no build step.
The monitor serves its elements as an ES module (/app.js). Let this page's
origin use it, then open the page:
contraagent monitor --runs-dir runs --cors-origin http://localhost:8000
python -m http.server 8000 --directory docs/examples
# http://localhost:8000/embed.html
-->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>contraagent, embedded</title>
<script type="module" src="http://127.0.0.1:8787/app.js"></script>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.pane { height: 70vh; border: 1px solid #93a1a1; border-radius: 8px; overflow: hidden; margin: 16px; }
</style>
</head>
<body>
<h1 style="margin: 16px">Our release pipeline</h1>
<!-- `memory`: the monitor keeps its view route to itself and leaves this page's URL alone.
The class sets a theme of your own on the monitor itself, which carries `data-ca-theme`. -->
<div class="pane">
<ca-monitor class="solarized-light" base-url="http://127.0.0.1:8787" history="memory"></ca-monitor>
</div>
</body>
</html>

A page on another origin can only import /app.js and read the API if the monitor names that origin: contraagent monitor --cors-origin http://localhost:8000.

A Vite application: the page holds <ca-monitor> in a box, and src/main.ts imports the package and sets the monitor’s base-url.

docs/examples/vite/index.html
<!doctype html>
<!-- The application's page (index.html), with the monitor in #monitor; main.ts names its server. -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Release dashboard</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
header { display: flex; align-items: center; gap: 16px; padding: 12px 16px; }
header h1 { flex: 1; margin: 0; font-size: 20px; }
/* The monitor fills the box it is in, and scrolls inside it: the box has a height. */
#monitor { height: calc(100vh - 80px); margin: 0 16px; border: 1px solid #8884; border-radius: 8px; overflow: hidden; }
</style>
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<header>
<h1>Release dashboard</h1>
<label>Language
<select id="language">
<option value="en">English</option>
<option value="ja">日本語</option>
</select>
</label>
</header>
<!-- `memory`: the view route stays in the element; the page's URL, title and <html> stay the application's. -->
<div id="monitor"><ca-monitor history="memory" locale="en"></ca-monitor></div>
</body>
</html>
docs/examples/vite/main.ts
// The monitor inside a Vite application, as src/main.ts. Its dependencies:
// @contraagent/ui, and its peers lit and @lit/localize.
//
// The monitor runs on another origin and lets this page read its API:
//
// contraagent monitor --runs-dir runs --cors-origin http://localhost:5173
// pnpm vite # http://localhost:5173
//
// The import registers <ca-monitor> (and every element it renders). Its
// properties take effect whenever they are set, before or after the import.
import '@contraagent/ui/monitor';
import type { CaMonitor } from '@contraagent/ui/monitor';
/** Where the monitor serves its API: another origin, named with `--cors-origin`. */
const MONITOR = 'http://127.0.0.1:8787';
const monitor = document.querySelector('ca-monitor')!;
monitor.baseUrl = MONITOR; // the `base-url` attribute
// `locale` changes the monitor's language in place.
const language = document.querySelector<HTMLSelectElement>('#language')!;
language.value = monitor.locale;
language.addEventListener('change', () => (monitor.locale = language.value as CaMonitor['locale']));

A page served by Vite answers an unknown path such as /api/runs with its own index.html: a monitor there without base-url shows “No monitor API at http://localhost:5173/api/info: the response is HTML, not JSON” (Connection, above).

Import the package once; the elements register themselves. When it connects, <ca-monitor> sets up the languages (configureLocales()) and adds the themes’ rules to its document (adoptThemes(this.ownerDocument)); a page that shows other elements without a monitor calls them itself (adoptThemes is in @contraagent/elements/themes). Give <ca-monitor> a box with a height: it fills it and scrolls inside it. It lays itself out by the box’s width, not the window’s: in a narrow box the header wraps and the tab list scrolls sideways. In React 19, Vue, Svelte, Astro or plain HTML, <ca-monitor> is an ordinary element. Properties without an attribute take objects and arrays: set them from script (el.rows = [...]), or with a property binding in your framework (.rows=${...} in Lit, rows={...} in React 19).