Add -termui=false flag to prevent panic in non-TTY environments

- Added -termui flag to control terminal UI usage (defaults to true)
- Added PTY detection to automatically disable terminal UI in non-TTY environments

Sketch did some weird stuff here, so this isn't particularly co-authored
by sketch.
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 86f57da..23d6297 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -31,6 +31,8 @@
 	"sketch.dev/skabandclient"
 	"sketch.dev/skribe"
 	"sketch.dev/termui"
+
+	"golang.org/x/term"
 )
 
 func main() {
@@ -166,6 +168,7 @@
 	outsideWorkingDir string
 	sketchBinaryLinux string
 	dockerArgs        string
+	termUI            bool
 }
 
 // parseCLIFlags parses all command-line flags and returns a CLIFlags struct
@@ -191,6 +194,7 @@
 	flag.BoolVar(&flags.forceRebuild, "force-rebuild-container", false, "rebuild Docker container")
 	flag.StringVar(&flags.initialCommit, "initial-commit", "HEAD", "the git commit reference to use as starting point (incompatible with -unsafe)")
 	flag.StringVar(&flags.dockerArgs, "docker-args", "", "additional arguments to pass to the docker create command (e.g., --memory=2g --cpus=2)")
+	flag.BoolVar(&flags.termUI, "termui", true, "enable terminal UI")
 
 	// Flags geared towards sketch developers or sketch internals:
 	flag.StringVar(&flags.gitUsername, "git-username", "", "(internal) username for git commits")
@@ -429,25 +433,40 @@
 		browser.Open(ps1URL)
 	}
 
-	// Create the termui instance
-	s := termui.New(agent, ps1URL)
+	// Check if terminal UI should be enabled
+	// Disable termui if the flag is explicitly set to false or if we detect no PTY is available
+	if !term.IsTerminal(int(os.Stdin.Fd())) {
+		flags.termUI = false
+	}
+
+	// Create a variable for terminal UI
+	var s *termui.TermUI
+
+	// Create the termui instance only if needed
+	if flags.termUI {
+		s = termui.New(agent, ps1URL)
+	}
 
 	// Start skaband connection loop if needed
 	if flags.skabandAddr != "" {
 		connectFn := func(connected bool) {
 			if flags.verbose {
 				if connected {
-					s.AppendSystemMessage("skaband connected")
+					if s != nil {
+						s.AppendSystemMessage("skaband connected")
+					}
 				} else {
-					s.AppendSystemMessage("skaband disconnected")
+					if s != nil {
+						s.AppendSystemMessage("skaband disconnected")
+					}
 				}
 			}
 		}
 		go skabandclient.DialAndServeLoop(ctx, flags.skabandAddr, flags.sessionID, pubKey, srv, connectFn)
 	}
 
-	// Handle one-shot mode
-	if flags.oneShot {
+	// Handle one-shot mode or mode without terminal UI
+	if flags.oneShot || s == nil {
 		it := agent.NewIterator(ctx, 0)
 		for {
 			m := it.Next()
@@ -459,10 +478,20 @@
 			}
 			if m.EndOfTurn && m.ParentConversationID == nil {
 				fmt.Printf("Total cost: $%0.2f\n", agent.TotalUsage().TotalCostUSD)
-				return nil
+				if flags.oneShot {
+					return nil
+				}
+			}
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			default:
 			}
 		}
 	}
+	if s == nil {
+		panic("Should have exited above.")
+	}
 
 	// Run the terminal UI
 	defer func() {
diff --git a/termui/termui.go b/termui/termui.go
index 4461b5c..322b23d 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -340,7 +340,7 @@
 	defer ui.mu.Unlock()
 
 	if !term.IsTerminal(int(ui.stdin.Fd())) {
-		return fmt.Errorf("this command requires terminal I/O")
+		return fmt.Errorf("this command requires terminal I/O when termui=true")
 	}
 
 	oldState, err := term.MakeRaw(int(ui.stdin.Fd()))