sketch: fix diff view editing of gitignore'd files and forward more http errors to logs

Fixes https://github.com/boldsoftware/sketch/issues/213

We had "sketch" git ignored, so "git add sketch/cmd/sketch/main.go" was
failing when a user was editing it in diff view. The gitignore was
incorrectly specified. ("git ls-files -i -c --exclude-standard"
returning main.go should have tipped us off, but who knew!)

Anyway, fixed that, and improved the logging.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s3ed65211dd497f76k
diff --git a/.gitignore b/.gitignore
index 2b55c68..540f606 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@
 dist
 
 # makefile artifact
-sketch
+/sketch
diff --git a/git_tools/git_tools.go b/git_tools/git_tools.go
index d047710..a91071b 100644
--- a/git_tools/git_tools.go
+++ b/git_tools/git_tools.go
@@ -434,20 +434,15 @@
 	const expectedMsg = "User changes from diff view."
 	amend := err == nil && commitMsg == expectedMsg
 
-	// Add the file to git
-	cmd = exec.CommandContext(ctx, "git", "add", filePath)
-	cmd.Dir = repoDir
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("error adding file to git: %w - git output: %s", err, string(out))
-	}
-
 	// Commit the changes
+	// Instead of calling git add first, we call git commit with a filepsec, which works the same,
+	// but would fail if the file isn't tracked by git already.
 	if amend {
 		// Amend the previous commit
-		cmd = exec.CommandContext(ctx, "git", "commit", "--amend", "--no-edit")
+		cmd = exec.CommandContext(ctx, "git", "commit", "--amend", "--no-edit", "--", filePath)
 	} else {
 		// Create a new commit
-		cmd = exec.CommandContext(ctx, "git", "commit", "-m", expectedMsg, filePath)
+		cmd = exec.CommandContext(ctx, "git", "commit", "-m", expectedMsg, "--", filePath)
 	}
 	cmd.Dir = repoDir
 
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index dcb315f..11dd045 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -67,6 +67,12 @@
 	Error   string `json:"error,omitempty"`
 }
 
+// httpError logs the error and sends an HTTP error response
+func httpError(w http.ResponseWriter, r *http.Request, message string, code int) {
+	slog.Error("HTTP error", "method", r.Method, "path", r.URL.Path, "message", message, "code", code)
+	http.Error(w, message, code)
+}
+
 // isGitHubURL checks if a URL is a GitHub URL
 func isGitHubURL(url string) bool {
 	return strings.Contains(url, "github.com")
@@ -237,7 +243,7 @@
 	// Create a reverse proxy to localhost:<port>
 	target, err := url.Parse(fmt.Sprintf("http://localhost:%s", port))
 	if err != nil {
-		http.Error(w, "Failed to parse proxy target", http.StatusInternalServerError)
+		httpError(w, r, "Failed to parse proxy target", http.StatusInternalServerError)
 		return
 	}
 
@@ -256,7 +262,7 @@
 	// Handle proxy errors
 	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
 		slog.Error("Proxy error", "error", err, "target", target.String(), "port", port)
-		http.Error(w, "Proxy error: "+err.Error(), http.StatusBadGateway)
+		httpError(w, r, "Proxy error: "+err.Error(), http.StatusBadGateway)
 	}
 
 	proxy.ServeHTTP(w, r)
@@ -294,7 +300,7 @@
 		if commit != "" {
 			// Validate the commit hash format
 			if !isValidGitSHA(commit) {
-				http.Error(w, fmt.Sprintf("Invalid git commit SHA format: %s", commit), http.StatusBadRequest)
+				httpError(w, r, fmt.Sprintf("Invalid git commit SHA format: %s", commit), http.StatusBadRequest)
 				return
 			}
 
@@ -304,7 +310,7 @@
 		}
 
 		if err != nil {
-			http.Error(w, fmt.Sprintf("Error generating diff: %v", err), http.StatusInternalServerError)
+			httpError(w, r, fmt.Sprintf("Error generating diff: %v", err), http.StatusInternalServerError)
 			return
 		}
 
@@ -319,25 +325,25 @@
 				slog.ErrorContext(r.Context(), "/init panic", slog.Any("recovered_err", err))
 
 				// Return an error response to the client
-				http.Error(w, fmt.Sprintf("panic: %v\n", err), http.StatusInternalServerError)
+				httpError(w, r, fmt.Sprintf("panic: %v\n", err), http.StatusInternalServerError)
 			}
 		}()
 
 		if r.Method != "POST" {
-			http.Error(w, "POST required", http.StatusBadRequest)
+			httpError(w, r, "POST required", http.StatusBadRequest)
 			return
 		}
 
 		body, err := io.ReadAll(r.Body)
 		r.Body.Close()
 		if err != nil {
-			http.Error(w, "failed to read request body: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "failed to read request body: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 
 		m := &InitRequest{}
 		if err := json.Unmarshal(body, m); err != nil {
-			http.Error(w, "bad request body: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "bad request body: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 
@@ -363,7 +369,7 @@
 			HostAddr: m.HostAddr,
 		}
 		if err := agent.Init(ini); err != nil {
-			http.Error(w, "init failed: "+err.Error(), http.StatusInternalServerError)
+			httpError(w, r, "init failed: "+err.Error(), http.StatusInternalServerError)
 			return
 		}
 		w.Header().Set("Content-Type", "application/json")
@@ -384,7 +390,7 @@
 		if startParam != "" {
 			start, err = strconv.Atoi(startParam)
 			if err != nil {
-				http.Error(w, "Invalid 'start' parameter", http.StatusBadRequest)
+				httpError(w, r, "Invalid 'start' parameter", http.StatusBadRequest)
 				return
 			}
 		}
@@ -393,7 +399,7 @@
 		if endParam != "" {
 			end, err = strconv.Atoi(endParam)
 			if err != nil {
-				http.Error(w, "Invalid 'end' parameter", http.StatusBadRequest)
+				httpError(w, r, "Invalid 'end' parameter", http.StatusBadRequest)
 				return
 			}
 		} else {
@@ -401,7 +407,7 @@
 		}
 
 		if start < 0 || start > end || end > currentCount {
-			http.Error(w, fmt.Sprintf("Invalid range: start %d end %d currentCount %d", start, end, currentCount), http.StatusBadRequest)
+			httpError(w, r, fmt.Sprintf("Invalid range: start %d end %d currentCount %d", start, end, currentCount), http.StatusBadRequest)
 			return
 		}
 
@@ -415,19 +421,19 @@
 
 		err = encoder.Encode(messages)
 		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			httpError(w, r, err.Error(), http.StatusInternalServerError)
 		}
 	})
 
 	// Handler for /logs - displays the contents of the log file
 	s.mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
 		if s.logFile == nil {
-			http.Error(w, "log file not set", http.StatusNotFound)
+			httpError(w, r, "log file not set", http.StatusNotFound)
 			return
 		}
 		logContents, err := os.ReadFile(s.logFile.Name())
 		if err != nil {
-			http.Error(w, "error reading log file: "+err.Error(), http.StatusInternalServerError)
+			httpError(w, r, "error reading log file: "+err.Error(), http.StatusInternalServerError)
 			return
 		}
 		w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -476,7 +482,7 @@
 		// Marshal the JSON with indentation for better readability
 		jsonData, err := json.MarshalIndent(downloadData, "", "  ")
 		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			httpError(w, r, err.Error(), http.StatusInternalServerError)
 			return
 		}
 		w.Write(jsonData)
@@ -494,7 +500,7 @@
 		if seenParam != "" {
 			clientMessageCount, err = strconv.Atoi(seenParam)
 			if err != nil {
-				http.Error(w, "Invalid 'seen' parameter", http.StatusBadRequest)
+				httpError(w, r, "Invalid 'seen' parameter", http.StatusBadRequest)
 				return
 			}
 		}
@@ -538,7 +544,7 @@
 
 		err = encoder.Encode(state)
 		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
+			httpError(w, r, err.Error(), http.StatusInternalServerError)
 		}
 	})
 
@@ -549,19 +555,19 @@
 	// TODO: The UI doesn't actually know how to use terminals 2-9!
 	s.mux.HandleFunc("/terminal/events/", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodGet {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 		pathParts := strings.Split(r.URL.Path, "/")
 		if len(pathParts) < 4 {
-			http.Error(w, "Invalid terminal ID", http.StatusBadRequest)
+			httpError(w, r, "Invalid terminal ID", http.StatusBadRequest)
 			return
 		}
 
 		sessionID := pathParts[3]
 		// Validate that the terminal ID is between 1-9
 		if len(sessionID) != 1 || sessionID[0] < '1' || sessionID[0] > '9' {
-			http.Error(w, "Terminal ID must be between 1 and 9", http.StatusBadRequest)
+			httpError(w, r, "Terminal ID must be between 1 and 9", http.StatusBadRequest)
 			return
 		}
 
@@ -570,12 +576,12 @@
 
 	s.mux.HandleFunc("/terminal/input/", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodPost {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 		pathParts := strings.Split(r.URL.Path, "/")
 		if len(pathParts) < 4 {
-			http.Error(w, "Invalid terminal ID", http.StatusBadRequest)
+			httpError(w, r, "Invalid terminal ID", http.StatusBadRequest)
 			return
 		}
 		sessionID := pathParts[3]
@@ -595,14 +601,14 @@
 	// Handler for /commit-description - returns the description of a git commit
 	s.mux.HandleFunc("/commit-description", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodGet {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 
 		// Get the revision parameter
 		revision := r.URL.Query().Get("revision")
 		if revision == "" {
-			http.Error(w, "Missing revision parameter", http.StatusBadRequest)
+			httpError(w, r, "Missing revision parameter", http.StatusBadRequest)
 			return
 		}
 
@@ -613,7 +619,7 @@
 
 		output, err := cmd.CombinedOutput()
 		if err != nil {
-			http.Error(w, "Failed to get commit description: "+err.Error(), http.StatusInternalServerError)
+			httpError(w, r, "Failed to get commit description: "+err.Error(), http.StatusInternalServerError)
 			return
 		}
 
@@ -631,14 +637,14 @@
 	// Handler for /screenshot/{id} - serves screenshot images
 	s.mux.HandleFunc("/screenshot/", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodGet {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 
 		// Extract the screenshot ID from the path
 		pathParts := strings.Split(r.URL.Path, "/")
 		if len(pathParts) < 3 {
-			http.Error(w, "Invalid screenshot ID", http.StatusBadRequest)
+			httpError(w, r, "Invalid screenshot ID", http.StatusBadRequest)
 			return
 		}
 
@@ -646,7 +652,7 @@
 
 		// Validate the ID format (prevent directory traversal)
 		if strings.Contains(screenshotID, "/") || strings.Contains(screenshotID, "\\") {
-			http.Error(w, "Invalid screenshot ID format", http.StatusBadRequest)
+			httpError(w, r, "Invalid screenshot ID format", http.StatusBadRequest)
 			return
 		}
 
@@ -655,7 +661,7 @@
 
 		// Check if the file exists
 		if _, err := os.Stat(filePath); os.IsNotExist(err) {
-			http.Error(w, "Screenshot not found", http.StatusNotFound)
+			httpError(w, r, "Screenshot not found", http.StatusNotFound)
 			return
 		}
 
@@ -668,7 +674,7 @@
 	// Handler for POST /chat
 	s.mux.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodPost {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 
@@ -679,13 +685,13 @@
 
 		decoder := json.NewDecoder(r.Body)
 		if err := decoder.Decode(&requestBody); err != nil {
-			http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "Invalid request body: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 		defer r.Body.Close()
 
 		if requestBody.Message == "" {
-			http.Error(w, "Message cannot be empty", http.StatusBadRequest)
+			httpError(w, r, "Message cannot be empty", http.StatusBadRequest)
 			return
 		}
 
@@ -697,7 +703,7 @@
 	// Handler for POST /upload - uploads a file to /tmp
 	s.mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodPost {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 
@@ -706,14 +712,14 @@
 
 		// Parse the multipart form
 		if err := r.ParseMultipartForm(10 * 1024 * 1024); err != nil {
-			http.Error(w, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 
 		// Get the file from the multipart form
 		file, handler, err := r.FormFile("file")
 		if err != nil {
-			http.Error(w, "Failed to get uploaded file: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "Failed to get uploaded file: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 		defer file.Close()
@@ -721,7 +727,7 @@
 		// Generate a unique ID (8 random bytes converted to 16 hex chars)
 		randBytes := make([]byte, 8)
 		if _, err := rand.Read(randBytes); err != nil {
-			http.Error(w, "Failed to generate random filename: "+err.Error(), http.StatusInternalServerError)
+			httpError(w, r, "Failed to generate random filename: "+err.Error(), http.StatusInternalServerError)
 			return
 		}
 
@@ -734,14 +740,14 @@
 		// Create the destination file
 		destFile, err := os.Create(filename)
 		if err != nil {
-			http.Error(w, "Failed to create destination file: "+err.Error(), http.StatusInternalServerError)
+			httpError(w, r, "Failed to create destination file: "+err.Error(), http.StatusInternalServerError)
 			return
 		}
 		defer destFile.Close()
 
 		// Copy the file contents to the destination file
 		if _, err := io.Copy(destFile, file); err != nil {
-			http.Error(w, "Failed to save file: "+err.Error(), http.StatusInternalServerError)
+			httpError(w, r, "Failed to save file: "+err.Error(), http.StatusInternalServerError)
 			return
 		}
 
@@ -759,7 +765,7 @@
 	// Handler for /cancel - cancels the current inner loop in progress
 	s.mux.HandleFunc("/cancel", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodPost {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 
@@ -771,7 +777,7 @@
 
 		decoder := json.NewDecoder(r.Body)
 		if err := decoder.Decode(&requestBody); err != nil && err != io.EOF {
-			http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "Invalid request body: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 		defer r.Body.Close()
@@ -784,7 +790,7 @@
 		if requestBody.ToolCallID != "" {
 			err := agent.CancelToolUse(requestBody.ToolCallID, fmt.Errorf("%s", cancelReason))
 			if err != nil {
-				http.Error(w, err.Error(), http.StatusBadRequest)
+				httpError(w, r, err.Error(), http.StatusBadRequest)
 				return
 			}
 			// Return a success response
@@ -806,7 +812,7 @@
 	// Handler for /end - shuts down the inner sketch process
 	s.mux.HandleFunc("/end", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodPost {
-			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			httpError(w, r, "Method not allowed", http.StatusMethodNotAllowed)
 			return
 		}
 
@@ -819,7 +825,7 @@
 
 		decoder := json.NewDecoder(r.Body)
 		if err := decoder.Decode(&requestBody); err != nil && err != io.EOF {
-			http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+			httpError(w, r, "Invalid request body: "+err.Error(), http.StatusBadRequest)
 			return
 		}
 		defer r.Body.Close()
@@ -916,7 +922,7 @@
 		session, err = s.createTerminalSession(sessionID)
 		if err != nil {
 			s.ptyMutex.Unlock()
-			http.Error(w, fmt.Sprintf("Failed to create terminal: %v", err), http.StatusInternalServerError)
+			httpError(w, r, fmt.Sprintf("Failed to create terminal: %v", err), http.StatusInternalServerError)
 			return
 		}
 
@@ -979,14 +985,14 @@
 	s.ptyMutex.Unlock()
 
 	if !exists {
-		http.Error(w, "Terminal session not found", http.StatusNotFound)
+		httpError(w, r, "Terminal session not found", http.StatusNotFound)
 		return
 	}
 
 	// Read the request body (terminal input or resize command)
 	body, err := io.ReadAll(r.Body)
 	if err != nil {
-		http.Error(w, "Failed to read request body", http.StatusBadRequest)
+		httpError(w, r, "Failed to read request body", http.StatusBadRequest)
 		return
 	}
 
@@ -1011,7 +1017,7 @@
 	_, err = session.pty.Write(body)
 	if err != nil {
 		slog.Error("Failed to write to pty", "error", err)
-		http.Error(w, "Failed to write to terminal", http.StatusInternalServerError)
+		httpError(w, r, "Failed to write to terminal", http.StatusInternalServerError)
 		return
 	}
 
@@ -1136,14 +1142,14 @@
 			// Call the DebugJSON method to get the conversation history
 			historyJSON, err := convoProvider.GetConvo().DebugJSON()
 			if err != nil {
-				http.Error(w, fmt.Sprintf("Error getting conversation history: %v", err), http.StatusInternalServerError)
+				httpError(w, r, fmt.Sprintf("Error getting conversation history: %v", err), http.StatusInternalServerError)
 				return
 			}
 
 			// Write the JSON response
 			w.Write(historyJSON)
 		} else {
-			http.Error(w, "Agent does not support conversation history debugging", http.StatusNotImplemented)
+			httpError(w, r, "Agent does not support conversation history debugging", http.StatusNotImplemented)
 		}
 	})
 
@@ -1182,7 +1188,7 @@
 	if fromParam != "" {
 		fromIndex, err = strconv.Atoi(fromParam)
 		if err != nil {
-			http.Error(w, "Invalid 'from' parameter", http.StatusBadRequest)
+			httpError(w, r, "Invalid 'from' parameter", http.StatusBadRequest)
 			return
 		}
 	}
@@ -1433,7 +1439,7 @@
 
 	// Check if we have enough parameters
 	if from == "" {
-		http.Error(w, "Missing required parameter: either 'commit' or at least 'from'", http.StatusBadRequest)
+		httpError(w, r, "Missing required parameter: either 'commit' or at least 'from'", http.StatusBadRequest)
 		return
 	}
 	// Note: 'to' can be empty to indicate working directory (unstaged changes)
@@ -1441,14 +1447,14 @@
 	// Call the git_tools function
 	diff, err := git_tools.GitRawDiff(repoDir, from, to)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error getting git diff: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error getting git diff: %v", err), http.StatusInternalServerError)
 		return
 	}
 
 	// Return the result as JSON
 	w.Header().Set("Content-Type", "application/json")
 	if err := json.NewEncoder(w).Encode(diff); err != nil {
-		http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
 		return
 	}
 }
@@ -1465,14 +1471,14 @@
 	// Parse query parameters
 	hash := r.URL.Query().Get("hash")
 	if hash == "" {
-		http.Error(w, "Missing required parameter: 'hash'", http.StatusBadRequest)
+		httpError(w, r, "Missing required parameter: 'hash'", http.StatusBadRequest)
 		return
 	}
 
 	// Call the git_tools function
 	show, err := git_tools.GitShow(repoDir, hash)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error running git show: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error running git show: %v", err), http.StatusInternalServerError)
 		return
 	}
 
@@ -1485,7 +1491,7 @@
 	// Return the result as JSON
 	w.Header().Set("Content-Type", "application/json")
 	if err := json.NewEncoder(w).Encode(response); err != nil {
-		http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
 		return
 	}
 }
@@ -1503,14 +1509,14 @@
 	// Call the git_tools function
 	log, err := git_tools.GitRecentLog(repoDir, initialCommit)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error getting git log: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error getting git log: %v", err), http.StatusInternalServerError)
 		return
 	}
 
 	// Return the result as JSON
 	w.Header().Set("Content-Type", "application/json")
 	if err := json.NewEncoder(w).Encode(log); err != nil {
-		http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
 		return
 	}
 }
@@ -1530,7 +1536,7 @@
 
 	// Check if path is provided
 	if path == "" {
-		http.Error(w, "Missing required parameter: path", http.StatusBadRequest)
+		httpError(w, r, "Missing required parameter: path", http.StatusBadRequest)
 		return
 	}
 
@@ -1543,14 +1549,14 @@
 		w.WriteHeader(http.StatusNoContent)
 		return
 	default:
-		http.Error(w, fmt.Sprintf("error reading file: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("error reading file: %v", err), http.StatusInternalServerError)
 		return
 	}
 
 	// Return the content as JSON for consistency with other endpoints
 	w.Header().Set("Content-Type", "application/json")
 	if err := json.NewEncoder(w).Encode(map[string]string{"output": content}); err != nil {
-		http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
 		return
 	}
 }
@@ -1571,34 +1577,34 @@
 	}
 
 	if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
-		http.Error(w, fmt.Sprintf("Error parsing request body: %v", err), http.StatusBadRequest)
+		httpError(w, r, fmt.Sprintf("Error parsing request body: %v", err), http.StatusBadRequest)
 		return
 	}
 	defer r.Body.Close()
 
 	// Check if path is provided
 	if requestBody.Path == "" {
-		http.Error(w, "Missing required parameter: path", http.StatusBadRequest)
+		httpError(w, r, "Missing required parameter: path", http.StatusBadRequest)
 		return
 	}
 
 	// Save file content using GitSaveFile
 	err := git_tools.GitSaveFile(repoDir, requestBody.Path, requestBody.Content)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error saving file: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error saving file: %v", err), http.StatusInternalServerError)
 		return
 	}
 
 	// Auto-commit the changes
 	err = git_tools.AutoCommitDiffViewChanges(r.Context(), repoDir, requestBody.Path)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error auto-committing changes: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error auto-committing changes: %v", err), http.StatusInternalServerError)
 		return
 	}
 
 	// Detect git changes to push and notify user
 	if err = s.agent.DetectGitChanges(r.Context()); err != nil {
-		http.Error(w, fmt.Sprintf("Error detecting git changes: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error detecting git changes: %v", err), http.StatusInternalServerError)
 		return
 	}
 
@@ -1616,7 +1622,7 @@
 	repoDir := s.agent.RepoRoot()
 	untrackedFiles, err := git_tools.GitGetUntrackedFiles(repoDir)
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error getting untracked files: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error getting untracked files: %v", err), http.StatusInternalServerError)
 		return
 	}
 
@@ -1641,13 +1647,13 @@
 	cmd.Dir = repoDir
 	output, err := cmd.Output()
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error getting HEAD commit: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error getting HEAD commit: %v", err), http.StatusInternalServerError)
 		return
 	}
 
 	parts := strings.Split(strings.TrimSpace(string(output)), "\x00")
 	if len(parts) != 2 {
-		http.Error(w, "Unexpected git log output format", http.StatusInternalServerError)
+		httpError(w, r, "Unexpected git log output format", http.StatusInternalServerError)
 		return
 	}
 	hash := parts[0]
@@ -1658,7 +1664,7 @@
 	cmd.Dir = repoDir
 	output, err = cmd.Output()
 	if err != nil {
-		http.Error(w, fmt.Sprintf("Error getting remotes: %v", err), http.StatusInternalServerError)
+		httpError(w, r, fmt.Sprintf("Error getting remotes: %v", err), http.StatusInternalServerError)
 		return
 	}
 
@@ -1720,13 +1726,13 @@
 	var requestBody GitPushRequest
 
 	if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
-		http.Error(w, fmt.Sprintf("Error parsing request body: %v", err), http.StatusBadRequest)
+		httpError(w, r, fmt.Sprintf("Error parsing request body: %v", err), http.StatusBadRequest)
 		return
 	}
 	defer r.Body.Close()
 
 	if requestBody.Remote == "" || requestBody.Branch == "" || requestBody.Commit == "" {
-		http.Error(w, "Missing required parameters: remote, branch, and commit", http.StatusBadRequest)
+		httpError(w, r, "Missing required parameters: remote, branch, and commit", http.StatusBadRequest)
 		return
 	}