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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to Atlas (CLI, TUI, MCP server and Atlas Desktop). Versions
- **Capture: screenshots, screen recordings and GIFs from a hotkey, in any app.** ⌃⌥3 captures the screen under the pointer, ⌃⌥4 drags a region (Space switches to a window; a magnifier and the pixel size follow the pointer), ⌃⌥5 picks a window, ⌃⌥6 records a region to mp4 (3-2-1 countdown, pause, stop, 10-minute limit) and ⌃⌥7 records a GIF (all changeable, with presets, in Settings → Capture; none is a macOS system key). The file is saved to `~/Pictures/Atlas Captures` (or your folder), copied to the clipboard and shown as a quick card in the corner: **Copy**, **Save As**, **Annotate**, **Send to agent** (attaches it to the dock's composer, which sends the path), **Reveal**, **Discard**, drag it into Finder, Mail or Slack, or turn a recording into a GIF with a size estimate. **Annotate** opens an editor (arrow, rectangle, text, highlight, blur, crop) that saves a `(edited)` copy next to the original and never overwrites it. Settings → Capture shows the Screen Recording permission and the history. Atlas's own capture windows and the dock are never in the picture. macOS only for now; the first capture asks for Screen Recording and macOS applies it after a restart.
- **Open anything with `atlas`, no setup.** `atlas .`, `atlas <folder>` and `atlas <file>` open it in Atlas Desktop (a file opens its project, then the file; the app is started if it is not running). `atlas.yaml` is now optional: in a folder without one, `atlas dev` (also `--detach`, `--headless`, `--ci`), `atlas down` and `atlas mcp` detect the services from `package.json`, `Cargo.toml`, `pyproject.toml` or `go.mod` in memory, print one line saying so and writing nothing; `atlas init` saves exactly that config. A subcommand always wins over a folder of the same name (`atlas ./dev` opens the folder); a typo like `atlas devv` still fails with "unrecognized subcommand" and the suggestion.
- **`atlas -- <cmd…>` runs a command as a service of this folder's stack.** `atlas -- bun dev` is supervised like any service (logs in `.logs/`, port probe, restart from the TUI/Desktop/MCP) without touching `atlas.yaml`: it joins a running background runner live, or starts the stack in the background with it. Named from the command (`bun-dev`, de-duplicated).
- **`atlas update` updates system packages (Arch, Debian/Ubuntu, Fedora/openSUSE).** It finds the owner with `pacman -Qo`, `dpkg -S` or `rpm -qf`, always reports installed and latest with `--check`, downloads the matching `.pkg.tar.zst`, `.deb` or `.rpm`, verifies its SHA-256 **before** asking for privileges, installs it through `sudo` (`pkexec` without a terminal) after a confirmation (`--yes` skips it) while holding the update lock, then refreshes skills and agent setups with the installed binary. The daily notice and exit banner fire for package installs and print the package manager's command (#144).

### Changed

Expand All @@ -34,6 +35,13 @@ All notable changes to Atlas (CLI, TUI, MCP server and Atlas Desktop). Versions
- **Windows: the tray icon was a solid black dot.** The notification area never tints icons, so the black macOS template showed as a disc; it now uses the full-colour Atlas icon. Also on Windows: "Reveal in File Explorer" instead of "Reveal in Finder", Windows fallbacks in the default font stacks (Segoe UI, Cascadia Mono, Consolas), `Ctrl` key hints instead of ⌘ glyphs in the sidebar, welcome and custom-agent texts, and no `afplay` call on notifications.
- **Windows: the embedded browser no longer swallows Atlas's shortcuts.** With a browser tab focused, Ctrl+1…9, Ctrl+Tab, Ctrl+PgUp/PgDn, Ctrl+Shift+T/W, Alt+1…9 and your remaps run their commands (the forwarding macOS already had), while Ctrl+F, F5, Ctrl+C and Ctrl+V stay with the page. Linux browser tabs still take every key while focused.
- **Windows: F5, Ctrl+R, Ctrl+F, Ctrl+P and F12 no longer reload Atlas or open Edge UI.** WebView2's browser accelerator keys are off for the app window and the tray popover (Atlas's own keymap still receives every key); embedded browser tabs keep them.
- **Updating Atlas Desktop can no longer leave you without the app.** macOS: the old `Atlas.app` is kept as `.Atlas.app.old` until the new one is in place, and an update killed between the two renames is repaired by the next `atlas` or `atlas desktop` run; half-copied staging folders are removed on every failure (a full disk used to leave them). Linux: the previous AppImage is kept until the new one is installed and its version recorded, and restored if either fails. Windows: the install folder is moved aside before the installer runs and restored when the installer fails, leaves no app, or leaves a different version than the one wanted.
- **`atlas update` says when it left a linked Atlas Desktop alone.** With the app linked to a dev build (`atlas link --desktop`) and the CLI not linked, one line now says the app was not updated and how to unlink.
- **Update checks and downloads survive a flaky network, and never answer with the wrong release.** If the release feed cannot be read, a beta or alpha check now fails (and keeps the last known result) instead of quietly reporting "Stable", and a hiccup while confirming that the newest build is published no longer makes `atlas update` skip it for an older one: only an explicit "not found" does. Requests time out on connect and retry on transient errors; downloads have no 2-minute total cap any more (slow links and the large desktop bundle used to fail), they are stopped only when they stall, and curl resumes them.
- **A failed Desktop sync no longer leaves skills and agent hooks on the version the CLI was rolled back from.** `atlas update` now refreshes skills, MCP entries and hooks only after the app followed, and when it did not, the restored previous binary refreshes them.
- **`atlas update` no longer overwrites a dev build, and no longer fails half way on permissions.** A debug build or an `atlas link` binary is refused with the same rule the `atlas dev` prompt uses; before downloading it checks that the CLI's folder and the installed app are writable and names the one that is not, instead of replacing the CLI and rolling it back.
- **The update cache survives crashes, damage and an offline start.** `~/.atlas/.update_check` is written atomically, a damaged file keeps your "not now" answer, and a failed check is stamped so an offline `atlas dev` start no longer waits two seconds for the network every time (it retries after 30 minutes).
- **Atlas Desktop no longer says "Atlas is up to date" after an update that installed nothing.** For Homebrew and system-package installs, `atlas update` reports `managed` (`@@atlas managed …`, and `action` `managed` or `package` in `--check --json`) and the app shows how to update instead.

## [1.5.0-alpha.23] — 2026-10-26

Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/lib/stores/updates.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
EMPTY_PREFS,
EMPTY_PROGRESS,
blurbOf,
finishOutcome,
finishToast,
justUpdated,
parseChannelState,
parseChannelViews,
Expand All @@ -32,6 +34,7 @@ import {
type ChannelAction,
type ChannelState,
type ChannelView,
type InstallOutcome,
type ProgressEvent,
type ReleaseNotes,
type UpdatePrefs,
Expand Down Expand Up @@ -71,7 +74,7 @@ class Updates {
checking = $state(false);
running = $state<null | Run>(null);
/** How the last install ended; drives the step list. */
outcome = $state<"running" | "done" | "failed">("running");
outcome = $state<InstallOutcome>("running");
log = $state<string[]>([]);
/** Why the last install failed (the CLI's last line); null until it does. */
failure = $state<string | null>(null);
Expand Down Expand Up @@ -353,10 +356,19 @@ class Updates {
this.#structured = false;
try {
const res = await invoke<boolean | null>(cmd, args);
if (res === true) this.relaunchNeeded = true;
this.outcome = "done";
if (!this.whatsNewOpen)
toast.success(kind === "unlink" ? "Back on the release build" : "Atlas is up to date");
// Progress events and the command result travel on separate channels: let the last
// `@@atlas` lines land before deciding how the run ended.
const { promise: landed, resolve } = Promise.withResolvers<void>();
setTimeout(resolve, 0);
await landed;
const ended = finishOutcome(this.progress);
this.outcome = ended;
if (ended === "done" && res === true) this.relaunchNeeded = true;
// The sheet has no copy for a managed install, so its toast is shown either way.
if (ended === "managed" || !this.whatsNewOpen) {
const t = finishToast(kind, ended, this.progress.managed);
toast[t.level](t.text);
}
await this.check();
// The app bundle itself may have been replaced even when we could not detect it.
if (kind === "reinstall") this.relaunchNeeded = true;
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src/lib/updates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
prereleaseBadge,
dateOf,
formatBytes,
finishOutcome,
finishToast,
isSkipped,
justUpdated,
parseNotes,
Expand Down Expand Up @@ -385,6 +387,49 @@ describe("progress reducer", () => {
expect(phaseFromLog("◆ Downloading Atlas-1.0.2-aarch64.app.tar.gz...")).toEqual(phase("update-app"));
expect(phaseFromLog("hello")).toBeNull();
});
test("a managed install is remembered with who manages it", () => {
expect(run({ kind: "managed", how: "homebrew" }).managed).toBe("homebrew");
expect(run({ kind: "managed", how: "package" }).managed).toBe("package");
expect(EMPTY_PROGRESS.managed).toBeNull();
});
test("a managed event does not move the phases", () => {
const p = run(phase("download"), { kind: "managed", how: "package" });
expect(p.at).toBe(0);
});
});

describe("how a finished install ends", () => {
const run = (...events) => events.reduce(reduceProgress, EMPTY_PROGRESS);

test("an exit-0 run that installed nothing because another tool owns Atlas is `managed`, never `done`", () => {
expect(finishOutcome(run({ kind: "managed", how: "homebrew" }))).toBe("managed");
expect(finishOutcome(run({ kind: "managed", how: "package" }))).toBe("managed");
});
test("any other exit-0 run is `done`", () => {
expect(finishOutcome(EMPTY_PROGRESS)).toBe("done");
expect(finishOutcome(run({ kind: "phase", id: "install-cli" }))).toBe("done");
});
test("the toast never says `up to date` for a managed install", () => {
const managed = finishToast("update", "managed", "homebrew");
expect(managed.text).not.toMatch(/up to date/i);
expect(managed.text).toMatch(/brew upgrade atlas/);
expect(managed.level).toBe("info");
expect(finishToast("update", "managed", "package").text).toMatch(/package manager/i);
expect(finishToast("update", "done", null)).toEqual({ level: "success", text: "Atlas is up to date" });
expect(finishToast("unlink", "done", null).text).toBe("Back on the release build");
});
test("a managed run is not a finished install: no relaunch, steps stay un-ticked", () => {
const p = run({ kind: "managed", how: "homebrew" });
expect(states(stepsFrom(p, "managed"))).toEqual([
"pending",
"pending",
"pending",
"pending",
"pending",
"pending",
]);
});
const states = (s) => s.map((x) => x.state);
});

describe("dateOf", () => {
Expand Down
54 changes: 47 additions & 7 deletions apps/desktop/src/lib/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ export type ProgressEvent =
| { kind: "phase"; id: string }
| { kind: "bytes"; done: number; total: number }
| { kind: "rollback"; state: string }
| { kind: "host"; state: string; live: number };
| { kind: "host"; state: string; live: number }
| { kind: "managed"; how: string };

/** What the install has reported so far. */
export interface Progress {
Expand All @@ -171,9 +172,18 @@ export interface Progress {
rollback: "restored" | "failed" | null;
/** How `atlas update` left the terminal host (`needs-restart` = running terminals, user decides). */
host: { state: string; live: number } | null;
/** Another tool owns this Atlas (`homebrew`, `package`): the CLI exited 0 without installing. */
managed: string | null;
}

export const EMPTY_PROGRESS: Progress = { at: -1, done: 0, total: 0, rollback: null, host: null };
export const EMPTY_PROGRESS: Progress = {
at: -1,
done: 0,
total: 0,
rollback: null,
host: null,
managed: null,
};

/** Fold one event into the progress; phases only move forward, bytes belong to the current phase. */
export function reduceProgress(p: Progress, e: ProgressEvent): Progress {
Expand All @@ -189,6 +199,8 @@ export function reduceProgress(p: Progress, e: ProgressEvent): Progress {
return { ...p, rollback: e.state === "restored" ? "restored" : "failed" };
case "host":
return { ...p, host: { state: e.state, live: e.live } };
case "managed":
return { ...p, managed: e.how };
}
}

Expand All @@ -203,17 +215,22 @@ export function phaseFromLog(line: string): ProgressEvent | null {
return null;
}

/**
* How the last install ended. `managed`: the CLI exited 0 but installed nothing because another
* tool owns this Atlas (Homebrew, a package manager with no usable command), so it is neither a
* success nor a failure and no step ran.
*/
export type InstallOutcome = "running" | "done" | "failed" | "managed";

/**
* The step list for the sheet and Settings. `failed` marks the active step; `done` completes
* every step except Relaunch, which is only done once the app restarts (`relaunched`).
* `managed` leaves every step pending: nothing was installed.
*/
export function stepsFrom(
p: Progress,
outcome: "running" | "done" | "failed",
relaunchNeeded = false,
): Step[] {
export function stepsFrom(p: Progress, outcome: InstallOutcome, relaunchNeeded = false): Step[] {
return STEPS.map((s, i) => {
let state: StepState;
if (outcome === "managed") return { ...s, state: "pending" };
if (s.id === "relaunch") state = outcome === "done" && relaunchNeeded ? "active" : "pending";
else if (outcome === "done") state = "done";
else if (i < p.at) state = "done";
Expand All @@ -225,6 +242,29 @@ export function stepsFrom(
});
}

/** A run that exited 0: `managed` when the CLI said another tool owns this install, else `done`. */
export function finishOutcome(p: Progress): "done" | "managed" {
return p.managed ? "managed" : "done";
}

/** The toast a finished run shows outside the sheet; a managed install is never "up to date". */
export function finishToast(
kind: "update" | "reinstall" | "unlink",
outcome: "done" | "managed",
managed: string | null,
): { level: "success" | "info"; text: string } {
if (outcome === "managed") {
return {
level: "info",
text:
managed === "homebrew"
? "Atlas is managed by Homebrew: run `brew upgrade atlas` to update it"
: "Atlas is managed by your package manager: update it there (or run `atlas update` in a terminal)",
};
}
return { level: "success", text: kind === "unlink" ? "Back on the release build" : "Atlas is up to date" };
}

/** `42%` of a known download; null while the size is unknown. */
export function percent(p: Progress): number | null {
return p.total > 0 ? Math.min(100, Math.round((p.done / p.total) * 100)) : null;
Expand Down
Loading
Loading