#!/usr/bin/env bash # workmux-palette — workmux as a tmux-palette source (the "workmux" palette, # ~/.config/tmux-palette/palettes/workmux.json). # # workmux-palette print palette items (JSON): every agent # workmux tracks in this tmux server (waiting, # then done, then working), the worktrees of the # active pane's repo with Close/Merge/Remove # items, New from a prompt, and the dashboard, # sidebar and last-done tools # workmux-palette --add BRANCH the palette's query item (what was typed): # `workmux add BRANCH` in the active pane's repo # workmux-palette --prompt the New item's popup body: `gum write` the # task (enter submits, ctrl+j newline, esc # cancels), then `workmux add -A -p TASK` # workmux-palette --popup DIR CMD… # run CMD… in DIR in a popup that closes when it # succeeds and waits for a key when it fails # workmux-palette --do DIR CMD… # run CMD… in DIR; a failure shows its last # output line as a tmux message # # Lifecycle commands (add, merge, remove) run in a popup: they print hook # output and may ask y/n (remove with unmerged commits). Open and close are # quick, so they run in place. set -uo pipefail self=$(readlink -f "$0") case ${1-} in --exec) # the popup's command (see --popup) shift "$@" status=$? [ "$status" -eq 0 ] && exit 0 printf '\n\033[2m[exit %s] any key to close\033[0m' "$status" read -rsn1 exit 0 ;; --popup) dir=$2 shift 2 # argv form: tmux runs it without a shell and expands no formats in it. title=${*/#"$self"/${self##*/}} exec tmux display-popup -E -w 80% -h 60% -d "$dir" -T " ${title//#/##} " "$self" --exec "$@" ;; --do) cd "$2" || exit 1 shift 2 if ! out=$("$@" 2>&1); then msg=$(tail -n1 <<<"$out") tmux display-message -d 5000 "workmux: ${msg//#/##}" fi exit 0 ;; --add) cwd=$(tmux display -p '#{pane_current_path}') exec "$self" --popup "$cwd" workmux add "$2" ;; --prompt) # The branch name comes from omp under auto_name in workmux's config.yaml. task=$(gum write --width 0 --height 8 \ --header "New worktree in ${PWD##*/}: describe the task (enter submits, ctrl+j newline, esc cancels)" \ --placeholder "What should the agent do?") || exit 0 [ -n "${task//[[:space:]]/}" ] || exit 0 printf '→ naming the branch…\n' exec workmux add -A -p "$task" ;; esac cwd=$(tmux display -p '#{pane_current_path}' 2>/dev/null) || cwd=$PWD root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || root= worktrees='[]' [ -n "$root" ] && worktrees=$(cd "$root" && workmux list --json 2>/dev/null) || worktrees='[]' workmux status --all --json 2>/dev/null | jq --arg self "$self" --arg home "$HOME" --arg root "$root" \ --argjson worktrees "${worktrees:-[]}" ' def tilde: if startswith($home) then "~" + .[($home | length):] else . end; def ago: if . < 60 then "\(.)s" elif . < 3600 then "\(. / 60 | floor)m" else "\(. / 3600 | floor)h\(. % 3600 / 60 | floor)m" end; def run($mode; $argv): {shell: ([$self, $mode, $root] + $argv | map(@sh) | join(" "))}; # An agent that registered but reported nothing yet has status "-". {working: {icon: "\uf110", color: "#5ea1ff", rank: 2, label: "working"}, waiting: {icon: "\uf075", color: "#f1ff5e", rank: 0, label: "waiting"}, done: {icon: "\uf00c", color: "#5eff6c", rank: 1, label: "done"}} as $look | {icon: "\uf544", color: "#7b8496", rank: 3, label: "idle"} as $idle | ($worktrees | map(select(.is_main | not))) as $trees | (if $root == "" then "" else $root | split("/") | last end) as $repo | [ ((.agents // []) | map(. + {look: ($look[.status // ""] // $idle)}) | sort_by([.look.rank, (.elapsed_secs // 0)])[] | {category: "Agents", icon: .look.icon, iconColor: .look.color, title: (if .worktree == .project then .project else "\(.project)/\(.worktree)" end), description: ([.look.label + (if .elapsed_secs == null then "" else " \(.elapsed_secs | ago)" end), (if (.title // "") == "" then .session else .title end)] | join(" · ")), action: {tmux: "switch-client -t \(.pane_id | @sh)"}}), ($trees[] | {category: "Worktrees: \($repo)", icon: "\uf126", title: .handle, description: ([.branch, (if .is_open then "open" else "closed" end)] + (if .has_uncommitted_changes then ["dirty"] else [] end) + [.path | tilde] | join(" · ")), action: run("--do"; ["workmux", "open", .handle])} + (if .is_open then {iconColor: "#5eff6c"} else {} end)), (if $root == "" then empty else {category: "New", icon: "\uf067", title: "New worktree from a prompt", description: "type the task (gum write); omp names the branch · \($repo)", aliases: ["add", "prompt"], action: run("--popup"; [$self, "--prompt"])} end), {category: "Workmux", icon: "\uf0e4", title: "Dashboard", description: "agents and worktrees TUI in a popup", aliases: ["dash"], action: {tmux: "display-popup -E -w 90% -h 90% workmux dashboard"}}, {category: "Workmux", icon: "\uf0db", title: "Sidebar", description: "toggle the agent status sidebar", shortcut: "Ctrl+Shift+S", aliases: ["side"], action: {tmux: "run-shell -b \("workmux sidebar" | @sh)"}}, {category: "Workmux", icon: "\uf0e2", title: "Last done", description: "jump to the agent that last finished or is waiting", aliases: ["ld"], action: {tmux: "run-shell -b \("workmux last-done" | @sh)"}}, ($trees[] | select(.is_open) | {category: "Close", icon: "\uf00d", title: "Close \(.handle)", description: "close the session, keep the worktree and branch", action: run("--do"; ["workmux", "close", .handle])}), ($trees[] | {category: "Merge", icon: "\uf419", title: "Merge \(.handle)", description: "merge \(.branch) into the main branch, then remove it", action: run("--popup"; ["workmux", "merge", .handle])}), ($trees[] | {category: "Remove", icon: "\uf1f8", title: "Remove \(.handle)", description: "remove the worktree, session and branch without merging", action: run("--popup"; ["workmux", "remove", .handle])}) ]'