Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ body:
id: version
attributes:
label: Version
description: Settings → About, or the GitHub Release tag (for example 0.19.0).
placeholder: 0.19.0
description: Settings → About, or the GitHub Release tag (for example 0.20.0).
placeholder: 0.20.0
validations:
required: true
- type: dropdown
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
run: |
mkdir -p images/agent/vendor
node scripts/prefetch-github-release-asset.mjs --owner coder --repo code-server --tag v4.96.4 --file code-server-4.96.4-linux-amd64.tar.gz --out images/agent/vendor/code-server.tar.gz
- run: docker build -t locus/agent:0.1 -t locus/agent:0.19.0 images/agent
- run: docker build -t locus/agent:0.1 -t locus/agent:0.20.0 images/agent

android:
runs-on: ubuntu-latest
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## Unreleased

## 0.20.0 — 2026-11-11

Eight new Terminal themes, a UI/UX polish pass on Swarm, the composer, and empty states, and launchers that check Docker is actually running before they start. Settings → About is **0.20.0**.

**Terminal themes.** Settings → Appearance adds a **Terminal** filter and group. Console is joined by eight terminal-window themes that share its titlebar, scanlines, and monospace type: Green Screen, White Phosphor, Cathode, Darkroom, Cobalt, Uplink, Mainframe, and Teletype (light).

**Docker readiness.** The Agents, Design, and Games launchers now check that the Docker daemon is answering before they start. When it isn't, they start on Host and say "Docker isn't running" instead of launching and failing, and the Docker footer reports when the daemon is down.

**UI/UX pass.** The Swarm sidebar no longer runs under the chat, narrow windows get the same rail and slide-over list as Developer, and teammate rows show full names. In the composer, the model picker shrinks instead of covering Stop, Queue, and Send. Empty states, Settings spacing, and the setup screen are more consistent, and the phone top bar is tidier.

**Fixes.** Repeated Docker errors and a stale "Waiting for the guest…" no longer linger in the thread, and "Loading local model…" no longer shows while a reply is streaming. The scroll tick rail no longer covers message text, collapsed-rail peek titles show in full, and Settings → Integrations lists each forge once.

## 0.19.0 — 2026-11-11

Inbox and a resizable sidebar with pinnable rail sections, the Console theme, a UI/UX polish pass across every section, and more reliable Jobs, worker uploads, and desktop updates. Settings → About is **0.19.0**.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ First agent without Docker: Settings → Providers (Ollama needs no key) then Ne
After merge to `main`:

```bash
git tag v0.19.0
git push origin v0.19.0
git tag v0.20.0
git push origin v0.20.0
```

GitHub Releases are the distribution channel (`v*` tags). Pushing a `v*` tag publishes Linux, macOS, and Windows installers, then **Prune GitHub Releases** deletes older published releases so only the last three remain (git tags are kept). See [docs/public-release.md](docs/public-release.md) for listing settings this tree cannot change (visibility, vulnerability reporting, unsigned macOS/Windows). Keep the repository description as the README tagline and topics `ai-agents`, `qemu`, `docker`, `computer-use`, `electron`, `local-first`.
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Locus is a desktop control plane for AI coding agents that run on hardware you own — a Docker container, a QEMU virtual machine, or a folder on disk. Chat on one side, the live guest on the other. Code, keys, sessions, and disks stay under `~/.locus`.

This is **v0.19.0** (MIT). The API binds to loopback. Linux, macOS, and Windows. Bring any LLM provider you have a key for.
This is **v0.20.0** (MIT). The API binds to loopback. Linux, macOS, and Windows. Bring any LLM provider you have a key for.

## Install

Expand Down Expand Up @@ -44,6 +44,8 @@ The runtime is chosen per agent and stays fixed. New agents default to Docker wh

The icon rail starts with **Inbox** (everything waiting on you), then **Agents, Design, IDE, Codebase, Jobs** (Automations), then **More** for Games, Assets, Board, Projects, Brain, Slack, Grafana, and Docker. Pin a More surface onto the rail from that menu, then drag it or use Move up / down. Hover a collapsed rail icon to peek that list. Alt+1–9 jumps Inbox then pinned sections. Drag the sidebar edge to resize. Session lists share All / Needs you / Running / Unread chips and can group by day, repo, or status; Docker uses All / Running chips. Inbox rows are tagged and can Approve / Deny waiting tools. Settings is a full page. Developer / Swarm lives in Appearance (and the command palette) — Developer is this control plane; Swarm hides the rail and is teammate-first (Teammates, Teams, Routines).

![Inbox](docs/screenshots/inbox.png)

### Setup

First-run wizard: provider key, then Developer or Swarm. Docker and QEMU sit under **Set up later**. **Skip for now** leaves a one-line Finish setup hint until you Continue or Dismiss it. The command palette **Finish setup** reopens the wizard.
Expand All @@ -54,7 +56,7 @@ First-run wizard: provider key, then Developer or Swarm. Docker and QEMU sit und

New agent is a full-page composer: Docker / VM / Host, a private folder or a folder you open, a repo (defaults to **No repo**; several linked remotes clone side by side under `/workspace/<name>`, then the repo is locked), then **More options** for image, jail, spend, named environment, and recipes. Recipes are **PR reviewer**, **Installer QA** (Windows VM), **Repo fixer**, **Setup environment** (install, start, verify, write `.locus/environment.json`), **Web QA** (headed Chromium), and **Ops loop** (Grafana, Jira, Confluence, Linear, Slack, incident.io, AWS IAM, Portainer, Kubernetes, Sentry). Sidebar overflow **Duplicate** forks the workspace into a new empty chat. Missing Docker or QEMU is a blocking callout — **Use Host instead**, or Finish setup.

Chat sits on the left. The guest sits on the right (Docker opens on **Files**; Host uses the same editor on `/workspace`). `.html` has a Browser preview, `.md` / `.svg` open in Preview, and `.pdf` renders inline. A listening HTTP port stacks **Preview** under Files. Host agents also get a **Browser** tab on the isolated Chrome profile. A coding or review agent can `canvas_write` a self-contained HTML report on a **Canvas** tab, separate from the repo and from Design's canvas.
Chat sits on the left. **Files** in the header opens the guest pane on the right (Docker opens on **Files**; Host uses the same editor on `/workspace`). `.html` has a Browser preview, `.md` / `.svg` open in Preview, and `.pdf` renders inline. A listening HTTP port stacks **Preview** under Files. Host agents also get a **Browser** tab on the isolated Chrome profile. A coding or review agent can `canvas_write` a self-contained HTML report on a **Canvas** tab, separate from the repo and from Design's canvas.

Consecutive reads, searches, and commands collapse into one summary (`Explored 8 files, 4 searches, ran 1 command`). Expand the row for the per-call chips. Edits, questions, sub-agents, and pending approvals stay on their own rows.

Expand Down Expand Up @@ -123,9 +125,9 @@ Type `@file` in the composer to rank that path in the repo map. TypeScript gets
</tr>
</table>

### Automations
### Jobs

Interval, daily, weekly, cron, **Adaptive (by time of day)**, **On git push**, **On pull request**, or **On webhook** (Slack or any HTTP POST). Pick a named environment so overnight jobs reuse that machine. Jobs can store an IANA timezone, blackout dates, and a lifetime spend cap. Overlap can **Skip if busy**, **Run latest when free**, or **Queue one**. Optional **Remember facts between runs**. The card shows last run, last fact, and next fire. Docker, VM, or **Host**. Codebase can auto-review new pull requests. CLI and operator notes: [docs/webhook-automations.md](docs/webhook-automations.md).
The **Jobs** rail holds automations. Interval, daily, weekly, cron, **Adaptive (by time of day)**, **On git push**, **On pull request**, or **On webhook** (Slack or any HTTP POST). Pick a named environment so overnight jobs reuse that machine. Jobs can store an IANA timezone, blackout dates, and a lifetime spend cap. Overlap can **Skip if busy**, **Run latest when free**, or **Queue one**. Optional **Remember facts between runs**. The card shows last run, last fact, and next fire. Docker, VM, or **Host**. Codebase can auto-review new pull requests. CLI and operator notes: [docs/webhook-automations.md](docs/webhook-automations.md).

<table>
<tr>
Expand Down Expand Up @@ -187,12 +189,12 @@ Settings → Integrations stores a Grafana API key or service account token. The

The **Docker** sidebar lists every container on this engine. Running / stopped / exited groups, a detail pane with Logs / Inspect / Stats / Files, and bulk stop/restart. Locus agents stay linked. Check running rows and **Stop selected**, or use the command palette to stop the running set. The IDE explorer lists the same containers.

**Swarm** on that rail lists nodes and services when this engine is in Docker Swarm mode (service logs and scale on a manager). That is the local Docker engine. Locus Swarm (the teammate shell) and the Portainer integration are separate. Agents get `docker_swarm`. CLI: `locus docker swarm`. Setup: [docs/docker-swarm.md](docs/docker-swarm.md).
The **Services** tab on that rail lists Swarm nodes and services when this engine is in Docker Swarm mode (service logs and scale on a manager). That is the local Docker engine. Locus Swarm (the teammate shell) and the Portainer integration are separate. Agents get `docker_swarm`. CLI: `locus docker swarm`. Setup: [docs/docker-swarm.md](docs/docker-swarm.md).

<table>
<tr>
<td align="center" valign="top" width="50%"><img src="docs/screenshots/docker_empty.png" alt="Docker containers"><br>Containers</td>
<td align="center" valign="top" width="50%"><img src="docs/screenshots/docker_workspace.png" alt="Docker Swarm"><br>Swarm</td>
<td align="center" valign="top" width="50%"><img src="docs/screenshots/docker_workspace.png" alt="Docker Swarm services"><br>Services (Swarm)</td>
</tr>
</table>

Expand All @@ -204,13 +206,13 @@ The **Docker** sidebar lists every container on this engine. Running / stopped /

### Settings

Grouped as **Models** (Providers, MCP, Skills), **Workspace** (Machines, Workers, Environments, Git, Integrations), and **System** (Appearance, Notifications, Phone, Advanced, About). The page can find a provider, MCP server, skill, worker, environment, Git account, or integration by name.
Grouped as **Models** (Providers, MCP, Skills), **Workspace** (Machines, Workers, Environments, Git, Integrations), and **System** (Appearance, Notifications, Phone, Chrome, Advanced, About). The page can find a provider, MCP server, skill, worker, environment, Git account, or integration by name.

Git includes **Sign in with GitHub** / **Sign in with GitLab** / **Sign in with Bitbucket**. MCP defaults to deferred tool schemas (**Preload tool schemas** per server). HTTP MCP to an AWS API Gateway signs SigV4 when AWS keys are in that server's env. Traces live on the agent or design ⋯ menu → **View trace**.

Settings → Phone mints a LAN pairing token (QR on that tab). Settings → Workers lists LAN / Cloud VM / Fargate hosts. Settings → Environments lists named boxes saved from New agent or Automations. Settings → Notifications toggles desktop alerts and quiet hours. Settings → Appearance filters themes and sets editor font size, format on save, and the minimap. Settings → Advanced **Tailscale Funnel** exposes **only** `/api/hooks` (never the whole control plane) so GitHub can reach forge webhooks; `locus config funnel enable|disable`.

Integrations: Slack, Grafana, Jira, Confluence, Linear, Miro, Figma, incident.io, AWS IAM, Portainer, Kubernetes, Sentry, Gmail, Google Calendar, Google Drive, and agent-authored HTTP plugins. Settings → About is **0.19.0**.
Integrations: Slack, Grafana, Jira, Confluence, Linear, Miro, Figma, incident.io, AWS IAM, Portainer, Kubernetes, Sentry, Gmail, Google Calendar, Google Drive, and agent-authored HTTP plugins. Settings → About is **0.20.0**.

<table>
<tr>
Expand Down Expand Up @@ -238,7 +240,8 @@ Integrations: Slack, Grafana, Jira, Confluence, Linear, Miro, Figma, incident.io
<td align="center" valign="top"><img src="docs/screenshots/settings_advanced.png" alt="Advanced"><br>Advanced</td>
</tr>
<tr>
<td align="center" valign="top" colspan="2"><img src="docs/screenshots/settings_about.png" alt="About"><br>About</td>
<td align="center" valign="top"><img src="docs/screenshots/settings_chrome.png" alt="Chrome"><br>Chrome</td>
<td align="center" valign="top"><img src="docs/screenshots/settings_about.png" alt="About"><br>About</td>
</tr>
</table>

Expand Down Expand Up @@ -303,7 +306,7 @@ Developer / Swarm in the sidebar switches the product shell. Developer keeps the

**CLI.** `npx locus` / `npm run locus --` is a loopback client for the same control plane: agents, design, games, assets, IDE, projects, Codebase, Brain, Slack, Grafana, Docker, automations, workflows, Settings, phone remote, and Funnel. `locus serve` starts the server when the app is not running. `locus project keep` leaves that control plane up after the desktop quits. `locus workflow` imports and runs Agent Loop Builder graphs. `locus brain setup` writes personal editor hooks that send a redacted transcript tail to the local brain. Live VNC and canvas clicking stay in the desktop. Guide: [docs/cli.md](docs/cli.md).

**App.** Ninety-eight themes (Ember default). Settings → About is **0.19.0**. Electron packages (Linux AppImage, macOS dmg, Windows NSIS), or `npm run dev` in the browser. **Ctrl/Cmd+K** command palette. **Ctrl/Cmd+N** new agent, new automation, or (on Design, Games, Assets) the empty composer. **Ctrl/Cmd+F** find in chat. **Ctrl/Cmd+,** settings.
**App.** 106 themes (Ember default). Settings → About is **0.20.0**. Electron packages (Linux AppImage, macOS dmg, Windows NSIS), or `npm run dev` in the browser. **Ctrl/Cmd+K** command palette. **Ctrl/Cmd+N** new agent, new automation, or (on Design, Games, Assets) the empty composer. **Ctrl/Cmd+F** find in chat. **Ctrl/Cmd+,** settings.

## Security model

Expand Down
4 changes: 2 additions & 2 deletions apps/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId = "app.locus.android"
minSdk = 26
targetSdk = 35
versionCode = 33
versionName = "0.19.0"
versionCode = 34
versionName = "0.20.0"
}
buildTypes {
release {
Expand Down
2 changes: 1 addition & 1 deletion apps/chrome-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Locus",
"version": "0.19.0",
"version": "0.20.0",
"description": "Let Locus host agents use your Chrome tabs — navigate, click, type, and screenshot the pages you already have open.",
"action": {
"default_title": "Locus",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@locus/cli",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"description": "Locus CLI — control plane client for agents, Slack, Codebase, Docker, and automations",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@locus/desktop",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"author": "Locus contributors",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ vi.mock("@/lib/api", () => ({
connected: false,
tabs: [],
})),
checkUpdates: vi.fn(async () => ({ status: "unavailable", current: "0.19.0" })),
checkUpdates: vi.fn(async () => ({ status: "unavailable", current: "0.20.0" })),
workersStatus: vi.fn(async () => []),
tailscale: vi.fn(async () => ({
installed: false,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export function App() {
gameStyles: gameCatalog.styles,
hypervisorKind: health.hypervisor === "qemu" ? "qemu" : "mock",
dockerKind: health.docker === "docker" ? "docker" : "mock",
dockerReady: health.docker === "docker" && health.dockerReady !== false,
setup: setup ?? undefined,
});
seenGatesRef.current = new Set(sessions.flatMap((session) => needsYouKeys(session)));
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/components/automations/Automations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ export function AutomationsPane() {
<div className="empty-card">
<EmptyHero
pose="rest"
eyebrow="Jobs"
title="When should an agent run?"
lead="Pick a trigger — clock, git push, pull request, or webhook — then a prompt. Each run executes on Docker, VM, or host and keeps a thread you can open."
/>
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/components/brain/Brain.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,9 @@ describe("BrainPane", () => {
});
expect(host.querySelector(".brain-pane.brain-empty")).toBeTruthy();
expect(host.querySelector(".brain-body-center .brain-start")).toBeTruthy();
expect([...host.querySelectorAll("button")].some((btn) => btn.textContent === "Browse")).toBe(
true,
);
expect(
[...host.querySelectorAll("button")].some((btn) => btn.textContent?.trim() === "Browse"),
).toBe(true);
});

it("hides data actions and the extra index banner until the brain has data", async () => {
Expand Down
5 changes: 1 addition & 4 deletions apps/desktop/src/components/brain/Brain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,6 @@ export function BrainPane() {
<label>
Repo folder
<div className="host-folder-row">
<span className="host-folder-icon" aria-hidden>
<IconFolder />
</span>
<input
value={path}
onChange={(e) => setPath(e.target.value)}
Expand All @@ -315,7 +312,7 @@ export function BrainPane() {
aria-label="Repo folder to index"
/>
<button type="button" className="btn ghost" onClick={() => setPickerOpen(true)}>
Browse
<IconFolder /> Browse
</button>
</div>
</label>
Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/src/components/chat/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import {
mergeComposerFiles,
} from "./ComposerFiles";
import { ModelPicker } from "@/components/settings/ModelPicker";
import { supersededPlainErrorIds } from "@/lib/automations";
import { SubagentPicker } from "./SubagentPicker";
import { ContextMeter } from "./ContextMeter";
import { PlanCard } from "./PlanCard";
Expand Down Expand Up @@ -297,7 +298,10 @@ export function ChatPane() {
},
[dispatch, state.selectedId, state.sessions],
);
const hideLifecycleIds = useMemo(() => supersededGuestLifecycleIds(messages), [messages]);
const hideLifecycleIds = useMemo(
() => new Set([...supersededGuestLifecycleIds(messages), ...supersededPlainErrorIds(messages)]),
[messages],
);
const findActiveId = findHits[Math.min(findIndex, Math.max(0, findHits.length - 1))];
const newMarkerId = visitMarkerId(state.chatNewVisit, sessionId);

Expand Down Expand Up @@ -948,9 +952,12 @@ export function ChatPane() {
: turning
? (state.thinking[session.id] ?? "")
: "";
// Tokens already arrived: the local server has loaded the model.
const streamedText = last?.role === "assistant" && Boolean(last.content.trim());
const localWaitLabel =
!viewing &&
turning &&
!streamedText &&
!waitingGuest &&
!waitingStartNote &&
!waitingClone &&
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/components/codebase/Codebase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,6 @@ export function CodebasePane() {
<div className={`empty-card codebase-empty${importing || accounts ? " has-import" : ""}`}>
<EmptyHero
pose="rest"
eyebrow="Codebase"
title="Link a repository"
lead="Catalog a GitHub, GitLab, Bitbucket, Gitea, Forgejo, or Azure DevOps repo, then review pulls here."
/>
Expand Down
Loading
Loading