@humanu/orchestra 0.5.69 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanu/orchestra",
3
- "version": "0.5.69",
3
+ "version": "0.5.72",
4
4
  "description": "AI-powered Git worktree and tmux session manager with modern TUI",
5
5
  "keywords": [
6
6
  "git",
@@ -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 worktree_name="$1"
247
- local session_display_name="$2"
248
- worktree_name="$(_tmux_status_escape_text "$worktree_name")"
249
- session_display_name="$(_tmux_status_escape_text "$session_display_name")"
250
- printf '#[fg=white,bg=colour22,bold] %s #[default] Rename: Ctrl+b,r | Go back (detach): Ctrl+b,d | Copy (scroll) mode: Ctrl+b,[ | Worktree: %s' "$session_display_name" "$worktree_name"
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 220 >/dev/null 2>&1 || true
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 "$working_dir" 2>/dev/null || true
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 "$working_dir" >/dev/null 2>&1 || {
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
- # Ensure status-left shows Orchestra help without full path
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() {
@@ -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