all: support popping a browser from termui

- Add 'browser', 'open', and 'b' command aliases to termui
- Open the current conversation URL in default browser
- Add help documentation for the new command

Add browser launch endpoint to Git server for Docker support.

We'll probably want to set up a proper mux for the no-longer-just-git
server pretty soon.

Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/dockerimg/githttp.go b/dockerimg/githttp.go
index 38a8b54..6270838 100644
--- a/dockerimg/githttp.go
+++ b/dockerimg/githttp.go
@@ -3,6 +3,7 @@
 import (
 	"crypto/subtle"
 	"fmt"
+	"io"
 	"log/slog"
 	"net/http"
 	"net/http/cgi"
@@ -14,6 +15,7 @@
 type gitHTTP struct {
 	gitRepoRoot string
 	pass        []byte
+	browserC    chan string // browser launch requests
 }
 
 func (g *gitHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -46,6 +48,33 @@
 		return
 	}
 
+	// TODO: real mux?
+	if strings.HasPrefix(r.URL.Path, "/browser") {
+		if r.Method != http.MethodPost {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			http.Error(w, "Failed to read request body: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+		defer r.Body.Close()
+		url := strings.TrimSpace(string(body))
+		if len(url) == 0 {
+			http.Error(w, "URL cannot be empty", http.StatusBadRequest)
+			return
+		}
+		select {
+		case g.browserC <- string(url):
+			slog.InfoContext(r.Context(), "open browser", "url", url)
+			w.WriteHeader(http.StatusOK)
+		default:
+			http.Error(w, "Too many browser launch requests", http.StatusTooManyRequests)
+		}
+		return
+	}
+
 	if runtime.GOOS == "darwin" {
 		// On the Mac, Docker connections show up from localhost. On Linux, the docker
 		// network is more arbitrary, so we don't do this additional check there.