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 {
diff --git a/loop/server/templates/system_prompt_debug.html b/loop/server/templates/system_prompt_debug.html
new file mode 100644
index 0000000..e7efdad
--- /dev/null
+++ b/loop/server/templates/system_prompt_debug.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Sketch System Prompt Debug</title>
+ <style>
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ margin: 40px;
+ }
+ h1 {
+ color: #333;
+ }
+ .prompt-container {
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 6px;
+ padding: 20px;
+ margin: 20px 0;
+ }
+ .prompt-content {
+ background: #ffffff;
+ border: 1px solid #d0d7de;
+ border-radius: 4px;
+ padding: 16px;
+ font-family: "SF Mono", Monaco, monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ max-height: 70vh;
+ overflow-y: auto;
+ }
+ .summary {
+ background: #e6f3ff;
+ border-left: 4px solid #0366d6;
+ padding: 16px;
+ margin-bottom: 30px;
+ }
+ .copy-button {
+ background: #0366d6;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-bottom: 10px;
+ font-size: 12px;
+ }
+ .copy-button:hover {
+ background: #0250a3;
+ }
+ .stats {
+ font-size: 0.9em;
+ color: #656d76;
+ margin-bottom: 10px;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Sketch System Prompt Debug</h1>
+ <div class="stats">
+ <strong>Length:</strong> {{.Length}} characters |
+ <strong>Lines:</strong> {{.Lines}}
+ </div>
+ <div class="prompt-container">
+ <button class="copy-button" onclick="copyToClipboard()">
+ Copy to Clipboard
+ </button>
+ <div class="prompt-content" id="prompt-content">{{.SystemPrompt}}</div>
+ </div>
+ <script>
+ function copyToClipboard() {
+ const content = document.getElementById("prompt-content").textContent;
+ navigator.clipboard
+ .writeText(content)
+ .then(() => {
+ const button = document.querySelector(".copy-button");
+ const originalText = button.textContent;
+ button.textContent = "Copied!";
+ setTimeout(() => {
+ button.textContent = originalText;
+ }, 2000);
+ })
+ .catch((err) => {
+ alert("Failed to copy to clipboard");
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/webui/src/web-components/sketch-container-status.ts b/webui/src/web-components/sketch-container-status.ts
index 74f2226..2e83df0 100644
--- a/webui/src/web-components/sketch-container-status.ts
+++ b/webui/src/web-components/sketch-container-status.ts
@@ -822,7 +822,7 @@
<div
class="flex items-center whitespace-nowrap mr-2.5 text-xs col-span-full mt-1.5 border-t border-gray-300 dark:border-gray-600 pt-1.5"
>
- <a href="logs" class="text-blue-600">Logs</a> (<a
+ <a href="debug/logs" class="text-blue-600">Logs</a> (<a
href="download"
class="text-blue-600"
>Download</a