Commit Graph
63 Commits
Author SHA1 Message Date
821ae58905 Reorder the agents in the panel, count Grok's tokens, and install Grok through mise (#14004)
* Let the agents in the panel be put in any order

Drag an agent by its mark to move its section; the header it will land
on lights up, and the move happens on release. Each agent's header is
now a keyboard stop with its own highlight, and Ctrl+Up/Down moves the
agent the cursor is in. The order is kept in agents/order.json beside
the usage records. The key catcher turns Ctrl+Up/Down into a reorder
only for panels that opt in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Light only an agent's mark when the cursor or a drag is on it

The mark is the handle the agent moves by, so the keyboard cursor and
the drop spot while dragging box it alone rather than the header line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep a lit agent mark's box from being clipped at the panel's edge

The box overhangs the content's left edge, which the scrolling area
clipped. The scrolling area now reaches a little into the panel's
padding with the content shifted back, so nothing moves and the box
draws whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reuse a Grok session scan only on the day it was made

A limits-only refresh just after midnight reused a scan from the evening
before, which counted yesterday's sessions as today's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep Ctrl+Up/Down from moving an agent when the cursor is outside one

The hero's buttons and the starter tiles carry indices too, and the
lookup read them as accounts, so with several accounts Ctrl+Up/Down on
one of them moved an unrelated agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Report a failed mise update even after the Grok upkeep runs

The Grok block ran after `mise up` and its last command set the script's
status, so a failed tool update could read as a success to callers that
warn about it. The update's own status is now the script's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Unpack a Grok update into ~/.grok whatever GROK_HOME says

The upkeep replaces ~/.grok's link, but the npm launcher unpacks into
GROK_HOME when it's set, so with a custom home the link never came back
at the new release and the old one was restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hold off reordering agents while an account name is being edited

Ctrl+Up/Down reached the key catcher during an inline rename, and moving
the agent rebuilt its section, dropping the unfinished name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Install Grok through mise's first-party package

The npm launcher keeps an old ~/.grok/bin binary because mise skips its
postinstall. Use mise's grok tool, and drop the npm tool so its shim
does not stay ahead of the stub.

* Drop the npm Grok workarounds now that mise installs Grok itself

With Grok installed through mise's first-party package, the CLI is the
binary mise manages, so the update upkeep that repointed ~/.grok/bin and
the shared bin directory for added Grok accounts have nothing left to do.
omarchy-update-mise is back to running mise up and nothing else, and
adding a first Grok account installs the same package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count Grok's tokens and prompts from its usage ledger

Each Grok session keeps a usage.json, the ledger `grok usage` prints, with
every finished turn's end time and tokens by model. The collector now
reads it for tokens today, by day for the last week, and by model, with
cached input kept apart, and counts today's prompts by the turns that
ended today. Found in #12352's research.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Count cache writes in Grok's daily token totals

Grok's totalTokens leaves cache writes out while the per-model buckets
count them, so a turn with cache writes added less to its day than to
its model. The day's total is now the sum of those same buckets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove the npm launcher's old Grok binaries when moving to mise's Grok

Omarchy's npm wrapper ran the binary the launcher unpacked into
~/.grok/bin, where x.ai's installer also puts a copy with a PATH entry
ahead of mise. Once the wrapper is replaced, a binary left there would
keep shadowing the mise tool, so the migration removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jesse Miller <jmiller@jmiller.com>
2026-10-02 03:34:05 +02:00
David Heinemeier HanssonandClaude Opus 5.5 b18ab4952b Add a no-animations mode, on by default in VMs (#13550)
A machine without a GPU, like most VMs, renders through llvmpipe on the
CPU, where every animated frame and every translucent window costs. In a
VM, opening and closing a terminal took ~5s of CPU; a panel ~3.4s.

omarchy toggle animations (also under Toggle > Animations) places a Hyprland
flag that turns off animations, blur and shadows and makes windows opaque.
The shell follows Hyprland's animations:enabled, rereading it on every
config reload: its one-shot animations run for Style.duration(ms), which
is then 0, and its spinners, pulses and title marquee hold still. A new
install in a VM starts with the flag in place.

Measured in the ISO test VM (llvmpipe), CPU per interaction:
terminal open+close 5000ms -> 481ms, workspace switch 1670ms -> 409ms,
audio panel 3446ms -> 796ms, volume OSD 2324ms -> 584ms, menu 2028ms ->
1241ms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:05 +02:00
7eb818e37b Install Hermes for the default agent as the desktop app's self-updating runtime
Choosing Hermes as the default agent built it through mise: a pipx environment with no checkout, so `hermes update` had nothing to move, and the only Hermes that could update itself was the one Hermes Desktop set up. Both paths now run the same setup. omarchy-install-hermes-cli installs the hermes-desktop package and runs upstream's installer from it, pinned to the packaged release and started on main, exactly as Install > AI did; omarchy-install-ai-hermes is that plus opening the app. The terminal, the default agent and the app share one runtime, and it updates itself.

--check answers whether --now has anything left to do, not merely whether a hermes runs: choosing Hermes from the menu asks first and opens a terminal only on a no, so a yes has to mean no minutes-long step would run where nobody can see it. With the app installed that means the runtime's own command, its completion marker and the seeded packaged app; a finished runtime whose command is gone, somebody else's, or its own but unable to run gets it back from upstream's path stage without bootstrapping again. Either way the command has to be the one PATH finds, because omarchy-agent runs bare `hermes` and Omarchy puts mise's shims ahead of ~/.local/bin; a command in the way is named rather than installed over. The modes are named outright because the app's launcher used to call this command with no arguments to reconcile a mise copy; a default of --now would turn every launch into an install. --check still refuses to run the retired wrapper, since running it built Hermes through mise, and a machine whose migration is pending can still have it on PATH.

Provisioning no longer writes the wrapper, Remove Preinstalls no longer looks for it, and the wrapper, the environment it built and what proves them Omarchy's are known to the installer alone: --retire-mise is the migration's whole job, and --now runs the same removal once the runtime installer has saved the wrapper aside, so a user who chose Hermes before their migration ran is not left with mise's shim answering `hermes`. Only the wrapper proves the environment is Omarchy's, at its path or in that saved copy, so the environment goes first and the wrapper last, judged by mise neither having it installed nor still requesting it; a removal that leaves either behind, or a listing that cannot be read, mise missing included, stops with the commands to finish by hand and leaves the migration pending. The migration that once installed the wrapper is kept as a no-op for late updaters, and one whose default agent was Hermes is told to choose it again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-09-21 19:26:02 -05:00
David Heinemeier Hansson b423f4993d Complete OWE service setup and lock feed fallback 2026-09-20 20:36:19 -05:00
David Heinemeier Hansson f2cf3ce9d2 Merge pull request #11656 from omacom/loosen-offline-node-pin
Loosen the offline Node pin so mise up tracks new releases
2026-09-18 08:03:00 -04:00
Erik Melton a73bcbfc0a Remove unsafe project bin PATH injection (#11336)
* Remove unsafe project bin PATH injection

* Cover customized unsafe Mise paths

* Revoke legacy Mise Work trust

* Harden legacy Mise trust cleanup

* Preserve ignored Mise Work configs

* Scope Mise path cleanup to env

* Accept paranoid Mise ignore marker

Reported-by: infosec-us-team
2026-09-15 18:02:17 +02:00
David Heinemeier Hansson a389bd1852 Add Cloudflare CLI lazy wrapper 2026-09-13 16:59:33 +02:00
David Heinemeier HanssonandClaude Fable 5 5db4a40195 Loosen the offline Node pin so mise up tracks new releases
Offline installs unpack the bundled tarball and pin Node to its exact
version, since latest can't be resolved without network. But nothing ever
loosened that pin, so Node stayed frozen at the ISO's version and mup
skipped it forever, while online installs tracked latest.

Rewrite the pin to latest right after registering the bundled version:
mise resolves latest to the installed version while offline (verified
with no network and an empty cache), and the first mise up with network
picks up new releases just like an online install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LseZ1jcLaFBnndRnW4yb5
2026-09-13 12:11:00 +02:00
David Heinemeier HanssonandClaude Opus 5 5ead870507 Add basecamp (basecamp-cli) as a lazy-installed mise tool (#10943)
Mirrors the hey-cli stub: the wrapper in ~/.local/bin installs and
upgrades through mise on first run, so the CLI tracks releases instead
of going stale as a manually dropped binary.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 15:13:43 +02:00
Ryan Hughes 0973169098 Merge pull request #7591 from omacom/mise-shim-preserve-argv0
Keep mise upgrades from pruning versions still in use
2026-09-07 12:57:43 -04:00
0d223fe820 Add Muse Code as a default coding agent (#9915)
* Add Muse Code as a default coding agent

Meta ships Muse Code only as a binary, so it installs from the AUR
(muse-code-bin) instead of mise, and a fresh install runs the muse login
browser flow in the install terminal before the agent opens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148qKzr366p2Ubu2igCLvPg

* Refine Muse Code menu and prompt forwarding

* Install Muse Code from OPR

* Install Muse Code through mise's HTTP backend

* Preinstall the Muse mise stub

* Use the shared Muse installation flow

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-09-06 22:29:16 +02:00
a62e34ea8e Add Cursor CLI as a coding agent choice (#10465)
* Add Cursor CLI as a coding agent choice

* Launch Cursor CLI through its agent subcommand with --trust

Cursor CLI still dispatches a one-word prompt that names one of its
subcommands (update, login, help) even after a bare --, so name the agent
subcommand outright and pass the prompt behind -- there, where it also
keeps a prompt starting with a dash from being read as an option. --yolo
only auto-allows commands; the workspace trust dialog is skipped only by
--trust, and a launcher that must not stop to ask needs both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Leave an official Cursor CLI install alone

Cursor's own installer symlinks ~/.local/bin/cursor-agent, the same path the
mise wrapper takes. The migration now installs the wrapper only when no
cursor-agent command exists, and Remove Preinstalls deletes the path only
when it holds the wrapper omarchy-mise-install wrote, the way the Hermes
wrapper is handled. Selecting the agent treats an executable at that path
other than the wrapper as the user's own install and skips mise, since the
mise shims precede ~/.local/bin on PATH and a mise copy would only shadow
it. The wrapper resolves through mise's registry, which lists cursor-agent
from 2026.8.15 on.

The tests write a real cursor-agent stub before Remove Preinstalls runs,
cover the preinstall opt-out for the new migration, and check that a
symlinked official install survives removal and selection alike while a
dead file at the same path still installs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Give Cursor its brand mark and one name in the menu

Add Cursor's mark to the Omarchy icon font as U+E90D and point the agent
entry and both editor entries at it, so one brand is drawn one way across
the menu. Label the agent entry "Cursor CLI", the name the command and the
manual already use, and spell it the same in the migration and the tests.
Append the manual row after the others at the standard width.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Keep an official Cursor CLI install through a user re-provision

User setup writes every mise wrapper unconditionally, which is fine on a
fresh install but replaces the symlink Cursor's own installer leaves at the
same path when omarchy-provision-user runs again with --force. Guard that
one line the way the migration does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: John Cavanaugh <59479+cavanaug@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 17:07:40 +02:00
David Heinemeier HanssonandClaude Opus 5 43d2fffaf0 Keep user setup running when Hermes cannot install
install/user/mise.sh is sourced through run_logged under `bash -eE`, and its
status reaches omarchy-provision-user's `set -euo pipefail`. Every other line
in the file writes a mise stub and cannot fail; omarchy-install-hermes-cli can,
and does whenever hermes-desktop is installed but the app has not been launched
yet -- what a second user on a shared machine meets on their first login.

The rest of provisioning runs after that source: refreshing applications, the
default browser, the mailto handler, the first-install migration markers and
the finalize-user marker. Without the marker the whole step retries and fails
again at every login, and omarchy-provision-first-run calls it with `|| true`,
so nothing surfaces. omarchy-install-ai-hermes and the migration already guard
this call the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:47:53 +02:00
a12a21c02f Add Hermes as a desktop app and a coding agent
Hermes joins Install > AI as a desktop app, sits beside it under Remove > AI,
and becomes a choice in the default-agent list. The CLI installs through
omarchy-install-hermes-cli rather than a bare `mise use`, so its interpreter
is pinned before mise builds it.

Rebased onto quattro. Ori claimed U+E909 in #7709 while this branch was open,
so the Hermes mark moves to U+E90A in the icon font, the menu entries, the
font README, and the charset the menu test pins. The glyph outline itself is
unchanged; it is spliced in beside Ori rather than over it.

Co-Authored-By: witcheer <witcheer.eth@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SySdB3RtCA8BNv6Am246BP
2026-08-26 18:04:41 +02:00
Ryan Hughes bf2013e6f3 Make --exec take the command as rest-of-line words
Replace --exec-arg with an ergonomic --exec that consumes the rest of the line
as the click command. The caller's shell tokenizes the words into discrete
arguments before the tool sees them, and the shell runs them as positional
parameters (never a re-parsed string), so safety is identical to the argv form
while the call sites read naturally: `--exec omarchy toggle something`.

Crucially the tool never splits a string itself — a single quoted whole-command
argument is rejected and points at the unquoted form, because whitespace-
splitting a string hands argument boundaries to whoever controls its content
(the injection we are avoiding). --exec must come last; migrate every caller.
2026-08-23 14:26:25 -04:00
Ryan Hughes eb988b42e6 Remove --exec entirely; --exec-arg is the only click-command form
A free-form shell-string --exec sitting next to the safe --exec-arg is a
standing invitation for the next caller to interpolate untrusted data and
reintroduce the RCE. Remove it: omarchy-notification-send --exec now errors and
points at --exec-arg, and the shell drops the omarchy-exec string hint and its
bash -lc execution path, leaving only the argv path.

Migrate the remaining string callers (the first-run invitation hooks, wifi and
welcome prompts) to --exec-arg, and update their notification mocks. Trim the
verbose security comments added along the way.
2026-08-23 13:35:02 -04:00
OmarchybotandCodex XHigh 1cc5c72e2c Add Ori as a lazy-loaded agent and a default-agent choice (#7709)
Ori is OpenRouter's harness: `ori claude`, `ori codex` and `ori opencode` start those agents against OpenRouter's model catalogue, and `ori code` is Ori's own agent. That last one is what the default-agent entry launches, bare — Ori has no approval prompt to skip, so there is no "don't stop to ask" flag to pass it the way the other agents get one.

The package is `github:OpenRouterLabs/ori-releases`, because upstream ships prebuilt binaries as release assets and publishes nothing to npm. mise's `github` backend picks the right asset per platform and verifies GitHub's artifact attestations on the way in; `ubi` resolves the same release but is deprecated for removal in mise 2027.1.

The menu glyph at U+E909 is OpenRouter's own mark. Ori publishes no logo of its own and its product page renders that one, so there was no Ori-specific mark to prefer over it.

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-23 11:44:23 +02:00
ed7bae4ac5 Replace Gemini coding agent with Antigravity (#6900)
* Replace Gemini coding agent with Antigravity

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Remove the dead Gemini mise wrapper in the Antigravity migration

Remove Preinstalls no longer lists gemini, so the wrapper Omarchy created
would have stayed in ~/.local/bin with nothing left to clean it up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Install Antigravity when it is the default a Gemini user is migrated onto

The opt-out check skipped the install but the rewrite ran anyway, so anyone
who had removed the preinstalls was left with a default agent naming a
command that is not there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix Antigravity skill provisioning and Gemini wrapper migration

- Wires Omarchy's default skills into Antigravity by linking them to ~/.gemini/config/skills/ in bin/omarchy-provision-user and migrations/1786719479.sh.
- Fixes the Gemini wrapper migration in migrations/1786719479.sh to recognize and remove wrappers containing either `mise use -g "gemini"` or `mise use -g --quiet "gemini"`, while leaving hand-written wrappers intact.
- Adds regression tests for both skill provisioning and wrapper removal in test/shell.d/default-agent-test.sh and test/shell.d/provision-user-test.sh.

* Stop the provisioning test from retheming the session it runs in

The test ran the real omarchy-provision-user, which sources install/user/all.sh and so reached omarchy-theme-set: hyprctl reload against the live compositor, gsettings against the live desktop, and a global Node install, none of which the skill symlinks it asserts need. Its mocks for omarchy-done and omarchy-refresh-applications were shadowed anyway, because provisioning prepends $OMARCHY_PATH/bin ahead of them, so stubbing the install suite at its own path is what a mock cannot do here. The exit status is checked rather than discarded: the assertion held even when provisioning died outright, because the symlinks are made twenty lines before the suite runs.

* Match the Gemini default and wrapper the way Omarchy writes them

The migration decided both questions differently from the code that owns them. It read the default agent with grep -qxF, while omarchy-default-agent takes the first line through read, so a padded "  gemini  " that the launcher still resolves was left naming an agent the launcher no longer supports. The wrapper it deletes was matched anywhere in the file, so a hand-written one that only mentions the installer's line in a comment went with Omarchy's own. Reading it the launcher's way and anchoring the match settles both against whoever wrote the file. The skills loop guards its glob the way migrations/1786539345.sh does, so an empty source cannot leave a symlink named "*" behind a migration already marked complete.

Co-Authored-By: Codex XHigh <noreply@anthropic.com>

* List Antigravity among the skill directories

The manual named Claude Code, Codex, Pi and the generic location; provisioning now links ~/.gemini/config/skills too.

Co-Authored-By: Codex XHigh <noreply@anthropic.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Omabot <omabot@omarchy.org>
2026-08-20 22:28:31 +02:00
Stanko Krtalić cf29ce1867 Add hey (hey-cli) as a lazy-installed mise tool (#7626) 2026-08-20 21:13:27 +02:00
David Heinemeier HanssonandClaude Opus 5 889b22dd85 Keep mise upgrades from pruning versions still in use
mise ships with upgrade.auto_prune on, so `mise up` deletes the old
install directory as soon as a tool upgrades. Long-running processes are
still executing out of that directory, and some re-exec themselves by
absolute path, so running `omarchy update` or `mup` breaks them mid-session.

Claude Code is the case that surfaced it. It records its own binary path
at startup and dispatches its bundled ugrep and bfs through it, so once
the upgrade pruned that path its grep and find wrappers fell back to the
mise wrapper in ~/.local/bin. That wrapper cannot stand in for the real
binary here: a shebang script never sees the argv[0] it was invoked
under, because the kernel hands the interpreter the script path instead.
The dispatch collapsed to plain `claude -G`, which fails.

Keeping the old versions costs some disk and leaves the stale directory
behind until something prunes it deliberately, which is the cheaper half
of the trade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:45:12 +02:00
David Heinemeier Hansson 2cc3510d2a Offer an AI diagnosis when a process crashes (#6746)
* Offer an AI diagnosis when a process crashes

systemd-coredump journals every core dump under a known MESSAGE_ID with the
crashing program, pid, and signal as structured fields. omarchy-crash-watch
follows that stream and raises a "Process crashed: <program>" toast; clicking it
opens omarchy-agent-crash, which briefs the default agent on the crash.

The toast goes through omarchy-notification-send --exec rather than a libnotify
action, because the shell runs clicks from its own omarchy-exec hint and never
emits ActionInvoked. It keeps the default "omarchy-action" app name too, the
only one shouldBypassDnd() lets through -- a crash being the last notification
worth swallowing. It stays quiet until an agent is configured, since a
diagnosis is all it offers.

The method lives in a diagnose-crash skill rather than the prompt, so it is
edited in one place and works with whichever agent is default. It covers
investigating the core, and reporting a confirmed Omarchy bug upstream: scoped
to bugs Omarchy controls, searched for duplicates first, only with the user's
agreement, and signed with the model and harness that produced it.

A migration reaches existing installs, whose skill symlinks and unit enablement
would otherwise sit behind one-time setup paths.

* Let the diagnosis clean up the core it extracted

"Do not modify or delete anything" contradicted the symbolization step right
above it, which writes a core to a temp file and deletes it on exit. Read
literally, the core survives -- and the same section warns it holds passwords
and tokens. The prohibition is about the system, not about your own scratch.

* Do not spend a crash toast on a dead notification server

The shell owns org.freedesktop.Notifications, so its own crash takes the
notification server down with it -- and a shell crash is exactly what you want
told about. The toast was sent once into that gap and the dedupe window was
recorded regardless, so the rest of the crash loop went quiet for a minute and
`journalctl -n 0` never replays what was missed.

It now waits for the restarted shell to reclaim the bus name, as
omarchy-migrate-notify already does, and only a delivered toast starts the
dedupe window.
2026-08-12 18:37:40 +02:00
David Heinemeier Hansson 9502b81f3b Reshape the agent launcher into omarchy agent (#6757)
* Reshape the agent launcher into omarchy agent

omarchy-launch-agent becomes omarchy-agent, with prompts on omarchy-agent-prompt
rather than the bare route: `omarchy agent` is both a command and a group, so a
positional prompt there would shadow any subcommand under it. The launcher takes
flags only and points at `omarchy agent prompt` when handed one.

Every agent window now launches under a fixed org.omarchy.agent app-id instead of
omarchy-launch-tui's default of org.omarchy.<binary>, so one rule floats them all
whichever agent is default.

Omarchy also stops picking an agent for you. omarchy-default-agent prints nothing
until one is chosen, leaving every entry under Setup > Defaults > Agent unchecked,
and a first-run invitation offers to take you there.

* Wordsmith

* Cover the agent routes and the invitation

The route split is the point of the change, so exercise `omarchy agent`,
`omarchy agent prompt`, and a rejected positional prompt through the router
rather than only the binaries behind them.

The invitation gets the same treatment as the Voxtype and fingerprint ones: it
notifies once, opens the agent defaults menu, and leaves both the notification
and the marker alone for anyone who already chose an agent.

* Offer the agent choice from the keybinding

Super + Shift + Ctrl + A now runs `omarchy-agent --pick`, which opens Setup >
Defaults > Agent when nothing is chosen yet. A keypress that writes to stderr
and opens nothing just looks broken.

* Reach existing installs with the agent invitation

first-run installs the invitation hook, and existing accounts marked it complete
long ago, so they would never see it -- while being the accounts most likely to
need it, since the old getter returned opencode implicitly and most have no
agent recorded at all. Post-update hooks run later in the same update, so the
invitation arrives without waiting for another one.

* Say what the Defaults submenus set

Setup > Defaults lists Agent, Browser, Terminal, Editor, but the header inside
each repeated the same bare word, which reads as a category rather than a
setting -- and says nothing at all when the menu is summoned straight into it.
The list keeps its short labels; the headers now name the setting.
2026-08-12 17:56:19 +02:00
David Heinemeier HanssonandClaude Opus 5 1c9dfc55f4 Greet the first login with a keybindings toast again
Opening the cheatsheet outright put a menu in front of someone who had not
asked for one, and it blocked first run until they dismissed it. Go back to a
toast that opens the same menu when clicked.

The body carries real newlines now. It was written with a literal \n, which the
card renders as the two characters rather than a line break.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 05:32:47 -07:00
David Heinemeier Hansson 9ea9f804cd Missed the glyph 2026-08-11 12:16:25 +02:00
David Heinemeier HanssonandClaude Opus 5 f97ba7375e Wait for a connection before prompting to update
A ping at hyprland.start answers for a machine that has not finished coming up.
Ethernet is still negotiating DHCP, so a working desktop was told to set up
Wi-Fi and offered an update it could already have run. Ask NetworkManager
instead: -s returns once it has tried every connection it could auto-activate,
which is the first moment the answer means anything, and -x then takes that
answer as it stands rather than waiting out a timeout that a laptop with
nothing to connect to would spend in silence.

The update prompt now waits for a connection rather than being phrased around
not having one. There is nothing to update against until a link lands, and one
usually does land later on the machines that started without it, so the prompt
follows the connection whenever it arrives.

That wait runs detached. It outlasts first run by design, and the keybindings
menu is on screen behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:56:30 +02:00
David Heinemeier HanssonandClaude Opus 5 23d85a992b Open the keybindings menu on first login
The welcome toast spent three lines telling you about a cheatsheet that takes
one keystroke to read, and the only way to act on it was to click the toast,
which opened that cheatsheet. Open it directly instead.

Dismissing the menu exits non-zero, since no selection was made, so the step
tolerates that rather than failing first run and retrying the whole sequence
next login.

It also goes last now. The menu blocks until it is answered, and the Wi-Fi and
update toasts are the only other things left to show, so sending those first
leaves them waiting underneath rather than behind an open menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:56:03 +02:00
David Heinemeier HanssonandClaude Opus 5 4ab51df2b0 Require a factory snapshot to reset a computer (#6680)
Machines without @factory fell back to a degraded reset that kept the
current system and only wiped user state. Turn them away with an
explanation instead, and drop the degraded staging path.

The first-boot worker still honors a wipe-degraded marker so a reset
staged by an older version finishes its scrub rather than handing the
machine over with the seller's accounts intact.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:41:05 +02:00
David Heinemeier HanssonandClaude 6fa4f78ee1 Add deferred first-boot provisioning and factory reset (#6621)
* Add OEM first-boot setup and factory reset

An OEM-mode ISO install (or omarchy-reset-computer) leaves the machine in OEM
state: fully installed, no user, /var/lib/omarchy/oem/pending armed. On the
next boot omarchy-oem-setup.service runs the configurator's user form on tty1,
creates the user with the groups system setup recorded, finalizes it offline
from the stashed Node tarball, re-keys LUKS from the throwaway install
passphrase to the user's password, and hands off to SDDM.

omarchy-reset-computer returns a machine to that state: it swaps the running
root for a fresh clone of the @factory snapshot the ISO takes at install time,
scrubs machine identity and prior users, and stages omarchy-factory-wipe to
drop the old root and recreate @home/@log on the next boot. Machines installed
before @factory existed get a degraded reset (current system kept, users and
state wiped) with that caveat surfaced in the confirmation.

omarchy-setup-system/-hardware gain --oem to run without an install user; the
group-granting install scripts now record their groups in
/var/lib/omarchy/oem/groups and only call usermod when the user exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden OEM setup: correct cryptsetup key-file usage, retry on failure

cryptsetup reads --test-passphrase/--key-file inputs byte-for-byte, so feed
passphrases through process substitution consistently instead of positional
args or stdin (which has different newline semantics). Run each first-boot
setup attempt as its own process so a failure offers a retry instead of
stranding the machine at a user-less login screen — bash ignores errexit
inside `while !` conditions, a child process does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Always grant wheel sudo in OEM first-boot setup

Detecting an existing %wheel grant by grepping sudoers is error-prone:
omarchy ships narrow '%wheel ALL=(ALL) NOPASSWD: <command>' rules (e.g.
asdcontrol) that match the naive pattern, which left the OEM-created user
matching sudoers entries but unable to run anything. Write the drop-in
unconditionally — a duplicate of an existing full grant is harmless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix LUKS re-key device resolution and OEM state readability

archinstall's encrypted installs put cryptdevice=PARTUUID=... on the kernel
cmdline, not UUID=, so the first-boot re-key never found its device and
silently skipped — leaving the throwaway auto-unlock keyfile in place, i.e.
the disk effectively unencrypted. Parse every cryptdevice= source spec form
and make any re-key failure abort the attempt loudly: a retry prompt beats a
machine that quietly boots without a passphrase forever.

The OEM state directory also has to be world-readable (its one secret,
luks-key, stays 0600): user finalization reads the stashed Node tarball as
the new user, and the 0700 directory forced it onto the network fallback.

Step markers now land in /var/log/omarchy-oem-setup.log for debuggability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Purge stale machine-id boot entries when resetting or re-keying

limine-entry-tool keys its limine.conf OS entries by machine-id. A factory
reset gives the machine a fresh identity, so the previous system's entry
survived every rebuild, sorted first, and made Limine stop at a Blake2b
hash-mismatch warning once the UKI was rebuilt. Start limine.conf over from
the shipped template (and drop foreign machine-id history directories on the
ESP) before any post-reset rebuild: in the staged chroot rebuild, in the
first-boot LUKS re-key, and — for unencrypted resets, where nothing else
rebuilds — in a dedicated first-boot refresh when foreign entries are found.

The staged rebuild also verifies every UKI hash referenced by limine.conf
against the file on the ESP before the subvolume swap, and the running
system's limine-snapper-sync is runtime-masked during staging so it cannot
rewrite the config behind the rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden reset and first-boot setup failure paths

Review findings from codex and Copilot:

- Generate throwaway passphrases without a trailing head stage: under
  pipefail, SIGPIPE from the infinite tr failed the substitution and errexit
  aborted every encrypted reset before it could stage anything.
- Stage the fallible parts of a degraded reset (LUKS re-key, boot rebuild)
  before arming the wipe, so a staging failure leaves the machine untouched
  instead of scheduling a wipe for a reset that never finished.
- Gate first-boot setup on the factory wipe having succeeded
  (ConditionPathExists=!wipe-pending plus an in-script guard): creating the
  new user on a half-wiped system would hand their data to the wipe retry.
- Abort the wipe (keeping its retry marker) when deleting the old root or
  recreating @home/@log fails, and abort resets that cannot remove a prior
  account — a surviving account keeps its password and wheel membership.
- Resume a partially-created account on setup retry instead of rejecting the
  username the failed attempt just created.
- Only purge machine-id directories the old limine.conf actually referenced;
  a shared ESP may hold other installations' boot artifacts.
- Recreate the hibernation swapfile (nested subvolume, so never captured by
  the factory snapshot) inside the factory root before its UKI rebuild, so a
  reset machine keeps disk-backed swap and a valid resume offset.
- Source base-test.sh in the OEM groups test per test conventions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Recreate the hibernation swapfile even when resume drop-ins survive

omarchy-hibernation-setup short-circuits as 'already set up' when the resume
mkinitcpio drop-in exists — which it always does in a factory root, while the
swapfile itself never survives the snapshot (nested subvolume). Drop the
marker when the swapfile is gone so setup reconfigures from scratch, and
verify the swapfile actually exists before proceeding with the reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Second review pass: encrypted-config coverage, factory-baseline sanitization, recoverable rekey

Codex xhigh round 2:

- Detect the LUKS backing device by walking the root's device tree, not only
  the cmdline cryptdevice=; reset/first-boot now re-key roots reached via
  rd.luks/crypttab too, instead of silently leaving the seller's slots valid.
- Sanitize the retained @factory baseline (accounts, /etc/shadow, machine
  identity) during a full reset: the new wheel user could otherwise mount it
  to recover the seller's data, and a second reset would restore the account.
- Re-key the disk recoverably: rebuild the no-auto-unlock UKI before killing
  the throwaway slot or destroying the staged key, and restore the keyfile if
  that rebuild fails, so a retry with a different password can never leave the
  disk locked to the first attempt's password.
- Roll back a degraded reset's live-root auto-unlock material if its boot
  rebuild fails, instead of leaving it for a later rebuild to embed.
- Treat a missing current-machine limine entry as stale so a retry after a
  failed rebuild repairs the config instead of clearing OEM state over it.
- Erase fingerprint enrollments (/var/lib/fprint) in degraded wipes.
- Remove the resume-offset drop-in too when recreating the factory swapfile,
  so the rebuilt UKI gets a correct offset.
- Pin first-boot retries to the account the first attempt created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Expose factory reset in the Setup menu

Add a 'Reset Computer' entry under Setup (Omarchy's Settings menu, where OS
factory resets conventionally live), guarded to btrfs roots and launched in a
floating terminal. omarchy-reset-computer now self-elevates via sudo so the
menu entry needs no sudo prefix, forwarding the caller's gum theme env as
env arguments so styling survives an env_reset sudoers. The typed 'reset'
confirmation and the sudo password prompt remain as the guards against
accidental triggering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Defer keyboard selection to first boot for OEM installs

The OEM first-boot setup now runs a keyboard step before the user form,
mirroring the ISO configurator: it loads the chosen layout on the live VT so
the password (and the LUKS re-key that follows) are typed under it, and
persists it with systemd-firstboot so the installed system gets both the
console KEYMAP and the XKB layout Hyprland reads — exactly what a normal
install writes. Layouts localectl doesn't know keep the default, same as the
installer.

This lets the OEM operator set nothing user-specific: the machine's owner
picks their keyboard alongside their account at first boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename factory-reset commands to omarchy-system-factory-reset[-finish]

omarchy-reset-computer      -> omarchy-system-factory-reset
omarchy-factory-wipe        -> omarchy-system-factory-reset-finish
(and its systemd unit, log path, and temp mount to match)

Pure rename: every reference — the Setup menu action, the first-boot finish
service the reset stages and enables, the oem-setup ordering/gating, comments,
and the menu test — moves together, with no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename OEM vocabulary to provisioning (runtime)

Commands unify under the provisioning family:
  omarchy-oem-setup      → omarchy-provision-owner
  omarchy-finalize-user  → omarchy-provision-user
  omarchy-first-run      → omarchy-provision-first-run

And the deferred-provisioning state/vocabulary replaces 'OEM':
  /var/lib/omarchy/oem/          → /var/lib/omarchy/provisioning/
  /etc/omarchy/oem.key           → /etc/omarchy/provisioning.key
  install/oem/                   → install/provisioning/
  OMARCHY_SETUP_CONTEXT=oem-firstboot → provision-owner
  omarchy-setup-system/-hardware --oem → --defer-provisioning

All callers (provision-first-run→provision-user, autostart, factory-reset
staging the provisioning units, the group-recording scripts) and comments
move together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop remaining OEM mentions from the provisioning groups test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Finish the omarchy-first-run rename in the docs

Two doc references to omarchy-first-run were missed when the script was renamed
to omarchy-provision-first-run; update them to match.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:54:21 +02:00
David Heinemeier HanssonandClaude Opus 5 5a58f79876 Keep clicking a notification working after a shell restart (#6636)
* Keep clicking a notification working after a shell restart

Notification actions lived only in the sending process: `-a` appended
`-A default=default`, so notify-send blocked on a D-Bus ActionInvoked signal and
the caller ran the command when it arrived. Nothing about that reached disk, so a
restored popup had no action to run and its sender stayed blocked forever.

Replace `-a` with `--exec <command>`, carried as an `omarchy-exec` hint into the
snapshot's `exec` role. It travels through the popup files and history, and the
shell runs it on click, so restored toasts behave exactly like live ones and the
sender exits immediately.

That drops the scaffolding whose only job was keeping a blocked sender alive: the
first-run invitations lose their `--show` re-entry and two transient units each,
omarchy-migrate-notify loses its transient service, and the screenshot,
recording, download, and taildrop toasts lose their wrapper subshells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep a failed toast from failing the work it announces

Moving these sends out of their backgrounded subshells put a fallible command
on the foreground path, where the `&` used to swallow its exit status. A
notification outage — including the shell restart this branch targets — now
propagates:

- taildrop's receiver dies under `set -e` mid-delivery
- omarchy-capture-screenshot reports failure for a screenshot it already saved
- a completed download exits before scheduling its thumbnail cleanup, leaking
  the mktemp file

Announcing is best-effort in all three: the work is already done by the time
the toast goes out.

Also drop the first-run sleep that spaced out the welcome and Wi-Fi toasts.
It compensated for the background notify-send processes this branch removes;
each send now returns only once the server has taken the toast, so sending in
order is enough to stack them newest-on-top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop tying the preview cleanup to the toast's expiry

The shell loads a notification thumbnail into memory when the toast appears and
never re-reads the file, so the preview only has to outlive that load. Deriving
the cleanup delay from the expiry was false precision, and it turned -t into a
variable for no reason: -t is already the helper's expiry setting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:30:47 +02:00
David Heinemeier Hansson 7b4fb0ad6b Fix default agent setup edge cases 2026-08-01 19:52:04 -07:00
David Heinemeier Hansson a9b84cea8e Refine default agent menu 2026-08-01 19:11:44 -07:00
David Heinemeier Hansson a0e2501e15 Add configurable default coding agent 2026-08-01 18:49:28 -07:00
David Heinemeier HanssonandClaude Fable 5 84e87ce67e Add oh-my-pi (omp) as a lazy-installed mise tool
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 19:55:02 -05:00
David Heinemeier Hansson 66427571bc Revert WhatsApp theme bridge
The native messaging host, extension shim, and lifecycle machinery are too complicated for the value this integration provides.
2026-08-01 12:44:40 -07:00
237405215d Make WhatsApp Web follow your Omarchy light/dark theme (#6484)
* Make WhatsApp Web follow your Omarchy light/dark theme

WhatsApp Web's "System default" theme follows prefers-color-scheme and
repaints live, so a small theme bridge is enough to make it track the active
Omarchy theme with no reload and no WhatsApp-specific CSS.

- omarchy-chromium-theme-host: push-only native messaging host that reads the
  active theme and emits it on connect and on every theme-set. Unlike copy-url/
  yt-dlp (one-shot), it stays connected and pushes, since theme-following needs
  the page to learn about changes while it is running.
- omarchy-chromium-theme-refresh: SIGUSR1s the running host(s); called from
  omarchy-theme-set's post_theme_commands.
- whatsapp-theme extension: decides dark vs. light from the theme background's
  WCAG luminance and drives a prefers-color-scheme shim, so WhatsApp's own
  theme does the repaint.

Wired like copy-url/yt-dlp and whatsapp-slim: bundled under
default/chromium/extensions, added to --load-extension, host manifest
registered from the fresh-install/refresh/browser-install paths, existing users
covered by a migration.

The host is named com.omarchy.theme (a generic theme bridge) rather than
WhatsApp-specific, so other bundled web-app extensions can follow the theme by
connecting to it and adding their id to the host manifest's allowed_origins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review on the WhatsApp theme bridge

Light/dark was decided by weighting raw sRGB bytes, which the comment above
it already described as WCAG relative luminance. sRGB is gamma-encoded, so
the weights only mean anything once each channel is linearized — the two
steps the shell already does in Panel.qml. Every shipped theme classifies the
same either way; a mid-tone custom background does not (#808080 reads 0.502
unlinearized and 0.216 linearized).

Drop the `tabs` permission. The WhatsApp host permission is what lets
tabs.query filter by url and what populates tab urls in onUpdated, so `tabs`
only widened this to every tab's url and title. Tabs without permission
arrive with url unset and fall out on the existing guard.

Give the two new bin commands their metadata directives. Without a summary
they failed test/cli's command metadata check.

Cover all three: the classifier over unambiguous and mid-tone backgrounds,
and the manifest for the permission it should no longer ask for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Scope color-scheme listeners to their query and test the real host

Registrations all shared one Set keyed only by callback, so an app that gave
the same callback to both the dark and the light query and later detached one
detached the other too, leaving the query it still held deaf to theme
changes. Record the owning MediaQueryList and match on it. Adds native
dedupe behaviour while there: registering the same callback twice fired it
twice. addListener is a legacy alias of addEventListener("change"), so the
two share one registration space and either remover cancels either add —
which is also why useEvent had nothing left to select and is gone.

The refresh test signalled a synthetic sleeper carrying its own USR1 trap, so
it proved the refresh command sends a signal but would have stayed green
through any regression in the host's own trap, watchdog wait, or second
write. Drive the real host over a FIFO instead, count framed messages, and
assert the second one is a usable theme. Verified by neutering the host's
USR1 trap: the old test passed, this one fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Harden Chromium theme bridge

* Address Chromium theme bridge review

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-01 13:16:06 -05:00
David Heinemeier HanssonandClaude Opus 5 6e07fd0e82 Supervise fcitx5 so XCompose sequences can't silently die
fcitx5 is what turns the CapsLock compose sequences in ~/.XCompose into
text for Wayland clients -- CapsLock m s for an emoji, CapsLock space n
for your name. It was launched fire-and-forget from Hyprland's autostart
via uwsm-app, so nothing supervised it, nothing restarted it, and it
logged nowhere. When it went away, every compose sequence stopped
working for the rest of the session with no visible symptom beyond
"emoji input is busted", and no record of why.

That is not hypothetical: it was found dead on a running machine with no
coredump, no OOM kill, and nothing in the journal to explain it.

Move it to a systemd user service:

- Restart=always, not on-failure. fcitx5 exits 0 when it finds another
  instance already owning org.fcitx.Fcitx5, and a clean exit still
  leaves the user with no input method.
- After=/PartOf=graphical-session.target. It needs WAYLAND_DISPLAY and
  DISPLAY, which uwsm imports into the user manager before reaching the
  target, and its wayland connection dies with the compositor.
- ConditionEnvironment=WAYLAND_DISPLAY. After= is ordering only and does
  not stop the unit from being started while the target is inactive. An
  update over SSH has a live user manager (pam_systemd) and no graphical
  session, and a fcitx5 started there comes up blind -- then stays
  active, so the later target activation won't pull in a working one,
  because Wants= does not restart what is already running. Skipping the
  start leaves the unit enabled and healthy for the next graphical login.

The migration hands over inside a live session only: it enables without
--now, and only when graphical-session.target is active does it drop the
autostart-launched process and start the unit. Because that kills a
fcitx5 that was working a moment ago, a failed start is reported instead
of leaving the session mute with the migration marked complete.

omarchy-restart-xcompose now drives the unit. It still clears any fcitx5
running outside it first: that process owns the bus name, so the unit's
instance would exit on arrival and the stale one would keep serving the
old table -- a restart that reports success and changes nothing.

Side benefit: fcitx5 now logs to the journal under its own unit, so the
next disappearance leaves a record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:49:15 -07:00
David Heinemeier HanssonandClaude Opus 5 4d93ad588f Register the Chromium native messaging hosts on fresh installs
Chromium ships in the base packages, so it never goes through
omarchy-install-browser, and a first install stamps every migration as
already applied without running it. That left both bundled extensions
loaded from chromium-flags.conf with no host to talk to: Copy URL did
nothing at all, since the clipboard write and the toast both live in the
host.

The user install now registers both hosts itself, and a migration
repairs the installs that already missed out — Copy URL since 1784763917
landed, Download Video since 1780517689, unless installing a browser or
refreshing Chromium happened to fix them along the way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VeAd2iERgi7bRSxq2gA1hp
2026-07-27 09:02:25 -07:00
David Heinemeier HanssonandClaude Opus 5 425c3ff84d Only check for pending migrations at login
omarchy-update-user-notify.path watched /usr/share/omarchy/migrations, but
pacman writes that directory during every update, including the blessed
omarchy update, which runs omarchy-migrate a step later. The watcher fired a
critical notification for the migrations the update was already applying in
the visible terminal. A watcher cannot tell that apart from a bypassed
pacman -Syu, so the only trigger that never collides with a running update is
a once-per-login check.

The service that already ran at graphical-session.target is now the whole
mechanism, renamed after the command it runs. That is also all the second-user
case needs: markers are per-user, so anyone who did not run the update finds
them missing at their next login.

Login timing means the toast can be sent before the shell has claimed
org.freedesktop.Notifications, so the notifier waits for a live server first.
The wait is omarchy-first-run's, lifted into omarchy-notification-wait rather
than duplicated.

The package keeps omarchy-update-user-notify.service as a symlink onto the new
unit. Existing users hold an absolute wants symlink to the old path, and the
migration that repoints it only runs for users who run an update, which is the
opposite of who the notifier is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:53:29 -07:00
David Heinemeier Hansson be781574d1 Let the invitation terminals actually open
Clicking the Voxtype or fingerprint invitation did nothing. The launcher
execs setsid, which forks because the transient unit's main process is
already a process group leader, so the unit exits within milliseconds and
systemd's default control-group kill took the still-starting terminal down
with it. Run those units with KillMode=process instead.

The invitation tests had also drifted from the two-unit design and were
failing; teach the systemd-run mock to run the command it is given.
2026-07-24 18:57:51 -07:00
David Heinemeier Hansson 0b564255ea Stack the Wi-Fi toast above the update toast on first run
Both notifications are sent from background subshells, so whichever
notify-send registered first won the bottom slot. Give the update toast a
tick to register so Wi-Fi lands newest and stacks on top.
2026-07-24 18:52:26 -07:00
David Heinemeier HanssonandClaude Opus 5 aa9f0c54c5 Add per-laptop speaker tunings, starting with the XPS 14
Laptop speakers ship voiced by the vendor's Windows DSP layer, which Linux does
not get. A tuning restores that as a PipeWire filter-chain in front of the
internal speaker sink, matched to the machine by DMI string and expected sink.

Adding a laptop is a directory under default/audio/tunings with two files and no
new code: matching is data. The XPS 14 DA14260 tuning included here was derived by
measuring the xps-audio-linux EasyEffects profile (MIT) and fitting a biquad chain
to it, so no impulse response or other upstream asset is redistributed. It measures
1.24 dB RMS against that reference, and matches its dynamic range within 0.1 LU --
the reference's multiband compressor turned out to contribute nothing, so a linear
chain replaces it. Bass Q is capped deliberately: a closer magnitude fit swung
group delay 31 ms across 63-80 Hz, which smears bass transients.

The graph runs as its own PipeWire client under its own config name rather than
loading into the audio daemon. The daemon only reads its config at startup, so a
daemon-loaded tuning could only be switched by restarting PipeWire -- which drops
every PulseAudio client's connection, and applications that do not reconnect
(Spotify) then have to be restarted by hand. Hosting it separately also contains
failure, since a malformed tuning breaks only that service.

Three things about the surrounding audio graph needed fixing for this to behave:

- Volume must live downstream of the tuning. omarchy-audio-output-sink is now the
  single definition of which sink an output's volume really uses, shared by the
  volume keys, the output switcher's OSD and the audio panel, so they cannot
  disagree. It resolves the current default output, which keeps it correct when
  headphones are selected while a tuning exists.
- The tuning's own output is a movable sink input, so rerouting "all streams" to a
  newly selected output would drag the processing onto headphones, or into the
  tuning's own sink, which is a cycle. It is pinned, and stream moves are limited
  to streams carrying an application.name.
- The physical sink a tuning fronts is not independently selectable, since picking
  it would only bypass the tuning, so it is kept out of the output list.

Applying happens at first-run, not finalize-user, because finalize-user also runs
in the ISO chroot where there is no audio server and nothing would retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:25:02 -07:00
David Heinemeier HanssonandClaude Opus 4.8 555cb4db75 Stop install invitations from re-showing after they run
Voxtype/fingerprint invitations run as a transient
omarchy-*-invitation.service that shows a critical toast and waits for a
click. On click the service execs the install terminal, so the service
stays active for the whole life of that terminal.

The installer ends with omarchy-restart-shell, which re-triggers any
still-running omarchy-*-invitation.service on the assumption that a
still-running unit is an unanswered toast wiped by the shell restart. But
an answered invitation is still running only because it is the parent of
the very install terminal that triggered the restart, so it gets falsely
re-fired and the invitation toast pops a second time right after the
install finishes.

Launch the installer in its own transient unit so the invitation service
exits immediately after the click. Genuinely unanswered invitations still
block in notify-send and are still recovered by restart-shell; answered
ones are gone and no longer match the re-show glob.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHPDqx5hwLTwq27f7KpM9d
2026-07-24 16:36:50 -07:00
David Heinemeier HanssonandClaude Fable 5 00eae9a5ef Invite fingerprint setup after the first system update
The setup installs packages (libfprint-git, fprintd, usbutils), which
isn't reliable on a fresh install before the databases have been
synced. Defer the invitation with a post-update hook, like Voxtype.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:25:06 -07:00
David Heinemeier Hansson b6e75d5e48 Language improvements 2026-07-23 14:08:08 -07:00
David Heinemeier HanssonandClaude Opus 4.8 e2bf0daaa8 Invite fingerprint setup on first run when a reader is present
Add a first-run notification, alongside the keybindings/Wi-Fi toasts, that
invites anyone with a fingerprint sensor to enable it. Clicking launches
omarchy-setup-security-fingerprint in a floating terminal.

Detection lives in a new omarchy-hw-fingerprint helper that reads sysfs
(device product descriptor plus a fingerprint-vendor allowlist), so it
works before fprintd/usbutils are installed and without nagging machines
that have no reader. The setup script reuses the same helper as an early
gate, bailing before installing any packages when no reader is found
(replacing the old post-install fprintd-list probe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:05:31 -07:00
David Heinemeier Hansson 51d6d3c291 Fix Voxtype invitation action 2026-07-21 11:22:51 -07:00
David Heinemeier HanssonandClaude Opus 4.8 0f691d60fb Scale the Dell XPS 13 default via display text size
Use omarchy-display-text-size 11 instead of a lone GTK text-scaling-factor,
so the XPS 13 nudge moves shell, GTK, and terminal text together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:08:17 -07:00
David Heinemeier HanssonandClaude Fable 5 b87de89372 Stop the migration notifier from busy-looping the session
omarchy-update-user-notify.path used PathExistsGlob= on the packaged
migrations directory. That directive is level-triggered: systemd re-checks
it every time the triggered unit deactivates and fires again while the glob
still matches. Since applied migrations stay on disk forever (state lives in
~/.local/state/omarchy/migrations), the glob always matches, so the oneshot
notifier re-triggered itself in a tight loop (~26-66 starts/sec) — burning
about a core and flooding the journal for the whole session.

The loop existed since the unit was introduced, but the default start-rate
limit killed it after 5 iterations, taking the .path unit down with
'unit-start-limit-hit'. That symptom was reported as #6174 and fixed
yesterday by setting StartLimitIntervalSec=0 — which removed the only brake
and turned the capped hiccup into an unbounded busy-loop.

Fix the actual cause instead:

* Drop PathExistsGlob= from the .path unit, keeping the edge-triggered
  PathModified= watch for updates that land mid-session.
* Revert the StartLimitIntervalSec=0 override; with the level trigger gone
  there is no self-re-fire to trip the limit, and the default limit is a
  useful backstop again.
* Preserve the once-per-login pending check the glob used to provide by
  giving the service its own WantedBy=graphical-session.target, enabled at
  first-run alongside the other user units.
* Add a migration that daemon-reloads, revives a rate-limit-killed .path,
  restarts the watcher, and enables the login-time notifier on existing
  installs.

Verified with transient path/service units: the old config runs the service
200 times in 3 seconds; the new config runs it zero times while idle and
exactly once when a new migration file lands.

Thanks to @HANCORE-Linux for finding and diagnosing the problem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:49:41 -07:00
David Heinemeier HanssonandClaude Fable 5 d211de2ffc Match Wi-Fi first-run notification format to the other invitations
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:17:04 -07:00
Stephen Taylor 9030ba870c Force software cursors on nouveau
Adapt the fix from PR #6161 to Quattro's per-user hardware setup and add coverage for detection, idempotency, and proprietary-driver handling.
2026-07-18 21:49:47 -07:00