Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions bin/omarchy-agent
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/bin/bash

# omarchy:summary=Launch the default coding agent in a terminal
# omarchy:args=[--inline] [--pick]
# omarchy:args=[--inline] [--pick] [--title=<title>]
# omarchy:examples=omarchy agent | omarchy agent --inline

inline=false
pick=false
title_override=""

# Flags only. A bare prompt would shadow the subcommands under this group, so
# prompts go through omarchy-agent-prompt, which passes one here as --prompt.
Expand All @@ -23,6 +24,14 @@ while (($#)); do
prompt=${2:?--prompt needs a value}
shift 2
;;
--title)
title_override=${2:?--title needs a value}
shift 2
;;
--title=*)
title_override="${1#--title=}"
shift
;;
*)
echo "Unexpected argument: $1" >&2
echo "To pass a prompt: omarchy agent prompt \"$*\"" >&2
Expand Down Expand Up @@ -136,10 +145,20 @@ pi)
;;
esac

if [[ -n ${title_override:-} ]]; then
title="$title_override"
elif [[ -n ${prompt:-} ]]; then
prompt_title="${prompt%%$'\n'*}"
title="$agent: $prompt_title"
else
title="$agent: $(basename "$PWD")"
fi

if [[ $inline == "true" ]]; then
[[ -t 1 ]] && printf '\033]0;%s\007' "$title"
exec "${command[@]}"
else
# A fixed app-id rather than the default org.omarchy.<binary>, so every agent
# window shares one class for window rules and themes to single out.
exec omarchy-launch-tui --app-id=org.omarchy.agent "${command[@]}"
exec omarchy-launch-tui --app-id=org.omarchy.agent --title="$title" "${command[@]}"
fi
30 changes: 23 additions & 7 deletions bin/omarchy-agent-prompt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash

# omarchy:summary=Launch the default coding agent with a prompt
# omarchy:args=[--inline] <prompt...>
# omarchy:args=[--inline] [--title=<title>] <prompt...>
# omarchy:examples=omarchy agent prompt "Review this project"

# Prompts live here rather than on `omarchy agent`, where a bare prompt would
Expand All @@ -10,14 +10,30 @@
set -euo pipefail

inline=()
if [[ ${1:-} == "--inline" ]]; then
inline=(--inline)
shift
fi
title=()
while (($#)); do
case "$1" in
--inline)
inline=(--inline)
shift
;;
--title)
title=(--title "${2:?--title needs a value}")
shift 2
;;
--title=*)
title=(--title "${1#--title=}")
shift
;;
*)
break
;;
esac
done

if (($# == 0)); then
echo "Usage: omarchy agent prompt [--inline] <prompt...>" >&2
echo "Usage: omarchy agent prompt [--inline] [--title=<title>] <prompt...>" >&2
exit 1
fi

exec omarchy-agent "${inline[@]}" --prompt "$*"
exec omarchy-agent "${inline[@]}" "${title[@]}" --prompt "$*"
46 changes: 39 additions & 7 deletions bin/omarchy-launch-tui
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
#!/bin/bash

# omarchy:summary=Launch a TUI command in the default terminal with Omarchy styling
# omarchy:args=[--app-id=<app-id>] <command> [args...]
# omarchy:args=[--app-id=<app-id>] [--title=<title>] <command> [args...]

if [[ ${1:-} == --app-id=* ]]; then
APP_ID="${1#--app-id=}"
shift
else
APP_ID="org.omarchy.$(basename $1)"
app_id=""
title=""

while [[ ${1:-} == --* ]]; do
case "$1" in
--app-id=*)
app_id="${1#--app-id=}"
shift
;;
--title=*)
title="${1#--title=}"
shift
;;
--)
shift
break
;;
*)
break
;;
esac
done

if [[ -z $app_id ]]; then
app_id="org.omarchy.$(basename "$1")"
fi

if [[ -n $title ]]; then
# Ghostty's --title locks the title and ignores later updates from the TUI.
# Set an initial title inside the terminal so session renames still propagate.
# Pass both the title and command as arguments, never interpolated shell code.
set -- bash -c '
title=${1//[[:cntrl:]]/}
shift
printf "\033]0;%s\007" "$title"
exec "$@"
' omarchy-launch-tui "$title" "$@"
fi

exec setsid uwsm-app -- xdg-terminal-exec --app-id=$APP_ID -e "$1" "${@:2}"
exec setsid uwsm-app -- xdg-terminal-exec --app-id="$app_id" -e "$@"
2 changes: 1 addition & 1 deletion config/tmux/tmux.conf
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ set -g window-status-separator ""
set -gw automatic-rename on
set -gw automatic-rename-format '#{b:pane_current_path}'
set -g set-titles on
set -g set-titles-string '#h:#W'
set -g set-titles-string '#{?pane_title,#{pane_title},#h:#W}'

# Theme
set -g status-style "bg=default,fg=default"
Expand Down
36 changes: 22 additions & 14 deletions test/shell.d/default-agent-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ assert_launch() {

printf '%s\n' "$agent" >"$agent_file"
omarchy-agent-prompt "Review this" project
assert_launched "$agent" "forwards the interactive prompt" "$@"
assert_launched "$agent" "forwards the interactive prompt" --title="$agent: Review this project" "$@"
}

assert_bypass() {
Expand All @@ -637,7 +637,7 @@ assert_bypass() {

printf '%s\n' "$agent" >"$agent_file"
omarchy-agent
assert_launched "$agent" "skips permission prompts" "$@"
assert_launched "$agent" "skips permission prompts" --title="$agent: $(basename "$PWD")" "$@"
}

assert_launch pi pi "Review this project"
Expand All @@ -658,13 +658,13 @@ pass "agent launcher adapts initial prompts for every supported agent"
literal_muse_prompt=$'--disable-sandbox !Crash {$(touch must-not-run)}\ntrailing\\ '
printf '%s\n' "muse" >"$agent_file"
omarchy-agent-prompt "$literal_muse_prompt"
assert_launched muse "separates prompt text from options" muse --approval-mode never -- "$literal_muse_prompt"
assert_launched muse "separates prompt text from options" --title="muse: ${literal_muse_prompt%%$'\n'*}" muse --approval-mode never -- "$literal_muse_prompt"
pass "Muse receives option-like prompts as one literal argument"

literal_hermes_prompt=$' --help !Crash /quit {$(touch must-not-run)}\ntrailing\\ '
printf '%s\n' "hermes" >"$agent_file"
omarchy-agent-prompt "$literal_hermes_prompt"
assert_launched hermes "binds its literal initial prompt" env -u HERMES_SESSION_SOURCE \
assert_launched hermes "binds its literal initial prompt" --title="hermes: ${literal_hermes_prompt%%$'\n'*}" env -u HERMES_SESSION_SOURCE \
hermes chat --yolo --tui "--query=$literal_hermes_prompt"
pass "Hermes receives prompted launches as one literal query argument"

Expand All @@ -686,7 +686,7 @@ pass "agent launcher skips permission prompts for every supported agent"
printf '%s\n' "opencode" >"$agent_file"
omarchy-agent
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent opencode --auto" ]] ||
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent --title=opencode: $(basename "$PWD") opencode --auto" ]] ||
fail "agent launcher starts the selected agent without an initial prompt"
pass "agent launcher starts the selected agent without an initial prompt"

Expand All @@ -701,23 +701,23 @@ pass "inline agent launcher runs in the current terminal"
: >"$launch_log"
omarchy agent
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent opencode --auto" ]] ||
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent --title=opencode: $(basename "$PWD") opencode --auto" ]] ||
fail "omarchy agent routes to the launcher"

# With an agent chosen there is nothing to pick, so the keybinding launches.
: >"$launch_log"
: >"$menu_log"
omarchy-agent --pick
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent opencode --auto" ]] ||
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent --title=opencode: $(basename "$PWD") opencode --auto" ]] ||
fail "--pick launches once an agent is chosen"
[[ ! -s $menu_log ]] || fail "--pick opens no menu once an agent is chosen"
pass "--pick launches once an agent is chosen"

: >"$launch_log"
omarchy agent prompt "Review this project"
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent opencode --auto --prompt Review this project" ]] ||
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent --title=opencode: Review this project opencode --auto --prompt Review this project" ]] ||
fail "omarchy agent prompt routes the prompt to the launcher"

: >"$launch_log"
Expand Down Expand Up @@ -765,7 +765,7 @@ 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" ]] ||
[[ ${launch_args[*]} == "--app-id=org.omarchy.agent --title=openclaw: $(basename "$PWD") 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"
Expand Down Expand Up @@ -793,11 +793,19 @@ 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[@]} == 6 &&
${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" ]] ||
${launch_args[1]} == "--title=openclaw: Review this project" &&
${launch_args[2]} == "omarchy-launch-openclaw" &&
${launch_args[3]} == "--tui" &&
${launch_args[4]} == "--message" &&
${launch_args[5]} == "Review this project" ]] ||
fail "OpenClaw receives prompts through --message" "argv: ${launch_args[*]}"
pass "OpenClaw receives prompts through --message"

: >"$launch_log"
omarchy-agent --title "Custom Session Title"
mapfile -d '' -t launch_args <"$launch_log"
[[ ${launch_args[1]} == "--title=Custom Session Title" ]] ||
fail "agent launcher accepts --title override"
pass "agent launcher accepts --title override"
97 changes: 97 additions & 0 deletions test/shell.d/launch-tui-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/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

cat >"$tmp_dir/setsid" <<'SUB'
#!/bin/bash
printf '%s\0' "$@" >"$TEST_LOG"
SUB
chmod +x "$tmp_dir/setsid"

export TEST_LOG="$tmp_dir/log"
export PATH="$tmp_dir:$ROOT/bin:$PATH"

assert_tui_launch() {
local description=$1
shift
local expected=("$@")

: >"$TEST_LOG"
"$ROOT/bin/omarchy-launch-tui" "${input_args[@]}"

mapfile -d '' -t actual <"$TEST_LOG"
(( ${#actual[@]} == ${#expected[@]} )) ||
fail "omarchy-launch-tui $description" "expected length ${#expected[@]}, got ${#actual[@]}\nexpected: ${expected[*]}\nactual: ${actual[*]}"

for ((index = 0; index < ${#expected[@]}; index++)); do
[[ ${actual[$index]} == "${expected[$index]}" ]] ||
fail "omarchy-launch-tui $description" "mismatch at $index: expected '${expected[$index]}', got '${actual[$index]}'"
done
pass "omarchy-launch-tui $description"
}

input_args=(nvim)
assert_tui_launch "defaults app-id to command name without title" \
uwsm-app -- xdg-terminal-exec --app-id=org.omarchy.nvim -e nvim

input_args=(--app-id=org.omarchy.agent agy)
assert_tui_launch "accepts custom app-id" \
uwsm-app -- xdg-terminal-exec --app-id=org.omarchy.agent -e agy

# Execute the command handed to the terminal to check the actual OSC output,
# argv boundaries, subsequent title updates, and exit status.
cat >"$tmp_dir/agy" <<'SUB'
#!/bin/bash
: >"$COMMAND_LOG"
if (($#)); then
printf '%s\0' "$@" >"$COMMAND_LOG"
fi
printf '\033]0;Renamed session\007'
exit "${COMMAND_STATUS:-0}"
SUB
chmod +x "$tmp_dir/agy"
ln -s agy "$tmp_dir/claude"
export COMMAND_LOG="$tmp_dir/command-log"

assert_titled_launch() {
local description=$1 app_id=$2 title=$3
shift 3
local expected=("$@") actual=() command_args=()

"$ROOT/bin/omarchy-launch-tui" "${input_args[@]}"
mapfile -d '' -t actual <"$TEST_LOG"
[[ ${actual[0]} == "uwsm-app" && ${actual[1]} == "--" &&
${actual[2]} == "xdg-terminal-exec" && ${actual[3]} == "--app-id=$app_id" &&
${actual[4]} == "-e" ]] || fail "$description: terminal options must allow title updates"

local output status=0
output=$("${actual[@]:5}") || status=$?
(( status == ${COMMAND_STATUS:-0} )) || fail "$description: command exit status is preserved"
[[ $output == $'\033]0;'"$title"$'\007\033]0;Renamed session\007' ]] ||
fail "$description: initial and subsequent titles are emitted"
mapfile -d '' -t command_args <"$COMMAND_LOG"
(( ${#command_args[@]} == ${#expected[@]} )) || fail "$description: command argv length"
for ((index = 0; index < ${#expected[@]}; index++)); do
[[ ${command_args[index]} == "${expected[index]}" ]] || fail "$description: command argument $index"
done
pass "omarchy-launch-tui $description"
}

input_args=(--title="agy: my-proj" agy)
assert_titled_launch "accepts title without custom app-id" org.omarchy.agy "agy: my-proj"

input_args=(--app-id=org.omarchy.agent --title="agy: my-proj" agy --dangerously-skip-permissions)
assert_titled_launch "accepts both app-id and title with extra command arguments" org.omarchy.agent "agy: my-proj" --dangerously-skip-permissions

input_args=(--title="claude: test" --app-id=org.omarchy.agent claude --permission-mode auto)
assert_titled_launch "accepts title before app-id" org.omarchy.agent "claude: test" --permission-mode auto

literal_title=$'Review "quotes"; $(exit 99) `exit 98` \a\033\n café'
input_args=(--title="$literal_title" agy "two words" "" '--title=literal' '$HOME; exit 97')
COMMAND_STATUS=23 assert_titled_launch "keeps title and command text literal and preserves exit status" org.omarchy.agy \
'Review "quotes"; $(exit 99) `exit 98` café' "two words" "" '--title=literal' '$HOME; exit 97'