Batty is a web UI for Pi Coding Agent. It keeps Pi's workspace, session, model, skill, and AGENTS.md behavior, but gives you a fast browser app for chatting, resuming sessions, and managing cron jobs.
- Responsive chat UI for desktop and mobile
- Live streaming over SSE for assistant output and tool activity
- Workspace picker with filtering and one-click workspace creation
- Session list per workspace, including resume and infinite scroll for older messages
- Model and thinking-level switching from the chat header
- Attachment support with drag and drop, file picker, and image rendering
- Local draft saving while you type, including offline/reconnecting states
- Queue follow-up prompts while a run is streaming, or send steer prompts mid-run
- Rich tool rendering, including inline diffs for edits and readable bash output
- Built-in cron jobs for scheduled agent turns
- Built-in
subagenttool for synchronous workspace-scoped delegation - Built-in
web-searchtool powered by Brave Search - Passkey auth with one-time setup codes for enrolling devices
- Web Push notifications when background runs finish
- PWA install support with offline-friendly cached session snapshots
- Auto-reconnect after server restarts and auto-refresh when a new client build is deployed
Batty runs a Fastify server and a Vue client, while Pi still owns the actual agent behavior:
- models come from Pi's model registry
- global agent resources come from
<batty-root>/.batty/ - workspace agent resources come from
<workspace>/.batty/ - instructions come from
<batty-root>/.batty/AGENTS.mdplus projectAGENTS.md - session history is stored in
<workspace>/.batty/sessions/
Batty adds a browser-native layer on top:
- workspace and session browsing
- streaming transcript UI
- local caching and drafts
- push notifications
- cron
- subagents
- web search
- passkey login
Create a Batty root directory. Batty stores its own state in <batty-root>/.batty/ and lists workspaces from the configured workspacesRoot.
Example options file:
<batty-root>/.batty/options.json
{
"authSecret": "generated-on-first-run",
"workspacesRoot": "/path/to/workspaces",
"webPushSubject": "https://your-batty-host",
"braveSearchKey": "optional-brave-search-api-key"
}Required fields:
workspacesRootwebPushSubject
authSecret is generated automatically if missing.
pnpm install
pnpm dev -- /path/to/batty-root- client:
http://127.0.0.1:5173 - server:
http://127.0.0.1:3147
pnpm build
pnpm start -- /path/to/batty-rootOn first boot with no registered passkeys, Batty prints a one-time setup code in the server terminal.
The production build output lives in dist/, but the deployment flow packages a self-contained install directory that includes:
dist/clientdist/servernode_modulesREADME.mdpackage.jsonpnpm-lock.yaml
Batty uses passkeys for passwordless login.
First device setup:
- Start Batty.
- Copy the setup code printed in the server terminal.
- Open Batty in the browser.
- Enter the setup code and register a passkey.
After that, sign-in uses the passkey directly.
To enroll another device later, generate a fresh setup code with the Batty CLI:
batty --root /path/to/batty-root auth codeBatty includes a small CLI for auth and cron jobs.
After deployment, ./scripts/deploy.sh installs it as:
batty --root /path/to/batty-root <command>For local repo usage before deployment, the equivalent command is:
pnpm batty -- --root /path/to/batty-root <command>batty auth code
batty cron list [--workspace ID] [--json]
batty cron add --workspace ID --prompt TEXT --thinking LEVEL (--in DUR | --at ISO | --every DUR | --cron EXPR) [--model ID] [--tz IANA] [--session new|daily-inline|daily-subagent] [--daily-context include|omit]
batty cron edit <jobId> [--workspace ID] [--prompt TEXT] [--model ID] [--thinking LEVEL] [--in DUR | --at ISO | --every DUR | --cron EXPR] [--tz IANA] [--session new|daily-inline|daily-subagent] [--daily-context include|omit]
batty cron rm <jobId>
batty --root /path/to/batty-root auth code
batty --root /path/to/batty-root cron list --workspace batty
batty --root /path/to/batty-root cron add --workspace batty --prompt "Check CI and summarize failures" --thinking medium --every 1h --session daily-subagent --daily-context include
batty --root /path/to/batty-root cron add --workspace batty --prompt "Morning summary" --thinking low --cron "0 8 * * 1-5" --tz Europe/Copenhagen --session daily-inline
batty --root /path/to/batty-root cron edit <jobId> --prompt "Updated prompt"
batty --root /path/to/batty-root cron rm <jobId>The same cron functionality is also available to the agent through Batty's built-in cron tool.
Batty can run future agent turns in any workspace.
Schedules supported by both the CLI and the built-in tool:
- one-shot at a specific time
- one-shot after a relative duration like
10mor2h - repeating interval schedules like
1hor1d - cron expressions with an optional timezone
- session mode
new,daily-inline, ordaily-subagent - daily-subagent context mode
includeoromit
If --tz / timezone is omitted for a cron expression, Batty uses the server's local timezone.
Daily session reuse keeps one cron conversation per workspace day. daily-inline runs directly in that daily session like a regular session turn. daily-subagent runs are stored as subagent tool calls in that session. Daily-subagent runs start fresh from the workspace system prompts by default. --daily-context include / session.includePreviousContext=true reuses earlier daily-session context for daily-subagent jobs. The day rollover defaults to 04:00 local time and can be changed in options.json.
Cron job state includes:
- next scheduled run
- last run time
- last status
- last error
- last session id
The chat header also includes a cron popover for browsing, editing prompt/model/thinking level/session mode, and deleting existing jobs.
pnpm check
pnpm test
pnpm build
pnpm start -- /path/to/batty-root
batty --root /path/to/batty-root auth codeBATTY_HOST— bind host, defaults to127.0.0.1BATTY_PORT— bind port, defaults to3147
Batty reads persisted server options from:
<batty-root>/.batty/options.json
Fields:
authSecret— cookie signing secret, generated if missingworkspacesRoot— required root containing workspace folderswebPushSubject— required VAPID subject; use a realhttps:origin or validmailto:URIcronDailySessionStartTime— local rollover time for daily cron session reuse, formatted asHH:MM; defaults to04:00braveSearchKey— optional Brave Search API key used by Batty's built-inweb-searchtool
If present, Batty also loads:
<batty-root>/.batty/environment.json
This file can provide environment variables before the server starts.
Batty stores local state in <batty-root>/.batty/, including:
options.jsonenvironment.jsonpasskeys.jsonsetup-code.jsonuploads/cron/jobs.jsonweb-push/vapid-keys.jsonweb-push/subscriptions.json
- Workspaces are direct child directories under
workspacesRoot. - New workspaces created in the UI are created directly under that root.
- Uploaded files are staged on disk before being handed to Pi.
- Non-image attachments are injected into the prompt as
<file>blocks. - Image attachments are sent as image inputs and also referenced as file placeholders.
- Session state is kept in Pi's session files, with Batty caching recent snapshots locally in the browser.
When working inside the Batty repo, use:
./scripts/reload-self.shThat flow is designed to let the current agent turn finish cleanly before the service reload happens.
Repo includes:
deploy/batty.service— systemd unitdeploy/batty.nginx.conf— nginx examplescripts/deploy.sh— build, package, install, and reload helper
Deploy on Linux with the project script:
sudo ./scripts/deploy.shThe Linux deploy script installs Batty to /opt/batty:
- versioned releases under
/opt/batty/releases/<git-sha> - current install symlink at
/opt/batty/current - systemd
WorkingDirectory=/opt/batty/current - service entrypoint
/opt/batty/current/dist/server/main.mjs - CLI entrypoint
/opt/batty/current/dist/server/cli.mjs
This repo also includes a Windows deployment flow that runs Batty behind IIS with the ASP.NET Core Module acting as the reverse proxy to the Node process.
Requirements:
- IIS site with HTTPS already configured
- ASP.NET Core Module V2 installed on the machine
- Node.js available at
node.exe - an elevated PowerShell session for the IIS application setup step
Example deployment for https://t14-dt-pc1028.cbrain.net/batty:
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-windows.ps1 `
-InstallRoot 'D:\Batty\app' `
-BattyRoot 'D:\Batty\root' `
-WorkspacesRoot 'D:\projects' `
-PublicOrigin 'https://t14-dt-pc1028.cbrain.net' `
-BaseUrl '/batty' `
-SiteName 'Default Web Site' `
-AppPath 'batty'That flow:
- installs dependencies
- runs checks, tests, and a production build
- writes
D:\Batty\root\.batty\options.json - packages a versioned release under
D:\Batty\app\releases\<git-sha> - updates
D:\Batty\app\currentas a junction - writes
web.configfor IIS out-of-process hosting - configures the IIS application automatically when run elevated
The generated IIS app serves Batty from the configured subpath and forwards requests to the Node server through a launcher script that sets:
BATTY_HOST=127.0.0.1BATTY_PORT=%ASPNETCORE_PORT%BATTY_SELF_PATH=<install-root>\current
If the main deploy script is run without elevation, finish the IIS step from an elevated PowerShell session:
powershell -ExecutionPolicy Bypass -File .\scripts\configure-iis-app.ps1 `
-SiteName 'Default Web Site' `
-AppPath 'batty' `
-PhysicalPath 'D:\Batty\app\current'