Smart attach: link window when inside tmux

When 'gt X attach' is run from inside a tmux session, link the target
session's window as a new tab instead of switching sessions entirely.
Use C-b n/p to navigate between tabs.

Outside tmux: unchanged behavior (full attach)
Inside tmux: links window as tab for convenient multi-session viewing

- Add tmux.LinkWindow() and tmux.IsInsideTmux()
- Update attachToTmuxSession() with smart detection
- Update mayor, deacon, crew, refinery attach commands
This commit is contained in:
Steve Yegge
2025-12-22 15:42:31 -08:00
parent fcda775db3
commit e9587bf045
4 changed files with 35 additions and 30 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -168,8 +169,21 @@ func isInTmuxSession(targetSession string) bool {
return currentSession == targetSession
}
// attachToTmuxSession attaches to a tmux session with proper TTY forwarding.
// attachToTmuxSession attaches to a tmux session with smart behavior:
// - If already inside tmux: links the session as a new tab (use C-b n/p to switch)
// - If outside tmux: attaches normally (takes over terminal)
func attachToTmuxSession(sessionID string) error {
// If already inside tmux, link the window as a tab instead of switching sessions
if tmux.IsInsideTmux() {
t := tmux.NewTmux()
if err := t.LinkWindow(sessionID, 0); err != nil {
return fmt.Errorf("linking window: %w", err)
}
fmt.Printf("Linked %s as a new tab. Use C-b n to switch to it.\n", sessionID)
return nil
}
// Outside tmux: attach normally
tmuxPath, err := exec.LookPath("tmux")
if err != nil {
return fmt.Errorf("tmux not found: %w", err)

View File

@@ -3,7 +3,6 @@ package cmd
import (
"errors"
"fmt"
"os/exec"
"strings"
"time"
@@ -199,13 +198,8 @@ func runDeaconAttach(cmd *cobra.Command, args []string) error {
}
// Session uses a respawn loop, so Claude restarts automatically if it exits
// Use exec to replace current process with tmux attach
tmuxPath, err := exec.LookPath("tmux")
if err != nil {
return fmt.Errorf("tmux not found: %w", err)
}
return execCommand(tmuxPath, "attach-session", "-t", DeaconSessionName)
// Use shared attach helper (smart: links if inside tmux, attaches if outside)
return attachToTmuxSession(DeaconSessionName)
}
func runDeaconStatus(cmd *cobra.Command, args []string) error {

View File

@@ -3,8 +3,6 @@ package cmd
import (
"errors"
"fmt"
"os"
"os/exec"
"time"
"github.com/spf13/cobra"
@@ -179,25 +177,8 @@ func runMayorAttach(cmd *cobra.Command, args []string) error {
}
}
// Use exec to replace current process with tmux attach
tmuxPath, err := exec.LookPath("tmux")
if err != nil {
return fmt.Errorf("tmux not found: %w", err)
}
return execCommand(tmuxPath, "attach-session", "-t", MayorSessionName)
}
// execCommand replaces the current process with the given command.
// This is used for attaching to tmux sessions.
func execCommand(name string, args ...string) error {
// On Unix, we would use syscall.Exec to replace the process
// For portability, we use exec.Command and wait
cmd := exec.Command(name, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
// Use shared attach helper (smart: links if inside tmux, attaches if outside)
return attachToTmuxSession(MayorSessionName)
}
func runMayorStatus(cmd *cobra.Command, args []string) error {