Add OpenClaw to Install > AI as a web app on its own gateway

OpenClaw's desktop experience on Linux is its Control UI, served by the
gateway the openclaw package runs, so the Install > AI entry installs
the package and a web app launcher that routes through the new
omarchy-launch-openclaw: first launch hands off to OpenClaw's own
onboarding wizard, later launches start the gateway when needed and open
the dashboard's single-use browser handoff URL as an app window.
Remove > AI tears the gateway service down through OpenClaw's own
gateway uninstall (falling back to systemctl by hand), aborts rather
than dropping the package under a gateway that will not stop, and keeps
the user's agent in ~/.openclaw.

OpenClaw also joins Setup > Defaults > Agent through the same
agent_installer seam Hermes carries: its CLI is the pacman package
rather than a mise tool, so omarchy-install-openclaw-cli answers
--check/--now with pacman, and omarchy-agent runs `openclaw chat`,
seeding prompts through --message.

The menu mark is a new U+E90C glyph traced from the package's lobster
favicon; E90B stays free for the Perplexity mark still in flight on its
own branch.
The launcher recovers the gateway through `openclaw gateway install --force`
(unit not enabled: missing, or an install that died after writing it) or
`openclaw gateway start` (enabled but stopped), never `openclaw dashboard
--yes`: as of OpenClaw 2026.9.1 that defers to "the owning supervisor" in
both cases, and once the gateway is up it copies a one-time browser pairing
URL into the clipboard. The dashboard probe is bounded so an app-grid launch
cannot hang without a terminal to interrupt it. Removal treats only
systemd's own "inactive"/"failed" as a stopped gateway, so an unreachable
user manager aborts instead of dropping the package under a live process.
All of it verified against a real 2026.9.1 install.
Removal also takes down the node-host unit if OpenClaw ever installed one, and
asks (default no, only on a terminal) whether ~/.openclaw should go too, with
its size: the chats and credentials live there next to hundreds of megabytes
of plugin runtimes and cache OpenClaw downloads for itself.
Onboarding goes through omarchy-openclaw-onboard rather than bare `openclaw
onboard`: as of 2026.9.1 the bare command is the guided flow, which ends by
running a foreground gateway and handing off to a browser tab without
returning, so the install script never reached the app launch and no service
was installed. The helper runs the classic wizard (--flow quickstart
--install-daemon --skip-ui) as a background job that keeps the terminal as its
stdin, so its prompts render and take input as upstream draws them, and stops
it once the gateway answers: upstream leaves the wizard running after its
outro (only the TUI branch exits, and the model sign-in holds a socket open).
Every quickstart prompt precedes the service install, so that point is safe.
A gateway that never comes up after this run applies setup ends the wait as a
failure instead
of hanging, an already-running OpenClaw is left alone rather than mistaken for
this run's success, a gateway answering on the port is only this run's once its
process is the unit's own MainPID (an orphan from an
earlier run) is not mistaken for the service this run installs, and a signal at
the helper takes the wizard down with it.
This commit is contained in:
Spencer Bull committed 2026-09-05 09:18:27 -05:00
1 parent 493067741e
commit 5345673988
17 files changed
+1088 -6

No files matched your search

+1
View File
@@ -65,6 +65,7 @@ GROUP_DESCRIPTIONS[monitor]="Monitor status helpers"
GROUP_DESCRIPTIONS[network]="Network status helpers"
GROUP_DESCRIPTIONS[notification]="Notification helpers"
GROUP_DESCRIPTIONS[mise]="Mise tool wrappers"
GROUP_DESCRIPTIONS[openclaw]="OpenClaw agent platform setup"
GROUP_DESCRIPTIONS[osd]="On-screen display status helpers"
GROUP_DESCRIPTIONS[pkg]="Package management helpers"
GROUP_DESCRIPTIONS[plugin]="Omarchy shell plugin and bar widget management"
+9
View File
@@ -82,6 +82,15 @@ grok)
command=(grok --permission-mode bypassPermissions)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
openclaw)
# The launcher owns onboarding and the gateway dance: OpenClaw's terminal UI
# must attach to the running gateway (the embedded `openclaw chat` refuses to
# start while the gateway owns the state directory). It has no permission
# prompts to skip, and --message seeds the session while keeping it
# interactive.
command=(omarchy-launch-openclaw --tui)
[[ -n ${prompt:-} ]] && command+=(--message "$prompt")
;;
codex)
command=(codex --approve-for-me)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
+5 -3
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# omarchy:summary=Set and launch the default coding agent
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush]
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush]
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
installing=false
@@ -32,11 +32,12 @@ claude | claude-code) agent="claude"; name="Claude Code" ;;
codex) agent="codex"; name="Codex" ;;
crush) agent="crush"; name="Crush" ;;
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
openclaw) agent="openclaw"; name="OpenClaw"; agent_installer="omarchy-install-openclaw-cli" ;;
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
*)
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush>"
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|openclaw|agy|hermes|copilot|crush>"
exit 1
;;
esac
@@ -45,7 +46,8 @@ agent_package=${agent_package:-$agent}
# Hermes reaches mise through its own installer rather than straight from
# here: it needs its interpreter pinned, and a bare `mise use` has nowhere to
# say so. See omarchy-install-hermes-cli.
# say so. See omarchy-install-hermes-cli. OpenClaw comes from its pacman
# package the same way; see omarchy-install-openclaw-cli.
if [[ -n ${agent_installer:-} ]]; then
# Not omarchy-cmd-present: the stub is on PATH from first boot and says
# nothing about whether Hermes is installed behind it. Treating a cold stub
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# omarchy:summary=Install the OpenClaw agent platform and its Control UI web app
# omarchy:requires-sudo=true
set -e
echo "Installing OpenClaw..."
omarchy-pkg-add openclaw
# The desktop app is OpenClaw's Control UI: a web app served by its own
# gateway. The launcher entry goes through omarchy-launch-openclaw, which
# onboards or starts that gateway before opening the window. The icon ships
# inside the package, so nothing is fetched here.
echo "Installing the OpenClaw web app..."
omarchy-webapp-install OpenClaw "http://127.0.0.1:18789" \
/usr/lib/node_modules/openclaw/dist/control-ui/apple-touch-icon.png \
omarchy-launch-openclaw
# A first install runs onboarding right here: launching the app instead would
# open a second floating terminal for the wizard, identical to this one.
# omarchy-openclaw-onboard runs the wizard the way this flow needs (terminal
# prompts, gateway as a user service, and it actually returns). A machine
# that is already onboarded goes straight to the app.
if [[ -f $HOME/.openclaw/openclaw.json ]]; then
echo "Opening OpenClaw..."
setsid uwsm-app -- gtk-launch OpenClaw >/dev/null 2>&1 &
elif omarchy-openclaw-onboard && [[ -f $HOME/.openclaw/openclaw.json ]]; then
echo "Opening OpenClaw..."
setsid uwsm-app -- gtk-launch OpenClaw >/dev/null 2>&1 &
fi
echo ""
echo "OpenClaw has been installed."
echo "If you skipped onboarding, launching OpenClaw from the app grid resumes it."
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# omarchy:summary=Ensure the OpenClaw CLI is installed for the default agent
# omarchy:args=[--check|--now]
# omarchy:requires-sudo=true
# OpenClaw is not a mise tool: the openclaw pacman package is the one OpenClaw
# installation on the machine — the CLI, the gateway, and the Install > AI web
# app all share it, and the fast ring keeps it current. The default-agent flow
# talks to that package through the same --check/--now contract mise-backed
# agents get from mise itself.
set -euo pipefail
case "${1:---now}" in
--check)
omarchy-pkg-present openclaw
;;
--now)
if ! omarchy-pkg-present openclaw; then
echo "Installing OpenClaw..."
omarchy-pkg-add openclaw
fi
;;
*)
echo "Usage: omarchy-install-openclaw-cli [--check|--now]" >&2
exit 1
;;
esac
+91
View File
@@ -0,0 +1,91 @@
#!/bin/bash
# omarchy:summary=Open the OpenClaw Control UI (or its terminal UI with --tui), onboarding or starting the gateway first when needed.
# omarchy:args=[--tui [--message <text>]]
set -euo pipefail
tui=false
message=()
if [[ ${1:-} == "--tui" ]]; then
tui=true
shift
if [[ ${1:-} == "--message" ]]; then
message=(--message "${2:?--message needs a value}")
shift 2
fi
fi
# Onboarding is OpenClaw's own interactive wizard, run through
# omarchy-openclaw-onboard so it stays in the terminal, installs the gateway
# as a user service, and returns (see that script for why bare `openclaw
# onboard` does none of those as of 2026.9.1). The config check gates what
# follows because the wizard's "Skip for now" also exits 0: only inference
# that passed writes the config, and skipping must not loop back into the
# wizard.
if [[ ! -f $HOME/.openclaw/openclaw.json ]]; then
if [[ $tui == "true" ]]; then
# Already in a terminal, so the wizard runs right here.
omarchy-openclaw-onboard
[[ -f $HOME/.openclaw/openclaw.json ]] || exit 1
else
# The wizard needs a real terminal, and chaining the relaunch means
# finishing it lands the user in the app. Single quotes so $HOME expands
# in the spawned terminal.
# shellcheck disable=SC2016
exec omarchy-launch-floating-terminal-with-presentation \
'omarchy-openclaw-onboard && [[ -f $HOME/.openclaw/openclaw.json ]] && omarchy-launch-openclaw'
fi
fi
dashboard_url() {
# browserUrl carries a single-use browser handoff; url is the shared-auth
# fallback for gateways predating the handoff flow. The timeout keeps a
# wedged CLI from hanging an app-grid launch that has no terminal to ^C.
timeout 10 openclaw dashboard --json 2>/dev/null | jq -re '.browserUrl // .url // empty'
}
# --json never starts the gateway, so an empty answer means it is not running.
# Recovery goes through the gateway's own service commands. `dashboard --yes`
# used to be the start/install-without-prompting path, but as of 2026.9.1 it
# defers to "the owning supervisor" for both a missing and a stopped unit,
# and once the gateway is up it copies a one-time pairing URL into the
# clipboard, which nothing here needs. Enablement, not the unit file, decides:
# a unit that was written but never enabled (an install that died halfway)
# would otherwise be "started" once and stay off at every following login,
# where --force rewrites and enables it.
if ! url=$(dashboard_url); then
if systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then
timeout 60 openclaw gateway start >&2 || true
else
timeout 120 openclaw gateway install --force >&2 || true
fi
# A first-ever service install (unit write, daemon-reload, first boot)
# takes materially longer than starting an installed unit, so the budget
# is sized for the slow case.
for _ in {1..30}; do
url=$(dashboard_url) && break
sleep 1
done
fi
if [[ -z ${url:-} ]]; then
echo "OpenClaw's gateway did not come up. Check it with: openclaw gateway status" >&2
echo "If onboarding never finished, rerun it with: omarchy-openclaw-onboard" >&2
exit 1
fi
if [[ $tui == "true" ]]; then
# Attach to the gateway rather than `openclaw chat`: chat is the embedded
# local runtime, which refuses to start while the gateway owns ~/.openclaw's
# state directory -- and on any machine set up through Install > AI, the
# gateway service is running whenever the desktop session is.
exec openclaw tui "${message[@]}"
fi
# The handoff URL rides in the browser's argv, which uwsm's app daemon echoes
# into the user journal. Accepted: the token is single-use with a ten-minute
# expiry against a loopback-only gateway, and journal access already implies
# access to ~/.openclaw itself.
exec omarchy-launch-webapp "$url"
+130
View File
@@ -0,0 +1,130 @@
#!/bin/bash
# omarchy:summary=Run OpenClaw's setup wizard the way Omarchy needs it: in the terminal, installing the gateway as a user service, and returning when it is done.
# Not bare `openclaw onboard`: as of 2026.9.1 that is the guided flow, which
# ends by running a foreground gateway and handing off to a browser tab, and
# never returns. --install-daemon keeps it to the classic wizard (minimal
# prompts with --flow quickstart) that installs the gateway service, and
# --skip-ui drops its closing Control UI/TUI prompt since whoever called this
# opens one next.
#
# That classic wizard has one wrinkle: with --skip-ui it prints "Onboarding
# complete" and then never exits. Upstream only calls exit(0) when it launched
# the TUI, and the model sign-in leaves an open socket that keeps the process
# alive otherwise. So this script watches for the gateway to answer and then
# stops the wizard. That is safe because every prompt in the quickstart flow
# runs before the gateway service is installed and reachable: by the time the
# dashboard answers, only the closing notes are left.
set -uo pipefail
wizard=(openclaw onboard --flow quickstart --install-daemon --skip-ui)
config=$HOME/.openclaw/openclaw.json
settle_seconds=${OMARCHY_OPENCLAW_ONBOARD_SETTLE_SECONDS:-3}
# Counted from the moment the config exists, i.e. once the wizard has applied
# setup; the prompts before that take as long as the user takes.
gateway_timeout=${OMARCHY_OPENCLAW_ONBOARD_GATEWAY_TIMEOUT:-180}
gateway_answers() {
timeout 10 openclaw dashboard --json 2>/dev/null | jq -e '.ok == true' >/dev/null 2>&1
}
# A gateway already answering means OpenClaw is set up, and this run must not
# read its own success off state that predates it: the wizard's repair pass
# would be stopped mid-prompt the moment the watcher looked. Nothing to do.
if [[ -f $config ]] && gateway_answers; then
echo "OpenClaw is already set up and its gateway is running." >&2
echo "To change providers or settings, run: openclaw onboard --classic" >&2
exit 0
fi
# Marks when this run began, so a config left behind by an earlier, incomplete
# setup is not mistaken for this run having applied its own: the gateway
# deadline below must not start ticking while the user is still at prompts.
started=$(mktemp)
trap 'rm -f "$started"' EXIT
# Backgrounded so this script can watch it, but with the terminal kept as its
# stdin (bash would otherwise hand a background job /dev/null). Job control is
# off in a script, so it stays in the terminal's foreground process group and
# reads from it freely. Ctrl-C does not reach it directly, though: bash starts
# async children with SIGINT ignored when job control is off, so the INT trap
# below is what turns Ctrl-C into the wizard's exit.
"${wizard[@]}" <&0 &
wizard_pid=$!
stop_wizard() {
kill -TERM "$wizard_pid" 2>/dev/null || true
}
# Any signal at this script, whether Ctrl-C from the terminal or a kill aimed
# at its pid alone, takes the wizard down with it rather than leaving it
# running unwatched.
trap 'stop_wizard' INT TERM HUP
# Whether this run has applied setup: the config exists and is not older than
# the run itself.
config_applied() {
[[ -f $config && ! $started -nt $config ]]
}
# Whether this run's gateway is up: setup applied, and the process answering
# on the gateway's port is the main process of the service the wizard
# installs. Not the dashboard alone: with no config on disk `openclaw
# dashboard --json` still probes the default loopback port, so a gateway left
# behind by something else (an earlier guided onboarding's foreground
# gateway, say) would read as this run's success while the user is still at
# the first prompt. And not the unit being active either: its Type=simple
# counts it active from the fork, before it has found the port taken by such
# an orphan, and the app would then open on the orphan rather than the
# service this run installed.
gateway_ready() {
config_applied || return 1
local json port listener main_pid
json=$(timeout 10 openclaw dashboard --json 2>/dev/null) || return 1
jq -e '.ok == true' <<<"$json" >/dev/null 2>&1 || return 1
port=$(jq -r '.port // empty' <<<"$json" 2>/dev/null)
[[ -n $port ]] || return 1
listener=$(ss -ltnpH "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
main_pid=$(systemctl --user show -p MainPID --value openclaw-gateway.service 2>/dev/null)
[[ -n $listener && -n $main_pid && $main_pid != 0 && $listener == "$main_pid" ]]
}
stopped=false
timed_out=false
config_seen_at=
while kill -0 "$wizard_pid" 2>/dev/null; do
sleep 2
if gateway_ready; then
# Let the outro finish printing, then end the process the wizard leaves
# running.
sleep "$settle_seconds"
stop_wizard
stopped=true
break
fi
config_applied || continue
: "${config_seen_at:=$SECONDS}"
if (( SECONDS - config_seen_at >= gateway_timeout )); then
# Setup was applied but the service never came up (port taken, unit
# failing, ...): the wizard would sit in its never-exiting state forever,
# and so would whoever is waiting on this script.
stop_wizard
timed_out=true
break
fi
done
wait "$wizard_pid"
rc=$?
if [[ $timed_out == true ]]; then
echo "OpenClaw's gateway did not come up within ${gateway_timeout}s of setup finishing." >&2
echo "Check it with: openclaw gateway status" >&2
exit 1
fi
# A wizard stopped here after the gateway came up did its job. One that
# exited on its own, including a user who chose "Skip for now" (no config,
# non-zero), keeps its own exit code.
[[ $stopped == true ]] && rc=0
exit "$rc"
+78
View File
@@ -0,0 +1,78 @@
#!/bin/bash
# omarchy:summary=Remove the OpenClaw agent platform along with its gateway service and web app.
# omarchy:requires-sudo=true
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
set -euo pipefail
unit_dir="$HOME/.config/systemd/user"
# Only systemd's own word counts as "stopped": a non-zero exit from is-active
# also covers an unreachable user manager, which says nothing about whether
# the process is alive.
unit_stopped() {
local state
state=$(systemctl --user is-active "$1" 2>/dev/null) || true
[[ $state == inactive || $state == failed ]]
}
# The user services OpenClaw installs for itself: the gateway through
# onboarding, and the node host if the user ever paired this machine to
# another gateway. OpenClaw writes them to $HOME/.config regardless of
# XDG_CONFIG_HOME, so look exactly there; no unit means nothing registered.
# Upstream's own teardown knows every piece its install wrote (unit file, the
# default.target.wants enablement symlink, the reload), so prefer it while the
# binary is still installed and fall back to doing the same by hand. A service
# that will not stop aborts the removal: dropping the package would strand the
# live process on deleted code with no way to restart it cleanly.
for unit_file in "$unit_dir"/openclaw-gateway.service "$unit_dir"/openclaw-node.service; do
[[ -f $unit_file ]] || continue
unit=${unit_file##*/}
role=${unit#openclaw-}
role=${role%.service}
if openclaw "$role" uninstall >/dev/null 2>&1 ||
systemctl --user disable --now "$unit" 2>/dev/null ||
unit_stopped "$unit"; then
# .bak is what `gateway install --force` leaves behind when it rewrites a
# unit, so it goes with the unit.
rm -f "$unit_file" "$unit_file.bak" "$unit_dir/default.target.wants/$unit"
systemctl --user daemon-reload 2>/dev/null || true
# A unit that had been failing stays listed as "not-found failed" after
# its file is gone until its failed state is reset.
systemctl --user reset-failed "$unit" 2>/dev/null || true
else
echo "Could not stop $unit; OpenClaw was not removed." >&2
exit 1
fi
done
omarchy-pkg-drop openclaw
# The web app launcher and icon omarchy-install-ai-openclaw created.
rm -f "$HOME/.local/share/applications/OpenClaw.desktop"
rm -f "$HOME/.local/share/icons/hicolor/256x256/apps/openclaw.png"
gtk-update-icon-cache "$HOME/.local/share/icons/hicolor" &>/dev/null || true
# ~/.openclaw stays unless asked: the chats, memories, and credentials in
# there are the user's agent, and finding them intact after a reinstall is the
# better surprise. But it also holds the plugin runtimes and caches OpenClaw
# downloads for itself, easily hundreds of megabytes, so the choice is put in
# front of the user with the size rather than left silent. Without a terminal
# to ask in, keeping it is the answer.
state_removed=false
if [[ -d $HOME/.openclaw && -t 0 ]] && command -v gum >/dev/null; then
size=$(du -sh "$HOME/.openclaw" 2>/dev/null | cut -f1)
if gum confirm --default=false "Also delete ~/.openclaw ($size: chats, memories, credentials, and downloaded plugins)?"; then
rm -rf "$HOME/.openclaw"
state_removed=true
fi
fi
echo ""
echo "OpenClaw has been removed."
if [[ $state_removed == true ]]; then
echo "Its chats, memories, and settings in ~/.openclaw are gone too."
elif [[ -d $HOME/.openclaw ]]; then
echo "Your agent's chats, memories, and settings are still in ~/.openclaw."
fi