diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4b1150a..7eda5fd 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -12,8 +12,8 @@ body: id: version attributes: label: Version - description: Settings → About, or the GitHub Release tag (for example 0.20.0). - placeholder: 0.20.0 + description: Settings → About, or the GitHub Release tag (for example 0.21.0). + placeholder: 0.21.0 validations: required: true - type: dropdown diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12eb295..b569695 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,10 +23,24 @@ jobs: run: npx playwright install --with-deps chromium working-directory: apps/desktop - run: npm run test:visual + - name: Upload visual diffs + if: ${{ failure() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: visual-diffs + path: apps/desktop/test-results/ + if-no-files-found: ignore + retention-days: 14 + # Run the remaining checks even when an earlier step failed, so a + # screenshot or test failure cannot hide a lint/type/format/build break. - run: npm run typecheck + if: ${{ !cancelled() }} - run: npm run lint + if: ${{ !cancelled() }} - run: npm run format:check + if: ${{ !cancelled() }} - run: npm run build --workspace=@locus/desktop + if: ${{ !cancelled() }} docker-image: runs-on: ubuntu-latest @@ -41,7 +55,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.20.0 images/agent + - run: docker build -t locus/agent:0.1 -t locus/agent:0.21.0 images/agent android: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 2922041..e3f559f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ ## Unreleased +## 0.21.0 — 2026-11-13 + +Decide from the Inbox, a turn summary card, tray and actionable notifications, encrypted backup and restore, background update checks with one-click install, a model benchmark, a smoother first hour with a public repo, and much faster workspaces on large repos. Settings → About is **0.21.0**. + +**Inbox decisions.** The Inbox shows a decision card for each waiting item: what will run, where it runs, the agent's last words, and a risk hint. Approve, Deny, Always allow and Open chat sit on the card, with keyboard shortcuts (J/K, A, D, Enter) and **Approve all** when several chats wait on the same tool. + +**Turn summary and first task.** When a turn changes files, runs tests or opens a PR, a card under the reply lists the files changed, whether tests passed, and next steps: Review changes, Open PR, Continue, or Undo this turn. The first-task card on New agent now suggests real work on your most recent repo or folder. + +**Tray and notifications.** The desktop app has a tray icon and dock/taskbar badge with the needs-you count. Notifications say what the agent wants and open the chat when clicked; on macOS, approvals can be answered straight from the notification. + +**Backup and restore.** Settings → Advanced → Back up and restore, or `locus backup` / `locus restore`, writes one passphrase-encrypted file with your sessions, settings, boards, projects, Jobs, Brain, workspaces and stored keys. Machine-bound keys are never copied; the vault is re-encrypted for the new machine, so a restore on a new laptop keeps every provider key. A wrong passphrase or tampered file is refused. + +**Updates.** The desktop checks for new releases shortly after launch and every six hours, and **Install** downloads, verifies the checksum and installs (in place for Linux AppImage). Turn it off in Settings → About. CLI: `locus update`. + +**Model benchmark.** Settings → Benchmark, or `locus eval run`, runs five small coding tasks against the models you pick and records pass rate, tokens, cost, tool calls and time per model. + +**First hour with a public repo.** With Docker stopped, actions that need a container say so and offer to start on Host instead. A linked repo is preselected across New agent, Design, Jobs, Brain and IDE. No-token accounts show their rate limit and when it resets. Codebase gains an About card for the repo, and the setup wizard tests local servers such as Ollama with a real call. + +**Performance on large repos.** Agent search after an edit no longer rebuilds the whole index (seconds down to milliseconds on very large repos), VM file listings, search without ripgrep, repo index rebuilds, the repo map and Quick Open are all much faster, and the desktop app now ships ripgrep. + +**Fixes.** Undo on a git workspace reliably restores edited files. The desktop app uses Electron's async secure-storage API, so the deprecation warnings are gone and existing vault keys still unlock. The control plane and worker start without `ssh-keygen` (only VM agents need it). + ## 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**. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ff761b..bf99306 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,7 @@ Thanks for trying Locus. This tree is a Node workspaces monorepo (`apps/*`, `pac - Node.js 22.22+ (Brain FTS5 needs `node:sqlite`; GitHub Actions uses 22.22) - npm 10+ (comes with Node) +- `ssh-keygen` (`openssh-client` on Debian/Ubuntu) for VM agents. Without it the control plane still starts and logs a warning; Host and Docker agents work - Optional: QEMU + `qemu-img` for real VMs; Docker CLI for real containers. Without them the mocks still run the UI ```bash @@ -48,8 +49,8 @@ First agent without Docker: Settings → Providers (Ollama needs no key) then Ne After merge to `main`: ```bash -git tag v0.20.0 -git push origin v0.20.0 +git tag v0.21.0 +git push origin v0.21.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`. diff --git a/README.md b/README.md index f8000de..f35066f 100644 --- a/README.md +++ b/README.md @@ -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.20.0** (MIT). The API binds to loopback. Linux, macOS, and Windows. Bring any LLM provider you have a key for. +This is **v0.21.0** (MIT). The API binds to loopback. Linux, macOS, and Windows. Bring any LLM provider you have a key for. ## Install @@ -206,13 +206,15 @@ The **Services** tab on that rail lists Swarm nodes and services when this engin ### Settings -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. +Grouped as **Models** (Providers, MCP, Skills, Benchmark), **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 → About checks GitHub Releases at launch and every six hours. **Install** verifies the download against `SHA256SUMS`, then swaps a Linux AppImage in place and relaunches, runs the Windows installer, or opens the dmg / deb / rpm. Settings → Advanced → **Back up and restore** writes one passphrase-encrypted `.locusbak` with sessions, settings, stored keys, boards, projects, Jobs and Brain, and restores it on another machine at the next start (`locus backup` / `locus restore`). Settings → **Benchmark** runs five small coding tasks as real Host turns against the models you pick and compares pass rate, cost and time (`locus eval run`). + 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.20.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.21.0**. @@ -306,7 +308,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.** 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. +**App.** 106 themes (Ember default). Settings → About is **0.21.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 diff --git a/SECURITY.md b/SECURITY.md index 278afab..82e1e41 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -49,6 +49,7 @@ Please use [GitHub private vulnerability reporting](https://docs.github.com/en/c - **What goes to providers.** Chat messages, tool results (after secret redaction), and screenshots when Vision is on are sent to the provider you enabled. Locus does not send traces, vault files, or `master.key`. - **Training.** Locus does not train models. Whether a provider trains on API traffic is that provider's policy; use your provider's zero-retention / API-data controls when you need them. - **Retention.** Session JSON, traces, screenshots, recordings, and disks stay under `~/.locus` on this machine until you delete the session or Reset all data. Automations that open a new chat per run keep the newest 60 chats for that job (pinned and in-flight chats stay) and delete the rest from this machine. There is no cloud copy of the control plane. +- **Backups.** A `.locusbak` from Settings → Advanced or `locus backup` contains every stored key and token, decrypted inside an AES-256-GCM archive keyed by your passphrase (scrypt). Treat the file and passphrase like the keys themselves. `master.key`, `master.key.safe` and `secrets.enc` are never copied; restore re-encrypts the vault with the target machine's key. Backup and restore routes refuse anyone but the local operator (not a paired phone). A wrong passphrase or tampered archive fails the GCM check and nothing is staged. - **Isolation.** This is a single-operator local app. Each agent has its own guest (Docker workspace or VM disk). That is workspace isolation, not multi-tenant SaaS tenancy. Do not run untrusted operators against one `~/.locus`. ## Reliability diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index 53a371c..c5cfaa1 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -10,8 +10,8 @@ android { applicationId = "app.locus.android" minSdk = 26 targetSdk = 35 - versionCode = 34 - versionName = "0.20.0" + versionCode = 35 + versionName = "0.21.0" } buildTypes { release { diff --git a/apps/chrome-extension/manifest.json b/apps/chrome-extension/manifest.json index eb67822..7cefd9d 100644 --- a/apps/chrome-extension/manifest.json +++ b/apps/chrome-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Locus", - "version": "0.20.0", + "version": "0.21.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", diff --git a/apps/cli/package.json b/apps/cli/package.json index ea33719..77a39a1 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@locus/cli", - "version": "0.20.0", + "version": "0.21.0", "private": true, "description": "Locus CLI — control plane client for agents, Slack, Codebase, Docker, and automations", "type": "module", @@ -22,6 +22,6 @@ "@types/node": "^26.6.5", "@types/ws": "^8.18.2", "typescript": "^7.0.2", - "vitest": "^5.0.5" + "vitest": "^5.1.0" } } diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts index f9117c0..071c75a 100644 --- a/apps/cli/src/args.ts +++ b/apps/cli/src/args.ts @@ -39,6 +39,8 @@ const BOOLEAN_FLAGS = new Set([ "printable", "no-texture", "no-enhance", + "include-disks", + "inspect", ]); const VALUE_FLAGS = new Set([ @@ -137,9 +139,15 @@ const VALUE_FLAGS = new Set([ "notify", "node", "memory", + "case", + "repeat", + "out", + "current", + "passphrase-env", + "poll-ms", ]); -const MULTI_VALUE_FLAGS = new Set(["arg", "repo"]); +const MULTI_VALUE_FLAGS = new Set(["arg", "repo", "model", "case"]); function appendFlag(prev: FlagValue | undefined, name: string, next: string): string { if (MULTI_VALUE_FLAGS.has(name) && typeof prev === "string" && prev.length) { diff --git a/apps/cli/src/commands.ts b/apps/cli/src/commands.ts index f06600b..74ca36e 100644 --- a/apps/cli/src/commands.ts +++ b/apps/cli/src/commands.ts @@ -60,6 +60,16 @@ import { helpFor } from "./help.js"; import { isCompletionShell, renderCompletion } from "./completion.js"; import { startServer } from "./serve.js"; import { CLI_VERSION } from "./version.js"; +import { + cmdBackup, + cmdEvalCancel, + cmdEvalCases, + cmdEvalList, + cmdEvalRun, + cmdEvalShow, + cmdRestore, + cmdUpdate, +} from "./maintenance.js"; export interface Ctx { client: LocusClient; @@ -388,6 +398,17 @@ const ROUTES: Array<{ path: string[]; run: Handler }> = [ { path: ["image", "fetch-virtio"], run: cmdImageFetchVirtio }, { path: ["image"], run: cmdImageList }, + { path: ["backup"], run: (ctx) => cmdBackup(ctx) }, + { path: ["restore"], run: (ctx) => cmdRestore(ctx) }, + { path: ["update"], run: cmdUpdate }, + + { path: ["eval", "cases"], run: cmdEvalCases }, + { path: ["eval", "run"], run: cmdEvalRun }, + { path: ["eval", "list"], run: cmdEvalList }, + { path: ["eval", "show"], run: cmdEvalShow }, + { path: ["eval", "cancel"], run: cmdEvalCancel }, + { path: ["eval"], run: cmdEvalList }, + { path: ["debug", "logs"], run: cmdDebugLogs }, { path: ["debug", "log"], run: cmdDebugLog }, { path: ["debug", "eval"], run: cmdDebugEval }, diff --git a/apps/cli/src/help.ts b/apps/cli/src/help.ts index 026dc04..d12e990 100644 --- a/apps/cli/src/help.ts +++ b/apps/cli/src/help.ts @@ -118,6 +118,16 @@ Config / Settings: locus config revoke-all --confirm REVOKE locus config reset --confirm RESET +Backup / updates: + locus backup [file.locusbak] [--include-disks] Encrypted copy of ~/.locus + locus restore --yes [--inspect] Applies at the next start + locus restore status|cancel + locus update [download] Check GitHub releases + +Model benchmark: + locus eval run --model provider/model[,…] [--case id,…] [--repeat N] [--wait] + locus eval cases|list|show |cancel + Images / debug: locus image list|capture|delete|fetch|fetch-virtio locus debug logs|log|eval|egress|approvals @@ -311,6 +321,41 @@ Agents flag bad claims with correct_graph; inbox Accept/Dismiss is the human gat locus config theme ember locus config workers add 'locus-worker:add?url=…' locus config reset --confirm RESET`, + backup: `Write an encrypted backup of ~/.locus (sessions, settings, vault, boards, projects, Brain). + + locus backup [file.locusbak] [--include-disks] + +The passphrase comes from LOCUS_BACKUP_PASSPHRASE (or --passphrase-env NAME), or a hidden +prompt. Stored secrets travel inside the encrypted file and are re-encrypted with the target +machine's key on restore. VM disks and images are left out unless --include-disks.`, + restore: `Restore a backup made by \`locus backup\` or Settings → Advanced. + + locus restore --inspect Check the passphrase and show what is inside + locus restore --yes Stage it; quit and reopen Locus to apply + locus restore status + locus restore cancel + +The swap happens at the next start, before anything loads. Current files move to +~/.locus/.before-restore-
+ + + + + + + + + + {run.summary.map((row) => ( + + + + + + + ))} + +
ModelPassedCostMedian time
{benchmarkTargetKey(row.target)} + {row.passed}/{row.total} + {row.total ? ` · ${Math.round(row.passRate * 100)}%` : ""} + {row.usd != null ? formatUsd(row.usd) : "–"}{seconds(row.medianMs)}
+ {open === run.id ? ( + + + + + + + + + + + {run.results.map((row, i) => ( + + + + + + + + ))} + +
CaseModelResultTime +
{row.caseId}{benchmarkTargetKey(row.target)} + {row.status} + {row.reason && row.status !== "passed" ? ` · ${row.reason}` : ""} + {seconds(row.durationMs)} + {row.sessionId && props.onOpenSession ? ( + + ) : null} +
+ ) : null} + + ); + })} + + ); +} diff --git a/apps/desktop/src/components/settings/Settings.test.tsx b/apps/desktop/src/components/settings/Settings.test.tsx index 73ddd44..7ffa4b9 100644 --- a/apps/desktop/src/components/settings/Settings.test.tsx +++ b/apps/desktop/src/components/settings/Settings.test.tsx @@ -75,7 +75,7 @@ vi.mock("@/lib/api", () => ({ rotateChromeExtension: vi.fn(), checkUpdates: vi.fn(async () => ({ status: "unavailable", - current: "0.20.0", + current: "0.21.0", message: "No GitHub release found yet. Check for updates works after a v* tag is published.", })), refreshCursorCli: vi.fn(async () => ({ changed: false, models: [] })), @@ -355,6 +355,7 @@ describe("Settings", () => { "Providers", "MCP", "Skills", + "Benchmark", "Machines", "Workers", "Environments", diff --git a/apps/desktop/src/components/settings/Settings.tsx b/apps/desktop/src/components/settings/Settings.tsx index dbe7062..b26f35f 100644 --- a/apps/desktop/src/components/settings/Settings.tsx +++ b/apps/desktop/src/components/settings/Settings.tsx @@ -27,6 +27,7 @@ import { promptQueueLimit, UBUNTU_DEFAULT_HARDWARE, defaultNetworking, + parseCheckForUpdates, parseProductMode, type AgentSkill, type AiProvider, @@ -41,6 +42,7 @@ import { type WorkerBackend, } from "@locus/shared"; import { api, type HypervisorStatus } from "@/lib/api"; +import { installAppUpdate, installOutcomeMessage, updateVersionLabel } from "@/lib/appUpdate"; import { APP_DEVELOPER, APP_LICENSE, @@ -85,6 +87,8 @@ import { import { PolicyBanner, policyControlLocked } from "./PolicyBanner"; import { AppearanceSettings } from "./AppearanceSettings"; import { NotificationSettings } from "./NotificationSettings"; +import { BenchmarkSettings } from "./BenchmarkSettings"; +import { BackupSettings } from "./BackupSettings"; import { HowItWorks, SettingsToggleRow } from "./SettingsBits"; import { SearchField } from "@/components/layout/SearchField"; import { BusyButton } from "@/components/layout/Skeleton"; @@ -968,7 +972,7 @@ export function Settings() { async function checkForUpdates() { setUpdateBusy(true); try { - const result = await api.checkUpdates(APP_VERSION); + const result = await api.checkUpdates(APP_VERSION, { refresh: true }); setUpdate(result); setUpdateDownload(null); } catch (err) { @@ -1250,6 +1254,13 @@ export function Settings() { /> )} + {tab === "benchmark" && ( + dispatch({ type: "select", id })} + /> + )} + {tab === "notifications" && ( ) : (

- No Git accounts connected yet. Add GitHub, GitLab, Bitbucket, Gitea, Forgejo, or - Azure DevOps in Git. + No Git tokens yet. Public repos still link and load without one (GitHub allows 60 + requests an hour). Add GitHub, GitLab, Bitbucket, Gitea, Forgejo, or Azure DevOps in + Git for private repos and higher limits.

)}