webui: add Jump to Bottom button for mobile and desktop chat views

Add a floating Jump to Bottom button that appears when users scroll up
from the bottom of chat interfaces, positioned at the boundary between
chat content and input areas.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sba26f10bfcccf392k
diff --git a/webui/src/web-components/demo/mobile-chat.demo.html b/webui/src/web-components/demo/mobile-chat.demo.html
new file mode 100644
index 0000000..5fc1800
--- /dev/null
+++ b/webui/src/web-components/demo/mobile-chat.demo.html
@@ -0,0 +1,102 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Mobile Chat Demo</title>
+    <style>
+      body {
+        margin: 0;
+        padding: 0;
+        height: 100vh;
+        font-family:
+          -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
+      }
+    </style>
+    <script type="module">
+      import "../mobile-chat.js";
+
+      const chatElement = document.querySelector("mobile-chat");
+
+      // Simulate multiple messages to test scroll behavior
+      const messages = [
+        {
+          id: "1",
+          type: "user",
+          content: "Hello, I need help with setting up a project.",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "2",
+          type: "agent",
+          content:
+            "Hello! I'd be happy to help you set up a project. What kind of project are you working on?",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "3",
+          type: "user",
+          content:
+            "I'm trying to build a web application using React and TypeScript.",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "4",
+          type: "agent",
+          content:
+            "Great choice! React with TypeScript is a powerful combination. Let me help you get started. First, we'll need to set up the project structure. Here are the steps:\n\n1. Create a new React app with TypeScript template\n2. Install additional dependencies\n3. Set up the folder structure\n4. Configure the development environment\n\nWould you like me to walk you through each step?",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "5",
+          type: "user",
+          content:
+            "Yes, please walk me through each step. I want to make sure I understand everything.",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "6",
+          type: "agent",
+          content:
+            "Perfect! Let's start with step 1:\n\n**Step 1: Create a new React app with TypeScript template**\n\nOpen your terminal and run:\n```bash\nnpx create-react-app my-app --template typescript\ncd my-app\n```\n\nThis will create a new React application with TypeScript already configured. The `--template typescript` flag tells Create React App to set up TypeScript for you automatically.",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "7",
+          type: "user",
+          content:
+            "Okay, I ran that command and it created the project. What's next?",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "8",
+          type: "agent",
+          content:
+            "Excellent! Now let's move to step 2:\n\n**Step 2: Install additional dependencies**\n\nYou might want to install some commonly used packages. Here are some recommendations:\n\n```bash\n# For routing\nnpm install react-router-dom\nnpm install @types/react-router-dom\n\n# For styling (choose one)\nnpm install styled-components  # or\nnpm install @mui/material\n\n# For state management (if needed)\nnpm install redux @reduxjs/toolkit react-redux\n```\n\nWhich of these sound useful for your project?",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "9",
+          type: "user",
+          content:
+            "I think I'll need routing and probably Material-UI for styling. Should I install both?",
+          timestamp: new Date().toISOString(),
+        },
+        {
+          id: "10",
+          type: "agent",
+          content:
+            "Absolutely! React Router and Material-UI work great together. Run these commands:\n\n```bash\nnpm install react-router-dom @types/react-router-dom\nnpm install @mui/material @emotion/react @emotion/styled\nnpm install @mui/icons-material\n```\n\nMaterial-UI (now called MUI) requires @emotion packages for styling, and the icons package gives you access to thousands of Material Design icons.",
+          timestamp: new Date().toISOString(),
+        },
+      ];
+
+      setTimeout(() => {
+        chatElement.messages = messages;
+      }, 100);
+    </script>
+  </head>
+  <body>
+    <mobile-chat style="height: 100vh; display: block"></mobile-chat>
+  </body>
+</html>
diff --git a/webui/src/web-components/mobile-chat.ts b/webui/src/web-components/mobile-chat.ts
index 74381cc..26ef963 100644
--- a/webui/src/web-components/mobile-chat.ts
+++ b/webui/src/web-components/mobile-chat.ts
@@ -16,6 +16,9 @@
 
   private scrollContainer = createRef<HTMLDivElement>();
 
+  @state()
+  private showJumpToBottom = false;
+
   static styles = css`
     :host {
       display: block;
@@ -288,6 +291,45 @@
       flex-shrink: 0;
       margin-left: 4px;
     }
+
+    .jump-to-bottom {
+      position: fixed;
+      bottom: 70px;
+      left: 50%;
+      transform: translateX(-50%);
+      background-color: rgba(0, 0, 0, 0.6);
+      color: white;
+      border: none;
+      border-radius: 12px;
+      padding: 4px 8px;
+      font-size: 11px;
+      font-weight: 400;
+      cursor: pointer;
+      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+      z-index: 1100;
+      transition: all 0.15s ease;
+      display: flex;
+      align-items: center;
+      gap: 4px;
+      opacity: 0.8;
+    }
+
+    .jump-to-bottom:hover {
+      background-color: rgba(0, 0, 0, 0.8);
+      transform: translateX(-50%) translateY(-1px);
+      opacity: 1;
+      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+    }
+
+    .jump-to-bottom:active {
+      transform: translateX(-50%) translateY(0);
+    }
+
+    .jump-to-bottom.hidden {
+      opacity: 0;
+      pointer-events: none;
+      transform: translateX(-50%) translateY(10px);
+    }
   `;
 
   updated(changedProperties: Map<string, any>) {
@@ -298,6 +340,14 @@
     ) {
       this.scrollToBottom();
     }
+
+    // Set up scroll listener if not already done
+    if (this.scrollContainer.value && !this.scrollContainer.value.onscroll) {
+      this.scrollContainer.value.addEventListener(
+        "scroll",
+        this.handleScroll.bind(this),
+      );
+    }
   }
 
   private scrollToBottom() {
@@ -310,6 +360,21 @@
     });
   }
 
+  private handleScroll() {
+    if (!this.scrollContainer.value) return;
+
+    const container = this.scrollContainer.value;
+    const isAtBottom =
+      container.scrollTop + container.clientHeight >=
+      container.scrollHeight - 50; // 50px tolerance
+
+    this.showJumpToBottom = !isAtBottom;
+  }
+
+  private jumpToBottom() {
+    this.scrollToBottom();
+  }
+
   private formatTime(timestamp: string): string {
     const date = new Date(timestamp);
     return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
@@ -516,6 +581,16 @@
     return "";
   }
 
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    if (this.scrollContainer.value) {
+      this.scrollContainer.value.removeEventListener(
+        "scroll",
+        this.handleScroll.bind(this),
+      );
+    }
+  }
+
   render() {
     const displayMessages = this.messages.filter((msg) =>
       this.shouldShowMessage(msg),
@@ -560,6 +635,18 @@
             `
           : ""}
       </div>
+
+      ${this.showJumpToBottom
+        ? html`
+            <button
+              class="jump-to-bottom"
+              @click=${this.jumpToBottom}
+              aria-label="Jump to bottom"
+            >
+              ↓ Jump to bottom
+            </button>
+          `
+        : ""}
     `;
   }
 }
diff --git a/webui/src/web-components/sketch-timeline.ts b/webui/src/web-components/sketch-timeline.ts
index 964574e..77aff74 100644
--- a/webui/src/web-components/sketch-timeline.ts
+++ b/webui/src/web-components/sketch-timeline.ts
@@ -125,21 +125,32 @@
     }
     #jump-to-latest {
       display: none;
-      position: absolute;
-      bottom: 20px;
-      right: 20px;
-      background: rgb(33, 150, 243);
+      position: fixed;
+      bottom: 80px; /* Position right on the boundary */
+      left: 50%;
+      transform: translateX(-50%);
+      background: rgba(0, 0, 0, 0.6);
       color: white;
-      border-radius: 8px;
-      padding: 0.5em;
-      margin: 0.5em;
-      font-size: x-large;
-      opacity: 0.5;
+      border: none;
+      border-radius: 12px;
+      padding: 4px 8px;
+      font-size: 11px;
+      font-weight: 400;
       cursor: pointer;
-      z-index: 50;
+      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+      z-index: 1000;
+      transition: all 0.15s ease;
+      white-space: nowrap;
+      opacity: 0.8;
     }
     #jump-to-latest:hover {
+      background-color: rgba(0, 0, 0, 0.8);
+      transform: translateX(-50%) translateY(-1px);
       opacity: 1;
+      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+    }
+    #jump-to-latest:active {
+      transform: translateX(-50%) translateY(0);
     }
     #jump-to-latest.floating {
       display: block;
@@ -1060,7 +1071,7 @@
           class="${this.scrollingState}"
           @click=${this.scrollToBottomWithRetry}
         >
-          ⇩
+          ↓ Jump to bottom
         </div>
       </div>
     `;