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/loop/agent.go b/loop/agent.go
index b12a5f5..cc861ed 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -6,6 +6,7 @@
 	_ "embed"
 	"encoding/json"
 	"fmt"
+	"io"
 	"log/slog"
 	"net/http"
 	"os"
@@ -17,6 +18,7 @@
 	"time"
 
 	"sketch.dev/ant"
+	"sketch.dev/browser"
 	"sketch.dev/claudetool"
 	"sketch.dev/claudetool/bashkit"
 )
@@ -92,6 +94,8 @@
 	OutsideHostname() string
 	OutsideWorkingDir() string
 	GitOrigin() string
+	// OpenBrowser is a best-effort attempt to open a browser at url in outside sketch.
+	OpenBrowser(url string)
 
 	// RestartConversation resets the conversation history
 	RestartConversation(ctx context.Context, rev string, initialPrompt string) error
@@ -274,6 +278,7 @@
 	lastHEAD          string        // hash of the last HEAD that was pushed to the host (only when under docker)
 	initialCommit     string        // hash of the Git HEAD when the agent was instantiated or Init()
 	gitRemoteAddr     string        // HTTP URL of the host git repo (only when under docker)
+	outsideHTTP       string        // base address of the outside webserver (only when under docker)
 	ready             chan struct{} // closed when the agent is initialized (only when under docker)
 	startedAt         time.Time
 	originalBudget    ant.Budget
@@ -393,6 +398,27 @@
 	return a.gitOrigin
 }
 
+func (a *Agent) OpenBrowser(url string) {
+	if !a.IsInContainer() {
+		browser.Open(url)
+		return
+	}
+	// We're in Docker, need to send a request to the Git server
+	// to signal that the outer process should open the browser.
+	httpc := &http.Client{Timeout: 5 * time.Second}
+	resp, err := httpc.Post(a.outsideHTTP+"/browser", "text/plain", strings.NewReader(url))
+	if err != nil {
+		slog.Debug("browser launch request connection failed", "err", err, "url", url)
+		return
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode == http.StatusOK {
+		return
+	}
+	body, _ := io.ReadAll(resp.Body)
+	slog.Debug("browser launch request execution failed", "status", resp.Status, "body", string(body))
+}
+
 // CurrentState returns the current state of the agent's state machine.
 func (a *Agent) CurrentState() State {
 	return a.stateMachine.CurrentState()
@@ -595,6 +621,7 @@
 
 	InDocker      bool
 	Commit        string
+	OutsideHTTP   string
 	GitRemoteAddr string
 	HostAddr      string
 }
@@ -627,6 +654,7 @@
 		}
 		a.lastHEAD = ini.Commit
 		a.gitRemoteAddr = ini.GitRemoteAddr
+		a.outsideHTTP = ini.OutsideHTTP
 		a.initialCommit = ini.Commit
 		if ini.HostAddr != "" {
 			a.url = "http://" + ini.HostAddr