From 769d125e60549d5de6b9d9d5c02d0e4b769cfde0 Mon Sep 17 00:00:00 2001 From: Damon <16412057+Modsofthenation@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:39:56 +0000 Subject: [PATCH] Publish Locus 0.20.0. Bring current product sources across and retarget auto-updates at Modsofthenation/Locus. --- .github/ISSUE_TEMPLATE/bug.yml | 4 +- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 12 + CONTRIBUTING.md | 4 +- README.md | 23 +- apps/android/app/build.gradle.kts | 4 +- apps/chrome-extension/manifest.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/App.test.tsx | 2 +- apps/desktop/src/App.tsx | 1 + .../components/automations/Automations.tsx | 1 - .../src/components/brain/Brain.test.tsx | 6 +- apps/desktop/src/components/brain/Brain.tsx | 5 +- apps/desktop/src/components/chat/ChatPane.tsx | 9 +- .../src/components/codebase/Codebase.tsx | 1 - .../src/components/design/DesignEmpty.tsx | 28 +- apps/desktop/src/components/docker/Docker.tsx | 16 +- .../src/components/grafana/Grafana.tsx | 2 +- .../src/components/layout/EmptyState.test.tsx | 34 +- .../src/components/layout/EmptyState.tsx | 46 ++- .../src/components/layout/FeatureNudge.tsx | 13 +- .../src/components/layout/MobileShell.tsx | 4 +- .../src/components/layout/Sidebar.test.tsx | 13 +- .../desktop/src/components/layout/Sidebar.tsx | 67 ++-- apps/desktop/src/components/layout/Topbar.tsx | 20 +- .../components/layout/sidebar/SideRail.tsx | 7 + .../src/components/projects/ProjectEmpty.tsx | 18 +- .../settings/AppearanceSettings.tsx | 4 +- .../src/components/settings/Settings.test.tsx | 2 +- .../src/components/settings/Settings.tsx | 62 +-- .../src/components/settings/SettingsCards.tsx | 2 +- .../src/components/settings/SetupWizard.tsx | 10 +- .../components/settings/ThemePreview.test.tsx | 1 + .../src/components/settings/ThemePreview.tsx | 1 + apps/desktop/src/index.css | 364 +++++++++++++++--- apps/desktop/src/lib/api.ts | 5 +- apps/desktop/src/lib/automations.test.ts | 22 ++ apps/desktop/src/lib/automations.ts | 19 + apps/desktop/src/lib/palette.ts | 2 +- apps/desktop/src/lib/shortcuts.test.ts | 8 + apps/desktop/src/lib/shortcuts.ts | 5 + apps/desktop/src/lib/sidebarCopy.ts | 5 +- apps/desktop/src/lib/theme.test.ts | 23 +- apps/desktop/src/lib/theme.ts | 2 + apps/desktop/src/lib/uxContract.test.ts | 2 +- apps/desktop/src/state/store.test.ts | 18 + apps/desktop/src/state/store.ts | 9 + apps/desktop/src/styles/swarm.css | 95 ++++- apps/desktop/src/styles/ux.css | 148 ++++++- apps/desktop/src/visual.tsx | 2 +- apps/server/package.json | 2 +- apps/server/src/app.test.ts | 2 +- apps/server/src/app.ts | 3 +- apps/server/src/integrations/locusMcp.ts | 2 +- apps/server/src/integrations/mcp.ts | 4 +- apps/server/src/session/sessions.ts | 15 + docs/screenshots/agent_settings.png | Bin 119012 -> 108800 bytes docs/screenshots/agents_workspace.png | Bin 96318 -> 110256 bytes docs/screenshots/assets_empty.png | Bin 84674 -> 73732 bytes docs/screenshots/assets_workspace.png | Bin 109542 -> 234935 bytes docs/screenshots/automations_empty.png | Bin 82365 -> 109734 bytes docs/screenshots/board_empty.png | Bin 62382 -> 52264 bytes docs/screenshots/board_workspace.png | Bin 67091 -> 80722 bytes docs/screenshots/brain_empty.png | Bin 68536 -> 45903 bytes docs/screenshots/codebase_empty.png | Bin 77565 -> 63123 bytes docs/screenshots/command_palette.png | Bin 92214 -> 100198 bytes docs/screenshots/design_empty.png | Bin 84715 -> 86885 bytes docs/screenshots/design_settings.png | Bin 129777 -> 135730 bytes docs/screenshots/design_workspace.png | Bin 113574 -> 135689 bytes docs/screenshots/docker_empty.png | Bin 56076 -> 76991 bytes docs/screenshots/docker_workspace.png | Bin 60863 -> 60280 bytes docs/screenshots/games_empty.png | Bin 91352 -> 87119 bytes docs/screenshots/games_workspace.png | Bin 73666 -> 93774 bytes docs/screenshots/grafana_empty.png | Bin 62321 -> 45502 bytes docs/screenshots/ide_empty.png | Bin 56243 -> 50862 bytes docs/screenshots/ide_workspace.png | Bin 80192 -> 77464 bytes docs/screenshots/inbox.png | Bin 0 -> 40069 bytes docs/screenshots/mobile_agents.png | Bin 21400 -> 36791 bytes docs/screenshots/mobile_board.png | Bin 15953 -> 19346 bytes docs/screenshots/mobile_chat.png | Bin 30159 -> 41016 bytes docs/screenshots/mobile_code.png | Bin 17350 -> 22477 bytes docs/screenshots/mobile_design.png | Bin 14962 -> 20077 bytes docs/screenshots/mobile_design_canvas.png | Bin 57630 -> 55964 bytes docs/screenshots/mobile_games.png | Bin 22394 -> 30446 bytes docs/screenshots/mobile_inbox.png | Bin 14697 -> 34999 bytes docs/screenshots/mobile_more.png | Bin 39924 -> 46511 bytes docs/screenshots/mobile_new_chat.png | Bin 12660 -> 16213 bytes docs/screenshots/mobile_slack.png | Bin 21937 -> 26251 bytes docs/screenshots/new_agent.png | Bin 53845 -> 88443 bytes docs/screenshots/new_automation.png | Bin 50863 -> 100560 bytes docs/screenshots/new_automation_webhook.png | Bin 137133 -> 141646 bytes docs/screenshots/projects_empty.png | Bin 65518 -> 55018 bytes docs/screenshots/projects_workspace.png | Bin 89224 -> 115237 bytes docs/screenshots/settings_about.png | Bin 131179 -> 158608 bytes docs/screenshots/settings_advanced.png | Bin 117403 -> 124085 bytes docs/screenshots/settings_appearance.png | Bin 110177 -> 140146 bytes docs/screenshots/settings_chrome.png | Bin 0 -> 68988 bytes docs/screenshots/settings_environments.png | Bin 67015 -> 71953 bytes docs/screenshots/settings_git.png | Bin 106418 -> 89334 bytes docs/screenshots/settings_integrations.png | Bin 120578 -> 120420 bytes docs/screenshots/settings_machines.png | Bin 121371 -> 132214 bytes docs/screenshots/settings_mcp.png | Bin 76706 -> 68607 bytes docs/screenshots/settings_notifications.png | Bin 92298 -> 104867 bytes docs/screenshots/settings_phone.png | Bin 70642 -> 65166 bytes docs/screenshots/settings_providers.png | Bin 99313 -> 91316 bytes docs/screenshots/settings_skills.png | Bin 120673 -> 143493 bytes docs/screenshots/settings_workers.png | Bin 97285 -> 69138 bytes docs/screenshots/setup_wizard.png | Bin 137890 -> 98517 bytes docs/screenshots/slack_empty.png | Bin 54382 -> 43148 bytes docs/troubleshooting.md | 2 +- docs/ux-review-2026-11.md | 115 ++++++ images/agent/README.md | 2 +- package-lock.json | 104 ++--- package.json | 4 +- packages/alb/package.json | 2 +- packages/compute/package.json | 2 +- packages/guest-agent/package.json | 2 +- packages/hypervisor/package.json | 2 +- packages/hypervisor/src/images.test.ts | 2 +- packages/providers/package.json | 2 +- packages/shared/package.json | 2 +- packages/shared/src/index.test.ts | 15 +- packages/shared/src/models/session.test.ts | 15 + packages/shared/src/models/session.ts | 9 + packages/shared/src/models/theme.ts | 79 +++- packages/shared/src/models/vm.ts | 2 +- packages/worker/package.json | 2 +- 128 files changed, 1262 insertions(+), 288 deletions(-) create mode 100644 docs/screenshots/inbox.png create mode 100644 docs/screenshots/settings_chrome.png create mode 100644 docs/ux-review-2026-11.md diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 6048042..4b1150a 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.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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab7822b..12eb295 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 467d28b..2922041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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**. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdc2608..7ff761b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`. diff --git a/README.md b/README.md index 775eafb..f8000de 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.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 @@ -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. @@ -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/`, 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. @@ -123,9 +125,9 @@ Type `@file` in the composer to rank that path in the repo map. TypeScript gets -### 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). @@ -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).
- +
Docker containers
Containers
Docker Swarm
Swarm
Docker Swarm services
Services (Swarm)
@@ -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**. @@ -238,7 +240,8 @@ Integrations: Slack, Grafana, Jira, Confluence, Linear, Miro, Figma, incident.io - + +
Advanced
Advanced
About
About
Chrome
Chrome
About
About
@@ -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 diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index b5e609d..53a371c 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 = 33 - versionName = "0.19.0" + versionCode = 34 + versionName = "0.20.0" } buildTypes { release { diff --git a/apps/chrome-extension/manifest.json b/apps/chrome-extension/manifest.json index 45b6b50..eb67822 100644 --- a/apps/chrome-extension/manifest.json +++ b/apps/chrome-extension/manifest.json @@ -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", diff --git a/apps/cli/package.json b/apps/cli/package.json index e0e3683..ea33719 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b2cca2d..4469f7a 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@locus/desktop", - "version": "0.19.0", + "version": "0.20.0", "private": true, "author": "Locus contributors", "license": "MIT", diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index e1cc771..3091b8b 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -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, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index be1e3e0..d168db6 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -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))); diff --git a/apps/desktop/src/components/automations/Automations.tsx b/apps/desktop/src/components/automations/Automations.tsx index ab911a9..8b09825 100644 --- a/apps/desktop/src/components/automations/Automations.tsx +++ b/apps/desktop/src/components/automations/Automations.tsx @@ -269,7 +269,6 @@ export function AutomationsPane() {
diff --git a/apps/desktop/src/components/brain/Brain.test.tsx b/apps/desktop/src/components/brain/Brain.test.tsx index 3ddb3e5..5201391 100644 --- a/apps/desktop/src/components/brain/Brain.test.tsx +++ b/apps/desktop/src/components/brain/Brain.test.tsx @@ -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 () => { diff --git a/apps/desktop/src/components/brain/Brain.tsx b/apps/desktop/src/components/brain/Brain.tsx index 3841f76..6a3c6ca 100644 --- a/apps/desktop/src/components/brain/Brain.tsx +++ b/apps/desktop/src/components/brain/Brain.tsx @@ -304,9 +304,6 @@ export function BrainPane() { diff --git a/apps/desktop/src/components/chat/ChatPane.tsx b/apps/desktop/src/components/chat/ChatPane.tsx index 3382131..2a95321 100644 --- a/apps/desktop/src/components/chat/ChatPane.tsx +++ b/apps/desktop/src/components/chat/ChatPane.tsx @@ -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"; @@ -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); @@ -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 && diff --git a/apps/desktop/src/components/codebase/Codebase.tsx b/apps/desktop/src/components/codebase/Codebase.tsx index 6a24253..3a42d4d 100644 --- a/apps/desktop/src/components/codebase/Codebase.tsx +++ b/apps/desktop/src/components/codebase/Codebase.tsx @@ -1219,7 +1219,6 @@ export function CodebasePane() {
diff --git a/apps/desktop/src/components/design/DesignEmpty.tsx b/apps/desktop/src/components/design/DesignEmpty.tsx index 09ba370..e80fe08 100644 --- a/apps/desktop/src/components/design/DesignEmpty.tsx +++ b/apps/desktop/src/components/design/DesignEmpty.tsx @@ -222,7 +222,9 @@ export function DesignEmpty({ surface = "design" }: { surface?: CanvasLaunchSurf const [branchLock, setBranchLock] = useState(false); /** Games / Assets keep unset context (repo, asset, kit, style, build) in the "+" menu. */ const [revealed, setRevealed] = useState>(() => new Set()); - const canCreate = Boolean(state.config); + const dockerDown = + runtime === "docker" && state.dockerKind === "docker" && !state.dockerReady && !backendId; + const canCreate = Boolean(state.config) && !dockerDown; const workers = state.config?.workers ?? []; const selectedWorker = workers.find((row) => row.id === backendId); const launch = CANVAS_LAUNCH[surface]; @@ -277,6 +279,17 @@ export function DesignEmpty({ surface = "design" }: { surface?: CanvasLaunchSurf }` : ""; + // Docker CLI installed but the daemon is down: start on Host instead of a launch that fails. + useEffect(() => { + if ( + state.dockerKind === "docker" && + !state.dockerReady && + policyAllowsHostAgents(state.config?.managedPolicy) + ) { + setRuntime((current) => (current === "docker" ? "host" : current)); + } + }, [state.dockerKind, state.dockerReady, state.config?.managedPolicy]); + useEffect(() => { const defaults = resolveAgentDefaults(state.config, "ubuntu", loadLastAgent()); setProviderId((current) => current || defaults.providerId); @@ -576,6 +589,17 @@ export function DesignEmpty({ surface = "design" }: { surface?: CanvasLaunchSurf testId="engine-setup-banner" /> ) : null} + {dockerDown ? ( + + + + ) : null} {workers.length ? (
@@ -719,7 +743,7 @@ export function DesignEmpty({ surface = "design" }: { surface?: CanvasLaunchSurf files={files} placeholder={ surface === "game" - ? `${launch.placeholder.replace(/…$/, "")} @asset to reference an asset…` + ? `${launch.placeholder.replace(/[.…]+$/, "")}. Type @ to reference an asset…` : launch.placeholder } onChange={(value) => { diff --git a/apps/desktop/src/components/docker/Docker.tsx b/apps/desktop/src/components/docker/Docker.tsx index 4be6cd7..e65e58c 100644 --- a/apps/desktop/src/components/docker/Docker.tsx +++ b/apps/desktop/src/components/docker/Docker.tsx @@ -1,3 +1,4 @@ +import { altKeyDisplay, shortcutDisplay } from "@/lib/shortcuts"; import { useEffect, useMemo, useRef, useState } from "react"; import { DEFAULT_DOCKER_LOG_TAIL, dockerContainerLive } from "@locus/shared"; import { api } from "@/lib/api"; @@ -113,6 +114,7 @@ function DockerContainersPane() { }); }, [parsed, grep, level]); const visibleLogText = visibleLines.map((line) => line.raw).join("\n"); + const logsHaveTime = visibleLines.some((line) => Boolean(line.time)); useEffect(() => { if (!follow) return; @@ -320,10 +322,6 @@ function DockerContainersPane() {
Session
{item.sessionTitle || "—"}
-
-
Status
-
{dockerStatusLabel(item)}
-
{error ? void refreshList()} /> : null}
@@ -388,7 +386,11 @@ function DockerContainersPane() { key={`${index}-${line.raw.slice(0, 24)}`} className={`docker-log-line ${line.level}`} > - {line.time ? {formatLogTime(line.time)} : null} + {line.time ? ( + {formatLogTime(line.time)} + ) : logsHaveTime ? ( + + ) : null} {line.level !== "other" ? ( {levelLabel(line.level)} ) : null} @@ -412,7 +414,9 @@ function DockerContainersPane() { up {compactDockerAge(item.status, item.createdAt)} ) : null} - ⌘C copy ⌘S save ⌥↑ jump to error + {shortcutDisplay("Ctrl/Cmd+C")} copy{" "} + {shortcutDisplay("Ctrl/Cmd+S")} save {altKeyDisplay()}↑ jump to + error + {dockerDown ? null : ( + + )} diff --git a/apps/desktop/src/components/layout/FeatureNudge.tsx b/apps/desktop/src/components/layout/FeatureNudge.tsx index c70a8b7..1d24a8f 100644 --- a/apps/desktop/src/components/layout/FeatureNudge.tsx +++ b/apps/desktop/src/components/layout/FeatureNudge.tsx @@ -6,6 +6,7 @@ import { type ChatNudgeId, } from "@/lib/discoverability"; import { useStore } from "@/state/store"; +import { IconClose } from "./Icons"; import type { AgentSession } from "@locus/shared"; export function FeatureNudge({ session }: { session: AgentSession }) { @@ -45,12 +46,18 @@ export function FeatureNudge({ session }: { session: AgentSession }) { return (
- {nudge.text} + {nudge.text} -
); diff --git a/apps/desktop/src/components/layout/MobileShell.tsx b/apps/desktop/src/components/layout/MobileShell.tsx index 3f313df..43dd01c 100644 --- a/apps/desktop/src/components/layout/MobileShell.tsx +++ b/apps/desktop/src/components/layout/MobileShell.tsx @@ -1698,8 +1698,10 @@ function MobileChat(props: { session: AgentSession; onBack: () => void }) { ? (viewing.lastLine ?? "") : "" : (state.thinking[session.id] ?? ""); + const lastMessage = messages[messages.length - 1]; + const streamedText = lastMessage?.role === "assistant" && Boolean(lastMessage.content.trim()); const localWait = - !viewing && turning && !waitingClone && !waitingBuild && !liveThought + !viewing && turning && !streamedText && !waitingClone && !waitingBuild && !liveThought ? localModelWaitLabel(provider) : undefined; const workingText = waitingBuild diff --git a/apps/desktop/src/components/layout/Sidebar.test.tsx b/apps/desktop/src/components/layout/Sidebar.test.tsx index 7f2f08a..580a365 100644 --- a/apps/desktop/src/components/layout/Sidebar.test.tsx +++ b/apps/desktop/src/components/layout/Sidebar.test.tsx @@ -278,7 +278,7 @@ describe("Sidebar", () => { ); expect(css).toMatch(/\.side-main \{[\s\S]*?container: side-main \/ inline-size/); expect(css).toMatch( - /@container side-main \(max-width: 300px\) \{\s*\.brand-actions \{[\s\S]*?flex-basis: 100%/, + /@container side-main \(max-width: 300px\) \{[\s\S]*?\.brand-actions:has\(\.product-mode-toggle\) \{[\s\S]*?flex-basis: 100%/, ); }); @@ -2050,7 +2050,8 @@ describe("Sidebar", () => { expect(host.querySelector(".repo-item.active .meta")?.textContent).toBe("acme/app · main"); expect(host.querySelector(".repo-item.active .pr-count")?.textContent).toBe("3"); expect(host.querySelector(".repo-item.active .repo-flag")?.textContent).toBe("auto"); - expect(host.querySelector(".side-foot .hint")?.textContent).toBe("2 repos"); + // The header shows the total; the footer only counts search matches. + expect(host.querySelector(".side-foot .hint")).toBeNull(); expect(host.querySelector(".search")?.getAttribute("placeholder")).toBe("Search repos"); expect(host.querySelector(".search-wrap .search-icon svg")).toBeTruthy(); expect(host.querySelector(".repo-item.active [aria-label='More for acme/app']")).toBeTruthy(); @@ -2107,7 +2108,7 @@ describe("Sidebar", () => { }, ], }); - expect(host.querySelector(".brand p")?.textContent).toBe("Slack · Locus Dev"); + expect(host.querySelector(".brand p")?.textContent).toBe("Locus Dev"); expect(host.querySelector(".search")?.getAttribute("placeholder")).toBe("Search channels"); expect([...host.querySelectorAll(".session-group-label")].map((el) => el.textContent)).toEqual([ "Channels", @@ -2593,9 +2594,9 @@ describe("Sidebar", () => { expect( host.querySelector(".side-actions [aria-label='Stop selected containers']"), ).toBeTruthy(); - expect(host.querySelector(".side-actions [aria-label='Refresh containers']")?.textContent).toBe( - "", - ); + expect( + host.querySelector(".side-actions [aria-label='Refresh containers']")?.textContent?.trim(), + ).toBe("Refresh"); }); it("makes Add Grafana the primary until Grafana is connected, then Investigate", () => { diff --git a/apps/desktop/src/components/layout/Sidebar.tsx b/apps/desktop/src/components/layout/Sidebar.tsx index 50c8dff..7925bc3 100644 --- a/apps/desktop/src/components/layout/Sidebar.tsx +++ b/apps/desktop/src/components/layout/Sidebar.tsx @@ -66,7 +66,6 @@ import { clampSplit, stepSplitValue } from "@/lib/splitter"; import { IconAgents, IconChevron, - IconClock, IconGear, IconPlus, IconRefresh, @@ -921,10 +920,8 @@ export function Sidebar() { const primaryAction = state.section === "automations" ? ( - primaryButton( - swarmMode ? "New routine" : "New job", - () => dispatch({ type: "newAutomation", open: true }), - , + primaryButton(swarmMode ? "New routine" : "New job", () => + dispatch({ type: "newAutomation", open: true }), ) ) : state.section === "board" ? ( primaryButton("New board", () => dispatch({ type: "selectBoard", id: null })) @@ -991,7 +988,7 @@ export function Sidebar() { ) : state.section === "docker" ? ( <> {state.dockerView === "containers" && pickedLive.length ? ( ); diff --git a/apps/desktop/src/components/layout/sidebar/SideRail.tsx b/apps/desktop/src/components/layout/sidebar/SideRail.tsx index 85769e4..a1a22f2 100644 --- a/apps/desktop/src/components/layout/sidebar/SideRail.tsx +++ b/apps/desktop/src/components/layout/sidebar/SideRail.tsx @@ -84,6 +84,8 @@ export function SideRail(props: { function showPeek(target: "inbox" | UiSection, el: HTMLElement) { if (!props.collapsed || props.swarmMode) return; + // Hover previews only: on touch a tap fires mouseenter and the flyout would cover the page. + if (!canHover()) return; clearHide(); const rail = el.closest(".side-rail"); const top = el.getBoundingClientRect().top - (rail?.getBoundingClientRect().top ?? 0); @@ -406,6 +408,11 @@ export function SideRail(props: { ); } +function canHover(): boolean { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") return true; + return window.matchMedia("(hover: hover)").matches; +} + export function shortcutCaption(index: number): string | undefined { if (index < 0 || index > 8) return undefined; return `Alt+${index + 1}`; diff --git a/apps/desktop/src/components/projects/ProjectEmpty.tsx b/apps/desktop/src/components/projects/ProjectEmpty.tsx index 1ff2033..d878aaa 100644 --- a/apps/desktop/src/components/projects/ProjectEmpty.tsx +++ b/apps/desktop/src/components/projects/ProjectEmpty.tsx @@ -100,20 +100,28 @@ export function ProjectEmpty() { } /> {hubMode ? ( -
+
) : null} diff --git a/apps/desktop/src/components/settings/AppearanceSettings.tsx b/apps/desktop/src/components/settings/AppearanceSettings.tsx index 55bcd51..2a711c8 100644 --- a/apps/desktop/src/components/settings/AppearanceSettings.tsx +++ b/apps/desktop/src/components/settings/AppearanceSettings.tsx @@ -48,6 +48,7 @@ const FILTERS: { id: ThemeFilter; label: string }[] = [ { id: "light", label: "Light" }, { id: "contrast", label: "High contrast" }, { id: "gradient", label: "Gradient" }, + { id: "terminal", label: "Terminal" }, ]; const THEME_RECENTS_KEY = "locus.themeRecents"; @@ -288,7 +289,7 @@ export function AppearanceSettings(props: { ); } -const THEME_FAMILY_ORDER: ThemeFamily[] = ["dark", "light", "contrast", "gradient"]; +const THEME_FAMILY_ORDER: ThemeFamily[] = ["dark", "light", "contrast", "gradient", "terminal"]; function groupedThemeFamilies( themes: ThemeOption[], @@ -322,6 +323,7 @@ function ThemeCard(props: { className="theme-thumb" data-theme={props.opt.id} {...(props.opt.gradient ? { "data-theme-gradient": "" } : {})} + {...(props.opt.terminal ? { "data-theme-terminal": "" } : {})} aria-hidden="true" >
diff --git a/apps/desktop/src/components/settings/Settings.test.tsx b/apps/desktop/src/components/settings/Settings.test.tsx index a5b6477..73ddd44 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.19.0", + current: "0.20.0", message: "No GitHub release found yet. Check for updates works after a v* tag is published.", })), refreshCursorCli: vi.fn(async () => ({ changed: false, models: [] })), diff --git a/apps/desktop/src/components/settings/Settings.tsx b/apps/desktop/src/components/settings/Settings.tsx index 3846d04..dbe7062 100644 --- a/apps/desktop/src/components/settings/Settings.tsx +++ b/apps/desktop/src/components/settings/Settings.tsx @@ -1527,12 +1527,8 @@ export function Settings() { {(draft.workers ?? []).length === 0 ? ( - - + hint="Paste a locus-worker:add line above, or use Add worker to fill one in by hand." + /> ) : ( (draft.workers ?? []).map((row, i) => ( Same accounts as Settings → Git. Restricted jail allowlists those hosts.

+ {draft.forges.some((f) => f.token) ? ( +
    + {draft.forges + .filter((f) => f.token) + .map((f) => { + const kind = forgeLabel(f.kind); + // Built-in accounts are named after their forge; don't print "GitHub · GitHub". + const name = + f.name.trim() && f.name.trim().toLowerCase() !== kind.toLowerCase() + ? `${kind} · ${f.name.trim()}` + : kind; + return ( +
  • + {name} + + {f.authMethod === "oauth" && f.username + ? `Signed in as ${f.username}` + : "Connected"} + +
  • + ); + })} +
+ ) : ( +

+ No Git accounts connected yet. Add GitHub, GitLab, Bitbucket, Gitea, Forgejo, or + Azure DevOps in Git. +

+ )} - {draft.forges.length ? ( -

- {draft.forges - .map( - (f) => - `${forgeLabel(f.kind)} · ${f.name}${ - f.token - ? f.authMethod === "oauth" && f.username - ? ` · signed in as ${f.username}` - : " · connected" - : "" - }`, - ) - .join(" · ")} -

- ) : ( -

- No Git accounts yet. Add GitHub, GitLab, Bitbucket, Gitea, Forgejo, or Azure DevOps - in Git. -

- )}

Work tools

{draft.managedPolicy?.policy.tokenFreeMode ? (

@@ -2138,7 +2142,7 @@ export function Settings() { setDraft({ diff --git a/apps/desktop/src/components/settings/SettingsCards.tsx b/apps/desktop/src/components/settings/SettingsCards.tsx index ddf11ae..71ac142 100644 --- a/apps/desktop/src/components/settings/SettingsCards.tsx +++ b/apps/desktop/src/components/settings/SettingsCards.tsx @@ -961,7 +961,7 @@ export function MachineCard({ {template.name} - {status} + {status} {cpuCores} c · {memoryMb} MB · {diskSizeGb} GB diff --git a/apps/desktop/src/components/settings/SetupWizard.tsx b/apps/desktop/src/components/settings/SetupWizard.tsx index 1e8cd06..144053c 100644 --- a/apps/desktop/src/components/settings/SetupWizard.tsx +++ b/apps/desktop/src/components/settings/SetupWizard.tsx @@ -285,10 +285,14 @@ export function SetupWizard() { {PRODUCT_MODE_LABELS.swarm} is teammate-first (avatars, Teams, Routines).{" "} {PRODUCT_MODE_LABELS.developer} is the full control plane. You can switch anytime. -

+