diff --git a/bin/omarchy b/bin/omarchy index 4219109b..2bace6df 100755 --- a/bin/omarchy +++ b/bin/omarchy @@ -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" diff --git a/bin/omarchy-agent b/bin/omarchy-agent index bc65ab9a..0aa0273b 100755 --- a/bin/omarchy-agent +++ b/bin/omarchy-agent @@ -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") diff --git a/bin/omarchy-default-agent b/bin/omarchy-default-agent index 9d575514..f832b22d 100755 --- a/bin/omarchy-default-agent +++ b/bin/omarchy-default-agent @@ -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 " + echo "Usage: omarchy-default-agent " 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 diff --git a/bin/omarchy-install-ai-openclaw b/bin/omarchy-install-ai-openclaw new file mode 100755 index 00000000..cb79a9ad --- /dev/null +++ b/bin/omarchy-install-ai-openclaw @@ -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." diff --git a/bin/omarchy-install-openclaw-cli b/bin/omarchy-install-openclaw-cli new file mode 100755 index 00000000..18e12317 --- /dev/null +++ b/bin/omarchy-install-openclaw-cli @@ -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 diff --git a/bin/omarchy-launch-openclaw b/bin/omarchy-launch-openclaw new file mode 100755 index 00000000..13ab6c24 --- /dev/null +++ b/bin/omarchy-launch-openclaw @@ -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 ]] + +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" diff --git a/bin/omarchy-openclaw-onboard b/bin/omarchy-openclaw-onboard new file mode 100755 index 00000000..5bac15bd --- /dev/null +++ b/bin/omarchy-openclaw-onboard @@ -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" diff --git a/bin/omarchy-remove-ai-openclaw b/bin/omarchy-remove-ai-openclaw new file mode 100755 index 00000000..47658a20 --- /dev/null +++ b/bin/omarchy-remove-ai-openclaw @@ -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 diff --git a/default/fonts/omarchy/README.md b/default/fonts/omarchy/README.md index 03b8a909..a4ab6935 100644 --- a/default/fonts/omarchy/README.md +++ b/default/fonts/omarchy/README.md @@ -13,6 +13,7 @@ The private-use glyphs in `omarchy.ttf` are: - `U+E908` — T3 Code, traced from the app icon in , since upstream publishes no monochrome SVG - `U+E909` — Ori, from , OpenRouter's own mark: Ori ships no separate logo and its product page uses this one - `U+E90A` — Hermes, Font Awesome's staff-snake (CC BY 4.0) from , the mark Hermes serves as its favicon: their app icon is a portrait that reads as a smudge at menu size +- `U+E90C` — OpenClaw, traced from the lobster mascot the openclaw package ships as `dist/control-ui/favicon.svg`, since upstream publishes no monochrome SVG; `E90B` is left for the Perplexity mark already in flight on another branch The agent marks are monochrome so the menu can render them using the active theme's foreground and selection colors. diff --git a/default/fonts/omarchy/omarchy.ttf b/default/fonts/omarchy/omarchy.ttf index 1ddeb85c..fb9d22a7 100644 Binary files a/default/fonts/omarchy/omarchy.ttf and b/default/fonts/omarchy/omarchy.ttf differ diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 30ee45fc..a82254c6 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -143,6 +143,7 @@ "setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"}, "setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"}, "setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"}, + "setup.default.agent.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","checked":"[[ \"$(omarchy-default-agent)\" == \"openclaw\" ]]","action":"omarchy-default-agent openclaw"}, "setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"}, "setup.default.agent.ori": {"icon":"","iconFont":"omarchy","label":"Ori","checked":"[[ \"$(omarchy-default-agent)\" == \"ori\" ]]","action":"omarchy-default-agent ori"}, "setup.default.agent.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"}, @@ -243,6 +244,7 @@ "install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"}, "install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"}, "install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""}, + "install.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","disabled":"omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-openclaw"}, "install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"}, "install.gaming.steam": {"icon":"","label":"Steam","disabled":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"}, "install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","disabled":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"}, @@ -308,6 +310,7 @@ "remove.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"omarchy-pkg-present grok-bot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-grok-bot"}, "remove.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"omarchy-pkg-present lmstudio-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-lm-studio"}, "remove.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"omarchy-pkg-present ollama","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-ollama"}, + "remove.ai.openclaw": {"icon":"","iconFont":"omarchy","label":"OpenClaw","when":"omarchy-pkg-present openclaw","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-openclaw"}, "remove.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","when":"omarchy-pkg-present t3code-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-t3-code"}, "remove.gaming.steam": {"icon":"","label":"Steam","when":"omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-steam"}, "remove.gaming.retroarch": {"icon":"","label":"RetroArch","when":"omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-gaming-retroarch"}, diff --git a/manual/17-ai.md b/manual/17-ai.md index 78b51430..f94c3546 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -44,10 +44,12 @@ Crashes can also be silenced one program at a time, which is what the diagnosis ### Desktop apps -The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, and Hermes Desktop. +The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, Hermes Desktop, and OpenClaw. Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself. +OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw updates arrive through Omarchy's package updates, so skip the Control UI's own "Update Gateway" button: it would try to write into the package-managed install and fail. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside the plugin runtimes OpenClaw downloads for itself; the default keeps it. + ### Local LLMs Omarchy recommends two ways of running local LLM models: LM Studio and Ollama. LM Studio provides a GUI interface for finding open-weight models, installing them, and running them. It's a great way to get going easily. Ollama offers a CLI for doing so similarly. But if you're new to local models, I'd start with LM Studio. You can install either under _Install > AI_ in the Omarchy Menu. diff --git a/test/shell.d/default-agent-test.sh b/test/shell.d/default-agent-test.sh index f421fabb..6a2f15b2 100644 --- a/test/shell.d/default-agent-test.sh +++ b/test/shell.d/default-agent-test.sh @@ -542,3 +542,68 @@ fi grep -F "missing is not installed" "$test_tmp/missing-output" >/dev/null || fail "agent launcher explains when the default command is missing" pass "agent launcher reports a missing default command" + +# OpenClaw comes from its pacman package, not mise: choosing it must route +# through omarchy-install-openclaw-cli and never touch a mise environment. +cat >"$mock_bin/omarchy-pkg-present" <<'SH' +#!/bin/bash +[[ $1 == openclaw && ${OMARCHY_TEST_OPENCLAW_INSTALLED:-false} == "true" ]] +SH +cat >"$mock_bin/omarchy-pkg-add" <<'SH' +#!/bin/bash +printf '%s\n' "pkg-add $*" >>"$OMARCHY_TEST_STUB_LOG" +SH +cat >"$mock_bin/omarchy-launch-openclaw" <<'SH' +#!/bin/bash +printf '%s\0' omarchy-launch-openclaw "$@" >"$OMARCHY_TEST_AGENT_INLINE_LOG" +SH +cat >"$mock_bin/openclaw" <<'SH' +#!/bin/bash +exit 0 +SH +chmod +x "$mock_bin/omarchy-pkg-present" "$mock_bin/omarchy-pkg-add" \ + "$mock_bin/omarchy-launch-openclaw" "$mock_bin/openclaw" + +: >"$launch_log" +: >"$terminal_log" +: >"$mise_history" +OMARCHY_TEST_OPENCLAW_INSTALLED=true omarchy-default-agent openclaw +read -r chosen <"$agent_file" +[[ $chosen == openclaw ]] || fail "choosing OpenClaw records it as the default agent" +mapfile -d '' -t launch_args <"$launch_log" +[[ ${launch_args[*]} == "--app-id=org.omarchy.agent omarchy-launch-openclaw --tui" ]] || + fail "choosing OpenClaw launches its terminal UI" +[[ ! -s $terminal_log ]] || fail "an installed OpenClaw needs no install terminal" +! grep -q 'use -g openclaw' "$mise_history" || fail "OpenClaw never installs through mise" +pass "choosing OpenClaw uses the package and launches its terminal UI" + +: >"$terminal_log" +OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent openclaw +mapfile -d '' -t terminal_args <"$terminal_log" +[[ ${terminal_args[*]} == "omarchy-default-agent --install openclaw" ]] || + fail "a missing OpenClaw routes through the install terminal" +pass "a missing OpenClaw routes through the install terminal" + +: >"$stub_log" +: >"$inline_log" +OMARCHY_TEST_OPENCLAW_INSTALLED=false omarchy-default-agent --install openclaw >/dev/null +grep -Fx "pkg-add openclaw" "$stub_log" >/dev/null || + fail "installing OpenClaw as default agent adds its package" +mapfile -d '' -t inline_args <"$inline_log" +[[ ${inline_args[*]} == "omarchy-launch-openclaw --tui" ]] || + fail "installing OpenClaw as default agent hands over to its terminal UI" +pass "installing OpenClaw as default agent adds its package" + +: >"$launch_log" +omarchy agent prompt "Review this project" +mapfile -d '' -t launch_args <"$launch_log" +# Element-wise: the prompt must travel as one argv entry, which a space-joined +# comparison could not tell apart from a prompt split into words. +[[ ${#launch_args[@]} == 5 && + ${launch_args[0]} == "--app-id=org.omarchy.agent" && + ${launch_args[1]} == "omarchy-launch-openclaw" && + ${launch_args[2]} == "--tui" && + ${launch_args[3]} == "--message" && + ${launch_args[4]} == "Review this project" ]] || + fail "OpenClaw receives prompts through --message" "argv: ${launch_args[*]}" +pass "OpenClaw receives prompts through --message" diff --git a/test/shell.d/launch-openclaw-test.sh b/test/shell.d/launch-openclaw-test.sh new file mode 100755 index 00000000..4c443e07 --- /dev/null +++ b/test/shell.d/launch-openclaw-test.sh @@ -0,0 +1,189 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +mkdir -p "$tmp_dir/bin" "$tmp_dir/home" +export TEST_LOG="$tmp_dir/log" +export PATH="$tmp_dir/bin:$PATH" +export HOME="$tmp_dir/home" + +for stub in omarchy-launch-webapp omarchy-launch-floating-terminal-with-presentation omarchy-openclaw-onboard; do + cat >"$tmp_dir/bin/$stub" <