Restart conversation support.

The idea here is to let the user restart the conversation, possibly with
a better prompt. This is a common manual workflow, and I'd like to make
it easier.

I hand wrote the agent.go stuff, but Sketch wrote the rest.

Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/ant/ant.go b/ant/ant.go
index 69c17c2..8759883 100644
--- a/ant/ant.go
+++ b/ant/ant.go
@@ -464,6 +464,29 @@
 	}
 }
 
+func (c *Convo) SubConvoWithHistory() *Convo {
+	id := newConvoID()
+	return &Convo{
+		Ctx:           skribe.ContextWithAttr(c.Ctx, slog.String("convo_id", id), slog.String("parent_convo_id", c.ID)),
+		HTTPC:         c.HTTPC,
+		URL:           c.URL,
+		APIKey:        c.APIKey,
+		Model:         c.Model,
+		MaxTokens:     c.MaxTokens,
+		PromptCaching: c.PromptCaching,
+		Parent:        c,
+		// For convenience, sub-convo usage shares tool uses map with parent,
+		// all other fields separate, propagated in AddResponse
+		usage:    newUsageWithSharedToolUses(c.usage),
+		mu:       c.mu,
+		Listener: c.Listener,
+		ID:       id,
+		// Do not copy Budget. Each budget is independent,
+		// and OverBudget checks whether any ancestor is over budget.
+		messages: slices.Clone(c.messages),
+	}
+}
+
 // Depth reports how many "sub-conversations" deep this conversation is.
 // That it, it walks up parents until it finds a root.
 func (c *Convo) Depth() int {
@@ -664,6 +687,11 @@
 	return toolResults, nil
 }
 
+// GetID returns the conversation ID
+func (c *Convo) GetID() string {
+	return c.ID
+}
+
 func (c *Convo) CancelToolUse(toolUseID string, err error) error {
 	c.muToolUseCancel.Lock()
 	defer c.muToolUseCancel.Unlock()
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 2720659..23654ab 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -257,6 +257,7 @@
 		OutsideHostname:   *outsideHostname,
 		OutsideOS:         *outsideOS,
 		OutsideWorkingDir: *outsideWorkingDir,
+		InDocker:          inDocker,
 	}
 	agent := loop.NewAgent(agentConfig)
 
diff --git a/loop/agent.go b/loop/agent.go
index 497f048..b698d22 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -92,6 +92,15 @@
 	OutsideHostname() string
 	OutsideWorkingDir() string
 	GitOrigin() string
+
+	// RestartConversation resets the conversation history
+	RestartConversation(ctx context.Context, rev string, initialPrompt string) error
+	// SuggestReprompt suggests a re-prompt based on the current conversation.
+	SuggestReprompt(ctx context.Context) (string, error)
+	// IsInContainer returns true if the agent is running in a container
+	IsInContainer() bool
+	// FirstMessageIndex returns the index of the first message in the current conversation
+	FirstMessageIndex() int
 }
 
 type CodingAgentMessageType string
@@ -248,26 +257,29 @@
 	OverBudget() error
 	SendMessage(message ant.Message) (*ant.MessageResponse, error)
 	SendUserTextMessage(s string, otherContents ...ant.Content) (*ant.MessageResponse, error)
+	GetID() string
 	ToolResultContents(ctx context.Context, resp *ant.MessageResponse) ([]ant.Content, error)
 	ToolResultCancelContents(resp *ant.MessageResponse) ([]ant.Content, error)
 	CancelToolUse(toolUseID string, cause error) error
+	SubConvoWithHistory() *ant.Convo
 }
 
 type Agent struct {
-	convo          ConvoInterface
-	config         AgentConfig // config for this agent
-	workingDir     string
-	repoRoot       string // workingDir may be a subdir of repoRoot
-	url            string
-	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)
-	ready          chan struct{} // closed when the agent is initialized (only when under docker)
-	startedAt      time.Time
-	originalBudget ant.Budget
-	title          string
-	branchName     string
-	codereview     *claudetool.CodeReviewer
+	convo             ConvoInterface
+	config            AgentConfig // config for this agent
+	workingDir        string
+	repoRoot          string // workingDir may be a subdir of repoRoot
+	url               string
+	firstMessageIndex int           // index of the first message in the current conversation
+	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)
+	ready             chan struct{} // closed when the agent is initialized (only when under docker)
+	startedAt         time.Time
+	originalBudget    ant.Budget
+	title             string
+	branchName        string
+	codereview        *claudetool.CodeReviewer
 	// Outside information
 	outsideHostname   string
 	outsideOS         string
@@ -379,6 +391,16 @@
 	return a.gitOrigin
 }
 
+func (a *Agent) IsInContainer() bool {
+	return a.config.InDocker
+}
+
+func (a *Agent) FirstMessageIndex() int {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	return a.firstMessageIndex
+}
+
 // SetTitleBranch sets the title and branch name of the conversation.
 func (a *Agent) SetTitleBranch(title, branchName string) {
 	a.mu.Lock()
@@ -531,6 +553,7 @@
 	SessionID        string
 	ClientGOOS       string
 	ClientGOARCH     string
+	InDocker         bool
 	UseAnthropicEdit bool
 	// Outside information
 	OutsideHostname   string
@@ -1382,3 +1405,82 @@
 	}
 	return strings.TrimSpace(string(out))
 }
+
+func (a *Agent) initGitRevision(ctx context.Context, workingDir, revision string) error {
+	cmd := exec.CommandContext(ctx, "git", "stash")
+	cmd.Dir = workingDir
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("git stash: %s: %v", out, err)
+	}
+	cmd = exec.CommandContext(ctx, "git", "fetch", "sketch-host")
+	cmd.Dir = workingDir
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("git fetch: %s: %w", out, err)
+	}
+	cmd = exec.CommandContext(ctx, "git", "checkout", "-f", revision)
+	cmd.Dir = workingDir
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("git checkout %s: %s: %w", revision, out, err)
+	}
+	a.lastHEAD = revision
+	a.initialCommit = revision
+	return nil
+}
+
+func (a *Agent) RestartConversation(ctx context.Context, rev string, initialPrompt string) error {
+	a.mu.Lock()
+	a.title = ""
+	a.firstMessageIndex = len(a.history)
+	a.convo = a.initConvo()
+	gitReset := func() error {
+		if a.config.InDocker && rev != "" {
+			err := a.initGitRevision(ctx, a.workingDir, rev)
+			if err != nil {
+				return err
+			}
+		} else if !a.config.InDocker && rev != "" {
+			return fmt.Errorf("Not resetting git repo when working outside of a container.")
+		}
+		return nil
+	}
+	err := gitReset()
+	a.mu.Unlock()
+	if err != nil {
+		a.pushToOutbox(a.config.Context, errorMessage(err))
+	}
+
+	a.pushToOutbox(a.config.Context, AgentMessage{
+		Type: AgentMessageType, Content: "Conversation restarted.",
+	})
+	if initialPrompt != "" {
+		a.UserMessage(ctx, initialPrompt)
+	}
+	return nil
+}
+
+func (a *Agent) SuggestReprompt(ctx context.Context) (string, error) {
+	msg := `The user has requested a suggestion for a re-prompt.
+
+	Given the current conversation thus far, suggest a re-prompt that would
+	capture the instructions and feedback so far, as well as any
+	research or other information that would be helpful in implementing
+	the task.
+
+	Reply with ONLY the reprompt text.
+	`
+	userMessage := ant.Message{
+		Role:    "user",
+		Content: []ant.Content{{Type: "text", Text: msg}},
+	}
+	// By doing this in a subconversation, the agent doesn't call tools (because
+	// there aren't any), and there's not a concurrency risk with on-going other
+	// outstanding conversations.
+	convo := a.convo.SubConvoWithHistory()
+	resp, err := convo.SendMessage(userMessage)
+	if err != nil {
+		a.pushToOutbox(ctx, errorMessage(err))
+		return "", err
+	}
+	textContent := collectTextContent(resp)
+	return textContent, nil
+}
diff --git a/loop/agent_test.go b/loop/agent_test.go
index 61b057c..bde0d20 100644
--- a/loop/agent_test.go
+++ b/loop/agent_test.go
@@ -267,6 +267,8 @@
 	cumulativeUsageFunc          func() ant.CumulativeUsage
 	resetBudgetFunc              func(ant.Budget)
 	overBudgetFunc               func() error
+	getIDFunc                    func() string
+	subConvoWithHistoryFunc      func() *ant.Convo
 }
 
 func (m *MockConvoInterface) SendMessage(message ant.Message) (*ant.MessageResponse, error) {
@@ -324,6 +326,20 @@
 	return nil
 }
 
+func (m *MockConvoInterface) GetID() string {
+	if m.getIDFunc != nil {
+		return m.getIDFunc()
+	}
+	return "mock-convo-id"
+}
+
+func (m *MockConvoInterface) SubConvoWithHistory() *ant.Convo {
+	if m.subConvoWithHistoryFunc != nil {
+		return m.subConvoWithHistoryFunc()
+	}
+	return nil
+}
+
 // TestAgentProcessTurnWithNilResponseNilError tests the scenario where Agent.processTurn receives
 // a nil value for initialResp and nil error from processUserMessage.
 // This test verifies that the implementation properly handles this edge case.
diff --git a/loop/mocks.go b/loop/mocks.go
index 264c6bc..7e05070 100644
--- a/loop/mocks.go
+++ b/loop/mocks.go
@@ -192,6 +192,16 @@
 	return nil
 }
 
+func (m *MockConvo) GetID() string {
+	m.recordCall("GetID")
+	return "mock-conversation-id"
+}
+
+func (m *MockConvo) SubConvoWithHistory() *ant.Convo {
+	m.recordCall("SubConvoWithHistory")
+	return nil
+}
+
 func (m *MockConvo) ResetBudget(_ ant.Budget) {
 	m.recordCall("ResetBudget")
 }
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index c1254e2..c19f806 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -64,6 +64,8 @@
 	SessionID            string               `json:"session_id"`
 	SSHAvailable         bool                 `json:"ssh_available"`
 	SSHError             string               `json:"ssh_error,omitempty"`
+	InContainer          bool                 `json:"in_container"`
+	FirstMessageIndex    int                  `json:"first_message_index"`
 
 	OutsideHostname   string `json:"outside_hostname,omitempty"`
 	InsideHostname    string `json:"inside_hostname,omitempty"`
@@ -388,6 +390,8 @@
 			SessionID:            agent.SessionID(),
 			SSHAvailable:         s.sshAvailable,
 			SSHError:             s.sshError,
+			InContainer:          agent.IsInContainer(),
+			FirstMessageIndex:    agent.FirstMessageIndex(),
 		}
 
 		// Create a JSON encoder with indentation for pretty-printing
@@ -451,6 +455,94 @@
 		w.Write(data)
 	})
 
+	// Handler for POST /restart - restarts the conversation
+	s.mux.HandleFunc("/restart", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+
+		// Parse the request body
+		var requestBody struct {
+			Revision      string `json:"revision"`
+			InitialPrompt string `json:"initial_prompt"`
+		}
+
+		decoder := json.NewDecoder(r.Body)
+		if err := decoder.Decode(&requestBody); err != nil {
+			http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
+			return
+		}
+		defer r.Body.Close()
+
+		// Call the restart method
+		err := agent.RestartConversation(r.Context(), requestBody.Revision, requestBody.InitialPrompt)
+		if err != nil {
+			http.Error(w, "Failed to restart conversation: "+err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		// Return success response
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(map[string]string{"status": "restarted"})
+	})
+
+	// Handler for /suggest-reprompt - suggests a reprompt based on conversation history
+	// 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)
+			return
+		}
+
+		// Get the revision parameter
+		revision := r.URL.Query().Get("revision")
+		if revision == "" {
+			http.Error(w, "Missing revision parameter", http.StatusBadRequest)
+			return
+		}
+
+		// Run git command to get commit description
+		cmd := exec.Command("git", "log", "--oneline", "--decorate", "-n", "1", revision)
+		// Use the working directory from the agent
+		cmd.Dir = s.agent.WorkingDir()
+
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			http.Error(w, "Failed to get commit description: "+err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		// Prepare the response
+		resp := map[string]string{
+			"description": strings.TrimSpace(string(output)),
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+		if err := json.NewEncoder(w).Encode(resp); err != nil {
+			slog.ErrorContext(r.Context(), "Error encoding commit description response", slog.Any("err", err))
+		}
+	})
+
+	// Handler for /suggest-reprompt - suggests a reprompt based on conversation history
+	s.mux.HandleFunc("/suggest-reprompt", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodGet {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+
+		// Call the suggest reprompt method
+		suggestedPrompt, err := agent.SuggestReprompt(r.Context())
+		if err != nil {
+			http.Error(w, "Failed to suggest reprompt: "+err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		// Return success response
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(map[string]string{"prompt": suggestedPrompt})
+	})
+
 	// Handler for POST /chat
 	s.mux.HandleFunc("/chat", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method != http.MethodPost {
@@ -774,9 +866,11 @@
 	mux := http.NewServeMux()
 	mux.HandleFunc("GET /debug/{$}", func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+		// TODO: pid is not as useful as "outside pid"
 		fmt.Fprintf(w, `<!doctype html>
 			<html><head><title>sketch debug</title></head><body>
 			<h1>sketch debug</h1>
+			pid %d
 			<ul>
 					<li><a href="/debug/pprof/cmdline">pprof/cmdline</a></li>
 					<li><a href="/debug/pprof/profile">pprof/profile</a></li>
@@ -787,7 +881,7 @@
 			</ul>
 			</body>
 			</html>
-			`)
+			`, os.Getpid())
 	})
 	mux.HandleFunc("GET /debug/pprof/", pprof.Index)
 	mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
diff --git a/webui/src/fixtures/dummy.ts b/webui/src/fixtures/dummy.ts
index 4a40337..1972b09 100644
--- a/webui/src/fixtures/dummy.ts
+++ b/webui/src/fixtures/dummy.ts
@@ -373,4 +373,6 @@
   inside_working_dir: "/Users/pokey/src/spaghetti",
   outstanding_llm_calls: 0,
   outstanding_tool_calls: [],
+  in_container: true,
+  first_message_index: 0,
 };
diff --git a/webui/src/types.ts b/webui/src/types.ts
index e8bb746..b9451b7 100644
--- a/webui/src/types.ts
+++ b/webui/src/types.ts
@@ -74,6 +74,8 @@
 	session_id: string;
 	ssh_available: boolean;
 	ssh_error?: string;
+	in_container: boolean;
+	first_message_index: number;
 	outside_hostname?: string;
 	inside_hostname?: string;
 	outside_os?: string;
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index 3ee27f6..1cd1a0b 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -13,6 +13,7 @@
 import "./sketch-terminal";
 import "./sketch-timeline";
 import "./sketch-view-mode-select";
+import "./sketch-restart-modal";
 
 import { createRef, ref } from "lit/directives/ref.js";
 
@@ -229,6 +230,27 @@
       margin-right: 50px;
     }
 
+    .restart-button {
+      background: #2196f3;
+      color: white;
+      border: none;
+      padding: 4px 10px;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 12px;
+      margin-right: 5px;
+    }
+
+    .restart-button:hover {
+      background-color: #0b7dda;
+    }
+
+    .restart-button:disabled {
+      background-color: #ccc;
+      cursor: not-allowed;
+      opacity: 0.6;
+    }
+
     .refresh-button {
       background: #4caf50;
       color: white;
@@ -304,8 +326,14 @@
     outstanding_tool_calls: [],
     session_id: "",
     ssh_available: false,
+    ssh_error: "",
+    in_container: false,
+    first_message_index: 0,
   };
 
+  @state()
+  private restartModalOpen = false;
+
   // Mutation observer to detect when new messages are added
   private mutationObserver: MutationObserver | null = null;
 
@@ -706,10 +734,6 @@
     this.updateDocumentTitle();
   }
 
-  /**
-   * Handle stop button click
-   * Sends a request to the server to stop the current operation
-   */
   // Update last commit information when new messages arrive
   private updateLastCommitInfo(newMessages: AgentMessage[]): void {
     if (!newMessages || newMessages.length === 0) return;
@@ -782,6 +806,14 @@
     }
   }
 
+  openRestartModal() {
+    this.restartModalOpen = true;
+  }
+
+  handleRestartModalClose() {
+    this.restartModalOpen = false;
+  }
+
   async _sendChat(e: CustomEvent) {
     console.log("app shell: _sendChat", e);
     const message = e.detail.message?.trim();
@@ -867,10 +899,14 @@
 
         <div class="refresh-control">
           <button
-            id="stopButton"
-            class="refresh-button stop-button"
-            @click="${this._handleStopClick}"
+            id="restartButton"
+            class="restart-button"
+            ?disabled=${this.containerState.message_count === 0}
+            @click=${this.openRestartModal}
           >
+            Restart
+          </button>
+          <button id="stopButton" class="refresh-button stop-button">
             Stop
           </button>
 
@@ -939,6 +975,13 @@
       <div id="chat-input">
         <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
       </div>
+
+      <sketch-restart-modal
+        ?open=${this.restartModalOpen}
+        @close=${this.handleRestartModalClose}
+        .containerState=${this.containerState}
+        .messages=${this.messages}
+      ></sketch-restart-modal>
     `;
   }
 
@@ -956,6 +999,27 @@
       50,
     );
 
+    // Setup stop button
+    const stopButton = this.renderRoot?.querySelector(
+      "#stopButton",
+    ) as HTMLButtonElement;
+    stopButton?.addEventListener("click", async () => {
+      try {
+        const response = await fetch("/cancel", {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/json",
+          },
+          body: JSON.stringify({ reason: "User clicked stop button" }),
+        });
+        if (!response.ok) {
+          console.error("Failed to cancel:", await response.text());
+        }
+      } catch (error) {
+        console.error("Error cancelling operation:", error);
+      }
+    });
+
     const pollToggleCheckbox = this.renderRoot?.querySelector(
       "#pollToggle",
     ) as HTMLInputElement;
diff --git a/webui/src/web-components/sketch-container-status.test.ts b/webui/src/web-components/sketch-container-status.test.ts
index db43a6d..f8dc685 100644
--- a/webui/src/web-components/sketch-container-status.test.ts
+++ b/webui/src/web-components/sketch-container-status.test.ts
@@ -24,6 +24,8 @@
   outstanding_tool_calls: [],
   session_id: "test-session-id",
   ssh_available: false,
+  in_container: true,
+  first_message_index: 0,
 };
 
 test("render props", async ({ mount }) => {
diff --git a/webui/src/web-components/sketch-restart-modal.ts b/webui/src/web-components/sketch-restart-modal.ts
new file mode 100644
index 0000000..b5cf7e4
--- /dev/null
+++ b/webui/src/web-components/sketch-restart-modal.ts
@@ -0,0 +1,907 @@
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { AgentMessage, State } from "../types";
+
+@customElement("sketch-restart-modal")
+export class SketchRestartModal extends LitElement {
+  @property({ type: Boolean })
+  open = false;
+
+  @property({ attribute: false })
+  containerState: State | null = null;
+
+  @property({ attribute: false })
+  messages: AgentMessage[] = [];
+
+  @state()
+  private restartType: "initial" | "current" | "other" = "current";
+
+  @state()
+  private customRevision = "";
+
+  @state()
+  private promptOption: "suggested" | "original" | "new" = "suggested";
+
+  @state()
+  private commitDescriptions: Record<string, string> = {
+    current: "",
+    initial: "",
+  };
+
+  @state()
+  private suggestedPrompt = "";
+
+  @state()
+  private originalPrompt = "";
+
+  @state()
+  private newPrompt = "";
+
+  @state()
+  private isLoading = false;
+
+  @state()
+  private isSuggestionLoading = false;
+
+  @state()
+  private isOriginalPromptLoading = false;
+
+  @state()
+  private errorMessage = "";
+
+  static styles = css`
+    :host {
+      display: block;
+      font-family:
+        system-ui,
+        -apple-system,
+        BlinkMacSystemFont,
+        "Segoe UI",
+        Roboto,
+        sans-serif;
+    }
+
+    .modal-description {
+      margin: 0 0 20px 0;
+      color: #555;
+      font-size: 14px;
+      line-height: 1.5;
+    }
+
+    .container-message {
+      margin: 10px 0;
+      padding: 8px 12px;
+      background-color: #f8f9fa;
+      border-left: 4px solid #6c757d;
+      color: #555;
+      font-size: 14px;
+      line-height: 1.5;
+      border-radius: 4px;
+    }
+
+    .modal-backdrop {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background-color: rgba(0, 0, 0, 0.5);
+      z-index: 1000;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      opacity: 0;
+      pointer-events: none;
+      transition: opacity 0.2s ease-in-out;
+    }
+
+    .modal-backdrop.open {
+      opacity: 1;
+      pointer-events: auto;
+    }
+
+    .modal-container {
+      background: white;
+      border-radius: 8px;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+      width: 600px;
+      max-width: 90%;
+      max-height: 90vh;
+      overflow-y: auto;
+      padding: 20px;
+    }
+
+    .modal-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 20px;
+      border-bottom: 1px solid #eee;
+      padding-bottom: 10px;
+    }
+
+    .modal-title {
+      font-size: 18px;
+      font-weight: 600;
+      margin: 0;
+    }
+
+    .close-button {
+      background: none;
+      border: none;
+      font-size: 18px;
+      cursor: pointer;
+      color: #666;
+    }
+
+    .close-button:hover {
+      color: #333;
+    }
+
+    .form-group {
+      margin-bottom: 16px;
+    }
+
+    .horizontal-radio-group {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 16px;
+      margin-bottom: 16px;
+    }
+
+    .revision-option {
+      border: 1px solid #e0e0e0;
+      border-radius: 4px;
+      padding: 8px 12px;
+      min-width: 180px;
+      cursor: pointer;
+      transition: all 0.2s;
+    }
+
+    .revision-option label {
+      font-size: 0.9em;
+      font-weight: bold;
+    }
+
+    .revision-option:hover {
+      border-color: #2196f3;
+      background-color: #f5f9ff;
+    }
+
+    .revision-option.selected {
+      border-color: #2196f3;
+      background-color: #e3f2fd;
+    }
+
+    .revision-option input[type="radio"] {
+      margin-right: 8px;
+    }
+
+    .revision-description {
+      margin-top: 4px;
+      color: #666;
+      font-size: 0.8em;
+      font-family: monospace;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      max-width: 200px;
+    }
+
+    .form-group label {
+      display: block;
+      margin-bottom: 8px;
+      font-weight: 500;
+    }
+
+    .radio-group {
+      margin-bottom: 8px;
+    }
+
+    .radio-option {
+      display: flex;
+      align-items: center;
+      margin-bottom: 8px;
+    }
+
+    .radio-option input {
+      margin-right: 8px;
+    }
+
+    .custom-revision {
+      margin-left: 24px;
+      margin-top: 8px;
+      width: calc(100% - 24px);
+      padding: 6px 8px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      display: block;
+    }
+
+    .prompt-container {
+      position: relative;
+      margin-top: 16px;
+    }
+
+    .prompt-textarea {
+      display: block;
+      box-sizing: border-box;
+      width: 100%;
+      min-height: 120px;
+      padding: 8px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      font-family: inherit;
+      resize: vertical;
+      background-color: white;
+      color: #333;
+    }
+
+    .prompt-textarea.disabled {
+      background-color: #f5f5f5;
+      color: #999;
+      cursor: not-allowed;
+    }
+
+    .actions {
+      display: flex;
+      justify-content: flex-end;
+      gap: 12px;
+      margin-top: 20px;
+    }
+
+    .btn {
+      padding: 8px 16px;
+      border-radius: 4px;
+      font-weight: 500;
+      cursor: pointer;
+      border: none;
+    }
+
+    .btn-cancel {
+      background: #f2f2f2;
+      color: #333;
+    }
+
+    .btn-restart {
+      background: #4caf50;
+      color: white;
+    }
+
+    .btn:disabled {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+
+    .error-message {
+      color: #e53935;
+      margin-top: 16px;
+      font-size: 14px;
+    }
+
+    .loading-indicator {
+      display: inline-block;
+      margin-right: 8px;
+      margin-left: 8px;
+      width: 16px;
+      height: 16px;
+      border: 2px solid rgba(255, 255, 255, 0.3);
+      border-radius: 50%;
+      border-top-color: white;
+      animation: spin 1s linear infinite;
+    }
+
+    .prompt-container .loading-overlay {
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background: rgba(255, 255, 255, 0.7);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      z-index: 2;
+      border-radius: 4px;
+    }
+
+    .prompt-container .loading-overlay .loading-indicator {
+      width: 24px;
+      height: 24px;
+      border: 2px solid rgba(0, 0, 0, 0.1);
+      border-top-color: #2196f3;
+      border-radius: 50%;
+      animation: spin 1s linear infinite;
+    }
+
+    .radio-option .loading-indicator {
+      display: inline-block;
+      width: 12px;
+      height: 12px;
+      border: 1.5px solid rgba(0, 0, 0, 0.2);
+      border-top-color: #2196f3;
+      vertical-align: middle;
+      margin-left: 8px;
+    }
+
+    .radio-option .status-ready {
+      display: inline-block;
+      width: 16px;
+      height: 16px;
+      color: #4caf50;
+      margin-left: 8px;
+      font-weight: bold;
+      vertical-align: middle;
+    }
+
+    @keyframes spin {
+      to {
+        transform: rotate(360deg);
+      }
+    }
+  `;
+
+  constructor() {
+    super();
+    this.handleEscape = this.handleEscape.bind(this);
+  }
+
+  connectedCallback() {
+    super.connectedCallback();
+    document.addEventListener("keydown", this.handleEscape);
+  }
+
+  // Handle keyboard navigation
+  firstUpdated() {
+    if (this.shadowRoot) {
+      // Set up proper tab navigation by ensuring all focusable elements are included
+      const focusableElements =
+        this.shadowRoot.querySelectorAll('[tabindex="0"]');
+      if (focusableElements.length > 0) {
+        // Set initial focus when modal opens
+        (focusableElements[0] as HTMLElement).focus();
+      }
+    }
+  }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener("keydown", this.handleEscape);
+  }
+
+  handleEscape(e: KeyboardEvent) {
+    if (e.key === "Escape" && this.open) {
+      this.closeModal();
+    }
+  }
+
+  closeModal() {
+    this.open = false;
+    this.dispatchEvent(new CustomEvent("close"));
+  }
+
+  async loadCommitDescription(
+    revision: string,
+    target: "current" | "initial" | "other" = "other",
+  ) {
+    try {
+      const response = await fetch(
+        `./commit-description?revision=${encodeURIComponent(revision)}`,
+      );
+      if (!response.ok) {
+        throw new Error(
+          `Failed to load commit description: ${response.statusText}`,
+        );
+      }
+
+      const data = await response.json();
+
+      if (target === "other") {
+        // For custom revisions, update the customRevision directly
+        this.customRevision = `${revision.slice(0, 8)} - ${data.description}`;
+      } else {
+        // For known targets, update the commitDescriptions object
+        this.commitDescriptions = {
+          ...this.commitDescriptions,
+          [target]: data.description,
+        };
+      }
+    } catch (error) {
+      console.error(`Error loading commit description for ${revision}:`, error);
+    }
+  }
+
+  handleRevisionChange(e?: Event) {
+    if (e) {
+      const target = e.target as HTMLInputElement;
+      this.restartType = target.value as "initial" | "current" | "other";
+    }
+
+    // Load commit description for any custom revision if needed
+    if (
+      this.restartType === "other" &&
+      this.customRevision &&
+      !this.customRevision.includes(" - ")
+    ) {
+      this.loadCommitDescription(this.customRevision, "other");
+    }
+  }
+
+  handleCustomRevisionChange(e: Event) {
+    const target = e.target as HTMLInputElement;
+    this.customRevision = target.value;
+  }
+
+  handlePromptOptionChange(e: Event) {
+    const target = e.target as HTMLInputElement;
+    this.promptOption = target.value as "suggested" | "original" | "new";
+
+    if (
+      this.promptOption === "suggested" &&
+      !this.isSuggestionLoading &&
+      this.suggestedPrompt === ""
+    ) {
+      this.loadSuggestedPrompt();
+    } else if (
+      this.promptOption === "original" &&
+      !this.isOriginalPromptLoading &&
+      this.originalPrompt === ""
+    ) {
+      this.loadOriginalPrompt();
+    }
+  }
+
+  handleSuggestedPromptChange(e: Event) {
+    const target = e.target as HTMLTextAreaElement;
+    this.suggestedPrompt = target.value;
+  }
+
+  handleOriginalPromptChange(e: Event) {
+    const target = e.target as HTMLTextAreaElement;
+    this.originalPrompt = target.value;
+  }
+
+  handleNewPromptChange(e: Event) {
+    const target = e.target as HTMLTextAreaElement;
+    this.newPrompt = target.value;
+  }
+
+  async loadSuggestedPrompt() {
+    try {
+      this.isSuggestionLoading = true;
+      this.errorMessage = "";
+
+      const response = await fetch("./suggest-reprompt");
+      if (!response.ok) {
+        throw new Error(`Failed to load suggestion: ${response.statusText}`);
+      }
+
+      const data = await response.json();
+      this.suggestedPrompt = data.prompt;
+    } catch (error) {
+      console.error("Error loading suggested prompt:", error);
+      this.errorMessage =
+        error instanceof Error ? error.message : "Failed to load suggestion";
+    } finally {
+      this.isSuggestionLoading = false;
+    }
+  }
+
+  async loadOriginalPrompt() {
+    try {
+      this.isOriginalPromptLoading = true;
+      this.errorMessage = "";
+
+      // Get the first message index from the container state
+      const firstMessageIndex = this.containerState?.first_message_index || 0;
+
+      // Find the first user message after the first_message_index
+      let firstUserMessage = "";
+
+      if (this.messages && this.messages.length > 0) {
+        for (const msg of this.messages) {
+          // Only look at messages starting from first_message_index
+          if (msg.idx >= firstMessageIndex && msg.type === "user") {
+            // Simply use the content field if it's a string
+            if (typeof msg.content === "string") {
+              firstUserMessage = msg.content;
+            } else {
+              // Fallback to stringifying content field for any other type
+              firstUserMessage = JSON.stringify(msg.content);
+            }
+            break;
+          }
+        }
+      }
+
+      if (!firstUserMessage) {
+        console.warn("Could not find original user message", this.messages);
+      }
+
+      this.originalPrompt = firstUserMessage;
+    } catch (error) {
+      console.error("Error loading original prompt:", error);
+      this.errorMessage =
+        error instanceof Error
+          ? error.message
+          : "Failed to load original prompt";
+    } finally {
+      this.isOriginalPromptLoading = false;
+    }
+  }
+
+  async handleRestart() {
+    try {
+      this.isLoading = true;
+      this.errorMessage = "";
+
+      let revision = "";
+      switch (this.restartType) {
+        case "initial":
+          // We'll leave revision empty for this case, backend will handle it
+          break;
+        case "current":
+          // We'll leave revision empty for this case too, backend will use current HEAD
+          break;
+        case "other":
+          revision = this.customRevision.trim();
+          if (!revision) {
+            throw new Error("Please enter a valid revision");
+          }
+          break;
+      }
+
+      // Determine which prompt to use based on selected option
+      let initialPrompt = "";
+      switch (this.promptOption) {
+        case "suggested":
+          initialPrompt = this.suggestedPrompt.trim();
+          if (!initialPrompt && this.isSuggestionLoading) {
+            throw new Error(
+              "Suggested prompt is still loading. Please wait or choose another option.",
+            );
+          }
+          break;
+        case "original":
+          initialPrompt = this.originalPrompt.trim();
+          if (!initialPrompt && this.isOriginalPromptLoading) {
+            throw new Error(
+              "Original prompt is still loading. Please wait or choose another option.",
+            );
+          }
+          break;
+        case "new":
+          initialPrompt = this.newPrompt.trim();
+          break;
+      }
+
+      // Validate we have a prompt when needed
+      if (!initialPrompt && this.promptOption !== "new") {
+        throw new Error(
+          "Unable to get prompt text. Please enter a new prompt or try again.",
+        );
+      }
+
+      const response = await fetch("./restart", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          revision: revision,
+          initial_prompt: initialPrompt,
+        }),
+      });
+
+      if (!response.ok) {
+        const errorText = await response.text();
+        throw new Error(`Failed to restart: ${errorText}`);
+      }
+
+      // Reload the page after successful restart
+      window.location.reload();
+    } catch (error) {
+      console.error("Error restarting conversation:", error);
+      this.errorMessage =
+        error instanceof Error
+          ? error.message
+          : "Failed to restart conversation";
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
+  updated(changedProperties: Map<string, any>) {
+    if (changedProperties.has("open") && this.open) {
+      // Reset form when opening
+      this.restartType = "current";
+      this.customRevision = "";
+      this.promptOption = "suggested";
+      this.suggestedPrompt = "";
+      this.originalPrompt = "";
+      this.newPrompt = "";
+      this.errorMessage = "";
+      this.commitDescriptions = {
+        current: "",
+        initial: "",
+      };
+
+      // Pre-load all available prompts and commit descriptions in the background
+      setTimeout(() => {
+        // Load prompt data
+        this.loadSuggestedPrompt();
+        this.loadOriginalPrompt();
+
+        // Load commit descriptions
+        this.loadCommitDescription("HEAD", "current");
+        if (this.containerState?.initial_commit) {
+          this.loadCommitDescription(
+            this.containerState.initial_commit,
+            "initial",
+          );
+        }
+
+        // Set focus to the first radio button for keyboard navigation
+        if (this.shadowRoot) {
+          const firstInput = this.shadowRoot.querySelector(
+            'input[type="radio"]',
+          ) as HTMLElement;
+          if (firstInput) {
+            firstInput.focus();
+          }
+        }
+      }, 0);
+    }
+  }
+
+  render() {
+    const inContainer = this.containerState?.in_container || false;
+
+    return html`
+      <div class="modal-backdrop ${this.open ? "open" : ""}">
+        <div class="modal-container">
+          <div class="modal-header">
+            <h2 class="modal-title">Restart Conversation</h2>
+            <button class="close-button" @click=${this.closeModal}>×</button>
+          </div>
+
+          <p class="modal-description">
+            Restarting the conversation hides the history from the agent. If you
+            want the agent to take a different direction, restart with a new
+            prompt.
+          </p>
+
+          <div class="form-group">
+            <label>Reset to which revision?</label>
+            <div class="horizontal-radio-group">
+              <div
+                class="revision-option ${this.restartType === "current"
+                  ? "selected"
+                  : ""}"
+                @click=${() => {
+                  this.restartType = "current";
+                  this.handleRevisionChange();
+                }}
+              >
+                <input
+                  type="radio"
+                  id="restart-current"
+                  name="restart-type"
+                  value="current"
+                  ?checked=${this.restartType === "current"}
+                  @change=${this.handleRevisionChange}
+                  tabindex="0"
+                />
+                <label for="restart-current">Current HEAD</label>
+                ${this.commitDescriptions.current
+                  ? html`<div class="revision-description">
+                      ${this.commitDescriptions.current}
+                    </div>`
+                  : ""}
+              </div>
+
+              ${inContainer
+                ? html`
+                    <div
+                      class="revision-option ${this.restartType === "initial"
+                        ? "selected"
+                        : ""}"
+                      @click=${() => {
+                        this.restartType = "initial";
+                        this.handleRevisionChange();
+                      }}
+                    >
+                      <input
+                        type="radio"
+                        id="restart-initial"
+                        name="restart-type"
+                        value="initial"
+                        ?checked=${this.restartType === "initial"}
+                        @change=${this.handleRevisionChange}
+                        tabindex="0"
+                      />
+                      <label for="restart-initial">Initial commit</label>
+                      ${this.commitDescriptions.initial
+                        ? html`<div class="revision-description">
+                            ${this.commitDescriptions.initial}
+                          </div>`
+                        : ""}
+                    </div>
+
+                    <div
+                      class="revision-option ${this.restartType === "other"
+                        ? "selected"
+                        : ""}"
+                      @click=${() => {
+                        this.restartType = "other";
+                        this.handleRevisionChange();
+                      }}
+                    >
+                      <input
+                        type="radio"
+                        id="restart-other"
+                        name="restart-type"
+                        value="other"
+                        ?checked=${this.restartType === "other"}
+                        @change=${this.handleRevisionChange}
+                        tabindex="0"
+                      />
+                      <label for="restart-other">Other revision</label>
+                    </div>
+                  `
+                : html`
+                    <div class="container-message">
+                      Additional revision options are not available because
+                      Sketch is not running inside a container.
+                    </div>
+                  `}
+            </div>
+
+            ${this.restartType === "other" && inContainer
+              ? html`
+                  <input
+                    type="text"
+                    class="custom-revision"
+                    placeholder="Enter commit hash"
+                    .value=${this.customRevision}
+                    @input=${this.handleCustomRevisionChange}
+                    tabindex="0"
+                  />
+                `
+              : ""}
+          </div>
+
+          <div class="form-group">
+            <label>Prompt options:</label>
+            <div class="radio-group">
+              <div class="radio-option">
+                <input
+                  type="radio"
+                  id="prompt-suggested"
+                  name="prompt-type"
+                  value="suggested"
+                  ?checked=${this.promptOption === "suggested"}
+                  @change=${this.handlePromptOptionChange}
+                  tabindex="0"
+                />
+                <label for="prompt-suggested">
+                  Suggest prompt based on history (default)
+                </label>
+              </div>
+
+              <div class="radio-option">
+                <input
+                  type="radio"
+                  id="prompt-original"
+                  name="prompt-type"
+                  value="original"
+                  ?checked=${this.promptOption === "original"}
+                  @change=${this.handlePromptOptionChange}
+                  tabindex="0"
+                />
+                <label for="prompt-original"> Original prompt </label>
+              </div>
+
+              <div class="radio-option">
+                <input
+                  type="radio"
+                  id="prompt-new"
+                  name="prompt-type"
+                  value="new"
+                  ?checked=${this.promptOption === "new"}
+                  @change=${this.handlePromptOptionChange}
+                  tabindex="0"
+                />
+                <label for="prompt-new">New prompt</label>
+              </div>
+            </div>
+          </div>
+
+          <div class="prompt-container">
+            ${this.promptOption === "suggested"
+              ? html`
+                  <textarea
+                    class="prompt-textarea${this.isSuggestionLoading
+                      ? " disabled"
+                      : ""}"
+                    placeholder="Loading suggested prompt..."
+                    .value=${this.suggestedPrompt}
+                    ?disabled=${this.isSuggestionLoading}
+                    @input=${this.handleSuggestedPromptChange}
+                    tabindex="0"
+                  ></textarea>
+                  ${this.isSuggestionLoading
+                    ? html`
+                        <div class="loading-overlay">
+                          <div class="loading-indicator"></div>
+                        </div>
+                      `
+                    : ""}
+                `
+              : this.promptOption === "original"
+                ? html`
+                    <textarea
+                      class="prompt-textarea${this.isOriginalPromptLoading
+                        ? " disabled"
+                        : ""}"
+                      placeholder="Loading original prompt..."
+                      .value=${this.originalPrompt}
+                      ?disabled=${this.isOriginalPromptLoading}
+                      @input=${this.handleOriginalPromptChange}
+                      tabindex="0"
+                    ></textarea>
+                    ${this.isOriginalPromptLoading
+                      ? html`
+                          <div class="loading-overlay">
+                            <div class="loading-indicator"></div>
+                          </div>
+                        `
+                      : ""}
+                  `
+                : html`
+                    <textarea
+                      class="prompt-textarea"
+                      placeholder="Enter a new prompt..."
+                      .value=${this.newPrompt}
+                      @input=${this.handleNewPromptChange}
+                      tabindex="0"
+                    ></textarea>
+                  `}
+          </div>
+
+          ${this.errorMessage
+            ? html` <div class="error-message">${this.errorMessage}</div> `
+            : ""}
+
+          <div class="actions">
+            <button
+              class="btn btn-cancel"
+              @click=${this.closeModal}
+              ?disabled=${this.isLoading}
+              tabindex="0"
+            >
+              Cancel
+            </button>
+            <button
+              class="btn btn-restart"
+              @click=${this.handleRestart}
+              ?disabled=${this.isLoading}
+              tabindex="0"
+            >
+              ${this.isLoading
+                ? html`<span class="loading-indicator"></span>`
+                : ""}
+              Restart
+            </button>
+          </div>
+        </div>
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "sketch-restart-modal": SketchRestartModal;
+  }
+}