@humanu/orchestra 0.5.65 → 0.5.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanu/orchestra",
3
- "version": "0.5.65",
3
+ "version": "0.5.69",
4
4
  "description": "AI-powered Git worktree and tmux session manager with modern TUI",
5
5
  "keywords": [
6
6
  "git",
@@ -109,6 +109,57 @@ _tmux_normalize_app_from_command() {
109
109
  printf '%s' "$base"
110
110
  }
111
111
 
112
+ _tmux_truthy() {
113
+ case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
114
+ 1|yes|true|on)
115
+ return 0
116
+ ;;
117
+ esac
118
+ return 1
119
+ }
120
+
121
+ _tmux_is_known_tui_app() {
122
+ local app
123
+ app="$(_tmux_normalize_app_from_command "$1")"
124
+ case "$app" in
125
+ opencode|claude|vim|nvim|vi|lazygit|gitui|tig|top|htop|btop|k9s|fzf|yazi|ranger|nnn|less|man)
126
+ return 0
127
+ ;;
128
+ esac
129
+ return 1
130
+ }
131
+
132
+ _tmux_is_tui_context() {
133
+ local pane_cmd="$1"
134
+ local alternate_on="$2"
135
+ local mouse_any_flag="$3"
136
+ local pane_mode="$4"
137
+ local window_name="$5"
138
+ local pane_title="$6"
139
+
140
+ if _tmux_truthy "$alternate_on" || _tmux_truthy "$mouse_any_flag"; then
141
+ return 0
142
+ fi
143
+
144
+ if [[ -n "$pane_mode" ]]; then
145
+ return 0
146
+ fi
147
+
148
+ if _tmux_is_known_tui_app "$pane_cmd"; then
149
+ return 0
150
+ fi
151
+
152
+ local combined
153
+ combined="$(printf '%s %s' "$window_name" "$pane_title" | tr '[:upper:]' '[:lower:]')"
154
+ case "$combined" in
155
+ *opencode*|*claude*|*vim*|*nvim*|*lazygit*|*gitui*|*tig*|*k9s*|*fzf*|*yazi*|*ranger*|*nnn*)
156
+ return 0
157
+ ;;
158
+ esac
159
+
160
+ return 1
161
+ }
162
+
112
163
  # Helper: absolute path to the command hook script (if present)
113
164
  _orchestra_command_hook() {
114
165
  local hook=""
@@ -145,6 +196,10 @@ _orchestra_command_hook() {
145
196
  echo ""
146
197
  }
147
198
 
199
+ _orchestra_bridge_script() {
200
+ echo "$(dirname "$_TMUX_API_DIR")/gw-bridge.sh"
201
+ }
202
+
148
203
  # Source the command hook inside a tmux session to enable command history logging
149
204
  _tmux_source_command_hook() {
150
205
  local session="$1"
@@ -156,6 +211,8 @@ _tmux_source_command_hook() {
156
211
  if ! tmux_session_exists "$session"; then
157
212
  return
158
213
  fi
214
+ local bridge
215
+ bridge="$(_orchestra_bridge_script)"
159
216
  local panes
160
217
  panes=$(tmux list-panes -t "$session" -F '#{pane_id} #{pane_current_command}' 2>/dev/null || echo "")
161
218
  if [[ -z "$panes" ]]; then
@@ -168,7 +225,7 @@ _tmux_source_command_hook() {
168
225
  pane_cmd="${line#* }"
169
226
  case "$pane_cmd" in
170
227
  bash|zsh)
171
- tmux send-keys -t "$pane_id" ". '$hook'" C-m 2>/dev/null || true
228
+ tmux send-keys -t "$pane_id" "export ORCHESTRA_BRIDGE_PATH='$bridge'; . '$hook'" C-m 2>/dev/null || true
172
229
  ;;
173
230
  *)
174
231
  ;;
@@ -179,6 +236,51 @@ _tmux_source_command_hook() {
179
236
  # Helper: orchestra prefix including delimiter
180
237
  _tmux_orch_prefix() { printf "orchestra%s" "$( _tmux_delim )"; }
181
238
 
239
+ _tmux_status_escape_text() {
240
+ local text="$1"
241
+ text="${text//\#/##}"
242
+ printf '%s' "$text"
243
+ }
244
+
245
+ _tmux_orchestra_status_left() {
246
+ local worktree_name="$1"
247
+ local session_display_name="$2"
248
+ worktree_name="$(_tmux_status_escape_text "$worktree_name")"
249
+ session_display_name="$(_tmux_status_escape_text "$session_display_name")"
250
+ printf '#[fg=white,bg=colour22,bold] %s #[default] Rename: Ctrl+b,r | Go back (detach): Ctrl+b,d | Copy (scroll) mode: Ctrl+b,[ | Worktree: %s' "$session_display_name" "$worktree_name"
251
+ }
252
+
253
+ _tmux_configure_orchestra_bindings() {
254
+ local bridge
255
+ bridge="$(_orchestra_bridge_script)"
256
+ [[ -f "$bridge" ]] || return
257
+
258
+ local quoted_bridge rename_command prompt_command
259
+ printf -v quoted_bridge '%q' "$bridge"
260
+ rename_command="$quoted_bridge manual-rename-session \\\"#{session_name}\\\" \\\"%%\\\" >/dev/null 2>&1"
261
+ prompt_command="command-prompt -p 'Rename Orchestra session:' 'run-shell -b \"$rename_command\"'"
262
+
263
+ tmux bind-key -T prefix r if-shell -F '#{@orchestra_display_name}' \
264
+ "$prompt_command" 'refresh-client -S' >/dev/null 2>&1 || true
265
+ }
266
+
267
+ _tmux_configure_orchestra_status() {
268
+ local session_name="$1"
269
+ local worktree_name="$2"
270
+ local session_display_name="${3:-}"
271
+ local status_left
272
+ if [[ -z "$session_display_name" ]]; then
273
+ session_display_name="$(tmux_format_session_display "$session_name" without-timestamp)"
274
+ fi
275
+ status_left="$(_tmux_orchestra_status_left "$worktree_name" "$session_display_name")"
276
+
277
+ tmux set-option -t "$session_name" @orchestra_display_name "$session_display_name" >/dev/null 2>&1 || true
278
+ tmux set-option -t "$session_name" @orchestra_worktree_name "$worktree_name" >/dev/null 2>&1 || true
279
+ tmux set-option -t "$session_name" status-left "$status_left" >/dev/null 2>&1 || true
280
+ tmux set-option -t "$session_name" status-left-length 220 >/dev/null 2>&1 || true
281
+ _tmux_configure_orchestra_bindings
282
+ }
283
+
182
284
  # Helper: split a string by multi-char delimiter into bash array named by ref
183
285
  # Usage: _tmux_split_by_delim "string" "::" out_array_name
184
286
  _tmux_split_by_delim() {
@@ -289,10 +391,7 @@ tmux_create_session() {
289
391
  else
290
392
  worktree_name="$(basename "$working_dir")"
291
393
  fi
292
- tmux set-option -t "$session_name" status-left "Go back (detach): Ctrl+b,d | Copy (scroll) mode: Ctrl+b,[ | Worktree: ${worktree_name}" >/dev/null 2>&1 || true
293
-
294
- # Increase status-left length to accommodate the message
295
- tmux set-option -t "$session_name" status-left-length 120 >/dev/null 2>&1 || true
394
+ _tmux_configure_orchestra_status "$session_name" "$worktree_name"
296
395
  fi
297
396
 
298
397
  echo "$session_name"
@@ -432,8 +531,7 @@ tmux_attach_session() {
432
531
  worktree_name="$branch_name"
433
532
  fi
434
533
  fi
435
- tmux set-option -t "$sess" status-left "Go back (detach): Ctrl+b,d | Copy (scroll) mode: Ctrl+b,[ | Worktree: ${worktree_name}" >/dev/null 2>&1 || true
436
- tmux set-option -t "$sess" status-left-length 120 >/dev/null 2>&1 || true
534
+ _tmux_configure_orchestra_status "$sess" "$worktree_name"
437
535
 
438
536
  if tmux_inside_session; then
439
537
  tmux switch-client -t "$sess" >/dev/null 2>&1 || true
@@ -511,6 +609,24 @@ tmux_rename_session() {
511
609
  err "Failed to rename session"
512
610
  return 1
513
611
  }
612
+ local worktree_name
613
+ worktree_name="$(tmux show-option -t "$new_session" -qv @orchestra_worktree_name 2>/dev/null || true)"
614
+ if [[ -z "$worktree_name" ]]; then
615
+ local session_dir old_pwd branch_name
616
+ session_dir="$(tmux display-message -t "$new_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
617
+ if [[ -n "$session_dir" && -d "$session_dir" ]]; then
618
+ old_pwd="$PWD"
619
+ cd "$session_dir" 2>/dev/null || true
620
+ branch_name="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
621
+ cd "$old_pwd" 2>/dev/null || true
622
+ if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
623
+ worktree_name="$branch_name"
624
+ else
625
+ worktree_name="$(basename "$session_dir")"
626
+ fi
627
+ fi
628
+ fi
629
+ _tmux_configure_orchestra_status "$new_session" "$worktree_name"
514
630
  >&2 echo "✏️ Renamed session to: $new_name"
515
631
  return 0
516
632
  }
@@ -595,66 +711,119 @@ tmux_find_session() {
595
711
 
596
712
  # Helper function to format session display names
597
713
  # Input: session_content (e.g., "opencode_fixing_auth_bug" or "my_feature_work")
598
- # Output: "Opencode: Fixing Auth Bug" or "My Feature Work"
714
+ # Output: "Opencode Fixing Auth Bug" or "My Feature Work"
599
715
  format_session_display_name() {
600
716
  local session_content="$1"
601
-
602
- # Known app prefixes and their display names
603
- local known_apps=("opencode" "running" "ssh" "docker" "k8s" "git" "db")
604
- local app_prefix=""
605
- local description=""
606
-
607
- # Check if session starts with a known app prefix
608
- for app in "${known_apps[@]}"; do
609
- if [[ "$session_content" =~ ^${app}_ ]]; then
610
- app_prefix="$app"
611
- # Remove the app prefix and following underscore
612
- description="${session_content#${app}_}"
613
- break
614
- fi
615
- done
616
-
617
- # If no app prefix found, treat whole string as description
618
- if [[ -z "$app_prefix" ]]; then
619
- description="$session_content"
717
+ local d
718
+ d="$(_tmux_delim)"
719
+ local ORCH_PREFIX
720
+ ORCH_PREFIX="$(_tmux_orch_prefix)"
721
+ if [[ "$session_content" == ${ORCH_PREFIX}* ]]; then
722
+ session_content="${session_content#${ORCH_PREFIX}}"
620
723
  fi
621
-
622
- # Convert underscores and hyphens to spaces and apply sentence case
623
- description="$(echo "$description" | tr '_-' ' ')"
624
-
625
- # Apply sentence case (capitalize first letter of each word)
724
+ if [[ "$session_content" == auto_* ]]; then
725
+ session_content="${session_content#auto_}"
726
+ fi
727
+ local description
728
+ description="$(echo "$session_content" | tr '_-' ' ')"
626
729
  description="$(echo "$description" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1')"
627
-
628
- # Format final display name
629
- if [[ -n "$app_prefix" ]]; then
630
- # Capitalize app name and format as "App: Description"
631
- local app_display="$(echo "$app_prefix" | awk '{print toupper(substr($1,1,1)) tolower(substr($1,2))}')"
632
- echo "${app_display}: ${description}"
633
- else
634
- echo "$description"
730
+ echo "$description"
731
+ }
732
+
733
+ _tmux_format_session_timestamp() {
734
+ local date_part="$1"
735
+ local time_part="$2"
736
+
737
+ if [[ ! "$date_part" =~ ^[0-9]{8}$ || ! "$time_part" =~ ^[0-9]{6}$ ]]; then
738
+ printf '%s %s\n' "$date_part" "$time_part"
739
+ return
740
+ fi
741
+
742
+ local month day hour minute ampm display_hour
743
+ month=$((10#${date_part:4:2}))
744
+ day=$((10#${date_part:6:2}))
745
+ hour=$((10#${time_part:0:2}))
746
+ minute="${time_part:2:2}"
747
+
748
+ ampm="am"
749
+ display_hour="$hour"
750
+ if (( hour >= 12 )); then
751
+ ampm="pm"
752
+ if (( hour > 12 )); then
753
+ display_hour=$((hour - 12))
754
+ fi
635
755
  fi
756
+ if (( hour == 0 )); then
757
+ display_hour=12
758
+ fi
759
+
760
+ local month_names=("" "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
761
+ local month_name="???"
762
+ if (( month > 0 && month <= 12 )); then
763
+ month_name="${month_names[$month]}"
764
+ fi
765
+
766
+ printf '%s %s %s:%s%s\n' "$month_name" "$day" "$display_hour" "$minute" "$ampm"
636
767
  }
637
768
 
638
769
  # Parse session name and format for display
639
770
  # Input: session_name (format: worktreename_repohash_YYYYMMDD_HHMMSS_readable_name or old formats)
640
- # Output: "App: Description" or formatted display name
771
+ # Output: formatted display name
641
772
  # Usage: tmux_format_session_display <session_name>
642
773
  tmux_format_session_display() {
643
774
  local session_name="$1"
644
-
775
+ local timestamp_mode="${2:-with-timestamp}"
776
+
645
777
  # Handle temporary renaming sessions first
646
778
  if [[ "$session_name" =~ _renaming$ ]]; then
647
779
  # Extract base name before _renaming suffix
648
780
  local base_name="${session_name%_renaming}"
649
781
  # Recursively process the base name to get proper display
650
- tmux_format_session_display "$base_name"
782
+ tmux_format_session_display "$base_name" "$timestamp_mode"
651
783
  return $?
652
784
  fi
653
785
 
654
- # Strip leading orchestra_ prefix if present
655
- if [[ "$session_name" =~ ^orchestra_(.+)$ ]]; then
786
+ local d ORCH_PREFIX
787
+ d="$(_tmux_delim)"
788
+ ORCH_PREFIX="$(_tmux_orch_prefix)"
789
+
790
+ if [[ "$session_name" == ${ORCH_PREFIX}* ]]; then
791
+ session_name="${session_name#${ORCH_PREFIX}}"
792
+ elif [[ "$session_name" =~ ^orchestra_(.+)$ ]]; then
656
793
  session_name="${BASH_REMATCH[1]}"
657
794
  fi
795
+
796
+ local parts
797
+ _tmux_split_by_delim "$session_name" "$d" parts
798
+ local idx_time=-1 idx_date=-1 i
799
+ for (( i=${#parts[@]}-1; i>=0; i-- )); do
800
+ if [[ ${parts[$i]} =~ ^[0-9]{6}$ ]] && (( i > 0 )) && [[ ${parts[$((i-1))]} =~ ^[0-9]{8}$ ]]; then
801
+ idx_time=$i
802
+ idx_date=$((i-1))
803
+ break
804
+ fi
805
+ done
806
+ if (( idx_time >= 0 && idx_date >= 0 )); then
807
+ local readable_name=""
808
+ local j
809
+ for (( j=idx_time+1; j<${#parts[@]}; j++ )); do
810
+ if [[ -n "$readable_name" ]]; then
811
+ readable_name+="$d"
812
+ fi
813
+ readable_name+="${parts[$j]}"
814
+ done
815
+ if [[ -n "$readable_name" ]]; then
816
+ local formatted_name timestamp
817
+ formatted_name="$(format_session_display_name "$readable_name")"
818
+ timestamp="$(_tmux_format_session_timestamp "${parts[$idx_date]}" "${parts[$idx_time]}")"
819
+ if [[ "$timestamp_mode" == "without-timestamp" ]]; then
820
+ echo "$formatted_name"
821
+ else
822
+ echo "${formatted_name} (${timestamp})"
823
+ fi
824
+ return 0
825
+ fi
826
+ fi
658
827
 
659
828
  # New format with repo hash: worktreename_repohash_YYYYMMDD_HHMMSS_readable_name
660
829
  if [[ "$session_name" =~ ^[^_]+_[a-f0-9]{8}_([0-9]{8})_([0-9]{6})_(.+)$ ]]; then
@@ -696,13 +865,17 @@ tmux_format_session_display() {
696
865
 
697
866
  # Format the readable name with proper formatting
698
867
  local formatted_name="$(format_session_display_name "$readable_name")"
699
-
700
- echo "${formatted_name} (${month_name} ${display_day} ${display_hour}:${minute}${ampm})"
868
+
869
+ if [[ "$timestamp_mode" == "without-timestamp" ]]; then
870
+ echo "$formatted_name"
871
+ else
872
+ echo "${formatted_name} (${month_name} ${display_day} ${display_hour}:${minute}${ampm})"
873
+ fi
701
874
  # Fallback: Try format without repo hash: appname_description_YYYYMMDD_HHMMSS
702
875
  elif [[ "$session_name" =~ ^(.*)_[0-9]{8}_[0-9]{6}$ ]]; then
703
876
  local session_content="${BASH_REMATCH[1]}" # appname_description
704
877
 
705
- # Convert to proper display format with app prefix
878
+ # Convert to proper display format
706
879
  local formatted_name="$(format_session_display_name "$session_content")"
707
880
  echo "$formatted_name"
708
881
  else
@@ -746,8 +919,12 @@ tmux_format_session_display() {
746
919
 
747
920
  # Format the readable name with proper formatting
748
921
  local formatted_name="$(format_session_display_name "$readable_name")"
749
-
750
- echo "${formatted_name} (${month_name} ${display_day} ${display_hour}:${minute}${ampm})"
922
+
923
+ if [[ "$timestamp_mode" == "without-timestamp" ]]; then
924
+ echo "$formatted_name"
925
+ else
926
+ echo "${formatted_name} (${month_name} ${display_day} ${display_hour}:${minute}${ampm})"
927
+ fi
751
928
  else
752
929
  # Fallback for sessions that don't match either format
753
930
  echo "$session_name"
@@ -1045,7 +1222,6 @@ NODE
1045
1222
  # Usage: tmux_generate_ai_session_name <session_name>
1046
1223
  tmux_generate_ai_session_name() {
1047
1224
  local session="$1"
1048
- TMUX_AI_APP_SOURCE=""
1049
1225
 
1050
1226
  # Load AI provider config and API keys
1051
1227
  tmux_load_ai_primary_provider
@@ -1077,20 +1253,17 @@ tmux_generate_ai_session_name() {
1077
1253
 
1078
1254
  local openai_model="${OPENAI_MODEL:-gpt-4o-mini}"
1079
1255
 
1080
- # Capture the tmux pane content (last 200 lines for better context)
1081
- local content
1082
- content="$(tmux capture-pane -t "$session" -p -S -200 2>/dev/null)" || {
1083
- err "Failed to capture tmux pane content"
1084
- return 1
1085
- }
1086
-
1087
- local pane_cmd
1256
+ local pane_cmd pane_mode window_name pane_title alternate_on mouse_any_flag
1088
1257
  pane_cmd="$(tmux display-message -t "$session" -p '#{pane_current_command}' 2>/dev/null || echo "")"
1258
+ pane_mode="$(tmux display-message -t "$session" -p '#{pane_mode}' 2>/dev/null || echo "")"
1259
+ window_name="$(tmux display-message -t "$session" -p '#{window_name}' 2>/dev/null || echo "")"
1260
+ pane_title="$(tmux display-message -t "$session" -p '#{pane_title}' 2>/dev/null || echo "")"
1261
+ alternate_on="$(tmux display-message -t "$session" -p '#{alternate_on}' 2>/dev/null || echo "0")"
1262
+ mouse_any_flag="$(tmux display-message -t "$session" -p '#{mouse_any_flag}' 2>/dev/null || echo "0")"
1089
1263
 
1090
1264
  local history_temp=""
1091
1265
  local history_arg=""
1092
1266
  local history_dir="${ORCHESTRA_HISTORY_DIR:-$HOME/.orchestra/history}"
1093
- local app_prefix=""
1094
1267
  local pane_id
1095
1268
  pane_id="$(tmux display-message -t "$session" -p '#{pane_id}' 2>/dev/null || echo "")"
1096
1269
  local pane_key=""
@@ -1100,18 +1273,25 @@ tmux_generate_ai_session_name() {
1100
1273
  local session_key
1101
1274
  session_key="$(_orchestra_history_key "$session")"
1102
1275
 
1103
- local app_source_path=""
1104
- if [[ -n "$pane_key" && -f "$history_dir/$pane_key.last_app" ]]; then
1105
- app_source_path="$history_dir/$pane_key.last_app"
1106
- elif [[ -n "$session_key" && -f "$history_dir/$session_key.last_app" ]]; then
1107
- app_source_path="$history_dir/$session_key.last_app"
1108
- fi
1109
- if [[ -n "$app_source_path" ]]; then
1110
- IFS= read -r app_prefix < "$app_source_path" || true
1276
+ local is_tui="false"
1277
+ if _tmux_is_tui_context "$pane_cmd" "$alternate_on" "$mouse_any_flag" "$pane_mode" "$window_name" "$pane_title"; then
1278
+ is_tui="true"
1111
1279
  fi
1112
1280
 
1113
- if [[ -z "$app_prefix" ]]; then
1114
- app_prefix="$(_tmux_normalize_app_from_command "$pane_cmd")"
1281
+ local content=""
1282
+ local visible_content=""
1283
+ if [[ "$is_tui" == "true" ]]; then
1284
+ visible_content="$(tmux capture-pane -e -p -q -a -t "${pane_id:-$session}" 2>/dev/null || echo "")"
1285
+ fi
1286
+ local scrollback_content
1287
+ scrollback_content="$(tmux capture-pane -t "$session" -p -S -200 2>/dev/null)" || {
1288
+ err "Failed to capture tmux pane content"
1289
+ return 1
1290
+ }
1291
+ if [[ -n "$visible_content" ]]; then
1292
+ content="### Visible terminal view\n${visible_content}\n\n### Recent scrollback\n${scrollback_content}"
1293
+ else
1294
+ content="$scrollback_content"
1115
1295
  fi
1116
1296
 
1117
1297
  local history_path=""
@@ -1131,6 +1311,16 @@ tmux_generate_ai_session_name() {
1131
1311
  history_arg="$history_temp"
1132
1312
  fi
1133
1313
 
1314
+ local metadata_block=""
1315
+ metadata_block+="Session metadata:\n"
1316
+ metadata_block+="- pane_current_command: ${pane_cmd:-unknown}\n"
1317
+ metadata_block+="- window_name: ${window_name:-}\n"
1318
+ metadata_block+="- pane_title: ${pane_title:-}\n"
1319
+ metadata_block+="- pane_mode: ${pane_mode:-}\n"
1320
+ metadata_block+="- alternate_screen_active: ${alternate_on:-0}\n"
1321
+ metadata_block+="- mouse_mode_active: ${mouse_any_flag:-0}\n"
1322
+ metadata_block+="- likely_tui_app: ${is_tui}\n"
1323
+
1134
1324
  # If content is empty or too short, keep a placeholder
1135
1325
  if [[ ${#content} -lt 10 ]]; then
1136
1326
  content="(no terminal output captured)"
@@ -1153,11 +1343,10 @@ tmux_generate_ai_session_name() {
1153
1343
 
1154
1344
  # Prepare the API request using Python for proper JSON encoding
1155
1345
  local request_body=""
1156
- local preloaded_app_prefix="$app_prefix"
1157
1346
  if have_cmd python3; then
1158
1347
  # Use Python to safely build the request body and extract typed commands from the capture
1159
1348
  request_payload=$(
1160
- python3 - "$temp_file" "$history_arg" "$provider" "$openai_model" 2>/dev/null <<'PYCODE'
1349
+ python3 - "$temp_file" "$history_arg" "$provider" "$openai_model" "$pane_cmd" "$window_name" "$pane_title" "$pane_mode" "$alternate_on" "$mouse_any_flag" "$is_tui" 2>/dev/null <<'PYCODE'
1161
1350
  import json
1162
1351
  import re
1163
1352
  import sys
@@ -1168,11 +1357,17 @@ content = temp_path.read_text()
1168
1357
  history_arg = sys.argv[2] if len(sys.argv) > 2 else ""
1169
1358
  provider = sys.argv[3] if len(sys.argv) > 3 else "anthropic"
1170
1359
  openai_model = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else "gpt-4o-mini"
1360
+ pane_cmd = sys.argv[5] if len(sys.argv) > 5 else ""
1361
+ window_name = sys.argv[6] if len(sys.argv) > 6 else ""
1362
+ pane_title = sys.argv[7] if len(sys.argv) > 7 else ""
1363
+ pane_mode = sys.argv[8] if len(sys.argv) > 8 else ""
1364
+ alternate_on = sys.argv[9] if len(sys.argv) > 9 else "0"
1365
+ mouse_any_flag = sys.argv[10] if len(sys.argv) > 10 else "0"
1366
+ is_tui = sys.argv[11] if len(sys.argv) > 11 else "false"
1171
1367
 
1172
1368
  lines = content.splitlines()
1173
1369
  command_candidates = [] # Commands captured with high confidence
1174
1370
  fallback_candidates = [] # All prompt-like lines (used if we fail to classify)
1175
- app_command_candidates = [] # Command-only strings for app prefix
1176
1371
 
1177
1372
  history_commands = []
1178
1373
  if history_arg:
@@ -1380,7 +1575,6 @@ for raw_line in lines:
1380
1575
  normalized_command = " ".join(tokens)
1381
1576
  formatted_line = f"{prompt_part}:{normalized_command}"
1382
1577
  fallback_candidates.append(formatted_line)
1383
- app_command_candidates.append(normalized_command)
1384
1578
 
1385
1579
  allowed = (
1386
1580
  first_token in common_commands
@@ -1444,11 +1638,28 @@ description_guidance = """Description rules:
1444
1638
  - Do NOT include any app/tool name or prefix in the output.
1445
1639
  - If git activity is visible, the description should reflect it (e.g., reviewing changes), but do not add 'git_'."""
1446
1640
 
1641
+ metadata_summary = "\n".join(
1642
+ line
1643
+ for line in [
1644
+ f"pane_current_command: {pane_cmd}" if pane_cmd else "",
1645
+ f"window_name: {window_name}" if window_name else "",
1646
+ f"pane_title: {pane_title}" if pane_title else "",
1647
+ f"pane_mode: {pane_mode}" if pane_mode else "",
1648
+ f"alternate_screen_active: {alternate_on}",
1649
+ f"mouse_mode_active: {mouse_any_flag}",
1650
+ f"likely_tui_app: {is_tui}",
1651
+ ]
1652
+ if line
1653
+ )
1654
+
1447
1655
  prompt = f"""Analyze this terminal session and create a descriptive tmux session name based on what the user actually did.
1448
1656
 
1449
1657
  Terminal output (last capture):
1450
1658
  {content}
1451
1659
 
1660
+ Tmux metadata (use as supporting hints, not as the primary source of truth):
1661
+ {metadata_summary}
1662
+
1452
1663
  Extracted command history (oldest to newest):
1453
1664
  {command_history}
1454
1665
 
@@ -1460,6 +1671,8 @@ Focus: produce a concise description of the activity. Follow these rules:
1460
1671
 
1461
1672
  {'No clear prompt/command lines were detected. Rely on the terminal output context.' if not commands_detected else ''}
1462
1673
 
1674
+ {'An interactive TUI is likely running. Prefer the visible screen content plus recent command history over generic app names.' if is_tui.lower() in {'1', 'true', 'yes', 'on'} else ''}
1675
+
1463
1676
  {'First 10 lines of terminal output (useful for identifying running app/service):\n' + first_ten_lines if not commands_detected and first_ten_lines else ''}
1464
1677
 
1465
1678
  If no commands were detected, fall back to terminal output analysis but describe the main activity in the name. Identify the dominant application or process implied by the output (e.g., web server, test runner, build tool).
@@ -1496,56 +1709,26 @@ else:
1496
1709
  ]
1497
1710
  }
1498
1711
 
1499
- def normalize_app(cmd: str) -> str:
1500
- cmd = cmd.strip()
1501
- if not cmd:
1502
- return ""
1503
- for lead in ("$ ", "> "):
1504
- if cmd.startswith(lead):
1505
- cmd = cmd[len(lead):].strip()
1506
- parts = cmd.split()
1507
- if not parts:
1508
- return ""
1509
- if parts[0] in ("sudo", "env", "command"):
1510
- parts = parts[1:]
1511
- if not parts:
1512
- return ""
1513
- base = parts[0].split("/")[-1].lower()
1514
- base = re.sub(r"[^a-z0-9]+", "_", base).strip("_")
1515
- if base in {"bash", "zsh", "sh", "fish", "tmux", "login", "sudo", "man", "less", "more", "cat", "tail", "watch", "source", "export", "set", "alias", "unalias", "history", "bindkey"}:
1516
- return ""
1517
- return base
1518
-
1519
- app_commands = history_commands if history_commands else app_command_candidates
1520
- app_prefix = ""
1521
- for candidate in reversed(app_commands):
1522
- app_prefix = normalize_app(candidate)
1523
- if app_prefix:
1524
- break
1525
-
1526
- print(app_prefix)
1527
1712
  print(json.dumps(request))
1528
1713
  PYCODE
1529
1714
  )
1530
- local parsed_app_prefix
1531
- parsed_app_prefix="${request_payload%%$'\n'*}"
1532
- request_body="${request_payload#*$'\n'}"
1533
- if [[ -n "$preloaded_app_prefix" ]]; then
1534
- app_prefix="$preloaded_app_prefix"
1535
- else
1536
- app_prefix="$parsed_app_prefix"
1537
- fi
1538
- if [[ "$request_body" == "$request_payload" ]]; then
1539
- request_body="$request_payload"
1540
- if [[ -z "$preloaded_app_prefix" ]]; then
1541
- app_prefix=""
1542
- fi
1543
- fi
1715
+ request_body="$request_payload"
1544
1716
  fi
1545
1717
  if [[ -z "$request_body" ]]; then
1546
1718
  # Fallback: base64 encode the content to avoid escaping issues
1547
1719
  local encoded_content
1548
- encoded_content=$(base64 < "$temp_file" | tr -d '\n')
1720
+ local fallback_file
1721
+ if [[ "$OSTYPE" == "darwin"* ]]; then
1722
+ fallback_file=$(mktemp -t gw_fb)
1723
+ else
1724
+ fallback_file=$(mktemp)
1725
+ fi
1726
+ {
1727
+ printf '%b\n\n' "$metadata_block"
1728
+ cat "$temp_file"
1729
+ } > "$fallback_file"
1730
+ encoded_content=$(base64 < "$fallback_file" | tr -d '\n')
1731
+ rm -f "$fallback_file"
1549
1732
  if [[ "$provider" == "openai" ]]; then
1550
1733
  request_body=$(cat <<EOF
1551
1734
  {
@@ -1555,7 +1738,7 @@ PYCODE
1555
1738
  "messages": [
1556
1739
  {
1557
1740
  "role": "user",
1558
- "content": "I'll send you base64-encoded terminal output. Please decode it, analyze it, and create a descriptive name for this tmux session.\n\nIMPORTANT: Ignore any window titles or metadata. Focus ONLY on the actual terminal content.\n\nCRITICAL: Check for Git patterns FIRST:\n- Git commands: git status, diff, add, commit, push, pull, checkout, branch, merge, rebase, log, stash\n- Git output: 'On branch', 'Changes not staged', 'modified:', 'new file:', diff output (+/-/@@), commit hashes\n- If ANY Git patterns found, the description should reflect git activity (but do not add 'git_').\n\nDESCRIPTION ONLY: Return a short description of the activity.\n- Do NOT include the app/tool name or any prefix in the output.\n\nUse underscores only. Max 100 chars total.\n\nExamples:\n- reviewing_changes_before_commit\n- resolving_merge_conflicts\n- fixing_auth_bug\n- running_nextjs_dev_server\n\nBase64 terminal output:\n${encoded_content}\n\nRespond with ONLY the session name, nothing else."
1741
+ "content": "I'll send you base64-encoded terminal output. Please decode it, analyze it, and create a descriptive name for this tmux session.\n\nIMPORTANT: Use terminal metadata only as supporting hints. Prioritize the visible terminal content and recent command history.\n\nCRITICAL: Check for Git patterns FIRST:\n- Git commands: git status, diff, add, commit, push, pull, checkout, branch, merge, rebase, log, stash\n- Git output: 'On branch', 'Changes not staged', 'modified:', 'new file:', diff output (+/-/@@), commit hashes\n- If ANY Git patterns found, the description should reflect git activity (but do not add 'git_').\n\nDESCRIPTION ONLY: Return a short description of the activity.\n- Do NOT include the app/tool name or any prefix in the output.\n\nUse underscores only. Max 100 chars total.\n\nExamples:\n- reviewing_changes_before_commit\n- resolving_merge_conflicts\n- fixing_auth_bug\n- running_nextjs_dev_server\n\nBase64 terminal output:\n${encoded_content}\n\nRespond with ONLY the session name, nothing else."
1559
1742
  }
1560
1743
  ]
1561
1744
  }
@@ -1569,7 +1752,7 @@ EOF
1569
1752
  "messages": [
1570
1753
  {
1571
1754
  "role": "user",
1572
- "content": "I'll send you base64-encoded terminal output. Please decode it, analyze it, and create a descriptive name for this tmux session.\n\nIMPORTANT: Ignore any window titles or metadata. Focus ONLY on the actual terminal content.\n\nCRITICAL: Check for Git patterns FIRST:\n- Git commands: git status, diff, add, commit, push, pull, checkout, branch, merge, rebase, log, stash\n- Git output: 'On branch', 'Changes not staged', 'modified:', 'new file:', diff output (+/-/@@), commit hashes\n- If ANY Git patterns found, the description should reflect git activity (but do not add 'git_').\n\nDESCRIPTION ONLY: Return a short description of the activity.\n- Do NOT include the app/tool name or any prefix in the output.\n\nUse underscores only. Max 100 chars total.\n\nExamples:\n- reviewing_changes_before_commit\n- resolving_merge_conflicts\n- fixing_auth_bug\n- running_nextjs_dev_server\n\nBase64 terminal output:\n${encoded_content}\n\nRespond with ONLY the session name, nothing else."
1755
+ "content": "I'll send you base64-encoded terminal output. Please decode it, analyze it, and create a descriptive name for this tmux session.\n\nIMPORTANT: Use terminal metadata only as supporting hints. Prioritize the visible terminal content and recent command history.\n\nCRITICAL: Check for Git patterns FIRST:\n- Git commands: git status, diff, add, commit, push, pull, checkout, branch, merge, rebase, log, stash\n- Git output: 'On branch', 'Changes not staged', 'modified:', 'new file:', diff output (+/-/@@), commit hashes\n- If ANY Git patterns found, the description should reflect git activity (but do not add 'git_').\n\nDESCRIPTION ONLY: Return a short description of the activity.\n- Do NOT include the app/tool name or any prefix in the output.\n\nUse underscores only. Max 100 chars total.\n\nExamples:\n- reviewing_changes_before_commit\n- resolving_merge_conflicts\n- fixing_auth_bug\n- running_nextjs_dev_server\n\nBase64 terminal output:\n${encoded_content}\n\nRespond with ONLY the session name, nothing else."
1573
1756
  }
1574
1757
  ]
1575
1758
  }
@@ -1591,6 +1774,7 @@ EOF
1591
1774
  context_file=$(mktemp)
1592
1775
  fi
1593
1776
  {
1777
+ printf '%b\n\n' "$metadata_block"
1594
1778
  printf 'TMUX SESSION CAPTURE\n\n'
1595
1779
  cat "$temp_file"
1596
1780
  if [[ -n "$history_arg" && -f "$history_arg" ]]; then
@@ -1782,17 +1966,6 @@ print(description)' <<<"$responses_resp" 2>/dev/null || true)"
1782
1966
  return 1
1783
1967
  fi
1784
1968
 
1785
- app_prefix=$(printf '%s' "$app_prefix" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '_')
1786
- app_prefix="${app_prefix//__/_}"
1787
- app_prefix="${app_prefix##_}"
1788
- app_prefix="${app_prefix%%_}"
1789
- TMUX_AI_APP_SOURCE="$app_prefix"
1790
- if [[ -n "$app_prefix" ]]; then
1791
- if [[ "$new_name" == "${app_prefix}_"* ]]; then
1792
- new_name="${new_name#${app_prefix}_}"
1793
- fi
1794
- new_name="${app_prefix}_${new_name}"
1795
- fi
1796
1969
  new_name="${new_name:0:100}"
1797
1970
 
1798
1971
  echo "$new_name"
@@ -107,7 +107,7 @@ case "${1:-}" in
107
107
  ;;
108
108
 
109
109
  "tmux-send-keys")
110
- bridge_tmux_send_keys "${2:-}" "$@"
110
+ bridge_tmux_send_keys "${2:-}" "${@:3}"
111
111
  ;;
112
112
 
113
113
  "session-metadata")
@@ -127,7 +127,7 @@ case "${1:-}" in
127
127
  ;;
128
128
 
129
129
  "manual-rename-session")
130
- bridge_manual_rename_session "${2:-}" "${3:-}"
130
+ bridge_manual_rename_session "${2:-}" "${@:3}"
131
131
  ;;
132
132
 
133
133
  "check-branch")
@@ -77,6 +77,10 @@ main() {
77
77
  exit 1
78
78
  fi
79
79
 
80
+ # This launcher is explicitly for local development; ignore installed binary
81
+ # overrides so gwr.sh resolves the TUI binary from the selected checkout.
82
+ unset GW_TUI_BIN
83
+
80
84
  if [[ ${#args[@]} -gt 0 ]]; then
81
85
  exec bash "$script_path" "${args[@]}"
82
86
  else
@@ -65,26 +65,6 @@ def extract_command(line: str) -> str:
65
65
  return line.split(marker)[-1].strip()
66
66
  return ""
67
67
 
68
- def normalize_app(cmd: str) -> str:
69
- cmd = cmd.strip()
70
- if not cmd:
71
- return ""
72
- for lead in ("$ ", "> "):
73
- if cmd.startswith(lead):
74
- cmd = cmd[len(lead):].strip()
75
- parts = cmd.split()
76
- if not parts:
77
- return ""
78
- if parts[0] in ("sudo", "env", "command"):
79
- parts = parts[1:]
80
- if not parts:
81
- return ""
82
- base = parts[0].split("/")[-1].lower()
83
- base = re.sub(r"[^a-z0-9]+", "_", base).strip("_")
84
- if base in {"bash", "zsh", "sh", "fish", "tmux", "login", "sudo", "man", "less", "more", "cat", "tail", "watch", "source", "export", "set", "alias", "unalias", "history", "bindkey"}:
85
- return ""
86
- return base
87
-
88
68
  lines = content.splitlines()
89
69
  last_cmd = ""
90
70
  for line in reversed(lines):
@@ -93,7 +73,6 @@ for line in reversed(lines):
93
73
  last_cmd = candidate
94
74
  break
95
75
 
96
- app_prefix = normalize_app(last_cmd) or ""
97
76
  prompt = f"""Analyze this terminal session and create a descriptive name based ONLY on the console output and commands executed.
98
77
 
99
78
  Terminal output:
@@ -137,16 +116,10 @@ else:
137
116
  "max_tokens": 100,
138
117
  "messages": [{"role": "user", "content": prompt}],
139
118
  }
140
- print(app_prefix)
141
119
  print(json.dumps(req))
142
120
  PY
143
121
  )
144
- app_prefix="${request_payload%%$'\n'*}"
145
- request_body="${request_payload#*$'\n'}"
146
- if [[ "$request_body" == "$request_payload" ]]; then
147
- request_body="$request_payload"
148
- app_prefix=""
149
- fi
122
+ request_body="$request_payload"
150
123
  if [[ "$provider" == "openai" ]]; then
151
124
  response=$(curl -s -X POST https://api.openai.com/v1/chat/completions \
152
125
  --max-time 20 \
@@ -168,16 +141,6 @@ PY
168
141
  if [[ -z "$new_name" ]]; then
169
142
  echo "\"ai_generation_failed\""
170
143
  else
171
- app_prefix="$(printf '%s' "$app_prefix" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '_')"
172
- app_prefix="${app_prefix//__/_}"
173
- app_prefix="${app_prefix##_}"
174
- app_prefix="${app_prefix%%_}"
175
- if [[ -n "$app_prefix" ]]; then
176
- if [[ "$new_name" == "${app_prefix}_"* ]]; then
177
- new_name="${new_name#${app_prefix}_}"
178
- fi
179
- new_name="${app_prefix}_${new_name}"
180
- fi
181
144
  new_name="${new_name:0:100}"
182
145
  printf '"%s"\n' "$new_name"
183
146
  fi
@@ -191,11 +154,18 @@ bridge_rename_session() {
191
154
  fi
192
155
  original_session_name="$1"
193
156
 
157
+ clear_auto_rename_requested() {
158
+ if tmux_available && tmux_session_exists "$original_session_name"; then
159
+ tmux set-option -qu -t "$original_session_name" @orchestra_auto_rename_requested >/dev/null 2>&1 || true
160
+ fi
161
+ }
162
+
194
163
  # Load API key from config file or fallback to .env
195
164
  load_openai_api_key
196
165
  load_anthropic_api_key
197
166
 
198
167
  if [[ -z "${OPENAI_API_KEY-}" && -z "${ANTHROPIC_API_KEY-}" ]]; then
168
+ clear_auto_rename_requested
199
169
  echo "\"missing_api_key\""
200
170
  return 0
201
171
  fi
@@ -212,7 +182,6 @@ bridge_rename_session() {
212
182
  fi
213
183
 
214
184
  # Generate AI description using the CURRENT session (no renaming to temp name)
215
- TMUX_AI_APP_SOURCE=""
216
185
  local err_file
217
186
  if [[ "$OSTYPE" == "darwin"* ]]; then
218
187
  err_file=$(mktemp -t gw_ai_err)
@@ -234,27 +203,21 @@ bridge_rename_session() {
234
203
  ai_name="$(echo "$ai_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)"
235
204
  ai_name="$(echo "$ai_name" | sed 's/^_*//')"
236
205
  if [[ -z "$ai_name" ]]; then
206
+ clear_auto_rename_requested
237
207
  echo "\"ai_generation_failed\""
238
208
  return 0
239
209
  fi
240
210
 
241
211
  # Perform the rename preserving canonical prefix and delimiter
242
212
  if tmux_rename_session "$original_session_name" "$ai_name" >/dev/null; then
243
- local app_source="${TMUX_AI_APP_SOURCE-}"
244
- app_source="$(printf '%s' "$app_source" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '_')"
245
- app_source="${app_source//__/_}"
246
- app_source="${app_source##_}"
247
- app_source="${app_source%%_}"
248
- if [[ -n "$app_source" ]]; then
249
- echo "\"success:app_source:${app_source}\""
250
- else
251
- echo "\"success\""
252
- fi
213
+ echo "\"success\""
253
214
  else
215
+ clear_auto_rename_requested
254
216
  echo "\"rename_failed\""
255
217
  fi
256
218
  else
257
219
  # AI generation failed, keep original name
220
+ clear_auto_rename_requested
258
221
  if [[ -n "$ai_err" ]]; then
259
222
  echo "\"ai_generation_failed: ${ai_err}\""
260
223
  elif [[ $ai_status -ne 0 ]]; then
@@ -267,12 +230,13 @@ bridge_rename_session() {
267
230
 
268
231
  # Manual rename session
269
232
  bridge_manual_rename_session() {
270
- if [[ -z "${1:-}" ]] || [[ -z "${2:-}" ]]; then
233
+ if [[ -z "${1:-}" ]] || [[ $# -lt 2 ]]; then
271
234
  json_error "Old and new session names required"
272
235
  return 1
273
236
  fi
274
237
  old_name="$1"
275
- new_display_name="$2"
238
+ shift
239
+ new_display_name="$*"
276
240
 
277
241
  if ! tmux_available; then
278
242
  echo "\"tmux_not_available\""
@@ -284,9 +248,10 @@ bridge_manual_rename_session() {
284
248
  echo "\"session_not_found\""
285
249
  return 0
286
250
  fi
251
+ new_display_name="$(printf '%s' "$new_display_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_]/_/g' | cut -c1-100)"
287
252
 
288
- # Validate new display name (alphanumeric, underscore, hyphen only)
289
- if [[ ! "$new_display_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
253
+ # Validate new display name after matching the TUI's sanitization rules.
254
+ if [[ -z "$new_display_name" || ! "$new_display_name" =~ ^[a-zA-Z0-9_]+$ ]]; then
290
255
  echo "\"invalid_name\""
291
256
  return 0
292
257
  fi
@@ -103,7 +103,7 @@ bridge_tmux_send_keys() {
103
103
  json_error "Session name required"
104
104
  return 1
105
105
  fi
106
- session_name="$1"; shift 2 || true
106
+ session_name="$1"; shift || true
107
107
  if tmux_available; then
108
108
  tmux_send_keys "$session_name" "$*" || { json_error "Failed to send keys"; return 1; }
109
109
  echo '{"ok":true}'
@@ -125,12 +125,32 @@ bridge_session_metadata() {
125
125
  window_name=$(tmux display-message -t "$session_name" -p '#{window_name}' 2>/dev/null || echo "")
126
126
  pane_title=$(tmux display-message -t "$session_name" -p '#{pane_title}' 2>/dev/null || echo "")
127
127
  pane_cmd=$(tmux display-message -t "$session_name" -p '#{pane_current_command}' 2>/dev/null || echo "")
128
-
128
+ pane_mode=$(tmux display-message -t "$session_name" -p '#{pane_mode}' 2>/dev/null || echo "")
129
+ alternate_on=$(tmux display-message -t "$session_name" -p '#{alternate_on}' 2>/dev/null || echo "0")
130
+ mouse_any_flag=$(tmux display-message -t "$session_name" -p '#{mouse_any_flag}' 2>/dev/null || echo "0")
131
+ auto_rename_requested=$(tmux show-options -qv -t "$session_name" @orchestra_auto_rename_requested 2>/dev/null || echo "")
132
+ case "$(printf '%s' "$alternate_on" | tr '[:upper:]' '[:lower:]')" in
133
+ 1|yes|true|on) alternate_on=true ;;
134
+ *) alternate_on=false ;;
135
+ esac
136
+ case "$(printf '%s' "$mouse_any_flag" | tr '[:upper:]' '[:lower:]')" in
137
+ 1|yes|true|on) mouse_any_flag=true ;;
138
+ *) mouse_any_flag=false ;;
139
+ esac
140
+ case "$(printf '%s' "$auto_rename_requested" | tr '[:upper:]' '[:lower:]')" in
141
+ 1|yes|true|on) auto_rename_requested=true ;;
142
+ *) auto_rename_requested=false ;;
143
+ esac
144
+
129
145
  # Return JSON with metadata
130
146
  json_object \
131
147
  "window_name:s=$window_name" \
132
148
  "pane_title:s=$pane_title" \
133
- "current_command:s=$pane_cmd"
149
+ "current_command:s=$pane_cmd" \
150
+ "pane_mode:s=$pane_mode" \
151
+ "alternate_on:b=$alternate_on" \
152
+ "mouse_any_flag:b=$mouse_any_flag" \
153
+ "auto_rename_requested:b=$auto_rename_requested"
134
154
  else
135
155
  json_error "tmux not available"
136
156
  fi
@@ -8,6 +8,16 @@ build_check_dependencies() {
8
8
  exit 1
9
9
  fi
10
10
 
11
+ if ! command -v cc >/dev/null 2>&1 && \
12
+ ! command -v clang >/dev/null 2>&1 && \
13
+ ! command -v gcc >/dev/null 2>&1; then
14
+ build_error "C compiler not found. Bundled SQLite requires a native C toolchain."
15
+ build_error " macOS: xcode-select --install"
16
+ build_error " Ubuntu/Debian: sudo apt install build-essential"
17
+ build_error " RHEL/CentOS: sudo yum groupinstall 'Development Tools'"
18
+ exit 1
19
+ fi
20
+
11
21
  if ! command -v jq >/dev/null 2>&1; then
12
22
  build_warn "jq not found. Installing via package manager is recommended."
13
23
  build_warn " macOS: brew install jq"
@@ -9,10 +9,12 @@ Usage: gwr [options]
9
9
  A terminal user interface for managing git worktrees and tmux sessions.
10
10
 
11
11
  Options:
12
- -d, --debug Enable debug mode
13
- -h, --help Show this help
14
- --check-updates Check for available updates
15
- --update Check for available updates (alias for --check-updates)
12
+ -d, --debug Enable debug mode
13
+ --new-tmux Create and attach to a new tmux session for the current branch
14
+ --cmd <command> Run a command in the new tmux session (repeatable; requires --new-tmux)
15
+ -h, --help Show this help
16
+ --check-updates Check for available updates
17
+ --update Check for available updates (alias for --check-updates)
16
18
 
17
19
  Features:
18
20
  • Interactive worktree and session management
@@ -44,6 +46,8 @@ For the original shell-based interface, use: gw
44
46
 
45
47
  Examples:
46
48
  gwr # Launch interactive TUI
49
+ gwr --new-tmux # Start a new tmux session for the current branch
50
+ gwr --new-tmux --cmd "pnpm install" --cmd "pnpm dev"
47
51
  gwr --debug # Launch with debug information
48
52
  gwr --help # Show this help
49
53
  gwr --update # Check for available updates
@@ -4,13 +4,62 @@
4
4
  #
5
5
  # Source this file from your shell profile to log each executed command to a
6
6
  # per-tmux-pane history file. The auto-rename pipeline can then use the
7
- # captured history as the primary signal for determining app prefixes.
7
+ # captured history as the primary context for generating session names.
8
8
  #
9
9
  # source /path/to/orchestrator/shell/orchestra-command-hook.sh
10
10
  #
11
11
  # On Bash we install a DEBUG pre-exec hook; on Zsh we install a preexec hook.
12
12
  # This captures the last user command reliably before execution.
13
13
 
14
+ orchestra_rename_session() {
15
+ local new_name="$*"
16
+ if [[ -z "$new_name" ]]; then
17
+ printf 'Usage: orchestra_rename_session <new name>\n' >&2
18
+ return 1
19
+ fi
20
+
21
+ if [[ -z "${TMUX-}" ]] || ! command -v tmux >/dev/null 2>&1; then
22
+ printf 'orchestra_rename_session must be run inside a tmux session\n' >&2
23
+ return 1
24
+ fi
25
+
26
+ if [[ -z "${ORCHESTRA_BRIDGE_PATH-}" || ! -x "$ORCHESTRA_BRIDGE_PATH" ]]; then
27
+ printf 'ORCHESTRA_BRIDGE_PATH is not set or executable\n' >&2
28
+ return 1
29
+ fi
30
+
31
+ local session_name=""
32
+ session_name="$(tmux display-message -p '#{session_name}' 2>/dev/null || echo "")"
33
+ if [[ -z "$session_name" ]]; then
34
+ printf 'Unable to determine current tmux session\n' >&2
35
+ return 1
36
+ fi
37
+
38
+ local result=""
39
+ result="$("$ORCHESTRA_BRIDGE_PATH" manual-rename-session "$session_name" "$new_name" 2>/dev/null || true)"
40
+ case "$result" in
41
+ '"success"')
42
+ printf 'Renamed session to: %s\n' "$new_name"
43
+ ;;
44
+ '"invalid_name"')
45
+ printf 'Invalid session name: %s\n' "$new_name" >&2
46
+ return 1
47
+ ;;
48
+ '"session_not_found"')
49
+ printf 'Current tmux session was not found\n' >&2
50
+ return 1
51
+ ;;
52
+ '"tmux_not_available"')
53
+ printf 'tmux is not available\n' >&2
54
+ return 1
55
+ ;;
56
+ *)
57
+ printf 'Failed to rename session\n' >&2
58
+ return 1
59
+ ;;
60
+ esac
61
+ }
62
+
14
63
  if [[ -n "${ORCHESTRA_PROMPT_HOOK_INSTALLED-}" ]]; then
15
64
  return 0 2>/dev/null || exit 0
16
65
  fi
@@ -18,6 +67,8 @@ fi
18
67
  ORCHESTRA_HISTORY_DIR="${ORCHESTRA_HISTORY_DIR:-$HOME/.orchestra/history}"
19
68
  ORCHESTRA_PROMPT_HOOK_INSTALLED=1
20
69
 
70
+ _ORCHESTRA_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
71
+
21
72
  _orchestra_history_key() {
22
73
  local key="$1"
23
74
  key="${key//\//_}"
@@ -31,6 +82,101 @@ _orchestra_history_key() {
31
82
  echo "$key"
32
83
  }
33
84
 
85
+ _orchestra_bridge_path() {
86
+ local bridge=""
87
+
88
+ if [[ -n "${GW_ORCHESTRATOR_ROOT-}" ]]; then
89
+ bridge="$GW_ORCHESTRATOR_ROOT/gw-bridge.sh"
90
+ if [[ -f "$bridge" ]]; then
91
+ echo "$bridge"
92
+ return
93
+ fi
94
+ fi
95
+
96
+ bridge="$(cd "$_ORCHESTRA_HOOK_DIR/.." && pwd -P)/gw-bridge.sh"
97
+ if [[ -f "$bridge" ]]; then
98
+ echo "$bridge"
99
+ return
100
+ fi
101
+
102
+ echo ""
103
+ }
104
+
105
+ _orchestra_should_auto_rename_session() {
106
+ local session_name="$1"
107
+ [[ -n "$session_name" && "$session_name" == *"__auto_"* ]]
108
+ }
109
+
110
+ _orchestra_should_trigger_auto_rename_command() {
111
+ local command="$1"
112
+ local app="$2"
113
+
114
+ [[ -n "$command" ]] || return 1
115
+
116
+ case "$app" in
117
+ ""|cd|ls|pwd|clear|reset)
118
+ return 1
119
+ ;;
120
+ esac
121
+
122
+ return 0
123
+ }
124
+
125
+ _orchestra_auto_rename_flag_path() {
126
+ local session_name="$1"
127
+ local session_key
128
+ session_key=$(_orchestra_history_key "$session_name")
129
+ if [[ -z "$session_key" ]]; then
130
+ return
131
+ fi
132
+ printf '%s/%s.rename_requested\n' "$ORCHESTRA_HISTORY_DIR" "$session_key"
133
+ }
134
+
135
+ _orchestra_auto_rename_delay_secs() {
136
+ local delay="${ORCHESTRA_AUTO_RENAME_DELAY_SECS:-10}"
137
+ if [[ ! "$delay" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
138
+ delay="10"
139
+ fi
140
+ printf '%s\n' "$delay"
141
+ }
142
+
143
+ _orchestra_trigger_auto_rename_async() {
144
+ local session_name="$1"
145
+ local command="$2"
146
+ local app="$3"
147
+
148
+ [[ -n "$session_name" ]] || return
149
+ _orchestra_should_auto_rename_session "$session_name" || return
150
+ _orchestra_should_trigger_auto_rename_command "$command" "$app" || return
151
+
152
+ local bridge
153
+ bridge="$(_orchestra_bridge_path)"
154
+ [[ -n "$bridge" && -f "$bridge" ]] || return
155
+
156
+ local flag_path
157
+ flag_path="$(_orchestra_auto_rename_flag_path "$session_name")"
158
+ [[ -n "$flag_path" ]] || return
159
+ if [[ -f "$flag_path" ]]; then
160
+ return
161
+ fi
162
+
163
+ local timestamp
164
+ timestamp=$(date '+%Y-%m-%dT%H:%M:%S%z')
165
+ printf '%s\n' "$timestamp" > "$flag_path"
166
+
167
+ if command -v tmux >/dev/null 2>&1; then
168
+ tmux set-option -q -t "$session_name" @orchestra_auto_rename_requested 1 >/dev/null 2>&1 || true
169
+ fi
170
+
171
+ local delay
172
+ delay="$(_orchestra_auto_rename_delay_secs)"
173
+
174
+ (
175
+ sleep "$delay"
176
+ command bash "$bridge" rename-session "$session_name" >/dev/null 2>&1 || true
177
+ ) >/dev/null 2>&1 &
178
+ }
179
+
34
180
  _orchestra_capture_command() {
35
181
  local cmd=""
36
182
  if [[ -n "${ZSH_VERSION-}" ]]; then
@@ -171,6 +317,10 @@ orchestra_log_command() {
171
317
  printf '%s\n' "$app" > "$ORCHESTRA_HISTORY_DIR/$key.last_app"
172
318
  fi
173
319
  printf '%s\n' "$command" > "$ORCHESTRA_HISTORY_DIR/$key.last_cmd"
320
+
321
+ if [[ -n "$session_name" && "$session_name" != "shell" ]]; then
322
+ _orchestra_trigger_auto_rename_async "$session_name" "$command" "$app" || true
323
+ fi
174
324
  }
175
325
 
176
326
  _orchestra_install_hook_bash() {
@@ -77,6 +77,10 @@ main() {
77
77
  exit 1
78
78
  fi
79
79
 
80
+ # This launcher is explicitly for local development; ignore installed binary
81
+ # overrides so gwr.sh resolves the TUI binary from the selected checkout.
82
+ unset GW_TUI_BIN
83
+
80
84
  if [[ ${#args[@]} -gt 0 ]]; then
81
85
  exec bash "$script_path" "${args[@]}"
82
86
  else