loop/server: add /debug/system-prompt, move logs to /debug/logs

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sdadfdbbcd1589be1k
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 84e3615..dacc4f6 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -4,12 +4,14 @@
 import (
 	"context"
 	"crypto/rand"
+	"embed"
 	"encoding/base64"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"html"
+	"html/template"
 	"io"
 	"log/slog"
 	"net/http"
@@ -36,6 +38,9 @@
 	"sketch.dev/loop/server/gzhandler"
 )
 
+//go:embed templates/*
+var templateFS embed.FS
+
 // Remote represents a git remote with display information.
 type Remote struct {
 	Name        string `json:"name"`
@@ -426,8 +431,8 @@
 		}
 	})
 
-	// Handler for /logs - displays the contents of the log file
-	s.mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
+	// Handler for /debug/logs - displays the contents of the log file
+	s.mux.HandleFunc("/debug/logs", func(w http.ResponseWriter, r *http.Request) {
 		if s.logFile == nil {
 			httpError(w, r, "log file not set", http.StatusNotFound)
 			return
@@ -1113,13 +1118,15 @@
 			pid %d<br>
 			build %s<br>
 			<ul>
-					<li><a href="pprof/cmdline">pprof/cmdline</a></li>
-					<li><a href="pprof/profile">pprof/profile</a></li>
-					<li><a href="pprof/symbol">pprof/symbol</a></li>
-					<li><a href="pprof/trace">pprof/trace</a></li>
-					<li><a href="pprof/goroutine?debug=1">pprof/goroutine?debug=1</a></li>
-					<li><a href="conversation-history">conversation-history</a></li>
+				<li><a href="pprof/cmdline">pprof/cmdline</a></li>
+				<li><a href="pprof/profile">pprof/profile</a></li>
+				<li><a href="pprof/symbol">pprof/symbol</a></li>
+				<li><a href="pprof/trace">pprof/trace</a></li>
+				<li><a href="pprof/goroutine?debug=1">pprof/goroutine?debug=1</a></li>
+				<li><a href="conversation-history">conversation-history</a></li>
 				<li><a href="tools">tools</a></li>
+				<li><a href="system-prompt">system-prompt</a></li>
+				<li><a href="logs">logs</a></li>
 			</ul>
 			</body>
 			</html>
@@ -1164,19 +1171,47 @@
 			GetConvo() loop.ConvoInterface
 		}
 
-		if convoProvider, ok := agent.(ConvoProvider); ok {
-			convoInterface := convoProvider.GetConvo()
-
-			// Type assert to get the actual conversation
-			if convo, ok := convoInterface.(*conversation.Convo); ok {
-				// Render the tools debug page
-				renderToolsDebugPage(w, convo.Tools)
-			} else {
-				http.Error(w, "Unable to access conversation tools", http.StatusInternalServerError)
-			}
-		} else {
+		convoProvider, ok := agent.(ConvoProvider)
+		if !ok {
 			http.Error(w, "Agent does not support conversation debugging", http.StatusNotImplemented)
+			return
 		}
+
+		convoInterface := convoProvider.GetConvo()
+		convo, ok := convoInterface.(*conversation.Convo)
+		if !ok {
+			http.Error(w, "Unable to access conversation tools", http.StatusInternalServerError)
+			return
+		}
+
+		// Render the tools debug page
+		renderToolsDebugPage(w, convo.Tools)
+	})
+
+	// Add system prompt debug handler
+	mux.HandleFunc("GET /debug/system-prompt", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+		// Try to get the conversation and its system prompt
+		type ConvoProvider interface {
+			GetConvo() loop.ConvoInterface
+		}
+
+		convoProvider, ok := agent.(ConvoProvider)
+		if !ok {
+			http.Error(w, "Agent does not support conversation debugging", http.StatusNotImplemented)
+			return
+		}
+
+		convoInterface := convoProvider.GetConvo()
+		convo, ok := convoInterface.(*conversation.Convo)
+		if !ok {
+			http.Error(w, "Unable to access conversation system prompt", http.StatusInternalServerError)
+			return
+		}
+
+		// Render the system prompt debug page
+		renderSystemPromptDebugPage(w, convo.SystemPrompt)
 	})
 
 	return mux
@@ -1250,6 +1285,38 @@
 </html>`)
 }
 
+// SystemPromptDebugData holds the data for the system prompt debug template
+type SystemPromptDebugData struct {
+	SystemPrompt string
+	Length       int
+	Lines        int
+}
+
+// renderSystemPromptDebugPage renders an HTML page showing the system prompt
+func renderSystemPromptDebugPage(w http.ResponseWriter, systemPrompt string) {
+	// Calculate stats
+	length := len(systemPrompt)
+	lines := strings.Count(systemPrompt, "\n") + 1
+
+	// Parse template
+	tmpl, err := template.ParseFS(templateFS, "templates/system_prompt_debug.html")
+	if err != nil {
+		http.Error(w, "Error loading template: "+err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	// Execute template
+	data := SystemPromptDebugData{
+		SystemPrompt: systemPrompt,
+		Length:       length,
+		Lines:        lines,
+	}
+
+	if err := tmpl.Execute(w, data); err != nil {
+		http.Error(w, "Error executing template: "+err.Error(), http.StatusInternalServerError)
+	}
+}
+
 // isValidGitSHA validates if a string looks like a valid git SHA hash.
 // Git SHAs are hexadecimal strings of at least 4 characters but typically 7, 8, or 40 characters.
 func isValidGitSHA(sha string) bool {