From 0d64e71e71707c056c827cda65ee64e83d7eda27 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 14 May 2026 15:30:01 +0200 Subject: [PATCH 1/2] feat(codex-fleet): iOS-style pane context menu via display-popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-click context menu was hitting the ceiling of tmux's display-menu: fixed 1-column rows, single-styled title, no per-row two-tone layout for left-icon + label + right-aligned shortcut chip, no rounded card chrome (border-lines are an enum), no live status chip in the header. Swap to display-popup -E -B running a bash renderer that draws the design with ANSI escapes using the existing lib/ios-menu.sh palette helpers — full chrome control, accent-pill hotkey chips, red Kill row, green LIVE header chip, disabled rows muted when swap/zoom not applicable. The mouse-line content travels into the popup pty via set-environment -g CODEX_FLEET_MENU_LINE "#{q:mouse_line}" so embedded quotes/spaces in the line text survive into the "Copy this line" path without fighting shell quoting. Mirrors the same change landed upstream in recodeee/recodee#1932 so the standalone extract stays in lockstep. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/codex-fleet/bin/pane-context-menu.sh | 197 +++++++++++++++++++ scripts/codex-fleet/style-tabs.sh | 26 ++- 2 files changed, 214 insertions(+), 9 deletions(-) create mode 100755 scripts/codex-fleet/bin/pane-context-menu.sh diff --git a/scripts/codex-fleet/bin/pane-context-menu.sh b/scripts/codex-fleet/bin/pane-context-menu.sh new file mode 100755 index 0000000..c3a998a --- /dev/null +++ b/scripts/codex-fleet/bin/pane-context-menu.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# pane-context-menu.sh — iOS-style right-click context menu for fleet panes. +# +# Bound to MouseDown3Pane via scripts/codex-fleet/style-tabs.sh through a +# `display-popup -E -B` so we get a full pty and can draw the rounded card + +# accent shortcut chips that tmux's built-in `display-menu` cannot render. +# +# Usage: pane-context-menu.sh +# pane_id e.g. %47 — set by tmux #{pane_id} at bind time +# +# The line text under the cursor at right-click time is read from +# $CODEX_FLEET_MENU_LINE (set by the MouseDown3Pane binding via +# `set-environment -g CODEX_FLEET_MENU_LINE "#{q:mouse_line}"` so that +# embedded quotes/spaces survive into the popup pty). +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PANE_ID="${1:-}" +MOUSE_LINE="${CODEX_FLEET_MENU_LINE:-}" + +if [[ -z "$PANE_ID" ]]; then + echo "pane-context-menu.sh: missing pane_id arg" >&2 + exit 2 +fi + +# shellcheck source=/dev/null +source "$SCRIPT_DIR/../lib/ios-menu.sh" + +CARD_W=54 +INNER_W=$(( CARD_W - 2 )) + +INDEX="$(tmux display -p -t "$PANE_ID" '#{pane_index}' 2>/dev/null || echo '?')" +PANES_IN_WIN="$(tmux display -p -t "$PANE_ID" '#{window_panes}' 2>/dev/null || echo 1)" +MARKED_ANYWHERE="$(tmux display -p -t "$PANE_ID" '#{pane_marked_set}' 2>/dev/null || echo 0)" +ZOOMED="$(tmux display -p -t "$PANE_ID" '#{window_zoomed_flag}' 2>/dev/null || echo 0)" +PANE_MARKED="$(tmux display -p -t "$PANE_ID" '#{pane_marked}' 2>/dev/null || echo 0)" + +# ── chrome helpers (operate inside the popup's pty) ──────────────────────── +draw_top() { + _ios_sgr "$IOS_BG3" "$IOS_BG" + printf '╭'; _ios_repeat '─' "$INNER_W"; printf '╮' + _ios_reset; printf '\n' +} +draw_bottom() { + _ios_sgr "$IOS_BG3" "$IOS_BG" + printf '╰'; _ios_repeat '─' "$INNER_W"; printf '╯' + _ios_reset; printf '\n' +} +draw_hairline() { + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_sgr "$IOS_BG3" "$IOS_BG2"; _ios_repeat '─' "$INNER_W" + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_reset; printf '\n' +} +draw_blank() { + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_sgr "$IOS_GRAY2" "$IOS_BG2"; printf '%*s' "$INNER_W" '' + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_reset; printf '\n' +} + +# Header row: ● pane · [ LIVE ] +draw_header() { + local title="pane ${INDEX} · ${PANE_ID}" + local chip="LIVE" + local title_len=${#title} + # Layout inside INNER_W: ' ● ' + pad + '[ LIVE ]' + ' ' + # Widths: 1 1 1 +len P 1+1+4+1+1 1 = INNER_W + local chip_render_w=$(( ${#chip} + 4 )) + local pad=$(( INNER_W - 3 - title_len - chip_render_w - 1 )) + (( pad < 1 )) && pad=1 + + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_sgr "$IOS_GREEN" "$IOS_BG2"; printf ' ● ' + _ios_sgr "$IOS_WHITE" "$IOS_BG2" bold; printf '%s' "$title" + _ios_sgr "$IOS_GRAY2" "$IOS_BG2"; printf '%*s' "$pad" '' + _ios_sgr "$IOS_WHITE" "$IOS_GREEN" bold; printf ' %s ' "$chip" + _ios_sgr "$IOS_GRAY2" "$IOS_BG2"; printf ' ' + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_reset; printf '\n' +} + +# Item row layout (INNER_W = 52): +# SP icon SP SP label PAD '[ ' key ' ]' SP +# 1 1 1 1 L P 2 1 2 1 = 9 + L + P → P = INNER_W - 9 - L +# +# Args: icon label key [style: normal|danger|disabled] +draw_item() { + local icon="$1" label="$2" key="$3" style="${4:-normal}" + local label_pad=$(( INNER_W - 9 - ${#label} )) + (( label_pad < 1 )) && label_pad=1 + + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + + case "$style" in + danger) + _ios_sgr "$IOS_RED" "$IOS_BG2"; printf ' %s ' "$icon" + _ios_sgr "$IOS_RED" "$IOS_BG2" bold; printf '%s' "$label" + ;; + disabled) + _ios_sgr "$IOS_GRAY" "$IOS_BG2"; printf ' %s ' "$icon" + _ios_sgr "$IOS_GRAY" "$IOS_BG2"; printf '%s' "$label" + ;; + *) + _ios_sgr "$IOS_GRAY2" "$IOS_BG2"; printf ' %s ' "$icon" + _ios_sgr "$IOS_WHITE" "$IOS_BG2"; printf '%s' "$label" + ;; + esac + + _ios_sgr "$IOS_GRAY2" "$IOS_BG2"; printf '%*s' "$label_pad" '' + if [[ "$style" == "disabled" ]]; then + _ios_sgr "$IOS_GRAY" "$IOS_BG3"; printf '[ ' + _ios_sgr "$IOS_GRAY" "$IOS_BG3" bold; printf '%s' "$key" + _ios_sgr "$IOS_GRAY" "$IOS_BG3"; printf ' ]' + else + _ios_sgr "$IOS_GRAY2" "$IOS_BG3"; printf '[ ' + _ios_sgr "$IOS_WHITE" "$IOS_BG3" bold; printf '%s' "$key" + _ios_sgr "$IOS_GRAY2" "$IOS_BG3"; printf ' ]' + fi + _ios_sgr "$IOS_GRAY2" "$IOS_BG2"; printf ' ' + _ios_sgr "$IOS_BG3" "$IOS_BG"; printf '│' + _ios_reset; printf '\n' +} + +# ── conditional labels / styles ──────────────────────────────────────────── +multi=normal +(( PANES_IN_WIN > 1 )) || multi=disabled +zoom_label="Zoom pane" +(( ZOOMED == 1 )) && zoom_label="Unzoom pane" +zoom_style="$multi" + +swap_marked_style=normal +(( MARKED_ANYWHERE == 1 )) || swap_marked_style=disabled +mark_label="Mark pane" +(( PANE_MARKED == 1 )) && mark_label="Unmark pane" + +# ── render ───────────────────────────────────────────────────────────────── +clear +printf '\n' # small top margin so the popup doesn't crowd row 0 + +draw_top +draw_header +draw_hairline + +draw_item '▣' "Copy whole session" 'C' +draw_item '▢' "Copy visible" 'c' +draw_item '─' "Copy this line" 'l' +draw_hairline +draw_item '⌕' "Search history…" '/' +draw_item '↟' "Scroll to top" '<' +draw_item '↡' "Scroll to bottom" '>' +draw_hairline +draw_item '⊟' "Horizontal split" 'h' +draw_item '⊞' "Vertical split" 'v' +draw_item '⤢' "$zoom_label" 'z' "$zoom_style" +draw_hairline +draw_item '↑' "Swap up" 'u' "$multi" +draw_item '↓' "Swap down" 'd' "$multi" +draw_item '⇄' "Swap with marked" 's' "$swap_marked_style" +draw_item '★' "$mark_label" 'm' +draw_hairline +draw_item '↻' "Respawn pane" 'R' +draw_item '✕' "Kill pane" 'X' danger +draw_bottom + +_ios_sgr "$IOS_GRAY" "$IOS_BG"; printf '\n press a hotkey · esc cancels' +_ios_reset + +# ── input + dispatch ─────────────────────────────────────────────────────── +choice='' +read -rsn1 -t 30 choice || choice='' +clear + +case "$choice" in + C) tmux capture-pane -t "$PANE_ID" -p -S - -E - | wl-copy + tmux display-message -d 1500 '▣ Pane history copied' ;; + c) tmux capture-pane -t "$PANE_ID" -p | wl-copy + tmux display-message -d 1500 '▢ Visible area copied' ;; + l) printf '%s' "$MOUSE_LINE" | wl-copy + tmux display-message -d 1500 '─ Line copied' ;; + /) tmux copy-mode -t "$PANE_ID" + tmux send-keys -X -t "$PANE_ID" search-backward '' ;; + '<') tmux copy-mode -t "$PANE_ID" + tmux send-keys -X -t "$PANE_ID" history-top ;; + '>') tmux copy-mode -t "$PANE_ID" + tmux send-keys -X -t "$PANE_ID" history-bottom ;; + h) tmux split-window -h -t "$PANE_ID" ;; + v) tmux split-window -v -t "$PANE_ID" ;; + z) (( PANES_IN_WIN > 1 )) && tmux resize-pane -Z -t "$PANE_ID" ;; + u) (( PANES_IN_WIN > 1 )) && tmux swap-pane -U -t "$PANE_ID" ;; + d) (( PANES_IN_WIN > 1 )) && tmux swap-pane -D -t "$PANE_ID" ;; + s) (( MARKED_ANYWHERE == 1 )) && tmux swap-pane -t "$PANE_ID" ;; + m) tmux select-pane -m -t "$PANE_ID" ;; + R) tmux respawn-pane -k -t "$PANE_ID" ;; + X) tmux kill-pane -t "$PANE_ID" ;; + *) : ;; +esac diff --git a/scripts/codex-fleet/style-tabs.sh b/scripts/codex-fleet/style-tabs.sh index b9fb67a..8d0a90c 100755 --- a/scripts/codex-fleet/style-tabs.sh +++ b/scripts/codex-fleet/style-tabs.sh @@ -238,17 +238,25 @@ tx_set menu-border-lines "rounded" # because bash splits the {} groups by whitespace. sticky_menu_conf=$(mktemp -t codex-fleet-menu.XXXXXX.tmux.conf) trap 'rm -f "$sticky_menu_conf"' EXIT -# iOS-style sectioned menu: -# 1. CAPTURE — Copy whole session (full scrollback), Copy visible, Copy line -# 2. NAVIGATE — Search history, Scroll to top/bottom -# 3. PANE — Horizontal/Vertical split, Zoom toggle -# 4. ARRANGE — Swap up/down/with-marked, Mark -# 5. DANGER — Respawn, Kill -# Each item prefixed with a glyph for icon-led readability; sections separated -# by tmux's '' separator. `-O` keeps the menu open until selection/Escape. +# iOS-style right-click context menu. +# +# tmux's built-in `display-menu` can't render the rounded card chrome, the +# pill-shaped accent shortcut chips on the right, or the live-status header +# that the operator-approved design calls for — it only exposes menu-style / +# menu-selected-style / menu-border-style plus inline #[…] markup, no +# per-row two-tone layout or right-aligned chip columns. +# +# Switch to `display-popup -E -B`: a full pty inside the popup that runs +# scripts/codex-fleet/bin/pane-context-menu.sh, which draws the design +# directly with ANSI escapes (lib/ios-menu.sh palette + helpers), reads one +# keystroke, and dispatches the same tmux commands the old display-menu did. +# +# CODEX_FLEET_MENU_LINE carries #{mouse_line} into the popup pty so the +# script can implement "Copy this line"; #{q:…} quoting survives embedded +# quotes/spaces in the line content. cat >"$sticky_menu_conf" <<'TMUX_CONF' unbind-key -T root MouseDown3Pane -bind-key -T root MouseDown3Pane if-shell -F -t = "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}" { select-pane -t = ; send-keys -M } { display-menu -O -T "#[align=centre,fg=#FF9500,bold] ◆ pane #{pane_index} · #{pane_id} " -t = -x M -y M " 📋 Copy whole session" C "run-shell \"tmux capture-pane -t '#{pane_id}' -p -S - -E - | wl-copy && tmux display-message -d 1500 '📋 Pane history copied to clipboard'\"" " 📄 Copy visible" c "run-shell \"tmux capture-pane -t '#{pane_id}' -p | wl-copy && tmux display-message -d 1500 '📄 Visible area copied'\"" " ✂ Copy this line" l "run-shell \"echo -n '#{q:mouse_line}' | wl-copy && tmux display-message -d 1500 '✂ Line copied'\"" '' " 🔎 Search history…" / { copy-mode -t= ; send-keys -X search-backward "" } " ⬆ Scroll to top" '<' { copy-mode -t= ; send-keys -X history-top } " ⬇ Scroll to bottom" '>' { copy-mode -t= ; send-keys -X history-bottom } '' " ⬓ Horizontal split" h { split-window -h } " ⬒ Vertical split" v { split-window -v } "#{?#{>:#{window_panes},1},,-} ⛶ #{?window_zoomed_flag,Unzoom,Zoom}" z { resize-pane -Z } '' "#{?#{>:#{window_panes},1},,-} ▲ Swap up" u { swap-pane -U } "#{?#{>:#{window_panes},1},,-} ▼ Swap down" d { swap-pane -D } "#{?pane_marked_set,,-} ⇄ Swap with marked" s { swap-pane } " ◈ #{?pane_marked,Unmark,Mark} pane" m { select-pane -m } '' " ↻ Respawn pane" R { respawn-pane -k } " ✕ Kill pane" X { kill-pane } } +bind-key -T root MouseDown3Pane if-shell -F -t = "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}" { select-pane -t = ; send-keys -M } { set-environment -g CODEX_FLEET_MENU_LINE "#{q:mouse_line}" ; display-popup -E -B -w 60 -h 28 -x M -y M -t = "bash \"${CODEX_FLEET_REPO_ROOT:-$HOME/Documents/recodee}/scripts/codex-fleet/bin/pane-context-menu.sh\" '#{pane_id}'" } # Mouse-wheel scroll into copy-mode even when the pane is in alt-screen # (plan-tree-anim / fleet-state-anim use \033[?1049h; the default tmux From 0a298b6ace0bb8e73cd61f7cc84d7ee298a0f72b Mon Sep 17 00:00:00 2001 From: NagyVikt <nagy.viktordp@gmail.com> Date: Thu, 14 May 2026 15:42:07 +0200 Subject: [PATCH 2/2] fix(codex-fleet): tmux ${VAR:-default} parses as invalid env var name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MouseDown3Pane binding in the previous commit used the shell-style default ${CODEX_FLEET_REPO_ROOT:-$HOME/Documents/recodee} but tmux's ${VAR} substitution does NOT honor shell defaults — it parses the whole `CODEX_FLEET_REPO_ROOT:-$HOME/Documents/recodee` as one variable name, finds `:` inside, and bails with: /tmp/codex-fleet-menu.XXX.tmux.conf:N: invalid environment variable Symptom: style-tabs.sh prints `WARN: sticky menu rebind failed` and the old display-menu binding stays active. Right-click never opens the new popup. Fix: resolve the fallback in bash before sourcing the tmux config, push the resolved value into tmux's global environment, and reference it as a plain ${CODEX_FLEET_REPO_ROOT} in the binding. Mirrors the same fix landing in recodeee/recodee#1932. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- scripts/codex-fleet/style-tabs.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/codex-fleet/style-tabs.sh b/scripts/codex-fleet/style-tabs.sh index 8d0a90c..a5ada2f 100755 --- a/scripts/codex-fleet/style-tabs.sh +++ b/scripts/codex-fleet/style-tabs.sh @@ -254,9 +254,18 @@ trap 'rm -f "$sticky_menu_conf"' EXIT # CODEX_FLEET_MENU_LINE carries #{mouse_line} into the popup pty so the # script can implement "Copy this line"; #{q:…} quoting survives embedded # quotes/spaces in the line content. +# +# CODEX_FLEET_REPO_ROOT must be in the tmux global environment because the +# bind uses tmux's ${VAR} substitution to find pane-context-menu.sh, and +# tmux ${VAR} does NOT support shell-style ${VAR:-default} (it parses the +# whole `VAR:-default` as one variable name and rejects it with "invalid +# environment variable"). Resolve the fallback in bash first, then push it +# into tmux's env so the binding sees a plain ${CODEX_FLEET_REPO_ROOT}. +_repo_root="${CODEX_FLEET_REPO_ROOT:-$HOME/Documents/recodee}" +tmux set-environment -g CODEX_FLEET_REPO_ROOT "$_repo_root" 2>/dev/null || true cat >"$sticky_menu_conf" <<'TMUX_CONF' unbind-key -T root MouseDown3Pane -bind-key -T root MouseDown3Pane if-shell -F -t = "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}" { select-pane -t = ; send-keys -M } { set-environment -g CODEX_FLEET_MENU_LINE "#{q:mouse_line}" ; display-popup -E -B -w 60 -h 28 -x M -y M -t = "bash \"${CODEX_FLEET_REPO_ROOT:-$HOME/Documents/recodee}/scripts/codex-fleet/bin/pane-context-menu.sh\" '#{pane_id}'" } +bind-key -T root MouseDown3Pane if-shell -F -t = "#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}" { select-pane -t = ; send-keys -M } { set-environment -g CODEX_FLEET_MENU_LINE "#{q:mouse_line}" ; display-popup -E -B -w 60 -h 28 -x M -y M -t = "bash ${CODEX_FLEET_REPO_ROOT}/scripts/codex-fleet/bin/pane-context-menu.sh '#{pane_id}'" } # Mouse-wheel scroll into copy-mode even when the pane is in alt-screen # (plan-tree-anim / fleet-state-anim use \033[?1049h; the default tmux