@humanu/orchestra 0.5.77 → 0.5.79
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/bin/{gw.js → orchestra-cli.js} +1 -1
- package/bin/orchestra.js +2 -2
- package/install.js +41 -42
- package/package.json +2 -3
- package/resources/api/git.sh +4 -444
- package/resources/api/tmux.sh +4 -2804
- package/resources/prebuilt/linux-x64/{gw-env-copy → env-copy} +0 -0
- package/resources/prebuilt/linux-x64/orchestra +0 -0
- package/resources/prebuilt/macos-arm64/{gw-env-copy → env-copy} +0 -0
- package/resources/prebuilt/macos-arm64/orchestra +0 -0
- package/resources/prebuilt/macos-intel/{gw-env-copy → env-copy} +0 -0
- package/resources/prebuilt/macos-intel/orchestra +0 -0
- package/resources/scripts/{gw.sh → orchestra-cli.sh} +14 -14
- package/resources/scripts/orchestra-local.sh +6 -6
- package/resources/scripts/{gwr.sh → orchestra.sh} +11 -55
- package/resources/scripts/{shell/bridge → server/services}/ai.sh +4 -4
- package/resources/scripts/{gw-bridge.sh → server/services/dispatch.sh} +62 -59
- package/resources/scripts/server/services/git/api.sh +447 -0
- package/resources/scripts/{shell/git/bridge_check_branch.sh → server/services/git/check_branch_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_create_worktree.sh → server/services/git/create_worktree_api.sh} +3 -3
- package/resources/scripts/{shell/git/bridge_create_worktree_from_existing.sh → server/services/git/create_worktree_from_existing_api.sh} +2 -2
- package/resources/scripts/{shell/git/bridge_create_worktree_from_remote.sh → server/services/git/create_worktree_from_remote_api.sh} +2 -2
- package/resources/scripts/{shell/git/bridge_delete_branch_only.sh → server/services/git/delete_branch_only_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_delete_worktree.sh → server/services/git/delete_worktree_api.sh} +2 -2
- package/resources/scripts/{shell/git/bridge_delete_worktree_only.sh → server/services/git/delete_worktree_only_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_enhanced_git_status.sh → server/services/git/enhanced_git_status_api.sh} +3 -3
- package/resources/scripts/{shell/git/bridge_git_status.sh → server/services/git/git_status_api.sh} +2 -2
- package/resources/scripts/{shell/git/bridge_list_worktrees.sh → server/services/git/list_worktrees_api.sh} +2 -2
- package/resources/scripts/server/services/git/merge_api.sh +12 -0
- package/resources/scripts/{shell/git/bridge_merge_from_primary.sh → server/services/git/merge_from_primary_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_merge_into_primary.sh → server/services/git/merge_into_primary_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_primary_branch.sh → server/services/git/primary_branch_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_rebase_from_primary.sh → server/services/git/rebase_from_primary_api.sh} +1 -1
- package/resources/scripts/server/services/git/repo_api.sh +12 -0
- package/resources/scripts/{shell/git/bridge_repo_info.sh → server/services/git/repo_info_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_squash_into_primary.sh → server/services/git/squash_into_primary_api.sh} +1 -1
- package/resources/scripts/{shell/git/bridge_switch_worktree.sh → server/services/git/switch_worktree_api.sh} +2 -2
- package/resources/scripts/server/services/git/worktree_api.sh +17 -0
- package/resources/scripts/{shell/bridge/utils.sh → server/services/json.sh} +23 -23
- package/resources/scripts/{shell/bridge → server/services/session}/tmux.sh +14 -14
- package/resources/scripts/server/session/tmux_api.sh +2812 -0
- package/resources/scripts/services.sh +6 -0
- package/resources/scripts/shell/AGENTS.md +63 -74
- package/resources/scripts/shell/build/dependencies.sh +33 -0
- package/resources/scripts/shell/build/install.sh +7 -0
- package/resources/scripts/shell/build/load.sh +10 -0
- package/resources/scripts/shell/build/logging.sh +6 -0
- package/resources/scripts/shell/build/rust.sh +18 -0
- package/resources/scripts/shell/build/services.sh +17 -0
- package/resources/scripts/shell/build/{build_usage.sh → usage.sh} +6 -6
- package/resources/scripts/shell/cli_load.sh +9 -0
- package/resources/scripts/shell/{gw_env_copy.sh → env_copy.sh} +11 -11
- package/resources/scripts/shell/env_copy_command.sh +2 -2
- package/resources/scripts/shell/git/checkout_worktree.sh +4 -4
- package/resources/scripts/shell/git/create_worktree.sh +2 -2
- package/resources/scripts/shell/git/delete_worktree.sh +1 -1
- package/resources/scripts/shell/git/merge.sh +1 -1
- package/resources/scripts/shell/git/repo.sh +1 -1
- package/resources/scripts/shell/git/worktree.sh +1 -1
- package/resources/scripts/shell/gwr/check-updates.sh +1 -1
- package/resources/scripts/shell/gwr_binary.sh +4 -4
- package/resources/scripts/shell/gwr_load.sh +1 -1
- package/resources/scripts/shell/gwr_services.sh +10 -0
- package/resources/scripts/shell/gwr_usage.sh +10 -10
- package/resources/scripts/shell/orchestra-command-hook.sh +15 -15
- package/resources/scripts/shell/orchestra-local.sh +6 -6
- package/resources/scripts/shell/tmux/new_session_command.sh +1 -1
- package/bin/gwr.js +0 -10
- package/resources/scripts/shell/build/build_bridge.sh +0 -17
- package/resources/scripts/shell/build/build_dependencies.sh +0 -33
- package/resources/scripts/shell/build/build_install.sh +0 -7
- package/resources/scripts/shell/build/build_load.sh +0 -10
- package/resources/scripts/shell/build/build_logging.sh +0 -6
- package/resources/scripts/shell/build/build_rust.sh +0 -18
- package/resources/scripts/shell/git/bridge_merge.sh +0 -12
- package/resources/scripts/shell/git/bridge_repo.sh +0 -12
- package/resources/scripts/shell/git/bridge_worktree.sh +0 -17
- package/resources/scripts/shell/gw_legacy_wrappers.sh +0 -7
- package/resources/scripts/shell/gw_load.sh +0 -10
- package/resources/scripts/shell/gwr_bridge.sh +0 -10
- /package/resources/scripts/shell/{gw_debug.sh → cli_debug.sh} +0 -0
- /package/resources/scripts/shell/{gw_err.sh → cli_err.sh} +0 -0
- /package/resources/scripts/shell/{gw_have_cmd.sh → cli_have_cmd.sh} +0 -0
- /package/resources/scripts/shell/{gw_info.sh → cli_info.sh} +0 -0
package/resources/api/tmux.sh
CHANGED
|
@@ -1,2807 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# tmux.sh – Dedicated tmux session management API
|
|
5
|
-
# ---------------------------------------------------------------------------
|
|
6
|
-
# This script provides a clean API for all tmux session operations.
|
|
7
|
-
# It can be sourced by other modules that need tmux functionality.
|
|
8
|
-
###############################################################################
|
|
3
|
+
# shellcheck shell=bash
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return 1 2>/dev/null || exit 1
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
# Use git_repo_root if available, otherwise fallback to repo_root
|
|
17
|
-
if declare -f git_repo_root >/dev/null 2>&1; then
|
|
18
|
-
repo_root() { git_repo_root; }
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
# --------------------------- Tmux Core API ----------------------------------
|
|
22
|
-
|
|
23
|
-
# Session delimiter (hardcoded)
|
|
24
|
-
# Note: tmux session names cannot contain ':'; use a safe delimiter
|
|
25
|
-
ORCHESTRA_SESSION_DELIM="__"
|
|
26
|
-
|
|
27
|
-
# Absolute directory that contains this script when sourced.
|
|
28
|
-
_TMUX_API_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
29
|
-
|
|
30
|
-
# Helper: return delimiter
|
|
31
|
-
_tmux_delim() { echo "$ORCHESTRA_SESSION_DELIM"; }
|
|
32
|
-
|
|
33
|
-
# Helper: sanitize tmux session name for filesystem usage
|
|
34
|
-
_orchestra_history_key() {
|
|
35
|
-
local key="$1"
|
|
36
|
-
key="${key//\//_}"
|
|
37
|
-
key=$(echo "$key" | tr '[:space:]' '_')
|
|
38
|
-
key=$(echo "$key" | tr -c '[:alnum:]_-' '_')
|
|
39
|
-
while [[ "$key" == *"__"* ]]; do
|
|
40
|
-
key="${key//__/_}"
|
|
41
|
-
done
|
|
42
|
-
key="${key##_}"
|
|
43
|
-
key="${key%%_}"
|
|
44
|
-
echo "$key"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
_tmux_normalize_app_from_command() {
|
|
48
|
-
local cmd="$1"
|
|
49
|
-
cmd="${cmd#"${cmd%%[![:space:]]*}"}"
|
|
50
|
-
cmd="${cmd%"${cmd##*[![:space:]]}"}"
|
|
51
|
-
if [[ -z "$cmd" ]]; then
|
|
52
|
-
return
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
local tokens=()
|
|
56
|
-
read -r -a tokens <<< "$cmd"
|
|
57
|
-
if [[ ${#tokens[@]} -eq 0 ]]; then
|
|
58
|
-
return
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
local i=0
|
|
62
|
-
while [[ $i -lt ${#tokens[@]} ]]; do
|
|
63
|
-
local t="${tokens[$i]}"
|
|
64
|
-
if [[ "$t" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
|
65
|
-
((i++))
|
|
66
|
-
continue
|
|
67
|
-
fi
|
|
68
|
-
case "$t" in
|
|
69
|
-
sudo|env|command|nohup|time)
|
|
70
|
-
((i++))
|
|
71
|
-
continue
|
|
72
|
-
;;
|
|
73
|
-
esac
|
|
74
|
-
break
|
|
75
|
-
done
|
|
76
|
-
|
|
77
|
-
if [[ $i -ge ${#tokens[@]} ]]; then
|
|
78
|
-
return
|
|
79
|
-
fi
|
|
80
|
-
|
|
81
|
-
local base="${tokens[$i]}"
|
|
82
|
-
base="${base##*/}"
|
|
83
|
-
base="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9._:-' '_')"
|
|
84
|
-
base="${base//__/_}"
|
|
85
|
-
base="${base##_}"
|
|
86
|
-
base="${base%%_}"
|
|
87
|
-
|
|
88
|
-
case "$base" in
|
|
89
|
-
python3|python3.*)
|
|
90
|
-
base="python"
|
|
91
|
-
;;
|
|
92
|
-
pip3|pip3.*)
|
|
93
|
-
base="pip"
|
|
94
|
-
;;
|
|
95
|
-
docker-compose|docker_compose)
|
|
96
|
-
base="docker_compose"
|
|
97
|
-
;;
|
|
98
|
-
nvim|nvim.app)
|
|
99
|
-
base="nvim"
|
|
100
|
-
;;
|
|
101
|
-
esac
|
|
102
|
-
|
|
103
|
-
case "$base" in
|
|
104
|
-
""|bash|zsh|sh|fish|tmux|login|sudo|man|less|more|cat|tail|watch|source|export|set|alias|unalias|history|bindkey)
|
|
105
|
-
return
|
|
106
|
-
;;
|
|
107
|
-
esac
|
|
108
|
-
|
|
109
|
-
printf '%s' "$base"
|
|
110
|
-
}
|
|
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
|
-
|
|
163
|
-
# Helper: absolute path to the command hook script (if present)
|
|
164
|
-
_orchestra_command_hook() {
|
|
165
|
-
local hook=""
|
|
166
|
-
|
|
167
|
-
# 1) Explicit install root set by wrappers (Homebrew/NPM)
|
|
168
|
-
if [[ -n "${GW_ORCHESTRATOR_ROOT-}" ]]; then
|
|
169
|
-
hook="$GW_ORCHESTRATOR_ROOT/shell/orchestra-command-hook.sh"
|
|
170
|
-
if [[ -f "$hook" ]]; then
|
|
171
|
-
echo "$hook"
|
|
172
|
-
return
|
|
173
|
-
fi
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
# 2) Resolve relative to this script's install location
|
|
177
|
-
local script_root
|
|
178
|
-
script_root="$(cd "$_TMUX_API_DIR/.." && pwd -P)"
|
|
179
|
-
hook="$script_root/shell/orchestra-command-hook.sh"
|
|
180
|
-
if [[ -f "$hook" ]]; then
|
|
181
|
-
echo "$hook"
|
|
182
|
-
return
|
|
183
|
-
fi
|
|
184
|
-
|
|
185
|
-
# 3) Fallback to repo root for legacy/dev flows
|
|
186
|
-
local root
|
|
187
|
-
root="$(repo_root)"
|
|
188
|
-
if [[ -n "$root" ]]; then
|
|
189
|
-
hook="$root/shell/orchestra-command-hook.sh"
|
|
190
|
-
if [[ -f "$hook" ]]; then
|
|
191
|
-
echo "$hook"
|
|
192
|
-
return
|
|
193
|
-
fi
|
|
194
|
-
fi
|
|
195
|
-
|
|
196
|
-
echo ""
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
_orchestra_bridge_script() {
|
|
200
|
-
echo "$(dirname "$_TMUX_API_DIR")/gw-bridge.sh"
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
# Source the command hook inside a tmux session to enable command history logging
|
|
204
|
-
_tmux_source_command_hook() {
|
|
205
|
-
local session="$1"
|
|
206
|
-
local hook
|
|
207
|
-
hook="$(_orchestra_command_hook)"
|
|
208
|
-
if [[ -z "$hook" ]]; then
|
|
209
|
-
return
|
|
210
|
-
fi
|
|
211
|
-
if ! tmux_session_exists "$session"; then
|
|
212
|
-
return
|
|
213
|
-
fi
|
|
214
|
-
local bridge
|
|
215
|
-
bridge="$(_orchestra_bridge_script)"
|
|
216
|
-
local panes
|
|
217
|
-
panes=$(tmux list-panes -t "$session" -F '#{pane_id} #{pane_current_command}' 2>/dev/null || echo "")
|
|
218
|
-
if [[ -z "$panes" ]]; then
|
|
219
|
-
return
|
|
220
|
-
fi
|
|
221
|
-
while IFS= read -r line; do
|
|
222
|
-
[[ -z "$line" ]] && continue
|
|
223
|
-
local pane_id pane_cmd
|
|
224
|
-
pane_id="${line%% *}"
|
|
225
|
-
pane_cmd="${line#* }"
|
|
226
|
-
case "$pane_cmd" in
|
|
227
|
-
bash|zsh)
|
|
228
|
-
tmux send-keys -t "$pane_id" "export ORCHESTRA_BRIDGE_PATH='$bridge'; . '$hook'" C-m 2>/dev/null || true
|
|
229
|
-
;;
|
|
230
|
-
*)
|
|
231
|
-
;;
|
|
232
|
-
esac
|
|
233
|
-
done <<< "$panes"
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
# Helper: orchestra prefix including delimiter
|
|
237
|
-
_tmux_orch_prefix() { printf "orchestra%s" "$( _tmux_delim )"; }
|
|
238
|
-
|
|
239
|
-
_tmux_session_registry_path() {
|
|
240
|
-
if [[ -n "${ORCHESTRA_SESSION_REGISTRY_PATH-}" ]]; then
|
|
241
|
-
printf '%s\n' "$ORCHESTRA_SESSION_REGISTRY_PATH"
|
|
242
|
-
return
|
|
243
|
-
fi
|
|
244
|
-
printf '%s\n' "$HOME/.orchestra/orchestra.sqlite3"
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
_tmux_shared_root_for_path() {
|
|
248
|
-
local path="$1"
|
|
249
|
-
local old_pwd="$PWD"
|
|
250
|
-
local root=""
|
|
251
|
-
if [[ -n "$path" && -d "$path" ]]; then
|
|
252
|
-
cd "$path" 2>/dev/null || return 1
|
|
253
|
-
root="$(git_shared_root 2>/dev/null || git_repo_root 2>/dev/null || true)"
|
|
254
|
-
cd "$old_pwd" 2>/dev/null || true
|
|
255
|
-
fi
|
|
256
|
-
[[ -n "$root" ]] || return 1
|
|
257
|
-
printf '%s\n' "$root"
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
_tmux_branch_for_path() {
|
|
261
|
-
local path="$1"
|
|
262
|
-
local old_pwd="$PWD"
|
|
263
|
-
local branch=""
|
|
264
|
-
if [[ -n "$path" && -d "$path" ]]; then
|
|
265
|
-
cd "$path" 2>/dev/null || return 1
|
|
266
|
-
branch="$(git_current_branch 2>/dev/null || true)"
|
|
267
|
-
cd "$old_pwd" 2>/dev/null || true
|
|
268
|
-
fi
|
|
269
|
-
[[ -n "$branch" ]] || return 1
|
|
270
|
-
printf '%s\n' "$branch"
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
_tmux_registry_upsert_session() {
|
|
274
|
-
local repo_root="$1"
|
|
275
|
-
local branch="$2"
|
|
276
|
-
local worktree_path="$3"
|
|
277
|
-
local tmux_name="$4"
|
|
278
|
-
local db_path
|
|
279
|
-
db_path="$(_tmux_session_registry_path)"
|
|
280
|
-
|
|
281
|
-
[[ -n "$repo_root" && -n "$branch" && -n "$worktree_path" && -n "$tmux_name" ]] || return 1
|
|
282
|
-
have_cmd python3 || return 1
|
|
283
|
-
|
|
284
|
-
python3 - "$db_path" "$repo_root" "$branch" "$worktree_path" "$tmux_name" <<'PY'
|
|
285
|
-
import os
|
|
286
|
-
import sqlite3
|
|
287
|
-
import sys
|
|
288
|
-
import time
|
|
289
|
-
|
|
290
|
-
db_path, repo_root, branch, worktree_path, tmux_name = sys.argv[1:6]
|
|
291
|
-
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
292
|
-
slug = branch.replace("/", "-")
|
|
293
|
-
now = int(time.time())
|
|
294
|
-
|
|
295
|
-
conn = sqlite3.connect(db_path)
|
|
296
|
-
conn.execute(
|
|
297
|
-
"""
|
|
298
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
299
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
300
|
-
tmux_name TEXT NOT NULL UNIQUE,
|
|
301
|
-
repo_root TEXT NOT NULL,
|
|
302
|
-
worktree_path TEXT NOT NULL,
|
|
303
|
-
branch TEXT NOT NULL,
|
|
304
|
-
slug TEXT NOT NULL,
|
|
305
|
-
display_name TEXT,
|
|
306
|
-
created_at INTEGER NOT NULL,
|
|
307
|
-
updated_at INTEGER NOT NULL,
|
|
308
|
-
last_seen_at INTEGER NOT NULL
|
|
309
|
-
)
|
|
310
|
-
"""
|
|
311
|
-
)
|
|
312
|
-
conn.execute(
|
|
313
|
-
"CREATE INDEX IF NOT EXISTS idx_sessions_worktree ON sessions(repo_root, worktree_path)"
|
|
314
|
-
)
|
|
315
|
-
conn.execute("CREATE INDEX IF NOT EXISTS idx_sessions_slug ON sessions(repo_root, slug)")
|
|
316
|
-
conn.execute(
|
|
317
|
-
"""
|
|
318
|
-
INSERT INTO sessions (
|
|
319
|
-
tmux_name, repo_root, worktree_path, branch, slug,
|
|
320
|
-
created_at, updated_at, last_seen_at
|
|
321
|
-
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6, ?6)
|
|
322
|
-
ON CONFLICT(tmux_name) DO UPDATE SET
|
|
323
|
-
repo_root = excluded.repo_root,
|
|
324
|
-
worktree_path = excluded.worktree_path,
|
|
325
|
-
branch = excluded.branch,
|
|
326
|
-
slug = excluded.slug,
|
|
327
|
-
updated_at = excluded.updated_at,
|
|
328
|
-
last_seen_at = excluded.last_seen_at
|
|
329
|
-
""",
|
|
330
|
-
(tmux_name, repo_root, worktree_path, branch, slug, now),
|
|
331
|
-
)
|
|
332
|
-
conn.commit()
|
|
333
|
-
PY
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
_tmux_registry_rename_session() {
|
|
337
|
-
local old_tmux_name="$1"
|
|
338
|
-
local new_tmux_name="$2"
|
|
339
|
-
local display_name="${3:-}"
|
|
340
|
-
local db_path
|
|
341
|
-
db_path="$(_tmux_session_registry_path)"
|
|
342
|
-
|
|
343
|
-
[[ -f "$db_path" ]] || return 0
|
|
344
|
-
have_cmd python3 || return 1
|
|
345
|
-
|
|
346
|
-
python3 - "$db_path" "$old_tmux_name" "$new_tmux_name" "$display_name" <<'PY'
|
|
347
|
-
import sqlite3
|
|
348
|
-
import sys
|
|
349
|
-
import time
|
|
350
|
-
|
|
351
|
-
db_path, old_tmux_name, new_tmux_name, display_name = sys.argv[1:5]
|
|
352
|
-
now = int(time.time())
|
|
353
|
-
value = display_name if display_name else None
|
|
354
|
-
|
|
355
|
-
conn = sqlite3.connect(db_path)
|
|
356
|
-
conn.execute(
|
|
357
|
-
"""
|
|
358
|
-
UPDATE sessions
|
|
359
|
-
SET tmux_name = ?1,
|
|
360
|
-
display_name = COALESCE(?2, display_name),
|
|
361
|
-
updated_at = ?3,
|
|
362
|
-
last_seen_at = ?3
|
|
363
|
-
WHERE tmux_name = ?4
|
|
364
|
-
""",
|
|
365
|
-
(new_tmux_name, value, now, old_tmux_name),
|
|
366
|
-
)
|
|
367
|
-
conn.commit()
|
|
368
|
-
PY
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
_tmux_registry_remove_session() {
|
|
372
|
-
local tmux_name="$1"
|
|
373
|
-
local db_path
|
|
374
|
-
db_path="$(_tmux_session_registry_path)"
|
|
375
|
-
|
|
376
|
-
[[ -f "$db_path" ]] || return 0
|
|
377
|
-
have_cmd python3 || return 1
|
|
378
|
-
|
|
379
|
-
python3 - "$db_path" "$tmux_name" <<'PY'
|
|
380
|
-
import sqlite3
|
|
381
|
-
import sys
|
|
382
|
-
|
|
383
|
-
db_path, tmux_name = sys.argv[1:3]
|
|
384
|
-
conn = sqlite3.connect(db_path)
|
|
385
|
-
conn.execute("DELETE FROM sessions WHERE tmux_name = ?1", (tmux_name,))
|
|
386
|
-
conn.commit()
|
|
387
|
-
PY
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
_tmux_registry_upsert_current_session() {
|
|
391
|
-
local session_name="$1"
|
|
392
|
-
local session_dir repo_root branch
|
|
393
|
-
session_dir="$(tmux display-message -t "$session_name" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
394
|
-
repo_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
395
|
-
branch="$(_tmux_branch_for_path "$session_dir" 2>/dev/null || true)"
|
|
396
|
-
[[ -n "$repo_root" && -n "$branch" ]] || return 1
|
|
397
|
-
_tmux_registry_upsert_session "$repo_root" "$branch" "$session_dir" "$session_name"
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
_tmux_registry_sync_active_workspace_sessions() {
|
|
401
|
-
local repo_root="$1"
|
|
402
|
-
[[ -n "$repo_root" ]] || return 1
|
|
403
|
-
tmux_available || return 1
|
|
404
|
-
|
|
405
|
-
local session_name session_dir session_root branch display_name
|
|
406
|
-
while IFS= read -r session_name; do
|
|
407
|
-
[[ -n "$session_name" ]] || continue
|
|
408
|
-
display_name="$(tmux show-option -t "$session_name" -qv @orchestra_display_name 2>/dev/null || true)"
|
|
409
|
-
[[ -n "$display_name" ]] || continue
|
|
410
|
-
session_dir="$(tmux display-message -t "$session_name" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
411
|
-
session_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
412
|
-
[[ "$session_root" == "$repo_root" ]] || continue
|
|
413
|
-
branch="$(_tmux_branch_for_path "$session_dir" 2>/dev/null || true)"
|
|
414
|
-
[[ -n "$branch" ]] || continue
|
|
415
|
-
_tmux_registry_upsert_session "$repo_root" "$branch" "$session_dir" "$session_name" >/dev/null 2>&1 || true
|
|
416
|
-
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null || true)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
_tmux_workspace_cycle_target_cached() {
|
|
420
|
-
local current_session="$1"
|
|
421
|
-
local direction="$2"
|
|
422
|
-
local db_path="$3"
|
|
423
|
-
local active_file="$4"
|
|
424
|
-
|
|
425
|
-
python3 - "$db_path" "$current_session" "$direction" "$active_file" <<'PY'
|
|
426
|
-
import sqlite3
|
|
427
|
-
import sys
|
|
428
|
-
|
|
429
|
-
db_path, current_session, direction, active_path = sys.argv[1:5]
|
|
430
|
-
active = set()
|
|
431
|
-
active_orchestra = set()
|
|
432
|
-
with open(active_path, "r", encoding="utf-8") as handle:
|
|
433
|
-
for raw_line in handle:
|
|
434
|
-
line = raw_line.rstrip("\n")
|
|
435
|
-
if not line:
|
|
436
|
-
continue
|
|
437
|
-
parts = line.split("\t")
|
|
438
|
-
name = parts[0].strip()
|
|
439
|
-
display_name = parts[1].strip() if len(parts) > 1 else ""
|
|
440
|
-
if not name:
|
|
441
|
-
continue
|
|
442
|
-
active.add(name)
|
|
443
|
-
if display_name:
|
|
444
|
-
active_orchestra.add(name)
|
|
445
|
-
|
|
446
|
-
try:
|
|
447
|
-
conn = sqlite3.connect(db_path)
|
|
448
|
-
current_row = conn.execute(
|
|
449
|
-
"SELECT repo_root FROM sessions WHERE tmux_name = ?1",
|
|
450
|
-
(current_session,),
|
|
451
|
-
).fetchone()
|
|
452
|
-
if current_row is None:
|
|
453
|
-
raise SystemExit(2)
|
|
454
|
-
|
|
455
|
-
registered = {
|
|
456
|
-
name
|
|
457
|
-
for (name,) in conn.execute("SELECT tmux_name FROM sessions").fetchall()
|
|
458
|
-
}
|
|
459
|
-
if active_orchestra.difference(registered):
|
|
460
|
-
raise SystemExit(2)
|
|
461
|
-
|
|
462
|
-
rows = conn.execute(
|
|
463
|
-
"""
|
|
464
|
-
SELECT tmux_name
|
|
465
|
-
FROM sessions
|
|
466
|
-
WHERE repo_root = ?1
|
|
467
|
-
ORDER BY worktree_path COLLATE NOCASE, created_at, tmux_name COLLATE NOCASE
|
|
468
|
-
""",
|
|
469
|
-
(current_row[0],),
|
|
470
|
-
).fetchall()
|
|
471
|
-
except sqlite3.Error:
|
|
472
|
-
raise SystemExit(2)
|
|
473
|
-
|
|
474
|
-
names = []
|
|
475
|
-
seen = set()
|
|
476
|
-
for (name,) in rows:
|
|
477
|
-
if name in active and name not in seen:
|
|
478
|
-
names.append(name)
|
|
479
|
-
seen.add(name)
|
|
480
|
-
|
|
481
|
-
if len(names) < 2 or current_session not in seen:
|
|
482
|
-
raise SystemExit(3)
|
|
483
|
-
|
|
484
|
-
index = names.index(current_session)
|
|
485
|
-
if direction == "next":
|
|
486
|
-
index = (index + 1) % len(names)
|
|
487
|
-
else:
|
|
488
|
-
index = (index - 1) % len(names)
|
|
489
|
-
print(names[index])
|
|
490
|
-
PY
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
_tmux_status_escape_text() {
|
|
494
|
-
local text="$1"
|
|
495
|
-
text="${text//\#/##}"
|
|
496
|
-
printf '%s' "$text"
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
_tmux_truncate_tab_label() {
|
|
500
|
-
local label="$1"
|
|
501
|
-
local max_len="${2:-14}"
|
|
502
|
-
if (( ${#label} > max_len )); then
|
|
503
|
-
printf '%s' "${label:0:max_len}"
|
|
504
|
-
else
|
|
505
|
-
printf '%s' "$label"
|
|
506
|
-
fi
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
_tmux_pad_menu_label() {
|
|
510
|
-
local label="$1"
|
|
511
|
-
local width="${2:-56}"
|
|
512
|
-
|
|
513
|
-
label="$(_tmux_truncate_tab_label "$label" "$width")"
|
|
514
|
-
printf '%-*s' "$width" "$label"
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
_tmux_workspace_session_rows() {
|
|
518
|
-
local current_session="$1"
|
|
519
|
-
local repo_root="$2"
|
|
520
|
-
local mode="${3:-visible}"
|
|
521
|
-
local db_path active_file
|
|
522
|
-
db_path="$(_tmux_session_registry_path)"
|
|
523
|
-
|
|
524
|
-
[[ -f "$db_path" ]] || return 0
|
|
525
|
-
have_cmd python3 || return 0
|
|
526
|
-
|
|
527
|
-
active_file="$(mktemp)"
|
|
528
|
-
tmux list-sessions -F '#{session_name}' > "$active_file" 2>/dev/null || {
|
|
529
|
-
rm -f "$active_file"
|
|
530
|
-
return 0
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
python3 - "$db_path" "$repo_root" "$current_session" "$active_file" "$mode" <<'PY'
|
|
534
|
-
import sqlite3
|
|
535
|
-
import sys
|
|
536
|
-
|
|
537
|
-
db_path, repo_root, current_session, active_path, mode = sys.argv[1:6]
|
|
538
|
-
with open(active_path, "r", encoding="utf-8") as handle:
|
|
539
|
-
active = {line.strip() for line in handle if line.strip()}
|
|
540
|
-
|
|
541
|
-
conn = sqlite3.connect(db_path)
|
|
542
|
-
rows = conn.execute(
|
|
543
|
-
"""
|
|
544
|
-
SELECT tmux_name, COALESCE(display_name, '')
|
|
545
|
-
FROM sessions
|
|
546
|
-
WHERE repo_root = ?1
|
|
547
|
-
ORDER BY worktree_path COLLATE NOCASE, created_at, tmux_name COLLATE NOCASE
|
|
548
|
-
""",
|
|
549
|
-
(repo_root,),
|
|
550
|
-
).fetchall()
|
|
551
|
-
|
|
552
|
-
filtered = []
|
|
553
|
-
seen = set()
|
|
554
|
-
for name, display_name in rows:
|
|
555
|
-
if name in active and name not in seen:
|
|
556
|
-
filtered.append((name, display_name or ""))
|
|
557
|
-
seen.add(name)
|
|
558
|
-
|
|
559
|
-
if not filtered:
|
|
560
|
-
raise SystemExit(0)
|
|
561
|
-
|
|
562
|
-
max_tabs = 8
|
|
563
|
-
if mode == "all" or len(filtered) <= max_tabs:
|
|
564
|
-
visible = filtered
|
|
565
|
-
prefix = suffix = False
|
|
566
|
-
else:
|
|
567
|
-
try:
|
|
568
|
-
current_index = [name for name, _ in filtered].index(current_session)
|
|
569
|
-
except ValueError:
|
|
570
|
-
current_index = 0
|
|
571
|
-
start = max(0, min(current_index - 2, len(filtered) - max_tabs))
|
|
572
|
-
end = min(len(filtered), start + max_tabs)
|
|
573
|
-
visible = filtered[start:end]
|
|
574
|
-
prefix = start > 0
|
|
575
|
-
suffix = end < len(filtered)
|
|
576
|
-
|
|
577
|
-
if prefix:
|
|
578
|
-
print("__ellipsis__\t")
|
|
579
|
-
for name, display_name in visible:
|
|
580
|
-
print(f"{name}\t{display_name}")
|
|
581
|
-
if suffix:
|
|
582
|
-
print("__ellipsis__\t")
|
|
583
|
-
PY
|
|
584
|
-
local status=$?
|
|
585
|
-
rm -f "$active_file"
|
|
586
|
-
return $status
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
_tmux_workspace_session_tabs() {
|
|
590
|
-
local current_session="$1"
|
|
591
|
-
local fallback_display_name="$2"
|
|
592
|
-
local session_dir repo_root rows tabs name display_name tab_label escaped_tab_label divider active_style inactive_style muted_style reset_style
|
|
593
|
-
local tab_max_width=24
|
|
594
|
-
local active_label_max_width=22
|
|
595
|
-
|
|
596
|
-
session_dir="$(tmux display-message -t "$current_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
597
|
-
repo_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
598
|
-
if [[ -n "$repo_root" ]]; then
|
|
599
|
-
_tmux_registry_sync_active_workspace_sessions "$repo_root" >/dev/null 2>&1 || true
|
|
600
|
-
rows="$(_tmux_workspace_session_rows "$current_session" "$repo_root" 2>/dev/null || true)"
|
|
601
|
-
else
|
|
602
|
-
rows=""
|
|
603
|
-
fi
|
|
604
|
-
|
|
605
|
-
if [[ -z "$rows" ]]; then
|
|
606
|
-
tab_label="$(_tmux_truncate_tab_label "$fallback_display_name" "$active_label_max_width")"
|
|
607
|
-
escaped_tab_label="$(_tmux_status_escape_text "$tab_label")"
|
|
608
|
-
printf '#[bg=#414868] #[fg=#ff9e64,bg=#414868,bold]● #[fg=#c0caf5,bg=#414868,bold]%s #[default]' "$escaped_tab_label"
|
|
609
|
-
return
|
|
610
|
-
fi
|
|
611
|
-
|
|
612
|
-
# Tokyo Night palette: midnight footer, muted inactive tabs, orange active marker.
|
|
613
|
-
active_style="#[fg=#c0caf5,bg=#414868,bold]"
|
|
614
|
-
inactive_style="#[fg=#a9b1d6,bg=#24283b,nobold]"
|
|
615
|
-
muted_style="#[fg=#565f89,bg=#1a1b26,nobold]"
|
|
616
|
-
reset_style="#[default]"
|
|
617
|
-
tabs=""
|
|
618
|
-
divider="${muted_style}|${reset_style}"
|
|
619
|
-
while IFS=$'\t' read -r name display_name; do
|
|
620
|
-
[[ -n "$name" ]] || continue
|
|
621
|
-
if [[ -n "$tabs" ]]; then
|
|
622
|
-
tabs+="$divider"
|
|
623
|
-
fi
|
|
624
|
-
if [[ "$name" == "__ellipsis__" ]]; then
|
|
625
|
-
tab_label="⋯"
|
|
626
|
-
escaped_tab_label="$(_tmux_status_escape_text "$tab_label")"
|
|
627
|
-
tabs+="${muted_style} ${escaped_tab_label} ${reset_style}"
|
|
628
|
-
continue
|
|
629
|
-
fi
|
|
630
|
-
|
|
631
|
-
if [[ -z "$display_name" ]]; then
|
|
632
|
-
display_name="$(tmux_format_session_display "$name" without-timestamp)"
|
|
633
|
-
fi
|
|
634
|
-
if [[ "$name" == "$current_session" ]]; then
|
|
635
|
-
tab_label="$(_tmux_truncate_tab_label "$display_name" "$active_label_max_width")"
|
|
636
|
-
escaped_tab_label="$(_tmux_status_escape_text "$tab_label")"
|
|
637
|
-
tabs+="#[bg=#414868] #[fg=#ff9e64,bg=#414868,bold]● ${active_style}${escaped_tab_label} ${reset_style}"
|
|
638
|
-
else
|
|
639
|
-
tab_label="$(_tmux_truncate_tab_label "$display_name" "$tab_max_width")"
|
|
640
|
-
escaped_tab_label="$(_tmux_status_escape_text "$tab_label")"
|
|
641
|
-
tabs+="${inactive_style} ${escaped_tab_label} ${reset_style}"
|
|
642
|
-
fi
|
|
643
|
-
done <<< "$rows"
|
|
644
|
-
|
|
645
|
-
printf '%s' "$tabs"
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
_tmux_current_session_label() {
|
|
649
|
-
local session_display_name="$1"
|
|
650
|
-
local label escaped_label
|
|
651
|
-
|
|
652
|
-
label="$(_tmux_truncate_tab_label "$session_display_name" 28)"
|
|
653
|
-
escaped_label="$(_tmux_status_escape_text "$label")"
|
|
654
|
-
printf '#[fg=#7aa2f7,bg=#1a1b26,bold] %s #[fg=#565f89,bg=#1a1b26,nobold]│#[default]' "$escaped_label"
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
_tmux_orchestra_status_left() {
|
|
658
|
-
local session_name="$1"
|
|
659
|
-
local session_display_name="$3"
|
|
660
|
-
printf '%s%s' \
|
|
661
|
-
"$(_tmux_current_session_label "$session_display_name")" \
|
|
662
|
-
"$(_tmux_workspace_session_tabs "$session_name" "$session_display_name")"
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
_tmux_orchestra_status_right() {
|
|
666
|
-
printf '#[fg=#565f89,bg=#1a1b26]Ctrl+b,h for help#[default]'
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
_tmux_configure_orchestra_bindings() {
|
|
670
|
-
local bridge
|
|
671
|
-
bridge="$(_orchestra_bridge_script)"
|
|
672
|
-
[[ -f "$bridge" ]] || return
|
|
673
|
-
|
|
674
|
-
local quoted_bridge rename_command prompt_command next_command previous_command new_session_command list_command close_command close_prompt_command help_command
|
|
675
|
-
printf -v quoted_bridge '%q' "$bridge"
|
|
676
|
-
rename_command="$quoted_bridge manual-rename-session \\\"#{session_name}\\\" \\\"%%\\\" >/dev/null 2>&1"
|
|
677
|
-
prompt_command="command-prompt -p 'Rename Orchestra session:' 'run-shell -b \"$rename_command\"'"
|
|
678
|
-
next_command="$quoted_bridge cycle-workspace-session \\\"#{session_name}\\\" next \\\"#{client_tty}\\\" >/dev/null 2>&1"
|
|
679
|
-
previous_command="$quoted_bridge cycle-workspace-session \\\"#{session_name}\\\" previous \\\"#{client_tty}\\\" >/dev/null 2>&1"
|
|
680
|
-
new_session_command="$quoted_bridge create-workspace-session \\\"#{session_name}\\\" \\\"#{client_tty}\\\" >/dev/null 2>&1 || true"
|
|
681
|
-
list_command="$quoted_bridge workspace-session-menu \\\"#{session_name}\\\" \\\"#{client_tty}\\\" >/dev/null 2>&1 || true"
|
|
682
|
-
close_command="$quoted_bridge close-workspace-session \\\"#{session_name}\\\" \\\"#{client_tty}\\\" >/dev/null 2>&1 || true"
|
|
683
|
-
close_prompt_command="confirm-before -p 'Close current Orchestra session? (y/n)' 'run-shell -b \"$close_command\"'"
|
|
684
|
-
help_command="$quoted_bridge tmux-help-popup \\\"#{client_tty}\\\" >/dev/null 2>&1 || true"
|
|
685
|
-
|
|
686
|
-
tmux bind-key -T prefix '?' if-shell -F '#{@orchestra_display_name}' \
|
|
687
|
-
"run-shell -b \"$help_command\"" 'list-keys -N' >/dev/null 2>&1 || true
|
|
688
|
-
tmux bind-key -T prefix h if-shell -F '#{@orchestra_display_name}' \
|
|
689
|
-
"run-shell -b \"$help_command\"" 'refresh-client -S' >/dev/null 2>&1 || true
|
|
690
|
-
tmux bind-key -T prefix n if-shell -F '#{@orchestra_display_name}' \
|
|
691
|
-
"run-shell -b \"$new_session_command\"" 'next-window' >/dev/null 2>&1 || true
|
|
692
|
-
tmux bind-key -T prefix l if-shell -F '#{@orchestra_display_name}' \
|
|
693
|
-
"run-shell -b \"$list_command\"" 'last-window' >/dev/null 2>&1 || true
|
|
694
|
-
tmux bind-key -T prefix q if-shell -F '#{@orchestra_display_name}' \
|
|
695
|
-
"$close_prompt_command" 'display-panes' >/dev/null 2>&1 || true
|
|
696
|
-
tmux bind-key -T prefix X if-shell -F '#{@orchestra_display_name}' \
|
|
697
|
-
"$close_prompt_command" 'confirm-before -p "kill-session #S? (y/n)" kill-session' >/dev/null 2>&1 || true
|
|
698
|
-
tmux bind-key -T prefix r if-shell -F '#{@orchestra_display_name}' \
|
|
699
|
-
"$prompt_command" 'refresh-client -S' >/dev/null 2>&1 || true
|
|
700
|
-
tmux bind-key -T prefix '>' if-shell -F '#{@orchestra_display_name}' \
|
|
701
|
-
"run-shell -b \"$next_command\"" 'switch-client -n' >/dev/null 2>&1 || true
|
|
702
|
-
tmux bind-key -T prefix '<' if-shell -F '#{@orchestra_display_name}' \
|
|
703
|
-
"run-shell -b \"$previous_command\"" 'switch-client -p' >/dev/null 2>&1 || true
|
|
704
|
-
tmux bind-key -r -T prefix Right if-shell -F '#{@orchestra_display_name}' \
|
|
705
|
-
"run-shell -b \"$next_command\"" 'select-pane -R' >/dev/null 2>&1 || true
|
|
706
|
-
tmux bind-key -r -T prefix Left if-shell -F '#{@orchestra_display_name}' \
|
|
707
|
-
"run-shell -b \"$previous_command\"" 'select-pane -L' >/dev/null 2>&1 || true
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
_tmux_configure_orchestra_status() {
|
|
711
|
-
local session_name="$1"
|
|
712
|
-
local worktree_name="$2"
|
|
713
|
-
local session_display_name="${3:-}"
|
|
714
|
-
local status_left status_right
|
|
715
|
-
if [[ -z "$session_display_name" ]]; then
|
|
716
|
-
session_display_name="$(tmux_format_session_display "$session_name" without-timestamp)"
|
|
717
|
-
fi
|
|
718
|
-
status_left="$(_tmux_orchestra_status_left "$session_name" "$worktree_name" "$session_display_name")"
|
|
719
|
-
status_right="$(_tmux_orchestra_status_right "$worktree_name")"
|
|
720
|
-
|
|
721
|
-
tmux set-option -t "$session_name" @orchestra_display_name "$session_display_name" >/dev/null 2>&1 || true
|
|
722
|
-
tmux set-option -t "$session_name" @orchestra_worktree_name "$worktree_name" >/dev/null 2>&1 || true
|
|
723
|
-
tmux set-option -t "$session_name" status on >/dev/null 2>&1 || true
|
|
724
|
-
tmux set-option -t "$session_name" status-position bottom >/dev/null 2>&1 || true
|
|
725
|
-
tmux set-option -t "$session_name" status-style "fg=#c0caf5,bg=#1a1b26" >/dev/null 2>&1 || true
|
|
726
|
-
tmux set-option -t "$session_name" status-left "$status_left" >/dev/null 2>&1 || true
|
|
727
|
-
tmux set-option -t "$session_name" status-left-length 1000 >/dev/null 2>&1 || true
|
|
728
|
-
tmux set-option -t "$session_name" status-right "$status_right" >/dev/null 2>&1 || true
|
|
729
|
-
tmux set-option -t "$session_name" status-right-length 40 >/dev/null 2>&1 || true
|
|
730
|
-
tmux set-option -t "$session_name" window-status-format "" >/dev/null 2>&1 || true
|
|
731
|
-
tmux set-option -t "$session_name" window-status-current-format "" >/dev/null 2>&1 || true
|
|
732
|
-
tmux set-option -t "$session_name" window-status-separator "" >/dev/null 2>&1 || true
|
|
733
|
-
_tmux_configure_orchestra_bindings
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
_tmux_refresh_orchestra_session_status() {
|
|
737
|
-
local session_name="$1"
|
|
738
|
-
local session_dir branch_name worktree_name display_name old_pwd
|
|
739
|
-
|
|
740
|
-
session_dir="$(tmux display-message -t "$session_name" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
741
|
-
branch_name=""
|
|
742
|
-
if [[ -n "$session_dir" && -d "$session_dir" ]]; then
|
|
743
|
-
old_pwd="$PWD"
|
|
744
|
-
cd "$session_dir" 2>/dev/null || true
|
|
745
|
-
branch_name="$(git_current_branch 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
746
|
-
cd "$old_pwd" 2>/dev/null || true
|
|
747
|
-
fi
|
|
748
|
-
|
|
749
|
-
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
750
|
-
worktree_name="$branch_name"
|
|
751
|
-
elif [[ -n "$session_dir" && -d "$session_dir" ]]; then
|
|
752
|
-
worktree_name="$(basename "$session_dir")"
|
|
753
|
-
else
|
|
754
|
-
worktree_name="$branch_name"
|
|
755
|
-
fi
|
|
756
|
-
|
|
757
|
-
display_name="$(tmux show-option -t "$session_name" -qv @orchestra_display_name 2>/dev/null || true)"
|
|
758
|
-
_tmux_configure_orchestra_status "$session_name" "$worktree_name" "$display_name"
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
# Helper: split a string by multi-char delimiter into bash array named by ref
|
|
762
|
-
# Usage: _tmux_split_by_delim "string" "::" out_array_name
|
|
763
|
-
_tmux_split_by_delim() {
|
|
764
|
-
local _s="$1" _d="$2" _ref="$3"
|
|
765
|
-
local _arr=()
|
|
766
|
-
if [[ -z "$_d" ]]; then
|
|
767
|
-
_arr=("$_s")
|
|
768
|
-
else
|
|
769
|
-
while :; do
|
|
770
|
-
if [[ "$_s" == *"$_d"* ]]; then
|
|
771
|
-
_arr+=("${_s%%"$_d"*}")
|
|
772
|
-
_s="${_s#*"$_d"}"
|
|
773
|
-
else
|
|
774
|
-
_arr+=("$_s")
|
|
775
|
-
break
|
|
776
|
-
fi
|
|
777
|
-
done
|
|
778
|
-
fi
|
|
779
|
-
# Use printf with %q to properly quote array elements for eval
|
|
780
|
-
local _quoted=()
|
|
781
|
-
local _item
|
|
782
|
-
for _item in "${_arr[@]}"; do
|
|
783
|
-
printf -v _q "%q" "$_item"
|
|
784
|
-
_quoted+=("$_q")
|
|
785
|
-
done
|
|
786
|
-
eval "$_ref=( ${_quoted[*]} )"
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
# Check if tmux is available
|
|
791
|
-
tmux_available() {
|
|
792
|
-
have_cmd tmux
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
# Check if currently inside a tmux session
|
|
796
|
-
tmux_inside_session() {
|
|
797
|
-
[[ -n "${TMUX-}" ]]
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
# Generate random readable name for tmux sessions
|
|
801
|
-
tmux_generate_readable_name() {
|
|
802
|
-
local adjectives=(
|
|
803
|
-
"swift" "brave" "clever" "gentle" "bright" "calm" "eager" "fierce" "happy" "kind"
|
|
804
|
-
"lively" "noble" "proud" "quick" "smart" "wise" "bold" "cool" "daring" "epic"
|
|
805
|
-
"fuzzy" "jolly" "lucky" "merry" "peppy" "rosy" "sunny" "zesty" "crisp" "fresh"
|
|
806
|
-
)
|
|
807
|
-
local animals=(
|
|
808
|
-
"bear" "wolf" "fox" "eagle" "hawk" "lion" "tiger" "panda" "otter" "seal"
|
|
809
|
-
"whale" "shark" "dolphin" "falcon" "raven" "deer" "moose" "lynx" "badger" "heron"
|
|
810
|
-
"phoenix" "dragon" "griffin" "unicorn" "pegasus" "kraken" "sphinx" "chimera" "hydra" "basilisk"
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
local adj_idx=$((RANDOM % ${#adjectives[@]}))
|
|
814
|
-
local animal_idx=$((RANDOM % ${#animals[@]}))
|
|
815
|
-
|
|
816
|
-
echo "${adjectives[$adj_idx]}_${animals[$animal_idx]}"
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
# --------------------------- Session Management -----------------------------
|
|
820
|
-
|
|
821
|
-
# Create a new tmux session
|
|
822
|
-
# Usage: tmux_create_session <session_name> <working_directory>
|
|
823
|
-
tmux_create_session() {
|
|
824
|
-
local session_name="$1"
|
|
825
|
-
local working_dir="$2"
|
|
826
|
-
|
|
827
|
-
if ! tmux_available; then
|
|
828
|
-
err "tmux not installed"
|
|
829
|
-
return 1
|
|
830
|
-
fi
|
|
831
|
-
|
|
832
|
-
# Get repository info from the working directory context
|
|
833
|
-
local repo_name=""
|
|
834
|
-
local branch_name=""
|
|
835
|
-
local repo_root=""
|
|
836
|
-
local old_pwd="$PWD"
|
|
837
|
-
local resolved_working_dir="$working_dir"
|
|
838
|
-
if [[ -n "$working_dir" && -d "$working_dir" ]]; then
|
|
839
|
-
resolved_working_dir="$(cd "$working_dir" 2>/dev/null && pwd -P || printf '%s' "$working_dir")"
|
|
840
|
-
fi
|
|
841
|
-
|
|
842
|
-
# Change to working directory to get accurate git info
|
|
843
|
-
cd "$resolved_working_dir" 2>/dev/null || true
|
|
844
|
-
|
|
845
|
-
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
846
|
-
repo_name="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")"
|
|
847
|
-
branch_name="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
|
|
848
|
-
repo_root="$(git_shared_root 2>/dev/null || git_repo_root 2>/dev/null || true)"
|
|
849
|
-
fi
|
|
850
|
-
|
|
851
|
-
# Restore original directory
|
|
852
|
-
cd "$old_pwd" 2>/dev/null || true
|
|
853
|
-
|
|
854
|
-
# Ensure orchestra prefix (configurable delimiter) on session name to mark origin
|
|
855
|
-
local ORCH_PREFIX
|
|
856
|
-
ORCH_PREFIX="$(_tmux_orch_prefix)"
|
|
857
|
-
if [[ "$session_name" != ${ORCH_PREFIX}* ]]; then
|
|
858
|
-
session_name="${ORCH_PREFIX}${session_name}"
|
|
859
|
-
fi
|
|
860
|
-
|
|
861
|
-
# Create session with custom Orchestra status configuration
|
|
862
|
-
tmux new-session -Ad -s "$session_name" -c "$resolved_working_dir" >/dev/null 2>&1 || {
|
|
863
|
-
err "Failed to create tmux session: $session_name"
|
|
864
|
-
return 1
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
local worktree_name=""
|
|
868
|
-
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
869
|
-
worktree_name="$branch_name"
|
|
870
|
-
else
|
|
871
|
-
worktree_name="$(basename "$resolved_working_dir")"
|
|
872
|
-
fi
|
|
873
|
-
|
|
874
|
-
if [[ -n "$repo_root" && -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
875
|
-
_tmux_registry_upsert_session "$repo_root" "$branch_name" "$resolved_working_dir" "$session_name" >/dev/null 2>&1 || true
|
|
876
|
-
fi
|
|
877
|
-
|
|
878
|
-
# Customize the default status bar to include Orchestra info on the left
|
|
879
|
-
if [[ -n "$repo_name" ]]; then
|
|
880
|
-
_tmux_configure_orchestra_status "$session_name" "$worktree_name"
|
|
881
|
-
fi
|
|
882
|
-
|
|
883
|
-
echo "$session_name"
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
# Generate a short hash from a path for unique session identification
|
|
887
|
-
# Usage: tmux_path_hash <path>
|
|
888
|
-
tmux_path_hash() {
|
|
889
|
-
local path="$1"
|
|
890
|
-
# Use MD5 hash of the path, take first 8 chars
|
|
891
|
-
# Works on both macOS and Linux
|
|
892
|
-
if command -v md5sum >/dev/null 2>&1; then
|
|
893
|
-
echo -n "$path" | md5sum | cut -c1-8
|
|
894
|
-
elif command -v md5 >/dev/null 2>&1; then
|
|
895
|
-
echo -n "$path" | md5 -q | cut -c1-8
|
|
896
|
-
else
|
|
897
|
-
# Fallback: use cksum if neither md5 available
|
|
898
|
-
echo -n "$path" | cksum | cut -d' ' -f1 | cut -c1-8
|
|
899
|
-
fi
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
# Alias for backward compatibility
|
|
903
|
-
tmux_repo_hash() {
|
|
904
|
-
tmux_path_hash "$1"
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
# Ensure a session exists for slug+name in worktree; prints session name
|
|
908
|
-
# Creates sessions with format: [worktreename]_[worktreetreehash]_[datetime]_[readable_name]
|
|
909
|
-
# Usage: tmux_ensure_session <slug> <name> <worktree_path>
|
|
910
|
-
tmux_ensure_session() {
|
|
911
|
-
local slug="$1"
|
|
912
|
-
local name="$2"
|
|
913
|
-
local wt="$3"
|
|
914
|
-
local d
|
|
915
|
-
d="$(_tmux_delim)"
|
|
916
|
-
local date_part time_part
|
|
917
|
-
date_part="$(date +%Y%m%d)"
|
|
918
|
-
time_part="$(date +%H%M%S)"
|
|
919
|
-
|
|
920
|
-
# Use a repo-scoped hash to avoid cross-repo collisions
|
|
921
|
-
# Hash the absolute worktree path (backward-compat listing supports old slug-hash)
|
|
922
|
-
local worktree_hash
|
|
923
|
-
worktree_hash="$(tmux_path_hash "$wt")"
|
|
924
|
-
|
|
925
|
-
# If name is provided, use it; otherwise generate a random readable name with auto_ prefix
|
|
926
|
-
if [[ -z "$name" || "$name" == "main" ]]; then
|
|
927
|
-
name="auto_$(tmux_generate_readable_name)"
|
|
928
|
-
fi
|
|
929
|
-
|
|
930
|
-
local sess="${slug}${d}${worktree_hash}${d}${date_part}${d}${time_part}${d}${name}"
|
|
931
|
-
tmux_create_session "$sess" "$wt"
|
|
932
|
-
_tmux_source_command_hook "$sess"
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
# Create a new auto-named Orchestra session in the current session's worktree and switch to it.
|
|
936
|
-
# Uses the same tmux_ensure_session path as the TUI tree view create action.
|
|
937
|
-
# Usage: tmux_create_workspace_session <current_session> [client_tty]
|
|
938
|
-
tmux_create_workspace_session() {
|
|
939
|
-
local current_session="$1"
|
|
940
|
-
local target_client="${2:-}"
|
|
941
|
-
local session_dir branch slug new_session
|
|
942
|
-
|
|
943
|
-
if ! tmux_available; then
|
|
944
|
-
err "tmux not installed"
|
|
945
|
-
return 1
|
|
946
|
-
fi
|
|
947
|
-
if [[ -z "$current_session" ]]; then
|
|
948
|
-
err "tmux_create_workspace_session: current session required"
|
|
949
|
-
return 1
|
|
950
|
-
fi
|
|
951
|
-
|
|
952
|
-
session_dir="$(tmux display-message -t "$current_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
953
|
-
if [[ -z "$session_dir" || ! -d "$session_dir" ]]; then
|
|
954
|
-
err "Unable to determine current worktree path"
|
|
955
|
-
return 1
|
|
956
|
-
fi
|
|
957
|
-
|
|
958
|
-
branch="$(_tmux_branch_for_path "$session_dir" 2>/dev/null || true)"
|
|
959
|
-
if [[ -z "$branch" || "$branch" == "detached" ]]; then
|
|
960
|
-
err "Unable to determine current worktree branch"
|
|
961
|
-
return 1
|
|
962
|
-
fi
|
|
963
|
-
|
|
964
|
-
slug="$(git_branch_to_slug "$branch")"
|
|
965
|
-
new_session="$(tmux_ensure_session "$slug" "" "$session_dir")" || return 1
|
|
966
|
-
|
|
967
|
-
if [[ -n "$target_client" ]]; then
|
|
968
|
-
tmux switch-client -c "$target_client" -t "$new_session" >/dev/null 2>&1 || return 1
|
|
969
|
-
else
|
|
970
|
-
tmux switch-client -t "$new_session" >/dev/null 2>&1 || return 1
|
|
971
|
-
fi
|
|
972
|
-
|
|
973
|
-
printf '%s\n' "$new_session"
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
# Close the current Orchestra session after switching the client to the next one.
|
|
977
|
-
# Usage: tmux_close_workspace_session <current_session> [client_tty]
|
|
978
|
-
tmux_close_workspace_session() {
|
|
979
|
-
local current_session="$1"
|
|
980
|
-
local target_client="${2:-}"
|
|
981
|
-
local target_session
|
|
982
|
-
|
|
983
|
-
if ! tmux_available; then
|
|
984
|
-
err "tmux not installed"
|
|
985
|
-
return 1
|
|
986
|
-
fi
|
|
987
|
-
if [[ -z "$current_session" ]]; then
|
|
988
|
-
err "tmux_close_workspace_session: current session required"
|
|
989
|
-
return 1
|
|
990
|
-
fi
|
|
991
|
-
|
|
992
|
-
if ! target_session="$(tmux_workspace_cycle_target "$current_session" next)"; then
|
|
993
|
-
tmux display-message -d 2500 "No other Orchestra sessions; current session kept" 2>/dev/null || true
|
|
994
|
-
return 1
|
|
995
|
-
fi
|
|
996
|
-
|
|
997
|
-
if [[ -n "$target_client" ]]; then
|
|
998
|
-
tmux switch-client -c "$target_client" -t "$target_session" >/dev/null 2>&1 || return 1
|
|
999
|
-
else
|
|
1000
|
-
tmux switch-client -t "$target_session" >/dev/null 2>&1 || return 1
|
|
1001
|
-
fi
|
|
1002
|
-
|
|
1003
|
-
tmux_kill_session "$current_session" >/dev/null 2>&1 || return 1
|
|
1004
|
-
_tmux_refresh_orchestra_session_status "$target_session" >/dev/null 2>&1 || true
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
# Check if a session exists
|
|
1008
|
-
# Usage: tmux_session_exists <session_name>
|
|
1009
|
-
tmux_session_exists() {
|
|
1010
|
-
local session_name="$1"
|
|
1011
|
-
tmux_available && tmux has-session -t "$session_name" 2>/dev/null
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
# Kill/delete a tmux session
|
|
1015
|
-
# Usage: tmux_kill_session <session_name>
|
|
1016
|
-
tmux_kill_session() {
|
|
1017
|
-
local session_name="$1"
|
|
1018
|
-
|
|
1019
|
-
if ! tmux_available; then
|
|
1020
|
-
err "tmux not installed"
|
|
1021
|
-
return 1
|
|
1022
|
-
fi
|
|
1023
|
-
|
|
1024
|
-
tmux kill-session -t "$session_name" 2>/dev/null || {
|
|
1025
|
-
err "Failed to kill session: $session_name"
|
|
1026
|
-
return 1
|
|
1027
|
-
}
|
|
1028
|
-
_tmux_registry_remove_session "$session_name" >/dev/null 2>&1 || true
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
# Attach or switch to session
|
|
1032
|
-
# Usage: tmux_attach_session <session_name>
|
|
1033
|
-
tmux_attach_session() {
|
|
1034
|
-
local sess="$1"
|
|
1035
|
-
|
|
1036
|
-
if ! tmux_available; then
|
|
1037
|
-
err "tmux not installed"
|
|
1038
|
-
return 1
|
|
1039
|
-
fi
|
|
1040
|
-
|
|
1041
|
-
_tmux_source_command_hook "$sess"
|
|
1042
|
-
|
|
1043
|
-
# Get repository info for terminal title and banner update
|
|
1044
|
-
local repo_name=""
|
|
1045
|
-
local branch_name=""
|
|
1046
|
-
local session_dir=""
|
|
1047
|
-
|
|
1048
|
-
# Try to get the session's working directory
|
|
1049
|
-
session_dir="$(tmux display-message -t "$sess" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
1050
|
-
|
|
1051
|
-
if [[ -n "$session_dir" ]] && [[ -d "$session_dir" ]]; then
|
|
1052
|
-
# Get git info from session's directory
|
|
1053
|
-
local old_pwd="$PWD"
|
|
1054
|
-
cd "$session_dir" 2>/dev/null || true
|
|
1055
|
-
|
|
1056
|
-
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1057
|
-
repo_name="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")"
|
|
1058
|
-
branch_name="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
|
|
1059
|
-
fi
|
|
1060
|
-
|
|
1061
|
-
cd "$old_pwd" 2>/dev/null || true
|
|
1062
|
-
else
|
|
1063
|
-
# Fallback to current directory
|
|
1064
|
-
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
1065
|
-
repo_name="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")"
|
|
1066
|
-
branch_name="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
|
|
1067
|
-
fi
|
|
1068
|
-
fi
|
|
1069
|
-
|
|
1070
|
-
# Set terminal window title (works in most terminal emulators)
|
|
1071
|
-
if [[ -n "$repo_name" ]]; then
|
|
1072
|
-
printf "\033]0;🎼 Orchestra: %s [%s]\007" "$repo_name" "$branch_name" >/dev/tty 2>/dev/null || true
|
|
1073
|
-
fi
|
|
1074
|
-
|
|
1075
|
-
# Show welcome message when attaching
|
|
1076
|
-
if [[ -n "$repo_name" ]]; then
|
|
1077
|
-
tmux display-message -t "$sess" -d 2000 \
|
|
1078
|
-
"🎼 Orchestra Session | Repository: $repo_name | Branch: $branch_name" 2>/dev/null || true
|
|
1079
|
-
fi
|
|
1080
|
-
|
|
1081
|
-
_tmux_refresh_orchestra_session_status "$sess"
|
|
1082
|
-
|
|
1083
|
-
if tmux_inside_session; then
|
|
1084
|
-
tmux switch-client -t "$sess" >/dev/null 2>&1 || true
|
|
1085
|
-
else
|
|
1086
|
-
tmux attach -t "$sess" >/dev/null 2>&1 || true
|
|
1087
|
-
fi
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
# Rename a tmux session while preserving the worktree prefix, repo hash, and datetime
|
|
1091
|
-
# Usage: tmux_rename_session <old_session_name> <new_name>
|
|
1092
|
-
tmux_rename_session() {
|
|
1093
|
-
local old_session="$1"
|
|
1094
|
-
local new_name="$2"
|
|
1095
|
-
|
|
1096
|
-
if ! tmux_available; then
|
|
1097
|
-
err "tmux not installed"
|
|
1098
|
-
return 1
|
|
1099
|
-
fi
|
|
1100
|
-
|
|
1101
|
-
local d ORCH_PREFIX
|
|
1102
|
-
d="$(_tmux_delim)"
|
|
1103
|
-
ORCH_PREFIX="$(_tmux_orch_prefix)"
|
|
1104
|
-
|
|
1105
|
-
# Detect and strip orchestra prefix for parsing
|
|
1106
|
-
local orch=""
|
|
1107
|
-
local base="$old_session"
|
|
1108
|
-
if [[ "$base" == ${ORCH_PREFIX}* ]]; then
|
|
1109
|
-
orch="$ORCH_PREFIX"
|
|
1110
|
-
base="${base#${ORCH_PREFIX}}"
|
|
1111
|
-
fi
|
|
1112
|
-
|
|
1113
|
-
# Split by current delimiter and scan from the right to tolerate delimiter inside segments
|
|
1114
|
-
local parts
|
|
1115
|
-
_tmux_split_by_delim "$base" "$d" parts
|
|
1116
|
-
|
|
1117
|
-
local n=${#parts[@]}
|
|
1118
|
-
local idx_time=-1 idx_date=-1 idx_hash=-1
|
|
1119
|
-
local i
|
|
1120
|
-
for (( i=n-1; i>=0; i-- )); do
|
|
1121
|
-
if [[ ${parts[$i]} =~ ^[0-9]{6}$ ]]; then idx_time=$i; break; fi
|
|
1122
|
-
done
|
|
1123
|
-
if (( idx_time > 0 )) && [[ ${parts[$((idx_time-1))]} =~ ^[0-9]{8}$ ]]; then
|
|
1124
|
-
idx_date=$((idx_time-1))
|
|
1125
|
-
fi
|
|
1126
|
-
if (( idx_date > 0 )) && [[ ${parts[$((idx_date-1))]} =~ ^[0-9a-f]{8}$ ]]; then
|
|
1127
|
-
idx_hash=$((idx_date-1))
|
|
1128
|
-
fi
|
|
1129
|
-
|
|
1130
|
-
local prefix=""
|
|
1131
|
-
if (( idx_time >= 0 && idx_date >= 0 )); then
|
|
1132
|
-
# Build prefix: slug + d + [hash + d] + date + d + time + d
|
|
1133
|
-
local upto=$idx_time
|
|
1134
|
-
local j
|
|
1135
|
-
for (( j=0; j<=upto; j++ )); do
|
|
1136
|
-
if (( j > 0 )); then prefix+="$d"; fi
|
|
1137
|
-
prefix+="${parts[$j]}"
|
|
1138
|
-
done
|
|
1139
|
-
prefix+="$d"
|
|
1140
|
-
|
|
1141
|
-
# Debug logging for rename parsing
|
|
1142
|
-
if [[ -n "${GW_DEBUG_RENAME-}" || -n "${DEBUG-}" ]]; then
|
|
1143
|
-
>&2 echo "[orchestra] rename DEBUG: old='$old_session' base='$base' delim='$d'"
|
|
1144
|
-
>&2 echo "[orchestra] parts: ${parts[*]}"
|
|
1145
|
-
>&2 echo "[orchestra] idx_time=$idx_time idx_date=$idx_date idx_hash=$idx_hash"
|
|
1146
|
-
>&2 echo "[orchestra] prefix='${orch}${prefix}' new_name='$new_name'"
|
|
1147
|
-
fi
|
|
1148
|
-
else
|
|
1149
|
-
err "Invalid session format"
|
|
1150
|
-
return 1
|
|
1151
|
-
fi
|
|
1152
|
-
|
|
1153
|
-
local new_session="${orch}${prefix}${new_name}"
|
|
1154
|
-
|
|
1155
|
-
tmux rename-session -t "$old_session" "$new_session" 2>/dev/null || {
|
|
1156
|
-
err "Failed to rename session"
|
|
1157
|
-
return 1
|
|
1158
|
-
}
|
|
1159
|
-
_tmux_registry_rename_session "$old_session" "$new_session" "$(format_session_display_name "$new_name")" >/dev/null 2>&1 || true
|
|
1160
|
-
local worktree_name
|
|
1161
|
-
worktree_name="$(tmux show-option -t "$new_session" -qv @orchestra_worktree_name 2>/dev/null || true)"
|
|
1162
|
-
if [[ -z "$worktree_name" ]]; then
|
|
1163
|
-
local session_dir old_pwd branch_name
|
|
1164
|
-
session_dir="$(tmux display-message -t "$new_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
1165
|
-
if [[ -n "$session_dir" && -d "$session_dir" ]]; then
|
|
1166
|
-
old_pwd="$PWD"
|
|
1167
|
-
cd "$session_dir" 2>/dev/null || true
|
|
1168
|
-
branch_name="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
1169
|
-
cd "$old_pwd" 2>/dev/null || true
|
|
1170
|
-
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
1171
|
-
worktree_name="$branch_name"
|
|
1172
|
-
else
|
|
1173
|
-
worktree_name="$(basename "$session_dir")"
|
|
1174
|
-
fi
|
|
1175
|
-
fi
|
|
1176
|
-
fi
|
|
1177
|
-
_tmux_configure_orchestra_status "$new_session" "$worktree_name"
|
|
1178
|
-
>&2 echo "✏️ Renamed session to: $new_name"
|
|
1179
|
-
return 0
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
# --------------------------- Session Discovery ------------------------------
|
|
1183
|
-
|
|
1184
|
-
# Get tmux sessions for a slug prefix; prints session names sorted by creation time, oldest first
|
|
1185
|
-
# Expected session format: [worktreename]_[worktreehash]_[datetime]_[readable_name]
|
|
1186
|
-
# Usage: tmux_list_sessions_for_slug <slug> [worktree_path]
|
|
1187
|
-
tmux_list_sessions_for_slug() {
|
|
1188
|
-
local slug="$1"
|
|
1189
|
-
local worktree_path="${2:-}"
|
|
1190
|
-
tmux_available || return 0
|
|
1191
|
-
|
|
1192
|
-
local d ORCH_PREFIX
|
|
1193
|
-
d="$(_tmux_delim)"
|
|
1194
|
-
ORCH_PREFIX="$(_tmux_orch_prefix)"
|
|
1195
|
-
|
|
1196
|
-
# If worktree_path provided, match hash-based and no-hash prefixes
|
|
1197
|
-
if [[ -n "$worktree_path" ]]; then
|
|
1198
|
-
local hash_slug hash_path branch_name
|
|
1199
|
-
hash_slug="$(tmux_path_hash "$slug")"
|
|
1200
|
-
hash_path="$(tmux_path_hash "$worktree_path")"
|
|
1201
|
-
branch_name="$(git_worktree_path_to_branch "$worktree_path" 2>/dev/null || true)"
|
|
1202
|
-
|
|
1203
|
-
local p1_new="${ORCH_PREFIX}${slug}${d}${hash_slug}${d}"
|
|
1204
|
-
local p2_new="${slug}${d}${hash_slug}${d}"
|
|
1205
|
-
local p1_old="${ORCH_PREFIX}${slug}${d}${hash_path}${d}"
|
|
1206
|
-
local p2_old="${slug}${d}${hash_path}${d}"
|
|
1207
|
-
local p1_branch_hash=""
|
|
1208
|
-
local p2_branch_hash=""
|
|
1209
|
-
if [[ -n "$branch_name" ]]; then
|
|
1210
|
-
p1_branch_hash="${ORCH_PREFIX}${branch_name}${d}${hash_path}${d}"
|
|
1211
|
-
p2_branch_hash="${branch_name}${d}${hash_path}${d}"
|
|
1212
|
-
fi
|
|
1213
|
-
|
|
1214
|
-
# List sessions with any known prefix variant
|
|
1215
|
-
tmux list-sessions -F '#{session_name}|||#{session_created}' 2>/dev/null \
|
|
1216
|
-
| sed 's/|||/\t/g' \
|
|
1217
|
-
| awk -v a="$p1_new" -v b="$p2_new" -v c="$p1_old" -v d="$p2_old" -v e="$p1_branch_hash" -v f="$p2_branch_hash" 'BEGIN{FS="\t"} (length(a)>0 && index($1, a)==1) || (length(b)>0 && index($1, b)==1) || (length(c)>0 && index($1, c)==1) || (length(d)>0 && index($1, d)==1) || (length(e)>0 && index($1, e)==1) || (length(f)>0 && index($1, f)==1) {print $1"\t"$2}' \
|
|
1218
|
-
| sort -t $'\t' -k2,2n -k1,1f \
|
|
1219
|
-
| awk -F '\t' '{print $1}' | awk '!seen[$0]++' || true
|
|
1220
|
-
else
|
|
1221
|
-
# New-format prefix matching without hash: orchestra__slug__... or slug__...
|
|
1222
|
-
local prefix1="${ORCH_PREFIX}${slug}${d}"
|
|
1223
|
-
local prefix2="${slug}${d}"
|
|
1224
|
-
tmux list-sessions -F '#{session_name}|||#{session_created}' 2>/dev/null \
|
|
1225
|
-
| sed 's/|||/\t/g' \
|
|
1226
|
-
| awk -v p1="$prefix1" -v p2="$prefix2" 'BEGIN{FS="\t"} index($1, p1)==1 || index($1, p2)==1 {print $1"\t"$2}' \
|
|
1227
|
-
| sort -t $'\t' -k2,2n -k1,1f \
|
|
1228
|
-
| awk -F '\t' '{print $1}' || true
|
|
1229
|
-
fi
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
# List all tmux sessions
|
|
1233
|
-
# Usage: tmux_list_all_sessions
|
|
1234
|
-
tmux_list_all_sessions() {
|
|
1235
|
-
tmux_available || return 0
|
|
1236
|
-
tmux list-sessions -F '#{session_name}' 2>/dev/null || true
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
# Find sessions matching a pattern
|
|
1240
|
-
# Usage: tmux_find_session <pattern>
|
|
1241
|
-
tmux_find_session() {
|
|
1242
|
-
local pattern="$1"
|
|
1243
|
-
|
|
1244
|
-
if ! tmux_available; then
|
|
1245
|
-
return 1
|
|
1246
|
-
fi
|
|
1247
|
-
|
|
1248
|
-
# Try exact match first
|
|
1249
|
-
if tmux_session_exists "$pattern"; then
|
|
1250
|
-
echo "$pattern"
|
|
1251
|
-
return 0
|
|
1252
|
-
fi
|
|
1253
|
-
|
|
1254
|
-
# Try pattern match
|
|
1255
|
-
tmux_list_all_sessions | grep -E "$pattern" | head -1 || true
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
# --------------------------- Session Information ----------------------------
|
|
1259
|
-
|
|
1260
|
-
# Helper function to format session display names
|
|
1261
|
-
# Input: session_content (e.g., "opencode_fixing_auth_bug" or "my_feature_work")
|
|
1262
|
-
# Output: "Opencode Fixing Auth Bug" or "My Feature Work"
|
|
1263
|
-
format_session_display_name() {
|
|
1264
|
-
local session_content="$1"
|
|
1265
|
-
local d
|
|
1266
|
-
d="$(_tmux_delim)"
|
|
1267
|
-
local ORCH_PREFIX
|
|
1268
|
-
ORCH_PREFIX="$(_tmux_orch_prefix)"
|
|
1269
|
-
if [[ "$session_content" == ${ORCH_PREFIX}* ]]; then
|
|
1270
|
-
session_content="${session_content#${ORCH_PREFIX}}"
|
|
1271
|
-
fi
|
|
1272
|
-
if [[ "$session_content" == auto_* ]]; then
|
|
1273
|
-
session_content="${session_content#auto_}"
|
|
1274
|
-
fi
|
|
1275
|
-
local description
|
|
1276
|
-
description="$(echo "$session_content" | tr '_-' ' ')"
|
|
1277
|
-
description="$(echo "$description" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1')"
|
|
1278
|
-
echo "$description"
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
_tmux_format_session_timestamp() {
|
|
1282
|
-
local date_part="$1"
|
|
1283
|
-
local time_part="$2"
|
|
1284
|
-
|
|
1285
|
-
if [[ ! "$date_part" =~ ^[0-9]{8}$ || ! "$time_part" =~ ^[0-9]{6}$ ]]; then
|
|
1286
|
-
printf '%s %s\n' "$date_part" "$time_part"
|
|
1287
|
-
return
|
|
1288
|
-
fi
|
|
1289
|
-
|
|
1290
|
-
local month day hour minute ampm display_hour
|
|
1291
|
-
month=$((10#${date_part:4:2}))
|
|
1292
|
-
day=$((10#${date_part:6:2}))
|
|
1293
|
-
hour=$((10#${time_part:0:2}))
|
|
1294
|
-
minute="${time_part:2:2}"
|
|
1295
|
-
|
|
1296
|
-
ampm="am"
|
|
1297
|
-
display_hour="$hour"
|
|
1298
|
-
if (( hour >= 12 )); then
|
|
1299
|
-
ampm="pm"
|
|
1300
|
-
if (( hour > 12 )); then
|
|
1301
|
-
display_hour=$((hour - 12))
|
|
1302
|
-
fi
|
|
1303
|
-
fi
|
|
1304
|
-
if (( hour == 0 )); then
|
|
1305
|
-
display_hour=12
|
|
1306
|
-
fi
|
|
1307
|
-
|
|
1308
|
-
local month_names=("" "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
|
|
1309
|
-
local month_name="???"
|
|
1310
|
-
if (( month > 0 && month <= 12 )); then
|
|
1311
|
-
month_name="${month_names[$month]}"
|
|
1312
|
-
fi
|
|
1313
|
-
|
|
1314
|
-
printf '%s %s %s:%s%s\n' "$month_name" "$day" "$display_hour" "$minute" "$ampm"
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
# Parse session name and format for display
|
|
1318
|
-
# Input: session_name (format: worktreename_repohash_YYYYMMDD_HHMMSS_readable_name or old formats)
|
|
1319
|
-
# Output: formatted display name
|
|
1320
|
-
# Usage: tmux_format_session_display <session_name>
|
|
1321
|
-
tmux_format_session_display() {
|
|
1322
|
-
local session_name="$1"
|
|
1323
|
-
local timestamp_mode="${2:-with-timestamp}"
|
|
1324
|
-
|
|
1325
|
-
# Handle temporary renaming sessions first
|
|
1326
|
-
if [[ "$session_name" =~ _renaming$ ]]; then
|
|
1327
|
-
# Extract base name before _renaming suffix
|
|
1328
|
-
local base_name="${session_name%_renaming}"
|
|
1329
|
-
# Recursively process the base name to get proper display
|
|
1330
|
-
tmux_format_session_display "$base_name" "$timestamp_mode"
|
|
1331
|
-
return $?
|
|
1332
|
-
fi
|
|
1333
|
-
|
|
1334
|
-
local d ORCH_PREFIX
|
|
1335
|
-
d="$(_tmux_delim)"
|
|
1336
|
-
ORCH_PREFIX="$(_tmux_orch_prefix)"
|
|
1337
|
-
|
|
1338
|
-
if [[ "$session_name" == ${ORCH_PREFIX}* ]]; then
|
|
1339
|
-
session_name="${session_name#${ORCH_PREFIX}}"
|
|
1340
|
-
elif [[ "$session_name" =~ ^orchestra_(.+)$ ]]; then
|
|
1341
|
-
session_name="${BASH_REMATCH[1]}"
|
|
1342
|
-
fi
|
|
1343
|
-
|
|
1344
|
-
local parts
|
|
1345
|
-
_tmux_split_by_delim "$session_name" "$d" parts
|
|
1346
|
-
local idx_time=-1 idx_date=-1 i
|
|
1347
|
-
for (( i=${#parts[@]}-1; i>=0; i-- )); do
|
|
1348
|
-
if [[ ${parts[$i]} =~ ^[0-9]{6}$ ]] && (( i > 0 )) && [[ ${parts[$((i-1))]} =~ ^[0-9]{8}$ ]]; then
|
|
1349
|
-
idx_time=$i
|
|
1350
|
-
idx_date=$((i-1))
|
|
1351
|
-
break
|
|
1352
|
-
fi
|
|
1353
|
-
done
|
|
1354
|
-
if (( idx_time >= 0 && idx_date >= 0 )); then
|
|
1355
|
-
local readable_name=""
|
|
1356
|
-
local j
|
|
1357
|
-
for (( j=idx_time+1; j<${#parts[@]}; j++ )); do
|
|
1358
|
-
if [[ -n "$readable_name" ]]; then
|
|
1359
|
-
readable_name+="$d"
|
|
1360
|
-
fi
|
|
1361
|
-
readable_name+="${parts[$j]}"
|
|
1362
|
-
done
|
|
1363
|
-
if [[ -n "$readable_name" ]]; then
|
|
1364
|
-
local formatted_name timestamp
|
|
1365
|
-
formatted_name="$(format_session_display_name "$readable_name")"
|
|
1366
|
-
timestamp="$(_tmux_format_session_timestamp "${parts[$idx_date]}" "${parts[$idx_time]}")"
|
|
1367
|
-
if [[ "$timestamp_mode" == "without-timestamp" ]]; then
|
|
1368
|
-
echo "$formatted_name"
|
|
1369
|
-
else
|
|
1370
|
-
echo "${formatted_name} (${timestamp})"
|
|
1371
|
-
fi
|
|
1372
|
-
return 0
|
|
1373
|
-
fi
|
|
1374
|
-
fi
|
|
1375
|
-
|
|
1376
|
-
# New format with repo hash: worktreename_repohash_YYYYMMDD_HHMMSS_readable_name
|
|
1377
|
-
if [[ "$session_name" =~ ^[^_]+_[a-f0-9]{8}_([0-9]{8})_([0-9]{6})_(.+)$ ]]; then
|
|
1378
|
-
local date_part="${BASH_REMATCH[1]}" # YYYYMMDD
|
|
1379
|
-
local time_part="${BASH_REMATCH[2]}" # HHMMSS
|
|
1380
|
-
local readable_name="${BASH_REMATCH[3]}" # readable_name
|
|
1381
|
-
|
|
1382
|
-
# Parse date: YYYYMMDD -> Jul 21
|
|
1383
|
-
local year="${date_part:0:4}"
|
|
1384
|
-
local month="${date_part:4:2}"
|
|
1385
|
-
local day="${date_part:6:2}"
|
|
1386
|
-
|
|
1387
|
-
# Parse time: HHMMSS -> 12:30am
|
|
1388
|
-
local hour="${time_part:0:2}"
|
|
1389
|
-
local minute="${time_part:2:2}"
|
|
1390
|
-
|
|
1391
|
-
# Convert to 12-hour format
|
|
1392
|
-
local ampm="am"
|
|
1393
|
-
local display_hour="$hour"
|
|
1394
|
-
if [[ "$hour" -ge 12 ]]; then
|
|
1395
|
-
ampm="pm"
|
|
1396
|
-
if [[ "$hour" -gt 12 ]]; then
|
|
1397
|
-
display_hour=$((hour - 12))
|
|
1398
|
-
fi
|
|
1399
|
-
fi
|
|
1400
|
-
if [[ "$hour" == "00" ]]; then
|
|
1401
|
-
display_hour="12"
|
|
1402
|
-
fi
|
|
1403
|
-
|
|
1404
|
-
# Remove leading zero from hour
|
|
1405
|
-
display_hour="${display_hour#0}"
|
|
1406
|
-
|
|
1407
|
-
# Convert month number to name
|
|
1408
|
-
local month_names=("" "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
|
|
1409
|
-
local month_name="${month_names[${month#0}]}"
|
|
1410
|
-
|
|
1411
|
-
# Remove leading zero from day
|
|
1412
|
-
local display_day="${day#0}"
|
|
1413
|
-
|
|
1414
|
-
# Format the readable name with proper formatting
|
|
1415
|
-
local formatted_name="$(format_session_display_name "$readable_name")"
|
|
1416
|
-
|
|
1417
|
-
if [[ "$timestamp_mode" == "without-timestamp" ]]; then
|
|
1418
|
-
echo "$formatted_name"
|
|
1419
|
-
else
|
|
1420
|
-
echo "${formatted_name} (${month_name} ${display_day} ${display_hour}:${minute}${ampm})"
|
|
1421
|
-
fi
|
|
1422
|
-
# Fallback: Try format without repo hash: appname_description_YYYYMMDD_HHMMSS
|
|
1423
|
-
elif [[ "$session_name" =~ ^(.*)_[0-9]{8}_[0-9]{6}$ ]]; then
|
|
1424
|
-
local session_content="${BASH_REMATCH[1]}" # appname_description
|
|
1425
|
-
|
|
1426
|
-
# Convert to proper display format
|
|
1427
|
-
local formatted_name="$(format_session_display_name "$session_content")"
|
|
1428
|
-
echo "$formatted_name"
|
|
1429
|
-
else
|
|
1430
|
-
# Check for old format without repo hash: worktreename_YYYYMMDD_HHMMSS_readable_name
|
|
1431
|
-
if [[ "$session_name" =~ ^[^_]+_([0-9]{8})_([0-9]{6})_(.+)$ ]]; then
|
|
1432
|
-
local date_part="${BASH_REMATCH[1]}" # YYYYMMDD
|
|
1433
|
-
local time_part="${BASH_REMATCH[2]}" # HHMMSS
|
|
1434
|
-
local readable_name="${BASH_REMATCH[3]}" # readable_name
|
|
1435
|
-
|
|
1436
|
-
# Parse date: YYYYMMDD -> Jul 21
|
|
1437
|
-
local year="${date_part:0:4}"
|
|
1438
|
-
local month="${date_part:4:2}"
|
|
1439
|
-
local day="${date_part:6:2}"
|
|
1440
|
-
|
|
1441
|
-
# Parse time: HHMMSS -> 12:30am
|
|
1442
|
-
local hour="${time_part:0:2}"
|
|
1443
|
-
local minute="${time_part:2:2}"
|
|
1444
|
-
|
|
1445
|
-
# Convert to 12-hour format
|
|
1446
|
-
local ampm="am"
|
|
1447
|
-
local display_hour="$hour"
|
|
1448
|
-
if [[ "$hour" -ge 12 ]]; then
|
|
1449
|
-
ampm="pm"
|
|
1450
|
-
if [[ "$hour" -gt 12 ]]; then
|
|
1451
|
-
display_hour=$((hour - 12))
|
|
1452
|
-
fi
|
|
1453
|
-
fi
|
|
1454
|
-
if [[ "$hour" == "00" ]]; then
|
|
1455
|
-
display_hour="12"
|
|
1456
|
-
fi
|
|
1457
|
-
|
|
1458
|
-
# Remove leading zero from hour
|
|
1459
|
-
display_hour="${display_hour#0}"
|
|
1460
|
-
|
|
1461
|
-
# Convert month number to name
|
|
1462
|
-
local month_names=("" "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
|
|
1463
|
-
local month_name="${month_names[${month#0}]}"
|
|
1464
|
-
|
|
1465
|
-
# Remove leading zero from day
|
|
1466
|
-
local display_day="${day#0}"
|
|
1467
|
-
|
|
1468
|
-
# Format the readable name with proper formatting
|
|
1469
|
-
local formatted_name="$(format_session_display_name "$readable_name")"
|
|
1470
|
-
|
|
1471
|
-
if [[ "$timestamp_mode" == "without-timestamp" ]]; then
|
|
1472
|
-
echo "$formatted_name"
|
|
1473
|
-
else
|
|
1474
|
-
echo "${formatted_name} (${month_name} ${display_day} ${display_hour}:${minute}${ampm})"
|
|
1475
|
-
fi
|
|
1476
|
-
else
|
|
1477
|
-
# Fallback for sessions that don't match either format
|
|
1478
|
-
echo "$session_name"
|
|
1479
|
-
fi
|
|
1480
|
-
fi
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
# Get active pane id for a session (best effort)
|
|
1484
|
-
# Usage: tmux_get_active_pane <session_name>
|
|
1485
|
-
tmux_get_active_pane() {
|
|
1486
|
-
local s="$1"
|
|
1487
|
-
tmux_available || return 1
|
|
1488
|
-
|
|
1489
|
-
# Find active window id
|
|
1490
|
-
local win
|
|
1491
|
-
win="$(tmux list-windows -t "$s" -F '#{window_active} #{window_id}' 2>/dev/null | awk '$1==1{print $2; exit}')" || true
|
|
1492
|
-
[[ -z "$win" ]] && return 1
|
|
1493
|
-
|
|
1494
|
-
# Find active pane id within that window
|
|
1495
|
-
tmux list-panes -t "$win" -F '#{pane_active} #{pane_id}' 2>/dev/null | awk '$1==1{print $2; exit}' || true
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
# Capture enhanced session preview showing current terminal view (bottom lines)
|
|
1499
|
-
# Usage: tmux_session_preview <session_name>
|
|
1500
|
-
tmux_session_preview() {
|
|
1501
|
-
local s="$1"
|
|
1502
|
-
tmux_available || { echo "(tmux not installed)"; return 0; }
|
|
1503
|
-
|
|
1504
|
-
local pane
|
|
1505
|
-
pane="$(tmux_get_active_pane "$s" || true)"
|
|
1506
|
-
if [[ -z "$pane" ]]; then
|
|
1507
|
-
echo "(no active pane found)"
|
|
1508
|
-
return 0
|
|
1509
|
-
fi
|
|
1510
|
-
|
|
1511
|
-
# Always capture from the BOTTOM of the terminal buffer (last visible lines)
|
|
1512
|
-
# -e flag preserves escape sequences for colors and formatting
|
|
1513
|
-
# -p prints to stdout
|
|
1514
|
-
# NO -S/-E flags means capture the current viewport (what's visible NOW)
|
|
1515
|
-
# This ensures we always see the bottom/most recent output
|
|
1516
|
-
local txt
|
|
1517
|
-
txt="$(tmux capture-pane -e -p -t "$pane" 2>/dev/null)"
|
|
1518
|
-
|
|
1519
|
-
if [[ -z "$txt" ]]; then
|
|
1520
|
-
echo "(no output yet)"
|
|
1521
|
-
return 0
|
|
1522
|
-
fi
|
|
1523
|
-
|
|
1524
|
-
# Check if the pane is idle (cursor at prompt, no running process)
|
|
1525
|
-
local pane_cmd pane_mode
|
|
1526
|
-
pane_cmd="$(tmux display-message -t "$pane" -p '#{pane_current_command}' 2>/dev/null || echo "")"
|
|
1527
|
-
pane_mode="$(tmux display-message -t "$pane" -p '#{pane_mode}' 2>/dev/null || echo "")"
|
|
1528
|
-
|
|
1529
|
-
# Detect idle state: shell running and not in copy mode
|
|
1530
|
-
local is_idle="false"
|
|
1531
|
-
case "$pane_cmd" in
|
|
1532
|
-
bash|zsh|sh|fish|dash|ksh)
|
|
1533
|
-
# Shell is running, check if we're at a prompt (not in copy mode)
|
|
1534
|
-
if [[ "$pane_mode" == "" ]]; then
|
|
1535
|
-
is_idle="true"
|
|
1536
|
-
fi
|
|
1537
|
-
;;
|
|
1538
|
-
esac
|
|
1539
|
-
|
|
1540
|
-
# Optimized: Get terminal info in a single command instead of multiple calls
|
|
1541
|
-
local term_info has_rgb
|
|
1542
|
-
term_info="$(tmux show-environment -t "$s" TERM 2>/dev/null | cut -d= -f2 || echo "unknown")"
|
|
1543
|
-
|
|
1544
|
-
# Fast check for RGB support without full grep
|
|
1545
|
-
if tmux show-options -t "$s" -s terminal-overrides 2>/dev/null | grep -q "RGB"; then
|
|
1546
|
-
has_rgb="true"
|
|
1547
|
-
else
|
|
1548
|
-
has_rgb="false"
|
|
1549
|
-
fi
|
|
1550
|
-
|
|
1551
|
-
# For ANSI-preserved preview with color mode info
|
|
1552
|
-
# Add markers for the Rust parser to detect color capabilities and idle state
|
|
1553
|
-
if [[ "$has_rgb" == "true" || "$term_info" == *"direct"* || "$term_info" == *"truecolor"* ]]; then
|
|
1554
|
-
echo "<<<COLORMODE:RGB>>>"
|
|
1555
|
-
elif [[ "$term_info" == *"256color"* ]]; then
|
|
1556
|
-
echo "<<<COLORMODE:256>>>"
|
|
1557
|
-
else
|
|
1558
|
-
echo "<<<COLORMODE:16>>>"
|
|
1559
|
-
fi
|
|
1560
|
-
|
|
1561
|
-
# Add idle marker so Rust can stop polling when terminal is idle
|
|
1562
|
-
if [[ "$is_idle" == "true" ]]; then
|
|
1563
|
-
echo "<<<IDLE:true>>>"
|
|
1564
|
-
else
|
|
1565
|
-
echo "<<<IDLE:false>>>"
|
|
1566
|
-
fi
|
|
1567
|
-
|
|
1568
|
-
# If no content after processing, show placeholder
|
|
1569
|
-
if [[ -z "$txt" ]]; then
|
|
1570
|
-
echo "(session active, no visible output)"
|
|
1571
|
-
else
|
|
1572
|
-
echo "$txt"
|
|
1573
|
-
fi
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
# --------------------------- Advanced Operations ----------------------------
|
|
1577
|
-
|
|
1578
|
-
# Send keys to a session and press Enter
|
|
1579
|
-
# Usage: tmux_send_keys <session_name> <command...>
|
|
1580
|
-
tmux_send_keys() {
|
|
1581
|
-
local session_name="$1"; shift || true
|
|
1582
|
-
local command_line="$*"
|
|
1583
|
-
if ! tmux_available; then
|
|
1584
|
-
err "tmux not installed"
|
|
1585
|
-
return 1
|
|
1586
|
-
fi
|
|
1587
|
-
if [[ -z "$session_name" || -z "$command_line" ]]; then
|
|
1588
|
-
err "tmux_send_keys: session and command required"
|
|
1589
|
-
return 1
|
|
1590
|
-
fi
|
|
1591
|
-
tmux send-keys -t "$session_name" -l -- "$command_line" 2>/dev/null || return 1
|
|
1592
|
-
tmux send-keys -t "$session_name" C-m 2>/dev/null || return 1
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
# Show Orchestra tmux shortcuts in a popup modal.
|
|
1596
|
-
# Usage: tmux_show_orchestra_help_popup [client_tty]
|
|
1597
|
-
tmux_show_orchestra_help_popup() {
|
|
1598
|
-
local target_client="${1:-}"
|
|
1599
|
-
if ! tmux_available; then
|
|
1600
|
-
err "tmux not installed"
|
|
1601
|
-
return 1
|
|
1602
|
-
fi
|
|
1603
|
-
|
|
1604
|
-
local target_args=()
|
|
1605
|
-
if [[ -n "$target_client" ]]; then
|
|
1606
|
-
target_args=(-c "$target_client")
|
|
1607
|
-
fi
|
|
1608
|
-
|
|
1609
|
-
local popup_command
|
|
1610
|
-
popup_command="$(cat <<'EOF'
|
|
1611
|
-
bash -lc '
|
|
1612
|
-
trap "exit 0" INT TERM
|
|
1613
|
-
printf "%s\n" \
|
|
1614
|
-
"Orchestra tmux shortcuts" \
|
|
1615
|
-
"" \
|
|
1616
|
-
"Ctrl+b, d Detach and return to Orchestra" \
|
|
1617
|
-
"Ctrl+b, r Rename the current Orchestra session" \
|
|
1618
|
-
"Ctrl+b, n New Orchestra session in this worktree" \
|
|
1619
|
-
"Ctrl+b, q Close current session and switch next" \
|
|
1620
|
-
"Ctrl+b, l List active sessions" \
|
|
1621
|
-
"" \
|
|
1622
|
-
"Ctrl+b, Left Previous Orchestra session in this workspace" \
|
|
1623
|
-
"Ctrl+b, Right Next Orchestra session in this workspace" \
|
|
1624
|
-
"Ctrl+b, < Previous Orchestra session in this workspace" \
|
|
1625
|
-
"Ctrl+b, > Next Orchestra session in this workspace" \
|
|
1626
|
-
"" \
|
|
1627
|
-
"Ctrl+b, [ Copy/scroll mode" \
|
|
1628
|
-
"Ctrl+b, h Show this help" \
|
|
1629
|
-
"Ctrl+b, ? Show this help" \
|
|
1630
|
-
"" \
|
|
1631
|
-
"Press any key to close..."
|
|
1632
|
-
IFS= read -rsn1 _ || true
|
|
1633
|
-
exit 0
|
|
1634
|
-
'
|
|
1635
|
-
EOF
|
|
1636
|
-
)"
|
|
1637
|
-
|
|
1638
|
-
tmux display-popup "${target_args[@]}" \
|
|
1639
|
-
-E \
|
|
1640
|
-
-w 84 \
|
|
1641
|
-
-h 22 \
|
|
1642
|
-
-s "fg=#c0caf5,bg=#1a1b26" \
|
|
1643
|
-
-S "fg=#7aa2f7,bg=#1a1b26" \
|
|
1644
|
-
-T "Orchestra shortcuts" \
|
|
1645
|
-
"$popup_command" >/dev/null 2>&1 || true
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
# Show active Orchestra sessions in a native tmux menu for arrow-key selection.
|
|
1649
|
-
# Usage: tmux_show_workspace_session_menu <current_session> [client_tty]
|
|
1650
|
-
tmux_show_workspace_session_menu() {
|
|
1651
|
-
local current_session="$1"
|
|
1652
|
-
local target_client="${2:-}"
|
|
1653
|
-
local session_dir repo_root rows name display_name label target_command
|
|
1654
|
-
local menu_width=56
|
|
1655
|
-
local min_menu_rows=10
|
|
1656
|
-
local menu_row_count=0
|
|
1657
|
-
|
|
1658
|
-
if ! tmux_available; then
|
|
1659
|
-
err "tmux not installed"
|
|
1660
|
-
return 1
|
|
1661
|
-
fi
|
|
1662
|
-
if [[ -z "$current_session" ]]; then
|
|
1663
|
-
err "tmux_show_workspace_session_menu: current session required"
|
|
1664
|
-
return 1
|
|
1665
|
-
fi
|
|
1666
|
-
|
|
1667
|
-
session_dir="$(tmux display-message -t "$current_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
1668
|
-
repo_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
1669
|
-
if [[ -z "$repo_root" ]]; then
|
|
1670
|
-
tmux display-message -d 2500 "Unable to determine Orchestra workspace" 2>/dev/null || true
|
|
1671
|
-
return 1
|
|
1672
|
-
fi
|
|
1673
|
-
|
|
1674
|
-
_tmux_registry_sync_active_workspace_sessions "$repo_root" >/dev/null 2>&1 || true
|
|
1675
|
-
rows="$(_tmux_workspace_session_rows "$current_session" "$repo_root" all 2>/dev/null || true)"
|
|
1676
|
-
if [[ -z "$rows" ]]; then
|
|
1677
|
-
tmux display-message -d 2500 "No active Orchestra sessions in this workspace" 2>/dev/null || true
|
|
1678
|
-
return 1
|
|
1679
|
-
fi
|
|
1680
|
-
|
|
1681
|
-
local target_args=()
|
|
1682
|
-
if [[ -n "$target_client" ]]; then
|
|
1683
|
-
target_args=(-c "$target_client")
|
|
1684
|
-
fi
|
|
1685
|
-
|
|
1686
|
-
local menu_items=()
|
|
1687
|
-
while IFS=$'\t' read -r name display_name; do
|
|
1688
|
-
[[ -n "$name" && "$name" != "__ellipsis__" ]] || continue
|
|
1689
|
-
if [[ -z "$display_name" ]]; then
|
|
1690
|
-
display_name="$(tmux_format_session_display "$name" without-timestamp)"
|
|
1691
|
-
fi
|
|
1692
|
-
label="$(_tmux_truncate_tab_label "$display_name" 42)"
|
|
1693
|
-
if [[ "$name" == "$current_session" ]]; then
|
|
1694
|
-
label="● $label"
|
|
1695
|
-
else
|
|
1696
|
-
label=" $label"
|
|
1697
|
-
fi
|
|
1698
|
-
label="$(_tmux_pad_menu_label "$label" "$menu_width")"
|
|
1699
|
-
label="$(_tmux_status_escape_text "$label")"
|
|
1700
|
-
target_command="switch-client -t \"$name\""
|
|
1701
|
-
menu_items+=("$label" "" "$target_command")
|
|
1702
|
-
menu_row_count=$((menu_row_count + 1))
|
|
1703
|
-
done <<< "$rows"
|
|
1704
|
-
|
|
1705
|
-
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
|
1706
|
-
tmux display-message -d 2500 "No active Orchestra sessions in this workspace" 2>/dev/null || true
|
|
1707
|
-
return 1
|
|
1708
|
-
fi
|
|
1709
|
-
|
|
1710
|
-
while (( menu_row_count < min_menu_rows )); do
|
|
1711
|
-
menu_items+=("$(_tmux_pad_menu_label "" "$menu_width")" "" "")
|
|
1712
|
-
menu_row_count=$((menu_row_count + 1))
|
|
1713
|
-
done
|
|
1714
|
-
|
|
1715
|
-
tmux display-menu "${target_args[@]}" \
|
|
1716
|
-
-s "fg=#c0caf5,bg=#1a1b26" \
|
|
1717
|
-
-S "fg=#7aa2f7,bg=#1a1b26" \
|
|
1718
|
-
-H "fg=#1a1b26,bg=#ff9e64,bold" \
|
|
1719
|
-
-T "Orchestra sessions" \
|
|
1720
|
-
"${menu_items[@]}" >/dev/null 2>&1 || true
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
# Find the adjacent active Orchestra session registered for the current repo.
|
|
1724
|
-
# Usage: tmux_workspace_cycle_target <current_session> <next|previous>
|
|
1725
|
-
tmux_workspace_cycle_target() {
|
|
1726
|
-
local current_session="$1"
|
|
1727
|
-
local direction="$2"
|
|
1728
|
-
local session_dir repo_root db_path active_file target query_status
|
|
1729
|
-
|
|
1730
|
-
if ! tmux_available; then
|
|
1731
|
-
err "tmux not installed"
|
|
1732
|
-
return 1
|
|
1733
|
-
fi
|
|
1734
|
-
if [[ -z "$current_session" ]]; then
|
|
1735
|
-
err "tmux_workspace_cycle_target: current session required"
|
|
1736
|
-
return 1
|
|
1737
|
-
fi
|
|
1738
|
-
case "$direction" in
|
|
1739
|
-
next|previous|prev) ;;
|
|
1740
|
-
*)
|
|
1741
|
-
err "tmux_workspace_cycle_target: direction must be next or previous"
|
|
1742
|
-
return 1
|
|
1743
|
-
;;
|
|
1744
|
-
esac
|
|
1745
|
-
|
|
1746
|
-
db_path="$(_tmux_session_registry_path)"
|
|
1747
|
-
have_cmd python3 || {
|
|
1748
|
-
err "python3 is required to read the Orchestra session registry"
|
|
1749
|
-
return 1
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
active_file="$(mktemp)"
|
|
1753
|
-
tmux list-sessions -F $'#{session_name}\t#{@orchestra_display_name}\t#{session_last_attached}\t#{session_activity}' > "$active_file" 2>/dev/null || {
|
|
1754
|
-
rm -f "$active_file"
|
|
1755
|
-
err "Unable to list tmux sessions"
|
|
1756
|
-
return 1
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
if [[ -f "$db_path" ]]; then
|
|
1760
|
-
if target="$(_tmux_workspace_cycle_target_cached "$current_session" "$direction" "$db_path" "$active_file")"; then
|
|
1761
|
-
query_status=0
|
|
1762
|
-
else
|
|
1763
|
-
query_status=$?
|
|
1764
|
-
fi
|
|
1765
|
-
if [[ $query_status -eq 0 && -n "$target" ]]; then
|
|
1766
|
-
rm -f "$active_file"
|
|
1767
|
-
printf '%s\n' "$target"
|
|
1768
|
-
return 0
|
|
1769
|
-
fi
|
|
1770
|
-
if [[ $query_status -eq 3 ]]; then
|
|
1771
|
-
rm -f "$active_file"
|
|
1772
|
-
return 1
|
|
1773
|
-
fi
|
|
1774
|
-
fi
|
|
1775
|
-
|
|
1776
|
-
_tmux_registry_upsert_current_session "$current_session" >/dev/null 2>&1 || true
|
|
1777
|
-
|
|
1778
|
-
session_dir="$(tmux display-message -t "$current_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
1779
|
-
repo_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
1780
|
-
if [[ -z "$repo_root" ]]; then
|
|
1781
|
-
rm -f "$active_file"
|
|
1782
|
-
err "Unable to determine Orchestra workspace for session"
|
|
1783
|
-
return 1
|
|
1784
|
-
fi
|
|
1785
|
-
_tmux_registry_sync_active_workspace_sessions "$repo_root" >/dev/null 2>&1 || true
|
|
1786
|
-
|
|
1787
|
-
if [[ ! -f "$db_path" ]]; then
|
|
1788
|
-
rm -f "$active_file"
|
|
1789
|
-
err "No Orchestra session registry found"
|
|
1790
|
-
return 1
|
|
1791
|
-
fi
|
|
1792
|
-
|
|
1793
|
-
if target="$(python3 - "$db_path" "$repo_root" "$current_session" "$direction" "$active_file" <<'PY'
|
|
1794
|
-
import sqlite3
|
|
1795
|
-
import sys
|
|
1796
|
-
|
|
1797
|
-
db_path, repo_root, current_session, direction, active_path = sys.argv[1:6]
|
|
1798
|
-
with open(active_path, "r", encoding="utf-8") as handle:
|
|
1799
|
-
active = {
|
|
1800
|
-
line.rstrip("\n").split("\t", 1)[0].strip()
|
|
1801
|
-
for line in handle
|
|
1802
|
-
if line.rstrip("\n").split("\t", 1)[0].strip()
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
conn = sqlite3.connect(db_path)
|
|
1806
|
-
rows = conn.execute(
|
|
1807
|
-
"""
|
|
1808
|
-
SELECT tmux_name
|
|
1809
|
-
FROM sessions
|
|
1810
|
-
WHERE repo_root = ?1
|
|
1811
|
-
ORDER BY worktree_path COLLATE NOCASE, created_at, tmux_name COLLATE NOCASE
|
|
1812
|
-
""",
|
|
1813
|
-
(repo_root,),
|
|
1814
|
-
).fetchall()
|
|
1815
|
-
|
|
1816
|
-
names = []
|
|
1817
|
-
seen = set()
|
|
1818
|
-
for (name,) in rows:
|
|
1819
|
-
if name in active and name not in seen:
|
|
1820
|
-
names.append(name)
|
|
1821
|
-
seen.add(name)
|
|
1822
|
-
|
|
1823
|
-
if len(names) < 2 or current_session not in seen:
|
|
1824
|
-
raise SystemExit(0)
|
|
1825
|
-
|
|
1826
|
-
index = names.index(current_session)
|
|
1827
|
-
if direction == "next":
|
|
1828
|
-
index = (index + 1) % len(names)
|
|
1829
|
-
else:
|
|
1830
|
-
index = (index - 1) % len(names)
|
|
1831
|
-
print(names[index])
|
|
1832
|
-
PY
|
|
1833
|
-
)"; then
|
|
1834
|
-
query_status=0
|
|
1835
|
-
else
|
|
1836
|
-
query_status=$?
|
|
1837
|
-
fi
|
|
1838
|
-
rm -f "$active_file"
|
|
1839
|
-
if [[ $query_status -ne 0 ]]; then
|
|
1840
|
-
err "Unable to query Orchestra session registry"
|
|
1841
|
-
return 1
|
|
1842
|
-
fi
|
|
1843
|
-
[[ -n "$target" ]] || return 1
|
|
1844
|
-
printf '%s\n' "$target"
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
# Switch the current tmux client to an adjacent registered Orchestra session.
|
|
1848
|
-
# Usage: tmux_cycle_workspace_session <current_session> <next|previous> [client_tty]
|
|
1849
|
-
tmux_cycle_workspace_session() {
|
|
1850
|
-
local current_session="$1"
|
|
1851
|
-
local direction="$2"
|
|
1852
|
-
local target_client="${3:-}"
|
|
1853
|
-
local target_session
|
|
1854
|
-
|
|
1855
|
-
if ! target_session="$(tmux_workspace_cycle_target "$current_session" "$direction")"; then
|
|
1856
|
-
tmux display-message -d 2500 "No other Orchestra sessions in this workspace" 2>/dev/null || true
|
|
1857
|
-
return 1
|
|
1858
|
-
fi
|
|
1859
|
-
|
|
1860
|
-
if [[ -n "$target_client" ]]; then
|
|
1861
|
-
tmux switch-client -c "$target_client" -t "$target_session" >/dev/null 2>&1 || return 1
|
|
1862
|
-
else
|
|
1863
|
-
tmux switch-client -t "$target_session" >/dev/null 2>&1 || return 1
|
|
1864
|
-
fi
|
|
1865
|
-
|
|
1866
|
-
{
|
|
1867
|
-
_tmux_refresh_orchestra_session_status "$target_session" >/dev/null 2>&1 || true
|
|
1868
|
-
} &
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
# Load .env file if it exists
|
|
1872
|
-
# Usage: tmux_load_env_file [env_file_path]
|
|
1873
|
-
tmux_load_env_file() {
|
|
1874
|
-
local env_file="${1:-$PWD/.env}"
|
|
1875
|
-
if [[ -f "$env_file" ]]; then
|
|
1876
|
-
# Source the .env file, but only export ANTHROPIC_API_KEY
|
|
1877
|
-
set -a # Auto-export variables
|
|
1878
|
-
source "$env_file"
|
|
1879
|
-
set +a # Turn off auto-export
|
|
1880
|
-
fi
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
# Load Anthropic API key from config file or fallback to .env files
|
|
1884
|
-
tmux_load_anthropic_api_key() {
|
|
1885
|
-
# First try to load from ~/.orchestra/config.json
|
|
1886
|
-
local config_file="$HOME/.orchestra/config.json"
|
|
1887
|
-
if [[ -f "$config_file" ]] && command -v jq >/dev/null 2>&1; then
|
|
1888
|
-
local api_key
|
|
1889
|
-
api_key="$(jq -r '.anthropic_api_key // empty' "$config_file" 2>/dev/null)"
|
|
1890
|
-
if [[ -n "$api_key" ]] && [[ "$api_key" != "null" ]]; then
|
|
1891
|
-
export ANTHROPIC_API_KEY="$api_key"
|
|
1892
|
-
return 0
|
|
1893
|
-
fi
|
|
1894
|
-
fi
|
|
1895
|
-
|
|
1896
|
-
# Fallback to .env file loading (existing logic)
|
|
1897
|
-
tmux_load_env_file "$PWD/.env"
|
|
1898
|
-
|
|
1899
|
-
# If still no API key, try repo root
|
|
1900
|
-
if [[ -z "${ANTHROPIC_API_KEY-}" ]]; then
|
|
1901
|
-
local root
|
|
1902
|
-
root="$(repo_root)"
|
|
1903
|
-
if [[ -n "$root" ]]; then
|
|
1904
|
-
tmux_load_env_file "$root/.env"
|
|
1905
|
-
fi
|
|
1906
|
-
fi
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
# Load OpenAI API key from config file or fallback to .env files
|
|
1910
|
-
tmux_load_openai_api_key() {
|
|
1911
|
-
local config_file="$HOME/.orchestra/config.json"
|
|
1912
|
-
if [[ -f "$config_file" ]]; then
|
|
1913
|
-
local api_key=""
|
|
1914
|
-
if command -v jq >/dev/null 2>&1; then
|
|
1915
|
-
api_key="$(jq -r '.openai_api_key // empty' "$config_file" 2>/dev/null)"
|
|
1916
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
1917
|
-
api_key="$(python3 - "$config_file" <<'PY'
|
|
1918
|
-
import json
|
|
1919
|
-
import sys
|
|
1920
|
-
try:
|
|
1921
|
-
data = json.load(open(sys.argv[1], 'r'))
|
|
1922
|
-
except Exception:
|
|
1923
|
-
data = {}
|
|
1924
|
-
value = data.get('openai_api_key') or ''
|
|
1925
|
-
print(value)
|
|
1926
|
-
PY
|
|
1927
|
-
)"
|
|
1928
|
-
elif command -v node >/dev/null 2>&1; then
|
|
1929
|
-
api_key="$(node - "$config_file" <<'NODE'
|
|
1930
|
-
const fs = require('fs');
|
|
1931
|
-
let value = '';
|
|
1932
|
-
try {
|
|
1933
|
-
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
1934
|
-
value = data.openai_api_key || '';
|
|
1935
|
-
} catch (err) {
|
|
1936
|
-
value = '';
|
|
1937
|
-
}
|
|
1938
|
-
process.stdout.write(value);
|
|
1939
|
-
NODE
|
|
1940
|
-
)"
|
|
1941
|
-
fi
|
|
1942
|
-
if [[ -n "$api_key" ]] && [[ "$api_key" != "null" ]]; then
|
|
1943
|
-
export OPENAI_API_KEY="$api_key"
|
|
1944
|
-
return 0
|
|
1945
|
-
fi
|
|
1946
|
-
fi
|
|
1947
|
-
|
|
1948
|
-
tmux_load_env_file "$PWD/.env"
|
|
1949
|
-
if [[ -z "${OPENAI_API_KEY-}" ]]; then
|
|
1950
|
-
local root
|
|
1951
|
-
root="$(repo_root)"
|
|
1952
|
-
if [[ -n "$root" ]]; then
|
|
1953
|
-
tmux_load_env_file "$root/.env"
|
|
1954
|
-
fi
|
|
1955
|
-
fi
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
# Load primary AI provider from config (defaults to anthropic)
|
|
1959
|
-
tmux_load_ai_primary_provider() {
|
|
1960
|
-
if [[ -n "${AI_PRIMARY_PROVIDER-}" ]]; then
|
|
1961
|
-
return 0
|
|
1962
|
-
fi
|
|
1963
|
-
local config_file="$HOME/.orchestra/config.json"
|
|
1964
|
-
local provider=""
|
|
1965
|
-
if [[ -f "$config_file" ]]; then
|
|
1966
|
-
if command -v jq >/dev/null 2>&1; then
|
|
1967
|
-
provider="$(jq -r '.ai_primary_provider // empty' "$config_file" 2>/dev/null)"
|
|
1968
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
1969
|
-
provider="$(python3 - "$config_file" <<'PY'
|
|
1970
|
-
import json
|
|
1971
|
-
import sys
|
|
1972
|
-
try:
|
|
1973
|
-
data = json.load(open(sys.argv[1], 'r'))
|
|
1974
|
-
except Exception:
|
|
1975
|
-
data = {}
|
|
1976
|
-
value = data.get('ai_primary_provider') or ''
|
|
1977
|
-
print(value)
|
|
1978
|
-
PY
|
|
1979
|
-
)"
|
|
1980
|
-
elif command -v node >/dev/null 2>&1; then
|
|
1981
|
-
provider="$(node - "$config_file" <<'NODE'
|
|
1982
|
-
const fs = require('fs');
|
|
1983
|
-
let value = '';
|
|
1984
|
-
try {
|
|
1985
|
-
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
1986
|
-
value = data.ai_primary_provider || '';
|
|
1987
|
-
} catch (err) {
|
|
1988
|
-
value = '';
|
|
1989
|
-
}
|
|
1990
|
-
process.stdout.write(value);
|
|
1991
|
-
NODE
|
|
1992
|
-
)"
|
|
1993
|
-
fi
|
|
1994
|
-
fi
|
|
1995
|
-
provider="$(printf '%s' "$provider" | tr '[:upper:]' '[:lower:]')"
|
|
1996
|
-
if [[ "$provider" != "openai" && "$provider" != "anthropic" ]]; then
|
|
1997
|
-
provider="anthropic"
|
|
1998
|
-
fi
|
|
1999
|
-
export AI_PRIMARY_PROVIDER="$provider"
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
# Load OpenAI model from config (defaults to gpt-4o-mini)
|
|
2003
|
-
tmux_load_openai_model() {
|
|
2004
|
-
if [[ -n "${OPENAI_MODEL-}" ]]; then
|
|
2005
|
-
return 0
|
|
2006
|
-
fi
|
|
2007
|
-
local config_file="$HOME/.orchestra/config.json"
|
|
2008
|
-
local model=""
|
|
2009
|
-
if [[ -f "$config_file" ]]; then
|
|
2010
|
-
if command -v jq >/dev/null 2>&1; then
|
|
2011
|
-
model="$(jq -r '.openai_model // empty' "$config_file" 2>/dev/null)"
|
|
2012
|
-
elif command -v python3 >/dev/null 2>&1; then
|
|
2013
|
-
model="$(python3 - "$config_file" <<'PY'
|
|
2014
|
-
import json
|
|
2015
|
-
import sys
|
|
2016
|
-
try:
|
|
2017
|
-
data = json.load(open(sys.argv[1], 'r'))
|
|
2018
|
-
except Exception:
|
|
2019
|
-
data = {}
|
|
2020
|
-
value = data.get('openai_model') or ''
|
|
2021
|
-
print(value)
|
|
2022
|
-
PY
|
|
2023
|
-
)"
|
|
2024
|
-
elif command -v node >/dev/null 2>&1; then
|
|
2025
|
-
model="$(node - "$config_file" <<'NODE'
|
|
2026
|
-
const fs = require('fs');
|
|
2027
|
-
let value = '';
|
|
2028
|
-
try {
|
|
2029
|
-
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
2030
|
-
value = data.openai_model || '';
|
|
2031
|
-
} catch (err) {
|
|
2032
|
-
value = '';
|
|
2033
|
-
}
|
|
2034
|
-
process.stdout.write(value);
|
|
2035
|
-
NODE
|
|
2036
|
-
)"
|
|
2037
|
-
fi
|
|
2038
|
-
fi
|
|
2039
|
-
if [[ -z "$model" || "$model" == "null" ]]; then
|
|
2040
|
-
model="gpt-4o-mini"
|
|
2041
|
-
fi
|
|
2042
|
-
export OPENAI_MODEL="$model"
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
# Generate a descriptive name for a tmux session using AI
|
|
2046
|
-
# Usage: tmux_generate_ai_session_name <session_name>
|
|
2047
|
-
tmux_generate_ai_session_name() {
|
|
2048
|
-
local session="$1"
|
|
2049
|
-
|
|
2050
|
-
# Load AI provider config and API keys
|
|
2051
|
-
tmux_load_ai_primary_provider
|
|
2052
|
-
tmux_load_openai_model
|
|
2053
|
-
tmux_load_anthropic_api_key
|
|
2054
|
-
tmux_load_openai_api_key
|
|
2055
|
-
|
|
2056
|
-
local primary="${AI_PRIMARY_PROVIDER:-anthropic}"
|
|
2057
|
-
primary="$(printf '%s' "$primary" | tr '[:upper:]' '[:lower:]')"
|
|
2058
|
-
local provider=""
|
|
2059
|
-
if [[ "$primary" == "openai" ]]; then
|
|
2060
|
-
if [[ -n "${OPENAI_API_KEY-}" ]]; then
|
|
2061
|
-
provider="openai"
|
|
2062
|
-
elif [[ -n "${ANTHROPIC_API_KEY-}" ]]; then
|
|
2063
|
-
provider="anthropic"
|
|
2064
|
-
fi
|
|
2065
|
-
else
|
|
2066
|
-
if [[ -n "${ANTHROPIC_API_KEY-}" ]]; then
|
|
2067
|
-
provider="anthropic"
|
|
2068
|
-
elif [[ -n "${OPENAI_API_KEY-}" ]]; then
|
|
2069
|
-
provider="openai"
|
|
2070
|
-
fi
|
|
2071
|
-
fi
|
|
2072
|
-
|
|
2073
|
-
if [[ -z "$provider" ]]; then
|
|
2074
|
-
err "No AI API key found in config file or .env file"
|
|
2075
|
-
return 1
|
|
2076
|
-
fi
|
|
2077
|
-
|
|
2078
|
-
local openai_model="${OPENAI_MODEL:-gpt-4o-mini}"
|
|
2079
|
-
|
|
2080
|
-
local pane_cmd pane_mode window_name pane_title alternate_on mouse_any_flag
|
|
2081
|
-
pane_cmd="$(tmux display-message -t "$session" -p '#{pane_current_command}' 2>/dev/null || echo "")"
|
|
2082
|
-
pane_mode="$(tmux display-message -t "$session" -p '#{pane_mode}' 2>/dev/null || echo "")"
|
|
2083
|
-
window_name="$(tmux display-message -t "$session" -p '#{window_name}' 2>/dev/null || echo "")"
|
|
2084
|
-
pane_title="$(tmux display-message -t "$session" -p '#{pane_title}' 2>/dev/null || echo "")"
|
|
2085
|
-
alternate_on="$(tmux display-message -t "$session" -p '#{alternate_on}' 2>/dev/null || echo "0")"
|
|
2086
|
-
mouse_any_flag="$(tmux display-message -t "$session" -p '#{mouse_any_flag}' 2>/dev/null || echo "0")"
|
|
2087
|
-
|
|
2088
|
-
local history_temp=""
|
|
2089
|
-
local history_arg=""
|
|
2090
|
-
local history_dir="${ORCHESTRA_HISTORY_DIR:-$HOME/.orchestra/history}"
|
|
2091
|
-
local pane_id
|
|
2092
|
-
pane_id="$(tmux display-message -t "$session" -p '#{pane_id}' 2>/dev/null || echo "")"
|
|
2093
|
-
local pane_key=""
|
|
2094
|
-
if [[ -n "$pane_id" ]]; then
|
|
2095
|
-
pane_key="$(_orchestra_history_key "$pane_id")"
|
|
2096
|
-
fi
|
|
2097
|
-
local session_key
|
|
2098
|
-
session_key="$(_orchestra_history_key "$session")"
|
|
2099
|
-
|
|
2100
|
-
local is_tui="false"
|
|
2101
|
-
if _tmux_is_tui_context "$pane_cmd" "$alternate_on" "$mouse_any_flag" "$pane_mode" "$window_name" "$pane_title"; then
|
|
2102
|
-
is_tui="true"
|
|
2103
|
-
fi
|
|
2104
|
-
|
|
2105
|
-
local content=""
|
|
2106
|
-
local visible_content=""
|
|
2107
|
-
if [[ "$is_tui" == "true" ]]; then
|
|
2108
|
-
visible_content="$(tmux capture-pane -e -p -q -a -t "${pane_id:-$session}" 2>/dev/null || echo "")"
|
|
2109
|
-
fi
|
|
2110
|
-
local scrollback_content
|
|
2111
|
-
scrollback_content="$(tmux capture-pane -t "$session" -p -S -200 2>/dev/null)" || {
|
|
2112
|
-
err "Failed to capture tmux pane content"
|
|
2113
|
-
return 1
|
|
2114
|
-
}
|
|
2115
|
-
if [[ -n "$visible_content" ]]; then
|
|
2116
|
-
content="### Visible terminal view\n${visible_content}\n\n### Recent scrollback\n${scrollback_content}"
|
|
2117
|
-
else
|
|
2118
|
-
content="$scrollback_content"
|
|
2119
|
-
fi
|
|
2120
|
-
|
|
2121
|
-
local history_path=""
|
|
2122
|
-
if [[ -n "$pane_key" && -f "$history_dir/$pane_key.log" ]]; then
|
|
2123
|
-
history_path="$history_dir/$pane_key.log"
|
|
2124
|
-
elif [[ -n "$session_key" && -f "$history_dir/$session_key.log" ]]; then
|
|
2125
|
-
history_path="$history_dir/$session_key.log"
|
|
2126
|
-
fi
|
|
2127
|
-
|
|
2128
|
-
if [[ -n "$history_path" ]]; then
|
|
2129
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
2130
|
-
history_temp=$(mktemp -t gw_hist)
|
|
2131
|
-
else
|
|
2132
|
-
history_temp=$(mktemp)
|
|
2133
|
-
fi
|
|
2134
|
-
tail -n 50 "$history_path" > "$history_temp"
|
|
2135
|
-
history_arg="$history_temp"
|
|
2136
|
-
fi
|
|
2137
|
-
|
|
2138
|
-
local metadata_block=""
|
|
2139
|
-
metadata_block+="Session metadata:\n"
|
|
2140
|
-
metadata_block+="- pane_current_command: ${pane_cmd:-unknown}\n"
|
|
2141
|
-
metadata_block+="- window_name: ${window_name:-}\n"
|
|
2142
|
-
metadata_block+="- pane_title: ${pane_title:-}\n"
|
|
2143
|
-
metadata_block+="- pane_mode: ${pane_mode:-}\n"
|
|
2144
|
-
metadata_block+="- alternate_screen_active: ${alternate_on:-0}\n"
|
|
2145
|
-
metadata_block+="- mouse_mode_active: ${mouse_any_flag:-0}\n"
|
|
2146
|
-
metadata_block+="- likely_tui_app: ${is_tui}\n"
|
|
2147
|
-
|
|
2148
|
-
# If content is empty or too short, keep a placeholder
|
|
2149
|
-
if [[ ${#content} -lt 10 ]]; then
|
|
2150
|
-
content="(no terminal output captured)"
|
|
2151
|
-
fi
|
|
2152
|
-
|
|
2153
|
-
# Truncate content if too long (to stay within token limits)
|
|
2154
|
-
if [[ ${#content} -gt 8000 ]]; then
|
|
2155
|
-
content="${content: -8000}"
|
|
2156
|
-
fi
|
|
2157
|
-
|
|
2158
|
-
# Create a temporary file for the content to avoid escaping issues
|
|
2159
|
-
# mktemp works differently on macOS vs Linux
|
|
2160
|
-
local temp_file
|
|
2161
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
2162
|
-
temp_file=$(mktemp -t gw_tmp)
|
|
2163
|
-
else
|
|
2164
|
-
temp_file=$(mktemp)
|
|
2165
|
-
fi
|
|
2166
|
-
printf '%s' "$content" > "$temp_file"
|
|
2167
|
-
|
|
2168
|
-
# Prepare the API request using Python for proper JSON encoding
|
|
2169
|
-
local request_body=""
|
|
2170
|
-
if have_cmd python3; then
|
|
2171
|
-
# Use Python to safely build the request body and extract typed commands from the capture
|
|
2172
|
-
request_payload=$(
|
|
2173
|
-
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'
|
|
2174
|
-
import json
|
|
2175
|
-
import re
|
|
2176
|
-
import sys
|
|
2177
|
-
from pathlib import Path
|
|
2178
|
-
|
|
2179
|
-
temp_path = Path(sys.argv[1])
|
|
2180
|
-
content = temp_path.read_text()
|
|
2181
|
-
history_arg = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
2182
|
-
provider = sys.argv[3] if len(sys.argv) > 3 else "anthropic"
|
|
2183
|
-
openai_model = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else "gpt-4o-mini"
|
|
2184
|
-
pane_cmd = sys.argv[5] if len(sys.argv) > 5 else ""
|
|
2185
|
-
window_name = sys.argv[6] if len(sys.argv) > 6 else ""
|
|
2186
|
-
pane_title = sys.argv[7] if len(sys.argv) > 7 else ""
|
|
2187
|
-
pane_mode = sys.argv[8] if len(sys.argv) > 8 else ""
|
|
2188
|
-
alternate_on = sys.argv[9] if len(sys.argv) > 9 else "0"
|
|
2189
|
-
mouse_any_flag = sys.argv[10] if len(sys.argv) > 10 else "0"
|
|
2190
|
-
is_tui = sys.argv[11] if len(sys.argv) > 11 else "false"
|
|
2191
|
-
|
|
2192
|
-
lines = content.splitlines()
|
|
2193
|
-
command_candidates = [] # Commands captured with high confidence
|
|
2194
|
-
fallback_candidates = [] # All prompt-like lines (used if we fail to classify)
|
|
2195
|
-
|
|
2196
|
-
history_commands = []
|
|
2197
|
-
if history_arg:
|
|
2198
|
-
hist_path = Path(history_arg)
|
|
2199
|
-
if hist_path.exists():
|
|
2200
|
-
for line in hist_path.read_text().splitlines():
|
|
2201
|
-
line = line.strip()
|
|
2202
|
-
if not line:
|
|
2203
|
-
continue
|
|
2204
|
-
if "\t" in line:
|
|
2205
|
-
history_commands.append(line.split("\t", 1)[1])
|
|
2206
|
-
else:
|
|
2207
|
-
history_commands.append(line)
|
|
2208
|
-
history_commands = history_commands[-50:]
|
|
2209
|
-
|
|
2210
|
-
prompt_pattern = re.compile(r"^[A-Za-z0-9_.@~/-]+$")
|
|
2211
|
-
prompt_sigil_pattern = re.compile(r"^(?P<prompt>[^\t]{0,120}?)(?P<sigil>[$#%])\s+(?P<cmd>.+)$")
|
|
2212
|
-
simple_sigil_pattern = re.compile(r"^(?P<sigil>[$#%])\s+(?P<cmd>.+)$")
|
|
2213
|
-
disallowed_prompts = {
|
|
2214
|
-
"warning",
|
|
2215
|
-
"error",
|
|
2216
|
-
"fatal",
|
|
2217
|
-
"hint",
|
|
2218
|
-
"note",
|
|
2219
|
-
"usage",
|
|
2220
|
-
"info",
|
|
2221
|
-
"debug",
|
|
2222
|
-
"trace",
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
disallowed_first_tokens = {
|
|
2226
|
-
"warning",
|
|
2227
|
-
"error",
|
|
2228
|
-
"fatal",
|
|
2229
|
-
"hint",
|
|
2230
|
-
"note",
|
|
2231
|
-
"usage",
|
|
2232
|
-
"not",
|
|
2233
|
-
"see",
|
|
2234
|
-
"for",
|
|
2235
|
-
"from",
|
|
2236
|
-
"with",
|
|
2237
|
-
"and",
|
|
2238
|
-
"or",
|
|
2239
|
-
"but",
|
|
2240
|
-
"at",
|
|
2241
|
-
"to",
|
|
2242
|
-
"in",
|
|
2243
|
-
"info",
|
|
2244
|
-
"debug",
|
|
2245
|
-
"trace",
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
common_commands = {
|
|
2249
|
-
"git",
|
|
2250
|
-
"gh",
|
|
2251
|
-
"npm",
|
|
2252
|
-
"pnpm",
|
|
2253
|
-
"yarn",
|
|
2254
|
-
"node",
|
|
2255
|
-
"npx",
|
|
2256
|
-
"bun",
|
|
2257
|
-
"cargo",
|
|
2258
|
-
"go",
|
|
2259
|
-
"python",
|
|
2260
|
-
"python3",
|
|
2261
|
-
"pip",
|
|
2262
|
-
"pip3",
|
|
2263
|
-
"poetry",
|
|
2264
|
-
"pipenv",
|
|
2265
|
-
"pytest",
|
|
2266
|
-
"uvicorn",
|
|
2267
|
-
"gunicorn",
|
|
2268
|
-
"flask",
|
|
2269
|
-
"django-admin",
|
|
2270
|
-
"rails",
|
|
2271
|
-
"bundle",
|
|
2272
|
-
"rake",
|
|
2273
|
-
"mix",
|
|
2274
|
-
"make",
|
|
2275
|
-
"cmake",
|
|
2276
|
-
"gradle",
|
|
2277
|
-
"mvn",
|
|
2278
|
-
"ant",
|
|
2279
|
-
"docker",
|
|
2280
|
-
"docker-compose",
|
|
2281
|
-
"kubectl",
|
|
2282
|
-
"helm",
|
|
2283
|
-
"terraform",
|
|
2284
|
-
"ansible",
|
|
2285
|
-
"ssh",
|
|
2286
|
-
"scp",
|
|
2287
|
-
"rsync",
|
|
2288
|
-
"sftp",
|
|
2289
|
-
"psql",
|
|
2290
|
-
"mysql",
|
|
2291
|
-
"mongo",
|
|
2292
|
-
"redis-cli",
|
|
2293
|
-
"sqlite3",
|
|
2294
|
-
"composer",
|
|
2295
|
-
"php",
|
|
2296
|
-
"ruby",
|
|
2297
|
-
"java",
|
|
2298
|
-
"javac",
|
|
2299
|
-
"deno",
|
|
2300
|
-
"dotnet",
|
|
2301
|
-
"msbuild",
|
|
2302
|
-
"tsc",
|
|
2303
|
-
"nx",
|
|
2304
|
-
"lerna",
|
|
2305
|
-
"eslint",
|
|
2306
|
-
"prettier",
|
|
2307
|
-
"ls",
|
|
2308
|
-
"cd",
|
|
2309
|
-
"pwd",
|
|
2310
|
-
"cat",
|
|
2311
|
-
"tail",
|
|
2312
|
-
"head",
|
|
2313
|
-
"less",
|
|
2314
|
-
"more",
|
|
2315
|
-
"grep",
|
|
2316
|
-
"rg",
|
|
2317
|
-
"fd",
|
|
2318
|
-
"find",
|
|
2319
|
-
"watch",
|
|
2320
|
-
"code",
|
|
2321
|
-
"open",
|
|
2322
|
-
"vim",
|
|
2323
|
-
"nvim",
|
|
2324
|
-
"tmux",
|
|
2325
|
-
"htop",
|
|
2326
|
-
"top",
|
|
2327
|
-
"brew",
|
|
2328
|
-
"tox",
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
allowed_prefixes = (
|
|
2332
|
-
"./",
|
|
2333
|
-
"../",
|
|
2334
|
-
"~/",
|
|
2335
|
-
"bin/",
|
|
2336
|
-
"sbin/",
|
|
2337
|
-
)
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
def clean_tokens(tokens):
|
|
2341
|
-
while tokens and tokens[0] in {"$", "#", "%"}:
|
|
2342
|
-
tokens = tokens[1:]
|
|
2343
|
-
while tokens and (tokens[0].endswith("$") or tokens[0].endswith("#") or tokens[0].endswith("%")):
|
|
2344
|
-
tokens = tokens[1:]
|
|
2345
|
-
return tokens
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
def extract_prompt_and_command(raw_line):
|
|
2349
|
-
stripped = raw_line.strip()
|
|
2350
|
-
if not stripped:
|
|
2351
|
-
return "", ""
|
|
2352
|
-
|
|
2353
|
-
# Pattern 1: host:path:command (common bash/zsh prompts)
|
|
2354
|
-
if ":" in stripped:
|
|
2355
|
-
maybe_prompt, maybe_cmd = stripped.split(":", 1)
|
|
2356
|
-
maybe_prompt = maybe_prompt.strip().rstrip("#$%")
|
|
2357
|
-
maybe_cmd = maybe_cmd.strip()
|
|
2358
|
-
if maybe_prompt and maybe_cmd and prompt_pattern.match(maybe_prompt):
|
|
2359
|
-
prompt_lower = maybe_prompt.lower()
|
|
2360
|
-
if prompt_lower not in disallowed_prompts:
|
|
2361
|
-
return maybe_prompt, maybe_cmd
|
|
2362
|
-
|
|
2363
|
-
# Pattern 2: prompt ending with $, #, % (supports prompts without ':')
|
|
2364
|
-
match = prompt_sigil_pattern.match(stripped)
|
|
2365
|
-
if match:
|
|
2366
|
-
maybe_prompt = (match.group("prompt") or "").strip().rstrip("#$%")
|
|
2367
|
-
maybe_cmd = (match.group("cmd") or "").strip()
|
|
2368
|
-
if maybe_cmd:
|
|
2369
|
-
if maybe_prompt and maybe_prompt.lower() in disallowed_prompts:
|
|
2370
|
-
return "", ""
|
|
2371
|
-
return (maybe_prompt if maybe_prompt else match.group("sigil")), maybe_cmd
|
|
2372
|
-
|
|
2373
|
-
# Pattern 3: bare prompt lines like "$ git status"
|
|
2374
|
-
match = simple_sigil_pattern.match(stripped)
|
|
2375
|
-
if match:
|
|
2376
|
-
maybe_cmd = (match.group("cmd") or "").strip()
|
|
2377
|
-
if maybe_cmd:
|
|
2378
|
-
return match.group("sigil"), maybe_cmd
|
|
2379
|
-
|
|
2380
|
-
return "", ""
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
for raw_line in lines:
|
|
2384
|
-
prompt_part, command_part = extract_prompt_and_command(raw_line)
|
|
2385
|
-
if not command_part:
|
|
2386
|
-
continue
|
|
2387
|
-
|
|
2388
|
-
tokens = clean_tokens(command_part.split())
|
|
2389
|
-
if not tokens:
|
|
2390
|
-
continue
|
|
2391
|
-
|
|
2392
|
-
first_token = tokens[0].lower()
|
|
2393
|
-
if first_token == "sudo" and len(tokens) > 1:
|
|
2394
|
-
first_token = tokens[1].lower()
|
|
2395
|
-
|
|
2396
|
-
if first_token in disallowed_first_tokens:
|
|
2397
|
-
continue
|
|
2398
|
-
|
|
2399
|
-
normalized_command = " ".join(tokens)
|
|
2400
|
-
formatted_line = f"{prompt_part}:{normalized_command}"
|
|
2401
|
-
fallback_candidates.append(formatted_line)
|
|
2402
|
-
|
|
2403
|
-
allowed = (
|
|
2404
|
-
first_token in common_commands
|
|
2405
|
-
or normalized_command.startswith(allowed_prefixes)
|
|
2406
|
-
or any(
|
|
2407
|
-
first_token.startswith(prefix)
|
|
2408
|
-
for prefix in (
|
|
2409
|
-
"git",
|
|
2410
|
-
"npm",
|
|
2411
|
-
"pnpm",
|
|
2412
|
-
"yarn",
|
|
2413
|
-
"node",
|
|
2414
|
-
"npx",
|
|
2415
|
-
"bun",
|
|
2416
|
-
"cargo",
|
|
2417
|
-
"python",
|
|
2418
|
-
"pip",
|
|
2419
|
-
"poetry",
|
|
2420
|
-
"pytest",
|
|
2421
|
-
"uvicorn",
|
|
2422
|
-
"docker",
|
|
2423
|
-
"kubectl",
|
|
2424
|
-
"helm",
|
|
2425
|
-
"terraform",
|
|
2426
|
-
"ansible",
|
|
2427
|
-
"ssh",
|
|
2428
|
-
"scp",
|
|
2429
|
-
"rsync",
|
|
2430
|
-
"rails",
|
|
2431
|
-
"bundle",
|
|
2432
|
-
"rake",
|
|
2433
|
-
"mix",
|
|
2434
|
-
"psql",
|
|
2435
|
-
"mysql",
|
|
2436
|
-
"mongo",
|
|
2437
|
-
"redis",
|
|
2438
|
-
)
|
|
2439
|
-
)
|
|
2440
|
-
)
|
|
2441
|
-
|
|
2442
|
-
if allowed:
|
|
2443
|
-
command_candidates.append(formatted_line)
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
if history_commands:
|
|
2447
|
-
command_candidates = history_commands
|
|
2448
|
-
elif not command_candidates:
|
|
2449
|
-
command_candidates = fallback_candidates
|
|
2450
|
-
|
|
2451
|
-
extracted_commands = command_candidates
|
|
2452
|
-
history_lines = [f"{idx + 1}. {line}" for idx, line in enumerate(extracted_commands)]
|
|
2453
|
-
command_history = "\n".join(history_lines) if history_lines else "None detected."
|
|
2454
|
-
last_command_line = extracted_commands[-1] if extracted_commands else ""
|
|
2455
|
-
commands_detected = bool(extracted_commands)
|
|
2456
|
-
|
|
2457
|
-
# Capture the first 10 lines of output as fallback context (joined by newline)
|
|
2458
|
-
first_ten_lines = "\n".join(lines[:10]) if lines else ""
|
|
2459
|
-
|
|
2460
|
-
description_guidance = """Description rules:
|
|
2461
|
-
- Describe the primary activity in a short phrase (3-8 words).
|
|
2462
|
-
- Do NOT include any app/tool name or prefix in the output.
|
|
2463
|
-
- If git activity is visible, the description should reflect it (e.g., reviewing changes), but do not add 'git_'."""
|
|
2464
|
-
|
|
2465
|
-
metadata_summary = "\n".join(
|
|
2466
|
-
line
|
|
2467
|
-
for line in [
|
|
2468
|
-
f"pane_current_command: {pane_cmd}" if pane_cmd else "",
|
|
2469
|
-
f"window_name: {window_name}" if window_name else "",
|
|
2470
|
-
f"pane_title: {pane_title}" if pane_title else "",
|
|
2471
|
-
f"pane_mode: {pane_mode}" if pane_mode else "",
|
|
2472
|
-
f"alternate_screen_active: {alternate_on}",
|
|
2473
|
-
f"mouse_mode_active: {mouse_any_flag}",
|
|
2474
|
-
f"likely_tui_app: {is_tui}",
|
|
2475
|
-
]
|
|
2476
|
-
if line
|
|
2477
|
-
)
|
|
2478
|
-
|
|
2479
|
-
prompt = f"""Analyze this terminal session and create a descriptive tmux session name based on what the user actually did.
|
|
2480
|
-
|
|
2481
|
-
Terminal output (last capture):
|
|
2482
|
-
{content}
|
|
2483
|
-
|
|
2484
|
-
Tmux metadata (use as supporting hints, not as the primary source of truth):
|
|
2485
|
-
{metadata_summary}
|
|
2486
|
-
|
|
2487
|
-
Extracted command history (oldest to newest):
|
|
2488
|
-
{command_history}
|
|
2489
|
-
|
|
2490
|
-
Most recent command (anchor for description and summary):
|
|
2491
|
-
{last_command_line if last_command_line else 'None detected'}
|
|
2492
|
-
|
|
2493
|
-
Focus: produce a concise description of the activity. Follow these rules:
|
|
2494
|
-
{description_guidance}
|
|
2495
|
-
|
|
2496
|
-
{'No clear prompt/command lines were detected. Rely on the terminal output context.' if not commands_detected else ''}
|
|
2497
|
-
|
|
2498
|
-
{'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 ''}
|
|
2499
|
-
|
|
2500
|
-
{'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 ''}
|
|
2501
|
-
|
|
2502
|
-
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).
|
|
2503
|
-
|
|
2504
|
-
Instructions:
|
|
2505
|
-
1. Produce a session name using lowercase letters, numbers, and underscores only.
|
|
2506
|
-
2. Maximum length: 100 characters.
|
|
2507
|
-
3. Do NOT include any app/tool name or prefix.
|
|
2508
|
-
4. Describe the concrete task or state in a short phrase.
|
|
2509
|
-
5. If git activity is visible anywhere, the description must reflect git activity.
|
|
2510
|
-
6. Respond with ONLY the final session name, nothing else."""
|
|
2511
|
-
|
|
2512
|
-
if provider == "openai":
|
|
2513
|
-
request = {
|
|
2514
|
-
"model": openai_model,
|
|
2515
|
-
"max_tokens": 100,
|
|
2516
|
-
"temperature": 0.2,
|
|
2517
|
-
"messages": [
|
|
2518
|
-
{
|
|
2519
|
-
"role": "user",
|
|
2520
|
-
"content": prompt
|
|
2521
|
-
}
|
|
2522
|
-
]
|
|
2523
|
-
}
|
|
2524
|
-
else:
|
|
2525
|
-
request = {
|
|
2526
|
-
"model": "claude-3-5-haiku-latest",
|
|
2527
|
-
"max_tokens": 100,
|
|
2528
|
-
"messages": [
|
|
2529
|
-
{
|
|
2530
|
-
"role": "user",
|
|
2531
|
-
"content": prompt
|
|
2532
|
-
}
|
|
2533
|
-
]
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
print(json.dumps(request))
|
|
2537
|
-
PYCODE
|
|
2538
|
-
)
|
|
2539
|
-
request_body="$request_payload"
|
|
2540
|
-
fi
|
|
2541
|
-
if [[ -z "$request_body" ]]; then
|
|
2542
|
-
# Fallback: base64 encode the content to avoid escaping issues
|
|
2543
|
-
local encoded_content
|
|
2544
|
-
local fallback_file
|
|
2545
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
2546
|
-
fallback_file=$(mktemp -t gw_fb)
|
|
2547
|
-
else
|
|
2548
|
-
fallback_file=$(mktemp)
|
|
2549
|
-
fi
|
|
2550
|
-
{
|
|
2551
|
-
printf '%b\n\n' "$metadata_block"
|
|
2552
|
-
cat "$temp_file"
|
|
2553
|
-
} > "$fallback_file"
|
|
2554
|
-
encoded_content=$(base64 < "$fallback_file" | tr -d '\n')
|
|
2555
|
-
rm -f "$fallback_file"
|
|
2556
|
-
if [[ "$provider" == "openai" ]]; then
|
|
2557
|
-
request_body=$(cat <<EOF
|
|
2558
|
-
{
|
|
2559
|
-
"model": "$openai_model",
|
|
2560
|
-
"max_tokens": 100,
|
|
2561
|
-
"temperature": 0.2,
|
|
2562
|
-
"messages": [
|
|
2563
|
-
{
|
|
2564
|
-
"role": "user",
|
|
2565
|
-
"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."
|
|
2566
|
-
}
|
|
2567
|
-
]
|
|
2568
|
-
}
|
|
2569
|
-
EOF
|
|
2570
|
-
)
|
|
2571
|
-
else
|
|
2572
|
-
request_body=$(cat <<EOF
|
|
2573
|
-
{
|
|
2574
|
-
"model": "claude-3-5-haiku-latest",
|
|
2575
|
-
"max_tokens": 100,
|
|
2576
|
-
"messages": [
|
|
2577
|
-
{
|
|
2578
|
-
"role": "user",
|
|
2579
|
-
"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."
|
|
2580
|
-
}
|
|
2581
|
-
]
|
|
2582
|
-
}
|
|
2583
|
-
EOF
|
|
2584
|
-
)
|
|
2585
|
-
fi
|
|
2586
|
-
fi
|
|
2587
|
-
|
|
2588
|
-
# Make the API call
|
|
2589
|
-
local response
|
|
2590
|
-
local new_name=""
|
|
2591
|
-
if [[ "$provider" == "openai" ]]; then
|
|
2592
|
-
# First attempt: OpenAI Responses API + Files API for structured app/description output.
|
|
2593
|
-
# If this path fails, we fall back to chat completions.
|
|
2594
|
-
local context_file=""
|
|
2595
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
2596
|
-
context_file=$(mktemp -t gw_ctx)
|
|
2597
|
-
else
|
|
2598
|
-
context_file=$(mktemp)
|
|
2599
|
-
fi
|
|
2600
|
-
{
|
|
2601
|
-
printf '%b\n\n' "$metadata_block"
|
|
2602
|
-
printf 'TMUX SESSION CAPTURE\n\n'
|
|
2603
|
-
cat "$temp_file"
|
|
2604
|
-
if [[ -n "$history_arg" && -f "$history_arg" ]]; then
|
|
2605
|
-
printf '\n\nRECENT COMMAND HISTORY\n\n'
|
|
2606
|
-
cat "$history_arg"
|
|
2607
|
-
fi
|
|
2608
|
-
} > "$context_file"
|
|
2609
|
-
|
|
2610
|
-
local file_upload_resp openai_file_id
|
|
2611
|
-
file_upload_resp=$(curl -s -X POST https://api.openai.com/v1/files \
|
|
2612
|
-
--max-time 20 \
|
|
2613
|
-
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
2614
|
-
-F purpose="user_data" \
|
|
2615
|
-
-F "file=@$context_file;type=text/plain" 2>/dev/null)
|
|
2616
|
-
rm -f "$context_file"
|
|
2617
|
-
|
|
2618
|
-
openai_file_id=""
|
|
2619
|
-
if have_cmd python3; then
|
|
2620
|
-
openai_file_id="$(python3 -c 'import json,sys
|
|
2621
|
-
try:
|
|
2622
|
-
data = json.load(sys.stdin)
|
|
2623
|
-
except Exception:
|
|
2624
|
-
data = {}
|
|
2625
|
-
print(data.get("id", ""))' <<<"$file_upload_resp" 2>/dev/null || true)"
|
|
2626
|
-
fi
|
|
2627
|
-
|
|
2628
|
-
if [[ -n "$openai_file_id" ]]; then
|
|
2629
|
-
local responses_body responses_resp ai_desc
|
|
2630
|
-
responses_body=$(python3 - "$openai_model" "$openai_file_id" <<'PYCODE'
|
|
2631
|
-
import json
|
|
2632
|
-
import sys
|
|
2633
|
-
|
|
2634
|
-
model = sys.argv[1]
|
|
2635
|
-
file_id = sys.argv[2]
|
|
2636
|
-
|
|
2637
|
-
prompt = """Analyze the provided terminal session file and produce ONLY a short activity description.
|
|
2638
|
-
|
|
2639
|
-
Return STRICT JSON in this exact shape:
|
|
2640
|
-
{"description":"<short_snake_case_description>"}
|
|
2641
|
-
|
|
2642
|
-
Rules:
|
|
2643
|
-
- description: concise snake_case description of the activity
|
|
2644
|
-
- do NOT include app/tool name or any prefix in the description
|
|
2645
|
-
- do not include punctuation beyond underscores
|
|
2646
|
-
- keep it short and specific
|
|
2647
|
-
"""
|
|
2648
|
-
|
|
2649
|
-
req = {
|
|
2650
|
-
"model": model,
|
|
2651
|
-
"temperature": 0.2,
|
|
2652
|
-
"max_output_tokens": 180,
|
|
2653
|
-
"input": [
|
|
2654
|
-
{
|
|
2655
|
-
"role": "user",
|
|
2656
|
-
"content": [
|
|
2657
|
-
{"type": "input_text", "text": prompt},
|
|
2658
|
-
{"type": "input_file", "file_id": file_id},
|
|
2659
|
-
],
|
|
2660
|
-
}
|
|
2661
|
-
],
|
|
2662
|
-
}
|
|
2663
|
-
print(json.dumps(req))
|
|
2664
|
-
PYCODE
|
|
2665
|
-
)
|
|
2666
|
-
|
|
2667
|
-
responses_resp=$(curl -s -X POST https://api.openai.com/v1/responses \
|
|
2668
|
-
--max-time 20 \
|
|
2669
|
-
-H "Content-Type: application/json" \
|
|
2670
|
-
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
2671
|
-
-d "$responses_body" 2>/dev/null)
|
|
2672
|
-
|
|
2673
|
-
# Best-effort cleanup of uploaded file
|
|
2674
|
-
curl -s -X DELETE "https://api.openai.com/v1/files/$openai_file_id" \
|
|
2675
|
-
-H "Authorization: Bearer $OPENAI_API_KEY" >/dev/null 2>&1 || true
|
|
2676
|
-
|
|
2677
|
-
ai_desc="$(python3 -c 'import json
|
|
2678
|
-
import re
|
|
2679
|
-
import sys
|
|
2680
|
-
|
|
2681
|
-
def clean(value):
|
|
2682
|
-
value = (value or "").strip().lower()
|
|
2683
|
-
value = re.sub(r"[^a-z0-9_ -]+", "", value)
|
|
2684
|
-
value = value.replace(" ", "_")
|
|
2685
|
-
value = re.sub(r"_+", "_", value).strip("_")
|
|
2686
|
-
return value
|
|
2687
|
-
|
|
2688
|
-
try:
|
|
2689
|
-
data = json.load(sys.stdin)
|
|
2690
|
-
except Exception:
|
|
2691
|
-
print("")
|
|
2692
|
-
raise SystemExit
|
|
2693
|
-
|
|
2694
|
-
text = ""
|
|
2695
|
-
if isinstance(data.get("output_text"), str):
|
|
2696
|
-
text = data["output_text"]
|
|
2697
|
-
|
|
2698
|
-
if not text:
|
|
2699
|
-
for item in data.get("output", []):
|
|
2700
|
-
if not isinstance(item, dict):
|
|
2701
|
-
continue
|
|
2702
|
-
if item.get("type") != "message":
|
|
2703
|
-
continue
|
|
2704
|
-
for content in item.get("content", []):
|
|
2705
|
-
if not isinstance(content, dict):
|
|
2706
|
-
continue
|
|
2707
|
-
if isinstance(content.get("text"), str):
|
|
2708
|
-
text += content["text"]
|
|
2709
|
-
|
|
2710
|
-
obj = None
|
|
2711
|
-
if text:
|
|
2712
|
-
text = text.strip()
|
|
2713
|
-
try:
|
|
2714
|
-
obj = json.loads(text)
|
|
2715
|
-
except Exception:
|
|
2716
|
-
match = re.search(r"\{.*\}", text, re.S)
|
|
2717
|
-
if match:
|
|
2718
|
-
try:
|
|
2719
|
-
obj = json.loads(match.group(0))
|
|
2720
|
-
except Exception:
|
|
2721
|
-
obj = None
|
|
2722
|
-
|
|
2723
|
-
description = ""
|
|
2724
|
-
if isinstance(obj, dict):
|
|
2725
|
-
description = clean(str(obj.get("description", "")))
|
|
2726
|
-
|
|
2727
|
-
if not description and text:
|
|
2728
|
-
description = clean(text)
|
|
2729
|
-
|
|
2730
|
-
print(description)' <<<"$responses_resp" 2>/dev/null || true)"
|
|
2731
|
-
|
|
2732
|
-
if [[ -n "$ai_desc" ]]; then
|
|
2733
|
-
new_name="$ai_desc"
|
|
2734
|
-
fi
|
|
2735
|
-
fi
|
|
2736
|
-
|
|
2737
|
-
if [[ -z "$new_name" ]]; then
|
|
2738
|
-
# Fallback: existing chat completions path
|
|
2739
|
-
response=$(curl -s -X POST https://api.openai.com/v1/chat/completions \
|
|
2740
|
-
--max-time 20 \
|
|
2741
|
-
-H "Content-Type: application/json" \
|
|
2742
|
-
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
2743
|
-
-d "$request_body" 2>&1) || {
|
|
2744
|
-
err "Failed to call OpenAI API: $response"
|
|
2745
|
-
return 1
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
if echo "$response" | grep -q '"error"'; then
|
|
2749
|
-
local error_msg=$(echo "$response" | python3 -c "import sys, json; print(json.load(sys.stdin).get('error', {}).get('message', 'Unknown error'))" 2>/dev/null || echo "API error")
|
|
2750
|
-
err "API error: $error_msg"
|
|
2751
|
-
return 1
|
|
2752
|
-
fi
|
|
2753
|
-
|
|
2754
|
-
new_name=$(echo "$response" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('choices', [{}])[0].get('message', {}).get('content', ''))" 2>/dev/null) || {
|
|
2755
|
-
new_name=$(echo "$response" | grep -o '"content":"[^"]*"' | head -1 | sed 's/"content":"\([^"]*\)"/\1/')
|
|
2756
|
-
}
|
|
2757
|
-
fi
|
|
2758
|
-
else
|
|
2759
|
-
response=$(curl -s -X POST https://api.anthropic.com/v1/messages \
|
|
2760
|
-
--max-time 20 \
|
|
2761
|
-
-H "Content-Type: application/json" \
|
|
2762
|
-
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
|
2763
|
-
-H "anthropic-version: 2023-06-01" \
|
|
2764
|
-
-d "$request_body" 2>&1) || {
|
|
2765
|
-
err "Failed to call Anthropic API: $response"
|
|
2766
|
-
return 1
|
|
2767
|
-
}
|
|
2768
|
-
|
|
2769
|
-
if echo "$response" | grep -q '"error"'; then
|
|
2770
|
-
local error_msg=$(echo "$response" | python3 -c "import sys, json; print(json.load(sys.stdin).get('error', {}).get('message', 'Unknown error'))" 2>/dev/null || echo "API error")
|
|
2771
|
-
err "API error: $error_msg"
|
|
2772
|
-
return 1
|
|
2773
|
-
fi
|
|
2774
|
-
|
|
2775
|
-
new_name=$(echo "$response" | python3 -c "import sys, json; data = json.load(sys.stdin); print(data.get('content', [{}])[0].get('text', ''))" 2>/dev/null) || {
|
|
2776
|
-
new_name=$(echo "$response" | grep -o '"text":"[^"]*"' | head -1 | sed 's/"text":"\([^"]*\)"/\1/')
|
|
2777
|
-
}
|
|
2778
|
-
fi
|
|
2779
|
-
|
|
2780
|
-
# Clean up temp files
|
|
2781
|
-
rm -f "$temp_file"
|
|
2782
|
-
if [[ -n "$history_temp" ]]; then
|
|
2783
|
-
rm -f "$history_temp"
|
|
2784
|
-
fi
|
|
2785
|
-
|
|
2786
|
-
# Clean up the AI description (remove any spaces, special chars except underscore and dash)
|
|
2787
|
-
new_name=$(echo "$new_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)
|
|
2788
|
-
if [[ -z "$new_name" ]]; then
|
|
2789
|
-
err "Failed to generate name from AI response"
|
|
2790
|
-
return 1
|
|
2791
|
-
fi
|
|
2792
|
-
|
|
2793
|
-
new_name="${new_name:0:100}"
|
|
2794
|
-
|
|
2795
|
-
echo "$new_name"
|
|
2796
|
-
}
|
|
2797
|
-
|
|
2798
|
-
# Display tmux message
|
|
2799
|
-
# Usage: tmux_display_message <message> [duration_ms]
|
|
2800
|
-
tmux_display_message() {
|
|
2801
|
-
local message="$1"
|
|
2802
|
-
local duration="${2:-3000}"
|
|
2803
|
-
|
|
2804
|
-
if tmux_available && tmux_inside_session; then
|
|
2805
|
-
tmux display-message -d "$duration" "$message"
|
|
2806
|
-
fi
|
|
2807
|
-
}
|
|
5
|
+
_ORCHESTRA_API_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# shellcheck source=/dev/null
|
|
7
|
+
source "$_ORCHESTRA_API_DIR/../server/session/tmux_api.sh"
|