@humanu/orchestra 0.5.71 → 0.5.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/resources/api/tmux.sh +540 -32
- package/resources/prebuilt/linux-x64/orchestra +0 -0
- package/resources/prebuilt/macos-arm64/orchestra +0 -0
- package/resources/prebuilt/macos-intel/orchestra +0 -0
- package/resources/scripts/gw-bridge.sh +11 -0
- package/resources/scripts/shell/bridge/tmux.sh +36 -0
package/package.json
CHANGED
package/resources/api/tmux.sh
CHANGED
|
@@ -236,18 +236,330 @@ _tmux_source_command_hook() {
|
|
|
236
236
|
# Helper: orchestra prefix including delimiter
|
|
237
237
|
_tmux_orch_prefix() { printf "orchestra%s" "$( _tmux_delim )"; }
|
|
238
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
|
+
|
|
239
419
|
_tmux_status_escape_text() {
|
|
240
420
|
local text="$1"
|
|
241
421
|
text="${text//\#/##}"
|
|
242
422
|
printf '%s' "$text"
|
|
243
423
|
}
|
|
244
424
|
|
|
425
|
+
_tmux_truncate_tab_label() {
|
|
426
|
+
local label="$1"
|
|
427
|
+
local max_len="${2:-18}"
|
|
428
|
+
if (( ${#label} > max_len )); then
|
|
429
|
+
printf '%s...' "${label:0:$((max_len - 3))}"
|
|
430
|
+
else
|
|
431
|
+
printf '%s' "$label"
|
|
432
|
+
fi
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_tmux_workspace_session_rows() {
|
|
436
|
+
local current_session="$1"
|
|
437
|
+
local repo_root="$2"
|
|
438
|
+
local db_path active_file
|
|
439
|
+
db_path="$(_tmux_session_registry_path)"
|
|
440
|
+
|
|
441
|
+
[[ -f "$db_path" ]] || return 0
|
|
442
|
+
have_cmd python3 || return 0
|
|
443
|
+
|
|
444
|
+
active_file="$(mktemp)"
|
|
445
|
+
tmux list-sessions -F '#{session_name}' > "$active_file" 2>/dev/null || {
|
|
446
|
+
rm -f "$active_file"
|
|
447
|
+
return 0
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
python3 - "$db_path" "$repo_root" "$current_session" "$active_file" <<'PY'
|
|
451
|
+
import sqlite3
|
|
452
|
+
import sys
|
|
453
|
+
|
|
454
|
+
db_path, repo_root, current_session, active_path = sys.argv[1:5]
|
|
455
|
+
with open(active_path, "r", encoding="utf-8") as handle:
|
|
456
|
+
active = {line.strip() for line in handle if line.strip()}
|
|
457
|
+
|
|
458
|
+
conn = sqlite3.connect(db_path)
|
|
459
|
+
rows = conn.execute(
|
|
460
|
+
"""
|
|
461
|
+
SELECT tmux_name, COALESCE(display_name, '')
|
|
462
|
+
FROM sessions
|
|
463
|
+
WHERE repo_root = ?1
|
|
464
|
+
ORDER BY worktree_path COLLATE NOCASE, created_at, tmux_name COLLATE NOCASE
|
|
465
|
+
""",
|
|
466
|
+
(repo_root,),
|
|
467
|
+
).fetchall()
|
|
468
|
+
|
|
469
|
+
filtered = []
|
|
470
|
+
seen = set()
|
|
471
|
+
for name, display_name in rows:
|
|
472
|
+
if name in active and name not in seen:
|
|
473
|
+
filtered.append((name, display_name or ""))
|
|
474
|
+
seen.add(name)
|
|
475
|
+
|
|
476
|
+
if not filtered:
|
|
477
|
+
raise SystemExit(0)
|
|
478
|
+
|
|
479
|
+
max_tabs = 8
|
|
480
|
+
if len(filtered) <= max_tabs:
|
|
481
|
+
visible = filtered
|
|
482
|
+
prefix = suffix = False
|
|
483
|
+
else:
|
|
484
|
+
try:
|
|
485
|
+
current_index = [name for name, _ in filtered].index(current_session)
|
|
486
|
+
except ValueError:
|
|
487
|
+
current_index = 0
|
|
488
|
+
start = max(0, min(current_index - 2, len(filtered) - max_tabs))
|
|
489
|
+
end = min(len(filtered), start + max_tabs)
|
|
490
|
+
visible = filtered[start:end]
|
|
491
|
+
prefix = start > 0
|
|
492
|
+
suffix = end < len(filtered)
|
|
493
|
+
|
|
494
|
+
if prefix:
|
|
495
|
+
print("__ellipsis__\t")
|
|
496
|
+
for name, display_name in visible:
|
|
497
|
+
print(f"{name}\t{display_name}")
|
|
498
|
+
if suffix:
|
|
499
|
+
print("__ellipsis__\t")
|
|
500
|
+
PY
|
|
501
|
+
local status=$?
|
|
502
|
+
rm -f "$active_file"
|
|
503
|
+
return $status
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_tmux_workspace_session_tabs() {
|
|
507
|
+
local current_session="$1"
|
|
508
|
+
local fallback_display_name="$2"
|
|
509
|
+
local session_dir repo_root rows tabs name display_name label escaped_label divider
|
|
510
|
+
|
|
511
|
+
session_dir="$(tmux display-message -t "$current_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
512
|
+
repo_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
513
|
+
if [[ -n "$repo_root" ]]; then
|
|
514
|
+
_tmux_registry_sync_active_workspace_sessions "$repo_root" >/dev/null 2>&1 || true
|
|
515
|
+
rows="$(_tmux_workspace_session_rows "$current_session" "$repo_root" 2>/dev/null || true)"
|
|
516
|
+
else
|
|
517
|
+
rows=""
|
|
518
|
+
fi
|
|
519
|
+
|
|
520
|
+
if [[ -z "$rows" ]]; then
|
|
521
|
+
label="$(_tmux_truncate_tab_label "$fallback_display_name")"
|
|
522
|
+
escaped_label="$(_tmux_status_escape_text "$label")"
|
|
523
|
+
printf '#[fg=white,bg=colour22,bold] %s #[default]' "$escaped_label"
|
|
524
|
+
return
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
tabs=""
|
|
528
|
+
divider="#[fg=white,bg=colour22] | #[default]"
|
|
529
|
+
while IFS=$'\t' read -r name display_name; do
|
|
530
|
+
[[ -n "$name" ]] || continue
|
|
531
|
+
if [[ -n "$tabs" ]]; then
|
|
532
|
+
tabs+="$divider"
|
|
533
|
+
fi
|
|
534
|
+
if [[ "$name" == "__ellipsis__" ]]; then
|
|
535
|
+
tabs+="#[fg=white,bg=colour22] ... #[default]"
|
|
536
|
+
continue
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
if [[ -z "$display_name" ]]; then
|
|
540
|
+
display_name="$(tmux_format_session_display "$name" without-timestamp)"
|
|
541
|
+
fi
|
|
542
|
+
label="$(_tmux_truncate_tab_label "$display_name")"
|
|
543
|
+
escaped_label="$(_tmux_status_escape_text "$label")"
|
|
544
|
+
|
|
545
|
+
if [[ "$name" == "$current_session" ]]; then
|
|
546
|
+
tabs+="#[fg=white,bg=colour22,bold] [${escaped_label}] #[default]"
|
|
547
|
+
else
|
|
548
|
+
tabs+="#[fg=white,bg=colour22] ${escaped_label} #[default]"
|
|
549
|
+
fi
|
|
550
|
+
done <<< "$rows"
|
|
551
|
+
|
|
552
|
+
printf '%s' "$tabs"
|
|
553
|
+
}
|
|
554
|
+
|
|
245
555
|
_tmux_orchestra_status_left() {
|
|
246
|
-
local
|
|
247
|
-
local session_display_name="$
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
556
|
+
local session_name="$1"
|
|
557
|
+
local session_display_name="$3"
|
|
558
|
+
_tmux_workspace_session_tabs "$session_name" "$session_display_name"
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
_tmux_orchestra_status_right() {
|
|
562
|
+
printf ''
|
|
251
563
|
}
|
|
252
564
|
|
|
253
565
|
_tmux_configure_orchestra_bindings() {
|
|
@@ -255,32 +567,78 @@ _tmux_configure_orchestra_bindings() {
|
|
|
255
567
|
bridge="$(_orchestra_bridge_script)"
|
|
256
568
|
[[ -f "$bridge" ]] || return
|
|
257
569
|
|
|
258
|
-
local quoted_bridge rename_command prompt_command
|
|
570
|
+
local quoted_bridge rename_command prompt_command next_command previous_command help_command
|
|
259
571
|
printf -v quoted_bridge '%q' "$bridge"
|
|
260
572
|
rename_command="$quoted_bridge manual-rename-session \\\"#{session_name}\\\" \\\"%%\\\" >/dev/null 2>&1"
|
|
261
573
|
prompt_command="command-prompt -p 'Rename Orchestra session:' 'run-shell -b \"$rename_command\"'"
|
|
574
|
+
next_command="$quoted_bridge cycle-workspace-session \\\"#{session_name}\\\" next \\\"#{client_tty}\\\" >/dev/null 2>&1"
|
|
575
|
+
previous_command="$quoted_bridge cycle-workspace-session \\\"#{session_name}\\\" previous \\\"#{client_tty}\\\" >/dev/null 2>&1"
|
|
576
|
+
help_command="$quoted_bridge tmux-help-popup \\\"#{client_tty}\\\" >/dev/null 2>&1"
|
|
262
577
|
|
|
578
|
+
tmux bind-key -T prefix '?' if-shell -F '#{@orchestra_display_name}' \
|
|
579
|
+
"run-shell -b \"$help_command\"" 'list-keys -N' >/dev/null 2>&1 || true
|
|
263
580
|
tmux bind-key -T prefix r if-shell -F '#{@orchestra_display_name}' \
|
|
264
581
|
"$prompt_command" 'refresh-client -S' >/dev/null 2>&1 || true
|
|
582
|
+
tmux bind-key -T prefix '>' if-shell -F '#{@orchestra_display_name}' \
|
|
583
|
+
"run-shell -b \"$next_command\"" 'switch-client -n' >/dev/null 2>&1 || true
|
|
584
|
+
tmux bind-key -T prefix '<' if-shell -F '#{@orchestra_display_name}' \
|
|
585
|
+
"run-shell -b \"$previous_command\"" 'switch-client -p' >/dev/null 2>&1 || true
|
|
586
|
+
tmux bind-key -r -T prefix Right if-shell -F '#{@orchestra_display_name}' \
|
|
587
|
+
"run-shell -b \"$next_command\"" 'select-pane -R' >/dev/null 2>&1 || true
|
|
588
|
+
tmux bind-key -r -T prefix Left if-shell -F '#{@orchestra_display_name}' \
|
|
589
|
+
"run-shell -b \"$previous_command\"" 'select-pane -L' >/dev/null 2>&1 || true
|
|
265
590
|
}
|
|
266
591
|
|
|
267
592
|
_tmux_configure_orchestra_status() {
|
|
268
593
|
local session_name="$1"
|
|
269
594
|
local worktree_name="$2"
|
|
270
595
|
local session_display_name="${3:-}"
|
|
271
|
-
local status_left
|
|
596
|
+
local status_left status_right
|
|
272
597
|
if [[ -z "$session_display_name" ]]; then
|
|
273
598
|
session_display_name="$(tmux_format_session_display "$session_name" without-timestamp)"
|
|
274
599
|
fi
|
|
275
|
-
status_left="$(_tmux_orchestra_status_left "$worktree_name" "$session_display_name")"
|
|
600
|
+
status_left="$(_tmux_orchestra_status_left "$session_name" "$worktree_name" "$session_display_name")"
|
|
601
|
+
status_right="$(_tmux_orchestra_status_right "$worktree_name")"
|
|
276
602
|
|
|
277
603
|
tmux set-option -t "$session_name" @orchestra_display_name "$session_display_name" >/dev/null 2>&1 || true
|
|
278
604
|
tmux set-option -t "$session_name" @orchestra_worktree_name "$worktree_name" >/dev/null 2>&1 || true
|
|
605
|
+
tmux set-option -t "$session_name" status on >/dev/null 2>&1 || true
|
|
606
|
+
tmux set-option -t "$session_name" status-position bottom >/dev/null 2>&1 || true
|
|
279
607
|
tmux set-option -t "$session_name" status-left "$status_left" >/dev/null 2>&1 || true
|
|
280
|
-
tmux set-option -t "$session_name" status-left-length
|
|
608
|
+
tmux set-option -t "$session_name" status-left-length 1000 >/dev/null 2>&1 || true
|
|
609
|
+
tmux set-option -t "$session_name" status-right "$status_right" >/dev/null 2>&1 || true
|
|
610
|
+
tmux set-option -t "$session_name" status-right-length 0 >/dev/null 2>&1 || true
|
|
611
|
+
tmux set-option -t "$session_name" window-status-format "" >/dev/null 2>&1 || true
|
|
612
|
+
tmux set-option -t "$session_name" window-status-current-format "" >/dev/null 2>&1 || true
|
|
613
|
+
tmux set-option -t "$session_name" window-status-separator "" >/dev/null 2>&1 || true
|
|
281
614
|
_tmux_configure_orchestra_bindings
|
|
282
615
|
}
|
|
283
616
|
|
|
617
|
+
_tmux_refresh_orchestra_session_status() {
|
|
618
|
+
local session_name="$1"
|
|
619
|
+
local session_dir branch_name worktree_name display_name old_pwd
|
|
620
|
+
|
|
621
|
+
session_dir="$(tmux display-message -t "$session_name" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
622
|
+
branch_name=""
|
|
623
|
+
if [[ -n "$session_dir" && -d "$session_dir" ]]; then
|
|
624
|
+
old_pwd="$PWD"
|
|
625
|
+
cd "$session_dir" 2>/dev/null || true
|
|
626
|
+
branch_name="$(git_current_branch 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
627
|
+
cd "$old_pwd" 2>/dev/null || true
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
631
|
+
worktree_name="$branch_name"
|
|
632
|
+
elif [[ -n "$session_dir" && -d "$session_dir" ]]; then
|
|
633
|
+
worktree_name="$(basename "$session_dir")"
|
|
634
|
+
else
|
|
635
|
+
worktree_name="$branch_name"
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
display_name="$(tmux show-option -t "$session_name" -qv @orchestra_display_name 2>/dev/null || true)"
|
|
639
|
+
_tmux_configure_orchestra_status "$session_name" "$worktree_name" "$display_name"
|
|
640
|
+
}
|
|
641
|
+
|
|
284
642
|
# Helper: split a string by multi-char delimiter into bash array named by ref
|
|
285
643
|
# Usage: _tmux_split_by_delim "string" "::" out_array_name
|
|
286
644
|
_tmux_split_by_delim() {
|
|
@@ -355,14 +713,20 @@ tmux_create_session() {
|
|
|
355
713
|
# Get repository info from the working directory context
|
|
356
714
|
local repo_name=""
|
|
357
715
|
local branch_name=""
|
|
716
|
+
local repo_root=""
|
|
358
717
|
local old_pwd="$PWD"
|
|
718
|
+
local resolved_working_dir="$working_dir"
|
|
719
|
+
if [[ -n "$working_dir" && -d "$working_dir" ]]; then
|
|
720
|
+
resolved_working_dir="$(cd "$working_dir" 2>/dev/null && pwd -P || printf '%s' "$working_dir")"
|
|
721
|
+
fi
|
|
359
722
|
|
|
360
723
|
# Change to working directory to get accurate git info
|
|
361
|
-
cd "$
|
|
724
|
+
cd "$resolved_working_dir" 2>/dev/null || true
|
|
362
725
|
|
|
363
726
|
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
364
727
|
repo_name="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")"
|
|
365
728
|
branch_name="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
|
|
729
|
+
repo_root="$(git_shared_root 2>/dev/null || git_repo_root 2>/dev/null || true)"
|
|
366
730
|
fi
|
|
367
731
|
|
|
368
732
|
# Restore original directory
|
|
@@ -376,21 +740,24 @@ tmux_create_session() {
|
|
|
376
740
|
fi
|
|
377
741
|
|
|
378
742
|
# Create session with custom Orchestra status configuration
|
|
379
|
-
tmux new-session -Ad -s "$session_name" -c "$
|
|
743
|
+
tmux new-session -Ad -s "$session_name" -c "$resolved_working_dir" >/dev/null 2>&1 || {
|
|
380
744
|
err "Failed to create tmux session: $session_name"
|
|
381
745
|
return 1
|
|
382
746
|
}
|
|
383
747
|
|
|
748
|
+
local worktree_name=""
|
|
749
|
+
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
750
|
+
worktree_name="$branch_name"
|
|
751
|
+
else
|
|
752
|
+
worktree_name="$(basename "$resolved_working_dir")"
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
if [[ -n "$repo_root" && -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
756
|
+
_tmux_registry_upsert_session "$repo_root" "$branch_name" "$resolved_working_dir" "$session_name" >/dev/null 2>&1 || true
|
|
757
|
+
fi
|
|
758
|
+
|
|
384
759
|
# Customize the default status bar to include Orchestra info on the left
|
|
385
760
|
if [[ -n "$repo_name" ]]; then
|
|
386
|
-
# Set custom status-left with Orchestra branding without full path
|
|
387
|
-
# Example: Go back (detach): Ctrl+b,d | Copy (scroll) mode: Ctrl+b,[ | Worktree: main
|
|
388
|
-
local worktree_name=""
|
|
389
|
-
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
390
|
-
worktree_name="$branch_name"
|
|
391
|
-
else
|
|
392
|
-
worktree_name="$(basename "$working_dir")"
|
|
393
|
-
fi
|
|
394
761
|
_tmux_configure_orchestra_status "$session_name" "$worktree_name"
|
|
395
762
|
fi
|
|
396
763
|
|
|
@@ -467,6 +834,7 @@ tmux_kill_session() {
|
|
|
467
834
|
err "Failed to kill session: $session_name"
|
|
468
835
|
return 1
|
|
469
836
|
}
|
|
837
|
+
_tmux_registry_remove_session "$session_name" >/dev/null 2>&1 || true
|
|
470
838
|
}
|
|
471
839
|
|
|
472
840
|
# Attach or switch to session
|
|
@@ -519,19 +887,7 @@ tmux_attach_session() {
|
|
|
519
887
|
"🎼 Orchestra Session | Repository: $repo_name | Branch: $branch_name" 2>/dev/null || true
|
|
520
888
|
fi
|
|
521
889
|
|
|
522
|
-
|
|
523
|
-
local worktree_name=""
|
|
524
|
-
if [[ -n "$branch_name" && "$branch_name" != "detached" ]]; then
|
|
525
|
-
worktree_name="$branch_name"
|
|
526
|
-
else
|
|
527
|
-
# Fallback to directory name of session path if available
|
|
528
|
-
if [[ -n "$session_dir" && -d "$session_dir" ]]; then
|
|
529
|
-
worktree_name="$(basename "$session_dir")"
|
|
530
|
-
else
|
|
531
|
-
worktree_name="$branch_name"
|
|
532
|
-
fi
|
|
533
|
-
fi
|
|
534
|
-
_tmux_configure_orchestra_status "$sess" "$worktree_name"
|
|
890
|
+
_tmux_refresh_orchestra_session_status "$sess"
|
|
535
891
|
|
|
536
892
|
if tmux_inside_session; then
|
|
537
893
|
tmux switch-client -t "$sess" >/dev/null 2>&1 || true
|
|
@@ -609,6 +965,7 @@ tmux_rename_session() {
|
|
|
609
965
|
err "Failed to rename session"
|
|
610
966
|
return 1
|
|
611
967
|
}
|
|
968
|
+
_tmux_registry_rename_session "$old_session" "$new_session" "$(format_session_display_name "$new_name")" >/dev/null 2>&1 || true
|
|
612
969
|
local worktree_name
|
|
613
970
|
worktree_name="$(tmux show-option -t "$new_session" -qv @orchestra_worktree_name 2>/dev/null || true)"
|
|
614
971
|
if [[ -z "$worktree_name" ]]; then
|
|
@@ -1044,6 +1401,157 @@ tmux_send_keys() {
|
|
|
1044
1401
|
tmux send-keys -t "$session_name" C-m 2>/dev/null || return 1
|
|
1045
1402
|
}
|
|
1046
1403
|
|
|
1404
|
+
# Show Orchestra tmux shortcuts in a popup modal.
|
|
1405
|
+
# Usage: tmux_show_orchestra_help_popup [client_tty]
|
|
1406
|
+
tmux_show_orchestra_help_popup() {
|
|
1407
|
+
local target_client="${1:-}"
|
|
1408
|
+
if ! tmux_available; then
|
|
1409
|
+
err "tmux not installed"
|
|
1410
|
+
return 1
|
|
1411
|
+
fi
|
|
1412
|
+
|
|
1413
|
+
local target_args=()
|
|
1414
|
+
if [[ -n "$target_client" ]]; then
|
|
1415
|
+
target_args=(-c "$target_client")
|
|
1416
|
+
fi
|
|
1417
|
+
|
|
1418
|
+
local popup_command
|
|
1419
|
+
popup_command='printf "%s\n" \
|
|
1420
|
+
"Orchestra tmux shortcuts" \
|
|
1421
|
+
"" \
|
|
1422
|
+
"Ctrl+b, Left Previous Orchestra session in this workspace" \
|
|
1423
|
+
"Ctrl+b, Right Next Orchestra session in this workspace" \
|
|
1424
|
+
"Ctrl+b, < Previous Orchestra session in this workspace" \
|
|
1425
|
+
"Ctrl+b, > Next Orchestra session in this workspace" \
|
|
1426
|
+
"Ctrl+b, r Rename the current Orchestra session" \
|
|
1427
|
+
"Ctrl+b, d Detach and return to Orchestra" \
|
|
1428
|
+
"Ctrl+b, [ Copy/scroll mode" \
|
|
1429
|
+
"Ctrl+b, ? Show this help" \
|
|
1430
|
+
"" \
|
|
1431
|
+
"Press Enter to close..."; read -r _'
|
|
1432
|
+
|
|
1433
|
+
tmux display-popup "${target_args[@]}" -E -w 78 -h 15 -T "Orchestra shortcuts" "$popup_command" >/dev/null 2>&1
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
# Find the adjacent active Orchestra session registered for the current repo.
|
|
1437
|
+
# Usage: tmux_workspace_cycle_target <current_session> <next|previous>
|
|
1438
|
+
tmux_workspace_cycle_target() {
|
|
1439
|
+
local current_session="$1"
|
|
1440
|
+
local direction="$2"
|
|
1441
|
+
local session_dir repo_root db_path active_file target
|
|
1442
|
+
|
|
1443
|
+
if ! tmux_available; then
|
|
1444
|
+
err "tmux not installed"
|
|
1445
|
+
return 1
|
|
1446
|
+
fi
|
|
1447
|
+
if [[ -z "$current_session" ]]; then
|
|
1448
|
+
err "tmux_workspace_cycle_target: current session required"
|
|
1449
|
+
return 1
|
|
1450
|
+
fi
|
|
1451
|
+
case "$direction" in
|
|
1452
|
+
next|previous|prev) ;;
|
|
1453
|
+
*)
|
|
1454
|
+
err "tmux_workspace_cycle_target: direction must be next or previous"
|
|
1455
|
+
return 1
|
|
1456
|
+
;;
|
|
1457
|
+
esac
|
|
1458
|
+
|
|
1459
|
+
_tmux_registry_upsert_current_session "$current_session" >/dev/null 2>&1 || true
|
|
1460
|
+
|
|
1461
|
+
session_dir="$(tmux display-message -t "$current_session" -p '#{pane_current_path}' 2>/dev/null || echo "")"
|
|
1462
|
+
repo_root="$(_tmux_shared_root_for_path "$session_dir" 2>/dev/null || true)"
|
|
1463
|
+
if [[ -z "$repo_root" ]]; then
|
|
1464
|
+
err "Unable to determine Orchestra workspace for session"
|
|
1465
|
+
return 1
|
|
1466
|
+
fi
|
|
1467
|
+
_tmux_registry_sync_active_workspace_sessions "$repo_root" >/dev/null 2>&1 || true
|
|
1468
|
+
|
|
1469
|
+
db_path="$(_tmux_session_registry_path)"
|
|
1470
|
+
if [[ ! -f "$db_path" ]]; then
|
|
1471
|
+
err "No Orchestra session registry found"
|
|
1472
|
+
return 1
|
|
1473
|
+
fi
|
|
1474
|
+
have_cmd python3 || {
|
|
1475
|
+
err "python3 is required to read the Orchestra session registry"
|
|
1476
|
+
return 1
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
active_file="$(mktemp)"
|
|
1480
|
+
tmux list-sessions -F '#{session_name}' > "$active_file" 2>/dev/null || {
|
|
1481
|
+
rm -f "$active_file"
|
|
1482
|
+
err "Unable to list tmux sessions"
|
|
1483
|
+
return 1
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
target="$(python3 - "$db_path" "$repo_root" "$current_session" "$direction" "$active_file" <<'PY'
|
|
1487
|
+
import sqlite3
|
|
1488
|
+
import sys
|
|
1489
|
+
|
|
1490
|
+
db_path, repo_root, current_session, direction, active_path = sys.argv[1:6]
|
|
1491
|
+
with open(active_path, "r", encoding="utf-8") as handle:
|
|
1492
|
+
active = {line.strip() for line in handle if line.strip()}
|
|
1493
|
+
|
|
1494
|
+
conn = sqlite3.connect(db_path)
|
|
1495
|
+
rows = conn.execute(
|
|
1496
|
+
"""
|
|
1497
|
+
SELECT tmux_name
|
|
1498
|
+
FROM sessions
|
|
1499
|
+
WHERE repo_root = ?1
|
|
1500
|
+
ORDER BY worktree_path COLLATE NOCASE, created_at, tmux_name COLLATE NOCASE
|
|
1501
|
+
""",
|
|
1502
|
+
(repo_root,),
|
|
1503
|
+
).fetchall()
|
|
1504
|
+
|
|
1505
|
+
names = []
|
|
1506
|
+
seen = set()
|
|
1507
|
+
for (name,) in rows:
|
|
1508
|
+
if name in active and name not in seen:
|
|
1509
|
+
names.append(name)
|
|
1510
|
+
seen.add(name)
|
|
1511
|
+
|
|
1512
|
+
if len(names) < 2 or current_session not in seen:
|
|
1513
|
+
raise SystemExit(0)
|
|
1514
|
+
|
|
1515
|
+
index = names.index(current_session)
|
|
1516
|
+
if direction == "next":
|
|
1517
|
+
index = (index + 1) % len(names)
|
|
1518
|
+
else:
|
|
1519
|
+
index = (index - 1) % len(names)
|
|
1520
|
+
print(names[index])
|
|
1521
|
+
PY
|
|
1522
|
+
)"
|
|
1523
|
+
local query_status=$?
|
|
1524
|
+
rm -f "$active_file"
|
|
1525
|
+
if [[ $query_status -ne 0 ]]; then
|
|
1526
|
+
err "Unable to query Orchestra session registry"
|
|
1527
|
+
return 1
|
|
1528
|
+
fi
|
|
1529
|
+
[[ -n "$target" ]] || return 1
|
|
1530
|
+
printf '%s\n' "$target"
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
# Switch the current tmux client to an adjacent registered Orchestra session.
|
|
1534
|
+
# Usage: tmux_cycle_workspace_session <current_session> <next|previous> [client_tty]
|
|
1535
|
+
tmux_cycle_workspace_session() {
|
|
1536
|
+
local current_session="$1"
|
|
1537
|
+
local direction="$2"
|
|
1538
|
+
local target_client="${3:-}"
|
|
1539
|
+
local target_session
|
|
1540
|
+
|
|
1541
|
+
if ! target_session="$(tmux_workspace_cycle_target "$current_session" "$direction")"; then
|
|
1542
|
+
tmux display-message -d 2500 "No other Orchestra sessions in this workspace" 2>/dev/null || true
|
|
1543
|
+
return 1
|
|
1544
|
+
fi
|
|
1545
|
+
|
|
1546
|
+
_tmux_refresh_orchestra_session_status "$target_session" >/dev/null 2>&1 || true
|
|
1547
|
+
|
|
1548
|
+
if [[ -n "$target_client" ]]; then
|
|
1549
|
+
tmux switch-client -c "$target_client" -t "$target_session" >/dev/null 2>&1 || return 1
|
|
1550
|
+
else
|
|
1551
|
+
tmux switch-client -t "$target_session" >/dev/null 2>&1 || return 1
|
|
1552
|
+
fi
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1047
1555
|
# Load .env file if it exists
|
|
1048
1556
|
# Usage: tmux_load_env_file [env_file_path]
|
|
1049
1557
|
tmux_load_env_file() {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -109,6 +109,14 @@ case "${1:-}" in
|
|
|
109
109
|
"tmux-send-keys")
|
|
110
110
|
bridge_tmux_send_keys "${2:-}" "${@:3}"
|
|
111
111
|
;;
|
|
112
|
+
|
|
113
|
+
"cycle-workspace-session")
|
|
114
|
+
bridge_cycle_workspace_session "${2:-}" "${3:-}" "${4:-}"
|
|
115
|
+
;;
|
|
116
|
+
|
|
117
|
+
"tmux-help-popup")
|
|
118
|
+
bridge_tmux_help_popup "${2:-}"
|
|
119
|
+
;;
|
|
112
120
|
|
|
113
121
|
"session-metadata")
|
|
114
122
|
bridge_session_metadata "${2:-}"
|
|
@@ -233,6 +241,9 @@ Commands:
|
|
|
233
241
|
delete-branch-only <branch> Delete branch only (keep worktree)
|
|
234
242
|
switch-worktree <path> Generate cd command for path
|
|
235
243
|
copy-env-files <source> <target> Copy .env files between directories
|
|
244
|
+
cycle-workspace-session <session> <next|previous> [client]
|
|
245
|
+
Switch within registered Orchestra sessions for this repo
|
|
246
|
+
tmux-help-popup [client] Show Orchestra tmux shortcut popup
|
|
236
247
|
repo-info Get repository information
|
|
237
248
|
git-status Get git status --porcelain
|
|
238
249
|
enhanced-git-status [dir] Get enhanced git status with file stats and line counts
|
|
@@ -112,6 +112,42 @@ bridge_tmux_send_keys() {
|
|
|
112
112
|
fi
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
# Cycle to next/previous registered Orchestra session in the current workspace
|
|
116
|
+
bridge_cycle_workspace_session() {
|
|
117
|
+
if [[ -z "${1:-}" ]] || [[ -z "${2:-}" ]]; then
|
|
118
|
+
json_error "Session name and direction required"
|
|
119
|
+
return 1
|
|
120
|
+
fi
|
|
121
|
+
session_name="$1"
|
|
122
|
+
direction="$2"
|
|
123
|
+
target_client="${3:-}"
|
|
124
|
+
|
|
125
|
+
if tmux_available; then
|
|
126
|
+
tmux_cycle_workspace_session "$session_name" "$direction" "$target_client" || {
|
|
127
|
+
json_error "Failed to cycle workspace session"
|
|
128
|
+
return 1
|
|
129
|
+
}
|
|
130
|
+
echo '{"ok":true}'
|
|
131
|
+
else
|
|
132
|
+
json_error "tmux not available"
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Show Orchestra tmux shortcut help popup
|
|
137
|
+
bridge_tmux_help_popup() {
|
|
138
|
+
target_client="${1:-}"
|
|
139
|
+
|
|
140
|
+
if tmux_available; then
|
|
141
|
+
tmux_show_orchestra_help_popup "$target_client" || {
|
|
142
|
+
json_error "Failed to show tmux help popup"
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
echo '{"ok":true}'
|
|
146
|
+
else
|
|
147
|
+
json_error "tmux not available"
|
|
148
|
+
fi
|
|
149
|
+
}
|
|
150
|
+
|
|
115
151
|
# Get session metadata
|
|
116
152
|
bridge_session_metadata() {
|
|
117
153
|
if [[ -z "${1:-}" ]]; then
|