diff --git a/code/internal/server/pty_session.go b/code/internal/server/pty_session.go index 20399aa..6e05530 100644 --- a/code/internal/server/pty_session.go +++ b/code/internal/server/pty_session.go @@ -2,6 +2,7 @@ package server import ( "fmt" + "log" "os" "os/exec" "strings" @@ -105,17 +106,47 @@ func spawnConsolePTY(projectDir, prompt string) (*consolePTYSession, error) { return sess, nil } +// spawnShellPTY starts an interactive claude CLI in a PTY for the console. +func spawnShellPTY(projectDir string) (*consolePTYSession, error) { + cmd := exec.Command("claude") + cmd.Dir = projectDir + cmd.Env = cleanEnvForPTY() + + ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 24, Cols: 120}) + if err != nil { + return nil, fmt.Errorf("start pty: %w", err) + } + + sess := &consolePTYSession{ + ID: fmt.Sprintf("shell-%d", time.Now().UnixNano()), + Ptmx: ptmx, + Cmd: cmd, + buffer: NewRingBuffer(outputBufferSize), + subscribers: make(map[string]chan []byte), + done: make(chan struct{}), + lastActive: time.Now(), + } + + go sess.readLoop() + go sess.waitExit() + + return sess, nil +} + // readLoop reads PTY output, writes to ring buffer, and forwards to subscribers. func (s *consolePTYSession) readLoop() { buf := make([]byte, 4096) + totalBytes := 0 for { n, err := s.Ptmx.Read(buf) if err != nil { + log.Printf("PTY[%s]: readLoop ended (read %d bytes total, err: %v)", s.ID, totalBytes, err) return } if n == 0 { continue } + totalBytes += n data := make([]byte, n) copy(data, buf[:n]) @@ -123,6 +154,7 @@ func (s *consolePTYSession) readLoop() { s.mu.Lock() s.lastActive = time.Now() + subs := len(s.subscribers) for _, ch := range s.subscribers { select { case ch <- data: @@ -130,6 +162,11 @@ func (s *consolePTYSession) readLoop() { } } s.mu.Unlock() + + if totalBytes == n { + // First chunk — log it + log.Printf("PTY[%s]: first output (%d bytes, %d subscribers)", s.ID, n, subs) + } } } diff --git a/code/internal/server/ws.go b/code/internal/server/ws.go index 299ba3d..43393cc 100644 --- a/code/internal/server/ws.go +++ b/code/internal/server/ws.go @@ -24,7 +24,8 @@ type wsResizeMsg struct { Rows uint16 `json:"rows"` } -// handleConsoleWS handles WebSocket connections for console PTY sessions. +// handleConsoleWS handles WebSocket connections for console terminals. +// Each connection gets its own independent shell PTY session. func (s *Server) handleConsoleWS(c *gin.Context) { sessionNum := c.Param("session") if sessionNum != "1" && sessionNum != "2" { @@ -39,71 +40,24 @@ func (s *Server) handleConsoleWS(c *gin.Context) { } defer conn.Close() - idx := 0 - if sessionNum == "2" { - idx = 1 + log.Printf("WS[%s]: connected, spawning shell", sessionNum) + + // Spawn a fresh interactive shell for this connection + ptySess, err := spawnShellPTY(s.projectRoot()) + if err != nil { + log.Printf("WS[%s]: shell spawn error: %v", sessionNum, err) + conn.WriteMessage(websocket.TextMessage, + []byte(fmt.Sprintf("\r\n\033[31m[Greška: %v]\033[0m\r\n", err))) + return } + defer ptySess.Close() - log.Printf("WS[%s]: connected", sessionNum) + log.Printf("WS[%s]: shell started (PID %d)", sessionNum, ptySess.Cmd.Process.Pid) - session := s.console.getSession(idx) - - // Wait for PTY session to be available (it gets set when a command is executed) - session.mu.Lock() - ptySess := session.ptySess - session.mu.Unlock() - - if ptySess == nil { - log.Printf("WS[%s]: no PTY yet, polling...", sessionNum) - conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Čekam pokretanje sesije...]\033[0m\r\n")) - - // Poll for session to start (up to 30s) - ticker := time.NewTicker(300 * time.Millisecond) - defer ticker.Stop() - timeout := time.After(30 * time.Second) - for { - select { - case <-ticker.C: - session.mu.Lock() - ptySess = session.ptySess - session.mu.Unlock() - if ptySess != nil { - log.Printf("WS[%s]: PTY found after polling", sessionNum) - goto connected - } - case <-timeout: - log.Printf("WS[%s]: timeout waiting for PTY", sessionNum) - conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[31m[Timeout — sesija nije pokrenuta]\033[0m\r\n")) - return - case <-c.Request.Context().Done(): - log.Printf("WS[%s]: client disconnected while polling", sessionNum) - return - } - } - } - -connected: - log.Printf("WS[%s]: subscribing to PTY", sessionNum) subID := fmt.Sprintf("ws-%d", time.Now().UnixNano()) outputCh := ptySess.Subscribe(subID) defer ptySess.Unsubscribe(subID) - // Send buffered output for reconnect - buffered := ptySess.GetBuffer() - log.Printf("WS[%s]: sending buffer (%d bytes)", sessionNum, len(buffered)) - if len(buffered) > 0 { - conn.WriteMessage(websocket.BinaryMessage, buffered) - } - - // Check if already done - select { - case <-ptySess.Done(): - log.Printf("WS[%s]: PTY already done, sending buffer only", sessionNum) - conn.WriteMessage(websocket.TextMessage, []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n")) - return - default: - } - // Serialized write channel writeCh := make(chan []byte, 256) writeDone := make(chan struct{}) @@ -126,17 +80,18 @@ connected: } }() + // Signal to stop goroutines when read pump exits + stopCh := make(chan struct{}) + // Watch for process exit go func() { - <-ptySess.Done() - log.Printf("WS[%s]: PTY process exited", sessionNum) select { - case writeCh <- []byte("\r\n\033[33m[Sesija završena]\033[0m\r\n"): - default: + case <-ptySess.Done(): + log.Printf("WS[%s]: shell exited", sessionNum) + conn.WriteMessage(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done")) + case <-stopCh: } - time.Sleep(500 * time.Millisecond) - conn.WriteMessage(websocket.CloseMessage, - websocket.FormatCloseMessage(websocket.CloseNormalClosure, "done")) }() // WebSocket → PTY (read pump) @@ -160,11 +115,12 @@ connected: // Regular keyboard input → PTY if _, err := ptySess.WriteInput(msg); err != nil { - log.Printf("WS[%s]: PTY write error: %v", sessionNum, err) + log.Printf("WS[%s]: write error: %v", sessionNum, err) break } } + close(stopCh) close(writeCh) <-writeDone log.Printf("WS[%s]: disconnected", sessionNum) diff --git a/code/web/templates/console.html b/code/web/templates/console.html index c75cb9c..79cc649 100644 --- a/code/web/templates/console.html +++ b/code/web/templates/console.html @@ -38,8 +38,7 @@