all: use make to build

This overhauls the build system.
We used to use a just-in-time clever build system
so that 'go run' and 'go install' Just Worked.

This was really nice, except that it make it
all but impossible to ship a single binary.
It also required our uses to install npm,
which some folks have an understandably negative reaction to.

This migrates to a makefile for building.
The core typescript building logic is mostly still in Go,
and untouched (boy did I learn that lesson the hard way).

The output is a single file that includes the webui, innie, and outie.

(There are still very mild shenanigans in which we write outie
out to a temp file and then 'docker cp' it into the docker container.
But this is pretty manageable.)

There are some significant follow-ups left after this commit:

- convert the nightly release builds to use the makefile
- lots of dead code removal
- maybe add -race support using a dockerfile for the cgo compilation
- maybe use 'docker cp' stdin reading with tar to avoid the temp outtie file
- all the rest of the "better release" todos (brew install, etc.)
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 3f90f8b..e223a60 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -10,7 +10,6 @@
 	"fmt"
 	"html"
 	"io"
-	"io/fs"
 	"log/slog"
 	"net/http"
 	"net/http/httputil"
@@ -26,14 +25,13 @@
 	"syscall"
 	"time"
 
-	"sketch.dev/git_tools"
-	"sketch.dev/loop/server/gzhandler"
-
 	"github.com/creack/pty"
 	"sketch.dev/claudetool/browse"
+	"sketch.dev/embedded"
+	"sketch.dev/git_tools"
 	"sketch.dev/llm/conversation"
 	"sketch.dev/loop"
-	"sketch.dev/webui"
+	"sketch.dev/loop/server/gzhandler"
 )
 
 // terminalSession represents a terminal session with its PTY and the event channel
@@ -216,11 +214,6 @@
 		sshError:         "",
 	}
 
-	webBundle, err := webui.Build()
-	if err != nil {
-		return nil, fmt.Errorf("failed to build web bundle, did you run 'go generate sketch.dev/loop/...'?: %w", err)
-	}
-
 	s.mux.HandleFunc("/stream", s.handleSSEStream)
 
 	// Git tool endpoints
@@ -488,7 +481,7 @@
 		}
 	})
 
-	s.mux.Handle("/static/", http.StripPrefix("/static/", gzhandler.New(webBundle)))
+	s.mux.Handle("/static/", http.StripPrefix("/static/", gzhandler.New(embedded.WebUIFS())))
 
 	// Terminal WebSocket handler
 	// Terminal endpoints - predefined terminals 1-9
@@ -528,33 +521,14 @@
 		s.handleTerminalInput(w, r, sessionID)
 	})
 
-	// Handler for interface selection via URL parameters (?m for mobile, ?d for desktop, auto-detect by default)
+	// Handler for interface selection via URL parameters (?m for mobile)
 	s.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		// Check URL parameters for interface selection
-		queryParams := r.URL.Query()
-
-		// Check if mobile interface is requested (?m parameter)
-		if queryParams.Has("m") {
-			// Serve the mobile-app-shell.html file
-			data, err := fs.ReadFile(webBundle, "mobile-app-shell.html")
-			if err != nil {
-				http.Error(w, "Mobile interface not found", http.StatusNotFound)
-				return
-			}
-			w.Header().Set("Content-Type", "text/html")
-			w.Write(data)
-			return
+		webuiFS := embedded.WebUIFS()
+		appShell := "sketch-app-shell.html"
+		if r.URL.Query().Has("m") {
+			appShell = "mobile-app-shell.html"
 		}
-
-		// Check if desktop interface is explicitly requested (?d parameter)
-		// or serve desktop by default
-		data, err := fs.ReadFile(webBundle, "sketch-app-shell.html")
-		if err != nil {
-			http.Error(w, "File not found", http.StatusNotFound)
-			return
-		}
-		w.Header().Set("Content-Type", "text/html")
-		w.Write(data)
+		http.ServeFileFS(w, r, webuiFS, appShell)
 	})
 
 	// Handler for /commit-description - returns the description of a git commit