debug mode, don't print to main window, rename go module and main window

This commit is contained in:
Levi Neely 2026-02-10 21:45:27 +01:00
parent 8b80aa0fa9
commit 6cb61a7a32
12 changed files with 102 additions and 90 deletions

BIN
acme-q

Binary file not shown.

BIN
anvillm Executable file

Binary file not shown.

2
go.mod
View File

@ -1,4 +1,4 @@
module acme-q
module anvillm
go 1.21

View File

@ -2,7 +2,8 @@
package pty
import (
"acme-q/internal/backend"
"anvillm/internal/debug"
"anvillm/internal/backend"
"bytes"
"context"
"fmt"
@ -133,7 +134,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
sess.state = "idle"
sess.output.Reset()
log.Printf("[session %s] ready (pid=%d)", sess.ID(), sess.pid)
debug.Log("[session %s] ready (pid=%d)", sess.ID(), sess.pid)
return sess, nil
}
@ -223,7 +224,7 @@ func (s *Session) reader() {
n, err := s.ptmx.Read(buf)
if err != nil {
if err != io.EOF {
log.Printf("[session %s] reader error: %v", s.id, err)
debug.Log("[session %s] reader error: %v", s.id, err)
}
close(s.dataCh)
// Reap the process
@ -313,7 +314,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
s.state = "running"
s.output.Reset()
log.Printf("[session %s] sending: %q", s.id, prompt)
debug.Log("[session %s] sending: %q", s.id, prompt)
if _, err := s.ptmx.Write([]byte(prompt + "\r")); err != nil {
s.state = "idle"
return "", fmt.Errorf("write failed: %w", err)
@ -329,7 +330,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
// Clean output
result := s.cleaner.Clean(prompt, s.output.String())
log.Printf("[session %s] response: %d bytes", s.id, len(result))
debug.Log("[session %s] response: %d bytes", s.id, len(result))
return result, nil
}
@ -346,7 +347,6 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error {
return fmt.Errorf("session closed")
}
s.output.Write(data)
log.Printf("[session %s] +%d bytes, total %d", s.id, len(data), s.output.Len())
if s.detector.IsComplete(prompt, s.output.String()) {
// Drain any remaining data
@ -357,7 +357,7 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error {
s.output.Write(extra)
}
case <-time.After(10 * time.Millisecond):
log.Printf("[session %s] response complete, %d bytes", s.id, s.output.Len())
debug.Log("[session %s] response complete, %d bytes", s.id, s.output.Len())
return nil
}
}

View File

@ -1,12 +1,12 @@
package tmux
import (
"acme-q/internal/backend"
"anvillm/internal/backend"
"anvillm/internal/debug"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
@ -110,7 +110,7 @@ func (s *Session) reader() {
if err != nil {
// Only log unexpected errors (not EOF or closed file during shutdown)
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
log.Printf("[session %s] reader error: %v", s.id, err)
debug.Log("[session %s] reader error: %v", s.id, err)
}
close(s.dataCh)
s.mu.Lock()
@ -141,19 +141,17 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error
}
s.output.Write(data)
log.Printf("[session %s] startup: %d bytes accumulated", s.id, s.output.Len())
// If we have a startup handler and it hasn't completed, call it
if !startupDone && s.startupHandler != nil {
keys, done := s.startupHandler.HandleStartup(s.output.String())
if keys != "" {
log.Printf("[session %s] startup handler sending keys: %q", s.id, keys)
debug.Log("[session %s] startup handler sending keys: %q", s.id, keys)
if err := sendKeys(s.target(), keys); err != nil {
return fmt.Errorf("failed to send startup response: %w", err)
}
}
if done {
log.Printf("[session %s] startup handler complete", s.id)
debug.Log("[session %s] startup handler complete", s.id)
startupDone = true
s.output.Reset() // Reset output after startup handling
}
@ -161,12 +159,12 @@ func (s *Session) waitForReady(ctx context.Context, timeout time.Duration) error
// Check if ready (only after startup is done)
if startupDone && s.detector.IsReady(s.output.String()) {
log.Printf("[session %s] ready detected", s.id)
debug.Log("[session %s] ready detected", s.id)
return nil
}
case <-time.After(time.Until(deadline)):
if time.Now().After(deadline) {
log.Printf("[session %s] timeout, current output: %s", s.id, s.output.String())
debug.Log("[session %s] timeout, current output: %s", s.id, s.output.String())
return fmt.Errorf("timeout waiting for ready")
}
}
@ -214,7 +212,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
// Release lock during long-running operations so state can be read
s.mu.Unlock()
log.Printf("[session %s] sending: %q", s.id, prompt)
debug.Log("[session %s] sending: %q", s.id, prompt)
// Send prompt using literal mode to avoid special character interpretation
if err := sendLiteral(s.target(), prompt); err != nil {
@ -250,9 +248,9 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
rawLen := s.output.Len()
rawOutput := s.output.String()
result := s.cleaner.Clean(prompt, rawOutput)
log.Printf("[session %s] raw: %d bytes, cleaned: %d bytes", s.id, rawLen, len(result))
debug.Log("[session %s] raw: %d bytes, cleaned: %d bytes", s.id, rawLen, len(result))
if len(result) == 0 && rawLen > 0 {
log.Printf("[session %s] WARNING: cleaner returned 0 bytes", s.id)
debug.Log("[session %s] WARNING: cleaner returned 0 bytes", s.id)
// Debug: show first and last 500 chars of raw output
debugFirst := rawOutput
if len(debugFirst) > 500 {
@ -262,13 +260,13 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
if len(debugLast) > 500 {
debugLast = debugLast[len(debugLast)-500:]
}
log.Printf("[session %s] DEBUG raw output (first 500 chars): %q", s.id, debugFirst)
log.Printf("[session %s] DEBUG raw output (last 500 chars): %q", s.id, debugLast)
debug.Log("[session %s] DEBUG raw output (first 500 chars): %q", s.id, debugFirst)
debug.Log("[session %s] DEBUG raw output (last 500 chars): %q", s.id, debugLast)
// Check for key markers (simple check, no ANSI stripping)
hasBullet := strings.Contains(rawOutput, "●")
hasThinking := strings.Contains(rawOutput, "(esc to interrupt)") || strings.Contains(rawOutput, "∴")
log.Printf("[session %s] DEBUG markers: hasBullet=%v, hasThinking=%v", s.id, hasBullet, hasThinking)
debug.Log("[session %s] DEBUG markers: hasBullet=%v, hasThinking=%v", s.id, hasBullet, hasThinking)
// Find where the bullet is
if hasBullet {
@ -281,7 +279,7 @@ func (s *Session) Send(ctx context.Context, prompt string) (string, error) {
if end > len(rawOutput) {
end = len(rawOutput)
}
log.Printf("[session %s] DEBUG bullet context: %q", s.id, rawOutput[start:end])
debug.Log("[session %s] DEBUG bullet context: %q", s.id, rawOutput[start:end])
}
}
@ -300,7 +298,6 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error {
return fmt.Errorf("session closed")
}
s.output.Write(data)
log.Printf("[session %s] +%d bytes, total %d", s.id, len(data), s.output.Len())
if s.detector.IsComplete(prompt, s.output.String()) {
// Drain any remaining data
@ -311,7 +308,7 @@ func (s *Session) waitForComplete(ctx context.Context, prompt string) error {
s.output.Write(extra)
}
case <-time.After(10 * time.Millisecond):
log.Printf("[session %s] response complete, %d bytes", s.id, s.output.Len())
debug.Log("[session %s] response complete, %d bytes", s.id, s.output.Len())
return nil
}
}
@ -352,14 +349,14 @@ func (s *Session) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
log.Printf("[session %s] Close() called, killing window %s:%s", s.id, s.tmuxSession, s.windowName)
debug.Log("[session %s] Close() called, killing window %s:%s", s.id, s.tmuxSession, s.windowName)
// 1. Kill tmux window first (closes pipe-pane writer)
if s.tmuxSession != "" && s.windowName != "" {
if err := killWindow(s.tmuxSession, s.windowName); err != nil {
log.Printf("[session %s] killWindow failed: %v", s.id, err)
debug.Log("[session %s] killWindow failed: %v", s.id, err)
} else {
log.Printf("[session %s] Window %s:%s killed", s.id, s.tmuxSession, s.windowName)
debug.Log("[session %s] Window %s:%s killed", s.id, s.tmuxSession, s.windowName)
}
}

View File

@ -2,10 +2,10 @@
package tmux
import (
"acme-q/internal/backend"
"anvillm/internal/backend"
"anvillm/internal/debug"
"context"
"fmt"
"log"
"os"
"sync/atomic"
"syscall"
@ -84,7 +84,7 @@ func (b *Backend) ensureTmuxSession() error {
return nil
}
log.Printf("[backend %s] creating persistent tmux session: %s", b.cfg.Name, b.tmuxSession)
debug.Log("[backend %s] creating persistent tmux session: %s", b.cfg.Name, b.tmuxSession)
// Keep window 0 alive to hold the session open
// It will be killed when the program exits
return createSession(b.tmuxSession, b.cfg.TmuxSize.Rows, b.cfg.TmuxSize.Cols)
@ -93,7 +93,7 @@ func (b *Backend) ensureTmuxSession() error {
// Cleanup kills the persistent tmux session (call on program exit)
func (b *Backend) Cleanup() error {
if sessionExists(b.tmuxSession) {
log.Printf("[backend %s] cleaning up tmux session: %s", b.cfg.Name, b.tmuxSession)
debug.Log("[backend %s] cleaning up tmux session: %s", b.cfg.Name, b.tmuxSession)
return killSession(b.tmuxSession)
}
return nil
@ -107,7 +107,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
id := fmt.Sprintf("%d", atomic.AddUint64(&b.counter, 1))
windowName := id // Use session ID as window name
log.Printf("[session %s] creating window in tmux session %s", id, b.tmuxSession)
debug.Log("[session %s] creating window in tmux session %s", id, b.tmuxSession)
// 1. Ensure persistent tmux session exists
if err := b.ensureTmuxSession(); err != nil {
@ -204,7 +204,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
// 9. Get PID
pid, err := getPanePID(target)
if err != nil {
log.Printf("[session %s] warning: failed to get PID: %v", id, err)
debug.Log("[session %s] warning: failed to get PID: %v", id, err)
pid = 0
}
@ -237,7 +237,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio
sess.state = "idle"
sess.output.Reset()
log.Printf("[session %s] ready (tmux=%s:%s, pid=%d)", sess.ID(), b.tmuxSession, windowName, sess.pid)
debug.Log("[session %s] ready (tmux=%s:%s, pid=%d)", sess.ID(), b.tmuxSession, windowName, sess.pid)
return sess, nil
}

View File

@ -2,8 +2,8 @@
package backends
import (
"acme-q/internal/backend"
"acme-q/internal/backend/tmux"
"anvillm/internal/backend"
"anvillm/internal/backend/tmux"
"bufio"
"context"
"fmt"

View File

@ -2,11 +2,11 @@
package backends
import (
"acme-q/internal/backend"
"acme-q/internal/backend/tmux"
"anvillm/internal/backend"
"anvillm/internal/backend/tmux"
"anvillm/internal/debug"
"context"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
@ -413,7 +413,7 @@ func SaveWithSmartFilename(sess backend.Session, id string, bodyText string) (st
smartPath, err := GenerateSmartFilename(tempPath, bodyText)
if err != nil {
log.Printf("[session %s] Smart filename failed: %v, keeping %s", id, err, tempPath)
debug.Log("[session %s] Smart filename failed: %v, keeping %s", id, err, tempPath)
return tempPath, nil
}
@ -421,7 +421,7 @@ func SaveWithSmartFilename(sess backend.Session, id string, bodyText string) (st
if err := os.Rename(tempPath, smartPath); err == nil {
return smartPath, nil
} else {
log.Printf("[session %s] Rename failed: %v", id, err)
debug.Log("[session %s] Rename failed: %v", id, err)
return tempPath, nil
}
}

13
internal/debug/debug.go Normal file
View File

@ -0,0 +1,13 @@
package debug
import "log"
// Enabled controls whether debug logs are printed
var Enabled = false
// Log prints a log message only when debug mode is enabled
func Log(format string, args ...interface{}) {
if Enabled {
log.Printf(format, args...)
}
}

View File

@ -2,10 +2,10 @@
package p9
import (
"acme-q/internal/backend"
"acme-q/internal/backend/tmux"
"acme-q/internal/backends"
"acme-q/internal/session"
"anvillm/internal/backend"
"anvillm/internal/backend/tmux"
"anvillm/internal/backends"
"anvillm/internal/session"
"context"
"errors"
"fmt"

View File

@ -2,7 +2,7 @@
package session
import (
"acme-q/internal/backend"
"anvillm/internal/backend"
"context"
"sync"
)

86
main.go
View File

@ -2,11 +2,12 @@
package main
import (
"acme-q/internal/backend"
"acme-q/internal/backend/tmux"
"acme-q/internal/backends"
"acme-q/internal/p9"
"acme-q/internal/session"
"anvillm/internal/backend"
"anvillm/internal/backend/tmux"
"anvillm/internal/backends"
"anvillm/internal/debug"
"anvillm/internal/p9"
"anvillm/internal/session"
"context"
"flag"
"fmt"
@ -23,7 +24,7 @@ import (
"9fans.net/go/acme"
)
const windowName = "/Q/"
const windowName = "/AnviLLM/"
// Terminal to use for tmux attach (configurable)
// Common options: "foot", "kitty", "xterm", "st", "alacritty"
@ -46,10 +47,10 @@ func main() {
switch backendName {
case "claude":
backend = backends.NewClaude()
log.Printf("Using claude backend")
debug.Log("Using claude backend")
case "kiro-cli":
backend = backends.NewKiroCLI()
log.Printf("Using kiro-cli backend")
debug.Log("Using kiro-cli backend")
default:
log.Fatalf("Unknown backend: %s (available: kiro-cli, claude)", backendName)
}
@ -127,47 +128,47 @@ func main() {
switch cmd {
case "New":
// Use current directory if no path specified
cwd := arg
if cwd == "" {
cwd, _ = os.Getwd()
// Require path argument
if arg == "" {
fmt.Fprintf(os.Stderr, "Error: New requires a path argument\n")
continue
}
sess, err := mgr.New(cwd)
sess, err := mgr.New(arg)
if err != nil {
w.Fprintf("body", "Error: %v\n", err)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
continue
}
_, err = openChatWindow(sess)
if err != nil {
w.Fprintf("body", "Error opening chat: %v\n", err)
fmt.Fprintf(os.Stderr, "Error opening chat: %v\n", err)
continue
}
// WinID is set inside openChatWindow
refreshList(w, mgr)
case "Open":
if arg == "" {
w.Fprintf("body", "Usage: Open <session-id>\n")
fmt.Fprintf(os.Stderr, "Usage: Open <session-id>\n")
continue
}
sess := mgr.Get(arg)
if sess == nil {
w.Fprintf("body", "Session not found: %s\n", arg)
fmt.Fprintf(os.Stderr, "Session not found: %s\n", arg)
continue
}
_, err := openChatWindow(sess)
if err != nil {
w.Fprintf("body", "Error opening chat: %v\n", err)
fmt.Fprintf(os.Stderr, "Error opening chat: %v\n", err)
continue
}
// WinID is set inside openChatWindow
case "Kill":
if arg == "" {
w.Fprintf("body", "Usage: Kill <pid>\n")
fmt.Fprintf(os.Stderr, "Usage: Kill <pid>\n")
continue
}
pid, err := strconv.Atoi(arg)
if err != nil {
w.Fprintf("body", "Invalid pid: %s\n", arg)
fmt.Fprintf(os.Stderr, "Invalid pid: %s\n", arg)
continue
}
// Find session by pid and kill it
@ -178,9 +179,9 @@ func main() {
if meta.Pid == pid {
// First kill the process
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
log.Printf("[session %s] Failed to kill PID %d: %v", id, pid, err)
debug.Log("[session %s] Failed to kill PID %d: %v", id, pid, err)
} else {
log.Printf("[session %s] Killed PID %d", id, pid)
debug.Log("[session %s] Killed PID %d", id, pid)
}
// Then close the tmux window
sess.Close()
@ -192,22 +193,23 @@ func main() {
refreshList(w, mgr)
case "Get":
refreshList(w, mgr)
case "Debug":
debug.Enabled = !debug.Enabled
state := "disabled"
if debug.Enabled {
state = "enabled"
}
fmt.Printf("Debug mode %s\n", state)
case "Login":
cmd := exec.Command("firefox")
cmd.Start()
cmd = exec.Command("foot", "-e", "q", "login")
log.Printf("starting terminal, pid will follow")
if err := cmd.Start(); err != nil {
log.Printf("terminal start error: %v", err)
} else {
log.Printf("terminal started, pid=%d", cmd.Process.Pid)
}
cmd.Start()
default:
w.WriteEvent(e)
}
case 'l', 'L':
text := strings.TrimSpace(string(e.Text))
log.Printf("[/Q/] Look: text=%q", text)
if sess := mgr.Get(text); sess != nil {
// Try to focus existing window, or open new one
meta := sess.Metadata()
@ -315,9 +317,9 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
case "Stop":
ctx := context.Background()
if err := sess.Stop(ctx); err != nil {
log.Printf("[session %s] Stop error: %v", sess.ID(), err)
debug.Log("[session %s] Stop error: %v", sess.ID(), err)
} else {
log.Printf("[session %s] Sent CTRL+C", sess.ID())
debug.Log("[session %s] Sent CTRL+C", sess.ID())
}
case "Attach":
// Attach to tmux window (only works for tmux-based backends)
@ -330,23 +332,23 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
target := fmt.Sprintf("%s:%s", tmuxSession, tmuxWindow)
cmd := exec.Command(terminalCommand, "-e", "tmux", "attach", "-t", target)
if err := cmd.Start(); err != nil {
log.Printf("[session %s] Failed to launch terminal: %v", sess.ID(), err)
debug.Log("[session %s] Failed to launch terminal: %v", sess.ID(), err)
} else {
log.Printf("[session %s] Launched terminal attached to window: %s", sess.ID(), target)
debug.Log("[session %s] Launched terminal attached to window: %s", sess.ID(), target)
}
}()
} else {
log.Printf("[session %s] Attach not supported: not a tmux-based backend", sess.ID())
debug.Log("[session %s] Attach not supported: not a tmux-based backend", sess.ID())
}
case "Alias":
if arg == "" {
log.Printf("[session %s] Usage: Alias <name>", sess.ID())
debug.Log("[session %s] Usage: Alias <name>", sess.ID())
continue
}
// Validate alias
matched, _ := regexp.MatchString(`^[A-Za-z0-9_-]+$`, arg)
if !matched {
log.Printf("[session %s] Invalid alias: must match [A-Za-z0-9_-]+", sess.ID())
debug.Log("[session %s] Invalid alias: must match [A-Za-z0-9_-]+", sess.ID())
continue
}
sess.SetAlias(arg)
@ -354,7 +356,7 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
meta := sess.Metadata()
newName := filepath.Join(meta.Cwd, fmt.Sprintf("+Chat.%s", arg))
w.Name(newName)
log.Printf("[session %s] alias set to %s", sess.ID(), arg)
debug.Log("[session %s] alias set to %s", sess.ID(), arg)
case "Save":
// Grab first part of window content for filename hints
var bodyText string
@ -376,10 +378,10 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
savePath, err = backends.SaveWithSmartFilename(sess, sess.ID(), bodyText)
}
if err != nil {
log.Printf("[session %s] Error saving: %v", sess.ID(), err)
debug.Log("[session %s] Error saving: %v", sess.ID(), err)
return
}
log.Printf("[session %s] Saved to %s", sess.ID(), savePath)
debug.Log("[session %s] Saved to %s", sess.ID(), savePath)
}()
case "Sessions":
openSessionsWindow(w, sess)
@ -395,14 +397,14 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
func sendPrompt(w *acme.Win, sess backend.Session) {
body, err := w.ReadAll("body")
if err != nil {
log.Printf("[session %s] Error reading body: %v", sess.ID(), err)
debug.Log("[session %s] Error reading body: %v", sess.ID(), err)
return
}
content := string(body)
idx := strings.LastIndex(content, "USER:")
if idx < 0 {
log.Printf("[session %s] No USER: section found", sess.ID())
debug.Log("[session %s] No USER: section found", sess.ID())
return
}
@ -416,7 +418,7 @@ func sendPrompt(w *acme.Win, sess backend.Session) {
ctx := context.Background()
response, err := sess.Send(ctx, prompt)
if err != nil {
log.Printf("[session %s] Error: %v", sess.ID(), err)
debug.Log("[session %s] Error: %v", sess.ID(), err)
} else {
w.Fprintf("body", "%s\n", response)
}