This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Federico Jaramillo Martinez 5759201a39 docs: document Pi extension dialogs and extensionDialogsTimeoutMs
Add the close-out documentation for extension dialog support:

- docs/config.md (+ synchronized config.html): extensionDialogsTimeoutMs
  in the config matrix, reload/restart guidance, the global config
  example, and a new Extension dialogs key-detail section covering the
  unattended-dialog safety valve (default 5 min, 0 = forever, effective
  deadline is the sooner of the extension's own timeout and this knob).
- docs/plugins.md (+ synchronized plugins.html): new Pi extension
  dialogs in PI WEB behavior note for extension authors — confirm/select/
  input render inline in the transcript and resolve with the real answer,
  answers use a dedicated daemon channel (never the prompt queue, so
  tool_call hooks park safely), session_start dialogs are reachable
  during create and open, reload rehydration, first-answer-wins across
  tabs, abort/runtime-replacement settlement, and the reload-mid-startup
  browser-local caveat.
- Add the extension-dialogs changeset (patch) for the release notes.
2026-07-29 07:52:21 +02:00

544 lines
30 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PI WEB plugins — extend the Pi web UI</title>
<meta
name="description"
content="Use built-in PI WEB plugins and develop trusted local UI plugins for the Pi Coding Agent web UI."
/>
<link rel="canonical" href="https://pi-web.dev/plugins" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="PI WEB" />
<meta property="og:title" content="PI WEB plugins — extend the Pi web UI" />
<meta
property="og:description"
content="Customize PI WEB with trusted browser-side plugins for actions, workspace panels, labels, terminals, and files."
/>
<meta property="og:url" content="https://pi-web.dev/plugins" />
<meta property="og:image" content="https://pi-web.dev/assets/pi-web-banner.png" />
<meta property="og:image:alt" content="PI WEB browser UI for persistent Pi Coding Agent sessions" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="PI WEB plugins — extend the Pi web UI" />
<meta
name="twitter:description"
content="Customize PI WEB with trusted browser-side plugins for actions, workspace panels, labels, terminals, and files."
/>
<meta name="twitter:image" content="https://pi-web.dev/assets/pi-web-banner.png" />
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
<script>
(() => {
const theme = window.localStorage.getItem("pi-web-theme");
if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme;
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header class="site-header">
<nav class="container nav" aria-label="Main navigation">
<a class="brand" href="./" aria-label="PI WEB home">PI WEB</a>
<div class="nav-links">
<div class="nav-pages">
<a href="remote-first">Remote-first</a>
<a href="machines">Fleet</a>
<a href="install">Install</a>
<a href="config">Config</a>
<a href="plugins" aria-current="page">Plugins</a>
<a href="faq">FAQ</a>
</div>
<div class="nav-actions">
<a class="github-link" href="https://github.com/jmfederico/pi-web" aria-label="PI WEB on GitHub">
<svg class="github-icon" viewBox="0 0 16 16" aria-hidden="true">
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.67 0 8.2c0 3.63 2.29 6.7 5.47 7.79.4.08.55-.18.55-.4 0-.2-.01-.85-.01-1.55-2.01.38-2.53-.5-2.69-.96-.09-.24-.48-.96-.82-1.16-.28-.16-.68-.56-.01-.57.63-.01 1.08.59 1.23.84.72 1.24 1.87.89 2.33.68.07-.53.28-.89.51-1.09-1.78-.21-3.64-.91-3.64-4.04 0-.89.31-1.62.82-2.2-.08-.2-.36-1.03.08-2.16 0 0 .67-.22 2.2.84A7.4 7.4 0 0 1 8 3.94c.68 0 1.36.09 2 .28 1.53-1.06 2.2-.84 2.2-.84.44 1.13.16 1.96.08 2.16.51.58.82 1.31.82 2.2 0 3.14-1.87 3.83-3.65 4.04.29.26.54.76.54 1.53 0 1.1-.01 1.99-.01 2.27 0 .22.15.49.55.4A8.12 8.12 0 0 0 16 8.2C16 3.67 12.42 0 8 0Z"
/>
</svg>
<span>GitHub</span>
</a>
<button class="theme-toggle" type="button" data-theme-toggle aria-label="Toggle light and dark theme">
<span data-theme-icon aria-hidden="true"></span>
<span data-theme-label>Theme</span>
</button>
</div>
</div>
</nav>
</header>
<main>
<section class="page-hero">
<div class="container">
<p class="eyebrow"><span class="pulse"></span> Plugin development</p>
<h1>Customize PI WEB with UI plugins.</h1>
<p>
Plugins are trusted browser-side ES modules. They can add actions, workspace panels, and compact workspace
labels to the PI WEB UI.
</p>
</div>
</section>
<section class="section compact">
<div class="container doc-layout">
<aside class="toc" aria-label="Plugin page contents">
<strong>On this page</strong>
<a href="#extend">What can be extended</a>
<a href="#packages-vs-plugins">Pi packages, extensions, and plugins</a>
<a href="#pi-extension-dialogs">Pi extension dialogs</a>
<a href="#ask-ai">What to ask AI to build</a>
<a href="#example">Canonical example</a>
<a href="#built-in-plugins">Built-in plugins</a>
<a href="#manage-plugins">Manage PI WEB plugins</a>
<a href="#remote-machine-plugins">Remote machine plugins</a>
<a href="#production">Production usage</a>
<a href="#agent-docs">AI-friendly docs</a>
<a href="#develop">Develop and debug</a>
<a href="#trust">Trust model</a>
</aside>
<div class="doc-content">
<section id="extend">
<h2>What can be extended</h2>
<p>Plugins can add small, focused UI extensions to PI WEB:</p>
<ul>
<li><strong>Action palette commands</strong> for custom workflows.</li>
<li><strong>Workspace tools and panels</strong> next to Files, Git, and Terminal.</li>
<li><strong>Workspace labels</strong> in the workspace list, header, and status bar.</li>
<li><strong>Static assets</strong> served from the plugin folder.</li>
<li><strong>Browser-side integrations</strong> using documented PI WEB plugin context helpers.</li>
</ul>
<p>
Plugins cannot extend the session daemon or add server-side hooks. They run in the browser UI only.
Use documented context helpers for the stable API surface. Daring plugins can reach into PI WEB
internals, but those private surfaces are experimental: they may graduate into stable helpers, change
shape, or disappear.
</p>
<div class="callout">
The plugin API is intentionally limited and actively developed. Feedback is appreciated: if an extension
point is missing, unclear, or awkward, please
<a href="https://github.com/jmfederico/pi-web/issues">open a GitHub issue</a>.
</div>
</section>
<section id="packages-vs-plugins">
<h2>Pi packages, Pi extensions, and PI WEB plugins</h2>
<p>
<strong>Pi packages</strong> are distribution bundles managed by Pi (<code>pi install</code>,
<code>pi remove</code>, <code>pi update</code>). A Pi package can provide Pi extensions, skills, prompt
templates, themes, context/system prompt files, and/or PI WEB browser plugins. Many Pi packages do not
include a PI WEB plugin.
</p>
<p>
<strong>Pi extensions</strong> are runtime modules loaded by the session daemon. They can register Pi
tools, hooks, commands, and model providers. They are not PI WEB plugins.
</p>
<p>
<strong>PI WEB plugins</strong> are browser-side UI modules discovered from bundled, local, dev, and
installed Pi-package sources. They cannot register model providers or server-side hooks. Enabling or
disabling a PI WEB plugin is a PI WEB config task; installing, removing, or updating a Pi package is a
separate Pi package-manager task.
</p>
<p>
Use <strong>Settings → Pi packages</strong> to view configured Pi packages or install/remove/update a
package. Enter only the package source, such as <code>npm:@scope/package</code>, a git/URL source, or a
local path. PI WEB uses Pi's default package location, equivalent to <code>pi install &lt;source&gt;</code>,
and does not ask for an install location.
</p>
<p>
When machine federation is enabled, <strong>Settings → Pi packages</strong> targets the currently selected
machine. The panel labels whether changes will run on the local/gateway machine or on a selected remote
PI WEB machine. If an older or unavailable remote PI WEB server does not expose package-management routes,
PI WEB reports the package management operation as unsupported or unavailable instead of silently falling
back to the gateway.
</p>
<p>
Use <strong>Settings → PI WEB plugins</strong> to enable or disable discovered PI WEB browser plugins
before the browser imports them. In a federated setup, this plugin enablement surface targets the
currently selected machine and labels where changes are saved. If an older or unavailable remote PI WEB
server does not advertise selected-machine settings support, PI WEB reports the plugin settings as
unsupported or unavailable instead of silently falling back to the gateway.
</p>
<p>
After installing, removing, or updating a Pi package, type <code>/reload</code> in each idle PI WEB
session on the target machine to refresh ordinary Pi resources such as extensions, skills, prompt
templates, themes, and context/system prompt files. Reload the browser page separately for newly
discovered or changed PI WEB browser plugins. A provider-registering Pi extension follows a separate
daemon-start policy; see
<a href="config#pi-extension-provider-baseline">Pi extension provider baseline</a>.
</p>
</section>
<section id="pi-extension-dialogs">
<h2>Pi extension dialogs in PI WEB</h2>
<p>
Pi extensions running under PI WEB's session daemon can ask the user questions with
<code>ctx.ui.confirm()</code>, <code>ctx.ui.select()</code>, and <code>ctx.ui.input()</code>. For these
three methods <code>ctx.hasUI</code> is true in fact: the call renders a dialog card inline in the
session transcript — including from <code>session_start</code> hooks while the session is still starting
and from in-flight <code>tool_call</code> hooks — and resolves with the user's actual answer.
</p>
<p>
Answers travel over a dedicated session-daemon channel, never the prompt queue, so a parked
<code>tool_call</code> hook cannot deadlock the run. Open dialogs survive browser reloads, the first
answer wins across tabs, and unanswered dialogs settle safely: aborting the run or replacing the runtime
resolves them immediately with the kind's cancel value (<code>false</code> for confirm,
<code>undefined</code> for select and input), and the effective deadline — the sooner of the extension's
own <code>timeout</code> and the daemon's <code>extensionDialogsTimeoutMs</code> safety valve (default 5
minutes, <code>0</code> waits forever) — does the same when no one answers. Other
<code>ExtensionUIContext</code> surfaces (widgets, status, editor, <code>custom</code>) remain no-ops
despite <code>hasUI === true</code>.
</p>
<p>
For the full behavior notes and author guidance, read <a href="plugins.md">plugins.md</a>; for the
timeout key, see <a href="config#extension-dialogs">Extension dialogs</a> in the configuration reference.
</p>
</section>
<section id="ask-ai">
<h2>What to ask AI to build</h2>
<p>
You do not need to hand-code plugins. Give an AI agent a clear goal, the workspace/project facts it
should use, and the PI WEB plugin rules.
</p>
<ul>
<li>Show a workspace badge from a repo file, environment file, branch name, container, or dev URL.</li>
<li>Add a workspace panel that summarizes project-specific health, commands, links, or runbooks.</li>
<li>Add action-palette commands for common project workflows or prompts.</li>
<li>Add links to local services, preview deployments, dashboards, CI, logs, or docs.</li>
</ul>
<div class="code-card">
<div class="copy-row">
<strong>Prompt: create a plugin</strong>
<button class="copy-button" data-copy="#plugin-create-prompt">Copy</button>
</div>
<pre id="plugin-create-prompt"><code>Build a PI WEB plugin for this project.
Goal: &lt;describe the UI behavior&gt;.
Before coding, read the PI WEB plugin docs:
https://pi-web.dev/plugins
Full API reference:
https://pi-web.dev/plugins.md
Create it as a local plugin under ~/.pi-web/plugins/&lt;plugin-id&gt;.
Use the appropriate extension points from the docs.
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
Do not modify PI WEB itself.</code></pre>
</div>
<div class="code-card">
<div class="copy-row">
<strong>Prompt: improve a plugin</strong>
<button class="copy-button" data-copy="#plugin-improve-prompt">Copy</button>
</div>
<pre id="plugin-improve-prompt"><code>Improve the PI WEB plugin at &lt;path&gt;.
Before coding, read the PI WEB plugin docs:
https://pi-web.dev/plugins
Full API reference:
https://pi-web.dev/plugins.md
Keep the plugin compatible with the documented v1 API.
After editing, check the manifest endpoint and browser-console failure cases.</code></pre>
</div>
</section>
<section id="example">
<h2>Canonical example</h2>
<p>
PI WEB ships a real bundled <strong>Info</strong> plugin. It is intentionally small while still using all
core contribution types: one action, one workspace label, and one workspace panel.
</p>
<p>
Bundled PI WEB plugins are developed as TypeScript in the repository, while their package metadata
points at the built JavaScript ES modules that the browser loads. <code>npm run dev:web</code> watches and
rebuilds bundled plugin TS into <code>dist/pi-web-plugins/</code> during development, and <code>npm run build</code>
emits JS before release packaging.
</p>
<ul>
<li><code>pi-web-plugins/info/package.json</code> shows the required metadata shape.</li>
<li><code>pi-web-plugins/info/pi-web-plugin.ts</code> is the skeleton: metadata and contribution definitions.</li>
<li><code>pi-web-plugins/info/infoInternals.ts</code> holds the bundled panel and action implementation, which you can ignore or replace when copying the plugin.</li>
<li><code>dist/pi-web-plugins/info/pi-web-plugin.js</code> is the built browser module in a checkout.</li>
</ul>
<p>
Read it on GitHub:
<a href="https://github.com/jmfederico/pi-web/tree/main/pi-web-plugins/info">pi-web-plugins/info</a>.
If you copy it, choose a new plugin id so it does not conflict with the bundled <code>info</code> plugin.
Its panel also doubles as an always-available PI WEB status view: it renders the host-provided
<code>context.state.piWebStatus</code> without issuing its own requests, and its action copies a
plain-text diagnostics summary suitable for bug reports.
</p>
<p>
The bundled <code>updates</code> plugin demonstrates dynamic <code>visible</code> and <code>badge</code>
callbacks for tabs that only appear when the host has status messages or needs extra install visibility.
</p>
</section>
<section id="built-in-plugins">
<h2>Built-in plugins</h2>
<p>
PI WEB ships core, discoverable plugins in the main <code>@jmfederico/pi-web</code> npm package. No
separate <code>pi install</code> step is required: update PI WEB, reload the browser tab, and the bundled
plugins appear in <code>/pi-web-plugins/manifest.json</code>.
</p>
<p>
Built-in plugins can be managed from <strong>Settings → PI WEB plugins</strong> or with the top-level
<code>plugins</code> config key.
</p>
<h3>Updates</h3>
<p>
<strong>Updates</strong> adds a conditional <strong>Updates</strong> workspace tab with PI WEB update,
restart, and installed-service guidance, plus a <strong>Check for PI WEB Updates</strong> action. It is
built into PI WEB, enabled by default, and uses the selected machine's plugin copy when machine
federation is active.
</p>
<ul>
<li>Plugin id: <code>updates</code></li>
<li>Selected-machine status refreshes every 15 minutes while a browser tab is connected.</li>
<li>Automatic npm release lookups are cached for six hours; the action bypasses the caches and checks immediately.</li>
</ul>
<div class="code-card">
<div class="copy-row">
<strong>Disable Updates</strong>
<button class="copy-button" data-copy="#updates-disable">Copy</button>
</div>
<pre id="updates-disable"><code>{
"plugins": {
"updates": { "enabled": false }
}
}</code></pre>
</div>
<h3>Workspace Tasks</h3>
<p>
<strong>Workspace Tasks</strong> adds a <strong>Tasks</strong> workspace tab for running configured shell
commands in dedicated PI WEB terminals. It is built into PI WEB and enabled by default.
</p>
<ul>
<li>Plugin id: <code>workspace-tasks</code></li>
<li>Config file: <code>.pi-web/tasks.json</code></li>
</ul>
<div class="code-card">
<div class="copy-row">
<strong>Disable Workspace Tasks</strong>
<button class="copy-button" data-copy="#workspace-tasks-disable">Copy</button>
</div>
<pre id="workspace-tasks-disable"><code>{
"plugins": {
"workspace-tasks": { "enabled": false }
}
}</code></pre>
</div>
<div class="code-card">
<div class="copy-row">
<strong>Example .pi-web/tasks.json</strong>
<button class="copy-button" data-copy="#workspace-tasks-config">Copy</button>
</div>
<pre id="workspace-tasks-config"><code>{
"version": 1,
"tasks": [
{
"id": "app.start",
"title": "Start app",
"group": "Development",
"description": "Start the local development server.",
"command": "npm run dev"
},
{
"id": "db.reset",
"title": "Reset DB",
"group": "Database",
"command": "go -C klingit-go run ./cli db reset",
"confirm": true
}
]
}</code></pre>
</div>
<p>
Open a workspace, choose the <strong>Tasks</strong> tab, and click <strong>Run</strong> next to a task.
Commands run in the workspace root because PI WEB creates the terminal for that workspace.
</p>
<p>
Review task configs before running them, especially in shared projects. Workspace Tasks runs trusted
shell commands from your repositories.
</p>
</section>
<section id="manage-plugins">
<h2>Manage PI WEB plugins</h2>
<p>
Open <strong>Settings → PI WEB plugins</strong> to review discovered bundled, local, dev, and
Pi-package-supplied PI WEB plugins for the selected PI WEB machine. When the local machine is selected,
this is the gateway plugin list; when a remote machine is selected, the list comes from that remote PI WEB
server and includes
disabled discovered plugins it exposes. PI WEB can disable any discovered selected-machine plugin before
the browser imports it. Core app contributions such as the command palette, base workspace tools, and
themes are not managed through this plugin list.
</p>
<p>
This surface is only for PI WEB plugin enablement. To install, remove, or update Pi packages that may
provide PI WEB plugins or other Pi resources, use <strong>Settings → Pi packages</strong>. In a federated
setup, both the Pi packages panel and the PI WEB plugins panel target the selected machine; plugin enablement
still writes the PI WEB <code>plugins</code> config key rather than changing Pi package-manager settings.
</p>
<div class="code-card">
<div class="copy-row">
<strong>Plugin config shape</strong>
<button class="copy-button" data-copy="#plugin-config-shape">Copy</button>
</div>
<pre id="plugin-config-shape"><code>{
"plugins": {
"workspace-tasks": {
"enabled": true,
"settings": {}
},
"info": {
"enabled": false
}
}
}</code></pre>
</div>
<p>
Plugins are enabled by default. Set <code>enabled</code> to <code>false</code> to remove a plugin from
<code>/pi-web-plugins/manifest.json</code> so it is not imported or activated on the next page load.
The optional <code>settings</code> object is reserved for plugin-specific settings.
</p>
<p>
After changing plugin enablement, reload the PI WEB browser tab. Already-loaded plugin JavaScript is not
unloaded from the current page.
</p>
</section>
<section id="remote-machine-plugins">
<h2>Remote machine plugins</h2>
<p>
With <a href="machines">machine federation</a>, PI WEB also loads discovered plugins from the selected
remote machine. Remote plugins are trusted browser-side code like local plugins, but their actions,
workspace panels, and workspace labels only appear while that machine is selected.
</p>
<ul>
<li>File and terminal helpers run against the selected remote machine.</li>
<li>Remote plugin code is loaded best-effort through the current gateway and cached for the page lifetime.</li>
<li>If the gateway and remote machine both have an enabled plugin with the same original id, <code>machineSpecific</code> metadata decides whether the gateway copy is reused or only the selected-machine copy can appear.</li>
<li>Remote theme contributions are ignored for now because themes are app-wide.</li>
<li>Mixed PI WEB versions across federated machines are best-effort and not guaranteed compatible.</li>
</ul>
<p>
Remote plugin enablement is controlled by the remote machine's PI WEB plugin config. To edit or disable
one, select that machine and use <strong>Settings → PI WEB plugins</strong> when the remote server exposes
selected-machine settings, or open that machine directly/update its config file.
</p>
<p>
Plugin package metadata can set <code>machineSpecific: true</code>. Use it for plugins like Updates whose
UI should come from the selected PI WEB instance; on remote machines, the gateway copy is hidden unless
the remote machine exposes its own copy.
</p>
<p>
Current PI WEB manifests publish leading application-root module references. The browser keeps them
inside the current application base, so local and federated plugins follow root or nested reverse-proxy
deployments without a prefix-specific build while remaining compatible with existing gateways.
Federated gateways also accept manifest-relative references such as
<code>./&lt;plugin-id&gt;/plugin.js</code> and legacy plugin-root-relative references such as
<code>nested/plugin.js</code> from remote machines.
</p>
<p>
For portable plugin assets, prefer URLs relative to the plugin module, such as
<code>new URL("./asset.json", import.meta.url)</code>. If a remote plugin constructs absolute asset URLs,
it should use the <code>pluginId</code> from <code>activate()</code>; hard-coded
<code>/pi-web-plugins/&lt;original-id&gt;/...</code> URLs may point at the gateway instead of the remote machine.
</p>
</section>
<section id="production">
<h2>Production usage</h2>
<p>
Local plugins work with the production native-service install. Put each plugin under
<code>~/.pi-web/plugins/&lt;plugin-id&gt;/</code>, or symlink it there while developing. No PI WEB rebuild or
session-daemon restart is required.
</p>
<div class="callout warning">
If <code>PI_WEB_DATA_DIR</code> is set, use <code>$PI_WEB_DATA_DIR/plugins</code> instead of
<code>~/.pi-web/plugins</code>.
</div>
<div class="code-card">
<div class="copy-row">
<strong>Install a local plugin folder</strong>
<button class="copy-button" data-copy="#plugin-symlink">Copy</button>
</div>
<pre id="plugin-symlink"><code><span class="prompt">$</span> mkdir -p ~/.pi-web/plugins
<span class="prompt">$</span> ln -s /path/to/plugin-folder ~/.pi-web/plugins/plugin-id</code></pre>
</div>
</section>
<section id="agent-docs">
<h2>AI-friendly docs</h2>
<p>
The full plugin reference is available as Markdown for AI agents to read, fetch, and follow:
<a href="plugins.md">plugins.md</a>.
</p>
<p>
If you want to understand the API yourself, ask an agent to read that file and explain the relevant
public extension points for the plugin you want. Private PI WEB internals exist for the daring; treat
them as experimental surfaces that may graduate into stable helpers, change shape, or disappear.
</p>
</section>
<section id="develop">
<h2>Develop and debug</h2>
<ul>
<li>Keep the source folder outside <code>~/.pi-web/plugins</code> and symlink it in.</li>
<li>After edits, reload or hard-reload the browser tab.</li>
<li>Check discovery with the manifest endpoint.</li>
<li>Check the browser console if a plugin fails to import or activate.</li>
</ul>
<div class="code-card">
<div class="copy-row">
<strong>Check discovered plugins</strong>
<button class="copy-button" data-copy="#plugin-manifest">Copy</button>
</div>
<pre id="plugin-manifest"><code><span class="prompt">$</span> curl http://127.0.0.1:8504/pi-web-plugins/manifest.json</code></pre>
</div>
<p>
For all contribution types and package metadata, read the
<a href="plugins.md">Plugin API reference</a>.
</p>
</section>
<section id="trust">
<h2>Trust model</h2>
<p>
Plugins are not sandboxed. They run as JavaScript in the browser, can use browser APIs, call documented
PI WEB helpers, and render arbitrary UI. If a plugin reaches beyond those helpers into private PI WEB
internals, treat that dependency as experimental: it may become stable, change shape, or disappear.
Install only plugins you trust.
</p>
<p>
Plugins do not run inside the session daemon and do not provide server-side hooks.
</p>
</section>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-inner">
<span>PI WEB plugin docs</span>
<div class="footer-links">
<a href="./">Home</a>
<a href="remote-first">Remote-first</a>
<a href="machines">Fleet</a>
<a href="install">Install</a>
<a href="config">Config</a>
<a href="faq">FAQ</a>
<a href="https://github.com/jmfederico/pi-web">GitHub</a>
</div>
</div>
</footer>
<script src="site.js"></script>
</body>
</html>