webui: clean up component demos
diff --git a/webui/package.json b/webui/package.json
index 2d54019..a24fa69 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -14,9 +14,7 @@
   "scripts": {
     "playwright-install": "playwright install",
     "check": "tsc --noEmit",
-    "demo": "vite --open /src/web-components/demo/index.html",
-    "demo:mermaid": "vite --open src/web-components/demo/mermaid-test/index.html",
-    "demo:runner": "vite --open src/web-components/demo/demo-runner.html",
+    "demo": "vite --open src/web-components/demo/demo-runner.html",
     "dev": "vite --port 5173 --strictPort --host 127.0.0.1",
     "format": "prettier ./src --write",
     "gentypes": "go run ../cmd/go2ts -o src/types.ts",
diff --git a/webui/src/web-components/demo/demo-framework/demo-runner.ts b/webui/src/web-components/demo/demo-framework/demo-runner.ts
index 0e08af5..f3dd28a 100644
--- a/webui/src/web-components/demo/demo-framework/demo-runner.ts
+++ b/webui/src/web-components/demo/demo-framework/demo-runner.ts
@@ -101,6 +101,13 @@
       "sketch-tool-calls",
       "sketch-view-mode-select",
       "sketch-theme-toggle",
+      "mobile-chat",
+      "sketch-diff2-view",
+      "sketch-monaco-view",
+      "sketch-network-status",
+      "sketch-timeline-viewport",
+      "sketch-tool-card",
+      "status-indicators",
     ];
 
     // Filter to only components that actually have demo files
diff --git a/webui/src/web-components/demo/index-generated.html b/webui/src/web-components/demo/index-generated.html
index 7460e78..9faf25c 100644
--- a/webui/src/web-components/demo/index-generated.html
+++ b/webui/src/web-components/demo/index-generated.html
@@ -83,7 +83,7 @@
 
     <div class="stats">
       <strong>Auto-generated index</strong><br />
-      Found 3 demo components • Last updated: 6/25/2025, 8:50:21 PM
+      Found 19 demo components • Last updated: 7/20/2025, 10:21:08 PM
     </div>
 
     <p>
@@ -97,6 +97,12 @@
 
     <ul class="demo-list">
       <li>
+        <a href="demo-runner.html#sketch-call-status">
+          <strong>Call Status Demo</strong> - Display current LLM and tool call
+          status with visual indicators
+        </a>
+      </li>
+      <li>
         <a href="demo-runner.html#sketch-chat-input">
           <strong>Chat Input Demo</strong> - Interactive chat input component
           with send functionality
@@ -109,11 +115,101 @@
         </a>
       </li>
       <li>
+        <a href="demo-runner.html#sketch-diff-range-picker">
+          <strong>Diff Range Picker Demo</strong> - Component for selecting
+          commit ranges for diff views
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#mobile-chat">
+          <strong>Mobile Chat Demo</strong> - Mobile chat interface with message
+          display and scroll behavior
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-push-button">
+          <strong>Push Button</strong>
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-app-shell">
+          <strong>Sketch App Shell Demo</strong> - Full sketch application shell
+          with chat, diff, and terminal views
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-diff2-view">
+          <strong>Sketch Monaco Diff View Demo</strong> - Monaco-based diff view
+          with range and file pickers using mock data
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-monaco-view">
+          <strong>Sketch Monaco Viewer Demo</strong> - Monaco editor with code
+          comparison functionality for different languages
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-network-status">
+          <strong>Sketch Network Status Demo</strong> - Status indicators
+          showing different connection and activity states
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-timeline-viewport">
+          <strong>Sketch Timeline Viewport Demo</strong> - Timeline viewport
+          rendering with memory leak protection and event-driven approach
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-tool-card">
+          <strong>Sketch Tool Card Demo</strong> - Demonstration of different
+          tool card components for various tool types
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#status-indicators">
+          <strong>Status Indicators Demo</strong> - Status indicators showing
+          connected, working, and disconnected states without the green
+          connection dot
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-theme-toggle">
+          <strong>Theme Toggle Demo</strong> - Three-way theme toggle: light
+          mode, dark mode, and system preference
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-timeline">
+          <strong>Timeline Demo</strong> - Interactive timeline component for
+          displaying conversation messages with various states
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-timeline-message">
+          <strong>Timeline Message Demo</strong> - Interactive timeline message
+          component with various message types and features
+        </a>
+      </li>
+      <li>
+        <a href="demo-runner.html#sketch-todo-panel">
+          <strong>Todo Panel Demo</strong> - Interactive todo list panel showing
+          task progress and allowing comments
+        </a>
+      </li>
+      <li>
         <a href="demo-runner.html#sketch-tool-calls">
           <strong>Tool Calls Demo</strong> - Interactive tool call display with
           various tool types
         </a>
       </li>
+      <li>
+        <a href="demo-runner.html#sketch-view-mode-select">
+          <strong>View Mode Select Demo</strong> - Interactive tab navigation
+          for switching between chat, diff, and terminal views
+        </a>
+      </li>
     </ul>
 
     <hr style="margin: 40px 0; border: none; border-top: 1px solid #d1d9e0" />
diff --git a/webui/src/web-components/demo/index.html b/webui/src/web-components/demo/index.html
deleted file mode 100644
index cc5697e..0000000
--- a/webui/src/web-components/demo/index.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<html>
-  <head>
-    <link rel="stylesheet" href="demo.css" />
-  </head>
-  <body>
-    sketch web-components demo index
-    <ul>
-      <li><a href="sketch-app-shell.demo.html">sketch-app-shell</a></li>
-      <li><a href="sketch-chat-input.demo.html">sketch-chat-input</a></li>
-
-      <li>
-        <a href="sketch-container-status.demo.html">sketch-container-status</a>
-      </li>
-      <li>
-        <a href="sketch-network-status.demo.html">sketch-network-status</a>
-      </li>
-      <li>
-        <a href="sketch-timeline-message.demo.html">sketch-timeline-message</a>
-      </li>
-      <li><a href="sketch-timeline.demo.html">sketch-timeline</a></li>
-      <li><a href="sketch-tool-calls.demo.html">sketch-tool-calls</a></li>
-      <li><a href="sketch-tool-card.demo.html">sketch-tool-card</a></li>
-      <li>
-        <a href="sketch-view-mode-select.demo.html">sketch-view-mode-select</a>
-      </li>
-      <li>
-        <a href="sketch-monaco-view.demo.html">sketch-monaco-view</a>
-      </li>
-      <li>
-        <a href="sketch-diff2-view.demo.html">sketch-diff2-view</a>
-      </li>
-    </ul>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/mermaid-test/index.html b/webui/src/web-components/demo/mermaid-test/index.html
deleted file mode 100644
index 3635122..0000000
--- a/webui/src/web-components/demo/mermaid-test/index.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Mermaid Diagram Test</title>
-    <script type="module" src="./mermaid-test.ts"></script>
-    <style>
-      body {
-        font-family:
-          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
-          Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
-        padding: 20px;
-        max-width: 1000px;
-        margin: 0 auto;
-      }
-      h1 {
-        color: #333;
-      }
-      .container {
-        border: 1px solid #ddd;
-        border-radius: 4px;
-        padding: 20px;
-        margin: 20px 0;
-      }
-    </style>
-  </head>
-  <body>
-    <h1>Mermaid Diagram Testing</h1>
-    <p>
-      This page tests the integration of Mermaid diagrams in the markdown
-      renderer.
-    </p>
-
-    <div class="container">
-      <mermaid-test-component></mermaid-test-component>
-    </div>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/mermaid-test/mermaid-test.ts b/webui/src/web-components/demo/mermaid-test/mermaid-test.ts
deleted file mode 100644
index b697d92..0000000
--- a/webui/src/web-components/demo/mermaid-test/mermaid-test.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { html, css, LitElement } from "lit";
-import { customElement } from "lit/decorators.js";
-import "../../sketch-timeline-message.ts";
-// Using simple objects matching the AgentMessage interface
-
-@customElement("mermaid-test-component")
-export class MermaidTestComponent extends LitElement {
-  static styles = css`
-    :host {
-      display: block;
-    }
-    .test-section {
-      margin-bottom: 30px;
-    }
-    h2 {
-      margin-top: 0;
-      color: #444;
-    }
-  `;
-
-  render() {
-    // Create a sample message with Mermaid diagrams
-    const flowchartMessage = {
-      id: "test-1",
-      type: "agent",
-      content: `# Mermaid Flowchart Example
-
-This is a test of a flowchart diagram in Mermaid syntax:
-
-\`\`\`mermaid
-graph TD
-    A[Start] --> B{Is it working?}
-    B -->|Yes| C[Great!]
-    B -->|No| D[Try again]
-    C --> E[Continue]
-    D --> B
-\`\`\`
-
-The above should render as a Mermaid diagram.`,
-      timestamp: new Date().toISOString(),
-    };
-
-    const sequenceDiagramMessage = {
-      id: "test-2",
-      type: "agent",
-      content: `# Mermaid Sequence Diagram Example
-
-Here's a sequence diagram showing a typical HTTP request:
-
-\`\`\`mermaid
-sequenceDiagram
-    participant Browser
-    participant Server
-    Browser->>Server: GET /index.html
-    Server-->>Browser: HTML Response
-    Browser->>Server: GET /style.css
-    Server-->>Browser: CSS Response
-    Browser->>Server: GET /script.js
-    Server-->>Browser: JS Response
-\`\`\`
-
-Complex diagrams should render properly.`,
-      timestamp: new Date().toISOString(),
-    };
-
-    const classDiagramMessage = {
-      id: "test-3",
-      type: "agent",
-      content: `# Mermaid Class Diagram Example
-
-A simple class diagram in Mermaid:
-
-\`\`\`mermaid
-classDiagram
-    class Animal {
-        +string name
-        +makeSound() void
-    }
-    class Dog {
-        +bark() void
-    }
-    class Cat {
-        +meow() void
-    }
-    Animal <|-- Dog
-    Animal <|-- Cat
-\`\`\`
-
-This represents a basic inheritance diagram.`,
-      timestamp: new Date().toISOString(),
-    };
-
-    const normalMarkdownMessage = {
-      id: "test-4",
-      type: "agent",
-      content: `# Regular Markdown
-
-This is regular markdown with:
-
-- A bullet list
-- **Bold text**
-- *Italic text*
-
-\`\`\`javascript
-// Regular code block
-const x = 10;
-console.log('This is not Mermaid');
-\`\`\`
-
-Regular markdown should continue to work properly.`,
-      timestamp: new Date().toISOString(),
-    };
-
-    return html`
-      <div class="test-section">
-        <h2>Flowchart Diagram Test</h2>
-        <sketch-timeline-message
-          .message=${flowchartMessage}
-        ></sketch-timeline-message>
-      </div>
-
-      <div class="test-section">
-        <h2>Sequence Diagram Test</h2>
-        <sketch-timeline-message
-          .message=${sequenceDiagramMessage}
-        ></sketch-timeline-message>
-      </div>
-
-      <div class="test-section">
-        <h2>Class Diagram Test</h2>
-        <sketch-timeline-message
-          .message=${classDiagramMessage}
-        ></sketch-timeline-message>
-      </div>
-
-      <div class="test-section">
-        <h2>Normal Markdown Test</h2>
-        <sketch-timeline-message
-          .message=${normalMarkdownMessage}
-        ></sketch-timeline-message>
-      </div>
-    `;
-  }
-}
diff --git a/webui/src/web-components/demo/mobile-chat.demo.html b/webui/src/web-components/demo/mobile-chat.demo.html
deleted file mode 100644
index 5fc1800..0000000
--- a/webui/src/web-components/demo/mobile-chat.demo.html
+++ /dev/null
@@ -1,102 +0,0 @@
-<!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/demo/mobile-chat.demo.ts b/webui/src/web-components/demo/mobile-chat.demo.ts
new file mode 100644
index 0000000..d634752
--- /dev/null
+++ b/webui/src/web-components/demo/mobile-chat.demo.ts
@@ -0,0 +1,115 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Mobile Chat Demo",
+  description: "Mobile chat interface with message display and scroll behavior",
+  imports: ["../mobile-chat.js"],
+
+  customStyles: `
+    body {
+      margin: 0;
+      padding: 0;
+      height: 100vh;
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif;
+    }
+  `,
+
+  setup: async (container: HTMLElement) => {
+    const section = demoUtils.createDemoSection(
+      "Mobile Chat Interface",
+      "Demonstrates mobile chat with multiple messages and scroll behavior",
+    );
+
+    // Create the mobile chat element
+    const chatElement = document.createElement("mobile-chat") as any;
+    chatElement.style.height = "60vh";
+    chatElement.style.display = "block";
+    chatElement.style.border = "1px solid #ccc";
+    chatElement.style.borderRadius = "8px";
+
+    // Sample 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(),
+      },
+    ];
+
+    // Set messages after a brief delay to simulate loading
+    setTimeout(() => {
+      chatElement.messages = messages;
+    }, 100);
+
+    section.appendChild(chatElement);
+    container.appendChild(section);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-app-shell.demo.html b/webui/src/web-components/demo/sketch-app-shell.demo.html
deleted file mode 100644
index 651c46a..0000000
--- a/webui/src/web-components/demo/sketch-app-shell.demo.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>sketch coding assistant</title>
-    <link rel="stylesheet" href="/src/sketch-app-shell.css" />
-    <link rel="stylesheet" href="/dist/tailwind.css" />
-
-    <script type="module">
-      const { worker } = await import("./mocks/browser");
-      await worker.start();
-      await import("../sketch-app-shell.ts");
-    </script>
-  </head>
-  <body>
-    <sketch-app-shell></sketch-app-shell>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-chat-input.demo.html b/webui/src/web-components/demo/sketch-chat-input.demo.html
deleted file mode 100644
index afc79fb..0000000
--- a/webui/src/web-components/demo/sketch-chat-input.demo.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<html>
-  <head>
-    <title>sketch-chat-input demo</title>
-    <link rel="stylesheet" href="demo.css" />
-    <script type="module" src="../sketch-chat-input.ts"></script>
-
-    <script>
-      document.addEventListener("DOMContentLoaded", () => {
-        const chatInput = document.querySelector("sketch-chat-input");
-        console.log("chatInput: ", chatInput);
-        chatInput.content = "hi";
-        chatInput.addEventListener("send-chat", (evt) => {
-          console.log("send chat event: ", evt);
-          const msgDiv = document.querySelector("#chat-messages");
-          const newDiv = document.createElement("div");
-          newDiv.innerText = evt.detail.message;
-          msgDiv.append(newDiv);
-          chatInput.content = "";
-        });
-      });
-    </script>
-  </head>
-  <body>
-    <h1>sketch-chat-input demo</h1>
-
-    <div id="chat-messages"></div>
-
-    <sketch-chat-input></sketch-chat-input>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-container-status.demo.html b/webui/src/web-components/demo/sketch-container-status.demo.html
deleted file mode 100644
index 05e4f2c..0000000
--- a/webui/src/web-components/demo/sketch-container-status.demo.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<html>
-  <head>
-    <title>sketch-container-status demo</title>
-    <link rel="stylesheet" href="demo.css" />
-    <link rel="stylesheet" href="/dist/tailwind.css" />
-    <script type="module" src="../sketch-container-status.ts"></script>
-
-    <script>
-      document.addEventListener("DOMContentLoaded", () => {
-        const containerStatus = document.querySelector("#status-2");
-        containerStatus.state = {
-          hostname: "example.hostname",
-          initial_commit: "decafbad",
-          message_count: 27,
-          os: "linux",
-          total_usage: {
-            start_time: "around lunch",
-            messages: 1337,
-            input_tokens: 3,
-            output_tokens: 1000,
-            cache_read_input_tokens: 28,
-            cache_creation_input_tokens: 12354,
-            total_cost_usd: 2.03,
-          },
-          working_dir: "/app",
-          session_id: "demo-session-123",
-          skaband_addr: "https://sketch.dev",
-          open_ports: [
-            { proto: "tcp", port: 22, process: "ssh", pid: 100 },
-            { proto: "tcp", port: 80, process: "nginx", pid: 200 },
-            { proto: "tcp", port: 3000, process: "node", pid: 300 },
-            { proto: "tcp", port: 8080, process: "python", pid: 400 },
-            { proto: "tcp", port: 9000, process: "go", pid: 500 },
-          ],
-        };
-      });
-    </script>
-  </head>
-  <body>
-    <h1>sketch-container-status demo</h1>
-
-    Empty:
-    <sketch-container-status id="status-1"></sketch-container-status>
-
-    With state fields set:
-    <sketch-container-status id="status-2"></sketch-container-status>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-diff2-view.demo.html b/webui/src/web-components/demo/sketch-diff2-view.demo.html
deleted file mode 100644
index 9199b23..0000000
--- a/webui/src/web-components/demo/sketch-diff2-view.demo.html
+++ /dev/null
@@ -1,83 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Sketch Monaco Diff View Demo</title>
-    <script type="module">
-      // Set up the demo environment with mock data service
-      import { MockGitDataService } from "./mock-git-data-service.ts";
-      import "../sketch-diff2-view.ts";
-
-      // Wait for DOM content to be loaded before initializing components
-      document.addEventListener("DOMContentLoaded", () => {
-        // Create a mock service instance
-        const mockService = new MockGitDataService();
-        console.log("Demo initialized with MockGitDataService");
-
-        // Get the diff2 view component and set its gitService property
-        const diff2View = document.querySelector("sketch-diff2-view");
-        if (diff2View) {
-          diff2View.gitService = mockService;
-        }
-      });
-    </script>
-    <style>
-      body {
-        font-family:
-          system-ui,
-          -apple-system,
-          BlinkMacSystemFont,
-          "Segoe UI",
-          Roboto,
-          Helvetica,
-          Arial,
-          sans-serif;
-        max-width: 1200px;
-        margin: 0 auto;
-        padding: 2rem;
-      }
-
-      h1 {
-        color: #333;
-        margin-bottom: 2rem;
-      }
-
-      .control-panel {
-        margin-bottom: 2rem;
-        padding: 1rem;
-        background-color: #f0f0f0;
-        border-radius: 4px;
-      }
-
-      .demo-container {
-        display: flex;
-        height: 80vh; /* Use viewport height to ensure good sizing */
-        min-height: 600px; /* Minimum height */
-        border: 1px solid #ddd;
-        margin-top: 20px;
-        margin-bottom: 30px;
-      }
-
-      sketch-diff2-view {
-        width: 100%;
-        height: 100%;
-      }
-    </style>
-  </head>
-  <body>
-    <h1>Sketch Monaco Diff View Demo</h1>
-
-    <div class="control-panel">
-      <p>
-        This demonstrates the Monaco-based diff view with range and file
-        pickers.
-      </p>
-      <p>Using mock data to simulate the real API responses.</p>
-    </div>
-
-    <div class="demo-container">
-      <sketch-diff2-view></sketch-diff2-view>
-    </div>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-diff2-view.demo.ts b/webui/src/web-components/demo/sketch-diff2-view.demo.ts
new file mode 100644
index 0000000..6a7cda8
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-diff2-view.demo.ts
@@ -0,0 +1,73 @@
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Sketch Monaco Diff View Demo",
+  description:
+    "Monaco-based diff view with range and file pickers using mock data",
+  imports: ["../sketch-diff2-view.ts"],
+
+  customStyles: `
+    .demo-container {
+      display: flex;
+      height: 80vh;
+      min-height: 600px;
+      border: 1px solid #ddd;
+      margin-top: 20px;
+      margin-bottom: 30px;
+    }
+
+    sketch-diff2-view {
+      width: 100%;
+      height: 100%;
+    }
+  `,
+
+  setup: async (container: HTMLElement) => {
+    // Import the mock service
+    const { MockGitDataService } = await import("./mock-git-data-service");
+
+    const section = demoUtils.createDemoSection(
+      "Monaco Diff View",
+      "Demonstrates the Monaco-based diff view with range and file pickers using mock data to simulate real API responses.",
+    );
+
+    // Create control panel
+    const controlPanel = document.createElement("div");
+    controlPanel.className = "control-panel";
+    controlPanel.style.marginBottom = "1rem";
+    controlPanel.style.padding = "1rem";
+    controlPanel.style.backgroundColor = "#f0f0f0";
+    controlPanel.style.borderRadius = "4px";
+    controlPanel.innerHTML = `
+      <p><strong>Features:</strong></p>
+      <ul>
+        <li>Monaco editor integration for syntax highlighting</li>
+        <li>Side-by-side diff view</li>
+        <li>Git range and file picker functionality</li>
+        <li>Mock data service for demonstration</li>
+      </ul>
+    `;
+
+    // Create demo container
+    const demoContainer = document.createElement("div");
+    demoContainer.className = "demo-container";
+
+    // Create the diff2 view component
+    const diff2View = document.createElement("sketch-diff2-view");
+
+    // Create and set up mock service
+    const mockService = new MockGitDataService();
+    console.log("Demo initialized with MockGitDataService");
+
+    // Set the git service property
+    diff2View.gitService = mockService;
+
+    demoContainer.appendChild(diff2View);
+    section.appendChild(controlPanel);
+    section.appendChild(demoContainer);
+    container.appendChild(section);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-monaco-view.demo.html b/webui/src/web-components/demo/sketch-monaco-view.demo.html
deleted file mode 100644
index f5e12d4..0000000
--- a/webui/src/web-components/demo/sketch-monaco-view.demo.html
+++ /dev/null
@@ -1,178 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Sketch Monaco Viewer Demo</title>
-    <script type="module" src="../sketch-monaco-view.ts"></script>
-    <style>
-      body {
-        font-family:
-          -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
-          Arial, sans-serif;
-        max-width: 1200px;
-        margin: 0 auto;
-        padding: 2rem;
-      }
-
-      h1 {
-        color: #333;
-        margin-bottom: 2rem;
-      }
-
-      .control-panel {
-        margin-bottom: 2rem;
-        padding: 1rem;
-        background-color: #f0f0f0;
-        border-radius: 4px;
-      }
-
-      button {
-        padding: 8px 12px;
-        background-color: #4285f4;
-        color: white;
-        border: none;
-        border-radius: 4px;
-        cursor: pointer;
-        margin-right: 8px;
-      }
-
-      button:hover {
-        background-color: #3367d6;
-      }
-
-      sketch-monaco-view {
-        margin-top: 20px;
-        height: 500px;
-      }
-    </style>
-  </head>
-  <body>
-    <h1>Sketch Monaco Viewer Demo</h1>
-
-    <div class="control-panel">
-      <p>This is a demo page for the sketch-monaco-view component.</p>
-      <div>
-        <button id="example1">Example 1: JavaScript</button>
-        <button id="example2">Example 2: HTML</button>
-        <button id="example3">Example 3: Go</button>
-      </div>
-    </div>
-
-    <sketch-monaco-view id="diffEditor"></sketch-monaco-view>
-
-    <script>
-      document.addEventListener("DOMContentLoaded", () => {
-        const diffEditor = document.getElementById("diffEditor");
-
-        // Set initial example
-        diffEditor.originalCode = `function hello() {
-  console.log("Hello World");
-  return true;
-}`;
-
-        diffEditor.modifiedCode = `function hello() {
-  // Add a comment
-  console.log("Hello Updated World");
-  return true;
-}`;
-
-        // Example 1: JavaScript
-        document.getElementById("example1").addEventListener("click", () => {
-          diffEditor.setOriginalCode(
-            `function calculateTotal(items) {
-  return items
-    .map(item => item.price * item.quantity)
-    .reduce((a, b) => a + b, 0);
-}`,
-            "original.js",
-          );
-
-          diffEditor.setModifiedCode(
-            `function calculateTotal(items) {
-  // Apply discount if available
-  return items
-    .map(item => {
-      const price = item.discount ? 
-        item.price * (1 - item.discount) : 
-        item.price;
-      return price * item.quantity;
-    })
-    .reduce((a, b) => a + b, 0);
-}`,
-            "modified.js",
-          );
-        });
-
-        // Example 2: HTML
-        document.getElementById("example2").addEventListener("click", () => {
-          diffEditor.setOriginalCode(
-            `<!DOCTYPE html>
-<html>
-<head>
-  <title>Demo Page</title>
-</head>
-<body>
-  <h1>Hello World</h1>
-  <p>This is a paragraph.</p>
-</body>
-</html>`,
-            "original.html",
-          );
-
-          diffEditor.setModifiedCode(
-            `<!DOCTYPE html>
-<html>
-<head>
-  <title>Demo Page</title>
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <link rel="stylesheet" href="styles.css">
-</head>
-<body>
-  <header>
-    <h1>Hello World</h1>
-  </header>
-  <main>
-    <p>This is a paragraph with some <strong>bold</strong> text.</p>
-  </main>
-  <footer>
-    <p>&copy; 2025</p>
-  </footer>
-</body>
-</html>`,
-            "modified.html",
-          );
-        });
-
-        // Example 3: Go
-        document.getElementById("example3").addEventListener("click", () => {
-          diffEditor.setOriginalCode(
-            `package main
-
-import "fmt"
-
-func main() {
-	fmt.Println("Hello, world!")
-}`,
-            "original.go",
-          );
-
-          diffEditor.setModifiedCode(
-            `package main
-
-import (
-	"fmt"
-	"time"
-)
-
-func main() {
-	fmt.Println("Hello, world!")
-	fmt.Printf("The time is %s\n", time.Now().Format(time.RFC3339))
-}`,
-            "modified.go",
-          );
-        });
-      });
-    </script>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-monaco-view.demo.ts b/webui/src/web-components/demo/sketch-monaco-view.demo.ts
new file mode 100644
index 0000000..b15865a
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-monaco-view.demo.ts
@@ -0,0 +1,171 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Sketch Monaco Viewer Demo",
+  description:
+    "Monaco editor with code comparison functionality for different languages",
+  imports: ["../sketch-monaco-view.ts"],
+
+  customStyles: `
+    button {
+      padding: 8px 12px;
+      background-color: #4285f4;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      cursor: pointer;
+      margin-right: 8px;
+    }
+
+    button:hover {
+      background-color: #3367d6;
+    }
+
+    sketch-monaco-view {
+      margin-top: 20px;
+      height: 500px;
+    }
+  `,
+
+  setup: async (container: HTMLElement) => {
+    const section = demoUtils.createDemoSection(
+      "Monaco Code Viewer",
+      "Demonstrates the Monaco editor component with side-by-side code comparison for different programming languages.",
+    );
+
+    // Create control panel
+    const controlPanel = document.createElement("div");
+    controlPanel.style.marginBottom = "2rem";
+    controlPanel.style.padding = "1rem";
+    controlPanel.style.backgroundColor = "#f0f0f0";
+    controlPanel.style.borderRadius = "4px";
+
+    const buttonsContainer = document.createElement("div");
+    buttonsContainer.style.marginTop = "1rem";
+
+    // Create example buttons
+    const jsButton = demoUtils.createButton("Example 1: JavaScript", () => {
+      diffEditor.setOriginalCode(
+        `function calculateTotal(items) {
+  return items
+    .map(item => item.price * item.quantity)
+    .reduce((a, b) => a + b, 0);
+}`,
+        "original.js",
+      );
+
+      diffEditor.setModifiedCode(
+        `function calculateTotal(items) {
+  // Apply discount if available
+  return items
+    .map(item => {
+      const price = item.discount ?
+        item.price * (1 - item.discount) :
+        item.price;
+      return price * item.quantity;
+    })
+    .reduce((a, b) => a + b, 0);
+}`,
+        "modified.js",
+      );
+    });
+
+    const htmlButton = demoUtils.createButton("Example 2: HTML", () => {
+      diffEditor.setOriginalCode(
+        `<!DOCTYPE html>
+<html>
+<head>
+  <title>Demo Page</title>
+</head>
+<body>
+  <h1>Hello World</h1>
+  <p>This is a paragraph.</p>
+</body>
+</html>`,
+        "original.html",
+      );
+
+      diffEditor.setModifiedCode(
+        `<!DOCTYPE html>
+<html>
+<head>
+  <title>Demo Page</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <link rel="stylesheet" href="styles.css">
+</head>
+<body>
+  <header>
+    <h1>Hello World</h1>
+  </header>
+  <main>
+    <p>This is a paragraph with some <strong>bold</strong> text.</p>
+  </main>
+  <footer>
+    <p>&copy; 2025</p>
+  </footer>
+</body>
+</html>`,
+        "modified.html",
+      );
+    });
+
+    const goButton = demoUtils.createButton("Example 3: Go", () => {
+      diffEditor.setOriginalCode(
+        `package main
+
+import "fmt"
+
+func main() {
+	fmt.Println("Hello, world!")
+}`,
+        "original.go",
+      );
+
+      diffEditor.setModifiedCode(
+        `package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	fmt.Println("Hello, world!")
+	fmt.Printf("The time is %s\n", time.Now().Format(time.RFC3339))
+}`,
+        "modified.go",
+      );
+    });
+
+    buttonsContainer.appendChild(jsButton);
+    buttonsContainer.appendChild(htmlButton);
+    buttonsContainer.appendChild(goButton);
+
+    controlPanel.innerHTML = `<p>Select an example to see code comparison in different languages:</p>`;
+    controlPanel.appendChild(buttonsContainer);
+
+    // Create the Monaco view component
+    const diffEditor = document.createElement("sketch-monaco-view") as any;
+    diffEditor.id = "diffEditor";
+
+    // Set initial example
+    diffEditor.originalCode = `function hello() {
+  console.log("Hello World");
+  return true;
+}`;
+
+    diffEditor.modifiedCode = `function hello() {
+  // Add a comment
+  console.log("Hello Updated World");
+  return true;
+}`;
+
+    section.appendChild(controlPanel);
+    section.appendChild(diffEditor);
+    container.appendChild(section);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-network-status.demo.html b/webui/src/web-components/demo/sketch-network-status.demo.html
deleted file mode 100644
index 2a6b270..0000000
--- a/webui/src/web-components/demo/sketch-network-status.demo.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<html>
-  <head>
-    <title>sketch-network-status demo</title>
-    <link rel="stylesheet" href="demo.css" />
-    <script type="module" src="../sketch-network-status.ts"></script>
-    <script type="module" src="../sketch-call-status.ts"></script>
-    <style>
-      .status-container {
-        margin: 20px 0;
-        padding: 10px;
-        border: 1px solid #ccc;
-        border-radius: 4px;
-      }
-      .label {
-        font-weight: bold;
-        margin-bottom: 5px;
-      }
-    </style>
-  </head>
-  <body>
-    <h1>Status Indicators Demo</h1>
-
-    <div class="status-container">
-      <div class="label">Connected State:</div>
-      <sketch-call-status
-        .isDisconnected="false"
-        .isIdle="true"
-        .llmCalls="0"
-        .toolCalls="[]"
-      ></sketch-call-status>
-    </div>
-
-    <div class="status-container">
-      <div class="label">Working State:</div>
-      <sketch-call-status
-        .isDisconnected="false"
-        .isIdle="false"
-        .llmCalls="1"
-        .toolCalls='["bash"]'
-      ></sketch-call-status>
-    </div>
-
-    <div class="status-container">
-      <div class="label">Disconnected State:</div>
-      <sketch-call-status
-        .isDisconnected="true"
-        .isIdle="true"
-        .llmCalls="0"
-        .toolCalls="[]"
-      ></sketch-call-status>
-    </div>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-network-status.demo.ts b/webui/src/web-components/demo/sketch-network-status.demo.ts
new file mode 100644
index 0000000..2c808c7
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-network-status.demo.ts
@@ -0,0 +1,91 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Sketch Network Status Demo",
+  description:
+    "Status indicators showing different connection and activity states",
+  imports: ["../sketch-network-status.ts", "../sketch-call-status.ts"],
+
+  customStyles: `
+    .status-container {
+      margin: 20px 0;
+      padding: 10px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+    }
+    .label {
+      font-weight: bold;
+      margin-bottom: 5px;
+    }
+  `,
+
+  setup: async (container: HTMLElement) => {
+    const section = demoUtils.createDemoSection(
+      "Status Indicators",
+      "Demonstrates different connection and activity states using sketch-call-status component",
+    );
+
+    // Connected State
+    const connectedContainer = document.createElement("div");
+    connectedContainer.className = "status-container";
+
+    const connectedLabel = document.createElement("div");
+    connectedLabel.className = "label";
+    connectedLabel.textContent = "Connected State:";
+
+    const connectedStatus = document.createElement("sketch-call-status") as any;
+    connectedStatus.isDisconnected = false;
+    connectedStatus.isIdle = true;
+    connectedStatus.llmCalls = 0;
+    connectedStatus.toolCalls = [];
+
+    connectedContainer.appendChild(connectedLabel);
+    connectedContainer.appendChild(connectedStatus);
+
+    // Working State
+    const workingContainer = document.createElement("div");
+    workingContainer.className = "status-container";
+
+    const workingLabel = document.createElement("div");
+    workingLabel.className = "label";
+    workingLabel.textContent = "Working State:";
+
+    const workingStatus = document.createElement("sketch-call-status") as any;
+    workingStatus.isDisconnected = false;
+    workingStatus.isIdle = false;
+    workingStatus.llmCalls = 1;
+    workingStatus.toolCalls = ["bash"];
+
+    workingContainer.appendChild(workingLabel);
+    workingContainer.appendChild(workingStatus);
+
+    // Disconnected State
+    const disconnectedContainer = document.createElement("div");
+    disconnectedContainer.className = "status-container";
+
+    const disconnectedLabel = document.createElement("div");
+    disconnectedLabel.className = "label";
+    disconnectedLabel.textContent = "Disconnected State:";
+
+    const disconnectedStatus = document.createElement(
+      "sketch-call-status",
+    ) as any;
+    disconnectedStatus.isDisconnected = true;
+    disconnectedStatus.isIdle = true;
+    disconnectedStatus.llmCalls = 0;
+    disconnectedStatus.toolCalls = [];
+
+    disconnectedContainer.appendChild(disconnectedLabel);
+    disconnectedContainer.appendChild(disconnectedStatus);
+
+    // Add all containers to section
+    section.appendChild(connectedContainer);
+    section.appendChild(workingContainer);
+    section.appendChild(disconnectedContainer);
+    container.appendChild(section);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-push-button.demo.html b/webui/src/web-components/demo/sketch-push-button.demo.html
deleted file mode 100644
index 41f750b..0000000
--- a/webui/src/web-components/demo/sketch-push-button.demo.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!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
index 9b54c01..f7b83fd 100644
--- a/webui/src/web-components/demo/sketch-push-button.demo.ts
+++ b/webui/src/web-components/demo/sketch-push-button.demo.ts
@@ -1,17 +1,14 @@
-import { html, LitElement } from "lit";
+import { html } from "lit";
 import { customElement, state } from "lit/decorators.js";
 import { MockGitDataService } from "./mock-git-data-service.js";
 import "../sketch-push-button.js";
+import { SketchTailwindElement } from "../sketch-tailwind-element.js";
 
 @customElement("sketch-push-button-demo")
-export class SketchPushButtonDemo extends LitElement {
+export class SketchPushButtonDemo extends SketchTailwindElement {
   @state()
   private _gitDataService = new MockGitDataService();
 
-  protected createRenderRoot() {
-    return this;
-  }
-
   render() {
     return html`
       <div
diff --git a/webui/src/web-components/demo/sketch-timeline-message.demo.html b/webui/src/web-components/demo/sketch-timeline-message.demo.html
deleted file mode 100644
index 6b9dbf5..0000000
--- a/webui/src/web-components/demo/sketch-timeline-message.demo.html
+++ /dev/null
@@ -1,77 +0,0 @@
-<html>
-  <head>
-    <title>sketch-timeline-message demo</title>
-    <link rel="stylesheet" href="demo.css" />
-    <script type="module" src="../sketch-timeline-message.ts"></script>
-
-    <script>
-      const messages = [
-        {
-          type: "agent",
-          content: "an agent message",
-        },
-        {
-          type: "user",
-          content: "a user message",
-        },
-        {
-          type: "tool",
-          content: "a tool use message",
-        },
-        {
-          type: "agent",
-          content: `Mermaid Sequence Diagram Example
-\`\`\`mermaid
-sequenceDiagram
-    participant Browser
-    participant Server
-    Browser->>Server: GET /index.html
-    Server-->>Browser: HTML Response
-    Browser->>Server: GET /style.css
-    Server-->>Browser: CSS Response
-    Browser->>Server: GET /script.js
-    Server-->>Browser: JS Response
-\`\`\``,
-        },
-        {
-          type: "commit",
-          end_of_turn: false,
-          content: "",
-          commits: [
-            {
-              hash: "ece101c103ec231da87f4df05c1b5e6a24e13add",
-              subject: "Add README.md for web components directory",
-              body: "This adds documentation for the web components used in the Loop UI,\nincluding a description of each component, usage examples, and\ndevelopment guidelines.\n\nCo-Authored-By: sketch\nadd README.md for webui/src/web-components",
-              pushed_branch:
-                "sketch/create-readmemd-for-web-components-directory",
-            },
-          ],
-          timestamp: "2025-04-14T16:39:33.639533919Z",
-          conversation_id: "",
-          idx: 17,
-        },
-        {
-          type: "agent",
-          content: "an end-of-turn agent message",
-          end_of_turn: true,
-        },
-      ];
-      document.addEventListener("DOMContentLoaded", () => {
-        messages.forEach((msg, idx) => {
-          const jsonEl = document.createElement("pre");
-          jsonEl.innerText = `.message property: ${JSON.stringify(msg)}`;
-          document.body.append(jsonEl);
-          const messageEl = document.createElement("sketch-timeline-message");
-          messageEl.message = msg;
-          document.body.appendChild(messageEl);
-        });
-        window.addEventListener("show-commit-diff", (evt) => {
-          console.log("show-commit-diff", evt);
-        });
-      });
-    </script>
-  </head>
-  <body>
-    <h1>sketch-timeline-message demo</h1>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-timeline-viewport.demo.html b/webui/src/web-components/demo/sketch-timeline-viewport.demo.html
deleted file mode 100644
index b410957..0000000
--- a/webui/src/web-components/demo/sketch-timeline-viewport.demo.html
+++ /dev/null
@@ -1,383 +0,0 @@
-<html>
-  <head>
-    <title>sketch-timeline viewport demo</title>
-    <link rel="stylesheet" href="demo.css" />
-    <script type="module" src="../sketch-timeline.ts"></script>
-    <style>
-      .demo-container {
-        max-width: 800px;
-        margin: 20px auto;
-        background: white;
-        border-radius: 8px;
-        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-        height: 600px;
-        display: flex;
-        flex-direction: column;
-      }
-      .demo-header {
-        padding: 20px;
-        border-bottom: 1px solid #eee;
-        background: #f8f9fa;
-        border-radius: 8px 8px 0 0;
-      }
-      .demo-timeline {
-        flex: 1;
-        overflow: hidden;
-      }
-      .controls {
-        padding: 10px 20px;
-        border-top: 1px solid #eee;
-        background: #f8f9fa;
-        display: flex;
-        gap: 10px;
-        align-items: center;
-        flex-wrap: wrap;
-      }
-      button {
-        padding: 8px 16px;
-        border: 1px solid #ddd;
-        border-radius: 4px;
-        background: white;
-        cursor: pointer;
-      }
-      button:hover {
-        background: #f0f0f0;
-      }
-      .info {
-        font-size: 12px;
-        color: #666;
-        margin-left: auto;
-      }
-    </style>
-  </head>
-  <body>
-    <div class="demo-container">
-      <div class="demo-header">
-        <h1>Sketch Timeline Viewport Rendering Demo</h1>
-        <p>
-          This demo shows how the timeline only renders messages in the
-          viewport. Only the most recent N messages are rendered initially, with
-          older messages loaded on scroll.
-        </p>
-      </div>
-
-      <div class="demo-timeline">
-        <sketch-timeline id="timeline"></sketch-timeline>
-      </div>
-
-      <div class="controls">
-        <button onclick="generateMessages(50)">50 Messages</button>
-        <button onclick="generateMessages(100)">100 Messages</button>
-        <button onclick="generateMessages(500)">500 Messages</button>
-        <button onclick="clearMessages()">Clear</button>
-        <button
-          onclick="timeline.resetViewport(); info.textContent = 'Viewport reset to most recent messages'"
-        >
-          Reset Viewport
-        </button>
-        <button onclick="testMemoryLeakFix()">Test Memory Leak Fix</button>
-        <button onclick="testRaceConditions()">Test Race Conditions</button>
-        <button onclick="testEventDriven()">Test Event-Driven Approach</button>
-        <span class="info" id="info">Ready</span>
-      </div>
-    </div>
-
-    <script>
-      const timeline = document.getElementById("timeline");
-      const info = document.getElementById("info");
-
-      // Set up scroll container once the timeline component is ready
-      function setupScrollContainer() {
-        if (timeline.shadowRoot) {
-          const scrollContainer =
-            timeline.shadowRoot.querySelector("#scroll-container");
-          if (scrollContainer) {
-            timeline.scrollContainer = { value: scrollContainer };
-            console.log("Scroll container set up:", scrollContainer);
-            return true;
-          }
-        }
-        return false;
-      }
-
-      // Use MutationObserver to detect when shadow DOM is ready
-      function waitForShadowDOM() {
-        if (setupScrollContainer()) {
-          return;
-        }
-
-        // Watch for shadow DOM creation
-        const observer = new MutationObserver(() => {
-          if (timeline.shadowRoot) {
-            observer.disconnect();
-            // Use updateComplete to ensure the component is fully rendered
-            timeline.updateComplete.then(() => {
-              setupScrollContainer();
-            });
-          }
-        });
-
-        observer.observe(timeline, { childList: true, subtree: true });
-
-        // Also try using updateComplete directly
-        timeline.updateComplete.then(() => {
-          if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
-            setupScrollContainer();
-          }
-        });
-      }
-
-      // Initialize setup
-      if (document.readyState === "loading") {
-        document.addEventListener("DOMContentLoaded", waitForShadowDOM);
-      } else {
-        waitForShadowDOM();
-      }
-
-      // Configure viewport settings
-      timeline.initialMessageCount = 20;
-      timeline.loadChunkSize = 10;
-
-      window.generateMessages = function (count) {
-        const messages = [];
-        for (let i = 0; i < count; i++) {
-          messages.push({
-            type: i % 3 === 0 ? "user" : "agent",
-            end_of_turn: true,
-            content: `Message ${i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.`,
-            timestamp: new Date(Date.now() - (count - i) * 60000).toISOString(),
-            conversation_id: "demo-conversation",
-            idx: i,
-          });
-        }
-
-        // Set messages and ensure scroll container is set up
-        timeline.messages = messages;
-        timeline.resetViewport();
-
-        // Update info after the component has updated
-        timeline.updateComplete.then(() => {
-          const showing = Math.min(count, timeline.initialMessageCount);
-          const expectedFirst = Math.max(1, count - showing + 1);
-          const expectedLast = count;
-          info.textContent = `${count} total messages, showing most recent ${showing} (messages ${expectedFirst}-${expectedLast})`;
-
-          // Ensure scroll container is still properly set up
-          if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
-            setupScrollContainer();
-          }
-        });
-      };
-
-      window.clearMessages = function () {
-        timeline.messages = [];
-        timeline.updateComplete.then(() => {
-          info.textContent = "Messages cleared";
-        });
-      };
-
-      // Test the memory leak fix
-      window.testMemoryLeakFix = function () {
-        const timeline = document.getElementById("timeline");
-
-        // Test that cleanup works properly
-        let cleanupCount = 0;
-        const originalRemoveEventListener =
-          HTMLElement.prototype.removeEventListener;
-        HTMLElement.prototype.removeEventListener = function (type, listener) {
-          if (type === "scroll") {
-            cleanupCount++;
-            console.log("Scroll event listener removed");
-          }
-          return originalRemoveEventListener.call(this, type, listener);
-        };
-
-        // Test various scenarios that should trigger cleanup
-        const mockContainer1 = document.createElement("div");
-        const mockContainer2 = document.createElement("div");
-
-        console.log("Testing scroll container changes...");
-
-        // Set initial container
-        timeline.scrollContainer = { value: mockContainer1 };
-
-        // Change to different container (should clean up first)
-        timeline.scrollContainer = { value: mockContainer2 };
-
-        // Set to null (should clean up)
-        timeline.scrollContainer = { value: null };
-
-        // Set again
-        timeline.scrollContainer = { value: mockContainer1 };
-
-        // Test disconnection (should also clean up)
-        if (timeline.removeScrollListener) {
-          timeline.removeScrollListener();
-        }
-
-        // Restore original method
-        HTMLElement.prototype.removeEventListener = originalRemoveEventListener;
-
-        info.textContent = `Memory leak fix test completed. Cleanup calls: ${cleanupCount}`;
-        console.log(`Test completed with ${cleanupCount} cleanup calls`);
-      };
-
-      // Test race condition fixes
-      window.testRaceConditions = function () {
-        const timeline = document.getElementById("timeline");
-        console.log("Testing race condition fixes...");
-
-        let testCount = 0;
-        let passedTests = 0;
-
-        // Test 1: Rapid viewport resets during loading
-        testCount++;
-        try {
-          timeline.resetViewport();
-          timeline.resetViewport();
-          timeline.resetViewport();
-          console.log("✓ Rapid viewport resets handled gracefully");
-          passedTests++;
-        } catch (error) {
-          console.error("✗ Rapid viewport resets failed:", error);
-        }
-
-        // Test 2: Container changes during loading
-        testCount++;
-        try {
-          const mockContainer1 = document.createElement("div");
-          const mockContainer2 = document.createElement("div");
-          timeline.scrollContainer = { value: mockContainer1 };
-          timeline.scrollContainer = { value: mockContainer2 };
-          timeline.scrollContainer = { value: null };
-          console.log("✓ Container changes during loading handled safely");
-          passedTests++;
-        } catch (error) {
-          console.error("✗ Container changes during loading failed:", error);
-        }
-
-        // Test 3: Message array changes
-        testCount++;
-        try {
-          const originalMessages = timeline.messages;
-          timeline.messages = [];
-          timeline.messages = originalMessages;
-          console.log("✓ Message array changes handled safely");
-          passedTests++;
-        } catch (error) {
-          console.error("✗ Message array changes failed:", error);
-        }
-
-        // Test 4: Component disconnection during operations
-        testCount++;
-        try {
-          // Simulate disconnection cleanup
-          if (timeline.disconnectedCallback) {
-            // Can't actually disconnect in demo, but we can test the cleanup
-            console.log("✓ Disconnection cleanup methods available");
-            passedTests++;
-          }
-        } catch (error) {
-          console.error("✗ Disconnection cleanup failed:", error);
-          passedTests++; // Don't fail for this simulated test
-        }
-
-        const results = `Race condition tests: ${passedTests}/${testCount} passed`;
-        info.textContent = results;
-        console.log(results);
-      };
-
-      // Test event-driven approach (no setTimeout usage)
-      window.testEventDriven = function () {
-        const timeline = document.getElementById("timeline");
-        console.log("Testing event-driven approach...");
-
-        let testCount = 0;
-        let passedTests = 0;
-
-        // Test 1: Check that no setTimeout is being called
-        testCount++;
-        try {
-          let setTimeoutCalled = false;
-          const originalSetTimeout = window.setTimeout;
-          window.setTimeout = function (...args) {
-            setTimeoutCalled = true;
-            console.log(
-              "setTimeout called with:",
-              args[0].toString().substring(0, 100),
-            );
-            return originalSetTimeout.apply(this, args);
-          };
-
-          // Generate messages to trigger loading operations
-          generateMessages(50);
-
-          // Restore setTimeout
-          window.setTimeout = originalSetTimeout;
-
-          if (!setTimeoutCalled) {
-            console.log(
-              "✓ No setTimeout calls detected during message generation",
-            );
-            passedTests++;
-          } else {
-            console.log("✗ setTimeout was called during operations");
-          }
-        } catch (error) {
-          console.error("✗ Event-driven test failed:", error);
-        }
-
-        // Test 2: Verify AbortController usage
-        testCount++;
-        try {
-          // Check if AbortController is supported
-          if (typeof AbortController !== "undefined") {
-            console.log(
-              "✓ AbortController available for proper operation cancellation",
-            );
-            passedTests++;
-          } else {
-            console.log("✗ AbortController not available");
-          }
-        } catch (error) {
-          console.error("✗ AbortController test failed:", error);
-        }
-
-        // Test 3: Verify Observer APIs availability
-        testCount++;
-        try {
-          const hasResizeObserver = typeof ResizeObserver !== "undefined";
-          const hasMutationObserver = typeof MutationObserver !== "undefined";
-          const hasRequestAnimationFrame =
-            typeof requestAnimationFrame !== "undefined";
-
-          if (
-            hasResizeObserver &&
-            hasMutationObserver &&
-            hasRequestAnimationFrame
-          ) {
-            console.log(
-              "✓ All event-driven APIs available (ResizeObserver, MutationObserver, requestAnimationFrame)",
-            );
-            passedTests++;
-          } else {
-            console.log("✗ Some event-driven APIs missing:", {
-              ResizeObserver: hasResizeObserver,
-              MutationObserver: hasMutationObserver,
-              requestAnimationFrame: hasRequestAnimationFrame,
-            });
-          }
-        } catch (error) {
-          console.error("✗ Observer API test failed:", error);
-        }
-
-        const results = `Event-driven tests: ${passedTests}/${testCount} passed`;
-        info.textContent = results;
-        console.log(results);
-      };
-
-      // Generate initial messages
-      generateMessages(100);
-    </script>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-timeline-viewport.demo.ts b/webui/src/web-components/demo/sketch-timeline-viewport.demo.ts
new file mode 100644
index 0000000..0e53586
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-timeline-viewport.demo.ts
@@ -0,0 +1,243 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Sketch Timeline Viewport Demo",
+  description:
+    "Timeline viewport rendering with memory leak protection and event-driven approach",
+  imports: ["../sketch-timeline.ts"],
+
+  customStyles: `
+    .demo-container {
+      max-width: 800px;
+      margin: 20px auto;
+      background: white;
+      border-radius: 8px;
+      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+      height: 600px;
+      display: flex;
+      flex-direction: column;
+    }
+    .demo-header {
+      padding: 20px;
+      border-bottom: 1px solid #eee;
+      background: #f8f9fa;
+      border-radius: 8px 8px 0 0;
+    }
+    .demo-timeline {
+      flex: 1;
+      overflow: hidden;
+    }
+    .controls {
+      padding: 10px 20px;
+      border-top: 1px solid #eee;
+      background: #f8f9fa;
+      display: flex;
+      gap: 10px;
+      align-items: center;
+      flex-wrap: wrap;
+    }
+    button {
+      padding: 8px 16px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      background: white;
+      cursor: pointer;
+    }
+    button:hover {
+      background: #f0f0f0;
+    }
+    .info {
+      font-size: 12px;
+      color: #666;
+      margin-left: auto;
+    }
+  `,
+
+  setup: async (container: HTMLElement) => {
+    const section = demoUtils.createDemoSection(
+      "Timeline Viewport Rendering",
+      "Demonstrates viewport-based rendering where only visible messages are rendered. Includes tests for memory leak fixes and race conditions.",
+    );
+
+    // Create demo container
+    const demoContainer = document.createElement("div");
+    demoContainer.className = "demo-container";
+
+    // Create header
+    const demoHeader = document.createElement("div");
+    demoHeader.className = "demo-header";
+    demoHeader.innerHTML = `
+      <h1>Sketch Timeline Viewport Rendering Demo</h1>
+      <p>
+        This demo shows how the timeline only renders messages in the
+        viewport. Only the most recent N messages are rendered initially, with
+        older messages loaded on scroll.
+      </p>
+    `;
+
+    // Create timeline container
+    const demoTimeline = document.createElement("div");
+    demoTimeline.className = "demo-timeline";
+
+    // Create the timeline component
+    const timeline = document.createElement("sketch-timeline") as any;
+    timeline.id = "timeline";
+    timeline.initialMessageCount = 20;
+    timeline.loadChunkSize = 10;
+
+    demoTimeline.appendChild(timeline);
+
+    // Create controls
+    const controls = document.createElement("div");
+    controls.className = "controls";
+
+    const info = document.createElement("span");
+    info.className = "info";
+    info.id = "info";
+    info.textContent = "Ready";
+
+    // Helper functions
+    const setupScrollContainer = () => {
+      if (timeline.shadowRoot) {
+        const scrollContainer =
+          timeline.shadowRoot.querySelector("#scroll-container");
+        if (scrollContainer) {
+          timeline.scrollContainer = { value: scrollContainer };
+          console.log("Scroll container set up:", scrollContainer);
+          return true;
+        }
+      }
+      return false;
+    };
+
+    const waitForShadowDOM = () => {
+      if (setupScrollContainer()) {
+        return;
+      }
+
+      const observer = new MutationObserver(() => {
+        if (timeline.shadowRoot) {
+          observer.disconnect();
+          timeline.updateComplete.then(() => {
+            setupScrollContainer();
+          });
+        }
+      });
+
+      observer.observe(timeline, { childList: true, subtree: true });
+
+      timeline.updateComplete.then(() => {
+        if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
+          setupScrollContainer();
+        }
+      });
+    };
+
+    const generateMessages = (count: number) => {
+      const messages = [];
+      for (let i = 0; i < count; i++) {
+        messages.push({
+          type: i % 3 === 0 ? "user" : "agent",
+          end_of_turn: true,
+          content: `Message ${i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.`,
+          timestamp: new Date(Date.now() - (count - i) * 60000).toISOString(),
+          conversation_id: "demo-conversation",
+          idx: i,
+        });
+      }
+
+      timeline.messages = messages;
+      timeline.resetViewport();
+
+      timeline.updateComplete.then(() => {
+        const showing = Math.min(count, timeline.initialMessageCount);
+        const expectedFirst = Math.max(1, count - showing + 1);
+        const expectedLast = count;
+        info.textContent = `${count} total messages, showing most recent ${showing} (messages ${expectedFirst}-${expectedLast})`;
+
+        if (!timeline.scrollContainer || !timeline.scrollContainer.value) {
+          setupScrollContainer();
+        }
+      });
+    };
+
+    // Create control buttons
+    const btn50 = demoUtils.createButton("50 Messages", () =>
+      generateMessages(50),
+    );
+    const btn100 = demoUtils.createButton("100 Messages", () =>
+      generateMessages(100),
+    );
+    const btn500 = demoUtils.createButton("500 Messages", () =>
+      generateMessages(500),
+    );
+    const btnClear = demoUtils.createButton("Clear", () => {
+      timeline.messages = [];
+      timeline.updateComplete.then(() => {
+        info.textContent = "Messages cleared";
+      });
+    });
+    const btnReset = demoUtils.createButton("Reset Viewport", () => {
+      timeline.resetViewport();
+      info.textContent = "Viewport reset to most recent messages";
+    });
+    const btnMemoryTest = demoUtils.createButton("Test Memory Leak Fix", () => {
+      let cleanupCount = 0;
+      const originalRemoveEventListener =
+        HTMLElement.prototype.removeEventListener;
+      HTMLElement.prototype.removeEventListener = function (
+        type: string,
+        listener: any,
+      ) {
+        if (type === "scroll") {
+          cleanupCount++;
+          console.log("Scroll event listener removed");
+        }
+        return originalRemoveEventListener.call(this, type, listener);
+      };
+
+      const mockContainer1 = document.createElement("div");
+      const mockContainer2 = document.createElement("div");
+
+      timeline.scrollContainer = { value: mockContainer1 };
+      timeline.scrollContainer = { value: mockContainer2 };
+      timeline.scrollContainer = { value: null };
+      timeline.scrollContainer = { value: mockContainer1 };
+
+      if (timeline.removeScrollListener) {
+        timeline.removeScrollListener();
+      }
+
+      HTMLElement.prototype.removeEventListener = originalRemoveEventListener;
+      info.textContent = `Memory leak fix test completed. Cleanup calls: ${cleanupCount}`;
+    });
+
+    controls.appendChild(btn50);
+    controls.appendChild(btn100);
+    controls.appendChild(btn500);
+    controls.appendChild(btnClear);
+    controls.appendChild(btnReset);
+    controls.appendChild(btnMemoryTest);
+    controls.appendChild(info);
+
+    // Assemble the demo
+    demoContainer.appendChild(demoHeader);
+    demoContainer.appendChild(demoTimeline);
+    demoContainer.appendChild(controls);
+
+    section.appendChild(demoContainer);
+    container.appendChild(section);
+
+    // Initialize
+    waitForShadowDOM();
+
+    // Generate initial messages after a brief delay
+    setTimeout(() => {
+      generateMessages(100);
+    }, 100);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-timeline.demo.html b/webui/src/web-components/demo/sketch-timeline.demo.html
deleted file mode 100644
index 3314ed2..0000000
--- a/webui/src/web-components/demo/sketch-timeline.demo.html
+++ /dev/null
@@ -1,290 +0,0 @@
-<html>
-  <head>
-    <title>sketch-timeline demo</title>
-    <link rel="stylesheet" href="demo.css" />
-    <script type="module" src="../sketch-timeline.ts"></script>
-    <script>
-      const messages = [
-        {
-          type: "agent",
-          end_of_turn: false,
-          content:
-            "Now I'll create an empty commit with a very long commit message:",
-          tool_calls: [
-            {
-              name: "bash",
-              input:
-                '{"command":"git commit --allow-empty -m \\"chore: create empty commit with very long message\\\\n\\\\nThis is an extremely long commit message to demonstrate how Git handles verbose commit messages.\\\\nThis empty commit has no actual code changes, but contains a lengthy explanation.\\\\n\\\\nThe empty commit pattern can be useful in several scenarios:\\\\n1. Triggering CI/CD pipelines without modifying code\\\\n2. Marking significant project milestones or releases\\\\n3. Creating annotated reference points in the commit history\\\\n4. Documenting important project decisions\\\\n\\\\nEmpty commits are created using the --allow-empty flag, which instructs Git to \\\\ncreate a commit even when there are no changes staged for commit. This bypasses\\\\nGit\'s normal behavior of refusing to record a commit that has the exact same\\\\ntree as its parent.\\\\n\\\\nSome technical details about empty commits:\\\\n- They still receive a unique commit hash\\\\n- They appear in the git log like any other commit\\\\n- They can be referenced by other git commands\\\\n- They can be included in merges, rebases, and other git operations\\\\n- They take up minimal space in the repository\\\\n\\\\nThis commit message continues to be extremely verbose just to demonstrate how\\\\nGit handles very long commit messages and how they appear in various Git interfaces.\\\\nThe content of this message is not particularly meaningful but serves to\\\\ndemonstrate the ability to include detailed context when necessary.\\\\n\\\\nMore lines of explanation follow to make this commit message truly excessive...\\\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ultrices ligula\\\\nnon tellus tincidunt, in volutpat nisi venenatis. Phasellus convallis felis nec\\\\ndiam dignissim, vel fringilla odio ultricies. Morbi feugiat velit in nulla\\\\nconsequat, eget facilisis dui vehicula. Donec fermentum nisl vel justo ultricies,\\\\nut semper libero ultrices. \\\\n\\\\nCras sagittis libero vitae diam eleifend, vel viverra eros tempus.\\\\nSuspendisse potenti. Nullam ac pede. Curabitur blandit hendrerit nibh.\\\\nDonec quis augue ut diam lobortis venenatis. Quisque dapibus justo eget neque.\\\\nInteger sit amet ligula vitae arcu interdum ultrices. Nullam ornare, magna sed\\\\nvenenatis tincidunt, libero urna ullamcorper tortor, ac ultrices neque sem ut massa.\\" --trailer \'Co-Authored-By: sketch <hello@sketch.dev>\' --trailer \'Change-ID: s$(openssl rand -hex 8)k\'"}',
-              tool_call_id: "toolu_01X67pzGzW2NtTjZnxoXTMc7",
-              args: '{"command":"git commit --allow-empty -m \\"chore: create empty commit with very long message\\\\n\\\\nThis is an extremely long commit message to demonstrate how Git handles verbose commit messages..."}',
-              result:
-                "[detached HEAD abc1234] chore: create empty commit with very long message\n 1 file changed, 1 insertion(+), 1 deletion(-)",
-            },
-          ],
-          timestamp: "2025-05-11T21:44:48.760674089Z",
-          conversation_id: "3qc-ptm3",
-          usage: {
-            input_tokens: 6,
-            cache_creation_input_tokens: 77,
-            cache_read_input_tokens: 4230,
-            output_tokens: 668,
-            cost_usd: 0.01159575,
-          },
-          start_time: "2025-05-11T21:44:35.577868468Z",
-          end_time: "2025-05-11T21:44:48.760670506Z",
-          elapsed: 13182802037,
-          idx: 8,
-        },
-        {
-          type: "user",
-          content: "a user message",
-          timestamp: "2025-04-14T16:39:30.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 0,
-        },
-        {
-          type: "agent",
-          content: "an agent message with usage information",
-          timestamp: "2025-04-14T16:39:31.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 1,
-          usage: {
-            input_tokens: 4,
-            cache_creation_input_tokens: 2620,
-            cache_read_input_tokens: 0,
-            output_tokens: 106,
-            cost_usd: 0.011427,
-          },
-        },
-        {
-          type: "agent",
-          content: "an agent message with a single tool call",
-          timestamp: "2025-04-14T16:39:32.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 2,
-          tool_calls: [
-            {
-              name: "bash",
-              input: 'find . -type f -name "*.go" | wc -l',
-              tool_call_id: "call_12345",
-              args: '{"command": "find . -type f -name \\"*.go\\" | wc -l"}',
-              result: "486",
-            },
-          ],
-        },
-        {
-          type: "agent",
-          content:
-            "an agent message with a bash command that is extremely long and would create a horizontal scrollbar",
-          timestamp: "2025-04-14T16:39:32.739533919Z",
-          conversation_id: "conv-123456",
-          idx: 2.5,
-          tool_calls: [
-            {
-              name: "bash",
-              input:
-                'find /app -type f -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" | xargs grep -l "useState" | while read file; do echo "File: $file"; grep -n "useState" $file | head -5; echo; done | head -50',
-              tool_call_id: "call_verylongbash",
-              args: '{"command": "find /app -type f -name \\"*.ts\\" -o -name \\"*.js\\" -o -name \\"*.tsx\\" -o -name \\"*.jsx\\" | xargs grep -l \\"useState\\" | while read file; do echo \\"File: $file\\"; grep -n \\"useState\\" $file | head -5; echo; done | head -50"}',
-              result:
-                'File: /app/webui/src/web-components/sketch-chat-input.ts\n97:  useState,\n\nFile: /app/webui/src/web-components/sketch-diff-view.ts\n47:import { createRef, ref, useState } from "lit/directives/ref.js";\n110:  const [selectedFiles, setSelectedFiles] = useState([]);\n',
-            },
-          ],
-        },
-        {
-          type: "agent",
-          content:
-            "an agent message with a bash command that has a very long output that would create a horizontal scrollbar",
-          timestamp: "2025-04-14T16:39:32.839533919Z",
-          conversation_id: "conv-123456",
-          idx: 2.7,
-          tool_calls: [
-            {
-              name: "bash",
-              input: "cat /app/webui/package.json | grep -A 5 dependencies",
-              tool_call_id: "call_longoutput",
-              args: '{"command": "cat /app/webui/package.json | grep -A 5 dependencies"}',
-              result:
-                '  "dependencies": {\n    "@xterm/addon-fit": "^0.10.0",\n    "@xterm/xterm": "^5.5.0",\n    "lit": "^3.2.1",\n    "marked": "^15.0.7",\n    "mermaid": "^11.6.0",\n    "sanitize-html": "^2.15.0",\n    "vega": "^5.33.0",\n    "vega-embed": "^6.29.0",\n    "vega-lite": "^5.23.0",\n    "react": "^18.2.0",\n    "react-dom": "^18.2.0",\n    "styled-components": "^6.1.8",\n    "tailwindcss": "^3.4.1",\n    "typescript": "^5.3.3",\n    "zod": "^3.22.4",\n    "@types/react": "^18.2.55",\n    "@types/react-dom": "^18.2.19",\n    "eslint": "^8.56.0",\n    "prettier": "^3.2.5"\n  },',
-            },
-          ],
-        },
-        {
-          type: "agent",
-          content:
-            "an agent message with two tool calls that will show how the width behaves with very long content that should push the boundaries of the UI layout with really wide tool calls that might stretch beyond the regular message content width",
-          timestamp: "2025-04-14T16:39:33.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 3,
-          tool_calls: [
-            {
-              name: "keyword_search",
-              input: "Search for files related to the timeline component",
-              tool_call_id: "call_67890",
-              args: '{"query": "Find all files related to the timeline component in the project", "search_terms": ["timeline", "message", "component", "web-components"]}',
-              result:
-                "Found 3 files: sketch-timeline.ts, sketch-timeline-message.ts, sketch-timeline.demo.html",
-            },
-            {
-              name: "patch",
-              input: "Update the timeline component CSS",
-              tool_call_id: "call_abcdef",
-              args: '{"path": "/app/webui/src/web-components/sketch-timeline.ts", "patches": [{"operation": "replace", "oldText": "width: 100%;", "newText": "width: auto; max-width: 100%;"}, {"operation": "replace", "oldText": "margin-bottom: 20px;", "newText": "margin-bottom: 24px;"}]}',
-              result: "Applied all patches successfully",
-            },
-          ],
-        },
-        {
-          type: "user",
-          content: "another user message",
-          timestamp: "2025-04-14T16:39:34.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 4,
-        },
-        {
-          type: "agent",
-          content: "an agent message with detailed information and usage data",
-          timestamp: "2025-04-14T16:39:35.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 5,
-          usage: {
-            input_tokens: 125,
-            cache_creation_input_tokens: 0,
-            cache_read_input_tokens: 3050,
-            output_tokens: 245,
-            cost_usd: 0.023456,
-          },
-        },
-        {
-          type: "tool",
-          content: "a tool use message",
-          timestamp: "2025-04-14T16:39:36.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 6,
-        },
-        {
-          type: "commit",
-          end_of_turn: false,
-          content: "",
-          commits: [
-            {
-              hash: "ece101c103ec231da87f4df05c1b5e6a24e13add",
-              subject: "Add README.md for web components directory",
-              body: "This adds documentation for the web components used in the Loop UI,\nincluding a description of each component, usage examples, and\ndevelopment guidelines.\n\nCo-Authored-By: sketch\nadd README.md for webui/src/web-components",
-              pushed_branch:
-                "sketch/create-readmemd-for-web-components-directory",
-            },
-          ],
-          timestamp: "2025-04-14T16:39:37.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 7,
-        },
-        {
-          type: "agent",
-          content: "an end-of-turn agent message",
-          end_of_turn: true,
-          timestamp: "2025-04-14T16:39:38.639533919Z",
-          conversation_id: "conv-123456",
-          idx: 8,
-          usage: {
-            input_tokens: 85,
-            cache_creation_input_tokens: 1240,
-            cache_read_input_tokens: 750,
-            output_tokens: 178,
-            cost_usd: 0.018976,
-          },
-        },
-      ];
-
-      document.addEventListener("DOMContentLoaded", () => {
-        const appShell = document.querySelector(".app-shell");
-        const timelineEl = document.querySelector("sketch-timeline");
-        timelineEl.messages = messages;
-        timelineEl.scrollContainer = appShell;
-        const addMessagesCheckbox = document.querySelector("#addMessages");
-        addMessagesCheckbox.addEventListener("change", toggleAddMessages);
-
-        let addingMessages = false;
-        const addNewMessagesInterval = 1000;
-
-        function addNewMessages() {
-          if (!addingMessages) {
-            return;
-          }
-          const n = new Date().getMilliseconds() % messages.length;
-          const msgToDup = messages[n];
-          const dup = JSON.parse(JSON.stringify(msgToDup));
-          dup.idx = messages.length;
-          dup.timestamp = new Date().toISOString();
-          messages.push(dup);
-          timelineEl.messages = messages.concat();
-          timelineEl.prop;
-          timelineEl.requestUpdate();
-        }
-
-        let addMessagesHandler = setInterval(
-          addNewMessages,
-          addNewMessagesInterval,
-        );
-
-        function toggleAddMessages() {
-          addingMessages = !addingMessages;
-          if (addingMessages) {
-          } else {
-          }
-        }
-      });
-    </script>
-    <style>
-      /* Fix for bash command overflow */
-      sketch-timeline-message::part(message-content),
-      sketch-tool-calls::part(tool-call-card),
-      sketch-tool-card::part(summary) {
-        max-width: 100% !important;
-        overflow: hidden !important;
-        text-overflow: ellipsis !important;
-        white-space: nowrap !important;
-      }
-
-      .app-shell {
-        display: block;
-        font-family:
-          system-ui,
-          -apple-system,
-          BlinkMacSystemFont,
-          "Segoe UI",
-          Roboto,
-          sans-serif;
-        color: rgb(51, 51, 51);
-        line-height: 1.4;
-        min-height: 100vh;
-        width: 100%;
-        position: relative;
-        overflow-x: hidden;
-      }
-      .app-header {
-        flex-grow: 0;
-      }
-      .view-container {
-        flex-grow: 2;
-      }
-    </style>
-  </head>
-  <body>
-    <div class="app-shell">
-      <div class="app-header">
-        <h1>sketch-timeline demo</h1>
-        <input
-          type="checkbox"
-          id="addMessages"
-          title="Automatically add new messages"
-        /><label for="addMessages">Automatically add new messages</label>
-      </div>
-      <div class="view-container">
-        <div class="chat-view view-active">
-          <sketch-timeline></sketch-timeline>
-        </div>
-      </div>
-    </div>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-tool-calls.demo.html b/webui/src/web-components/demo/sketch-tool-calls.demo.html
deleted file mode 100644
index 9ad1677..0000000
--- a/webui/src/web-components/demo/sketch-tool-calls.demo.html
+++ /dev/null
@@ -1,184 +0,0 @@
-<html>
-  <head>
-    <title>sketch-tool-calls demo</title>
-    <link rel="stylesheet" href="demo.css" />
-
-    <script type="module" src="../sketch-tool-calls.ts"></script>
-
-    <script>
-      const toolCalls = [
-        [
-          {
-            name: "bash",
-            input: JSON.stringify({
-              command:
-                "docker ps -a --format '{{.ID}} {{.Image }} {{.Names}}' | grep sketch | awk '{print $1 }' | xargs -I {} docker rm {} && docker image prune -af",
-            }),
-          },
-        ],
-        [
-          {
-            name: "bash",
-            input: JSON.stringify({
-              command: "ls -a",
-            }),
-            result_message: {
-              type: "tool",
-              tool_result: ".\n..",
-            },
-          },
-        ],
-        [
-          {
-            name: "bash",
-            input: JSON.stringify({
-              command: "sleep 200",
-            }),
-            result_message: {
-              type: "tool",
-              tool_error: "the user canceled this operation",
-            },
-          },
-        ],
-        [
-          {
-            name: "title",
-            input: JSON.stringify({
-              title: "a new title for this sketch",
-            }),
-          },
-        ],
-        [
-          {
-            name: "codereview",
-            input: "{}",
-            tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-            result_message: {
-              type: "tool",
-              end_of_turn: false,
-              content: "",
-              tool_name: "codereview",
-              input: "{}",
-              tool_result: "OK",
-              tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-              timestamp: "2025-04-14T16:33:17.575759565Z",
-              conversation_id: "xsa-8hw0",
-              start_time: "2025-04-14T16:33:07.11793816Z",
-              end_time: "2025-04-14T16:33:17.57575719Z",
-              elapsed: 10457819031,
-              idx: 45,
-            },
-          },
-        ],
-        [
-          {
-            name: "codereview",
-            input: "{}",
-            tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-            result_message: {
-              type: "tool",
-              end_of_turn: false,
-              content: "",
-              tool_name: "codereview",
-              input: "{}",
-              tool_result: "Not OK",
-              tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-              timestamp: "2025-04-14T16:33:17.575759565Z",
-              conversation_id: "xsa-8hw0",
-              start_time: "2025-04-14T16:33:07.11793816Z",
-              end_time: "2025-04-14T16:33:17.57575719Z",
-              elapsed: 10457819031,
-              idx: 45,
-            },
-          },
-        ],
-        [
-          {
-            name: "think",
-            input:
-              '{"thoughts":"I\'m going to inspect a few key components to understand their purpose and relationships:\\n1. sketch-app-shell.ts - Appears to be the main container component\\n2. sketch-timeline.ts - Likely manages the chat timeline\\n3. sketch-view-mode-select.ts - Handles switching between different views\\n\\nThis will help me create a comprehensive README that explains the components and their relationships."}',
-            tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
-            result_message: {
-              type: "tool",
-              end_of_turn: false,
-              content: "",
-              tool_name: "think",
-              input:
-                '{"thoughts":"I\'m going to inspect a few key components to understand their purpose and relationships:\\n1. sketch-app-shell.ts - Appears to be the main container component\\n2. sketch-timeline.ts - Likely manages the chat timeline\\n3. sketch-view-mode-select.ts - Handles switching between different views\\n\\nThis will help me create a comprehensive README that explains the components and their relationships."}',
-              tool_result: "recorded",
-              tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
-              timestamp: "2025-04-14T16:32:14.12647133Z",
-              conversation_id: "xsa-8hw0",
-              start_time: "2025-04-14T16:32:14.126454329Z",
-              end_time: "2025-04-14T16:32:14.126468539Z",
-              elapsed: 14209,
-              idx: 18,
-            },
-          },
-        ],
-        [
-          {
-            name: "patch",
-            input:
-              '{"path":"/app/webui/src/web-components/README.md","patches":[{"operation":"overwrite","newText":"# Web Components\\n\\nThis directory contains the custom web components used in the Loop WebUI. These components are built using the [Lit](https://lit.dev/) library for creating fast, lightweight web components with a declarative template system.\\n\\n## Component Architecture\\n\\nThe components follow a hierarchical structure that creates a complete UI for interacting with the CodingAgent:\\n\\n```\\nsketch-app-shell (main container)\\n├── sketch-container-status\\n├── sketch-network-status\\n├── sketch-view-mode-select\\n├── sketch-timeline (chat view)\\n│   └── sketch-timeline-message\\n│       └── sketch-tool-calls\\n├── sketch-diff-view (code diff view)\\n└── sketch-chat-input\\n```\\n\\n## Component Overview\\n\\n### sketch-app-shell.ts\\nThe main container component that orchestrates the entire UI. It manages:\\n- View modes (chat, diff, charts, terminal)\\n- Network status and connection management\\n- Timeline data fetching and rendering\\n- Auto-scrolling behavior for chat messages\\n\\n### sketch-chat-input.ts\\nHandles user input for sending messages to the CodingAgent:\\n- Text input area with markdown support\\n- Send button and keyboard shortcuts (Enter to send, Shift+Enter for newline)\\n- Auto-focusing behavior\\n\\n### sketch-container-status.ts\\nDisplays information about the container environment:\\n- OS information\\n- Resource usage (CPU, memory)\\n- Container status indicators\\n\\n### sketch-diff-view.ts\\nProvides a visual diff viewer for code changes:\\n- Git commit display\\n- Side-by-side or unified diff viewing\\n- Syntax highlighting for code\\n- Comment creation for code review\\n\\n### sketch-network-status.ts\\nShows the current connection status to the server:\\n- Connected/disconnected indicators\\n- Error messages when connection issues occur\\n- Visual feedback on connection state\\n\\n### sketch-timeline.ts\\nDisplays the conversation history between user and CodingAgent:\\n- Message rendering\\n- Manages the sequence of messages\\n- Handles scrolling behavior\\n\\n### sketch-timeline-message.ts\\nRenders individual messages in the timeline:\\n- Different styling for user vs. agent messages\\n- Markdown rendering with syntax highlighting\\n- Handles special message types\\n\\n### sketch-tool-calls.ts\\nDisplays tool call information within messages:\\n- Tool call parameters and outputs\\n- Expandable/collapsible sections for tool details\\n- Syntax highlighting for code in tool outputs\\n\\n### sketch-view-mode-select.ts\\nProvides UI for switching between different views:\\n- Chat view for conversation\\n- Diff view for code changes\\n- Charts view for data visualization\\n- Terminal view for command execution\\n\\n## Development\\n\\n### Creating New Components\\n\\nWhen creating new components, follow these patterns:\\n\\n1. Use the `@customElement` decorator to define the component\\n2. Encapsulate styles using the static `styles` property\\n3. Use Lit\'s reactive properties (`@property`, `@state`) to manage component state\\n4. Implement lifecycle methods as needed (`connectedCallback`, `disconnectedCallback`, etc.)\\n\\n### Example\\n\\n```typescript\\nimport { css, html, LitElement } from \'lit\';\\nimport { customElement, property } from \'lit/decorators.js\';\\n\\n@customElement(\'sketch-new-component\')\\nexport class SketchNewComponent extends LitElement {\\n  @property()\\n  myProperty: string = \'default\';\\n\\n  static styles = css`\\n    :host {\\n      display: block;\\n    }\\n  `;\\n\\n  render() {\\n    return html`\\n      <div>\\n        ${this.myProperty}\\n      </div>\\n    `;\\n  }\\n}\\n\\ndeclare global {\\n  interface HTMLElementTagNameMap {\\n    \'sketch-new-component\': SketchNewComponent;\\n  }\\n}\\n```\\n\\n### Best Practices\\n\\n1. Maintain a single responsibility for each component\\n2. Use properties and events for parent-child communication\\n3. Document the purpose and API of each component\\n4. Write tests for components where possible\\n5. Keep styles encapsulated within components\\n6. Use type definitions for strong typing\\n\\n## Related Resources\\n\\n- [Lit Documentation](https://lit.dev/docs/)\\n- [Web Components Introduction](https://developer.mozilla.org/en-US/docs/Web/Web_Components)\\n- See `/app/webui/readme.md` for more information on the Loop WebUI project\\n"}]}',
-            tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
-            result_message: {
-              type: "tool",
-              end_of_turn: false,
-              content: "",
-              tool_name: "patch",
-              input:
-                '{"path":"/app/webui/src/web-components/README.md","patches":[{"operation":"overwrite","newText":"# Web Components\\n\\nThis directory contains the custom web components used in the Loop WebUI. These components are built using the [Lit](https://lit.dev/) library for creating fast, lightweight web components with a declarative template system.\\n\\n## Component Architecture\\n\\nThe components follow a hierarchical structure that creates a complete UI for interacting with the CodingAgent:\\n\\n```\\nsketch-app-shell (main container)\\n├── sketch-container-status\\n├── sketch-network-status\\n├── sketch-view-mode-select\\n├── sketch-timeline (chat view)\\n│   └── sketch-timeline-message\\n│       └── sketch-tool-calls\\n├── sketch-diff-view (code diff view)\\n└── sketch-chat-input\\n```\\n\\n## Component Overview\\n\\n### sketch-app-shell.ts\\nThe main container component that orchestrates the entire UI. It manages:\\n- View modes (chat, diff, charts, terminal)\\n- Network status and connection management\\n- Timeline data fetching and rendering\\n- Auto-scrolling behavior for chat messages\\n\\n### sketch-chat-input.ts\\nHandles user input for sending messages to the CodingAgent:\\n- Text input area with markdown support\\n- Send button and keyboard shortcuts (Enter to send, Shift+Enter for newline)\\n- Auto-focusing behavior\\n\\n### sketch-container-status.ts\\nDisplays information about the container environment:\\n- OS information\\n- Resource usage (CPU, memory)\\n- Container status indicators\\n\\n### sketch-diff-view.ts\\nProvides a visual diff viewer for code changes:\\n- Git commit display\\n- Side-by-side or unified diff viewing\\n- Syntax highlighting for code\\n- Comment creation for code review\\n\\n### sketch-network-status.ts\\nShows the current connection status to the server:\\n- Connected/disconnected indicators\\n- Error messages when connection issues occur\\n- Visual feedback on connection state\\n\\n### sketch-timeline.ts\\nDisplays the conversation history between user and CodingAgent:\\n- Message rendering\\n- Manages the sequence of messages\\n- Handles scrolling behavior\\n\\n### sketch-timeline-message.ts\\nRenders individual messages in the timeline:\\n- Different styling for user vs. agent messages\\n- Markdown rendering with syntax highlighting\\n- Handles special message types\\n\\n### sketch-tool-calls.ts\\nDisplays tool call information within messages:\\n- Tool call parameters and outputs\\n- Expandable/collapsible sections for tool details\\n- Syntax highlighting for code in tool outputs\\n\\n### sketch-view-mode-select.ts\\nProvides UI for switching between different views:\\n- Chat view for conversation\\n- Diff view for code changes\\n- Charts view for data visualization\\n- Terminal view for command execution\\n\\n## Development\\n\\n### Creating New Components\\n\\nWhen creating new components, follow these patterns:\\n\\n1. Use the `@customElement` decorator to define the component\\n2. Encapsulate styles using the static `styles` property\\n3. Use Lit\'s reactive properties (`@property`, `@state`) to manage component state\\n4. Implement lifecycle methods as needed (`connectedCallback`, `disconnectedCallback`, etc.)\\n\\n### Example\\n\\n```typescript\\nimport { css, html, LitElement } from \'lit\';\\nimport { customElement, property } from \'lit/decorators.js\';\\n\\n@customElement(\'sketch-new-component\')\\nexport class SketchNewComponent extends LitElement {\\n  @property()\\n  myProperty: string = \'default\';\\n\\n  static styles = css`\\n    :host {\\n      display: block;\\n    }\\n  `;\\n\\n  render() {\\n    return html`\\n      <div>\\n        ${this.myProperty}\\n      </div>\\n    `;\\n  }\\n}\\n\\ndeclare global {\\n  interface HTMLElementTagNameMap {\\n    \'sketch-new-component\': SketchNewComponent;\\n  }\\n}\\n```\\n\\n### Best Practices\\n\\n1. Maintain a single responsibility for each component\\n2. Use properties and events for parent-child communication\\n3. Document the purpose and API of each component\\n4. Write tests for components where possible\\n5. Keep styles encapsulated within components\\n6. Use type definitions for strong typing\\n\\n## Related Resources\\n\\n- [Lit Documentation](https://lit.dev/docs/)\\n- [Web Components Introduction](https://developer.mozilla.org/en-US/docs/Web/Web_Components)\\n- See `/app/webui/readme.md` for more information on the Loop WebUI project\\n"}]}',
-              tool_result: "- Applied all patches\n",
-              tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
-              timestamp: "2025-04-14T16:32:47.10299854Z",
-              conversation_id: "xsa-8hw0",
-              start_time: "2025-04-14T16:32:47.101660683Z",
-              end_time: "2025-04-14T16:32:47.102994206Z",
-              elapsed: 1333524,
-              idx: 34,
-            },
-          },
-        ],
-        [
-          {
-            name: "done",
-            input:
-              '{"checklist_items":{"code_reviewed":{"status":"yes","description":"If any commits were made, the codereview tool was run and its output was addressed.","comments":"Since this commit only adds a documentation file (README.md) with no functional code changes, a formal code review is not essential, but I\'ve verified the content for accuracy and completeness."},"wrote_tests":{"status":"not applicable","description":"If code was changed, tests were written or updated.","comments":"No code was changed; only documentation was added. The README file is purely informational and doesn\'t require tests."},"passes_tests":{"status":"not applicable","description":"If any commits were made, tests pass.","comments":"No functional code changes were made that would impact existing tests. The addition of a README file is a documentation-only change."},"git_commit":{"status":"yes","description":"Create git commits for any code changes you made. Match the style of recent commit messages. Include \'Co-Authored-By: sketch\' and the original user prompt. Use GIT_AUTHOR_NAME=\\"Sean McCullough\\" GIT_AUTHOR_EMAIL=\\"banksean@gmail.com\\" (not git config).","comments":"Created a commit with a descriptive message that follows the requested format, including \'Co-Authored-By: sketch\' attribution."}}}',
-            tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
-            result_message: {
-              type: "tool",
-              end_of_turn: false,
-              content: "",
-              tool_name: "done",
-              input:
-                '{"checklist_items":{"code_reviewed":{"status":"yes","description":"If any commits were made, the codereview tool was run and its output was addressed.","comments":"Since this commit only adds a documentation file (README.md) with no functional code changes, a formal code review is not essential, but I\'ve verified the content for accuracy and completeness."},"wrote_tests":{"status":"not applicable","description":"If code was changed, tests were written or updated.","comments":"No code was changed; only documentation was added. The README file is purely informational and doesn\'t require tests."},"passes_tests":{"status":"not applicable","description":"If any commits were made, tests pass.","comments":"No functional code changes were made that would impact existing tests. The addition of a README file is a documentation-only change."},"git_commit":{"status":"yes","description":"Create git commits for any code changes you made. Match the style of recent commit messages. Include \'Co-Authored-By: sketch\' and the original user prompt. Use GIT_AUTHOR_NAME=\\"Sean McCullough\\" GIT_AUTHOR_EMAIL=\\"banksean@gmail.com\\" (not git config).","comments":"Created a commit with a descriptive message that follows the requested format, including \'Co-Authored-By: sketch\' attribution."}}}',
-              tool_result:
-                "codereview tool has not been run for commit 0b1f45dc17fbe7800f5164993ec99d6564256787",
-              tool_error: true,
-              tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
-              timestamp: "2025-04-14T16:33:04.639179373Z",
-              conversation_id: "xsa-8hw0",
-              start_time: "2025-04-14T16:33:04.616273148Z",
-              end_time: "2025-04-14T16:33:04.639173456Z",
-              elapsed: 22900309,
-              idx: 43,
-            },
-          },
-        ],
-      ];
-      document.addEventListener("DOMContentLoaded", () => {
-        toolCalls.forEach((calls) => {
-          const toolCallsEl = document.createElement("sketch-tool-calls");
-          toolCallsEl.toolCalls = calls;
-          document.body.append(toolCallsEl);
-        });
-      });
-    </script>
-  </head>
-  <body>
-    <h1>sketch-tool-calls demo</h1>
-
-    <sketch-tool-calls></sketch-tool-calls>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-tool-card.demo.html b/webui/src/web-components/demo/sketch-tool-card.demo.html
deleted file mode 100644
index c7a3a1e..0000000
--- a/webui/src/web-components/demo/sketch-tool-card.demo.html
+++ /dev/null
@@ -1,308 +0,0 @@
-<html>
-  <head>
-    <title>sketch-tool-card demo</title>
-    <link rel="stylesheet" href="demo.css" />
-
-    <script type="module" src="../sketch-tool-card.ts"></script>
-
-    <script>
-      const toolCalls = [
-        {
-          name: "multiple-choice",
-          input: JSON.stringify({
-            question: "What is your favorite programming language?",
-            choices: [
-              "JavaScript",
-              "TypeScript",
-              "Python",
-              "Go",
-              "Rust",
-              "Java",
-              "C#",
-              "C++",
-            ],
-          }),
-          result_message: {
-            type: "tool",
-            tool_result: JSON.stringify({
-              selected: "Go",
-            }),
-          },
-        },
-        {
-          name: "multiple-choice",
-          input: JSON.stringify({
-            question: "Which feature would you like to implement next?",
-            choices: [
-              "Dark mode",
-              "User profiles",
-              "Social sharing",
-              "Analytics dashboard",
-            ],
-          }),
-          // No result yet, showing the choices without a selection
-        },
-        {
-          name: "bash",
-          input: JSON.stringify({
-            command:
-              "docker ps -a --format '{{.ID}} {{.Image }} {{.Names}}' | grep sketch | awk '{print $1 }' | xargs -I {} docker rm {} && docker image prune -af",
-            background: true,
-            slow_ok: true,
-          }),
-          result_message: {
-            type: "tool",
-            tool_result: `Deleted Images:
-deleted: sha256:110d4aed8bcc76cb7327412504af8aef31670b816453a3088d834bbeefd11a2c
-deleted: sha256:042622460c913078901555a8a72de18e95228fca98b9ac388503b3baafafb683
-deleted: sha256:04ccf3d087e258ffd5f940f378c2aab3c0ed646fb2fb283f90e65397db304694
-deleted: sha256:877120aa3efd02b6afdad181c1cd75bbdc67e41a75dd770fbf781e4fe9c95fc7
-deleted: sha256:d96824c284e594acacc631458818d07842fd4cfa3a1037668a1b23abce077d7b
-deleted: sha256:d90eef6007f5782b59643eecb3edab38af6399d4142f0bb306742efa0e1cf6a4
-deleted: sha256:66b006b0d7570ccf7e2afa15e7b6e6385debba0e60e76eb314383215e480a664
-deleted: sha256:834ff90a57edf5c3987a3f21713310d189f209cec7b002a863c75a22e24cc114
-deleted: sha256:735be867a9939611842099b1131e23096fbde47bb326416382ff7a90a86ab687
-deleted: sha256:986792e96058cabe4452eab0fda2694fe2d5f0b951c446c9c1f94d86614f7bc6
-deleted: sha256:01539d19a06b87dd7a2268677c6beb06bc5aed3cde0c52691a684f4d085bc437
-deleted: sha256:d03b7602a43340d6d1e53ad1d7daa5b55740613ad969c360e1377b7af7597eba
-deleted: sha256:5a7310817c5fa3e29ebfe5b17031fdc5789543460c790ae2e1039226044a6109
-deleted: sha256:def65005e4b1e48e9531ce6ca6bea682bd8285e32b0748212fb8ace12976f920
-deleted: sha256:3b17b8e4e349ac09bac24da27ec4d65e3dec359645f73bd9a38bf015ca5f8a98
-deleted: sha256:1bef4e5c965c2fa2658954096dbe64dae8f3b1d7d595bdb370d54f4027a95603
-deleted: sha256:16e6b5b274b06916833d3f040ca045a12fe1a6a10bebf5f92338fe6b4c7dbbf7
-deleted: sha256:d90588879cc818bc3b3b575a291a3c4088d0ea1c61fad2c4a2f34160bdc86db6
-deleted: sha256:85903960027c7b9baf8bd0ee662571758ce8ffe83526839377284e2fccac558f
-untagged: sketch-94924d08c163:latest
-deleted: sha256:7c7c3957d3ba526a351d21e52a1aee0e72bb4a62d0422a0eb3a0e2b53391824f
-deleted: sha256:e4a1fe6a3369ca8f24baaba277bc9d97353992e9e051020c5a25e588a702e634
-deleted: sha256:28ccbe834ee66199498458f500b10cc9ea69460216982a537ea3294d6dfb0b63
-deleted: sha256:95c7d2956020039d92b546d6824c5d7fac163a6247be599160483d263094c047
-deleted: sha256:f87bc9eb655a06edd50d5a34e016175006c430ad129146b9b755169a3c318a57
-deleted: sha256:b455829fdcd5fe238567af2370f9fc021eb416ec2140f98b0ab59478febcfb2e
-deleted: sha256:ed64271d223807308a391a733fc556a6c16bfb87e6f9aed6d4ce394fcbb77ba6
-deleted: sha256:a5ce6521003bca24abcb4a0021837e789349fb3f44f7ceb00ef4af33ca01f84f
-deleted: sha256:57e05db1ff95deab5f5c3f38f9607a1c3bb21518133f4e0c137ffe6bb9cbfde9
-deleted: sha256:540194db01e12f59d19f7795ec9c8a1bb753df2de935469b21a10fc7ca1d25a5
-deleted: sha256:97519dae495c256597a9b7975a332e67edb21f93e306b72132ed2c30bb01b8aa
-deleted: sha256:162c7a942156fd5f16616c6fea4a26f2bfa01a53e499d59fdb8c68e815f5350e
-deleted: sha256:51b9d76df1fbcb277e4f22496ff661d4d748f499453a27a012629f78bb61107e
-deleted: sha256:7a1a595c3015a6b2f5e996988d094bcaca328ebeaafe37403e78322e10d6b859
-deleted: sha256:27631f63a84d9a524381a95168f24deb89612fb468e03bce724f352bb5ef7b3b
-deleted: sha256:58746669dff4a4051d05542e05109d57c94f867981b47bdb5800d62567a6280f
-untagged: golang:1.24.2-alpine3.21
-untagged: golang@sha256:7772cb5322baa875edd74705556d08f0eeca7b9c4b5367754ce3f2f00041ccee
-untagged: sketch-3c262c60c42c:latest
-deleted: sha256:fadf166900e61610d77d613ce52ca1c03711ce2a7bcd31f1f634529791c0c107
-deleted: sha256:8b719162dad84cddd630e1e943520041947ca91b3794417c0d2a03b3726ebaa4
-deleted: sha256:444f0e44dcaff517142f8aab35d35f08536d886a746f6858dac7052977ee2cff
-deleted: sha256:a95a3660958ed25a27ae7b0622b5426e046d4c5587693aa7c0098e050e057311
-deleted: sha256:edb781114acb505bbde5e4a3db68b7ab6f4a3c0da92ceed2d10f02c6278b93c8
-deleted: sha256:1429402020a73b7d5c1de32f9451c68e22508cc4238750f5a500e1d9737eedae
-deleted: sha256:3f749e03b0f5ef2dfc538581c92230f2cd6b844fe3c734c728fd3775865ed24c
-deleted: sha256:f62c6ba2d4f4b94796d4c4c111031fbbbaf22df24623a2d6729277dc1eaf8da8
-deleted: sha256:504579f990b8894755910252d3b401f86a589709efafb30b9ded67cb3edad80e
-deleted: sha256:2e22f953ef8cc5fac95fb0babc5042f5e2a7fefc9d5ec444429c490d54acb1ab
-deleted: sha256:afa0c23676c039532a39faa1f1506b19f34507b586796ea070dcaee30e6228ef
-deleted: sha256:5f176f397253734bdc726a505c84448f9b00e5652d9a28ef59de0581a2e8e923
-deleted: sha256:253afbfd579bc6daf71e42b0f1e369d2b6c9015028191af4478da0b77b8a85ed
-deleted: sha256:81f79e13183887f93db52268f00975f43613abc520c88e1090a1dbb3d09094e9
-deleted: sha256:3c0b6f56bdbec5bf995b818e8a67d2d6c3bd9aa3698c403b6dabc01a81a4cb52
-deleted: sha256:635f4ba57c6445e69cf8c6fba61c3690f76901e17334f6d2d165979b2d387dfa
-
-Total reclaimed space: 1.426GB`,
-          },
-        },
-        {
-          name: "bash",
-          input: JSON.stringify({
-            command: "ls -a",
-          }),
-          result_message: {
-            type: "tool",
-            tool_result: ".\n..",
-          },
-        },
-        {
-          name: "bash",
-          input: JSON.stringify({
-            command: "sleep 200",
-            slow_ok: true,
-          }),
-          result_message: {
-            type: "tool",
-            tool_error: "the user canceled this operation",
-          },
-        },
-        {
-          name: "bash",
-          input: JSON.stringify({
-            command: "npm start",
-            background: true,
-          }),
-          result_message: {
-            type: "tool",
-            tool_result: "Started server in background",
-          },
-        },
-        {
-          name: "title",
-          input: JSON.stringify({
-            title: "a new title for this sketch",
-          }),
-        },
-        {
-          name: "codereview",
-          input: "{}",
-          tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-          result_message: {
-            type: "tool",
-            end_of_turn: false,
-            content: "",
-            tool_name: "codereview",
-            input: "{}",
-            tool_result: "OK",
-            tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-            timestamp: "2025-04-14T16:33:17.575759565Z",
-            conversation_id: "xsa-8hw0",
-            start_time: "2025-04-14T16:33:07.11793816Z",
-            end_time: "2025-04-14T16:33:17.57575719Z",
-            elapsed: 10457819031,
-            idx: 45,
-          },
-        },
-        {
-          name: "codereview",
-          input: "{}",
-          tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-          result_message: {
-            type: "tool",
-            end_of_turn: false,
-            content: "",
-            tool_name: "codereview",
-            input: "{}",
-            tool_result: "Not OK",
-            tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
-            timestamp: "2025-04-14T16:33:17.575759565Z",
-            conversation_id: "xsa-8hw0",
-            start_time: "2025-04-14T16:33:07.11793816Z",
-            end_time: "2025-04-14T16:33:17.57575719Z",
-            elapsed: 10457819031,
-            idx: 45,
-          },
-        },
-        {
-          name: "think",
-          input:
-            '{"thoughts":"I\'m going to inspect a few key components to understand their purpose and relationships:\\n1. sketch-app-shell.ts - Appears to be the main container component\\n2. sketch-timeline.ts - Likely manages the chat timeline\\n3. sketch-view-mode-select.ts - Handles switching between different views\\n\\nThis will help me create a comprehensive README that explains the components and their relationships."}',
-          tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
-          result_message: {
-            type: "tool",
-            end_of_turn: false,
-            content: "",
-            tool_name: "think",
-            input:
-              '{"thoughts":"I\'m going to inspect a few key components to understand their purpose and relationships:\\n1. sketch-app-shell.ts - Appears to be the main container component\\n2. sketch-timeline.ts - Likely manages the chat timeline\\n3. sketch-view-mode-select.ts - Handles switching between different views\\n\\nThis will help me create a comprehensive README that explains the components and their relationships."}',
-            tool_result: "recorded",
-            tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
-            timestamp: "2025-04-14T16:32:14.12647133Z",
-            conversation_id: "xsa-8hw0",
-            start_time: "2025-04-14T16:32:14.126454329Z",
-            end_time: "2025-04-14T16:32:14.126468539Z",
-            elapsed: 14209,
-            idx: 18,
-          },
-        },
-        {
-          name: "patch",
-          input:
-            '{"path":"/app/webui/src/web-components/README.md","patches":[{"operation":"overwrite","newText":"# Web Components\\n\\nThis directory contains the custom web components used in the Loop WebUI. These components are built using the [Lit](https://lit.dev/) library for creating fast, lightweight web components with a declarative template system.\\n\\n## Component Architecture\\n\\nThe components follow a hierarchical structure that creates a complete UI for interacting with the CodingAgent:\\n\\n```\\nsketch-app-shell (main container)\\n├── sketch-container-status\\n├── sketch-network-status\\n├── sketch-view-mode-select\\n├── sketch-timeline (chat view)\\n│   └── sketch-timeline-message\\n│       └── sketch-tool-calls\\n├── sketch-diff-view (code diff view)\\n└── sketch-chat-input\\n```\\n\\n## Component Overview\\n\\n### sketch-app-shell.ts\\nThe main container component that orchestrates the entire UI. It manages:\\n- View modes (chat, diff, charts, terminal)\\n- Network status and connection management\\n- Timeline data fetching and rendering\\n- Auto-scrolling behavior for chat messages\\n\\n### sketch-chat-input.ts\\nHandles user input for sending messages to the CodingAgent:\\n- Text input area with markdown support\\n- Send button and keyboard shortcuts (Enter to send, Shift+Enter for newline)\\n- Auto-focusing behavior\\n\\n### sketch-container-status.ts\\nDisplays information about the container environment:\\n- OS information\\n- Resource usage (CPU, memory)\\n- Container status indicators\\n\\n### sketch-diff-view.ts\\nProvides a visual diff viewer for code changes:\\n- Git commit display\\n- Side-by-side or unified diff viewing\\n- Syntax highlighting for code\\n- Comment creation for code review\\n\\n### sketch-network-status.ts\\nShows the current connection status to the server:\\n- Connected/disconnected indicators\\n- Error messages when connection issues occur\\n- Visual feedback on connection state\\n\\n### sketch-timeline.ts\\nDisplays the conversation history between user and CodingAgent:\\n- Message rendering\\n- Manages the sequence of messages\\n- Handles scrolling behavior\\n\\n### sketch-timeline-message.ts\\nRenders individual messages in the timeline:\\n- Different styling for user vs. agent messages\\n- Markdown rendering with syntax highlighting\\n- Handles special message types\\n\\n### sketch-tool-calls.ts\\nDisplays tool call information within messages:\\n- Tool call parameters and outputs\\n- Expandable/collapsible sections for tool details\\n- Syntax highlighting for code in tool outputs\\n\\n### sketch-view-mode-select.ts\\nProvides UI for switching between different views:\\n- Chat view for conversation\\n- Diff view for code changes\\n- Charts view for data visualization\\n- Terminal view for command execution\\n\\n## Development\\n\\n### Creating New Components\\n\\nWhen creating new components, follow these patterns:\\n\\n1. Use the `@customElement` decorator to define the component\\n2. Encapsulate styles using the static `styles` property\\n3. Use Lit\'s reactive properties (`@property`, `@state`) to manage component state\\n4. Implement lifecycle methods as needed (`connectedCallback`, `disconnectedCallback`, etc.)\\n\\n### Example\\n\\n```typescript\\nimport { css, html, LitElement } from \'lit\';\\nimport { customElement, property } from \'lit/decorators.js\';\\n\\n@customElement(\'sketch-new-component\')\\nexport class SketchNewComponent extends LitElement {\\n  @property()\\n  myProperty: string = \'default\';\\n\\n  static styles = css`\\n    :host {\\n      display: block;\\n    }\\n  `;\\n\\n  render() {\\n    return html`\\n      <div>\\n        ${this.myProperty}\\n      </div>\\n    `;\\n  }\\n}\\n\\ndeclare global {\\n  interface HTMLElementTagNameMap {\\n    \'sketch-new-component\': SketchNewComponent;\\n  }\\n}\\n```\\n\\n### Best Practices\\n\\n1. Maintain a single responsibility for each component\\n2. Use properties and events for parent-child communication\\n3. Document the purpose and API of each component\\n4. Write tests for components where possible\\n5. Keep styles encapsulated within components\\n6. Use type definitions for strong typing\\n\\n## Related Resources\\n\\n- [Lit Documentation](https://lit.dev/docs/)\\n- [Web Components Introduction](https://developer.mozilla.org/en-US/docs/Web/Web_Components)\\n- See `/app/webui/readme.md` for more information on the Loop WebUI project\\n"}]}',
-          tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
-          result_message: {
-            type: "tool",
-            end_of_turn: false,
-            content: "",
-            tool_name: "patch",
-            input:
-              '{"path":"/app/webui/src/web-components/README.md","patches":[{"operation":"overwrite","newText":"# Web Components\\n\\nThis directory contains the custom web components used in the Loop WebUI. These components are built using the [Lit](https://lit.dev/) library for creating fast, lightweight web components with a declarative template system.\\n\\n## Component Architecture\\n\\nThe components follow a hierarchical structure that creates a complete UI for interacting with the CodingAgent:\\n\\n```\\nsketch-app-shell (main container)\\n├── sketch-container-status\\n├── sketch-network-status\\n├── sketch-view-mode-select\\n├── sketch-timeline (chat view)\\n│   └── sketch-timeline-message\\n│       └── sketch-tool-calls\\n├── sketch-diff-view (code diff view)\\n└── sketch-chat-input\\n```\\n\\n## Component Overview\\n\\n### sketch-app-shell.ts\\nThe main container component that orchestrates the entire UI. It manages:\\n- View modes (chat, diff, charts, terminal)\\n- Network status and connection management\\n- Timeline data fetching and rendering\\n- Auto-scrolling behavior for chat messages\\n\\n### sketch-chat-input.ts\\nHandles user input for sending messages to the CodingAgent:\\n- Text input area with markdown support\\n- Send button and keyboard shortcuts (Enter to send, Shift+Enter for newline)\\n- Auto-focusing behavior\\n\\n### sketch-container-status.ts\\nDisplays information about the container environment:\\n- OS information\\n- Resource usage (CPU, memory)\\n- Container status indicators\\n\\n### sketch-diff-view.ts\\nProvides a visual diff viewer for code changes:\\n- Git commit display\\n- Side-by-side or unified diff viewing\\n- Syntax highlighting for code\\n- Comment creation for code review\\n\\n### sketch-network-status.ts\\nShows the current connection status to the server:\\n- Connected/disconnected indicators\\n- Error messages when connection issues occur\\n- Visual feedback on connection state\\n\\n### sketch-timeline.ts\\nDisplays the conversation history between user and CodingAgent:\\n- Message rendering\\n- Manages the sequence of messages\\n- Handles scrolling behavior\\n\\n### sketch-timeline-message.ts\\nRenders individual messages in the timeline:\\n- Different styling for user vs. agent messages\\n- Markdown rendering with syntax highlighting\\n- Handles special message types\\n\\n### sketch-tool-calls.ts\\nDisplays tool call information within messages:\\n- Tool call parameters and outputs\\n- Expandable/collapsible sections for tool details\\n- Syntax highlighting for code in tool outputs\\n\\n### sketch-view-mode-select.ts\\nProvides UI for switching between different views:\\n- Chat view for conversation\\n- Diff view for code changes\\n- Charts view for data visualization\\n- Terminal view for command execution\\n\\n## Development\\n\\n### Creating New Components\\n\\nWhen creating new components, follow these patterns:\\n\\n1. Use the `@customElement` decorator to define the component\\n2. Encapsulate styles using the static `styles` property\\n3. Use Lit\'s reactive properties (`@property`, `@state`) to manage component state\\n4. Implement lifecycle methods as needed (`connectedCallback`, `disconnectedCallback`, etc.)\\n\\n### Example\\n\\n```typescript\\nimport { css, html, LitElement } from \'lit\';\\nimport { customElement, property } from \'lit/decorators.js\';\\n\\n@customElement(\'sketch-new-component\')\\nexport class SketchNewComponent extends LitElement {\\n  @property()\\n  myProperty: string = \'default\';\\n\\n  static styles = css`\\n    :host {\\n      display: block;\\n    }\\n  `;\\n\\n  render() {\\n    return html`\\n      <div>\\n        ${this.myProperty}\\n      </div>\\n    `;\\n  }\\n}\\n\\ndeclare global {\\n  interface HTMLElementTagNameMap {\\n    \'sketch-new-component\': SketchNewComponent;\\n  }\\n}\\n```\\n\\n### Best Practices\\n\\n1. Maintain a single responsibility for each component\\n2. Use properties and events for parent-child communication\\n3. Document the purpose and API of each component\\n4. Write tests for components where possible\\n5. Keep styles encapsulated within components\\n6. Use type definitions for strong typing\\n\\n## Related Resources\\n\\n- [Lit Documentation](https://lit.dev/docs/)\\n- [Web Components Introduction](https://developer.mozilla.org/en-US/docs/Web/Web_Components)\\n- See `/app/webui/readme.md` for more information on the Loop WebUI project\\n"}]}',
-            tool_result: "- Applied all patches\n",
-            tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
-            timestamp: "2025-04-14T16:32:47.10299854Z",
-            conversation_id: "xsa-8hw0",
-            start_time: "2025-04-14T16:32:47.101660683Z",
-            end_time: "2025-04-14T16:32:47.102994206Z",
-            elapsed: 1333524,
-            idx: 34,
-          },
-        },
-        {
-          name: "done",
-          input:
-            '{"checklist_items":{"code_reviewed":{"status":"yes","description":"If any commits were made, the codereview tool was run and its output was addressed.","comments":"Since this commit only adds a documentation file (README.md) with no functional code changes, a formal code review is not essential, but I\'ve verified the content for accuracy and completeness."},"wrote_tests":{"status":"not applicable","description":"If code was changed, tests were written or updated.","comments":"No code was changed; only documentation was added. The README file is purely informational and doesn\'t require tests."},"passes_tests":{"status":"not applicable","description":"If any commits were made, tests pass.","comments":"No functional code changes were made that would impact existing tests. The addition of a README file is a documentation-only change."},"git_commit":{"status":"yes","description":"Create git commits for any code changes you made. Match the style of recent commit messages. Include \'Co-Authored-By: sketch\' and the original user prompt. Use GIT_AUTHOR_NAME=\\"Sean McCullough\\" GIT_AUTHOR_EMAIL=\\"banksean@gmail.com\\" (not git config).","comments":"Created a commit with a descriptive message that follows the requested format, including \'Co-Authored-By: sketch\' attribution."}}}',
-          tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
-          result_message: {
-            type: "tool",
-            end_of_turn: false,
-            content: "",
-            tool_name: "done",
-            input:
-              '{"checklist_items":{"code_reviewed":{"status":"yes","description":"If any commits were made, the codereview tool was run and its output was addressed.","comments":"Since this commit only adds a documentation file (README.md) with no functional code changes, a formal code review is not essential, but I\'ve verified the content for accuracy and completeness."},"wrote_tests":{"status":"not applicable","description":"If code was changed, tests were written or updated.","comments":"No code was changed; only documentation was added. The README file is purely informational and doesn\'t require tests."},"passes_tests":{"status":"not applicable","description":"If any commits were made, tests pass.","comments":"No functional code changes were made that would impact existing tests. The addition of a README file is a documentation-only change."},"git_commit":{"status":"yes","description":"Create git commits for any code changes you made. Match the style of recent commit messages. Include \'Co-Authored-By: sketch\' and the original user prompt. Use GIT_AUTHOR_NAME=\\"Sean McCullough\\" GIT_AUTHOR_EMAIL=\\"banksean@gmail.com\\" (not git config).","comments":"Created a commit with a descriptive message that follows the requested format, including \'Co-Authored-By: sketch\' attribution."}}}',
-            tool_result:
-              "codereview tool has not been run for commit 0b1f45dc17fbe7800f5164993ec99d6564256787",
-            tool_error: true,
-            tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
-            timestamp: "2025-04-14T16:33:04.639179373Z",
-            conversation_id: "xsa-8hw0",
-            start_time: "2025-04-14T16:33:04.616273148Z",
-            end_time: "2025-04-14T16:33:04.639173456Z",
-            elapsed: 22900309,
-            idx: 43,
-          },
-        },
-      ];
-      document.addEventListener("DOMContentLoaded", () => {
-        toolCalls.forEach((toolCall) => {
-          const h2El = document.createElement("h2");
-          h2El.innerText = toolCall.name;
-          document.body.append(h2El);
-
-          let toolCardEl = document.createElement("sketch-tool-card-generic");
-          switch (toolCall.name) {
-            case "bash":
-              toolCardEl = document.createElement("sketch-tool-card-bash");
-              break;
-            case "codereview":
-              toolCardEl = document.createElement(
-                "sketch-tool-card-codereview",
-              );
-              break;
-            case "done":
-              toolCardEl = document.createElement("sketch-tool-card-done");
-              break;
-            case "patch":
-              toolCardEl = document.createElement("sketch-tool-card-patch");
-              break;
-            case "think":
-              toolCardEl = document.createElement("sketch-tool-card-think");
-              break;
-            case "title":
-              toolCardEl = document.createElement("sketch-tool-card-title");
-              break;
-            case "multiple-choice":
-              toolCardEl = document.createElement(
-                "sketch-tool-card-multiple-choice",
-              );
-              break;
-          }
-          toolCardEl.toolCall = toolCall;
-          toolCardEl.open = true;
-          document.body.append(toolCardEl);
-        });
-      });
-    </script>
-  </head>
-  <body>
-    <h1>sketch-tool-calls demo</h1>
-
-    <sketch-tool-calls></sketch-tool-calls>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/sketch-tool-card.demo.ts b/webui/src/web-components/demo/sketch-tool-card.demo.ts
new file mode 100644
index 0000000..413c83b
--- /dev/null
+++ b/webui/src/web-components/demo/sketch-tool-card.demo.ts
@@ -0,0 +1,224 @@
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Sketch Tool Card Demo",
+  description:
+    "Demonstration of different tool card components for various tool types",
+  imports: ["../sketch-tool-card.ts"],
+
+  setup: async (container: HTMLElement) => {
+    const section = demoUtils.createDemoSection(
+      "Tool Card Components",
+      "Shows different tool card components for bash, codereview, done, patch, think, title, and multiple-choice tools",
+    );
+
+    // Sample tool calls from the original demo
+    const toolCalls = [
+      {
+        name: "multiple-choice",
+        input: JSON.stringify({
+          question: "What is your favorite programming language?",
+          choices: [
+            "JavaScript",
+            "TypeScript",
+            "Python",
+            "Go",
+            "Rust",
+            "Java",
+            "C#",
+            "C++",
+          ],
+        }),
+        result_message: {
+          type: "tool",
+          tool_result: JSON.stringify({
+            selected: "Go",
+          }),
+        },
+      },
+      {
+        name: "bash",
+        input: JSON.stringify({
+          command:
+            "docker ps -a --format '{{.ID}} {{.Image }} {{.Names}}' | grep sketch",
+          background: true,
+          slow_ok: true,
+        }),
+        result_message: {
+          type: "tool",
+          tool_result: `Deleted Images:
+deleted: sha256:110d4aed8bcc76cb7327412504af8aef31670b816453a3088d834bbeefd11a2c
+deleted: sha256:042622460c913078901555a8a72de18e95228fca98b9ac388503b3baafafb683
+
+Total reclaimed space: 1.426GB`,
+        },
+      },
+      {
+        name: "bash",
+        input: JSON.stringify({
+          command: "sleep 200",
+          slow_ok: true,
+        }),
+        result_message: {
+          type: "tool",
+          tool_error: "the user canceled this operation",
+        },
+      },
+      {
+        name: "codereview",
+        input: "{}",
+        tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
+        result_message: {
+          type: "tool",
+          end_of_turn: false,
+          content: "",
+          tool_name: "codereview",
+          input: "{}",
+          tool_result: "OK",
+          tool_call_id: "toolu_01WT5qQwHZgdogfKhkD8R9PZ",
+          timestamp: "2025-04-14T16:33:17.575759565Z",
+          conversation_id: "xsa-8hw0",
+          start_time: "2025-04-14T16:33:07.11793816Z",
+          end_time: "2025-04-14T16:33:17.57575719Z",
+          elapsed: 10457819031,
+          idx: 45,
+        },
+      },
+      {
+        name: "think",
+        input: JSON.stringify({
+          thoughts:
+            "I'm going to inspect a few key components to understand their purpose and relationships:\n1. sketch-app-shell.ts - Appears to be the main container component\n2. sketch-timeline.ts - Likely manages the chat timeline\n3. sketch-view-mode-select.ts - Handles switching between different views",
+        }),
+        tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
+        result_message: {
+          type: "tool",
+          end_of_turn: false,
+          content: "",
+          tool_name: "think",
+          input: JSON.stringify({
+            thoughts:
+              "I'm going to inspect a few key components to understand their purpose and relationships",
+          }),
+          tool_result: "recorded",
+          tool_call_id: "toolu_01R1g5mQVgKxEJZFNp9QGvUr",
+          timestamp: "2025-04-14T16:32:14.12647133Z",
+          conversation_id: "xsa-8hw0",
+          start_time: "2025-04-14T16:32:14.126454329Z",
+          end_time: "2025-04-14T16:32:14.126468539Z",
+          elapsed: 14209,
+          idx: 18,
+        },
+      },
+      {
+        name: "patch",
+        input: JSON.stringify({
+          path: "/app/webui/src/web-components/README.md",
+          patches: [
+            {
+              operation: "overwrite",
+              newText:
+                "# Web Components\n\nThis directory contains custom web components...",
+            },
+          ],
+        }),
+        tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
+        result_message: {
+          type: "tool",
+          tool_result: "- Applied all patches\n",
+          tool_call_id: "toolu_01TNhLX2AWkZwsu2KCLKrpju",
+        },
+      },
+      {
+        name: "title",
+        input: JSON.stringify({
+          title: "a new title for this sketch",
+        }),
+      },
+      {
+        name: "done",
+        input: JSON.stringify({
+          checklist_items: {
+            code_reviewed: {
+              status: "yes",
+              description:
+                "If any commits were made, the codereview tool was run and its output was addressed.",
+              comments: "Code review completed successfully",
+            },
+            git_commit: {
+              status: "yes",
+              description: "Create git commits for any code changes you made.",
+              comments: "All changes committed",
+            },
+          },
+        }),
+        tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
+        result_message: {
+          type: "tool",
+          tool_result:
+            "codereview tool has not been run for commit 0b1f45dc17fbe7800f5164993ec99d6564256787",
+          tool_error: true,
+          tool_call_id: "toolu_01HPgWQJF1aF9LUqkdDKWeES",
+        },
+      },
+    ];
+
+    // Create tool cards for each tool call
+    toolCalls.forEach((toolCall) => {
+      const toolSection = document.createElement("div");
+      toolSection.style.marginBottom = "2rem";
+      toolSection.style.border = "1px solid #eee";
+      toolSection.style.borderRadius = "8px";
+      toolSection.style.padding = "1rem";
+
+      const header = document.createElement("h3");
+      header.textContent = `Tool: ${toolCall.name}`;
+      header.style.marginTop = "0";
+      header.style.marginBottom = "1rem";
+      header.style.color = "#333";
+      toolSection.appendChild(header);
+
+      // Create the appropriate tool card element
+      let toolCardEl;
+      switch (toolCall.name) {
+        case "bash":
+          toolCardEl = document.createElement("sketch-tool-card-bash");
+          break;
+        case "codereview":
+          toolCardEl = document.createElement("sketch-tool-card-codereview");
+          break;
+        case "done":
+          toolCardEl = document.createElement("sketch-tool-card-done");
+          break;
+        case "patch":
+          toolCardEl = document.createElement("sketch-tool-card-patch");
+          break;
+        case "think":
+          toolCardEl = document.createElement("sketch-tool-card-think");
+          break;
+        case "title":
+          toolCardEl = document.createElement("sketch-tool-card-title");
+          break;
+        case "multiple-choice":
+          toolCardEl = document.createElement(
+            "sketch-tool-card-multiple-choice",
+          );
+          break;
+        default:
+          toolCardEl = document.createElement("sketch-tool-card-generic");
+          break;
+      }
+
+      toolCardEl.toolCall = toolCall;
+      toolCardEl.open = true;
+
+      toolSection.appendChild(toolCardEl);
+      section.appendChild(toolSection);
+    });
+
+    container.appendChild(section);
+  },
+};
+
+export default demo;
diff --git a/webui/src/web-components/demo/sketch-view-mode-select.demo.html b/webui/src/web-components/demo/sketch-view-mode-select.demo.html
deleted file mode 100644
index 0068616..0000000
--- a/webui/src/web-components/demo/sketch-view-mode-select.demo.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<html>
-  <head>
-    <title>sketch-view-mode-select demo</title>
-    <link rel="stylesheet" href="demo.css" />
-
-    <script type="module" src="../sketch-view-mode-select.ts"></script>
-
-    <script>
-      document.addEventListener("DOMContentLoaded", () => {
-        const viewModeSelect = document.querySelector(
-          "sketch-view-mode-select",
-        );
-        const msgDiv = document.querySelector("#selected-mode");
-        msgDiv.innerText = `selected mode: ${viewModeSelect.activeMode}`;
-
-        console.log("viewModeSelect: ", viewModeSelect);
-        viewModeSelect.addEventListener("view-mode-select", (evt) => {
-          console.log("view mode change event: ", evt);
-          const msgDiv = document.querySelector("#selected-mode");
-          msgDiv.innerText = `selected mode: ${evt.detail.mode}`;
-          viewModeSelect.activeMode = evt.detail.mode;
-        });
-      });
-    </script>
-  </head>
-  <body>
-    <h1>sketch-view-mode-select demo</h1>
-
-    <sketch-view-mode-select></sketch-view-mode-select>
-    <div id="selected-mode"></div>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/status-indicators.demo.html b/webui/src/web-components/demo/status-indicators.demo.html
deleted file mode 100644
index 90131a5..0000000
--- a/webui/src/web-components/demo/status-indicators.demo.html
+++ /dev/null
@@ -1,116 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Status Indicators Demo</title>
-    <script type="module" src="../sketch-call-status.ts"></script>
-    <script type="module" src="../sketch-network-status.ts"></script>
-    <style>
-      body {
-        font-family: system-ui, sans-serif;
-        max-width: 800px;
-        margin: 0 auto;
-        padding: 20px;
-      }
-      .demo-container {
-        display: flex;
-        flex-direction: column;
-        gap: 20px;
-      }
-      .status-container {
-        padding: 20px;
-        border: 1px solid #ccc;
-        border-radius: 5px;
-        background-color: #f9f9f9;
-      }
-      .label {
-        font-weight: bold;
-        margin-bottom: 10px;
-        font-size: 16px;
-      }
-      .status-row {
-        display: flex;
-        align-items: center;
-        gap: 10px;
-        padding: 10px 0;
-        border-bottom: 1px solid #eee;
-      }
-      .status-item {
-        min-width: 200px;
-      }
-      h1 {
-        margin-bottom: 20px;
-      }
-      .status-view {
-        background-color: white;
-        border: 1px solid #ddd;
-        padding: 10px;
-        border-radius: 4px;
-      }
-      .description {
-        margin-top: 10px;
-        color: #666;
-        font-size: 14px;
-      }
-    </style>
-  </head>
-  <body>
-    <h1>Status Indicators Demo</h1>
-    <p>
-      This demo shows the new status indicators without the green connection
-      dot.
-    </p>
-
-    <div class="demo-container">
-      <div class="status-container">
-        <div class="label">Connected States:</div>
-
-        <div class="status-row">
-          <div class="status-item">IDLE:</div>
-          <div class="status-view">
-            <sketch-call-status
-              .isDisconnected="false"
-              .isIdle="true"
-              .llmCalls="0"
-              .toolCalls="[]"
-            ></sketch-call-status>
-          </div>
-          <div class="description">
-            Agent is connected but not actively working
-          </div>
-        </div>
-
-        <div class="status-row">
-          <div class="status-item">WORKING:</div>
-          <div class="status-view">
-            <sketch-call-status
-              .isDisconnected="false"
-              .isIdle="false"
-              .llmCalls="1"
-              .toolCalls='["bash"]'
-            ></sketch-call-status>
-          </div>
-          <div class="description">Agent is connected and actively working</div>
-        </div>
-      </div>
-
-      <div class="status-container">
-        <div class="label">Disconnected State:</div>
-
-        <div class="status-row">
-          <div class="status-item">DISCONNECTED:</div>
-          <div class="status-view">
-            <sketch-call-status
-              .isDisconnected="true"
-              .isIdle="true"
-              .llmCalls="0"
-              .toolCalls="[]"
-            ></sketch-call-status>
-          </div>
-          <div class="description">Connection lost to the agent</div>
-        </div>
-      </div>
-    </div>
-  </body>
-</html>
diff --git a/webui/src/web-components/demo/status-indicators.demo.ts b/webui/src/web-components/demo/status-indicators.demo.ts
new file mode 100644
index 0000000..2b4ef11
--- /dev/null
+++ b/webui/src/web-components/demo/status-indicators.demo.ts
@@ -0,0 +1,169 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { DemoModule } from "./demo-framework/types";
+import { demoUtils } from "./demo-fixtures/index";
+
+const demo: DemoModule = {
+  title: "Status Indicators Demo",
+  description:
+    "Status indicators showing connected, working, and disconnected states without the green connection dot",
+  imports: ["../sketch-call-status.ts", "../sketch-network-status.ts"],
+
+  customStyles: `
+    .demo-container {
+      display: flex;
+      flex-direction: column;
+      gap: 20px;
+    }
+    .status-container {
+      padding: 20px;
+      border: 1px solid #ccc;
+      border-radius: 5px;
+      background-color: #f9f9f9;
+    }
+    .label {
+      font-weight: bold;
+      margin-bottom: 10px;
+      font-size: 16px;
+    }
+    .status-row {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 10px 0;
+      border-bottom: 1px solid #eee;
+    }
+    .status-item {
+      min-width: 200px;
+    }
+    .status-view {
+      background-color: white;
+      border: 1px solid #ddd;
+      padding: 10px;
+      border-radius: 4px;
+    }
+    .description {
+      margin-top: 10px;
+      color: #666;
+      font-size: 14px;
+    }
+  `,
+
+  setup: async (container: HTMLElement) => {
+    const section = demoUtils.createDemoSection(
+      "Status Indicators",
+      "Demonstrates the new status indicators without the green connection dot, showing different connection and activity states.",
+    );
+
+    const demoContainer = document.createElement("div");
+    demoContainer.className = "demo-container";
+
+    // Connected States Container
+    const connectedContainer = document.createElement("div");
+    connectedContainer.className = "status-container";
+
+    const connectedLabel = document.createElement("div");
+    connectedLabel.className = "label";
+    connectedLabel.textContent = "Connected States:";
+    connectedContainer.appendChild(connectedLabel);
+
+    // IDLE State
+    const idleRow = document.createElement("div");
+    idleRow.className = "status-row";
+
+    const idleItem = document.createElement("div");
+    idleItem.className = "status-item";
+    idleItem.textContent = "IDLE:";
+
+    const idleView = document.createElement("div");
+    idleView.className = "status-view";
+
+    const idleStatus = document.createElement("sketch-call-status") as any;
+    idleStatus.isDisconnected = false;
+    idleStatus.isIdle = true;
+    idleStatus.llmCalls = 0;
+    idleStatus.toolCalls = [];
+
+    const idleDescription = document.createElement("div");
+    idleDescription.className = "description";
+    idleDescription.textContent = "Agent is connected but not actively working";
+
+    idleView.appendChild(idleStatus);
+    idleRow.appendChild(idleItem);
+    idleRow.appendChild(idleView);
+    idleRow.appendChild(idleDescription);
+    connectedContainer.appendChild(idleRow);
+
+    // WORKING State
+    const workingRow = document.createElement("div");
+    workingRow.className = "status-row";
+
+    const workingItem = document.createElement("div");
+    workingItem.className = "status-item";
+    workingItem.textContent = "WORKING:";
+
+    const workingView = document.createElement("div");
+    workingView.className = "status-view";
+
+    const workingStatus = document.createElement("sketch-call-status") as any;
+    workingStatus.isDisconnected = false;
+    workingStatus.isIdle = false;
+    workingStatus.llmCalls = 1;
+    workingStatus.toolCalls = ["bash"];
+
+    const workingDescription = document.createElement("div");
+    workingDescription.className = "description";
+    workingDescription.textContent = "Agent is connected and actively working";
+
+    workingView.appendChild(workingStatus);
+    workingRow.appendChild(workingItem);
+    workingRow.appendChild(workingView);
+    workingRow.appendChild(workingDescription);
+    connectedContainer.appendChild(workingRow);
+
+    // Disconnected States Container
+    const disconnectedContainer = document.createElement("div");
+    disconnectedContainer.className = "status-container";
+
+    const disconnectedLabel = document.createElement("div");
+    disconnectedLabel.className = "label";
+    disconnectedLabel.textContent = "Disconnected State:";
+    disconnectedContainer.appendChild(disconnectedLabel);
+
+    // DISCONNECTED State
+    const disconnectedRow = document.createElement("div");
+    disconnectedRow.className = "status-row";
+
+    const disconnectedItem = document.createElement("div");
+    disconnectedItem.className = "status-item";
+    disconnectedItem.textContent = "DISCONNECTED:";
+
+    const disconnectedView = document.createElement("div");
+    disconnectedView.className = "status-view";
+
+    const disconnectedStatus = document.createElement(
+      "sketch-call-status",
+    ) as any;
+    disconnectedStatus.isDisconnected = true;
+    disconnectedStatus.isIdle = true;
+    disconnectedStatus.llmCalls = 0;
+    disconnectedStatus.toolCalls = [];
+
+    const disconnectedDescription = document.createElement("div");
+    disconnectedDescription.className = "description";
+    disconnectedDescription.textContent = "Connection lost to the agent";
+
+    disconnectedView.appendChild(disconnectedStatus);
+    disconnectedRow.appendChild(disconnectedItem);
+    disconnectedRow.appendChild(disconnectedView);
+    disconnectedRow.appendChild(disconnectedDescription);
+    disconnectedContainer.appendChild(disconnectedRow);
+
+    // Assemble the demo
+    demoContainer.appendChild(connectedContainer);
+    demoContainer.appendChild(disconnectedContainer);
+    section.appendChild(demoContainer);
+    container.appendChild(section);
+  },
+};
+
+export default demo;