sketch: "git push" button

Ultimately, we want to allow users to push their changes to github, and
thereby do a good chunk of work without resorting to the terminal (and
figuring out how to move the git references around, which requires a
bunch of esotiric and annoying expertise).

This commit introduces:

1. For outtie's HTTP server (which is now comically Go HTTP ->
   CGI-effing-bin -> git -> shell script -> git in this case), there's a
   custom git hook that forwards changes to refs/remotes/origin/foo to
   origin/foo. This is a git proxy of sorts. By forwarding the
   SSH_AUTH_SOCK, we can use outtie's auth options without giving innie
   the actual credentials. This works by creating a temporary directory
   for git hooks (for outtie).

2. Innie sets up a new remote, "upstream" when a "passthrough-upstream"
   flag is pasksed. This remote kind of looks like the real upstream (so
   upstream/foo) is fetched. This will let the agent handle rebases
   better.

3. Innie exposes a /pushinfo handler that returns the list of remotes
   and the current commit and such. These have nice display names for
   the outtie's machine and github if useful.

   There's also a /push handler. This is the thing that knows about the
   refs/remotes/origin/foo thing. There's no magic git push refspec that
   makes this all work without that, I think. (Maybe there is? I don't
   think there is.)

   Note that there's been some changes about what the remotes look like,
   and when we use the remotes and when we use agent.GitOrigin().
   We may be able to simplify this by using git's insteadof
   configurations, but I think it's fine.

4. The web UI exposes a button to push, choose the remote and branch,
   and such. If it can't do the push, you'll get a button to try to get
   the agent to rebase.

   We don't allow force pushes in the UI. We're treating those
   as an advanced feature, and, if you need to do that, you can
   figure it out.

This was collaboration with a gazillion sketch sessions.
diff --git a/webui/src/web-components/demo/mock-git-data-service.ts b/webui/src/web-components/demo/mock-git-data-service.ts
index 70bf26b..92fae9e 100644
--- a/webui/src/web-components/demo/mock-git-data-service.ts
+++ b/webui/src/web-components/demo/mock-git-data-service.ts
@@ -10,6 +10,8 @@
 export class MockGitDataService implements GitDataService {
   constructor() {
     console.log("MockGitDataService instance created");
+    // Setup mock push endpoints when service is created
+    setupMockPushEndpoints();
   }
 
   // Mock commit history
@@ -573,3 +575,70 @@
     ];
   }
 }
+
+// Mock HTTP endpoints for push demo
+export function setupMockPushEndpoints() {
+  // Mock the git/pushinfo endpoint
+  const originalFetch = window.fetch;
+
+  window.fetch = async (url: RequestInfo | URL, init?: RequestInit) => {
+    const urlString = typeof url === "string" ? url : url.toString();
+
+    // Mock pushinfo endpoint
+    if (urlString.includes("/git/pushinfo")) {
+      await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate network delay
+
+      return new Response(
+        JSON.stringify({
+          hash: "abc123456789",
+          subject: "Implement new file picker UI",
+          remotes: [
+            {
+              name: "origin",
+              url: "https://github.com/boldsoftware/bold.git",
+              display_name: "boldsoftware/bold",
+              is_github: true,
+            },
+            {
+              name: "upstream",
+              url: "https://github.com/anotheruser/bold.git",
+              display_name: "anotheruser/bold",
+              is_github: true,
+            },
+          ],
+        }),
+        {
+          status: 200,
+          headers: { "Content-Type": "application/json" },
+        },
+      );
+    }
+
+    // Mock push endpoint
+    if (urlString.includes("/git/push")) {
+      await new Promise((resolve) => setTimeout(resolve, 1500)); // Simulate push delay
+
+      const body = init?.body ? JSON.parse(init.body as string) : {};
+      const isDryRun = body.dry_run || false;
+
+      const mockOutput = isDryRun
+        ? `To https://github.com/boldsoftware/bold.git\n   abc1234..def5678  ${body.branch || "main"} -> ${body.branch || "main"} (dry-run)`
+        : `To https://github.com/boldsoftware/bold.git\n   abc1234..def5678  ${body.branch || "main"} -> ${body.branch || "main"}\n\nCreate a pull request for '${body.branch || "main"}' on GitHub by visiting:\n  https://github.com/boldsoftware/bold/pull/new/${body.branch || "main"}`;
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          output: mockOutput,
+          dry_run: isDryRun,
+        }),
+        {
+          status: 200,
+          headers: { "Content-Type": "application/json" },
+        },
+      );
+    }
+
+    // Fall back to original fetch for other requests
+    return originalFetch(url, init);
+  };
+}
diff --git a/webui/src/web-components/demo/sketch-push-button.demo.html b/webui/src/web-components/demo/sketch-push-button.demo.html
new file mode 100644
index 0000000..41f750b
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-push-button.demo.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Push Button Demo</title>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <style>
+      body {
+        font-family:
+          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+        margin: 0;
+        padding: 20px;
+        background-color: #f9fafb;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="max-w-4xl mx-auto">
+      <h1 class="text-2xl font-bold mb-8 text-gray-800">
+        Push Button Component Demo
+      </h1>
+
+      <div class="bg-white rounded-lg shadow-lg p-6">
+        <sketch-push-button-demo></sketch-push-button-demo>
+      </div>
+    </div>
+
+    <script type="module" src="./sketch-push-button.demo.ts"></script>
+  </body>
+</html>
diff --git a/webui/src/web-components/demo/sketch-push-button.demo.ts b/webui/src/web-components/demo/sketch-push-button.demo.ts
new file mode 100644
index 0000000..9b54c01
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-push-button.demo.ts
@@ -0,0 +1,47 @@
+import { html, LitElement } from "lit";
+import { customElement, state } from "lit/decorators.js";
+import { MockGitDataService } from "./mock-git-data-service.js";
+import "../sketch-push-button.js";
+
+@customElement("sketch-push-button-demo")
+export class SketchPushButtonDemo extends LitElement {
+  @state()
+  private _gitDataService = new MockGitDataService();
+
+  protected createRenderRoot() {
+    return this;
+  }
+
+  render() {
+    return html`
+      <div
+        class="p-4 bg-white rounded-lg shadow-sm border border-gray-200 max-w-md mx-auto"
+      >
+        <h2 class="text-lg font-semibold mb-4">Push Button Demo</h2>
+
+        <div class="mb-4">
+          <p class="text-sm text-gray-600 mb-2">
+            Test the push button component:
+          </p>
+          <sketch-push-button></sketch-push-button>
+        </div>
+
+        <div class="text-xs text-gray-500">
+          <p>Click the push button to test:</p>
+          <ul class="list-disc list-inside mt-1">
+            <li>Modal opens with git information</li>
+            <li>Input fields can be disabled during loading</li>
+            <li>Buttons show individual spinners</li>
+            <li>No full modal overwrite during operations</li>
+          </ul>
+        </div>
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "sketch-push-button-demo": SketchPushButtonDemo;
+  }
+}
diff --git a/webui/src/web-components/sketch-app-shell-base.ts b/webui/src/web-components/sketch-app-shell-base.ts
index 3e3dbe3..3ddc183 100644
--- a/webui/src/web-components/sketch-app-shell-base.ts
+++ b/webui/src/web-components/sketch-app-shell-base.ts
@@ -15,6 +15,7 @@
 import "./sketch-monaco-view";
 import "./sketch-network-status";
 import "./sketch-call-status";
+import "./sketch-push-button";
 import "./sketch-terminal";
 import "./sketch-timeline";
 import "./sketch-view-mode-select";
@@ -156,6 +157,7 @@
     this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
     this._handleMutlipleChoiceSelected =
       this._handleMutlipleChoiceSelected.bind(this);
+    this._handlePushRebaseRequest = this._handlePushRebaseRequest.bind(this);
     this._handleStopClick = this._handleStopClick.bind(this);
     this._handleEndClick = this._handleEndClick.bind(this);
     this._handleNotificationsToggle =
@@ -203,6 +205,10 @@
       "multiple-choice-selected",
       this._handleMutlipleChoiceSelected,
     );
+    window.addEventListener(
+      "push-rebase-request",
+      this._handlePushRebaseRequest,
+    );
 
     // register event listeners
     this.dataManager.addEventListener(
@@ -251,6 +257,10 @@
       "multiple-choice-selected",
       this._handleMutlipleChoiceSelected,
     );
+    window.removeEventListener(
+      "push-rebase-request",
+      this._handlePushRebaseRequest,
+    );
 
     // unregister data manager event listeners
     this.dataManager.removeEventListener(
@@ -770,6 +780,25 @@
     }
   }
 
+  async _handlePushRebaseRequest(e: CustomEvent) {
+    const chatInput = this.querySelector(
+      "sketch-chat-input",
+    ) as SketchChatInput;
+    if (chatInput) {
+      if (chatInput.content && chatInput.content.trim() !== "") {
+        chatInput.content += "\n\n";
+      }
+      chatInput.content += e.detail.message;
+      chatInput.focus();
+      // Adjust textarea height to accommodate new content
+      requestAnimationFrame(() => {
+        if (chatInput.adjustChatSpacing) {
+          chatInput.adjustChatSpacing();
+        }
+      });
+    }
+  }
+
   async _sendChat(e: CustomEvent) {
     console.log("app shell: _sendChat", e);
     e.preventDefault();
diff --git a/webui/src/web-components/sketch-container-status.ts b/webui/src/web-components/sketch-container-status.ts
index 2bd9935..bd9524b 100644
--- a/webui/src/web-components/sketch-container-status.ts
+++ b/webui/src/web-components/sketch-container-status.ts
@@ -3,6 +3,7 @@
 import { customElement, property, state } from "lit/decorators.js";
 import { formatNumber } from "../utils";
 import { SketchTailwindElement } from "./sketch-tailwind-element";
+import "./sketch-push-button";
 
 @customElement("sketch-container-status")
 export class SketchContainerStatus extends SketchTailwindElement {
@@ -645,6 +646,9 @@
           `;
         })()}
 
+        <!-- Push button -->
+        <sketch-push-button class="ml-2"></sketch-push-button>
+
         <!-- Info toggle button -->
         <button
           class="info-toggle ml-2 w-6 h-6 rounded-full flex items-center justify-center ${this
@@ -665,6 +669,7 @@
           class="${this.showDetails
             ? "block"
             : "hidden"} absolute min-w-max top-full z-100 bg-white rounded-lg p-4 shadow-lg mt-1.5"
+          style="left: 50%; transform: translateX(-50%);"
         >
           <!-- Last Commit section moved to main grid -->
 
diff --git a/webui/src/web-components/sketch-push-button.ts b/webui/src/web-components/sketch-push-button.ts
new file mode 100644
index 0000000..b43ca4b
--- /dev/null
+++ b/webui/src/web-components/sketch-push-button.ts
@@ -0,0 +1,537 @@
+import { html } from "lit";
+import { customElement, state } from "lit/decorators.js";
+import { SketchTailwindElement } from "./sketch-tailwind-element.js";
+import type { Remote } from "../types.js";
+
+@customElement("sketch-push-button")
+export class SketchPushButton extends SketchTailwindElement {
+  @state()
+  private _modalOpen = false;
+
+  @state()
+  private _loading = false;
+
+  @state()
+  private _pushingAction: "dry-run" | "push" | null = null;
+
+  @state()
+  private _headCommit: { hash: string; subject: string } | null = null;
+
+  @state()
+  private _remotes: Remote[] = [];
+
+  @state()
+  private _selectedRemote = "";
+
+  @state()
+  private _branch = "";
+
+  @state()
+  private _pushResult: {
+    success: boolean;
+    output: string;
+    error?: string;
+    dry_run: boolean;
+  } | null = null;
+
+  private async _openModal() {
+    this._modalOpen = true;
+    this._loading = true;
+    this._pushResult = null;
+
+    try {
+      // Fetch push info (HEAD commit and remotes)
+      const response = await fetch("./git/pushinfo");
+      if (response.ok) {
+        const data = await response.json();
+        this._headCommit = {
+          hash: data.hash,
+          subject: data.subject,
+        };
+        this._remotes = data.remotes;
+
+        // Auto-select first remote if available
+        if (this._remotes.length > 0) {
+          this._selectedRemote = this._remotes[0].name;
+        }
+      }
+    } catch (error) {
+      console.error("Error fetching git data:", error);
+    } finally {
+      this._loading = false;
+    }
+  }
+
+  private _closeModal() {
+    this._modalOpen = false;
+    this._pushResult = null;
+  }
+
+  private _clickOutsideHandler = (event: MouseEvent) => {
+    if (this._modalOpen && !this.contains(event.target as Node)) {
+      this._closeModal();
+    }
+  };
+
+  // Close the modal when clicking outside
+  connectedCallback() {
+    super.connectedCallback();
+    document.addEventListener("click", this._clickOutsideHandler);
+  }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener("click", this._clickOutsideHandler);
+  }
+
+  private async _handlePush(dryRun: boolean = false, event?: Event) {
+    if (event) {
+      event.stopPropagation();
+    }
+
+    if (!this._selectedRemote || !this._branch || !this._headCommit) {
+      return;
+    }
+
+    this._loading = true;
+    this._pushingAction = dryRun ? "dry-run" : "push";
+
+    try {
+      const response = await fetch("./git/push", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          remote: this._selectedRemote,
+          branch: this._branch,
+          commit: this._headCommit.hash,
+          dry_run: dryRun,
+        }),
+      });
+
+      if (response.ok) {
+        this._pushResult = await response.json();
+      } else {
+        this._pushResult = {
+          success: false,
+          output: "",
+          error: `HTTP ${response.status}: ${response.statusText}`,
+          dry_run: dryRun,
+        };
+      }
+    } catch (error) {
+      this._pushResult = {
+        success: false,
+        output: "",
+        error: `Network error: ${error}`,
+        dry_run: dryRun,
+      };
+    } finally {
+      this._loading = false;
+      this._pushingAction = null;
+    }
+  }
+
+  private _handleRebase(event?: Event) {
+    if (event) {
+      event.stopPropagation();
+    }
+
+    // Send message to chat asking agent to rebase
+    const message = `fetch and rebase onto ${this._selectedRemote}/${this._branch}; force tag ${this._selectedRemote}/${this._branch} as the new sketch-base`;
+
+    // Dispatch custom event to send message to chat
+    const chatEvent = new CustomEvent("push-rebase-request", {
+      detail: { message },
+      bubbles: true,
+      composed: true,
+    });
+
+    window.dispatchEvent(chatEvent);
+  }
+
+  private _formatRemoteDisplay(remote: Remote): string {
+    return `${remote.display_name} (${remote.name})`;
+  }
+
+  private _renderRemoteDisplay(remote: Remote) {
+    const displayText = this._formatRemoteDisplay(remote);
+    if (remote.is_github) {
+      const githubURL = `https://github.com/${remote.display_name}`;
+      if (githubURL) {
+        return html`<a
+          href="${githubURL}"
+          target="_blank"
+          class="text-blue-600 hover:text-blue-800 underline"
+          >${displayText}</a
+        >`;
+      }
+    }
+    return html`<span>${displayText}</span>`;
+  }
+
+  private _makeLinksClickable(output: string): string {
+    // Regex to match http:// or https:// URLs
+    return output.replace(/(https?:\/\/[^\s]+)/g, (match) => {
+      // Clean up URL (remove trailing punctuation)
+      const cleanURL = match.replace(/[.,!?;]+$/, "");
+      const trailingPunctuation = match.substring(cleanURL.length);
+      return `<a href="${cleanURL}" target="_blank" class="text-blue-600 hover:text-blue-800 underline">${cleanURL}</a>${trailingPunctuation}`;
+    });
+  }
+
+  private _getSelectedRemote(): Remote | null {
+    return this._remotes.find((r) => r.name === this._selectedRemote) || null;
+  }
+
+  private _computeBranchURL(): string {
+    const selectedRemote = this._getSelectedRemote();
+    if (!selectedRemote) {
+      return "";
+    }
+    return `https://github.com/${selectedRemote?.display_name}/tree/${this._branch}`;
+  }
+
+  private _renderRemoteSelection() {
+    if (this._remotes.length === 0) {
+      return html``;
+    }
+
+    if (this._remotes.length === 1) {
+      // Single remote - just show it, no selection needed
+      const remote = this._remotes[0];
+      if (!this._selectedRemote) {
+        this._selectedRemote = remote.name;
+      }
+      return html`
+        <div class="mb-3">
+          <label class="block text-xs font-medium mb-1">Remote:</label>
+          <div class="p-2 bg-gray-50 rounded text-xs text-gray-700">
+            ${this._renderRemoteDisplay(remote)}
+          </div>
+        </div>
+      `;
+    }
+
+    if (this._remotes.length === 2) {
+      // Two remotes - use radio buttons
+      return html`
+        <div class="mb-3">
+          <label class="block text-xs font-medium mb-1">Remote:</label>
+          <div class="space-y-2">
+            ${this._remotes.map(
+              (remote) => html`
+                <label class="flex items-center space-x-2 cursor-pointer">
+                  <input
+                    type="radio"
+                    name="remote"
+                    .value=${remote.name}
+                    .checked=${remote.name === this._selectedRemote}
+                    ?disabled=${this._loading}
+                    @change=${(e: Event) => {
+                      this._selectedRemote = (
+                        e.target as HTMLInputElement
+                      ).value;
+                    }}
+                    class="text-blue-600 focus:ring-blue-500"
+                  />
+                  <span class="text-xs text-gray-700"
+                    >${this._renderRemoteDisplay(remote)}</span
+                  >
+                </label>
+              `,
+            )}
+          </div>
+        </div>
+      `;
+    }
+
+    // Three or more remotes - use dropdown
+    return html`
+      <div class="mb-3">
+        <label class="block text-xs font-medium mb-1">Remote:</label>
+        <select
+          .value=${this._selectedRemote}
+          ?disabled=${this._loading}
+          @change=${(e: Event) => {
+            this._selectedRemote = (e.target as HTMLSelectElement).value;
+          }}
+          class="w-full p-2 border border-gray-300 rounded text-xs focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+        >
+          <option value="">Select a remote...</option>
+          ${this._remotes.map(
+            (remote) => html`
+              <option
+                value="${remote.name}"
+                ?selected=${remote.name === this._selectedRemote}
+              >
+                ${this._formatRemoteDisplay(remote)}
+              </option>
+            `,
+          )}
+        </select>
+      </div>
+    `;
+  }
+
+  render() {
+    return html`
+      <div class="relative">
+        <!-- Push Button -->
+        <button
+          @click=${this._openModal}
+          class="flex items-center gap-1.5 px-2 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
+          title="Push current HEAD to remote"
+        >
+          <svg
+            class="w-4 h-4"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="2"
+          >
+            <path d="M12 19V5M5 12l7-7 7 7" />
+          </svg>
+          <span class="max-sm:hidden">Push</span>
+        </button>
+
+        <!-- Overlay Popup -->
+        <div
+          class="${this._modalOpen
+            ? "block"
+            : "hidden"} absolute top-full z-50 bg-white rounded-lg p-4 shadow-lg mt-1.5 border border-gray-200"
+          style="width: 420px; left: 50%; transform: translateX(-50%);"
+        >
+          <div class="flex justify-between items-center mb-3">
+            <h3 class="text-sm font-semibold">Push to Remote</h3>
+            <button
+              @click=${this._closeModal}
+              class="text-gray-500 hover:text-gray-700 transition-colors"
+            >
+              <svg
+                class="w-4 h-4"
+                viewBox="0 0 24 24"
+                fill="none"
+                stroke="currentColor"
+                stroke-width="2"
+              >
+                <path d="M18 6L6 18M6 6l12 12" />
+              </svg>
+            </button>
+          </div>
+
+          ${this._loading && !this._headCommit
+            ? html`
+                <div class="text-center py-4">
+                  <div
+                    class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"
+                  ></div>
+                  <p class="mt-2 text-gray-600 text-xs">Loading...</p>
+                </div>
+              `
+            : html`
+                <!-- Current HEAD info -->
+                ${this._headCommit
+                  ? html`
+                      <div class="mb-3 p-2 bg-gray-50 rounded">
+                        <p class="text-xs">
+                          <span class="text-gray-600 font-mono"
+                            >${this._headCommit.hash.substring(0, 7)}</span
+                          >
+                          <span class="text-gray-800 ml-2"
+                            >${this._headCommit.subject}</span
+                          >
+                        </p>
+                      </div>
+                    `
+                  : ""}
+
+                <!-- Remote selection -->
+                ${this._renderRemoteSelection()}
+
+                <!-- Branch input -->
+                <div class="mb-3">
+                  <label class="block text-xs font-medium mb-1">Branch:</label>
+                  <input
+                    type="text"
+                    .value=${this._branch}
+                    ?disabled=${this._loading}
+                    @input=${(e: Event) => {
+                      this._branch = (e.target as HTMLInputElement).value;
+                    }}
+                    placeholder="Enter branch name..."
+                    class="w-full p-2 border border-gray-300 rounded text-xs focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+                  />
+                </div>
+
+                <!-- Action buttons -->
+                <div class="flex gap-2 mb-3">
+                  <button
+                    @click=${(e: Event) => this._handlePush(true, e)}
+                    ?disabled=${!this._selectedRemote ||
+                    !this._branch ||
+                    !this._headCommit ||
+                    this._loading}
+                    class="flex-1 px-3 py-1.5 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors flex items-center justify-center"
+                  >
+                    ${this._pushingAction === "dry-run"
+                      ? html`
+                          <div
+                            class="animate-spin rounded-full h-3 w-3 border-b border-white mr-1"
+                          ></div>
+                        `
+                      : ""}
+                    Dry Run
+                  </button>
+                  <button
+                    @click=${(e: Event) => this._handlePush(false, e)}
+                    ?disabled=${!this._selectedRemote ||
+                    !this._branch ||
+                    !this._headCommit ||
+                    this._loading}
+                    class="flex-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded text-xs transition-colors flex items-center justify-center"
+                  >
+                    ${this._pushingAction === "push"
+                      ? html`
+                          <div
+                            class="animate-spin rounded-full h-3 w-3 border-b border-white mr-1"
+                          ></div>
+                        `
+                      : ""}
+                    Push
+                  </button>
+                </div>
+
+                <!-- Push result -->
+                ${this._pushResult
+                  ? html`
+                      <div
+                        class="p-3 rounded ${this._pushResult.success
+                          ? "bg-green-50 border border-green-200"
+                          : "bg-red-50 border border-red-200"} relative"
+                      >
+                        ${this._loading
+                          ? html`
+                              <div
+                                class="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center rounded"
+                              >
+                                <div
+                                  class="flex items-center text-xs text-gray-600"
+                                >
+                                  <div
+                                    class="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"
+                                  ></div>
+                                  Processing...
+                                </div>
+                              </div>
+                            `
+                          : ""}
+
+                        <div class="flex items-center justify-between mb-2">
+                          <p
+                            class="text-xs font-medium ${this._pushResult
+                              .success
+                              ? "text-green-800"
+                              : "text-red-800"}"
+                          >
+                            ${this._pushResult.dry_run ? "Dry Run" : "Push"}
+                            ${this._pushResult.success
+                              ? "Successful"
+                              : "Failed"}
+                          </p>
+                          ${this._pushResult.success &&
+                          !this._pushResult.dry_run
+                            ? (() => {
+                                const branchURL = this._computeBranchURL();
+                                return branchURL
+                                  ? html`
+                                      <a
+                                        href="${branchURL}"
+                                        target="_blank"
+                                        class="inline-flex items-center gap-1 px-2 py-1 text-xs bg-gray-900 hover:bg-gray-800 text-white rounded transition-colors"
+                                      >
+                                        <svg
+                                          class="w-3 h-3"
+                                          viewBox="0 0 24 24"
+                                          fill="currentColor"
+                                        >
+                                          <path
+                                            d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"
+                                          />
+                                        </svg>
+                                        Open on GitHub
+                                      </a>
+                                    `
+                                  : "";
+                              })()
+                            : ""}
+                        </div>
+                        ${this._pushResult.output
+                          ? html`
+                              <pre
+                                class="text-xs text-gray-700 whitespace-pre-wrap font-mono mb-2 break-words"
+                                .innerHTML="${this._makeLinksClickable(
+                                  this._pushResult.output,
+                                )}"
+                              ></pre>
+                            `
+                          : ""}
+                        ${this._pushResult.error
+                          ? html`
+                              <p class="text-xs text-red-700 mb-2">
+                                ${this._pushResult.error}
+                              </p>
+                            `
+                          : ""}
+
+                        <div class="flex gap-2 items-center">
+                          ${!this._pushResult.success
+                            ? html`
+                                <button
+                                  @click=${(e: Event) => this._handleRebase(e)}
+                                  class="px-3 py-1 bg-orange-600 hover:bg-orange-700 text-white text-xs rounded transition-colors"
+                                >
+                                  Ask Agent to Rebase
+                                </button>
+                              `
+                            : ""}
+
+                          <button
+                            @click=${(e: Event) => {
+                              e.stopPropagation();
+                              this._closeModal();
+                            }}
+                            class="px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white text-xs rounded transition-colors ml-auto"
+                          >
+                            Close
+                          </button>
+                        </div>
+                      </div>
+                    `
+                  : this._loading
+                    ? html`
+                        <div
+                          class="p-3 rounded bg-gray-50 border border-gray-200"
+                        >
+                          <div class="flex items-center text-xs text-gray-600">
+                            <div
+                              class="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"
+                            ></div>
+                            Processing...
+                          </div>
+                        </div>
+                      `
+                    : ""}
+              `}
+        </div>
+      </div>
+    `;
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    "sketch-push-button": SketchPushButton;
+  }
+}