Files
fn_registry/functions/infra/pty_capture_stream.md
T
2026-06-04 23:44:39 +02:00

101 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: pty_capture_stream
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func PTYCaptureStream(ctx context.Context, name string, args []string, warmup time.Duration, inputs []string, stepDelay, snapshotInterval, idle, maxDur time.Duration) (<-chan string, error)"
description: "Lanza un comando dentro de un pseudo-terminal (PTY) y emite snapshots acumulativos del buffer de output a través de un canal, en intervalos regulares. Cada snapshot es el contenido RAW completo del PTY hasta ese instante (ANSI incluido). Permite hacer streaming del render de una TUI mientras sigue generando, sin esperar al final."
tags: ["terminal", "pty", "tui", "capture", "automation", "streaming", "terminal-capture"]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
- "context"
- "time"
- "github.com/creack/pty"
tested: true
tests:
- "snapshots crecientes con pausas"
- "snapshot final siempre presente"
- "timeout duro con sleep 10"
test_file_path: "functions/infra/pty_capture_stream_test.go"
file_path: "functions/infra/pty_capture_stream.go"
params:
- name: ctx
desc: "Contexto de cancelación. Si se cancela, la goroutine interna aborta, emite el snapshot final y cierra el canal."
- name: name
desc: "Nombre o path del ejecutable a lanzar (debe existir en PATH o ser un path absoluto)."
- name: args
desc: "Argumentos posicionales para el ejecutable. Puede ser nil o vacío."
- name: warmup
desc: "Tiempo que se espera después de arrancar el proceso antes de enviar inputs. Permite que la TUI inicialice su render. Típico: 500ms4s para CLIs lentas como claude."
- name: inputs
desc: "Lista de strings a escribir al PTY en secuencia. Incluir '\\r' al final para simular Enter. Puede ser nil si solo se quiere observar sin interactuar."
- name: stepDelay
desc: "Espera entre cada input enviado. Permite que la TUI procese y renderice la respuesta de cada paso antes de enviar el siguiente."
- name: snapshotInterval
desc: "Cada cuánto tiempo se emite un snapshot del buffer acumulado al canal. Controla la frecuencia de actualización del streaming. Valores recomendados: 100ms300ms. Por debajo de 50ms genera mucho ruido y CPU innecesario."
- name: idle
desc: "Tiempo sin nuevos bytes en el PTY que se considera 'respuesta terminada'. Cuando el terminal lleva este tiempo sin actividad, la captura finaliza. Típico: 2s4s para claude, 500ms para CLIs rápidas."
- name: maxDur
desc: "Timeout duro desde el inicio de la función. Garantiza que el canal se cierra aunque la TUI siga emitiendo (spinners, relojes, progress bars). Típico: 60s120s para prompts de claude."
output: "Canal de strings (<-chan string). Cada string es el output RAW acumulado del terminal desde el arranque hasta ese instante, con secuencias ANSI intactas (no deltas). El canal se cierra cuando termina la captura; el último valor enviado antes del cierre es siempre el snapshot final completo. Error si pty.Start falla al arrancar el proceso."
---
## Ejemplo
```go
// Streaming de una sesión claude: ver la respuesta formarse en tiempo real.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
ch, err := PTYCaptureStream(
ctx,
"claude", nil,
4*time.Second, // warmup: esperar que claude cargue
[]string{"hola, responde PONG\r"}, // inputs: enviar mensaje + Enter
300*time.Millisecond, // stepDelay entre inputs
150*time.Millisecond, // snapshotInterval: snapshot cada 150ms
4*time.Second, // idle: cortar cuando lleve 4s sin output
120*time.Second, // maxDur: timeout duro
)
if err != nil {
log.Fatal(err)
}
var lastRender string
for raw := range ch {
// Aplicar VT render para reconstruir la pantalla 2D desde ANSI.
screen := VTRender(raw) // vt_render_go_tui
// Parsear el estado actual de la respuesta de claude.
resp := ParseClaudeTUI(screen) // parse_claude_tui_go_tui
if resp.Response != lastRender {
fmt.Printf("\r[streaming] %s", resp.Response)
lastRender = resp.Response
}
}
// Al salir del for, el canal está cerrado: captura terminada.
fmt.Println("\n[done]", lastRender)
```
## Cuando usarla
Cuando quieras observar el render de una TUI **mientras sigue generando** — por ejemplo, ver la respuesta de `claude` formarse en tiempo real en lugar de esperar al final. Cada snapshot del canal es el estado completo de la pantalla en ese instante; aplica `vt_render_go_tui` + `parse_claude_tui_go_tui` para extraer texto interpretado de cada frame.
Para captura one-shot (solo quieres el output final), usa `pty_capture_idle_go_infra` — más simple, sin goroutina consumidora.
## Gotchas
- **Linux/Unix only.** Usa PTY POSIX (`creack/pty`). No funciona en Windows.
- **Snapshots ACUMULATIVOS, no deltas.** Cada string del canal es el buffer completo desde el inicio, no solo los bytes nuevos. Para calcular lo nuevo en cada tick: `delta := snapshot[len(prevSnapshot):]` — o usa `text_prefix_delta_go_core` si existe. El consumidor decide si quiere el frame completo o el incremento.
- **El consumidor DEBE drenar el canal o cancelar ctx.** Si el canal (capacidad 16) se llena y el consumidor deja de leer, la goroutina interna se bloquea. Patrón seguro: `for range ch {}` en goroutina separada si no se necesita el contenido.
- **La TUI re-renderiza el frame entero.** El buffer crudo crece monotónicamente en bytes, pero el render VT interpretado puede no ser monótono (la TUI puede limpiar la pantalla y re-dibujar). Comparar `VTRender(snapshot)` frame a frame para detectar cambios reales.
- **snapshotInterval < 50ms genera ruido.** El output ANSI de una TUI activa puede cambiar miles de veces por segundo; muestrear muy rápido satura el canal con frames casi idénticos y consume CPU innecesariamente.
- **Idle es heurístico.** Si la TUI tiene spinners o progress bars que emiten bytes continuamente, `idle` nunca se dispara y la función espera hasta `maxDur`. Subir `maxDur` o detener el spinner antes de capturar.
- **EIO/EOF al cerrar PTY es normal en Linux.** El goroutine lector lo absorbe silenciosamente.
- **SIGTERM → SIGKILL.** Al terminar, se envía SIGTERM y se espera 2s antes de SIGKILL.