Files
fn_registry/functions/infra/tmux_swap_window_into_console.go
T
egutierrez 927437a8d8 feat(infra): grupo claude-fleet — FleetView TUI + orquestacion de Claudes
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una
sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI:

- list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals +
  runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId.
- list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables.
- wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console
  (preserva ancho del sidebar), tmux_map_claude_panes.
- launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r,
  mouse on, remain-on-exit off; fija el ancho del sidebar con hooks.
- docs/capabilities/claude-fleet.md + entrada en el INDEX.

Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes
varios de docs e infra de otra sesion, agrupados aqui para no perderlos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:04:41 +02:00

149 lines
5.3 KiB
Go

//go:build !windows
package infra
import (
"fmt"
"strconv"
"strings"
)
// TmuxSwapWindowIntoConsole trae el primer pane de <windowID> al pane derecho
// de la window "console" de <session> (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 <windowID> 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 <socket>).
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 <session>.
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
}