//go:build !windows package infra import ( "fmt" "strconv" "strings" ) // TmuxSwapWindowIntoConsole trae el primer pane de al pane derecho // de la window "console" de (al lado del pane 0 = la TUI), parkeando // el Claude que estuviera a la derecha en su propia window (detached, sin robar // foco), y re-fija el ancho del pane 0 (TUI) a 40 columnas. // // Contrato de la window console: // - pane indice 0 = siempre la TUI fleetview (no se toca). // - cualquier otro pane en console = el Claude activo (puede no haber ninguno). // // Idempotente: si ES ya la window console, no hace nada. Si el Claude // objetivo ya esta en console, tampoco rompe nada (el break-pane se aplica al // pane derecho que estuviera; si no lo hay, se salta). Opera SIEMPRE sobre el // socket aislado pasado como parametro (tmux -L ). func TmuxSwapWindowIntoConsole(socket, session, windowID string) error { if socket == "" { return fmt.Errorf("tmux_swap_window_into_console: socket vacio") } if session == "" { return fmt.Errorf("tmux_swap_window_into_console: session vacia") } if windowID == "" { return fmt.Errorf("tmux_swap_window_into_console: windowID vacio") } consoleTarget := session + ":console" // Capturar el ancho ACTUAL del pane 0 (la TUI) antes de tocar nada, para // preservarlo tras el break/join (que redistribuyen el espacio). Así el ancho // del sidebar lo decide quien creó la sesión (launch_kittyclaude), no un valor // fijo aquí. width := tmuxPane0Width(socket, session) // Caso borde: si windowID ya ES la window console, no hay nada que hacer. // Resolvemos el window_id real de console y lo comparamos con el pedido. consoleID, err := tmuxConsoleWindowID(socket, session) if err != nil { return err } if consoleID == windowID { // El objetivo ya es console. Solo re-fijamos el ancho de la TUI. return tmuxResizeConsoleTUI(socket, session, width) } // 1. Localiza el pane derecho actual de console (cualquier pane con indice != 0). out, stderr, err := runTmux(socket, "list-panes", "-t", consoleTarget, "-F", "#{pane_index} #{pane_id}") if err != nil { return fmt.Errorf("tmux_swap_window_into_console: list-panes de %q: %w (%s)", consoleTarget, err, stderr) } var rightPaneID string for _, line := range strings.Split(strings.TrimSpace(out), "\n") { line = strings.TrimSpace(line) if line == "" { continue } fields := strings.Fields(line) if len(fields) < 2 { continue } if fields[0] != "0" { rightPaneID = fields[1] break } } // 2. Si existe un pane no-0 en console, sacarlo a su propia window (parking), // detached y sin cambiar foco. if rightPaneID != "" { if _, stderr, err := runTmux(socket, "break-pane", "-d", "-s", rightPaneID); err != nil { return fmt.Errorf("tmux_swap_window_into_console: break-pane de %q: %w (%s)", rightPaneID, err, stderr) } } // 3. Traer el primer pane de windowID a la derecha de la TUI (-h = split // horizontal, lado a lado). join-pane requiere que origen y destino sean // windows distintas (ya garantizado: consoleID != windowID arriba). src := windowID + ".0" dst := consoleTarget + ".0" if _, stderr, err := runTmux(socket, "join-pane", "-h", "-s", src, "-t", dst); err != nil { return fmt.Errorf("tmux_swap_window_into_console: join-pane %q -> %q: %w (%s)", src, dst, err, stderr) } // 4. Re-fijar el ancho del pane 0 (TUI) al que tenia antes del swap. return tmuxResizeConsoleTUI(socket, session, width) } // tmuxConsoleWindowID resuelve el window_id (ej "@3") de la window llamada // "console" en . func tmuxConsoleWindowID(socket, session string) (string, error) { out, stderr, err := runTmux(socket, "list-windows", "-t", session, "-F", "#{window_id} #{window_name}") if err != nil { return "", fmt.Errorf("tmux_swap_window_into_console: list-windows de %q: %w (%s)", session, err, stderr) } for _, line := range strings.Split(strings.TrimSpace(out), "\n") { fields := strings.Fields(strings.TrimSpace(line)) if len(fields) < 2 { continue } if fields[1] == "console" { return fields[0], nil } } return "", fmt.Errorf("tmux_swap_window_into_console: window 'console' no encontrada en %q", session) } // tmuxPane0Width devuelve el ancho a preservar para el pane 0 (la TUI). Solo // tiene sentido leer el ancho actual si console ya tiene >1 pane (TUI + Claude); // con un único pane, el pane 0 es full-width y no representa el sidebar, así que // se usa el default (47 columnas). func tmuxPane0Width(socket, session string) int { const def = 47 out, _, err := runTmux(socket, "list-panes", "-t", session+":console", "-F", "#{pane_index} #{pane_width}") if err != nil { return def } lines := strings.Split(strings.TrimSpace(out), "\n") if len(lines) <= 1 { return def } for _, l := range lines { f := strings.Fields(strings.TrimSpace(l)) if len(f) >= 2 && f[0] == "0" { if n, e := strconv.Atoi(f[1]); e == nil && n > 0 { return n } } } return def } // tmuxResizeConsoleTUI fija el ancho del pane 0 de console a width columnas. func tmuxResizeConsoleTUI(socket, session string, width int) error { target := session + ":console.0" if _, stderr, err := runTmux(socket, "resize-pane", "-t", target, "-x", strconv.Itoa(width)); err != nil { return fmt.Errorf("tmux_swap_window_into_console: resize-pane de %q a %d col: %w (%s)", target, width, err, stderr) } return nil }